@useparallel/agent-identification 0.0.0-stage → 0.8.2

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.
@@ -0,0 +1,492 @@
1
+ // src/server/tokens.ts
2
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
3
+
4
+ // src/protocol.ts
5
+ var CHOICES = ["ai_assistant", "person", "prefer_not_to_say"];
6
+ function isChoice(value) {
7
+ return typeof value === "string" && CHOICES.includes(value);
8
+ }
9
+ var OUTCOMES = ["attempted", "rejected", "confirmed"];
10
+ var ADDRESS_CHECKS = ["operator", "elsewhere"];
11
+ function isAddressCheck(value) {
12
+ return typeof value === "string" && ADDRESS_CHECKS.includes(value);
13
+ }
14
+ var MAX_PATH = 512;
15
+ var ID_SEGMENTS = [
16
+ /^\d+$/,
17
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
18
+ /^[0-9a-f]{12,}$/i,
19
+ /@/,
20
+ // Letters mixed with three or more digits, as in most generated IDs and tokens.
21
+ /^(?=(?:[^\d]*\d){3})(?=.*[a-z])[a-z0-9_-]{8,}$/i
22
+ ];
23
+ function routePath(path) {
24
+ const bare = path.replace(/[?#][\s\S]*$/, "");
25
+ const masked = bare.split("/").map((segment) => {
26
+ let text = segment;
27
+ try {
28
+ text = decodeURIComponent(segment);
29
+ } catch {
30
+ }
31
+ return ID_SEGMENTS.some((pattern) => pattern.test(text)) ? ":id" : segment;
32
+ }).join("/");
33
+ return (masked.startsWith("/") ? masked : `/${masked}`).slice(0, MAX_PATH);
34
+ }
35
+ function isReference(value) {
36
+ return typeof value === "string" && /^[A-Za-z0-9_.:-]{1,128}$/.test(value);
37
+ }
38
+ function isActionName(value) {
39
+ return typeof value === "string" && /^[a-z][a-z0-9_.:-]{0,63}$/.test(value);
40
+ }
41
+ var AGENT_USER_AGENT_TOKENS = [
42
+ "Claude-Agent",
43
+ "ChatGPT-User",
44
+ "OAI-SearchBot",
45
+ "GPTBot",
46
+ "Claude-User",
47
+ "Claude-SearchBot",
48
+ "ClaudeBot",
49
+ "Perplexity-User",
50
+ "PerplexityBot",
51
+ "MistralAI-User",
52
+ "DuckAssistBot"
53
+ ];
54
+ function userAgentSignals(userAgent) {
55
+ const found = [];
56
+ const token = AGENT_USER_AGENT_TOKENS.find(
57
+ (name) => new RegExp(`(^|[^A-Za-z-])${name}(/|;|\\s|$)`).test(userAgent)
58
+ );
59
+ if (token) found.push({ signal: "agent_user_agent", detail: token });
60
+ if (/HeadlessChrome\//.test(userAgent))
61
+ found.push({ signal: "headless_browser", detail: "HeadlessChrome" });
62
+ return found;
63
+ }
64
+ function isOutcome(value) {
65
+ return typeof value === "string" && OUTCOMES.includes(value);
66
+ }
67
+ var LINK_TIMING = "agent-id";
68
+ function isLink(value) {
69
+ return typeof value === "string" && /^[A-Za-z0-9_-]{22}$/.test(value);
70
+ }
71
+
72
+ // src/server/tokens.ts
73
+ var HEADER = Buffer.from(JSON.stringify({ alg: "HS256", typ: "AID", v: 1 })).toString(
74
+ "base64url"
75
+ );
76
+ var HASH = /^[a-f0-9]{64}$/;
77
+ var ID = /^[a-zA-Z0-9_-]{1,100}$/;
78
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
79
+ function isUuid(value) {
80
+ return typeof value === "string" && UUID.test(value);
81
+ }
82
+ function isSiteId(value) {
83
+ return typeof value === "string" && ID.test(value);
84
+ }
85
+ function isAllowedOrigin(value) {
86
+ if (typeof value !== "string") return false;
87
+ try {
88
+ const url = new URL(value);
89
+ return url.origin === value && !url.username && !url.password && (url.protocol === "https:" || url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname));
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+ function hashToken(token) {
95
+ return createHash("sha256").update(token).digest("hex");
96
+ }
97
+ function hashBinding(siteId, binding) {
98
+ if (typeof binding !== "string" || !binding || binding.length > 2048) {
99
+ throw new Error("sessionBinding must be an opaque, nonempty value");
100
+ }
101
+ return hashToken(JSON.stringify(["agent-identification-binding-v1", siteId, binding]));
102
+ }
103
+ function secretEquals(actual, expected) {
104
+ return timingSafeEqual(
105
+ Buffer.from(hashToken(actual), "hex"),
106
+ Buffer.from(hashToken(expected), "hex")
107
+ );
108
+ }
109
+ function seconds(now) {
110
+ if (!Number.isFinite(now.getTime())) throw new Error("Invalid current time");
111
+ return Math.floor(now.getTime() / 1e3);
112
+ }
113
+ function assertSecret(secret) {
114
+ if (typeof secret !== "string" || secret.length < 32 || secret.length > 1024) {
115
+ throw new Error("A server-only site secret of at least 32 characters is required");
116
+ }
117
+ }
118
+ function sign(claims, secret) {
119
+ assertSecret(secret);
120
+ const payload = Buffer.from(JSON.stringify(claims)).toString("base64url");
121
+ const signed = `${HEADER}.${payload}`;
122
+ const signature = createHmac("sha256", secret).update(signed).digest("base64url");
123
+ return `${signed}.${signature}`;
124
+ }
125
+ function verify(token, expected) {
126
+ assertSecret(expected.secret);
127
+ if (!isSiteId(expected.siteId) || !isAllowedOrigin(expected.origin)) {
128
+ throw new Error("Invalid expected site or origin");
129
+ }
130
+ if (typeof token !== "string" || token.length > 4096) {
131
+ throw new Error("Invalid signed token");
132
+ }
133
+ const parts = token.split(".");
134
+ if (parts.length !== 3 || parts[0] !== HEADER || !/^[A-Za-z0-9_-]+$/.test(parts[1]) || !/^[A-Za-z0-9_-]{43}$/.test(parts[2])) {
135
+ throw new Error("Invalid signed token");
136
+ }
137
+ const signature = createHmac("sha256", expected.secret).update(`${parts[0]}.${parts[1]}`).digest("base64url");
138
+ if (!secretEquals(signature, parts[2])) throw new Error("Invalid signed token");
139
+ let claims;
140
+ try {
141
+ claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
142
+ } catch {
143
+ throw new Error("Invalid signed token");
144
+ }
145
+ const now = seconds(expected.now ?? /* @__PURE__ */ new Date());
146
+ if (!claims || Array.isArray(claims) || claims.v !== 1 || claims.siteId !== expected.siteId || claims.origin !== expected.origin || !Number.isSafeInteger(claims.iat) || !Number.isSafeInteger(claims.exp) || claims.iat > now || claims.exp <= now || claims.exp <= claims.iat) {
147
+ throw new Error("Signed token is invalid or expired");
148
+ }
149
+ return claims;
150
+ }
151
+ function exactKeys(value, keys) {
152
+ return Object.keys(value).sort().join(",") === keys.sort().join(",");
153
+ }
154
+ function createBindingContext(options) {
155
+ const ttl = options.expiresInSeconds ?? 300;
156
+ if (!Number.isInteger(ttl) || ttl < 1 || ttl > 900) {
157
+ throw new Error("Binding context lifetime must be 1\u2013900 seconds");
158
+ }
159
+ if (!isSiteId(options.siteId) || !isAllowedOrigin(options.origin)) {
160
+ throw new Error("Invalid site or origin");
161
+ }
162
+ const iat = seconds(options.now ?? /* @__PURE__ */ new Date());
163
+ return sign(
164
+ {
165
+ v: 1,
166
+ purpose: "binding_context",
167
+ siteId: options.siteId,
168
+ origin: options.origin,
169
+ bindingHash: hashBinding(options.siteId, options.sessionBinding),
170
+ iat,
171
+ exp: iat + ttl
172
+ },
173
+ options.secret
174
+ );
175
+ }
176
+ function verifyDeclaration(receipt, expected) {
177
+ const c = verify(receipt, expected);
178
+ const expectedHash = hashBinding(expected.siteId, expected.sessionBinding);
179
+ if (c.purpose !== "declaration" || c.evidence !== "self_reported" || !isUuid(c.sessionId) || !isUuid(c.eventId) || !isChoice(c.choice) || typeof c.bindingHash !== "string" || !HASH.test(c.bindingHash) || !secretEquals(c.bindingHash, expectedHash) || c.exp - c.iat > 1800 || !exactKeys(c, [
180
+ "v",
181
+ "purpose",
182
+ "siteId",
183
+ "origin",
184
+ "bindingHash",
185
+ "iat",
186
+ "exp",
187
+ "sessionId",
188
+ "eventId",
189
+ "choice",
190
+ "evidence"
191
+ ])) {
192
+ throw new Error("Declaration does not match the active session");
193
+ }
194
+ return c;
195
+ }
196
+
197
+ // src/server/online.ts
198
+ async function verifyDeclarationOnline(receipt, options) {
199
+ const claims = verifyDeclaration(receipt, options);
200
+ const url = new URL(options.apiUrl);
201
+ if (!isAllowedOrigin(url.origin) || url.username || url.password || url.search || url.hash)
202
+ throw new Error("Invalid collector API URL");
203
+ url.pathname = `${url.pathname.replace(/\/$/, "")}/v1/verify`;
204
+ const response = await (options.fetch ?? globalThis.fetch)(url, {
205
+ method: "POST",
206
+ headers: {
207
+ authorization: `Bearer ${options.secret}`,
208
+ "content-type": "application/json"
209
+ },
210
+ credentials: "omit",
211
+ cache: "no-store",
212
+ redirect: "error",
213
+ signal: AbortSignal.timeout(1e4),
214
+ body: JSON.stringify({
215
+ siteId: options.siteId,
216
+ origin: options.origin,
217
+ sessionBinding: options.sessionBinding,
218
+ receipt
219
+ })
220
+ });
221
+ if (!response.ok) throw new Error("Declaration is not active or verification is unavailable");
222
+ const result = await response.json();
223
+ if (!result || typeof result !== "object" || !("valid" in result) || result.valid !== true || !("sessionId" in result) || result.sessionId !== claims.sessionId || !("eventId" in result) || result.eventId !== claims.eventId || !("choice" in result) || result.choice !== claims.choice || !("evidence" in result) || result.evidence !== "self_reported")
224
+ throw new Error("Invalid declaration verification response");
225
+ return claims;
226
+ }
227
+
228
+ // src/server/activity.ts
229
+ async function reportAction(input, options) {
230
+ const id = input.id ?? crypto.randomUUID();
231
+ const fail = (error) => {
232
+ (options.onError ?? ((e) => console.warn(`Action report failed: ${e.message}`)))(error);
233
+ return { status: "failed", id, error };
234
+ };
235
+ if (!isActionName(input.action)) return fail(new Error("Invalid action name"));
236
+ if (!isOutcome(input.outcome)) return fail(new Error("Invalid outcome"));
237
+ for (const field of ["resourceRef", "accountRef"]) {
238
+ if (input[field] !== void 0 && !isReference(input[field])) {
239
+ return fail(new Error(`${field} must be an opaque ID`));
240
+ }
241
+ }
242
+ let api;
243
+ try {
244
+ api = new URL(options.apiUrl);
245
+ } catch {
246
+ return fail(new Error("Invalid collector API URL"));
247
+ }
248
+ if (!isAllowedOrigin(api.origin) || api.username || api.password || api.search || api.hash) {
249
+ return fail(new Error("Invalid collector API URL"));
250
+ }
251
+ api.pathname = `${api.pathname.replace(/\/$/, "")}/v1/actions`;
252
+ const body = JSON.stringify({
253
+ siteId: options.siteId,
254
+ actions: [
255
+ {
256
+ id,
257
+ action: input.action,
258
+ outcome: input.outcome,
259
+ occurredAt: (input.occurredAt ?? /* @__PURE__ */ new Date()).toISOString(),
260
+ ...input.resourceRef ? { resourceRef: input.resourceRef } : {},
261
+ ...input.accountRef ? { accountRef: input.accountRef } : {},
262
+ ...input.sessionBinding ? { sessionBinding: input.sessionBinding } : {}
263
+ }
264
+ ]
265
+ });
266
+ for (let attempt = 0; ; attempt++) {
267
+ let retryable = true;
268
+ try {
269
+ const response = await (options.fetch ?? globalThis.fetch)(api, {
270
+ method: "POST",
271
+ headers: {
272
+ authorization: `Bearer ${options.secret}`,
273
+ "content-type": "application/json"
274
+ },
275
+ credentials: "omit",
276
+ cache: "no-store",
277
+ redirect: "error",
278
+ signal: AbortSignal.timeout(5e3),
279
+ body
280
+ });
281
+ if (!response.ok) {
282
+ retryable = response.status === 429 || response.status >= 500;
283
+ throw new Error(`The collector answered ${response.status}`);
284
+ }
285
+ const result = await response.json();
286
+ const match = result?.results?.find((item) => item.id === id);
287
+ if (!match || match.status !== "recorded" && match.status !== "duplicate") {
288
+ retryable = false;
289
+ throw new Error(
290
+ match?.status === "conflict" ? "This ID was already used for a different action" : "Invalid response from the collector"
291
+ );
292
+ }
293
+ return {
294
+ status: "recorded",
295
+ id,
296
+ sessionId: typeof match.sessionId === "string" ? match.sessionId : null,
297
+ duplicate: match.status === "duplicate"
298
+ };
299
+ } catch (error) {
300
+ const failure = error instanceof Error ? error : new Error("Action report failed");
301
+ if (!retryable || attempt >= 2) return fail(failure);
302
+ await new Promise((resolve) => setTimeout(resolve, 300 * 3 ** attempt));
303
+ }
304
+ }
305
+ }
306
+
307
+ // src/server/web-bot-auth.ts
308
+ var ASSET = /\.(?:m?js|css|map|png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf|mp4|webm)$/i;
309
+ function isAssetRequest(url, headers) {
310
+ const destination = headers["sec-fetch-dest"];
311
+ return destination !== void 0 && !["document", "iframe", "frame"].includes(destination) || ASSET.test(new URL(url, "https://placeholder.invalid").pathname);
312
+ }
313
+ function headerMap(request) {
314
+ const headers = {};
315
+ if (typeof Headers !== "undefined" && request.headers instanceof Headers) {
316
+ request.headers.forEach((value, name) => {
317
+ headers[name.toLowerCase()] = value;
318
+ });
319
+ return headers;
320
+ }
321
+ for (const [name, value] of Object.entries(
322
+ request.headers
323
+ )) {
324
+ if (value !== void 0) {
325
+ headers[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
326
+ }
327
+ }
328
+ return headers;
329
+ }
330
+ function requestUrl(request, headers) {
331
+ if (typeof Request !== "undefined" && request instanceof Request) return request.url;
332
+ const node = request;
333
+ const path = node.originalUrl ?? node.url ?? "/";
334
+ return `${node.protocol ?? "https"}://${headers.host ?? ""}${path}`;
335
+ }
336
+ async function reportSignedRequest(request, options) {
337
+ const fail = (error) => {
338
+ (options.onError ?? ((e) => console.warn(`Web Bot Auth report failed: ${e.message}`)))(error);
339
+ return { status: "failed", error };
340
+ };
341
+ try {
342
+ const headers = headerMap(request);
343
+ const input = headers["signature-input"];
344
+ if (!input || !headers.signature) return { status: "skipped" };
345
+ const url = options.url ?? requestUrl(request, headers);
346
+ if (!options.includeAssets && isAssetRequest(url, headers)) return { status: "skipped" };
347
+ const covered = /* @__PURE__ */ new Set(["signature", "signature-input", "signature-agent"]);
348
+ for (const match of input.matchAll(/"([a-z0-9-]+)"/g)) covered.add(match[1]);
349
+ const forwarded = Object.fromEntries(
350
+ [...covered].filter((name) => headers[name] !== void 0).map((name) => [name, headers[name]])
351
+ );
352
+ const api = new URL(options.apiUrl);
353
+ if (!isAllowedOrigin(api.origin) || api.username || api.password || api.search || api.hash) {
354
+ return fail(new Error("Invalid collector API URL"));
355
+ }
356
+ api.pathname = `${api.pathname.replace(/\/$/, "")}/v1/signatures`;
357
+ const response = await (options.fetch ?? globalThis.fetch)(api, {
358
+ method: "POST",
359
+ headers: {
360
+ authorization: `Bearer ${options.secret}`,
361
+ "content-type": "application/json"
362
+ },
363
+ credentials: "omit",
364
+ cache: "no-store",
365
+ redirect: "error",
366
+ signal: AbortSignal.timeout(5e3),
367
+ body: JSON.stringify({
368
+ siteId: options.siteId,
369
+ request: {
370
+ method: (request.method ?? "GET").toUpperCase(),
371
+ url,
372
+ headers: forwarded
373
+ },
374
+ ...options.sessionId ? { sessionId: options.sessionId } : {},
375
+ ...options.accountRef ? { accountRef: options.accountRef } : {},
376
+ ...options.link ? { link: options.link } : {}
377
+ })
378
+ });
379
+ if (!response.ok) return fail(new Error(`The collector answered ${response.status}`));
380
+ const result = await response.json();
381
+ const event = result?.event;
382
+ if (!event || event.type !== "signature" || typeof event.id !== "string") {
383
+ return fail(new Error("Invalid response from the collector"));
384
+ }
385
+ return { status: "recorded", event };
386
+ } catch (error) {
387
+ return fail(error instanceof Error ? error : new Error("Web Bot Auth report failed"));
388
+ }
389
+ }
390
+
391
+ // src/server/signals.ts
392
+ import { isIP } from "node:net";
393
+ async function reportRequestSignals(request, options) {
394
+ const fail = (error) => {
395
+ (options.onError ?? ((e) => console.warn(`Signal report failed: ${e.message}`)))(error);
396
+ return { status: "failed", error };
397
+ };
398
+ try {
399
+ const headers = headerMap(request);
400
+ const found = userAgentSignals(headers["user-agent"] ?? "");
401
+ if (!found.length || isAssetRequest(requestUrl(request, headers), headers)) {
402
+ return { status: "skipped" };
403
+ }
404
+ const api = new URL(options.apiUrl);
405
+ if (!isAllowedOrigin(api.origin) || api.username || api.password || api.search || api.hash) {
406
+ return fail(new Error("Invalid collector API URL"));
407
+ }
408
+ api.pathname = `${api.pathname.replace(/\/$/, "")}/v1/signals`;
409
+ const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
410
+ const ip = typeof options.clientIp === "string" ? options.clientIp.trim() : "";
411
+ const address = ip && isIP(ip.replace(/^\[|\]$/g, "").replace(/%.*$/, "")) ? ip : null;
412
+ const signals = found.map((item) => ({
413
+ id: crypto.randomUUID(),
414
+ ...item,
415
+ ...address && item.signal === "agent_user_agent" ? { ip: address } : {},
416
+ occurredAt,
417
+ ...options.sessionBinding ? { sessionBinding: options.sessionBinding } : {},
418
+ ...options.link ? { link: options.link } : {}
419
+ }));
420
+ for (let attempt = 0; ; attempt++) {
421
+ let retryable = true;
422
+ try {
423
+ const response = await (options.fetch ?? globalThis.fetch)(api, {
424
+ method: "POST",
425
+ headers: {
426
+ authorization: `Bearer ${options.secret}`,
427
+ "content-type": "application/json"
428
+ },
429
+ credentials: "omit",
430
+ cache: "no-store",
431
+ redirect: "error",
432
+ signal: AbortSignal.timeout(5e3),
433
+ body: JSON.stringify({ siteId: options.siteId, signals })
434
+ });
435
+ if (!response.ok) {
436
+ retryable = response.status === 429 || response.status >= 500;
437
+ throw new Error(`The collector answered ${response.status}`);
438
+ }
439
+ const result = await response.json();
440
+ const confirmed = signals.every(
441
+ (signal) => result?.results?.some(
442
+ (item) => item.id === signal.id && (item.status === "recorded" || item.status === "duplicate")
443
+ )
444
+ );
445
+ if (!confirmed) {
446
+ retryable = false;
447
+ throw new Error("Invalid response from the collector");
448
+ }
449
+ return {
450
+ status: "recorded",
451
+ signals: found.map((item, index) => {
452
+ const answer = result.results.find((entry) => entry.id === signals[index].id);
453
+ return { ...item, address: isAddressCheck(answer?.address) ? answer.address : null };
454
+ })
455
+ };
456
+ } catch (error) {
457
+ const failure = error instanceof Error ? error : new Error("Signal report failed");
458
+ if (!retryable || attempt >= 2) return fail(failure);
459
+ await new Promise((resolve) => setTimeout(resolve, 300 * 3 ** attempt));
460
+ }
461
+ }
462
+ } catch (error) {
463
+ return fail(error instanceof Error ? error : new Error("Signal report failed"));
464
+ }
465
+ }
466
+
467
+ // src/server/links.ts
468
+ function reportLink(request, options = {}) {
469
+ const headers = headerMap(request);
470
+ const signed = Boolean(headers["signature-input"] && headers.signature);
471
+ if (!signed && !userAgentSignals(headers["user-agent"] ?? "").length) return null;
472
+ if (isAssetRequest(options.url ?? requestUrl(request, headers), headers)) return null;
473
+ return Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString("base64url");
474
+ }
475
+ function serverTiming(link) {
476
+ return `${LINK_TIMING};desc="${link}"`;
477
+ }
478
+ export {
479
+ AGENT_USER_AGENT_TOKENS,
480
+ LINK_TIMING,
481
+ createBindingContext,
482
+ isLink,
483
+ reportAction,
484
+ reportLink,
485
+ reportRequestSignals,
486
+ reportSignedRequest,
487
+ routePath,
488
+ serverTiming,
489
+ userAgentSignals,
490
+ verifyDeclaration,
491
+ verifyDeclarationOnline
492
+ };
@@ -0,0 +1,40 @@
1
+ import { type Choice, type Mode, type RecordedDeclaration, type SessionStatus } from "../protocol.js";
2
+ export type { Choice, Mode, RecordedDeclaration, SessionStatus };
3
+ export { track, type Tracker, type TrackerOptions } from "./track.js";
4
+ export interface WidgetOptions {
5
+ container: HTMLElement | string;
6
+ siteId: string;
7
+ apiUrl: string;
8
+ mode?: Mode;
9
+ gate?: HTMLElement | string;
10
+ styleUrl?: string;
11
+ bindingContext?: string | (() => Promise<string>);
12
+ storage?: "session" | "memory";
13
+ onRecorded?: (declaration: RecordedDeclaration) => void;
14
+ onError?: (error: Error) => void;
15
+ /**
16
+ * Records a page view once the session is ready, and again after reset. Only the path is
17
+ * sent, without its query or fragment and with parts that look like IDs masked. Defaults to
18
+ * true.
19
+ */
20
+ pageViews?: boolean;
21
+ /** Your route pattern for this page, such as /tickets/:id, for parts masking can't catch. */
22
+ route?: string;
23
+ /**
24
+ * Reports whether this browser is automated (navigator.webdriver) or headless, as heuristic
25
+ * evidence that never labels a visitor an AI agent. Off by default.
26
+ */
27
+ automationSignals?: boolean;
28
+ }
29
+ export interface Widget {
30
+ readonly ready: Promise<SessionStatus>;
31
+ getState(): SessionStatus | null;
32
+ reset(): Promise<SessionStatus>;
33
+ /** Records a page view, for apps that change pages without reloading. */
34
+ pageView(options?: {
35
+ route?: string;
36
+ }): Promise<void>;
37
+ destroy(): void;
38
+ }
39
+ /** Mount a self-reported visitor choice. Required mode controls only the supplied UI gate. */
40
+ export declare function init(options: WidgetOptions): Widget;
@@ -0,0 +1,8 @@
1
+ import { init } from "./index.js";
2
+ declare global {
3
+ interface Window {
4
+ AgentIdentification: {
5
+ init: typeof init;
6
+ };
7
+ }
8
+ }
@@ -0,0 +1,38 @@
1
+ /** Session and request code shared by the declaration widget and the page tracker. */
2
+ import type { Signal } from "../protocol.js";
3
+ export interface StoredSession {
4
+ token: string;
5
+ sessionId: string;
6
+ expiresAt: string;
7
+ }
8
+ export interface Target {
9
+ apiUrl: string;
10
+ siteId: string;
11
+ origin: string;
12
+ }
13
+ export declare class HttpError extends Error {
14
+ readonly status: number;
15
+ constructor(status: number);
16
+ }
17
+ export declare function endpoint(value: string, name: string): string;
18
+ export declare function siteId(value: string): string;
19
+ export declare function pageOrigin(): string;
20
+ export declare function record(value: unknown): value is Record<string, unknown>;
21
+ export declare function opaque(value: unknown, min?: number, max?: number): value is string;
22
+ export declare function uuid(value: unknown): value is string;
23
+ export declare function sessionToken(value: unknown): value is string;
24
+ export declare function futureDate(value: unknown): value is string;
25
+ export declare function readStored(value: unknown): StoredSession | null;
26
+ export declare function storageKey(target: Target, context: string | undefined): Promise<string>;
27
+ export declare function loadSession(key: string, storage: "session" | "memory"): StoredSession | null;
28
+ export declare function persistSession(key: string, storage: "session" | "memory", value: StoredSession | null): void;
29
+ export declare function asError(value: unknown): Error;
30
+ /** One collector request. Anything but a successful, well-formed response throws. */
31
+ export declare function send(target: Target, operation: AbortSignal, path: string, method: string, body?: unknown, token?: string): Promise<unknown>;
32
+ /** Whether the collector confirmed that exact event ID as recorded, now or before. */
33
+ export declare function confirmed(body: unknown, id: string): boolean;
34
+ /** What this browser shows of automation: evidence, never identity. */
35
+ export declare function automationSignals(): {
36
+ signal: Signal;
37
+ detail: string;
38
+ }[];
@@ -0,0 +1,8 @@
1
+ import { track } from "./track.js";
2
+ declare global {
3
+ interface Window {
4
+ AgentIdentificationTracker: {
5
+ track: typeof track;
6
+ };
7
+ }
8
+ }
@@ -0,0 +1,34 @@
1
+ export interface TrackerOptions {
2
+ siteId: string;
3
+ apiUrl: string;
4
+ /** Where the tab's session is kept: sessionStorage (the default) or memory only. */
5
+ storage?: "session" | "memory";
6
+ /** Your route pattern for this page, such as /tickets/:id, for parts masking can't catch. */
7
+ route?: string;
8
+ /**
9
+ * Records a page view whenever the path changes without a reload, through the History API or
10
+ * the back and forward buttons. Defaults to true; turn it off if you call pageView() yourself.
11
+ */
12
+ navigation?: boolean;
13
+ /**
14
+ * Reports whether this browser is automated (navigator.webdriver) or headless, once per
15
+ * session, as heuristic evidence that never labels a visitor an AI agent. Defaults to true.
16
+ */
17
+ automationSignals?: boolean;
18
+ onError?: (error: Error) => void;
19
+ }
20
+ export interface Tracker {
21
+ /** Resolves once this page load's page view is confirmed, and rejects if it couldn't be. */
22
+ readonly ready: Promise<void>;
23
+ /** Records a page view, for apps that change pages without the History API. */
24
+ pageView(options?: {
25
+ route?: string;
26
+ }): Promise<void>;
27
+ /** Stops recording. It never ends the session or removes anything already recorded. */
28
+ destroy(): void;
29
+ }
30
+ /**
31
+ * Records this page's views and automation signals for one site, without drawing anything.
32
+ * Calling it again for the same site returns the same tracker.
33
+ */
34
+ export declare function track(options: TrackerOptions): Tracker;