@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 +21 -0
- package/README.md +268 -2
- package/dist/agent-id-track.js +1 -0
- package/dist/agent-id.css +94 -0
- package/dist/agent-id.js +1 -0
- package/dist/browser/index.js +894 -0
- package/dist/react/index.js +646 -0
- package/dist/server/index.js +603 -0
- package/dist/types/browser/index.d.ts +40 -0
- package/dist/types/browser/script.d.ts +8 -0
- package/dist/types/browser/shared.d.ts +38 -0
- package/dist/types/browser/track-script.d.ts +8 -0
- package/dist/types/browser/track.d.ts +39 -0
- package/dist/types/protocol.d.ts +189 -0
- package/dist/types/react/index.d.ts +7 -0
- package/dist/types/server/activity.d.ts +40 -0
- package/dist/types/server/index.d.ts +16 -0
- package/dist/types/server/links.d.ts +12 -0
- package/dist/types/server/online.d.ts +13 -0
- package/dist/types/server/setup.d.ts +60 -0
- package/dist/types/server/signals.d.ts +50 -0
- package/dist/types/server/tokens.d.ts +59 -0
- package/dist/types/server/web-bot-auth.d.ts +50 -0
- package/package.json +95 -4
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
|
-
#
|
|
1
|
+
# Agent Identification
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
}
|
package/dist/agent-id.js
ADDED
|
@@ -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()}})();
|