@useparallel/agent-identification 0.9.0 → 0.10.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
@@ -24,8 +24,9 @@ hosted collector is `https://parallel-agent-identification.vercel.app`; use it
24
24
  wherever the examples below say `https://YOUR-COLLECTOR`. Full documentation is at
25
25
  [parallel-agent-dashboard.vercel.app/docs](https://parallel-agent-dashboard.vercel.app/docs).
26
26
 
27
- The server code runs on Node.js 20 or later. The page script and widget run in any
28
- current browser.
27
+ The server code runs on Node.js 20 or later, in Next.js middleware on the Edge runtime,
28
+ and in Cloudflare Workers with a compatibility date of 2024-11-11 or later. The page
29
+ script and widget run in any current browser.
29
30
 
30
31
  ## Report agents from your server
31
32
 
@@ -47,7 +48,7 @@ app.use(agentIdentification({ siteId: "your-public-site-id" }).express());
47
48
  // }
48
49
  ```
49
50
 
50
- It reports signed requests, agents that name themselves (including Claude Code and Codex),
51
+ It reports signed requests, agents that name themselves (including Claude Code, Codex and Claude desktop's browser),
51
52
  and command-line clients such as curl, each with the page requested, and checks Anthropic's
52
53
  and OpenAI's agents against their published addresses. Report actions with
53
54
  `agentId.action({ action, outcome, sessionBinding })`.
@@ -67,7 +68,7 @@ Add one tag to every page. It draws nothing, sets no cookies and needs no secret
67
68
  ```html
68
69
  <script
69
70
  defer
70
- src="https://YOUR-COLLECTOR/sdk/v0.9.0/agent-id-track.js"
71
+ src="https://YOUR-COLLECTOR/sdk/v0.10.0/agent-id-track.js"
71
72
  data-site="YOUR-SITE-ID"
72
73
  ></script>
73
74
  ```
@@ -93,7 +94,7 @@ on servers.
93
94
  <div id="visitor-choice"></div>
94
95
  <script
95
96
  defer
96
- src="https://YOUR-COLLECTOR/sdk/v0.9.0/agent-id.js"
97
+ src="https://YOUR-COLLECTOR/sdk/v0.10.0/agent-id.js"
97
98
  data-site="YOUR-SITE-ID"
98
99
  data-target="#visitor-choice"
99
100
  data-mode="optional"
@@ -114,7 +115,7 @@ To require a choice before using a section of the page:
114
115
  <form id="checkout"><!-- Your existing form --></form>
115
116
  <script
116
117
  defer
117
- src="https://YOUR-COLLECTOR/sdk/v0.9.0/agent-id.js"
118
+ src="https://YOUR-COLLECTOR/sdk/v0.10.0/agent-id.js"
118
119
  data-site="YOUR-SITE-ID"
119
120
  data-target="#visitor-choice"
120
121
  data-mode="required"
@@ -248,7 +249,7 @@ For a restrictive Content Security Policy, allow the collector in `script-src`,
248
249
  `style-src` and `connect-src`. Use external integration scripts or your existing
249
250
  nonce policy. The widget uses an external stylesheet and requires no inline
250
251
  script/style exceptions. Its `styleUrl` can point to your own copy of the CSS.
251
- Versioned asset hashes are at `/sdk/v0.9.0/integrity.json`.
252
+ Versioned asset hashes are at `/sdk/v0.10.0/integrity.json`.
252
253
 
253
254
  ## What's collected
254
255
 
@@ -1 +1 @@
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.")}})();
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 I(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 R(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 L(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 D=new Set,H=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 T=class{constructor(n,e){this.config=n;this.key=R(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:I(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(D.has(n))return!1;if(D.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(H.has(e.sessionId))return;H.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=L(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 b(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 T(n,t.route);return w.set(e,i),i}window.AgentIdentificationTracker=Object.freeze({track:b});var u=document.currentScript;function G(t){document.dispatchEvent(new CustomEvent("agent-identification:error",{detail:{message:t}}))}if(u?.dataset.site)try{b({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.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
+ "use strict";(()=>{var E="0.10.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 U=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 R(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=R(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=R(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=R(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)),U.delete(this.config.container),this.retryAction=null)}};function H(t){let e=G(t),i=U.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 U.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.9.0";
2
+ var VERSION = "0.10.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);
@@ -4,7 +4,7 @@
4
4
  import { useEffect, useRef } from "react";
5
5
 
6
6
  // src/protocol.ts
7
- var VERSION = "0.9.0";
7
+ var VERSION = "0.10.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);
@@ -1,6 +1,3 @@
1
- // src/server/tokens.ts
2
- import { createHash, createHmac, timingSafeEqual } from "node:crypto";
3
-
4
1
  // src/protocol.ts
5
2
  var CHOICES = ["ai_assistant", "person", "prefer_not_to_say"];
6
3
  function isChoice(value) {
@@ -55,6 +52,9 @@ var CODING_AGENT_PRODUCTS = [
55
52
  [/\bclaude-code\/\d/i, "Claude Code"],
56
53
  [/\bcodex_cli_rs\/\d/i, "Codex"]
57
54
  ];
55
+ var AGENT_BROWSER_PRODUCTS = [
56
+ [/(?:^|[\s;(])Claude\/\d/, "Claude desktop"]
57
+ ];
58
58
  var HTTP_CLIENTS = [
59
59
  [/^curl\//i, "curl"],
60
60
  [/^Wget\//i, "Wget"],
@@ -75,7 +75,9 @@ var HTTP_CLIENTS = [
75
75
  ];
76
76
  function userAgentSignals(userAgent) {
77
77
  const found = [];
78
- const product = CODING_AGENT_PRODUCTS.find(([pattern]) => pattern.test(userAgent))?.[1];
78
+ const product = [...CODING_AGENT_PRODUCTS, ...AGENT_BROWSER_PRODUCTS].find(
79
+ ([pattern]) => pattern.test(userAgent)
80
+ )?.[1];
79
81
  const token = product ?? AGENT_USER_AGENT_TOKENS.find(
80
82
  (name) => new RegExp(`(^|[^A-Za-z-])${name}(/|;|\\s|$)`).test(userAgent)
81
83
  );
@@ -94,10 +96,184 @@ function isLink(value) {
94
96
  return typeof value === "string" && /^[A-Za-z0-9_-]{22}$/.test(value);
95
97
  }
96
98
 
99
+ // src/server/encoding.ts
100
+ var encoder = new TextEncoder();
101
+ var decoder = new TextDecoder("utf-8", { ignoreBOM: true });
102
+ function utf8(text) {
103
+ if (typeof text !== "string") throw new TypeError("Expected a string");
104
+ return encoder.encode(text);
105
+ }
106
+ function fromUtf8(bytes) {
107
+ return decoder.decode(bytes);
108
+ }
109
+ function hex(bytes) {
110
+ let text = "";
111
+ for (const byte of bytes) text += byte.toString(16).padStart(2, "0");
112
+ return text;
113
+ }
114
+ function base64url(bytes) {
115
+ let binary = "";
116
+ for (const byte of bytes) binary += String.fromCharCode(byte);
117
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
118
+ }
119
+ function fromBase64url(text) {
120
+ if (!/^[A-Za-z0-9_-]*$/.test(text)) throw new Error("Invalid base64url");
121
+ const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
122
+ const bytes = new Uint8Array(binary.length);
123
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
124
+ return bytes;
125
+ }
126
+
127
+ // src/server/sha256.ts
128
+ var K = new Uint32Array([
129
+ 1116352408,
130
+ 1899447441,
131
+ 3049323471,
132
+ 3921009573,
133
+ 961987163,
134
+ 1508970993,
135
+ 2453635748,
136
+ 2870763221,
137
+ 3624381080,
138
+ 310598401,
139
+ 607225278,
140
+ 1426881987,
141
+ 1925078388,
142
+ 2162078206,
143
+ 2614888103,
144
+ 3248222580,
145
+ 3835390401,
146
+ 4022224774,
147
+ 264347078,
148
+ 604807628,
149
+ 770255983,
150
+ 1249150122,
151
+ 1555081692,
152
+ 1996064986,
153
+ 2554220882,
154
+ 2821834349,
155
+ 2952996808,
156
+ 3210313671,
157
+ 3336571891,
158
+ 3584528711,
159
+ 113926993,
160
+ 338241895,
161
+ 666307205,
162
+ 773529912,
163
+ 1294757372,
164
+ 1396182291,
165
+ 1695183700,
166
+ 1986661051,
167
+ 2177026350,
168
+ 2456956037,
169
+ 2730485921,
170
+ 2820302411,
171
+ 3259730800,
172
+ 3345764771,
173
+ 3516065817,
174
+ 3600352804,
175
+ 4094571909,
176
+ 275423344,
177
+ 430227734,
178
+ 506948616,
179
+ 659060556,
180
+ 883997877,
181
+ 958139571,
182
+ 1322822218,
183
+ 1537002063,
184
+ 1747873779,
185
+ 1955562222,
186
+ 2024104815,
187
+ 2227730452,
188
+ 2361852424,
189
+ 2428436474,
190
+ 2756734187,
191
+ 3204031479,
192
+ 3329325298
193
+ ]);
194
+ var INITIAL = [
195
+ 1779033703,
196
+ 3144134277,
197
+ 1013904242,
198
+ 2773480762,
199
+ 1359893119,
200
+ 2600822924,
201
+ 528734635,
202
+ 1541459225
203
+ ];
204
+ var BLOCK = 64;
205
+ function rotr(x, n) {
206
+ return x >>> n | x << 32 - n;
207
+ }
208
+ function sha256(data) {
209
+ const padded = new Uint8Array(Math.ceil((data.length + 9) / BLOCK) * BLOCK);
210
+ padded.set(data);
211
+ padded[data.length] = 128;
212
+ const view = new DataView(padded.buffer);
213
+ const bits = data.length * 8;
214
+ view.setUint32(padded.length - 8, Math.floor(bits / 4294967296));
215
+ view.setUint32(padded.length - 4, bits >>> 0);
216
+ const h = new Uint32Array(INITIAL);
217
+ const w = new Uint32Array(64);
218
+ for (let offset = 0; offset < padded.length; offset += BLOCK) {
219
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4);
220
+ for (let i = 16; i < 64; i++) {
221
+ const a2 = w[i - 15];
222
+ const b2 = w[i - 2];
223
+ const s0 = rotr(a2, 7) ^ rotr(a2, 18) ^ a2 >>> 3;
224
+ const s1 = rotr(b2, 17) ^ rotr(b2, 19) ^ b2 >>> 10;
225
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1;
226
+ }
227
+ let a = h[0];
228
+ let b = h[1];
229
+ let c = h[2];
230
+ let d = h[3];
231
+ let e = h[4];
232
+ let f = h[5];
233
+ let g = h[6];
234
+ let k = h[7];
235
+ for (let i = 0; i < 64; i++) {
236
+ const t1 = k + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K[i] + w[i] | 0;
237
+ const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
238
+ k = g;
239
+ g = f;
240
+ f = e;
241
+ e = d + t1 | 0;
242
+ d = c;
243
+ c = b;
244
+ b = a;
245
+ a = t1 + t2 | 0;
246
+ }
247
+ h[0] += a;
248
+ h[1] += b;
249
+ h[2] += c;
250
+ h[3] += d;
251
+ h[4] += e;
252
+ h[5] += f;
253
+ h[6] += g;
254
+ h[7] += k;
255
+ }
256
+ const digest = new Uint8Array(32);
257
+ const out = new DataView(digest.buffer);
258
+ for (let i = 0; i < 8; i++) out.setUint32(i * 4, h[i]);
259
+ return digest;
260
+ }
261
+ function hmacSha256(key, data) {
262
+ const block = new Uint8Array(BLOCK);
263
+ block.set(key.length > BLOCK ? sha256(key) : key);
264
+ const inner = new Uint8Array(BLOCK + data.length);
265
+ const outer = new Uint8Array(BLOCK + 32);
266
+ for (let i = 0; i < BLOCK; i++) {
267
+ inner[i] = block[i] ^ 54;
268
+ outer[i] = block[i] ^ 92;
269
+ }
270
+ inner.set(data, BLOCK);
271
+ outer.set(sha256(inner), BLOCK);
272
+ return sha256(outer);
273
+ }
274
+
97
275
  // src/server/tokens.ts
98
- var HEADER = Buffer.from(JSON.stringify({ alg: "HS256", typ: "AID", v: 1 })).toString(
99
- "base64url"
100
- );
276
+ var HEADER = base64url(utf8(JSON.stringify({ alg: "HS256", typ: "AID", v: 1 })));
101
277
  var HASH = /^[a-f0-9]{64}$/;
102
278
  var ID = /^[a-zA-Z0-9_-]{1,100}$/;
103
279
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
@@ -117,7 +293,7 @@ function isAllowedOrigin(value) {
117
293
  }
118
294
  }
119
295
  function hashToken(token) {
120
- return createHash("sha256").update(token).digest("hex");
296
+ return hex(sha256(utf8(token)));
121
297
  }
122
298
  function hashBinding(siteId, binding) {
123
299
  if (typeof binding !== "string" || !binding || binding.length > 2048) {
@@ -126,10 +302,14 @@ function hashBinding(siteId, binding) {
126
302
  return hashToken(JSON.stringify(["agent-identification-binding-v1", siteId, binding]));
127
303
  }
128
304
  function secretEquals(actual, expected) {
129
- return timingSafeEqual(
130
- Buffer.from(hashToken(actual), "hex"),
131
- Buffer.from(hashToken(expected), "hex")
132
- );
305
+ const a = sha256(utf8(actual));
306
+ const b = sha256(utf8(expected));
307
+ let difference = 0;
308
+ for (let i = 0; i < a.length; i++) difference |= a[i] ^ b[i];
309
+ return difference === 0;
310
+ }
311
+ function hmac(secret, data) {
312
+ return base64url(hmacSha256(utf8(secret), utf8(data)));
133
313
  }
134
314
  function seconds(now) {
135
315
  if (!Number.isFinite(now.getTime())) throw new Error("Invalid current time");
@@ -142,10 +322,9 @@ function assertSecret(secret) {
142
322
  }
143
323
  function sign(claims, secret) {
144
324
  assertSecret(secret);
145
- const payload = Buffer.from(JSON.stringify(claims)).toString("base64url");
325
+ const payload = base64url(utf8(JSON.stringify(claims)));
146
326
  const signed = `${HEADER}.${payload}`;
147
- const signature = createHmac("sha256", secret).update(signed).digest("base64url");
148
- return `${signed}.${signature}`;
327
+ return `${signed}.${hmac(secret, signed)}`;
149
328
  }
150
329
  function verify(token, expected) {
151
330
  assertSecret(expected.secret);
@@ -159,11 +338,11 @@ function verify(token, expected) {
159
338
  if (parts.length !== 3 || parts[0] !== HEADER || !/^[A-Za-z0-9_-]+$/.test(parts[1]) || !/^[A-Za-z0-9_-]{43}$/.test(parts[2])) {
160
339
  throw new Error("Invalid signed token");
161
340
  }
162
- const signature = createHmac("sha256", expected.secret).update(`${parts[0]}.${parts[1]}`).digest("base64url");
341
+ const signature = hmac(expected.secret, `${parts[0]}.${parts[1]}`);
163
342
  if (!secretEquals(signature, parts[2])) throw new Error("Invalid signed token");
164
343
  let claims;
165
344
  try {
166
- claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
345
+ claims = JSON.parse(fromUtf8(fromBase64url(parts[1])));
167
346
  } catch {
168
347
  throw new Error("Invalid signed token");
169
348
  }
@@ -234,7 +413,8 @@ async function verifyDeclarationOnline(receipt, options) {
234
413
  },
235
414
  credentials: "omit",
236
415
  cache: "no-store",
237
- redirect: "error",
416
+ // A redirect isn't ok, so it fails and is never followed. Cloudflare Workers reject "error".
417
+ redirect: "manual",
238
418
  signal: AbortSignal.timeout(1e4),
239
419
  body: JSON.stringify({
240
420
  siteId: options.siteId,
@@ -299,7 +479,8 @@ async function reportAction(input, options) {
299
479
  },
300
480
  credentials: "omit",
301
481
  cache: "no-store",
302
- redirect: "error",
482
+ // A redirect isn't ok, so it fails and is never followed. Cloudflare Workers reject "error".
483
+ redirect: "manual",
303
484
  signal: AbortSignal.timeout(5e3),
304
485
  body
305
486
  });
@@ -387,7 +568,8 @@ async function reportSignedRequest(request, options) {
387
568
  },
388
569
  credentials: "omit",
389
570
  cache: "no-store",
390
- redirect: "error",
571
+ // A redirect isn't ok, so it fails and is never followed. Cloudflare Workers reject "error".
572
+ redirect: "manual",
391
573
  signal: AbortSignal.timeout(5e3),
392
574
  body: JSON.stringify({
393
575
  siteId: options.siteId,
@@ -413,8 +595,21 @@ async function reportSignedRequest(request, options) {
413
595
  }
414
596
  }
415
597
 
598
+ // src/server/ip.ts
599
+ var V4_SEGMENT = "(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])";
600
+ var V4 = `(?:${V4_SEGMENT}\\.){3}${V4_SEGMENT}`;
601
+ var V6_SEGMENT = "(?:[0-9a-fA-F]{1,4})";
602
+ var IPV4 = new RegExp(`^${V4}$`);
603
+ var IPV6 = new RegExp(
604
+ `^(?:(?:${V6_SEGMENT}:){7}(?:${V6_SEGMENT}|:)|(?:${V6_SEGMENT}:){6}(?:${V4}|:${V6_SEGMENT}|:)|(?:${V6_SEGMENT}:){5}(?::${V4}|(?::${V6_SEGMENT}){1,2}|:)|(?:${V6_SEGMENT}:){4}(?:(?::${V6_SEGMENT}){0,1}:${V4}|(?::${V6_SEGMENT}){1,3}|:)|(?:${V6_SEGMENT}:){3}(?:(?::${V6_SEGMENT}){0,2}:${V4}|(?::${V6_SEGMENT}){1,4}|:)|(?:${V6_SEGMENT}:){2}(?:(?::${V6_SEGMENT}){0,3}:${V4}|(?::${V6_SEGMENT}){1,5}|:)|(?:${V6_SEGMENT}:){1}(?:(?::${V6_SEGMENT}){0,4}:${V4}|(?::${V6_SEGMENT}){1,6}|:)|(?::(?:(?::${V6_SEGMENT}){0,5}:${V4}|(?::${V6_SEGMENT}){1,7}|:)))(?:%[0-9a-zA-Z-.:]{1,})?$`
605
+ );
606
+ function ipVersion(value) {
607
+ if (IPV4.test(value)) return 4;
608
+ if (IPV6.test(value)) return 6;
609
+ return 0;
610
+ }
611
+
416
612
  // src/server/signals.ts
417
- import { isIP } from "node:net";
418
613
  async function reportRequestSignals(request, options) {
419
614
  const fail = (error) => {
420
615
  (options.onError ?? ((e) => console.warn(`Signal report failed: ${e.message}`)))(error);
@@ -439,7 +634,7 @@ async function reportRequestSignals(request, options) {
439
634
  api.pathname = `${api.pathname.replace(/\/$/, "")}/v1/signals`;
440
635
  const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
441
636
  const ip = typeof options.clientIp === "string" ? options.clientIp.trim() : "";
442
- const address = ip && isIP(ip.replace(/^\[|\]$/g, "").replace(/%.*$/, "")) ? ip : null;
637
+ const address = ip && ipVersion(ip.replace(/^\[|\]$/g, "").replace(/%.*$/, "")) ? ip : null;
443
638
  const signals = found.map((item) => ({
444
639
  id: crypto.randomUUID(),
445
640
  ...item,
@@ -460,7 +655,8 @@ async function reportRequestSignals(request, options) {
460
655
  },
461
656
  credentials: "omit",
462
657
  cache: "no-store",
463
- redirect: "error",
658
+ // A redirect isn't ok, so it fails and is never followed. Cloudflare Workers reject "error".
659
+ redirect: "manual",
464
660
  signal: AbortSignal.timeout(5e3),
465
661
  body: JSON.stringify({ siteId: options.siteId, signals })
466
662
  });
@@ -502,7 +698,7 @@ function reportLink(request, options = {}) {
502
698
  const signed = Boolean(headers["signature-input"] && headers.signature);
503
699
  if (!signed && !userAgentSignals(headers["user-agent"] ?? "").length) return null;
504
700
  if (isAssetRequest(options.url ?? requestUrl(request, headers), headers)) return null;
505
- return Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString("base64url");
701
+ return base64url(crypto.getRandomValues(new Uint8Array(16)));
506
702
  }
507
703
  function serverTiming(link) {
508
704
  return `${LINK_TIMING};desc="${link}"`;
@@ -561,7 +757,15 @@ function agentIdentification(options = {}) {
561
757
  },
562
758
  next(request, event, response, extra = {}) {
563
759
  const url = new URL(request.url);
564
- url.host = request.headers.get("host") ?? url.host;
760
+ const host = request.headers.get("host");
761
+ if (host) {
762
+ try {
763
+ const asked = new URL(`${url.protocol}//${host}`);
764
+ url.hostname = asked.hostname;
765
+ url.port = asked.port;
766
+ } catch {
767
+ }
768
+ }
565
769
  const clientIp = extra.clientIp !== void 0 ? extra.clientIp : environment.VERCEL ? request.headers.get("x-real-ip") : null;
