@aamp/protocol 1.1.2 → 1.1.3

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/src/publisher.ts CHANGED
@@ -1,107 +1,303 @@
1
- /**
2
- * Layer 2: Publisher Middleware
3
- * Used by content owners to enforce policy and log access.
4
- */
5
- import { AccessPolicy, AccessPurpose, SignedAccessRequest, ContentOrigin, FeedbackSignal } from './types';
6
- import { verifySignature, signData } from './crypto';
7
- import { MAX_CLOCK_SKEW_MS, HEADERS } from './constants';
8
-
9
- export interface VerificationResult {
10
- allowed: boolean;
11
- reason: string;
12
- responseHeaders?: Record<string, string>;
13
- }
14
-
15
- export class AAMPPublisher {
16
- private policy: AccessPolicy;
17
- private keyPair: CryptoKeyPair | null = null;
18
-
19
- constructor(policy: AccessPolicy) {
20
- this.policy = policy;
21
- }
22
-
23
- // Publishers now need keys too, to sign their Content Origin declarations
24
- async initialize(keyPair: CryptoKeyPair) {
25
- this.keyPair = keyPair;
26
- }
27
-
28
- getPolicy(): AccessPolicy {
29
- return this.policy;
30
- }
31
-
32
- /**
33
- * Verifies an incoming AI access request.
34
- *
35
- * @param request The parsed request object
36
- * @param requestPublicKey The agent's public key
37
- * @param rawPayload (Optional) The raw string received over the wire. REQUIRED for robust verification.
38
- */
39
- async verifyRequest(
40
- request: SignedAccessRequest,
41
- requestPublicKey: CryptoKey,
42
- rawPayload?: string
43
- ): Promise<VerificationResult> {
44
-
45
- // 1. Replay Attack Prevention (Timestamp Check)
46
- const requestTime = new Date(request.header.ts).getTime();
47
- const now = Date.now();
48
- if (Math.abs(now - requestTime) > MAX_CLOCK_SKEW_MS) {
49
- return { allowed: false, reason: 'TIMESTAMP_INVALID: Clock skew exceeded.' };
50
- }
51
-
52
- // 2. Policy Enforcement (Usage Type)
53
- // STRICT CHECK: Training
54
- if (request.header.purpose === AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
55
- return { allowed: false, reason: 'POLICY_DENIED: Training not allowed by site owner.' };
56
- }
57
- // STRICT CHECK: RAG / Retrieval
58
- if (request.header.purpose === AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
59
- return { allowed: false, reason: 'POLICY_DENIED: RAG Retrieval not allowed.' };
60
- }
61
-
62
- // 3. Economic Policy Enforcement
63
- if (this.policy.requiresPayment) {
64
- const isExemptViaAds = this.policy.allowAdSupportedAccess && request.header.context.ads_displayed;
65
-
66
- if (!isExemptViaAds) {
67
- return {
68
- allowed: false,
69
- reason: 'PAYMENT_REQUIRED: Site requires payment or ad-supported access.'
70
- };
71
- }
72
- }
73
-
74
- // 4. Cryptographic Verification (The "Proof")
75
- // CRITICAL: We prefer the rawPayload if available to avoid JSON parsing/stringify mismatches.
76
- const signableString = rawPayload || JSON.stringify(request.header);
77
-
78
- const isValid = await verifySignature(requestPublicKey, signableString, request.signature);
79
-
80
- if (!isValid) {
81
- return { allowed: false, reason: 'CRYPTO_FAIL: Signature verification failed.' };
82
- }
83
-
84
- return { allowed: true, reason: 'OK' };
85
- }
86
-
87
- /**
88
- * Verifies a Feedback Signal (Spam Report) from an Agent.
89
- * Part of the AAMP Immune System.
90
- */
91
- async verifyFeedback(signal: FeedbackSignal, signature: string, agentPublicKey: CryptoKey): Promise<boolean> {
92
- const signableString = JSON.stringify(signal);
93
- return await verifySignature(agentPublicKey, signableString, signature);
94
- }
95
-
96
- async generateResponseHeaders(origin: ContentOrigin): Promise<Record<string, string>> {
97
- if (!this.keyPair) throw new Error("Publisher keys not initialized");
98
-
99
- const payload = JSON.stringify({ origin, ts: Date.now() });
100
- const signature = await signData(this.keyPair.privateKey, payload);
101
-
102
- return {
103
- [HEADERS.CONTENT_ORIGIN]: origin,
104
- [HEADERS.PROVENANCE_SIG]: signature
105
- };
106
- }
1
+ /**
2
+ * Layer 2: Publisher Middleware
3
+ * Used by content owners to enforce policy, log access, and filter bots.
4
+ */
5
+ import { AccessPolicy, AccessPurpose, SignedAccessRequest, ContentOrigin, FeedbackSignal, AgentIdentityManifest, EvaluationResult, UnauthenticatedStrategy, IdentityCache } from './types';
6
+ import { verifySignature, signData, exportPublicKey } from './crypto';
7
+ import { MAX_CLOCK_SKEW_MS, HEADERS, WELL_KNOWN_AGENT_PATH } from './constants';
8
+
9
+ interface VerificationResult {
10
+ allowed: boolean;
11
+ reason: string;
12
+ identityVerified: boolean;
13
+ }
14
+
15
+ /**
16
+ * Default In-Memory Cache (Fallback only)
17
+ * NOT recommended for high-traffic Serverless production.
18
+ */
19
+ class MemoryCache implements IdentityCache {
20
+ private store = new Map<string, { val: string, exp: number }>();
21
+
22
+ async get(key: string): Promise<string | null> {
23
+ const item = this.store.get(key);
24
+ if (!item) return null;
25
+ if (Date.now() > item.exp) {
26
+ this.store.delete(key);
27
+ return null;
28
+ }
29
+ return item.val;
30
+ }
31
+
32
+ async set(key: string, value: string, ttlSeconds: number): Promise<void> {
33
+ this.store.set(key, {
34
+ val: value,
35
+ exp: Date.now() + (ttlSeconds * 1000)
36
+ });
37
+ }
38
+ }
39
+
40
+ export class AAMPPublisher {
41
+ private policy: AccessPolicy;
42
+ private keyPair: CryptoKeyPair | null = null;
43
+ private unauthenticatedStrategy: UnauthenticatedStrategy;
44
+ private cache: IdentityCache;
45
+
46
+ // Default TTL: 1 Hour
47
+ private readonly CACHE_TTL_SECONDS = 3600;
48
+
49
+ constructor(
50
+ policy: AccessPolicy,
51
+ strategy: UnauthenticatedStrategy = 'PASSIVE',
52
+ cacheImpl?: IdentityCache
53
+ ) {
54
+ this.policy = policy;
55
+ this.unauthenticatedStrategy = strategy;
56
+ this.cache = cacheImpl || new MemoryCache();
57
+ }
58
+
59
+ async initialize(keyPair: CryptoKeyPair) {
60
+ this.keyPair = keyPair;
61
+ }
62
+
63
+ getPolicy(): AccessPolicy {
64
+ return this.policy;
65
+ }
66
+
67
+ /**
68
+ * Main Entry Point: Evaluate ANY visitor (Human, Bot, or Agent)
69
+ */
70
+ async evaluateVisitor(
71
+ reqHeaders: Record<string, string | undefined>,
72
+ rawPayload?: string
73
+ ): Promise<EvaluationResult> {
74
+
75
+ // 1. Check for AAMP Headers
76
+ const hasAamp = reqHeaders[HEADERS.PAYLOAD] && reqHeaders[HEADERS.SIGNATURE] && reqHeaders[HEADERS.PUBLIC_KEY];
77
+
78
+ if (hasAamp) {
79
+ // It claims to be an Agent. Verify it.
80
+ return await this.handleAgent(reqHeaders, rawPayload);
81
+ }
82
+
83
+ // 2. It's not an AAMP Agent. Apply Strategy.
84
+ if (this.unauthenticatedStrategy === 'STRICT') {
85
+ return {
86
+ allowed: false,
87
+ status: 401,
88
+ reason: "STRICT_MODE: Only AAMP verified agents allowed.",
89
+ visitorType: 'UNIDENTIFIED_BOT'
90
+ };
91
+ }
92
+
93
+ if (this.unauthenticatedStrategy === 'PASSIVE') {
94
+ return {
95
+ allowed: true,
96
+ status: 200,
97
+ reason: "PASSIVE_MODE: Allowed without verification.",
98
+ visitorType: 'LIKELY_HUMAN'
99
+ };
100
+ }
101
+
102
+ // 3. HYBRID MODE: Heuristic Analysis (The "Lazy Bot" Filter)
103
+ const isHuman = this.performBrowserHeuristics(reqHeaders);
104
+
105
+ if (isHuman) {
106
+ return {
107
+ allowed: true,
108
+ status: 200,
109
+ reason: "BROWSER_VERIFIED: Heuristics passed.",
110
+ visitorType: 'LIKELY_HUMAN'
111
+ };
112
+ } else {
113
+ return {
114
+ allowed: false,
115
+ status: 403,
116
+ reason: "BOT_DETECTED: Request lacks browser signatures and AAMP headers.",
117
+ visitorType: 'UNIDENTIFIED_BOT'
118
+ };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Browser Heuristics (Hardened)
124
+ * 1. Checks Known Bot Signatures (Fast Fail)
125
+ * 2. Checks Trusted Upstream Signals (Cloudflare/Vercel)
126
+ * 3. Checks Browser Header Consistency
127
+ */
128
+ private performBrowserHeuristics(headers: Record<string, string | undefined>): boolean {
129
+ const userAgent = headers['user-agent'] || '';
130
+
131
+ // A. The "Obvious Bot" Blocklist (Fast Fail)
132
+ const botSignatures = ['python-requests', 'curl', 'wget', 'scrapy', 'bot', 'crawler', 'spider'];
133
+ // Exception: Googlebot (if you want SEO). We'll treat Googlebot as a bot,
134
+ // real implementations might white-list it via IP verification (not possible in just JS headers).
135
+ if (botSignatures.some(sig => userAgent.toLowerCase().includes(sig))) {
136
+ return false;
137
+ }
138
+
139
+ // B. Trusted Infrastructure Signals (The Real World Solution)
140
+ // If Cloudflare or Vercel says "This is a real user", we trust them.
141
+ // Cloudflare: 'cf-visitor' exists. 'cf-ipcountry' exists.
142
+ if (headers['cf-visitor'] || headers['cf-ray']) return true;
143
+
144
+ // Vercel: 'x-vercel-id'
145
+ if (headers['x-vercel-id']) return true;
146
+
147
+ // AWS CloudFront: 'cloudfront-viewer-address'
148
+ if (headers['cloudfront-viewer-address']) return true;
149
+
150
+ // C. The "Browser Fingerprint" (Fallback for direct connections)
151
+ // Real browsers almost always send these headers
152
+ const hasAcceptLanguage = !!headers['accept-language'];
153
+ const hasSecFetchDest = !!headers['sec-fetch-dest'];
154
+ const hasUpgradeInsecure = !!headers['upgrade-insecure-requests'];
155
+
156
+ // If it has typical browser headers, we allow it.
157
+ if (hasAcceptLanguage && (hasSecFetchDest || hasUpgradeInsecure)) {
158
+ return true;
159
+ }
160
+
161
+ // If it has no browser headers and no trusted proxy headers -> It's likely a script.
162
+ return false;
163
+ }
164
+
165
+ /**
166
+ * Handle AAMP Protocol Logic
167
+ */
168
+ private async handleAgent(reqHeaders: Record<string, string | undefined>, rawPayload?: string): Promise<EvaluationResult> {
169
+ try {
170
+ const payloadHeader = reqHeaders[HEADERS.PAYLOAD]!;
171
+ const sigHeader = reqHeaders[HEADERS.SIGNATURE]!;
172
+ const keyHeader = reqHeaders[HEADERS.PUBLIC_KEY]!;
173
+
174
+ const headerJson = atob(payloadHeader);
175
+ const requestHeader = JSON.parse(headerJson);
176
+
177
+ const signedRequest: SignedAccessRequest = {
178
+ header: requestHeader,
179
+ signature: sigHeader,
180
+ publicKey: keyHeader
181
+ };
182
+
183
+ const agentKey = await crypto.subtle.importKey(
184
+ "spki",
185
+ new Uint8Array(atob(keyHeader).split('').map(c => c.charCodeAt(0))),
186
+ { name: "ECDSA", namedCurve: "P-256" },
187
+ true,
188
+ ["verify"]
189
+ );
190
+
191
+ // Verify Core Logic
192
+ const result = await this.verifyRequestLogic(signedRequest, agentKey, headerJson);
193
+
194
+ if (!result.allowed) {
195
+ return {
196
+ allowed: false,
197
+ status: 403,
198
+ reason: result.reason,
199
+ visitorType: 'VERIFIED_AGENT'
200
+ };
201
+ }
202
+
203
+ return {
204
+ allowed: true,
205
+ status: 200,
206
+ reason: "AAMP_VERIFIED",
207
+ visitorType: 'VERIFIED_AGENT',
208
+ metadata: requestHeader
209
+ };
210
+
211
+ } catch (e) {
212
+ return { allowed: false, status: 400, reason: "INVALID_SIGNATURE", visitorType: 'UNIDENTIFIED_BOT' };
213
+ }
214
+ }
215
+
216
+ private async verifyRequestLogic(
217
+ request: SignedAccessRequest,
218
+ requestPublicKey: CryptoKey,
219
+ rawPayload?: string
220
+ ): Promise<VerificationResult> {
221
+
222
+ // 1. Replay Attack Prevention
223
+ const requestTime = new Date(request.header.ts).getTime();
224
+ if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
225
+ return { allowed: false, reason: 'TIMESTAMP_INVALID', identityVerified: false };
226
+ }
227
+
228
+ // 2. Crypto Verification
229
+ const signableString = rawPayload || JSON.stringify(request.header);
230
+ const isCryptoValid = await verifySignature(requestPublicKey, signableString, request.signature);
231
+ if (!isCryptoValid) return { allowed: false, reason: 'CRYPTO_FAIL', identityVerified: false };
232
+
233
+ // 3. Identity Verification (DNS Binding) with Cache
234
+ let identityVerified = false;
235
+ const claimedDomain = request.header.agent_id;
236
+ const pubKeyString = await exportPublicKey(requestPublicKey);
237
+
238
+ // Check Cache First
239
+ const cachedKey = await this.cache.get(claimedDomain);
240
+
241
+ if (cachedKey === pubKeyString) {
242
+ identityVerified = true;
243
+ } else if (this.isDomain(claimedDomain)) {
244
+ // Cache Miss: Perform DNS Fetch
245
+ identityVerified = await this.verifyDnsBinding(claimedDomain, pubKeyString);
246
+ if (identityVerified) {
247
+ await this.cache.set(claimedDomain, pubKeyString, this.CACHE_TTL_SECONDS);
248
+ }
249
+ }
250
+
251
+ if (this.policy.requireIdentityBinding && !identityVerified) {
252
+ return { allowed: false, reason: 'IDENTITY_FAIL: DNS Binding could not be verified.', identityVerified: false };
253
+ }
254
+
255
+ // 4. Policy Check: Purpose
256
+ if (request.header.purpose === AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
257
+ return { allowed: false, reason: 'POLICY_DENIED: Training not allowed.', identityVerified };
258
+ }
259
+ if (request.header.purpose === AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
260
+ return { allowed: false, reason: 'POLICY_DENIED: RAG not allowed.', identityVerified };
261
+ }
262
+
263
+ // 5. Policy Check: Economics
264
+ if (this.policy.requiresPayment) {
265
+ const isAdExempt = this.policy.allowAdSupportedAccess && request.header.context.ads_displayed;
266
+ if (!isAdExempt) {
267
+ return { allowed: false, reason: 'PAYMENT_REQUIRED: Content requires payment or ads.', identityVerified };
268
+ }
269
+ }
270
+
271
+ return { allowed: true, reason: 'OK', identityVerified };
272
+ }
273
+
274
+ private async verifyDnsBinding(domain: string, requestKeySpki: string): Promise<boolean> {
275
+ try {
276
+ const url = `https://${domain}${WELL_KNOWN_AGENT_PATH}`;
277
+ // In production, we need a short timeout to prevent hanging
278
+ const controller = new AbortController();
279
+ const timeoutId = setTimeout(() => controller.abort(), 1500); // 1.5s max for DNS check
280
+
281
+ const response = await fetch(url, { signal: controller.signal });
282
+ clearTimeout(timeoutId);
283
+
284
+ if (!response.ok) return false;
285
+ const manifest = await response.json() as AgentIdentityManifest;
286
+ return manifest.agent_id === domain && manifest.public_key === requestKeySpki;
287
+ } catch { return false; }
288
+ }
289
+
290
+ private isDomain(s: string): boolean {
291
+ return /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(s);
292
+ }
293
+
294
+ async generateResponseHeaders(origin: ContentOrigin): Promise<Record<string, string>> {
295
+ if (!this.keyPair) throw new Error("Publisher keys not initialized");
296
+ const payload = JSON.stringify({ origin, ts: Date.now() });
297
+ const signature = await signData(this.keyPair.privateKey, payload);
298
+ return {
299
+ [HEADERS.CONTENT_ORIGIN]: origin,
300
+ [HEADERS.PROVENANCE_SIG]: signature
301
+ };
302
+ }
107
303
  }
package/src/types.ts CHANGED
@@ -1,98 +1,113 @@
1
- /**
2
- * Layer 1: Protocol Definitions
3
- * Shared types used by both Agent and Publisher.
4
- */
5
-
6
- export enum AccessPurpose {
7
- CRAWL_TRAINING = 'CRAWL_TRAINING',
8
- RAG_RETRIEVAL = 'RAG_RETRIEVAL',
9
- SUMMARY = 'SUMMARY',
10
- QUOTATION = 'QUOTATION',
11
- EMBEDDING = 'EMBEDDING'
12
- }
13
-
14
- export enum ContentOrigin {
15
- HUMAN = 'HUMAN', // Created by humans. High training value.
16
- SYNTHETIC = 'SYNTHETIC', // Created by AI. Risk of model collapse.
17
- HYBRID = 'HYBRID' // Edited by humans, drafted by AI.
18
- }
19
-
20
- export enum QualityFlag {
21
- SEO_SPAM = 'SEO_SPAM',
22
- INACCURATE = 'INACCURATE',
23
- HATE_SPEECH = 'HATE_SPEECH',
24
- HIGH_QUALITY = 'HIGH_QUALITY'
25
- }
26
-
27
- /**
28
- * Optional Rate Limiting (The Speed Limit)
29
- * Defines technical boundaries for the handshake.
30
- */
31
- export interface RateLimitConfig {
32
- requestsPerMinute: number;
33
- tokensPerMinute?: number;
34
- }
35
-
36
- /**
37
- * Optional Monetization (The Settlement Layer)
38
- *
39
- * AAMP is neutral. Settlement can happen via:
40
- * 1. BROKER: A 3rd party clearing house (e.g., "AI-AdSense").
41
- * 2. CRYPTO: Direct on-chain settlement.
42
- * 3. TREATY: A private legal contract signed offline (Enterprise).
43
- */
44
- export interface MonetizationConfig {
45
- method: 'BROKER' | 'CRYPTO' | 'PRIVATE_TREATY';
46
- /**
47
- * The destination for settlement.
48
- * - If BROKER: The API URL of the clearing house.
49
- * - If CRYPTO: The wallet address.
50
- * - If TREATY: The Contract ID or "Contact Sales".
51
- */
52
- location: string;
53
- }
54
-
55
- export interface AccessPolicy {
56
- version: '1.1';
57
- allowTraining: boolean;
58
- allowRAG: boolean;
59
- attributionRequired: boolean;
60
-
61
- // Economic Signals
62
- allowAdSupportedAccess: boolean; // If true, Agents showing ads are exempt from payment
63
- requiresPayment: boolean; // If true, Access is denied unless Ad-Supported condition is met
64
- paymentPointer?: string; // @deprecated (Legacy support)
65
-
66
- // V1.1: Optional Traffic Control
67
- rateLimit?: RateLimitConfig;
68
-
69
- // V1.1: Optional Settlement Info
70
- // If undefined, parties are assumed to have no economic relationship or settled offline.
71
- monetization?: MonetizationConfig;
72
- }
73
-
74
- export interface ProtocolHeader {
75
- v: '1.1';
76
- ts: string;
77
- agent_id: string;
78
- resource: string;
79
- purpose: AccessPurpose;
80
- // Access Context
81
- context: {
82
- ads_displayed: boolean; // Is the AI Agent displaying ads alongside this content?
83
- };
84
- }
85
-
86
- export interface SignedAccessRequest {
87
- header: ProtocolHeader;
88
- signature: string;
89
- publicKey?: string;
90
- }
91
-
92
- export interface FeedbackSignal {
93
- target_resource: string;
94
- agent_id: string;
95
- quality_score: number; // 0.0 to 1.0
96
- flags: QualityFlag[];
97
- timestamp: string;
1
+ /**
2
+ * Layer 1: Protocol Definitions
3
+ * Shared types used by both Agent and Publisher.
4
+ */
5
+
6
+ export enum AccessPurpose {
7
+ CRAWL_TRAINING = 'CRAWL_TRAINING',
8
+ RAG_RETRIEVAL = 'RAG_RETRIEVAL',
9
+ SUMMARY = 'SUMMARY',
10
+ QUOTATION = 'QUOTATION',
11
+ EMBEDDING = 'EMBEDDING'
12
+ }
13
+
14
+ export enum ContentOrigin {
15
+ HUMAN = 'HUMAN', // Created by humans. High training value.
16
+ SYNTHETIC = 'SYNTHETIC', // Created by AI. Risk of model collapse.
17
+ HYBRID = 'HYBRID' // Edited by humans, drafted by AI.
18
+ }
19
+
20
+ export enum QualityFlag {
21
+ SEO_SPAM = 'SEO_SPAM',
22
+ INACCURATE = 'INACCURATE',
23
+ HATE_SPEECH = 'HATE_SPEECH',
24
+ HIGH_QUALITY = 'HIGH_QUALITY'
25
+ }
26
+
27
+ /**
28
+ * DNS Identity Manifest
29
+ * Hosted at: https://{agent_id}/.well-known/aamp-agent.json
30
+ */
31
+ export interface AgentIdentityManifest {
32
+ agent_id: string; // e.g. "bot.openai.com"
33
+ public_key: string; // Base64 SPKI
34
+ contact_email?: string;
35
+ }
36
+
37
+ /**
38
+ * PRODUCTION INFRASTRUCTURE: Cache Interface
39
+ * Required for Serverless/Edge environments to prevent repeated DNS fetches.
40
+ */
41
+ export interface IdentityCache {
42
+ get(key: string): Promise<string | null>; // Returns stored PublicKey
43
+ set(key: string, value: string, ttlSeconds: number): Promise<void>;
44
+ }
45
+
46
+ /**
47
+ * Optional Monetization (The Settlement Layer)
48
+ */
49
+ export interface MonetizationConfig {
50
+ method: 'BROKER' | 'CRYPTO' | 'PRIVATE_TREATY';
51
+ location: string;
52
+ }
53
+
54
+ /**
55
+ * Handling Non-AAMP Visitors
56
+ *
57
+ * PASSIVE: Allow everyone (Legacy web behavior).
58
+ * HYBRID: Allow verified Agents AND likely Humans (Browser Heuristics). Block bots.
59
+ * STRICT: Allow ONLY verified AAMP Agents. (API Mode).
60
+ */
61
+ export type UnauthenticatedStrategy = 'PASSIVE' | 'HYBRID' | 'STRICT';
62
+
63
+ export interface AccessPolicy {
64
+ version: '1.1';
65
+ allowTraining: boolean;
66
+ allowRAG: boolean;
67
+ attributionRequired: boolean;
68
+
69
+ // Economic Signals
70
+ allowAdSupportedAccess: boolean;
71
+ requiresPayment: boolean;
72
+ paymentPointer?: string;
73
+
74
+ // Identity Strictness
75
+ requireIdentityBinding?: boolean;
76
+
77
+ // V1.1: Optional Settlement Info
78
+ monetization?: MonetizationConfig;
79
+ }
80
+
81
+ export interface ProtocolHeader {
82
+ v: '1.1';
83
+ ts: string;
84
+ agent_id: string;
85
+ resource: string;
86
+ purpose: AccessPurpose;
87
+ context: {
88
+ ads_displayed: boolean;
89
+ };
90
+ }
91
+
92
+ export interface SignedAccessRequest {
93
+ header: ProtocolHeader;
94
+ signature: string;
95
+ publicKey?: string;
96
+ }
97
+
98
+ export interface FeedbackSignal {
99
+ target_resource: string;
100
+ agent_id: string;
101
+ quality_score: number;
102
+ flags: QualityFlag[];
103
+ timestamp: string;
104
+ }
105
+
106
+ // Result of the full evaluation pipeline
107
+ export interface EvaluationResult {
108
+ allowed: boolean;
109
+ status: 200 | 400 | 401 | 403;
110
+ reason: string;
111
+ visitorType: 'VERIFIED_AGENT' | 'LIKELY_HUMAN' | 'UNIDENTIFIED_BOT';
112
+ metadata?: any;
98
113
  }