@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 +8 -7
- package/dist/agent-id-track.js +1 -1
- package/dist/agent-id.js +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/react/index.js +1 -1
- package/dist/server/index.js +229 -25
- package/dist/types/protocol.d.ts +7 -1
- package/dist/types/server/encoding.d.ts +6 -0
- package/dist/types/server/ip.d.ts +2 -0
- package/dist/types/server/sha256.d.ts +2 -0
- package/dist/types/server/tokens.d.ts +1 -0
- package/package.json +3 -1
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
|
|
28
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
252
|
+
Versioned asset hashes are at `/sdk/v0.10.0/integrity.json`.
|
|
252
253
|
|
|
253
254
|
## What's collected
|
|
254
255
|
|
package/dist/agent-id-track.js
CHANGED
|
@@ -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()}})();
|
package/dist/browser/index.js
CHANGED
package/dist/react/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { useEffect, useRef } from "react";
|
|
5
5
|
|
|
6
6
|
// src/protocol.ts
|
|
7
|
-
var VERSION = "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);
|
package/dist/server/index.js
CHANGED
|
@@ -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(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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 =
|
|
325
|
+
const payload = base64url(utf8(JSON.stringify(claims)));
|
|
146
326
|
const signed = `${HEADER}.${payload}`;
|
|
147
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 &&
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
package/dist/types/protocol.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Shared wire protocol. Classifications are always self-reported. */
|
|
2
|
-
export declare const VERSION = "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;
|
|
@@ -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.
|
|
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",
|