566
770
  const timing = report(request, {
567
771
  url: url.href,
@@ -1,5 +1,5 @@
1
1
  /** Shared wire protocol. Classifications are always self-reported. */
2
- export declare const VERSION = "0.9.0";
2
+ export declare const VERSION = "0.10.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";
@@ -170,6 +170,12 @@ export declare const AGENT_USER_AGENT_TOKENS: readonly ["Claude-Agent", "ChatGPT
170
170
  * runs, so it's named on its own rather than checked against Anthropic's server addresses.
171
171
  */
172
172
  export declare const CODING_AGENT_PRODUCTS: readonly [RegExp, string][];
173
+ /**
174
+ * Browsers built into AI agent apps, named by a product token in their user agent. Claude
175
+ * desktop's built-in browser sends "Claude/<version>" among Chrome's tokens. Claude drives it, and
176
+ * so can the person using the app, so it says where a visit happened, not who drove it (0.10.0).
177
+ */
178
+ export declare const AGENT_BROWSER_PRODUCTS: readonly [RegExp, string][];
173
179
  /** The signals a user agent string carries on its own. */
174
180
  export declare function userAgentSignals(userAgent: string): {
175
181
  signal: Signal;
@@ -0,0 +1,6 @@
1
+ export declare function utf8(text: string): Uint8Array;
2
+ export declare function fromUtf8(bytes: Uint8Array): string;
3
+ export declare function hex(bytes: Uint8Array): string;
4
+ export declare function base64url(bytes: Uint8Array): string;
5
+ /** Throws on text that isn't base64url. */
6
+ export declare function fromBase64url(text: string): Uint8Array;
@@ -0,0 +1,2 @@
1
+ /** 4 for an IPv4 address, 6 for IPv6, else 0, as net.isIP() answers. */
2
+ export declare function ipVersion(value: string): 0 | 4 | 6;
@@ -0,0 +1,2 @@
1
+ export declare function sha256(data: Uint8Array): Uint8Array;
2
+ export declare function hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array;
@@ -38,6 +38,7 @@ export declare function isAllowedOrigin(value: unknown): value is string;
38
38
  export declare function validateSite(site: SiteConfig): void;
39
39
  export declare function hashToken(token: string): string;
40
40
  export declare function hashBinding(siteId: string, binding: string): string;
41
+ /** Compares two values in constant time, through their hashes, so neither length shows. */
41
42
  export declare function secretEquals(actual: string, expected: string): boolean;
42
43
  /** Mint on your server only, using its current session rather than browser input. */
43
44
  export declare function createBindingContext(options: BindingOptions): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useparallel/agent-identification",
3
- "version": "0.9.0",
3
+ "version": "0.10.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",
@@ -63,6 +63,7 @@
63
63
  "test:e2e": "playwright test",
64
64
  "smoke:hosted": "node scripts/smoke-hosted.mjs",
65
65
  "test:package": "node scripts/test-package.mjs",
66
+ "test:edge-runtimes": "node --import tsx scripts/test-edge-runtimes.mjs",
66
67
  "check": "npm run build && npm run typecheck && npm run lint && npm run format:check && npm test && npm run test:package",
67
68
  "dev:collector": "node --import tsx scripts/dev-server.ts",
68
69
  "site:create": "node scripts/site-config.mjs",
@@ -73,6 +74,7 @@
73
74
  "access": "public"
74
75
  },
75
76
  "devDependencies": {
77
+ "@edge-runtime/vm": "5.0.0",
76
78
  "@electric-sql/pglite": "0.5.10",
77
79
  "@electric-sql/pglite-socket": "0.2.13",
78
80
  "@eslint/js": "10.0.1",