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