@useparallel/agent-identification 0.8.2 → 0.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 CHANGED
@@ -29,37 +29,29 @@ current browser.
29
29
 
30
30
  ## Report agents from your server
31
31
 
32
- Install the package and add these calls where your server handles requests. They
33
- skip ordinary requests without a network call, and none of them throws or slows the
34
- response. Keep the site's secret in `AGENT_ID_SITE_SECRET` on the server only.
32
+ Install the package and add one call where your server handles requests. It skips
33
+ ordinary requests without a network call, and never throws or slows the response. Keep
34
+ the site's secret in `AGENT_ID_SITE_SECRET` on the server only.
35
35
 
36
36
  ```ts
37
- import {
38
- reportLink,
39
- reportRequestSignals,
40
- reportSignedRequest,
41
- serverTiming,
42
- } from "@useparallel/agent-identification/server";
37
+ import { agentIdentification } from "@useparallel/agent-identification/server";
43
38
 
44
- const agentId = {
45
- apiUrl: "https://YOUR-COLLECTOR",
46
- siteId: "your-public-site-id",
47
- secret: process.env.AGENT_ID_SITE_SECRET!,
48
- };
39
+ // Express. Reads your server secret from AGENT_ID_SITE_SECRET; the hosted collector is the
40
+ // default. Behind a proxy, set "trust proxy" so req.ip is the visitor's address.
41
+ app.use(agentIdentification({ siteId: "your-public-site-id" }).express());
49
42
 
50
- // Express:
51
- app.use((req, res, next) => {
52
- // Only for a request it reports, so the page script can join the reports to its visit.
53
- const link = reportLink(req);
54
- if (link) res.append("Server-Timing", serverTiming(link));
55
- void reportSignedRequest(req, { ...agentId, link });
56
- // req.ip lets the collector check a named agent against its operator's published addresses.
57
- // Behind a proxy or load balancer, set Express's "trust proxy" so req.ip is the visitor's.
58
- void reportRequestSignals(req, { ...agentId, link, clientIp: req.ip });
59
- next();
60
- });
43
+ // Next.js proxy.ts:
44
+ // const agentId = agentIdentification({ siteId: "your-public-site-id" });
45
+ // export function proxy(request, event) {
46
+ // return agentId.next(request, event, NextResponse.next());
47
+ // }
61
48
  ```
62
49
 
50
+ It reports signed requests, agents that name themselves (including Claude Code and Codex),
51
+ and command-line clients such as curl, each with the page requested, and checks Anthropic's
52
+ and OpenAI's agents against their published addresses. Report actions with
53
+ `agentId.action({ action, outcome, sessionBinding })`.
54
+
63
55
  The page script reads the link from its own page's `Server-Timing` header and claims
64
56
  it, so a signed request joins that visit. Only that browser has the link, and no
65
57
  cookie is set. Keep pages that carry a link out of shared caches.
@@ -75,7 +67,7 @@ Add one tag to every page. It draws nothing, sets no cookies and needs no secret
75
67
  ```html
76
68
  <script
77
69
  defer
78
- src="https://YOUR-COLLECTOR/sdk/v0.8.2/agent-id-track.js"
70
+ src="https://YOUR-COLLECTOR/sdk/v0.9.0/agent-id-track.js"
79
71
  data-site="YOUR-SITE-ID"
80
72
  ></script>
81
73
  ```
@@ -101,7 +93,7 @@ on servers.
101
93
  <div id="visitor-choice"></div>
102
94
  <script
103
95
  defer
104
- src="https://YOUR-COLLECTOR/sdk/v0.8.2/agent-id.js"
96
+ src="https://YOUR-COLLECTOR/sdk/v0.9.0/agent-id.js"
105
97
  data-site="YOUR-SITE-ID"
106
98
  data-target="#visitor-choice"
107
99
  data-mode="optional"
@@ -122,7 +114,7 @@ To require a choice before using a section of the page:
122
114
  <form id="checkout"><!-- Your existing form --></form>
123
115
  <script
124
116
  defer
125
- src="https://YOUR-COLLECTOR/sdk/v0.8.2/agent-id.js"
117
+ src="https://YOUR-COLLECTOR/sdk/v0.9.0/agent-id.js"
126
118
  data-site="YOUR-SITE-ID"
127
119
  data-target="#visitor-choice"
128
120
  data-mode="required"
@@ -256,7 +248,7 @@ For a restrictive Content Security Policy, allow the collector in `script-src`,
256
248
  `style-src` and `connect-src`. Use external integration scripts or your existing
257
249
  nonce policy. The widget uses an external stylesheet and requires no inline
258
250
  script/style exceptions. Its `styleUrl` can point to your own copy of the CSS.
259
- Versioned asset hashes are at `/sdk/v0.8.2/integrity.json`.
251
+ Versioned asset hashes are at `/sdk/v0.9.0/integrity.json`.
260
252
 
261
253
  ## What's collected
262
254
 
