@useparallel/agent-identification 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Parallel AI Research contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,269 @@
1
- # Temporary Holding Version
1
+ # Agent Identification
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ See which AI agents use your product. Your server reports the requests that agents
4
+ sign with Web Bot Auth, which the collector verifies against the signer's published
5
+ keys, and requests whose user agent names an AI agent, which are only signals. A
6
+ page script that draws nothing gives each visit a session with its page views, and
7
+ reports browsers that say they're automated. Visitors see nothing, and agent
8
+ builders install nothing.
9
+
10
+ An optional declaration widget asks visitors to choose **AI assistant**,
11
+ **Person**, or **Prefer not to say**. Every visitor sees it, people included, so
12
+ it isn't part of the default install. Its answers are **visible, self-reported
13
+ classification**. Nothing here detects every agent, proves who an agent acts for,
14
+ or grants permissions.
15
+
16
+ ```sh
17
+ npm install @useparallel/agent-identification
18
+ ```
19
+
20
+ Create a product in the [dashboard](https://parallel-agent-dashboard.vercel.app) to
21
+ get your site ID and server secret. Its **Install** tab has every snippet below with
22
+ your values filled in, plus instructions you can hand to your coding agent. The
23
+ hosted collector is `https://parallel-agent-identification.vercel.app`; use it
24
+ wherever the examples below say `https://YOUR-COLLECTOR`. Full documentation is at
25
+ [parallel-agent-dashboard.vercel.app/docs](https://parallel-agent-dashboard.vercel.app/docs).
26
+
27
+ The server code runs on Node.js 20 or later. The page script and widget run in any
28
+ current browser.
29
+
30
+ ## Report agents from your server
31
+
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
+
36
+ ```ts
37
+ import { agentIdentification } from "@useparallel/agent-identification/server";
38
+
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());
42
+
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
+ // }
48
+ ```
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
+
55
+ The page script reads the link from its own page's `Server-Timing` header and claims
56
+ it, so a signed request joins that visit. Only that browser has the link, and no
57
+ cookie is set. Keep pages that carry a link out of shared caches.
58
+
59
+ In Next.js, put them in `proxy.ts` and pass the URL the visitor asked for, because
60
+ Next.js rewrites the request's host to `localhost`. The dashboard's install page
61
+ and docs have that example and one for servers in other languages.
62
+
63
+ ## Add the page script
64
+
65
+ Add one tag to every page. It draws nothing, sets no cookies and needs no secret.
66
+
67
+ ```html
68
+ <script
69
+ defer
70
+ src="https://YOUR-COLLECTOR/sdk/v0.9.0/agent-id-track.js"
71
+ data-site="YOUR-SITE-ID"
72
+ ></script>
73
+ ```
74
+
75
+ For each tab it starts a session and records a page view on load and whenever the
76
+ path changes without a reload, with the path only: no query or fragment, and parts
77
+ that look like IDs or email addresses masked. Once per session it reports
78
+ `navigator.webdriver` or a headless Chrome user agent as a signal, never as proof of
79
+ an agent. Turn these off with `data-navigation="off"` or
80
+ `data-automation-signals="off"`, and give a page's route pattern with `data-route`.
81
+ In a bundled app, call `track({ siteId, apiUrl })` from the package once in the
82
+ browser. Failures never change the page; listen for `agent-identification:error`
83
+ on `document` to see them.
84
+
85
+ ## Add the declaration widget (every visitor sees it)
86
+
87
+ Ask your collector operator for its URL and a public site ID registered for your
88
+ exact website origins. `https://example.com` and `https://www.example.com` are
89
+ separate origins. The site ID is public; the site's signing/export secret stays
90
+ on servers.
91
+
92
+ ```html
93
+ <div id="visitor-choice"></div>
94
+ <script
95
+ defer
96
+ src="https://YOUR-COLLECTOR/sdk/v0.9.0/agent-id.js"
97
+ data-site="YOUR-SITE-ID"
98
+ data-target="#visitor-choice"
99
+ data-mode="optional"
100
+ ></script>
101
+ ```
102
+
103
+ With the page script on the same page, the widget joins its tab session; add
104
+ `data-page-views="off"` so a visit's page view isn't recorded twice.
105
+
106
+ The script uses its own origin for the API and stylesheet by default. Set
107
+ `data-api` or `data-style` if you host the assets separately. Without `data-target`,
108
+ it appends its container to the document body.
109
+
110
+ To require a choice before using a section of the page:
111
+
112
+ ```html
113
+ <div id="visitor-choice"></div>
114
+ <form id="checkout"><!-- Your existing form --></form>
115
+ <script
116
+ defer
117
+ src="https://YOUR-COLLECTOR/sdk/v0.9.0/agent-id.js"
118
+ data-site="YOUR-SITE-ID"
119
+ data-target="#visitor-choice"
120
+ data-mode="required"
121
+ data-gate="#checkout"
122
+ ></script>
123
+ ```
124
+
125
+ The widget restores the gate's original `inert` state. For no flash before
126
+ initialization, keep the form hidden with your application's loading state, then
127
+ show it after initialization.
128
+
129
+ All three answers satisfy the UI gate, and it unlocks only after the backend
130
+ acknowledges the response. A browser-only gate can be bypassed; use the server
131
+ integration below for protected actions. The widget must sit outside its gate.
132
+
133
+ Listen on the container for `agent-identification:recorded`. Its `event.detail`
134
+ contains the choice, session/event IDs, expiry, evidence label and signed receipt.
135
+ It does not contain the session bearer token.
136
+
137
+ ```js
138
+ document
139
+ .querySelector("#visitor-choice")
140
+ .addEventListener("agent-identification:recorded", (event) => console.log(event.detail.choice));
141
+ ```
142
+
143
+ ## Install as a package
144
+
145
+ ```sh
146
+ npm install @useparallel/agent-identification
147
+ ```
148
+
149
+ ```js
150
+ import { init } from "@useparallel/agent-identification";
151
+
152
+ const widget = init({
153
+ container: "#visitor-choice",
154
+ siteId: "YOUR-SITE-ID",
155
+ apiUrl: "https://YOUR-COLLECTOR",
156
+ mode: "required",
157
+ gate: "#checkout",
158
+ onRecorded: (declaration) => {
159
+ // Store the receipt in your application if using server verification.
160
+ console.log(declaration.choice);
161
+ },
162
+ });
163
+ await widget.ready;
164
+ ```
165
+
166
+ Call `await widget.reset()` to revoke the previous collector session and start
167
+ over. Call `widget.destroy()` on unmount/navigation; it
168
+ does not revoke a session. The default storage is per-tab `sessionStorage`, scoped
169
+ to site, origin, collector and binding context. If storage is blocked it uses
170
+ memory. Choose `storage: "memory"` when persistence across reloads is unwanted.
171
+ Restoring a session always checks the server before accepting its previous answer.
172
+
173
+ ### React / Next.js
174
+
175
+ ```tsx
176
+ "use client";
177
+ import { AgentIdentification } from "@useparallel/agent-identification/react";
178
+
179
+ export function VisitorChoice() {
180
+ return (
181
+ <AgentIdentification
182
+ siteId="YOUR-SITE-ID"
183
+ apiUrl="https://YOUR-COLLECTOR"
184
+ mode="optional"
185
+ onRecorded={(declaration) => console.log(declaration.choice)}
186
+ />
187
+ );
188
+ }
189
+ ```
190
+
191
+ React is an optional peer dependency. Network and DOM work happen after mounting.
192
+ For required mode pass a `gate` selector pointing to an existing sibling element.
193
+ Keep `bindingContext` functions stable with `useCallback` to avoid reinitializing
194
+ the widget on every render. The component removes its instance on unmount.
195
+
196
+ ## Enforce a protected action
197
+
198
+ Add two small pieces to your application's existing server:
199
+
200
+ 1. An authenticated endpoint creates a short-lived binding context from the
201
+ server's **actual current session**.
202
+ 2. The protected action checks its normal permissions and calls
203
+ `verifyDeclarationOnline` against that same active session before proceeding.
204
+
205
+ ```ts
206
+ import {
207
+ createBindingContext,
208
+ verifyDeclarationOnline,
209
+ } from "@useparallel/agent-identification/server";
210
+
211
+ // On your server only. Resolve the current session using your existing auth.
212
+ const settings = {
213
+ siteId: "YOUR-SITE-ID",
214
+ origin: "https://your-website.example",
215
+ secret: process.env.AGENT_ID_SITE_SECRET!,
216
+ sessionBinding: currentAuthenticatedSession.id,
217
+ };
218
+ const context = createBindingContext(settings);
219
+
220
+ // In the protected action, after normal authentication/authorization:
221
+ const declaration = await verifyDeclarationOnline(receiptFromRequest, {
222
+ ...settings,
223
+ apiUrl: "https://YOUR-COLLECTOR",
224
+ });
225
+ // All errors must deny/defer the action. A choice never replaces permissions.
226
+ ```
227
+
228
+ Return `context` from a no-store same-origin endpoint, then provide it to
229
+ `init({ ..., bindingContext: async () => ... })`. The widget sends it to the
230
+ collector; receipts are bound to that site, origin and session. Do not accept
231
+ the expected origin/session from arbitrary browser input. Do not embed a secret
232
+ or raw login cookie in HTML. Refresh the context after expiry.
233
+
234
+ Anonymous script-only receipts deliberately **cannot** pass server verification.
235
+ Online verification also checks revocation in storage. `verifyDeclaration`
236
+ performs offline signature/binding checks only and cannot see a later revocation.
237
+ The check and your application's action are separate operations; this package
238
+ does not provide a distributed transaction or replace your authorization system.
239
+
240
+ ## Browser and CSP support
241
+
242
+ The core uses standard DOM, fetch, Web Crypto and Shadow DOM, with no React or
243
+ WebMCP requirement. The automated suite targets Chromium, Firefox and WebKit.
244
+ Brave-specific settings and every browser version have not been exhaustively
245
+ tested. There is no dependency on third-party cookies.
246
+
247
+ For a restrictive Content Security Policy, allow the collector in `script-src`,
248
+ `style-src` and `connect-src`. Use external integration scripts or your existing
249
+ nonce policy. The widget uses an external stylesheet and requires no inline
250
+ script/style exceptions. Its `styleUrl` can point to your own copy of the CSS.
251
+ Versioned asset hashes are at `/sdk/v0.9.0/integrity.json`.
252
+
253
+ ## What's collected
254
+
255
+ Declarations keep the session ID, origin, choice and timestamps, plus an agent's own
256
+ name, provider and purpose when it gives them; token and binding values are hashed in
257
+ storage. Sessions are pruned after seven days and activity events after 30. The page
258
+ script, and the widget unless `data-page-views="off"` or `pageViews: false` is set,
259
+ record page views with the path only: no query string or fragment, and segments that
260
+ look like IDs or email addresses masked. Page titles, page text, screenshots and form
261
+ contents are never collected. A visitor's IP address is sent only with a request
262
+ whose user agent names an agent, checked against that agent's operator's published
263
+ addresses, and never stored.
264
+
265
+ ## Developing this package
266
+
267
+ See [development](https://github.com/Parallel-AI-Research/agent-identification/blob/main/docs/development.md),
268
+ [operating the collector](https://github.com/Parallel-AI-Research/agent-identification/blob/main/docs/operations.md)
269
+ and the [API contract](https://github.com/Parallel-AI-Research/agent-identification/blob/main/docs/protocol.md).
@@ -0,0 +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.")}})();
@@ -0,0 +1,94 @@
1
+ :host {
2
+ display: block;
3
+ color-scheme: light dark;
4
+ font-family: var(--agent-id-font, ui-sans-serif, system-ui, sans-serif);
5
+ font-size: 14px;
6
+ line-height: 1.5;
7
+ color: var(--agent-id-text, #17202a);
8
+ }
9
+
10
+ * {
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ .agent-identification {
15
+ padding: 20px;
16
+ border: 1px solid var(--agent-id-border, #d7dce1);
17
+ border-radius: var(--agent-id-radius, 12px);
18
+ background: var(--agent-id-background, #fff);
19
+ }
20
+
21
+ h2 {
22
+ margin: 0 0 6px;
23
+ font-size: 18px;
24
+ font-weight: 600;
25
+ line-height: 1.3;
26
+ }
27
+
28
+ p {
29
+ margin: 0 0 16px;
30
+ }
31
+
32
+ .choices {
33
+ display: flex;
34
+ flex-wrap: wrap;
35
+ gap: 8px;
36
+ }
37
+
38
+ button {
39
+ min-height: 44px;
40
+ padding: 10px 14px;
41
+ border: 1px solid var(--agent-id-button-border, #64748b);
42
+ border-radius: 6px;
43
+ background: var(--agent-id-button-background, #fff);
44
+ color: var(--agent-id-text, #17202a);
45
+ font: inherit;
46
+ cursor: pointer;
47
+ }
48
+
49
+ button:hover:not(:disabled) {
50
+ background: var(--agent-id-hover, #f1f5f9);
51
+ }
52
+
53
+ button:focus-visible {
54
+ outline: 3px solid var(--agent-id-focus, #2563eb);
55
+ outline-offset: 3px;
56
+ }
57
+
58
+ button:disabled {
59
+ cursor: default;
60
+ opacity: 0.65;
61
+ }
62
+
63
+ button[hidden] {
64
+ display: none;
65
+ }
66
+
67
+ .status {
68
+ margin: 14px 0 0;
69
+ overflow-wrap: anywhere;
70
+ }
71
+
72
+ .status + button {
73
+ margin-top: 12px;
74
+ }
75
+
76
+ @media (prefers-color-scheme: dark) {
77
+ :host {
78
+ color: var(--agent-id-text, #f1f5f9);
79
+ }
80
+
81
+ .agent-identification {
82
+ background: var(--agent-id-background, #17202a);
83
+ border-color: var(--agent-id-border, #64748b);
84
+ }
85
+
86
+ button {
87
+ background: var(--agent-id-button-background, #17202a);
88
+ color: var(--agent-id-text, #f1f5f9);
89
+ }
90
+
91
+ button:hover:not(:disabled) {
92
+ background: var(--agent-id-hover, #273445);
93
+ }
94
+ }
@@ -0,0 +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()}})();