@@ -1 +1 @@
1
- "use strict";(()=>{var B=512;var M=[/^\d+$/,/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,/^[0-9a-f]{12,}$/i,/@/,/^(?=(?:[^\d]*\d){3})(?=.*[a-z])[a-z0-9_-]{8,}$/i];function I(e){let t=e.replace(/[?#][\s\S]*$/,"").split("/").map(n=>{let i=n;try{i=decodeURIComponent(n)}catch{}return M.some(d=>d.test(i))?":id":n}).join("/");return(t.startsWith("/")?t:`/${t}`).slice(0,B)}function y(e){return typeof e=="string"&&/^\/[A-Za-z0-9_.~:/{}*-]{0,255}$/.test(e)}var T="agent-id";function b(e){return typeof e=="string"&&/^[A-Za-z0-9_-]{22}$/.test(e)}var f=new Map,z=15e3,p=class extends Error{constructor(t){super(`Identification service returned HTTP ${t}.`);this.status=t;this.name="HttpError"}status};function C(e,r){let t;try{t=new URL(e,window.location.href)}catch{throw new Error(`${r} must be a valid URL.`)}let n=["localhost","127.0.0.1","[::1]"].includes(t.hostname);if(t.protocol!=="https:"&&!(t.protocol==="http:"&&n)||t.username||t.password||t.search||t.hash)throw new Error(`${r} must use HTTPS (or localhost HTTP), without credentials, query or fragment.`);return t.href.replace(/\/$/,"")}function P(e){if(!/^[a-zA-Z0-9_-]{1,100}$/.test(e))throw new Error("siteId must contain 1\u2013100 letters, numbers, underscores or hyphens.");return e}function O(){if(window.location.origin==="null")throw new Error("Serve this website over HTTPS (or localhost HTTP) to use identification.");return window.location.origin}function g(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function S(e){return typeof e=="string"&&/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(e)}function m(e){return typeof e=="string"&&/^[A-Za-z0-9_-]{32,512}$/.test(e)}function h(e){return typeof e=="string"&&/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(e)&&Number.isFinite(Date.parse(e))&&Date.parse(e)>Date.now()}function _(e){return!g(e)||!m(e.token)||!S(e.sessionId)||!h(e.expiresAt)?null:{token:e.token,sessionId:e.sessionId,expiresAt:e.expiresAt}}async function U(e,r){let t=JSON.stringify([e.apiUrl,e.siteId,e.origin,r??null]),n=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t));return`parallel-agent-id:v1:${Array.from(new Uint8Array(n),i=>i.toString(16).padStart(2,"0")).join("")}`}function v(e,r){let t=f.get(e)??null;if(r==="session")try{let n=sessionStorage.getItem(e);t=n?_(JSON.parse(n)):t,n&&!t&&sessionStorage.removeItem(e)}catch{}return _(t)}function E(e,r,t){for(let[n,i]of f)h(i.expiresAt)||f.delete(n);if(t?f.set(e,{...t}):f.delete(e),r==="session")try{t?sessionStorage.setItem(e,JSON.stringify(t)):sessionStorage.removeItem(e)}catch{}}function R(e){return e instanceof Error?e:new Error("Identification request failed.")}async function x(e,r,t,n,i,d){let s=new AbortController,o=()=>s.abort();r.addEventListener("abort",o,{once:!0}),r.aborted&&s.abort();let a=setTimeout(()=>s.abort(),z);try{let c=await fetch(`${e.apiUrl}${t}`,{method:n,headers:{...i===void 0?{}:{"Content-Type":"application/json"},...d?{Authorization:`Bearer ${d}`,"X-Agent-Site":e.siteId}:{}},...i===void 0?{}:{body:JSON.stringify(i)},credentials:"omit",cache:"no-store",mode:"cors",referrerPolicy:"no-referrer",signal:s.signal});if(!c.ok)throw new p(c.status);if(n==="DELETE"){if(c.status!==204)throw new Error("Session revocation was not confirmed.");return null}if(!c.headers.get("content-type")?.toLowerCase().includes("application/json"))throw new Error("Identification service did not return JSON.");return await c.json()}catch(c){throw s.signal.aborted&&!r.aborted?new Error("Identification service timed out. Please retry.",{cause:c}):c}finally{clearTimeout(a),r.removeEventListener("abort",o)}}function $(e,r){return g(e)&&Array.isArray(e.results)&&e.results.some(t=>g(t)&&t.id===r&&(t.status==="recorded"||t.status==="duplicate"))}function N(){let e=[];return navigator.webdriver===!0&&e.push({signal:"webdriver",detail:"navigator.webdriver"}),/HeadlessChrome\//.test(navigator.userAgent)&&e.push({signal:"headless_browser",detail:"HeadlessChrome"}),e}var w=new Map;function j(){try{let[e]=performance.getEntriesByType("navigation"),r=(e?.serverTiming??[]).filter(t=>t.name===T&&b(t.description)).map(t=>t.description);return[...new Set(r)].slice(0,5)}catch{return[]}}var L=new Set,H=new WeakMap;function G(e){let{history:r}=window,t=H.get(r);if(!t){let n=()=>{for(let i of[...t])i()};t=new Set,H.set(r,t);for(let i of["pushState","replaceState"]){let d=r[i];r[i]=function(...s){let o=d.apply(this,s);return n(),o}}window.addEventListener("popstate",n)}return t.add(e),()=>t.delete(e)}function V(e){let r=e.storage??"session";if(r!=="session"&&r!=="memory")throw new Error("Invalid storage option.");if(e.route!==void 0&&!y(e.route))throw new Error("route must be a path pattern such as /tickets/:id.");return{apiUrl:C(e.apiUrl,"apiUrl"),siteId:P(e.siteId),origin:O(),storage:r,navigation:e.navigation??!0,automationSignals:e.automationSignals??!0,onError:e.onError}}function Z(e,r){return Object.keys(e).every(t=>t==="onError"&&r[t]===void 0||e[t]===r[t])}var k=class{constructor(r,t){this.config=r;this.key=U(r,void 0),this.stopWatching=r.navigation?G(()=>this.navigated()):()=>{},this.ready=this.pageView({route:t}),this.ready.then(()=>this.claimLinks()).catch(()=>{})}config;ready;controller=new AbortController;key;stopWatching;session=null;starting=null;destroyed=!1;path=window.location.pathname;async claimLinks(){let r=j();r.length&&await this.deliver("/v1/links",{links:r},r[0],"links",t=>r.every(n=>g(t)&&Array.isArray(t.results)&&t.results.some(i=>g(i)&&i.id===n)))}navigated(){this.destroyed||window.location.pathname===this.path||this.pageView().catch(()=>{})}async pageView(r={}){if(this.destroyed)throw new Error("This tracker was destroyed.");if(r.route!==void 0&&!y(r.route))throw new Error("route must be a path pattern such as /tickets/:id.");this.path=window.location.pathname;let t={id:crypto.randomUUID(),path:I(this.path),...r.route?{route:r.route}:{},occurredAt:new Date().toISOString()},n=await this.deliver("/v1/page-views",{views:[t]},t.id,"page view");this.config.automationSignals&&await this.firstSignals(n.sessionId)&&this.reportSignals().catch(()=>{})}async firstSignals(r){let t=`${await this.key}:signals`;if(L.has(r))return!1;if(L.add(r),this.config.storage==="memory")return!0;try{if(sessionStorage.getItem(t)===r)return!1;sessionStorage.setItem(t,r)}catch{}return!0}async reportSignals(){for(let r of N()){let t={id:crypto.randomUUID(),...r,occurredAt:new Date().toISOString()};await this.deliver("/v1/browser-signals",{signals:[t]},t.id,"signal")}}async current(){let r=await this.key;return this.session??=v(r,this.config.storage),this.session&&h(this.session.expiresAt)?this.session:(this.starting??=this.start(r).finally(()=>this.starting=null),this.starting)}async start(r){let t=await x(this.config,this.controller.signal,"/v1/sessions","POST",{siteId:this.config.siteId});if(!g(t)||!S(t.sessionId)||t.siteId!==this.config.siteId||t.origin!==this.config.origin||!h(t.expiresAt)||!m(t.sessionToken))throw new Error("Identification service returned an invalid new session.");return this.session={token:t.sessionToken,sessionId:t.sessionId,expiresAt:t.expiresAt},E(r,this.config.storage,this.session),this.session}async deliver(r,t,n,i,d=s=>$(s,n)){for(let s=0;;s++){let o=null;try{o=await this.current();let a=await x(this.config,this.controller.signal,r,"POST",t,o.token);if(!d(a))throw new Error(`Identification service did not confirm the ${i}.`);return o}catch(a){if(this.destroyed)throw a;let c=o!==null&&a instanceof p&&[401,404,410].includes(a.status);if(c&&o&&this.session===o){this.session=null;let l=await this.key;v(l,this.config.storage)?.token===o.token&&E(l,this.config.storage,null)}if(!(c||!(a instanceof p)||a.status===429||a.status>=500)||s>=2){let l=R(a);try{this.config.onError?.(l)}catch{}throw l}if(c||await new Promise(l=>setTimeout(l,300*3**s)),this.destroyed)throw a}}}destroy(){this.destroyed||(this.destroyed=!0,this.stopWatching(),this.controller.abort(),w.get(this.config.apiUrl+" "+this.config.siteId)===this&&w.delete(this.config.apiUrl+" "+this.config.siteId))}};function A(e){let r=V(e),t=r.apiUrl+" "+r.siteId,n=w.get(t);if(n){if(!Z(n.config,r))throw new Error("This site is already tracked on this page with different options.");return n}let i=new k(r,e.route);return w.set(t,i),i}window.AgentIdentificationTracker=Object.freeze({track:A});var u=document.currentScript;function D(e){document.dispatchEvent(new CustomEvent("agent-identification:error",{detail:{message:e}}))}if(u?.dataset.site)try{A({siteId:u.dataset.site,apiUrl:u.dataset.api??new URL(u.src).origin,storage:u.dataset.storage,route:u.dataset.route,navigation:u.dataset.navigation!=="off",automationSignals:u.dataset.automationSignals!=="off",onError:e=>D(e.message)})}catch(e){D(e instanceof Error?e.message:"Agent Identification could not start.")}})();
1
+ "use strict";(()=>{var M=512;var j=[/^\d+$/,/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,/^[0-9a-f]{12,}$/i,/@/,/^(?=(?:[^\d]*\d){3})(?=.*[a-z])[a-z0-9_-]{8,}$/i];function T(t){let e=t.replace(/[?#][\s\S]*$/,"").split("/").map(r=>{let i=r;try{i=decodeURIComponent(r)}catch{}return j.some(d=>d.test(i))?":id":r}).join("/");return(e.startsWith("/")?e:`/${e}`).slice(0,M)}function m(t){return typeof t=="string"&&/^\/[A-Za-z0-9_.~:/{}*-]{0,255}$/.test(t)}var A="agent-id";function C(t){return typeof t=="string"&&/^[A-Za-z0-9_-]{22}$/.test(t)}var f=new Map,z=15e3,p=class extends Error{constructor(e){super(`Identification service returned HTTP ${e}.`);this.status=e;this.name="HttpError"}status};function P(t,n){let e;try{e=new URL(t,window.location.href)}catch{throw new Error(`${n} must be a valid URL.`)}let r=["localhost","127.0.0.1","[::1]"].includes(e.hostname);if(e.protocol!=="https:"&&!(e.protocol==="http:"&&r)||e.username||e.password||e.search||e.hash)throw new Error(`${n} must use HTTPS (or localhost HTTP), without credentials, query or fragment.`);return e.href.replace(/\/$/,"")}function O(t){if(!/^[a-zA-Z0-9_-]{1,100}$/.test(t))throw new Error("siteId must contain 1\u2013100 letters, numbers, underscores or hyphens.");return t}function U(){if(window.location.origin==="null")throw new Error("Serve this website over HTTPS (or localhost HTTP) to use identification.");return window.location.origin}function g(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function S(t){return typeof t=="string"&&/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(t)}function v(t){return typeof t=="string"&&/^[A-Za-z0-9_-]{32,512}$/.test(t)}function h(t){return typeof t=="string"&&/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(t)&&Number.isFinite(Date.parse(t))&&Date.parse(t)>Date.now()}function _(t){return!g(t)||!v(t.token)||!S(t.sessionId)||!h(t.expiresAt)?null:{token:t.token,sessionId:t.sessionId,expiresAt:t.expiresAt}}async function L(t,n){let e=JSON.stringify([t.apiUrl,t.siteId,t.origin,n??null]),r=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(e));return`parallel-agent-id:v1:${Array.from(new Uint8Array(r),i=>i.toString(16).padStart(2,"0")).join("")}`}function x(t,n){let e=f.get(t)??null;if(n==="session")try{let r=sessionStorage.getItem(t);e=r?_(JSON.parse(r)):e,r&&!e&&sessionStorage.removeItem(t)}catch{}return _(e)}function E(t,n,e){for(let[r,i]of f)h(i.expiresAt)||f.delete(r);if(e?f.set(t,{...e}):f.delete(t),n==="session")try{e?sessionStorage.setItem(t,JSON.stringify(e)):sessionStorage.removeItem(t)}catch{}}function R(t){return t instanceof Error?t:new Error("Identification request failed.")}async function k(t,n,e,r,i,d){let o=new AbortController,s=()=>o.abort();n.addEventListener("abort",s,{once:!0}),n.aborted&&o.abort();let a=setTimeout(()=>o.abort(),z);try{let c=await fetch(`${t.apiUrl}${e}`,{method:r,headers:{...i===void 0?{}:{"Content-Type":"application/json"},...d?{Authorization:`Bearer ${d}`,"X-Agent-Site":t.siteId}:{}},...i===void 0?{}:{body:JSON.stringify(i)},credentials:"omit",cache:"no-store",mode:"cors",referrerPolicy:"no-referrer",signal:o.signal});if(!c.ok)throw new p(c.status);if(r==="DELETE"){if(c.status!==204)throw new Error("Session revocation was not confirmed.");return null}if(!c.headers.get("content-type")?.toLowerCase().includes("application/json"))throw new Error("Identification service did not return JSON.");return await c.json()}catch(c){throw o.signal.aborted&&!n.aborted?new Error("Identification service timed out. Please retry.",{cause:c}):c}finally{clearTimeout(a),n.removeEventListener("abort",s)}}function N(t,n){return g(t)&&Array.isArray(t.results)&&t.results.some(e=>g(e)&&e.id===n&&(e.status==="recorded"||e.status==="duplicate"))}function $(){let t=[];return navigator.webdriver===!0&&t.push({signal:"webdriver",detail:"navigator.webdriver"}),/HeadlessChrome\//.test(navigator.userAgent)&&t.push({signal:"headless_browser",detail:"HeadlessChrome"}),t}var w=new Map;function W(){try{let[t]=performance.getEntriesByType("navigation"),n=(t?.serverTiming??[]).filter(e=>e.name===A&&C(e.description)).map(e=>e.description);return[...new Set(n)].slice(0,5)}catch{return[]}}var H=new Set,D=new Set,B=new WeakMap;function K(t){let{history:n}=window,e=B.get(n);if(!e){let r=()=>{for(let i of[...e])i()};e=new Set,B.set(n,e);for(let i of["pushState","replaceState"]){let d=n[i];n[i]=function(...o){let s=d.apply(this,o);return r(),s}}window.addEventListener("popstate",r)}return e.add(t),()=>e.delete(t)}function V(t){if(!(t==null||t==="")){if(typeof t!="string"||t.length>4096)throw new Error("bindingContext must be the signed context your server created.");return t}}function q(t){let n=0,e=!1,r=()=>performance.now()-n<=1e3,i=()=>{n=performance.now()},d=()=>{e=r()},o=c=>{if(!c.isTrusted||r())return;let y=c.inputType;(y==="insertText"||y==="insertCompositionText"&&!e)&&(a(),t("typing"))},s={capture:!0,passive:!0};window.addEventListener("keydown",i,s),window.addEventListener("compositionstart",d,s),window.addEventListener("input",o,s);function a(){window.removeEventListener("keydown",i,s),window.removeEventListener("compositionstart",d,s),window.removeEventListener("input",o,s)}return a}function Z(t){let n=t.storage??"session";if(n!=="session"&&n!=="memory")throw new Error("Invalid storage option.");if(t.route!==void 0&&!m(t.route))throw new Error("route must be a path pattern such as /tickets/:id.");return{apiUrl:P(t.apiUrl,"apiUrl"),siteId:O(t.siteId),origin:U(),storage:n,navigation:t.navigation??!0,automationSignals:t.automationSignals??!0,bindingContext:V(t.bindingContext),onError:t.onError}}function J(t,n){return Object.keys(t).every(e=>e==="onError"&&n[e]===void 0||t[e]===n[e])}var b=class{constructor(n,e){this.config=n;this.key=L(n,n.bindingContext),this.stopInput=n.automationSignals?q(r=>{this.reportInput(r).catch(()=>{})}):()=>{},this.stopWatching=n.navigation?K(()=>this.navigated()):()=>{},this.ready=this.pageView({route:e}),this.ready.then(()=>this.claimLinks()).catch(()=>{})}config;ready;controller=new AbortController;key;stopWatching;stopInput;session=null;starting=null;destroyed=!1;path=window.location.pathname;async claimLinks(){let n=W();n.length&&await this.deliver("/v1/links",{links:n},n[0],"links",e=>n.every(r=>g(e)&&Array.isArray(e.results)&&e.results.some(i=>g(i)&&i.id===r)))}navigated(){this.destroyed||window.location.pathname===this.path||this.pageView().catch(()=>{})}async pageView(n={}){if(this.destroyed)throw new Error("This tracker was destroyed.");if(n.route!==void 0&&!m(n.route))throw new Error("route must be a path pattern such as /tickets/:id.");this.path=window.location.pathname;let e={id:crypto.randomUUID(),path:T(this.path),...n.route?{route:n.route}:{},occurredAt:new Date().toISOString()},r=await this.deliver("/v1/page-views",{views:[e]},e.id,"page view");this.config.automationSignals&&await this.firstSignals(r.sessionId)&&this.reportSignals().catch(()=>{})}async firstSignals(n){let e=`${await this.key}:signals`;if(H.has(n))return!1;if(H.add(n),this.config.storage==="memory")return!0;try{if(sessionStorage.getItem(e)===n)return!1;sessionStorage.setItem(e,n)}catch{}return!0}async reportInput(n){let e=await this.current(),r=`${await this.key}:input`;if(D.has(e.sessionId))return;D.add(e.sessionId);try{if(this.config.storage==="session"){if(sessionStorage.getItem(r)===e.sessionId)return;sessionStorage.setItem(r,e.sessionId)}}catch{}let i={id:crypto.randomUUID(),signal:"synthetic_input",detail:n,occurredAt:new Date().toISOString()};await this.deliver("/v1/browser-signals",{signals:[i]},i.id,"signal")}async reportSignals(){for(let n of $()){let e={id:crypto.randomUUID(),...n,occurredAt:new Date().toISOString()};await this.deliver("/v1/browser-signals",{signals:[e]},e.id,"signal")}}async current(){let n=await this.key;return this.session??=x(n,this.config.storage),this.session&&h(this.session.expiresAt)?this.session:(this.starting??=this.start(n).finally(()=>this.starting=null),this.starting)}async start(n){let e=await k(this.config,this.controller.signal,"/v1/sessions","POST",{siteId:this.config.siteId,...this.config.bindingContext?{bindingContext:this.config.bindingContext}:{}});if(!g(e)||!S(e.sessionId)||e.siteId!==this.config.siteId||e.origin!==this.config.origin||!h(e.expiresAt)||!v(e.sessionToken))throw new Error("Identification service returned an invalid new session.");return this.session={token:e.sessionToken,sessionId:e.sessionId,expiresAt:e.expiresAt},E(n,this.config.storage,this.session),this.session}async deliver(n,e,r,i,d=o=>N(o,r)){for(let o=0;;o++){let s=null;try{s=await this.current();let a=await k(this.config,this.controller.signal,n,"POST",e,s.token);if(!d(a))throw new Error(`Identification service did not confirm the ${i}.`);return s}catch(a){if(this.destroyed)throw a;let c=s!==null&&a instanceof p&&[401,404,410].includes(a.status);if(c&&s&&this.session===s){this.session=null;let l=await this.key;x(l,this.config.storage)?.token===s.token&&E(l,this.config.storage,null)}if(!(c||!(a instanceof p)||a.status===429||a.status>=500)||o>=2){let l=R(a);try{this.config.onError?.(l)}catch{}throw l}if(c||await new Promise(l=>setTimeout(l,300*3**o)),this.destroyed)throw a}}}destroy(){this.destroyed||(this.destroyed=!0,this.stopWatching(),this.stopInput(),this.controller.abort(),w.get(this.config.apiUrl+" "+this.config.siteId)===this&&w.delete(this.config.apiUrl+" "+this.config.siteId))}};function I(t){let n=Z(t),e=n.apiUrl+" "+n.siteId,r=w.get(e);if(r){if(!J(r.config,n))throw new Error("This site is already tracked on this page with different options.");return r}let i=new b(n,t.route);return w.set(e,i),i}window.AgentIdentificationTracker=Object.freeze({track:I});var u=document.currentScript;function G(t){document.dispatchEvent(new CustomEvent("agent-identification:error",{detail:{message:t}}))}if(u?.dataset.site)try{I({siteId:u.dataset.site,apiUrl:u.dataset.api??new URL(u.src).origin,storage:u.dataset.storage,route:u.dataset.route,navigation:u.dataset.navigation!=="off",automationSignals:u.dataset.automationSignals!=="off",...u.dataset.binding?{bindingContext:u.dataset.binding}:{},onError:t=>G(t.message)})}catch(t){G(t instanceof Error?t.message:"Agent Identification could not start.")}})();
package/dist/agent-id.js CHANGED
@@ -1 +1 @@
1
- "use strict";(()=>{var E="0.8.2",W=["ai_assistant","person","prefer_not_to_say"];function D(t){return typeof t=="string"&&W.includes(t)}var V=512;var j=[/^\d+$/,/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,/^[0-9a-f]{12,}$/i,/@/,/^(?=(?:[^\d]*\d){3})(?=.*[a-z])[a-z0-9_-]{8,}$/i];function b(t){let i=t.replace(/[?#][\s\S]*$/,"").split("/").map(n=>{let r=n;try{r=decodeURIComponent(n)}catch{}return j.some(s=>s.test(r))?":id":n}).join("/");return(i.startsWith("/")?i:`/${i}`).slice(0,V)}function m(t){return typeof t=="string"&&/^\/[A-Za-z0-9_.~:/{}*-]{0,255}$/.test(t)}var h=new Map,B=15e3,u=class extends Error{constructor(i){super(`Identification service returned HTTP ${i}.`);this.status=i;this.name="HttpError"}status};function w(t,e){let i;try{i=new URL(t,window.location.href)}catch{throw new Error(`${e} must be a valid URL.`)}let n=["localhost","127.0.0.1","[::1]"].includes(i.hostname);if(i.protocol!=="https:"&&!(i.protocol==="http:"&&n)||i.username||i.password||i.search||i.hash)throw new Error(`${e} must use HTTPS (or localhost HTTP), without credentials, query or fragment.`);return i.href.replace(/\/$/,"")}function x(t){if(!/^[a-zA-Z0-9_-]{1,100}$/.test(t))throw new Error("siteId must contain 1\u2013100 letters, numbers, underscores or hyphens.");return t}function I(){if(window.location.origin==="null")throw new Error("Serve this website over HTTPS (or localhost HTTP) to use identification.");return window.location.origin}function g(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function N(t,e=1,i=512){return typeof t=="string"&&t.length>=e&&t.length<=i&&/^[A-Za-z0-9._~-]+$/.test(t)}function p(t){return typeof t=="string"&&/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(t)}function v(t){return typeof t=="string"&&/^[A-Za-z0-9_-]{32,512}$/.test(t)}function f(t){return typeof t=="string"&&/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(t)&&Number.isFinite(Date.parse(t))&&Date.parse(t)>Date.now()}function H(t){return!g(t)||!v(t.token)||!p(t.sessionId)||!f(t.expiresAt)?null:{token:t.token,sessionId:t.sessionId,expiresAt:t.expiresAt}}async function k(t,e){let i=JSON.stringify([t.apiUrl,t.siteId,t.origin,e??null]),n=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(i));return`parallel-agent-id:v1:${Array.from(new Uint8Array(n),r=>r.toString(16).padStart(2,"0")).join("")}`}function A(t,e){let i=h.get(t)??null;if(e==="session")try{let n=sessionStorage.getItem(t);i=n?H(JSON.parse(n)):i,n&&!i&&sessionStorage.removeItem(t)}catch{}return H(i)}function C(t,e,i){for(let[n,r]of h)f(r.expiresAt)||h.delete(n);if(i?h.set(t,{...i}):h.delete(t),e==="session")try{i?sessionStorage.setItem(t,JSON.stringify(i)):sessionStorage.removeItem(t)}catch{}}function S(t){return t instanceof Error?t:new Error("Identification request failed.")}async function T(t,e,i,n,r,s){let o=new AbortController,a=()=>o.abort();e.addEventListener("abort",a,{once:!0}),e.aborted&&o.abort();let d=setTimeout(()=>o.abort(),B);try{let l=await fetch(`${t.apiUrl}${i}`,{method:n,headers:{...r===void 0?{}:{"Content-Type":"application/json"},...s?{Authorization:`Bearer ${s}`,"X-Agent-Site":t.siteId}:{}},...r===void 0?{}:{body:JSON.stringify(r)},credentials:"omit",cache:"no-store",mode:"cors",referrerPolicy:"no-referrer",signal:o.signal});if(!l.ok)throw new u(l.status);if(n==="DELETE"){if(l.status!==204)throw new Error("Session revocation was not confirmed.");return null}if(!l.headers.get("content-type")?.toLowerCase().includes("application/json"))throw new Error("Identification service did not return JSON.");return await l.json()}catch(l){throw o.signal.aborted&&!e.aborted?new Error("Identification service timed out. Please retry.",{cause:l}):l}finally{clearTimeout(d),e.removeEventListener("abort",a)}}function _(t,e){return g(t)&&Array.isArray(t.results)&&t.results.some(i=>g(i)&&i.id===e&&(i.status==="recorded"||i.status==="duplicate"))}function P(){let t=[];return navigator.webdriver===!0&&t.push({signal:"webdriver",detail:"navigator.webdriver"}),/HeadlessChrome\//.test(navigator.userAgent)&&t.push({signal:"headless_browser",detail:"HeadlessChrome"}),t}var R=new WeakMap,L=new WeakMap,O={ai_assistant:"AI assistant",person:"Person",prefer_not_to_say:"Prefer not to say"};function q(t,e){let i=typeof t=="string"?document.querySelector(t):t;if(!(i instanceof HTMLElement))throw new Error(`${e} must identify an existing HTML element.`);return i}function z(t){let e=q(t.container,"container"),i=t.gate===void 0?void 0:q(t.gate,"gate"),n=w(t.apiUrl,"apiUrl"),r=t.mode??"optional",s=t.storage??"session";if(r!=="optional"&&r!=="required")throw new Error("Invalid identification mode.");if(s!=="session"&&s!=="memory")throw new Error("Invalid storage option.");if(i&&(i===e||i.contains(e)))throw new Error("The gate must not contain the identification widget.");let o=I();if(t.route!==void 0&&!m(t.route))throw new Error("route must be a path pattern such as /tickets/:id.");return{container:e,gate:i,apiUrl:n,siteId:x(t.siteId),origin:o,mode:r,storage:s,styleUrl:w(t.styleUrl??`${n}/sdk/v${E}/agent-id.css`,"styleUrl"),bindingContext:t.bindingContext,onRecorded:t.onRecorded,onError:t.onError,pageViews:t.pageViews??!0,route:t.route,automationSignals:t.automationSignals??!1}}function G(t,e){return Object.keys(t).every(i=>(i==="onRecorded"||i==="onError")&&e[i]===void 0?!0:t[i]===e[i])}function U(t,e,i,n){if(!g(t)||!p(t.sessionId)||i!==void 0&&t.sessionId!==i||t.siteId!==e.siteId||t.origin!==e.origin||!f(t.expiresAt)||t.evidence!=="self_reported")throw new Error("Identification service returned an invalid or mismatched session.");let r=t.choice===null&&t.receipt===null&&t.eventId===null,s=D(t.choice)&&N(t.receipt,1,8192)&&p(t.eventId);if(!r&&!s||n!==void 0&&t.choice!==n)throw new Error("Identification service returned an invalid or mismatched response.");return{sessionId:t.sessionId,siteId:e.siteId,origin:e.origin,expiresAt:t.expiresAt,choice:t.choice,receipt:t.receipt,eventId:t.eventId,evidence:"self_reported"}}var M=class{constructor(e){this.config=e;this.originalInert=e.gate?.inert??!1,this.build(),this.lock(),this.ready=this.start(this.version),this.ready.catch(()=>{})}config;ready;host=document.createElement("div");root=this.host.attachShadow({mode:"open"});status=document.createElement("p");retry=document.createElement("button");buttons=[];originalInert;version=0;controller=new AbortController;destroyed=!1;state=null;session=null;key=null;pending=null;retryAction=null;expiry;viewed=null;signalled=null;build(){this.host.dataset.agentIdentification=E;let e=document.createElement("link");e.rel="stylesheet",e.href=this.config.styleUrl;let i=document.createElement("section");i.className="agent-identification",i.setAttribute("aria-label","Visitor identification");let n=document.createElement("h2");n.textContent=this.config.mode==="required"?"Before continuing":"How are you visiting?";let r=document.createElement("p");r.textContent="Choose one option. Your answer is self-reported.";let s=document.createElement("div");s.className="choices";for(let o of Object.keys(O)){let a=document.createElement("button");a.type="button",a.textContent=O[o],a.disabled=!0,a.addEventListener("click",()=>{this.pending={choice:o,requestId:crypto.randomUUID()},this.submit(this.version).catch(()=>{})}),this.buttons.push(a),s.append(a)}this.status.className="status",this.status.setAttribute("role","status"),this.status.setAttribute("aria-live","polite"),this.status.setAttribute("aria-atomic","true"),this.retry.type="button",this.retry.textContent="Retry",this.retry.hidden=!0,this.retry.addEventListener("click",()=>{let o=this.retryAction;o&&o().catch(()=>{})}),i.append(n,r,s,this.status,this.retry),this.root.append(e,i),this.config.container.append(this.host)}active(e){if(this.destroyed||e!==this.version)throw new DOMException("Widget operation was superseded.","AbortError")}lock(){this.config.mode==="required"&&this.config.gate&&(this.config.gate.inert=!0)}busy(e){this.status.textContent=e,this.retry.hidden=!0,this.retryAction=null;for(let i of this.buttons)i.disabled=!0}fail(e,i,n,r){if(this.destroyed||i!==this.version)return;let s=S(e);this.status.textContent=`${r} ${s.message}`,this.retry.hidden=!1,this.retryAction=n,this.notifyError(s)}notifyError(e){try{this.config.onError?.(e)}catch{}}accepted(e){if(this.state=e,clearTimeout(this.expiry),this.expiry=setTimeout(()=>{this.destroyed||this.state?.sessionId!==e.sessionId||(this.state=null,this.lock(),this.busy("Your identification session expired."),this.retry.hidden=!1,this.retryAction=()=>this.restart())},Math.min(Date.parse(e.expiresAt)-Date.now(),2147483647)),this.retry.hidden=!0,this.retryAction=null,e.choice!==null){this.status.textContent=`Response recorded: ${O[e.choice]}.`,this.config.mode==="required"&&this.config.gate&&(this.config.gate.inert=this.originalInert);try{this.config.onRecorded?.({...e})}catch{this.notifyError(new Error("Your onRecorded callback failed after the response was recorded."))}}else this.status.textContent=this.config.mode==="required"?"Select any option to continue.":"You can choose an option or continue without answering.";for(let i of this.buttons)i.disabled=e.choice!==null;return{...e}}load(){return this.key?A(this.key,this.config.storage):null}persist(e){this.key&&C(this.key,this.config.storage,e)}request(e,i,n,r){return T(this.config,this.controller.signal,e,i,n,r)}async start(e){this.busy("Loading identification\u2026"),this.lock();try{let i=typeof this.config.bindingContext=="function"?await this.config.bindingContext():this.config.bindingContext;if(this.active(e),i!==void 0&&(typeof i!="string"||i.length===0||i.length>8192))throw new Error("bindingContext must be a nonempty signed context.");let n=await k(this.config,i);if(this.active(e),this.key=n,this.session=this.load(),this.session)try{let o=await this.request("/v1/sessions/current","GET",void 0,this.session.token);this.active(e);let a=U(o,this.config,this.session.sessionId);return this.session.expiresAt=a.expiresAt,this.persist(this.session),this.view(this.accepted(a))}catch(o){if(this.active(e),!(o instanceof u)||![401,404,410].includes(o.status))throw o;this.persist(null),this.session=null}let r=await this.request("/v1/sessions","POST",{siteId:this.config.siteId,...i===void 0?{}:{bindingContext:i}});this.active(e);let s=U(r,this.config);if(!g(r)||!v(r.sessionToken)||s.choice!==null)throw new Error("Identification service returned an invalid new session.");return this.session={token:r.sessionToken,sessionId:s.sessionId,expiresAt:s.expiresAt},this.persist(this.session),this.view(this.accepted(s))}catch(i){throw this.fail(i,e,()=>this.restart(),"Could not load identification."),i}}async submit(e){if(!this.pending||!this.session)throw new Error("Identification session is not ready.");let i=this.pending,n=this.session;this.busy("Recording your response\u2026");try{let r=await this.request("/v1/declarations","POST",i,n.token);this.active(e);let s=U(r,this.config,n.sessionId,i.choice);return n.expiresAt=s.expiresAt,this.persist(n),this.pending=null,this.accepted(s)}catch(r){throw this.fail(r,e,()=>this.submit(this.version),"Could not record your response."),r}}view(e){return this.config.pageViews&&this.viewed!==e.sessionId&&(this.viewed=e.sessionId,this.pageView({route:this.config.route}).catch(()=>{})),this.config.automationSignals&&this.signalled!==e.sessionId&&(this.signalled=e.sessionId,this.reportSignals().catch(()=>{})),e}async pageView(e={}){if(this.active(this.version),e.route!==void 0&&!m(e.route))throw new Error("route must be a path pattern such as /tickets/:id.");let i={id:crypto.randomUUID(),path:b(window.location.pathname),...e.route?{route:e.route}:{},occurredAt:new Date().toISOString()};await this.deliver("/v1/page-views",{views:[i]},i.id,"page view")}async deliver(e,i,n,r){let s=this.controller.signal;for(let o=0;;o++){let a=this.session;try{if(!a)throw new Error("Identification session is not ready.");let d=await this.request(e,"POST",i,a.token);if(!_(d,n))throw new Error(`Identification service did not confirm the ${r}.`);return}catch(d){if(s.aborted||this.destroyed)throw d;if(!(a!==null&&(!(d instanceof u)||d.status===429||d.status>=500))||o>=2){let y=S(d);throw this.notifyError(y),y}if(await new Promise(y=>setTimeout(y,300*3**o)),s.aborted||this.destroyed)throw d}}}async reportSignals(){for(let e of P()){let i={id:crypto.randomUUID(),...e,occurredAt:new Date().toISOString()};await this.deliver("/v1/browser-signals",{signals:[i]},i.id,"signal")}}restart(){return this.active(this.version),this.controller.abort(),clearTimeout(this.expiry),this.controller=new AbortController,this.version+=1,this.start(this.version)}getState(){return this.state?{...this.state}:null}async reset(){this.active(this.version),this.controller.abort(),clearTimeout(this.expiry),this.controller=new AbortController;let e=++this.version;this.busy("Resetting identification\u2026"),this.state=null,this.pending=null,this.lock();try{this.session&&(await this.request("/v1/sessions/current","DELETE",void 0,this.session.token),this.active(e)),this.persist(null),this.session=null}catch(i){throw this.fail(i,e,()=>this.reset(),"Could not reset identification."),i}return this.start(e)}destroy(){this.destroyed||(this.destroyed=!0,this.version+=1,this.controller.abort(),clearTimeout(this.expiry),this.host.remove(),this.config.mode==="required"&&this.config.gate&&(this.config.gate.inert=this.originalInert,L.delete(this.config.gate)),R.delete(this.config.container),this.retryAction=null)}};function $(t){let e=z(t),i=R.get(e.container);if(i){if(!G(i.config,e))throw new Error("This container already has an identification widget with different options.");return i}if(e.mode==="required"&&e.gate&&L.has(e.gate))throw new Error("This gate is already controlled by another identification widget.");let n=new M(e);return R.set(e.container,n),e.mode==="required"&&e.gate&&L.set(e.gate,n),n}window.AgentIdentification=Object.freeze({init:$});var c=document.currentScript;if(c?.dataset.site){let t=()=>{try{let e;c.dataset.target?e=c.dataset.target:(e=document.createElement("div"),e.dataset.agentIdentificationContainer="",document.body.append(e));let i={container:e,siteId:c.dataset.site,apiUrl:c.dataset.api??new URL(c.src).origin,mode:c.dataset.mode,gate:c.dataset.gate,storage:c.dataset.storage,styleUrl:c.dataset.style,pageViews:c.dataset.pageViews!=="off",route:c.dataset.route,automationSignals:c.dataset.automationSignals==="on",onRecorded:n=>{(typeof e=="string"?document.querySelector(e):e).dispatchEvent(new CustomEvent("agent-identification:recorded",{detail:n,bubbles:!0}))},onError:n=>{document.dispatchEvent(new CustomEvent("agent-identification:error",{detail:{message:n.message}}))}};$(i)}catch(e){let i=e instanceof Error?e.message:"Identification widget could not initialize.",n=document.createElement("p");n.setAttribute("role","alert"),n.textContent=`Visitor identification unavailable. ${i}`,c.insertAdjacentElement("afterend",n),document.dispatchEvent(new CustomEvent("agent-identification:error",{detail:{message:i}}))}};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t,{once:!0}):t()}})();
1
+ "use strict";(()=>{var E="0.9.0",W=["ai_assistant","person","prefer_not_to_say"];function $(t){return typeof t=="string"&&W.includes(t)}var V=512;var B=[/^\d+$/,/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,/^[0-9a-f]{12,}$/i,/@/,/^(?=(?:[^\d]*\d){3})(?=.*[a-z])[a-z0-9_-]{8,}$/i];function b(t){let i=t.replace(/[?#][\s\S]*$/,"").split("/").map(n=>{let r=n;try{r=decodeURIComponent(n)}catch{}return B.some(s=>s.test(r))?":id":n}).join("/");return(i.startsWith("/")?i:`/${i}`).slice(0,V)}function m(t){return typeof t=="string"&&/^\/[A-Za-z0-9_.~:/{}*-]{0,255}$/.test(t)}var h=new Map,j=15e3,l=class extends Error{constructor(i){super(`Identification service returned HTTP ${i}.`);this.status=i;this.name="HttpError"}status};function w(t,e){let i;try{i=new URL(t,window.location.href)}catch{throw new Error(`${e} must be a valid URL.`)}let n=["localhost","127.0.0.1","[::1]"].includes(i.hostname);if(i.protocol!=="https:"&&!(i.protocol==="http:"&&n)||i.username||i.password||i.search||i.hash)throw new Error(`${e} must use HTTPS (or localhost HTTP), without credentials, query or fragment.`);return i.href.replace(/\/$/,"")}function x(t){if(!/^[a-zA-Z0-9_-]{1,100}$/.test(t))throw new Error("siteId must contain 1\u2013100 letters, numbers, underscores or hyphens.");return t}function I(){if(window.location.origin==="null")throw new Error("Serve this website over HTTPS (or localhost HTTP) to use identification.");return window.location.origin}function g(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function N(t,e=1,i=512){return typeof t=="string"&&t.length>=e&&t.length<=i&&/^[A-Za-z0-9._~-]+$/.test(t)}function p(t){return typeof t=="string"&&/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(t)}function v(t){return typeof t=="string"&&/^[A-Za-z0-9_-]{32,512}$/.test(t)}function f(t){return typeof t=="string"&&/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(t)&&Number.isFinite(Date.parse(t))&&Date.parse(t)>Date.now()}function M(t){return!g(t)||!v(t.token)||!p(t.sessionId)||!f(t.expiresAt)?null:{token:t.token,sessionId:t.sessionId,expiresAt:t.expiresAt}}async function k(t,e){let i=JSON.stringify([t.apiUrl,t.siteId,t.origin,e??null]),n=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(i));return`parallel-agent-id:v1:${Array.from(new Uint8Array(n),r=>r.toString(16).padStart(2,"0")).join("")}`}function C(t,e){let i=h.get(t)??null;if(e==="session")try{let n=sessionStorage.getItem(t);i=n?M(JSON.parse(n)):i,n&&!i&&sessionStorage.removeItem(t)}catch{}return M(i)}function T(t,e,i){for(let[n,r]of h)f(r.expiresAt)||h.delete(n);if(i?h.set(t,{...i}):h.delete(t),e==="session")try{i?sessionStorage.setItem(t,JSON.stringify(i)):sessionStorage.removeItem(t)}catch{}}function S(t){return t instanceof Error?t:new Error("Identification request failed.")}async function A(t,e,i,n,r,s){let o=new AbortController,a=()=>o.abort();e.addEventListener("abort",a,{once:!0}),e.aborted&&o.abort();let c=setTimeout(()=>o.abort(),j);try{let u=await fetch(`${t.apiUrl}${i}`,{method:n,headers:{...r===void 0?{}:{"Content-Type":"application/json"},...s?{Authorization:`Bearer ${s}`,"X-Agent-Site":t.siteId}:{}},...r===void 0?{}:{body:JSON.stringify(r)},credentials:"omit",cache:"no-store",mode:"cors",referrerPolicy:"no-referrer",signal:o.signal});if(!u.ok)throw new l(u.status);if(n==="DELETE"){if(u.status!==204)throw new Error("Session revocation was not confirmed.");return null}if(!u.headers.get("content-type")?.toLowerCase().includes("application/json"))throw new Error("Identification service did not return JSON.");return await u.json()}catch(u){throw o.signal.aborted&&!e.aborted?new Error("Identification service timed out. Please retry.",{cause:u}):u}finally{clearTimeout(c),e.removeEventListener("abort",a)}}function P(t,e){return g(t)&&Array.isArray(t.results)&&t.results.some(i=>g(i)&&i.id===e&&(i.status==="recorded"||i.status==="duplicate"))}function _(){let t=[];return navigator.webdriver===!0&&t.push({signal:"webdriver",detail:"navigator.webdriver"}),/HeadlessChrome\//.test(navigator.userAgent)&&t.push({signal:"headless_browser",detail:"HeadlessChrome"}),t}var R=new WeakMap,L=new WeakMap,O={ai_assistant:"AI assistant",person:"Person",prefer_not_to_say:"Prefer not to say"};function q(t,e){let i=typeof t=="string"?document.querySelector(t):t;if(!(i instanceof HTMLElement))throw new Error(`${e} must identify an existing HTML element.`);return i}function G(t){let e=q(t.container,"container"),i=t.gate===void 0?void 0:q(t.gate,"gate"),n=w(t.apiUrl,"apiUrl"),r=t.mode??"optional",s=t.storage??"session";if(r!=="optional"&&r!=="required")throw new Error("Invalid identification mode.");if(s!=="session"&&s!=="memory")throw new Error("Invalid storage option.");if(i&&(i===e||i.contains(e)))throw new Error("The gate must not contain the identification widget.");let o=I();if(t.route!==void 0&&!m(t.route))throw new Error("route must be a path pattern such as /tickets/:id.");return{container:e,gate:i,apiUrl:n,siteId:x(t.siteId),origin:o,mode:r,storage:s,styleUrl:w(t.styleUrl??`${n}/sdk/v${E}/agent-id.css`,"styleUrl"),bindingContext:t.bindingContext,onRecorded:t.onRecorded,onError:t.onError,pageViews:t.pageViews??!0,route:t.route,automationSignals:t.automationSignals??!1}}function z(t,e){return Object.keys(t).every(i=>(i==="onRecorded"||i==="onError")&&e[i]===void 0?!0:t[i]===e[i])}function U(t,e,i,n){if(!g(t)||!p(t.sessionId)||i!==void 0&&t.sessionId!==i||t.siteId!==e.siteId||t.origin!==e.origin||!f(t.expiresAt)||t.evidence!=="self_reported")throw new Error("Identification service returned an invalid or mismatched session.");let r=t.choice===null&&t.receipt===null&&t.eventId===null,s=$(t.choice)&&N(t.receipt,1,8192)&&p(t.eventId);if(!r&&!s||n!==void 0&&t.choice!==n)throw new Error("Identification service returned an invalid or mismatched response.");return{sessionId:t.sessionId,siteId:e.siteId,origin:e.origin,expiresAt:t.expiresAt,choice:t.choice,receipt:t.receipt,eventId:t.eventId,evidence:"self_reported"}}var D=class{constructor(e){this.config=e;this.originalInert=e.gate?.inert??!1,this.build(),this.lock(),this.ready=this.start(this.version),this.ready.catch(()=>{})}config;ready;host=document.createElement("div");root=this.host.attachShadow({mode:"open"});status=document.createElement("p");retry=document.createElement("button");buttons=[];originalInert;version=0;controller=new AbortController;destroyed=!1;state=null;session=null;key=null;pending=null;retryAction=null;expiry;viewed=null;signalled=null;build(){this.host.dataset.agentIdentification=E;let e=document.createElement("link");e.rel="stylesheet",e.href=this.config.styleUrl;let i=document.createElement("section");i.className="agent-identification",i.setAttribute("aria-label","Visitor identification");let n=document.createElement("h2");n.textContent=this.config.mode==="required"?"Before continuing":"How are you visiting?";let r=document.createElement("p");r.textContent="Choose one option. Your answer is self-reported.";let s=document.createElement("div");s.className="choices";for(let o of Object.keys(O)){let a=document.createElement("button");a.type="button",a.textContent=O[o],a.disabled=!0,a.addEventListener("click",()=>{this.pending={choice:o,requestId:crypto.randomUUID()},this.submit(this.version).catch(()=>{})}),this.buttons.push(a),s.append(a)}this.status.className="status",this.status.setAttribute("role","status"),this.status.setAttribute("aria-live","polite"),this.status.setAttribute("aria-atomic","true"),this.retry.type="button",this.retry.textContent="Retry",this.retry.hidden=!0,this.retry.addEventListener("click",()=>{let o=this.retryAction;o&&o().catch(()=>{})}),i.append(n,r,s,this.status,this.retry),this.root.append(e,i),this.config.container.append(this.host)}active(e){if(this.destroyed||e!==this.version)throw new DOMException("Widget operation was superseded.","AbortError")}lock(){this.config.mode==="required"&&this.config.gate&&(this.config.gate.inert=!0)}busy(e){this.status.textContent=e,this.retry.hidden=!0,this.retryAction=null;for(let i of this.buttons)i.disabled=!0}fail(e,i,n,r){if(this.destroyed||i!==this.version)return;let s=S(e);this.status.textContent=`${r} ${s.message}`,this.retry.hidden=!1,this.retryAction=n,this.notifyError(s)}notifyError(e){try{this.config.onError?.(e)}catch{}}accepted(e){if(this.state=e,clearTimeout(this.expiry),this.expiry=setTimeout(()=>{this.destroyed||this.state?.sessionId!==e.sessionId||(this.state=null,this.lock(),this.busy("Your identification session expired."),this.retry.hidden=!1,this.retryAction=()=>this.restart())},Math.min(Date.parse(e.expiresAt)-Date.now(),2147483647)),this.retry.hidden=!0,this.retryAction=null,e.choice!==null){this.status.textContent=`Response recorded: ${O[e.choice]}.`,this.config.mode==="required"&&this.config.gate&&(this.config.gate.inert=this.originalInert);try{this.config.onRecorded?.({...e})}catch{this.notifyError(new Error("Your onRecorded callback failed after the response was recorded."))}}else this.status.textContent=this.config.mode==="required"?"Select any option to continue.":"You can choose an option or continue without answering.";for(let i of this.buttons)i.disabled=e.choice!==null;return{...e}}load(){return this.key?C(this.key,this.config.storage):null}persist(e){this.key&&T(this.key,this.config.storage,e)}request(e,i,n,r){return A(this.config,this.controller.signal,e,i,n,r)}async start(e){this.busy("Loading identification\u2026"),this.lock();try{let i=typeof this.config.bindingContext=="function"?await this.config.bindingContext():this.config.bindingContext;if(this.active(e),i!==void 0&&(typeof i!="string"||i.length===0||i.length>8192))throw new Error("bindingContext must be a nonempty signed context.");let n=await k(this.config,i);if(this.active(e),this.key=n,this.session=this.load(),this.session)try{let o=await this.request("/v1/sessions/current","GET",void 0,this.session.token);this.active(e);let a=U(o,this.config,this.session.sessionId);return this.session.expiresAt=a.expiresAt,this.persist(this.session),this.view(this.accepted(a))}catch(o){if(this.active(e),!(o instanceof l)||![401,404,410].includes(o.status))throw o;this.persist(null),this.session=null}let r=await this.request("/v1/sessions","POST",{siteId:this.config.siteId,...i===void 0?{}:{bindingContext:i}});this.active(e);let s=U(r,this.config);if(!g(r)||!v(r.sessionToken)||s.choice!==null)throw new Error("Identification service returned an invalid new session.");return this.session={token:r.sessionToken,sessionId:s.sessionId,expiresAt:s.expiresAt},this.persist(this.session),this.view(this.accepted(s))}catch(i){throw this.fail(i,e,()=>this.restart(),"Could not load identification."),i}}async submit(e){if(!this.pending||!this.session)throw new Error("Identification session is not ready.");let i=this.pending,n=this.session;this.busy("Recording your response\u2026");try{let r=await this.request("/v1/declarations","POST",i,n.token);this.active(e);let s=U(r,this.config,n.sessionId,i.choice);return n.expiresAt=s.expiresAt,this.persist(n),this.pending=null,this.accepted(s)}catch(r){throw this.fail(r,e,()=>this.submit(this.version),"Could not record your response."),r}}view(e){return this.config.pageViews&&this.viewed!==e.sessionId&&(this.viewed=e.sessionId,this.pageView({route:this.config.route}).catch(()=>{})),this.config.automationSignals&&this.signalled!==e.sessionId&&(this.signalled=e.sessionId,this.reportSignals().catch(()=>{})),e}async pageView(e={}){if(this.active(this.version),e.route!==void 0&&!m(e.route))throw new Error("route must be a path pattern such as /tickets/:id.");let i={id:crypto.randomUUID(),path:b(window.location.pathname),...e.route?{route:e.route}:{},occurredAt:new Date().toISOString()};await this.deliver("/v1/page-views",{views:[i]},i.id,"page view")}async deliver(e,i,n,r){let s=this.controller.signal;for(let o=0;;o++){let a=this.session;try{if(!a)throw new Error("Identification session is not ready.");let c=await this.request(e,"POST",i,a.token);if(!P(c,n))throw new Error(`Identification service did not confirm the ${r}.`);return}catch(c){if(s.aborted||this.destroyed)throw c;if(!(a!==null&&(!(c instanceof l)||c.status===429||c.status>=500))||o>=2){let y=S(c);throw this.notifyError(y),y}if(await new Promise(y=>setTimeout(y,300*3**o)),s.aborted||this.destroyed)throw c}}}async reportSignals(){for(let e of _()){let i={id:crypto.randomUUID(),...e,occurredAt:new Date().toISOString()};await this.deliver("/v1/browser-signals",{signals:[i]},i.id,"signal")}}restart(){return this.active(this.version),this.controller.abort(),clearTimeout(this.expiry),this.controller=new AbortController,this.version+=1,this.start(this.version)}getState(){return this.state?{...this.state}:null}async reset(){this.active(this.version),this.controller.abort(),clearTimeout(this.expiry),this.controller=new AbortController;let e=++this.version;this.busy("Resetting identification\u2026"),this.state=null,this.pending=null,this.lock();try{this.session&&(await this.request("/v1/sessions/current","DELETE",void 0,this.session.token),this.active(e)),this.persist(null),this.session=null}catch(i){throw this.fail(i,e,()=>this.reset(),"Could not reset identification."),i}return this.start(e)}destroy(){this.destroyed||(this.destroyed=!0,this.version+=1,this.controller.abort(),clearTimeout(this.expiry),this.host.remove(),this.config.mode==="required"&&this.config.gate&&(this.config.gate.inert=this.originalInert,L.delete(this.config.gate)),R.delete(this.config.container),this.retryAction=null)}};function H(t){let e=G(t),i=R.get(e.container);if(i){if(!z(i.config,e))throw new Error("This container already has an identification widget with different options.");return i}if(e.mode==="required"&&e.gate&&L.has(e.gate))throw new Error("This gate is already controlled by another identification widget.");let n=new D(e);return R.set(e.container,n),e.mode==="required"&&e.gate&&L.set(e.gate,n),n}window.AgentIdentification=Object.freeze({init:H});var d=document.currentScript;if(d?.dataset.site){let t=()=>{try{let e;d.dataset.target?e=d.dataset.target:(e=document.createElement("div"),e.dataset.agentIdentificationContainer="",document.body.append(e));let i={container:e,siteId:d.dataset.site,apiUrl:d.dataset.api??new URL(d.src).origin,mode:d.dataset.mode,gate:d.dataset.gate,storage:d.dataset.storage,styleUrl:d.dataset.style,pageViews:d.dataset.pageViews!=="off",route:d.dataset.route,automationSignals:d.dataset.automationSignals==="on",onRecorded:n=>{(typeof e=="string"?document.querySelector(e):e).dispatchEvent(new CustomEvent("agent-identification:recorded",{detail:n,bubbles:!0}))},onError:n=>{document.dispatchEvent(new CustomEvent("agent-identification:error",{detail:{message:n.message}}))}};H(i)}catch(e){let i=e instanceof Error?e.message:"Identification widget could not initialize.",n=document.createElement("p");n.setAttribute("role","alert"),n.textContent=`Visitor identification unavailable. ${i}`,d.insertAdjacentElement("afterend",n),document.dispatchEvent(new CustomEvent("agent-identification:error",{detail:{message:i}}))}};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t,{once:!0}):t()}})();
@@ -1,5 +1,5 @@
1
1
  // src/protocol.ts
2
- var VERSION = "0.8.2";
2
+ var VERSION = "0.9.0";
3
3
  var CHOICES = ["ai_assistant", "person", "prefer_not_to_say"];
4
4
  function isChoice(value) {
5
5
  return typeof value === "string" && CHOICES.includes(value);
@@ -191,6 +191,7 @@ function pageLinks() {
191
191
  }
192
192
  }
193
193
  var signalledSessions = /* @__PURE__ */ new Set();
194
+ var inputSessions = /* @__PURE__ */ new Set();
194
195
  var navigationListeners = /* @__PURE__ */ new WeakMap();
195
196
  function watchNavigation(listener) {
196
197
  const { history } = window;
@@ -214,6 +215,42 @@ function watchNavigation(listener) {
214
215
  listeners.add(listener);
215
216
  return () => listeners.delete(listener);
216
217
  }
218
+ function bindingContext(value) {
219
+ if (value === void 0 || value === null || value === "") return void 0;
220
+ if (typeof value !== "string" || value.length > 4096) {
221
+ throw new Error("bindingContext must be the signed context your server created.");
222
+ }
223
+ return value;
224
+ }
225
+ function watchInput(found) {
226
+ let lastKey = 0;
227
+ let composedByKey = false;
228
+ const keyPressed = () => performance.now() - lastKey <= 1e3;
229
+ const keydown = () => {
230
+ lastKey = performance.now();
231
+ };
232
+ const compositionstart = () => {
233
+ composedByKey = keyPressed();
234
+ };
235
+ const input = (event) => {
236
+ if (!event.isTrusted || keyPressed()) return;
237
+ const inputType = event.inputType;
238
+ if (inputType === "insertText" || inputType === "insertCompositionText" && !composedByKey) {
239
+ stop();
240
+ found("typing");
241
+ }
242
+ };
243
+ const options = { capture: true, passive: true };
244
+ window.addEventListener("keydown", keydown, options);
245
+ window.addEventListener("compositionstart", compositionstart, options);
246
+ window.addEventListener("input", input, options);
247
+ function stop() {
248
+ window.removeEventListener("keydown", keydown, options);
249
+ window.removeEventListener("compositionstart", compositionstart, options);
250
+ window.removeEventListener("input", input, options);
251
+ }
252
+ return stop;
253
+ }
217
254
  function configure(options) {
218
255
  const storage = options.storage ?? "session";
219
256
  if (storage !== "session" && storage !== "memory") throw new Error("Invalid storage option.");
@@ -227,6 +264,7 @@ function configure(options) {
227
264
  storage,
228
265
  navigation: options.navigation ?? true,
229
266
  automationSignals: options.automationSignals ?? true,
267
+ bindingContext: bindingContext(options.bindingContext),
230
268
  onError: options.onError
231
269
  };
232
270
  }
@@ -238,7 +276,10 @@ function sameConfiguration(left, right) {
238
276
  var Controller = class {
239
277
  constructor(config, route) {
240
278
  this.config = config;
241
- this.key = storageKey(config, void 0);
279
+ this.key = storageKey(config, config.bindingContext);
280
+ this.stopInput = config.automationSignals ? watchInput((detail) => void this.reportInput(detail).catch(() => {
281
+ })) : () => {
282
+ };
242
283
  this.stopWatching = config.navigation ? watchNavigation(() => this.navigated()) : () => {
243
284
  };
244
285
  this.ready = this.pageView({ route });
@@ -250,6 +291,7 @@ var Controller = class {
250
291
  controller = new AbortController();
251
292
  key;
252
293
  stopWatching;
294
+ stopInput;
253
295
  session = null;
254
296
  starting = null;
255
297
  destroyed = false;
@@ -309,6 +351,27 @@ var Controller = class {
309
351
  }
310
352
  return true;
311
353
  }
354
+ /** Reports injected input once per session, kept with the session like the other signals. */
355
+ async reportInput(detail) {
356
+ const session = await this.current();
357
+ const key = `${await this.key}:input`;
358
+ if (inputSessions.has(session.sessionId)) return;
359
+ inputSessions.add(session.sessionId);
360
+ try {
361
+ if (this.config.storage === "session") {
362
+ if (sessionStorage.getItem(key) === session.sessionId) return;
363
+ sessionStorage.setItem(key, session.sessionId);
364
+ }
365
+ } catch {
366
+ }
367
+ const event = {
368
+ id: crypto.randomUUID(),
369
+ signal: "synthetic_input",
370
+ detail,
371
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString()
372
+ };
373
+ await this.deliver("/v1/browser-signals", { signals: [event] }, event.id, "signal");
374
+ }
312
375
  async reportSignals() {
313
376
  for (const signal of automationSignals()) {
314
377
  const event = { id: crypto.randomUUID(), ...signal, occurredAt: (/* @__PURE__ */ new Date()).toISOString() };
@@ -325,7 +388,8 @@ var Controller = class {
325
388
  }
326
389
  async start(key) {
327
390
  const body = await send(this.config, this.controller.signal, "/v1/sessions", "POST", {
328
- siteId: this.config.siteId
391
+ siteId: this.config.siteId,
392
+ ...this.config.bindingContext ? { bindingContext: this.config.bindingContext } : {}
329
393
  });
330
394
  if (!record(body) || !uuid(body.sessionId) || body.siteId !== this.config.siteId || body.origin !== this.config.origin || !futureDate(body.expiresAt) || !sessionToken(body.sessionToken)) {
331
395
  throw new Error("Identification service returned an invalid new session.");
@@ -386,6 +450,7 @@ var Controller = class {
386
450
  if (this.destroyed) return;
387
451
  this.destroyed = true;
388
452
  this.stopWatching();
453
+ this.stopInput();
389
454
  this.controller.abort();
390
455
  if (trackers.get(this.config.apiUrl + " " + this.config.siteId) === this) {
391
456
  trackers.delete(this.config.apiUrl + " " + this.config.siteId);
@@ -4,7 +4,7 @@
4
4
  import { useEffect, useRef } from "react";
5
5
 
6
6
  // src/protocol.ts
7
- var VERSION = "0.8.2";
7
+ var VERSION = "0.9.0";
8
8
  var CHOICES = ["ai_assistant", "person", "prefer_not_to_say"];
9
9
  function isChoice(value) {
10
10
  return typeof value === "string" && CHOICES.includes(value);
@@ -51,12 +51,37 @@ var AGENT_USER_AGENT_TOKENS = [
51
51
  "MistralAI-User",
52
52
  "DuckAssistBot"
53
53
  ];
54
+ var CODING_AGENT_PRODUCTS = [
55
+ [/\bclaude-code\/\d/i, "Claude Code"],
56
+ [/\bcodex_cli_rs\/\d/i, "Codex"]
57
+ ];
58
+ var HTTP_CLIENTS = [
59
+ [/^curl\//i, "curl"],
60
+ [/^Wget\//i, "Wget"],
61
+ [/^python-requests\//i, "Python requests"],
62
+ [/^python-httpx\//i, "Python httpx"],
63
+ [/^Python-urllib\//i, "Python urllib"],
64
+ [/^aiohttp\//i, "Python aiohttp"],
65
+ [/^axios\//i, "axios"],
66
+ [/^node-fetch\b/i, "node-fetch"],
67
+ [/^(node|undici)$/i, "Node.js fetch"],
68
+ [/^Go-http-client\//i, "Go HTTP client"],
69
+ [/^okhttp\//i, "OkHttp"],
70
+ [/^Java-http-client\//i, "Java HTTP client"],
71
+ [/^libwww-perl\//i, "Perl LWP"],
72
+ [/^HTTPie\//i, "HTTPie"],
73
+ [/^Deno\//i, "Deno"],
74
+ [/^Bun\//i, "Bun"]
75
+ ];
54
76
  function userAgentSignals(userAgent) {
55
77
  const found = [];
56
- const token = AGENT_USER_AGENT_TOKENS.find(
78
+ const product = CODING_AGENT_PRODUCTS.find(([pattern]) => pattern.test(userAgent))?.[1];
79
+ const token = product ?? AGENT_USER_AGENT_TOKENS.find(
57
80
  (name) => new RegExp(`(^|[^A-Za-z-])${name}(/|;|\\s|$)`).test(userAgent)
58
81
  );
59
82
  if (token) found.push({ signal: "agent_user_agent", detail: token });
83
+ const client = HTTP_CLIENTS.find(([pattern]) => pattern.test(userAgent.trim()))?.[1];
84
+ if (client && !token) found.push({ signal: "http_client", detail: client });
60
85
  if (/HeadlessChrome\//.test(userAgent))
61
86
  found.push({ signal: "headless_browser", detail: "HeadlessChrome" });
62
87
  return found;
@@ -398,9 +423,15 @@ async function reportRequestSignals(request, options) {
398
423
  try {
399
424
  const headers = headerMap(request);
400
425
  const found = userAgentSignals(headers["user-agent"] ?? "");
401
- if (!found.length || isAssetRequest(requestUrl(request, headers), headers)) {
426
+ const url = options.url ?? requestUrl(request, headers);
427
+ if (!found.length || isAssetRequest(url, headers)) {
402
428
  return { status: "skipped" };
403
429
  }
430
+ let path = null;
431
+ try {
432
+ path = routePath(new URL(url, "http://localhost").pathname);
433
+ } catch {
434
+ }
404
435
  const api = new URL(options.apiUrl);
405
436
  if (!isAllowedOrigin(api.origin) || api.username || api.password || api.search || api.hash) {
406
437
  return fail(new Error("Invalid collector API URL"));
@@ -413,6 +444,7 @@ async function reportRequestSignals(request, options) {
413
444
  id: crypto.randomUUID(),
414
445
  ...item,
415
446
  ...address && item.signal === "agent_user_agent" ? { ip: address } : {},
447
+ ...path ? { path } : {},
416
448
  occurredAt,
417
449
  ...options.sessionBinding ? { sessionBinding: options.sessionBinding } : {},
418
450
  ...options.link ? { link: options.link } : {}
@@ -475,9 +507,88 @@ function reportLink(request, options = {}) {
475
507
  function serverTiming(link) {
476
508
  return `${LINK_TIMING};desc="${link}"`;
477
509
  }
510
+
511
+ // src/server/setup.ts
512
+ var DEFAULT_COLLECTOR = "https://parallel-agent-identification.vercel.app";
513
+ var warned = /* @__PURE__ */ new Set();
514
+ function agentIdentification(options = {}) {
515
+ const environment = typeof process === "undefined" ? {} : process.env;
516
+ const config = {
517
+ apiUrl: options.apiUrl ?? environment.AGENT_ID_API_URL ?? DEFAULT_COLLECTOR,
518
+ siteId: options.siteId ?? environment.AGENT_ID_SITE_ID ?? "",
519
+ secret: options.secret ?? environment.AGENT_ID_SITE_SECRET ?? "",
520
+ ...options.onError ? { onError: options.onError } : {},
521
+ ...options.fetch ? { fetch: options.fetch } : {}
522
+ };
523
+ const ready = () => {
524
+ if (config.siteId && config.secret) return true;
525
+ const missing = [!config.siteId && "AGENT_ID_SITE_ID", !config.secret && "AGENT_ID_SITE_SECRET"].filter(Boolean).join(" and ");
526
+ if (!warned.has(missing)) {
527
+ warned.add(missing);
528
+ const error = new Error(`Agent identification is off: set ${missing}.`);
529
+ (options.onError ?? ((failure) => console.warn(failure.message)))(error);
530
+ }
531
+ return false;
532
+ };
533
+ const report = (request, requestOptions = {}) => {
534
+ if (!ready()) return null;
535
+ const link = reportLink(request, requestOptions.url ? { url: requestOptions.url } : {});
536
+ const shared = {
537
+ ...config,
538
+ link,
539
+ ...requestOptions.url ? { url: requestOptions.url } : {}
540
+ };
541
+ const work = Promise.all([
542
+ reportSignedRequest(request, shared),
543
+ reportRequestSignals(request, {
544
+ ...shared,
545
+ clientIp: requestOptions.clientIp ?? null,
546
+ ...requestOptions.sessionBinding ? { sessionBinding: requestOptions.sessionBinding } : {}
547
+ })
548
+ ]);
549
+ if (requestOptions.waitUntil) requestOptions.waitUntil(work);
550
+ else void work;
551
+ return link ? serverTiming(link) : null;
552
+ };
553
+ return {
554
+ report,
555
+ express() {
556
+ return (request, response, next) => {
557
+ const timing = report(request, { clientIp: request.ip ?? null });
558
+ if (timing) response.append("Server-Timing", timing);
559
+ next();
560
+ };
561
+ },
562
+ next(request, event, response, extra = {}) {
563
+ const url = new URL(request.url);
564
+ url.host = request.headers.get("host") ?? url.host;
565
+ const clientIp = extra.clientIp !== void 0 ? extra.clientIp : environment.VERCEL ? request.headers.get("x-real-ip") : null;
566
+ const timing = report(request, {
567
+ url: url.href,
568
+ clientIp,
569
+ ...extra.sessionBinding ? { sessionBinding: extra.sessionBinding } : {},
570
+ waitUntil: (work) => event.waitUntil(work)
571
+ });
572
+ if (timing) response.headers.append("Server-Timing", timing);
573
+ return response;
574
+ },
575
+ action(input) {
576
+ if (!ready()) {
577
+ return Promise.resolve({
578
+ status: "failed",
579
+ id: input.id ?? "",
580
+ error: new Error("Agent identification is off: set its site ID and secret.")
581
+ });
582
+ }
583
+ return reportAction(input, config);
584
+ }
585
+ };
586
+ }
478
587
  export {
479
588
  AGENT_USER_AGENT_TOKENS,
589
+ DEFAULT_COLLECTOR,
480
590
  LINK_TIMING,
591
+ agentIdentification,
481
592
  createBindingContext,
482
593
  isLink,
483
594
  reportAction,
@@ -15,6 +15,11 @@ export interface TrackerOptions {
15
15
  * session, as heuristic evidence that never labels a visitor an AI agent. Defaults to true.
16
16
  */
17
17
  automationSignals?: boolean;
18
+ /**
19
+ * 0.9.0: a binding context your server signed with createBindingContext() for this visitor's
20
+ * own session. Actions your server reports with the same sessionBinding then join this visit.
21
+ */
22
+ bindingContext?: string;
18
23
  onError?: (error: Error) => void;
19
24
  }
20
25
  export interface Tracker {
@@ -1,5 +1,5 @@
1
1
  /** Shared wire protocol. Classifications are always self-reported. */
2
- export declare const VERSION = "0.8.2";
2
+ export declare const VERSION = "0.9.0";
3
3
  export declare const CHOICES: readonly ["ai_assistant", "person", "prefer_not_to_say"];
4
4
  export type Choice = (typeof CHOICES)[number];
5
5
  export type Mode = "optional" | "required";
@@ -120,7 +120,7 @@ export interface SignatureEvent extends EventBase {
120
120
  * Heuristic automation signals. Each is evidence, never identity: an automated browser isn't
121
121
  * necessarily an AI agent, and a user agent that names one is only a claim anyone can make.
122
122
  */
123
- export declare const SIGNALS: readonly ["webdriver", "headless_browser", "agent_user_agent"];
123
+ export declare const SIGNALS: readonly ["webdriver", "headless_browser", "agent_user_agent", "http_client", "synthetic_input"];
124
124
  export type Signal = (typeof SIGNALS)[number];
125
125
  /** The signals a page can observe itself. */
126
126
  export declare const BROWSER_SIGNALS: readonly Signal[];
@@ -137,6 +137,8 @@ export interface SignalEvent extends EventBase {
137
137
  signal: Signal;
138
138
  /** What triggered it, such as the user agent's ChatGPT-User token. */
139
139
  detail: string | null;
140
+ /** 0.9.0: for a server's report, the masked path of the page requested. Null from a page. */
141
+ path: string | null;
140
142
  /**
141
143
  * For a server-reported agent user agent sent with the visitor's address, whether that address
142
144
  * is on the agent's operator's published list. Null when it wasn't checked.
@@ -162,6 +164,12 @@ export declare function isSignal(value: unknown): value is Signal;
162
164
  * request claims to be that agent, which anyone can do, so it's a signal, never verification.
163
165
  */
164
166
  export declare const AGENT_USER_AGENT_TOKENS: readonly ["Claude-Agent", "ChatGPT-User", "OAI-SearchBot", "GPTBot", "Claude-User", "Claude-SearchBot", "ClaudeBot", "Perplexity-User", "PerplexityBot", "MistralAI-User", "DuckAssistBot"];
167
+ /**
168
+ * Coding agents that fetch pages from the developer's own machine, named by a product token in
169
+ * their user agent. Claude Code sends "Claude-User (claude-code/2.1.317; …)" from wherever it
170
+ * runs, so it's named on its own rather than checked against Anthropic's server addresses.
171
+ */
172
+ export declare const CODING_AGENT_PRODUCTS: readonly [RegExp, string][];
165
173
  /** The signals a user agent string carries on its own. */
166
174
  export declare function userAgentSignals(userAgent: string): {
167
175
  signal: Signal;
@@ -7,6 +7,8 @@ export type { ActionReport, ActionReportOptions, ReportedAction } from "./activi
7
7
  export { reportSignedRequest } from "./web-bot-auth.js";
8
8
  export { reportRequestSignals } from "./signals.js";
9
9
  export { reportLink, serverTiming } from "./links.js";
10
+ export { agentIdentification, DEFAULT_COLLECTOR } from "./setup.js";
11
+ export type { AgentIdentification, AgentIdentificationOptions, RequestReportOptions, } from "./setup.js";
10
12
  export type { SignalReport, SignalReportOptions } from "./signals.js";
11
13
  export type { ReportableRequest, SignatureReport, SignatureReportOptions } from "./web-bot-auth.js";
12
14
  export { routePath, userAgentSignals, isLink, AGENT_USER_AGENT_TOKENS, LINK_TIMING, } from "../protocol.js";
@@ -0,0 +1,60 @@
1
+ import { type ActionReport, type ReportedAction } from "./activity.js";
2
+ import { type ReportableRequest } from "./web-bot-auth.js";
3
+ /** The hosted collector, used unless you pass apiUrl or set AGENT_ID_API_URL. */
4
+ export declare const DEFAULT_COLLECTOR = "https://parallel-agent-identification.vercel.app";
5
+ export interface AgentIdentificationOptions {
6
+ /** Your public site ID. Defaults to the AGENT_ID_SITE_ID environment variable. */
7
+ siteId?: string;
8
+ /** Your server secret. Defaults to AGENT_ID_SITE_SECRET. Never put it in browser code. */
9
+ secret?: string;
10
+ /** The collector. Defaults to AGENT_ID_API_URL, then the hosted collector. */
11
+ apiUrl?: string;
12
+ /** Called when a report fails. Defaults to a console warning without any secret. */
13
+ onError?: (error: Error) => void;
14
+ fetch?: typeof globalThis.fetch;
15
+ }
16
+ export interface RequestReportOptions {
17
+ /** The URL the visitor asked for, if your framework rewrites the request's own. */
18
+ url?: string;
19
+ /**
20
+ * The visitor's IP address as your platform gives it, never from a header visitors can set.
21
+ * It's sent only with a request that names an agent, to check it against the agent's
22
+ * operator's published addresses, and the collector keeps only the result.
23
+ */
24
+ clientIp?: string | null;
25
+ /** Your server's own session binding, as in the page script's binding context. */
26
+ sessionBinding?: string;
27
+ /** Keeps the reports running after the response, such as Next.js's event.waitUntil. */
28
+ waitUntil?: (work: Promise<unknown>) => void;
29
+ }
30
+ export interface AgentIdentification {
31
+ /**
32
+ * Reports one request: a signed agent's signature, and an agent or automation its user agent
33
+ * names. Ordinary requests and assets are skipped without a network call. Returns the
34
+ * Server-Timing value to add to this page's response, or null, so the page script can join the
35
+ * reports to its visit. It never throws and never waits for the collector.
36
+ */
37
+ report(request: ReportableRequest, options?: RequestReportOptions): string | null;
38
+ /** Express or Connect middleware that reports every request. */
39
+ express(): (request: ReportableRequest & {
40
+ ip?: string;
41
+ }, response: {
42
+ append(name: string, value: string): unknown;
43
+ }, next: () => void) => void;
44
+ /**
45
+ * For a Next.js proxy: reports the request and adds the link to your response. On Vercel it
46
+ * passes the visitor's address from x-real-ip, which Vercel sets; elsewhere pass clientIp.
47
+ */
48
+ next<T extends {
49
+ headers: Headers;
50
+ }>(request: Request, event: {
51
+ waitUntil(work: Promise<unknown>): void;
52
+ }, response: T, options?: Pick<RequestReportOptions, "clientIp" | "sessionBinding">): T;
53
+ /** Reports an action your server handled, such as an order placed. Never throws. */
54
+ action(input: ReportedAction): Promise<ActionReport>;
55
+ }
56
+ /**
57
+ * Sets up agent identification in one call, from AGENT_ID_SITE_ID and AGENT_ID_SITE_SECRET unless
58
+ * you pass them. Use .express() as middleware, or .next() in a Next.js proxy.
59
+ */
60
+ export declare function agentIdentification(options?: AgentIdentificationOptions): AgentIdentification;
@@ -17,6 +17,11 @@ export interface SignalReportOptions {
17
17
  * address.
18
18
  */
19
19
  clientIp?: string | null;
20
+ /**
21
+ * The URL the visitor asked for, when your framework changes the request's own (Next.js uses
22
+ * localhost). Its masked path goes with the report, so you can see which pages agents fetched.
23
+ */
24
+ url?: string;
20
25
  /** Called when a report fails. Defaults to a console warning without any secret. */
21
26
  onError?: (error: Error) => void;
22
27
  fetch?: typeof globalThis.fetch;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useparallel/agent-identification",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "See which AI agents use your website: report agents from your server, record visits with a page script that draws nothing, and verify signed agents and operators' published addresses.",
5
5
  "type": "module",
6
6
  "license": "MIT",