@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/dist/agent.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Layer 2: Agent SDK
3
3
  */
4
- import { AccessPurpose, SignedAccessRequest, FeedbackSignal, QualityFlag } from './types';
4
+ import { AccessPurpose, SignedAccessRequest, FeedbackSignal, QualityFlag, AgentIdentityManifest } from './types';
5
5
  export interface AccessOptions {
6
6
  adsDisplayed?: boolean;
7
7
  }
@@ -10,10 +10,15 @@ export declare class AAMPAgent {
10
10
  agentId: string;
11
11
  /**
12
12
  * Initialize the Agent Identity (Ephemeral or Persisted)
13
- * @param customAgentId Optional persistent ID for this agent. If omitted, generates a session ID.
13
+ * @param customAgentId For PRODUCTION, this should be your domain (e.g., "bot.openai.com")
14
14
  */
15
15
  initialize(customAgentId?: string): Promise<void>;
16
16
  createAccessRequest(resource: string, purpose: AccessPurpose, options?: AccessOptions): Promise<SignedAccessRequest>;
17
+ /**
18
+ * Helper: Generate the JSON file you must host on your domain
19
+ * Host this at: https://{agentId}/.well-known/aamp-agent.json
20
+ */
21
+ getIdentityManifest(contactEmail?: string): Promise<AgentIdentityManifest>;
17
22
  /**
18
23
  * NEW IN V1.1: Quality Feedback Loop
19
24
  * Allows the Agent to report spam or verify quality of a resource.
package/dist/agent.js CHANGED
@@ -10,7 +10,7 @@ class AAMPAgent {
10
10
  }
11
11
  /**
12
12
  * Initialize the Agent Identity (Ephemeral or Persisted)
13
- * @param customAgentId Optional persistent ID for this agent. If omitted, generates a session ID.
13
+ * @param customAgentId For PRODUCTION, this should be your domain (e.g., "bot.openai.com")
14
14
  */
15
15
  async initialize(customAgentId) {
16
16
  this.keyPair = await (0, crypto_1.generateKeyPair)();
@@ -34,6 +34,20 @@ class AAMPAgent {
34
34
  const publicKeyExport = await (0, crypto_1.exportPublicKey)(this.keyPair.publicKey);
35
35
  return { header, signature, publicKey: publicKeyExport };
36
36
  }
37
+ /**
38
+ * Helper: Generate the JSON file you must host on your domain
39
+ * Host this at: https://{agentId}/.well-known/aamp-agent.json
40
+ */
41
+ async getIdentityManifest(contactEmail) {
42
+ if (!this.keyPair)
43
+ throw new Error("Agent not initialized.");
44
+ const publicKey = await (0, crypto_1.exportPublicKey)(this.keyPair.publicKey);
45
+ return {
46
+ agent_id: this.agentId,
47
+ public_key: publicKey,
48
+ contact_email: contactEmail
49
+ };
50
+ }
37
51
  /**
38
52
  * NEW IN V1.1: Quality Feedback Loop
39
53
  * Allows the Agent to report spam or verify quality of a resource.
@@ -3,6 +3,7 @@
3
3
  * These values are immutable and defined by the AAMP Specification.
4
4
  */
5
5
  export declare const AAMP_VERSION = "1.1";
6
+ export declare const WELL_KNOWN_AGENT_PATH = "/.well-known/aamp-agent.json";
6
7
  export declare const HEADERS: {
7
8
  readonly PAYLOAD: "x-aamp-payload";
8
9
  readonly SIGNATURE: "x-aamp-signature";
package/dist/constants.js CHANGED
@@ -4,8 +4,11 @@
4
4
  * These values are immutable and defined by the AAMP Specification.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.MAX_CLOCK_SKEW_MS = exports.CRYPTO_CONFIG = exports.HEADERS = exports.AAMP_VERSION = void 0;
7
+ exports.MAX_CLOCK_SKEW_MS = exports.CRYPTO_CONFIG = exports.HEADERS = exports.WELL_KNOWN_AGENT_PATH = exports.AAMP_VERSION = void 0;
8
8
  exports.AAMP_VERSION = '1.1';
9
+ // The path where Agents MUST host their public key to prove identity.
10
+ // Example: https://bot.openai.com/.well-known/aamp-agent.json
11
+ exports.WELL_KNOWN_AGENT_PATH = '/.well-known/aamp-agent.json';
9
12
  // HTTP Headers used for the handshake
10
13
  exports.HEADERS = {
11
14
  // Transport: The signed payload (Base64 encoded JSON of ProtocolHeader)
package/dist/express.d.ts CHANGED
@@ -1,10 +1,12 @@
1
- import { AccessPolicy, ContentOrigin } from './types';
1
+ import { AccessPolicy, ContentOrigin, UnauthenticatedStrategy, IdentityCache } from './types';
2
2
  export interface AAMPConfig {
3
3
  policy: Omit<AccessPolicy, 'version'>;
4
4
  meta: {
5
5
  origin: keyof typeof ContentOrigin;
6
6
  paymentPointer?: string;
7
7
  };
8
+ strategy?: UnauthenticatedStrategy;
9
+ cache?: IdentityCache;
8
10
  }
9
11
  export declare class AAMP {
10
12
  private publisher;
package/dist/express.js CHANGED
@@ -8,13 +8,9 @@ exports.AAMP = void 0;
8
8
  const publisher_1 = require("./publisher");
9
9
  const types_1 = require("./types");
10
10
  const crypto_1 = require("./crypto");
11
- const constants_1 = require("./constants");
12
11
  class AAMP {
13
12
  constructor(config) {
14
- this.publisher = new publisher_1.AAMPPublisher({
15
- version: '1.1',
16
- ...config.policy
17
- });
13
+ this.publisher = new publisher_1.AAMPPublisher({ version: '1.1', ...config.policy }, config.strategy || 'PASSIVE', config.cache);
18
14
  this.origin = types_1.ContentOrigin[config.meta.origin];
19
15
  this.ready = (0, crypto_1.generateKeyPair)().then(keys => {
20
16
  return this.publisher.initialize(keys);
@@ -29,39 +25,35 @@ class AAMP {
29
25
  middleware() {
30
26
  return async (req, res, next) => {
31
27
  await this.ready;
32
- // 1. Inject Provenance Headers (Passive Protection)
33
- const headers = await this.publisher.generateResponseHeaders(this.origin);
34
- Object.entries(headers).forEach(([k, v]) => {
35
- res.setHeader(k, v);
28
+ // Normalize headers to lowercase dictionary
29
+ const headers = {};
30
+ Object.keys(req.headers).forEach(key => {
31
+ headers[key.toLowerCase()] = req.headers[key];
36
32
  });
37
- // 2. Active Verification (If Agent sends signed headers)
38
- const payloadHeader = req.headers[constants_1.HEADERS.PAYLOAD];
39
- const sigHeader = req.headers[constants_1.HEADERS.SIGNATURE];
40
- const keyHeader = req.headers[constants_1.HEADERS.PUBLIC_KEY];
41
- if (payloadHeader && sigHeader && keyHeader) {
42
- try {
43
- const headerJson = atob(payloadHeader); // RAW STRING
44
- const requestHeader = JSON.parse(headerJson);
45
- const signedRequest = {
46
- header: requestHeader,
47
- signature: sigHeader,
48
- publicKey: keyHeader
49
- };
50
- const agentKey = await crypto.subtle.importKey("spki", new Uint8Array(atob(keyHeader).split('').map(c => c.charCodeAt(0))), { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]);
51
- // Pass raw headerJson to ensure signature matches exactly what was signed
52
- const result = await this.publisher.verifyRequest(signedRequest, agentKey, headerJson);
53
- if (!result.allowed) {
54
- res.status(403).json({ error: result.reason });
55
- return;
56
- }
57
- // Verified!
58
- req.aamp = { verified: true, ...requestHeader };
59
- }
60
- catch (e) {
61
- res.status(400).json({ error: "Invalid AAMP Signature" });
62
- return;
63
- }
33
+ // Retrieve Raw Payload if available (optional but good for crypto)
34
+ // Note: Express body parsing might interfere, so we usually rely on the header content.
35
+ const rawPayload = headers['x-aamp-payload'];
36
+ // Evaluate Visitor
37
+ const result = await this.publisher.evaluateVisitor(headers, rawPayload);
38
+ // Enforce Decision
39
+ if (!result.allowed) {
40
+ res.status(result.status).json({
41
+ error: result.reason,
42
+ visitor_type: result.visitorType
43
+ });
44
+ return;
64
45
  }
46
+ // Inject Provenance Headers (For the humans/agents that got through)
47
+ const respHeaders = await this.publisher.generateResponseHeaders(this.origin);
48
+ Object.entries(respHeaders).forEach(([k, v]) => {
49
+ res.setHeader(k, v);
50
+ });
51
+ // Attach metadata to request for downstream use
52
+ req.aamp = {
53
+ verified: result.visitorType === 'VERIFIED_AGENT',
54
+ type: result.visitorType,
55
+ ...result.metadata
56
+ };
65
57
  next();
66
58
  };
67
59
  }
package/dist/nextjs.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AccessPolicy, ContentOrigin } from './types';
1
+ import { AccessPolicy, ContentOrigin, UnauthenticatedStrategy, IdentityCache } from './types';
2
2
  type NextRequest = any;
3
3
  type NextResponse = any;
4
4
  export interface AAMPConfig {
@@ -7,6 +7,8 @@ export interface AAMPConfig {
7
7
  origin: keyof typeof ContentOrigin;
8
8
  paymentPointer?: string;
9
9
  };
10
+ strategy?: UnauthenticatedStrategy;
11
+ cache?: IdentityCache;
10
12
  }
11
13
  export declare class AAMPNext {
12
14
  private publisher;
package/dist/nextjs.js CHANGED
@@ -8,7 +8,6 @@ exports.AAMPNext = void 0;
8
8
  const publisher_1 = require("./publisher");
9
9
  const types_1 = require("./types");
10
10
  const crypto_1 = require("./crypto");
11
- const constants_1 = require("./constants");
12
11
  const createJsonResponse = (body, status = 200) => {
13
12
  return new Response(JSON.stringify(body), {
14
13
  status,
@@ -17,10 +16,7 @@ const createJsonResponse = (body, status = 200) => {
17
16
  };
18
17
  class AAMPNext {
19
18
  constructor(config) {
20
- this.publisher = new publisher_1.AAMPPublisher({
21
- version: '1.1',
22
- ...config.policy
23
- });
19
+ this.publisher = new publisher_1.AAMPPublisher({ version: '1.1', ...config.policy }, config.strategy || 'PASSIVE', config.cache);
24
20
  this.origin = types_1.ContentOrigin[config.meta.origin];
25
21
  this.ready = (0, crypto_1.generateKeyPair)().then(keys => this.publisher.initialize(keys));
26
22
  }
@@ -33,33 +29,22 @@ class AAMPNext {
33
29
  withProtection(handler) {
34
30
  return async (req) => {
35
31
  await this.ready;
36
- // 1. Active Verification
37
- const payloadHeader = req.headers.get(constants_1.HEADERS.PAYLOAD);
38
- const sigHeader = req.headers.get(constants_1.HEADERS.SIGNATURE);
39
- const keyHeader = req.headers.get(constants_1.HEADERS.PUBLIC_KEY);
40
- if (payloadHeader && sigHeader && keyHeader) {
41
- try {
42
- const headerJson = atob(payloadHeader); // RAW STRING
43
- const requestHeader = JSON.parse(headerJson);
44
- const signedRequest = {
45
- header: requestHeader,
46
- signature: sigHeader,
47
- publicKey: keyHeader
48
- };
49
- const agentKey = await crypto.subtle.importKey("spki", new Uint8Array(atob(keyHeader).split('').map(c => c.charCodeAt(0))), { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]);
50
- // Pass raw headerJson to ensure signature matches exactly what was signed
51
- const result = await this.publisher.verifyRequest(signedRequest, agentKey, headerJson);
52
- if (!result.allowed) {
53
- return createJsonResponse({ error: result.reason }, 403);
54
- }
55
- }
56
- catch (e) {
57
- return createJsonResponse({ error: "Invalid AAMP Signature" }, 400);
58
- }
32
+ // Extract Headers map
33
+ const headers = {};
34
+ req.headers.forEach((value, key) => {
35
+ headers[key.toLowerCase()] = value;
36
+ });
37
+ // Evaluate
38
+ const result = await this.publisher.evaluateVisitor(headers, headers['x-aamp-payload']);
39
+ if (!result.allowed) {
40
+ return createJsonResponse({
41
+ error: result.reason,
42
+ visitor_type: result.visitorType
43
+ }, result.status);
59
44
  }
60
- // 2. Execute Handler
45
+ // Execute Handler
61
46
  const response = await handler(req);
62
- // 3. Inject Provenance Headers (Passive)
47
+ // Inject Provenance
63
48
  const aampHeaders = await this.publisher.generateResponseHeaders(this.origin);
64
49
  if (response && response.headers) {
65
50
  Object.entries(aampHeaders).forEach(([k, v]) => {
@@ -1,31 +1,34 @@
1
1
  /**
2
2
  * Layer 2: Publisher Middleware
3
- * Used by content owners to enforce policy and log access.
3
+ * Used by content owners to enforce policy, log access, and filter bots.
4
4
  */
5
- import { AccessPolicy, SignedAccessRequest, ContentOrigin, FeedbackSignal } from './types';
6
- export interface VerificationResult {
7
- allowed: boolean;
8
- reason: string;
9
- responseHeaders?: Record<string, string>;
10
- }
5
+ import { AccessPolicy, ContentOrigin, EvaluationResult, UnauthenticatedStrategy, IdentityCache } from './types';
11
6
  export declare class AAMPPublisher {
12
7
  private policy;
13
8
  private keyPair;
14
- constructor(policy: AccessPolicy);
9
+ private unauthenticatedStrategy;
10
+ private cache;
11
+ private readonly CACHE_TTL_SECONDS;
12
+ constructor(policy: AccessPolicy, strategy?: UnauthenticatedStrategy, cacheImpl?: IdentityCache);
15
13
  initialize(keyPair: CryptoKeyPair): Promise<void>;
16
14
  getPolicy(): AccessPolicy;
17
15
  /**
18
- * Verifies an incoming AI access request.
19
- *
20
- * @param request The parsed request object
21
- * @param requestPublicKey The agent's public key
22
- * @param rawPayload (Optional) The raw string received over the wire. REQUIRED for robust verification.
16
+ * Main Entry Point: Evaluate ANY visitor (Human, Bot, or Agent)
17
+ */
18
+ evaluateVisitor(reqHeaders: Record<string, string | undefined>, rawPayload?: string): Promise<EvaluationResult>;
19
+ /**
20
+ * Browser Heuristics (Hardened)
21
+ * 1. Checks Known Bot Signatures (Fast Fail)
22
+ * 2. Checks Trusted Upstream Signals (Cloudflare/Vercel)
23
+ * 3. Checks Browser Header Consistency
23
24
  */
24
- verifyRequest(request: SignedAccessRequest, requestPublicKey: CryptoKey, rawPayload?: string): Promise<VerificationResult>;
25
+ private performBrowserHeuristics;
25
26
  /**
26
- * Verifies a Feedback Signal (Spam Report) from an Agent.
27
- * Part of the AAMP Immune System.
27
+ * Handle AAMP Protocol Logic
28
28
  */
29
- verifyFeedback(signal: FeedbackSignal, signature: string, agentPublicKey: CryptoKey): Promise<boolean>;
29
+ private handleAgent;
30
+ private verifyRequestLogic;
31
+ private verifyDnsBinding;
32
+ private isDomain;
30
33
  generateResponseHeaders(origin: ContentOrigin): Promise<Record<string, string>>;
31
34
  }
package/dist/publisher.js CHANGED
@@ -3,17 +3,45 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AAMPPublisher = void 0;
4
4
  /**
5
5
  * Layer 2: Publisher Middleware
6
- * Used by content owners to enforce policy and log access.
6
+ * Used by content owners to enforce policy, log access, and filter bots.
7
7
  */
8
8
  const types_1 = require("./types");
9
9
  const crypto_1 = require("./crypto");
10
10
  const constants_1 = require("./constants");
11
+ /**
12
+ * Default In-Memory Cache (Fallback only)
13
+ * NOT recommended for high-traffic Serverless production.
14
+ */
15
+ class MemoryCache {
16
+ constructor() {
17
+ this.store = new Map();
18
+ }
19
+ async get(key) {
20
+ const item = this.store.get(key);
21
+ if (!item)
22
+ return null;
23
+ if (Date.now() > item.exp) {
24
+ this.store.delete(key);
25
+ return null;
26
+ }
27
+ return item.val;
28
+ }
29
+ async set(key, value, ttlSeconds) {
30
+ this.store.set(key, {
31
+ val: value,
32
+ exp: Date.now() + (ttlSeconds * 1000)
33
+ });
34
+ }
35
+ }
11
36
  class AAMPPublisher {
12
- constructor(policy) {
37
+ constructor(policy, strategy = 'PASSIVE', cacheImpl) {
13
38
  this.keyPair = null;
39
+ // Default TTL: 1 Hour
40
+ this.CACHE_TTL_SECONDS = 3600;
14
41
  this.policy = policy;
42
+ this.unauthenticatedStrategy = strategy;
43
+ this.cache = cacheImpl || new MemoryCache();
15
44
  }
16
- // Publishers now need keys too, to sign their Content Origin declarations
17
45
  async initialize(keyPair) {
18
46
  this.keyPair = keyPair;
19
47
  }
@@ -21,54 +49,192 @@ class AAMPPublisher {
21
49
  return this.policy;
22
50
  }
23
51
  /**
24
- * Verifies an incoming AI access request.
25
- *
26
- * @param request The parsed request object
27
- * @param requestPublicKey The agent's public key
28
- * @param rawPayload (Optional) The raw string received over the wire. REQUIRED for robust verification.
52
+ * Main Entry Point: Evaluate ANY visitor (Human, Bot, or Agent)
29
53
  */
30
- async verifyRequest(request, requestPublicKey, rawPayload) {
31
- // 1. Replay Attack Prevention (Timestamp Check)
32
- const requestTime = new Date(request.header.ts).getTime();
33
- const now = Date.now();
34
- if (Math.abs(now - requestTime) > constants_1.MAX_CLOCK_SKEW_MS) {
35
- return { allowed: false, reason: 'TIMESTAMP_INVALID: Clock skew exceeded.' };
54
+ async evaluateVisitor(reqHeaders, rawPayload) {
55
+ // 1. Check for AAMP Headers
56
+ const hasAamp = reqHeaders[constants_1.HEADERS.PAYLOAD] && reqHeaders[constants_1.HEADERS.SIGNATURE] && reqHeaders[constants_1.HEADERS.PUBLIC_KEY];
57
+ if (hasAamp) {
58
+ // It claims to be an Agent. Verify it.
59
+ return await this.handleAgent(reqHeaders, rawPayload);
36
60
  }
37
- // 2. Policy Enforcement (Usage Type)
38
- // STRICT CHECK: Training
39
- if (request.header.purpose === types_1.AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
40
- return { allowed: false, reason: 'POLICY_DENIED: Training not allowed by site owner.' };
61
+ // 2. It's not an AAMP Agent. Apply Strategy.
62
+ if (this.unauthenticatedStrategy === 'STRICT') {
63
+ return {
64
+ allowed: false,
65
+ status: 401,
66
+ reason: "STRICT_MODE: Only AAMP verified agents allowed.",
67
+ visitorType: 'UNIDENTIFIED_BOT'
68
+ };
41
69
  }
42
- // STRICT CHECK: RAG / Retrieval
43
- if (request.header.purpose === types_1.AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
44
- return { allowed: false, reason: 'POLICY_DENIED: RAG Retrieval not allowed.' };
70
+ if (this.unauthenticatedStrategy === 'PASSIVE') {
71
+ return {
72
+ allowed: true,
73
+ status: 200,
74
+ reason: "PASSIVE_MODE: Allowed without verification.",
75
+ visitorType: 'LIKELY_HUMAN'
76
+ };
45
77
  }
46
- // 3. Economic Policy Enforcement
47
- if (this.policy.requiresPayment) {
48
- const isExemptViaAds = this.policy.allowAdSupportedAccess && request.header.context.ads_displayed;
49
- if (!isExemptViaAds) {
78
+ // 3. HYBRID MODE: Heuristic Analysis (The "Lazy Bot" Filter)
79
+ const isHuman = this.performBrowserHeuristics(reqHeaders);
80
+ if (isHuman) {
81
+ return {
82
+ allowed: true,
83
+ status: 200,
84
+ reason: "BROWSER_VERIFIED: Heuristics passed.",
85
+ visitorType: 'LIKELY_HUMAN'
86
+ };
87
+ }
88
+ else {
89
+ return {
90
+ allowed: false,
91
+ status: 403,
92
+ reason: "BOT_DETECTED: Request lacks browser signatures and AAMP headers.",
93
+ visitorType: 'UNIDENTIFIED_BOT'
94
+ };
95
+ }
96
+ }
97
+ /**
98
+ * Browser Heuristics (Hardened)
99
+ * 1. Checks Known Bot Signatures (Fast Fail)
100
+ * 2. Checks Trusted Upstream Signals (Cloudflare/Vercel)
101
+ * 3. Checks Browser Header Consistency
102
+ */
103
+ performBrowserHeuristics(headers) {
104
+ const userAgent = headers['user-agent'] || '';
105
+ // A. The "Obvious Bot" Blocklist (Fast Fail)
106
+ const botSignatures = ['python-requests', 'curl', 'wget', 'scrapy', 'bot', 'crawler', 'spider'];
107
+ // Exception: Googlebot (if you want SEO). We'll treat Googlebot as a bot,
108
+ // real implementations might white-list it via IP verification (not possible in just JS headers).
109
+ if (botSignatures.some(sig => userAgent.toLowerCase().includes(sig))) {
110
+ return false;
111
+ }
112
+ // B. Trusted Infrastructure Signals (The Real World Solution)
113
+ // If Cloudflare or Vercel says "This is a real user", we trust them.
114
+ // Cloudflare: 'cf-visitor' exists. 'cf-ipcountry' exists.
115
+ if (headers['cf-visitor'] || headers['cf-ray'])
116
+ return true;
117
+ // Vercel: 'x-vercel-id'
118
+ if (headers['x-vercel-id'])
119
+ return true;
120
+ // AWS CloudFront: 'cloudfront-viewer-address'
121
+ if (headers['cloudfront-viewer-address'])
122
+ return true;
123
+ // C. The "Browser Fingerprint" (Fallback for direct connections)
124
+ // Real browsers almost always send these headers
125
+ const hasAcceptLanguage = !!headers['accept-language'];
126
+ const hasSecFetchDest = !!headers['sec-fetch-dest'];
127
+ const hasUpgradeInsecure = !!headers['upgrade-insecure-requests'];
128
+ // If it has typical browser headers, we allow it.
129
+ if (hasAcceptLanguage && (hasSecFetchDest || hasUpgradeInsecure)) {
130
+ return true;
131
+ }
132
+ // If it has no browser headers and no trusted proxy headers -> It's likely a script.
133
+ return false;
134
+ }
135
+ /**
136
+ * Handle AAMP Protocol Logic
137
+ */
138
+ async handleAgent(reqHeaders, rawPayload) {
139
+ try {
140
+ const payloadHeader = reqHeaders[constants_1.HEADERS.PAYLOAD];
141
+ const sigHeader = reqHeaders[constants_1.HEADERS.SIGNATURE];
142
+ const keyHeader = reqHeaders[constants_1.HEADERS.PUBLIC_KEY];
143
+ const headerJson = atob(payloadHeader);
144
+ const requestHeader = JSON.parse(headerJson);
145
+ const signedRequest = {
146
+ header: requestHeader,
147
+ signature: sigHeader,
148
+ publicKey: keyHeader
149
+ };
150
+ const agentKey = await crypto.subtle.importKey("spki", new Uint8Array(atob(keyHeader).split('').map(c => c.charCodeAt(0))), { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]);
151
+ // Verify Core Logic
152
+ const result = await this.verifyRequestLogic(signedRequest, agentKey, headerJson);
153
+ if (!result.allowed) {
50
154
  return {
51
155
  allowed: false,
52
- reason: 'PAYMENT_REQUIRED: Site requires payment or ad-supported access.'
156
+ status: 403,
157
+ reason: result.reason,
158
+ visitorType: 'VERIFIED_AGENT'
53
159
  };
54
160
  }
161
+ return {
162
+ allowed: true,
163
+ status: 200,
164
+ reason: "AAMP_VERIFIED",
165
+ visitorType: 'VERIFIED_AGENT',
166
+ metadata: requestHeader
167
+ };
168
+ }
169
+ catch (e) {
170
+ return { allowed: false, status: 400, reason: "INVALID_SIGNATURE", visitorType: 'UNIDENTIFIED_BOT' };
171
+ }
172
+ }
173
+ async verifyRequestLogic(request, requestPublicKey, rawPayload) {
174
+ // 1. Replay Attack Prevention
175
+ const requestTime = new Date(request.header.ts).getTime();
176
+ if (Math.abs(Date.now() - requestTime) > constants_1.MAX_CLOCK_SKEW_MS) {
177
+ return { allowed: false, reason: 'TIMESTAMP_INVALID', identityVerified: false };
55
178
  }
56
- // 4. Cryptographic Verification (The "Proof")
57
- // CRITICAL: We prefer the rawPayload if available to avoid JSON parsing/stringify mismatches.
179
+ // 2. Crypto Verification
58
180
  const signableString = rawPayload || JSON.stringify(request.header);
59
- const isValid = await (0, crypto_1.verifySignature)(requestPublicKey, signableString, request.signature);
60
- if (!isValid) {
61
- return { allowed: false, reason: 'CRYPTO_FAIL: Signature verification failed.' };
181
+ const isCryptoValid = await (0, crypto_1.verifySignature)(requestPublicKey, signableString, request.signature);
182
+ if (!isCryptoValid)
183
+ return { allowed: false, reason: 'CRYPTO_FAIL', identityVerified: false };
184
+ // 3. Identity Verification (DNS Binding) with Cache
185
+ let identityVerified = false;
186
+ const claimedDomain = request.header.agent_id;
187
+ const pubKeyString = await (0, crypto_1.exportPublicKey)(requestPublicKey);
188
+ // Check Cache First
189
+ const cachedKey = await this.cache.get(claimedDomain);
190
+ if (cachedKey === pubKeyString) {
191
+ identityVerified = true;
192
+ }
193
+ else if (this.isDomain(claimedDomain)) {
194
+ // Cache Miss: Perform DNS Fetch
195
+ identityVerified = await this.verifyDnsBinding(claimedDomain, pubKeyString);
196
+ if (identityVerified) {
197
+ await this.cache.set(claimedDomain, pubKeyString, this.CACHE_TTL_SECONDS);
198
+ }
199
+ }
200
+ if (this.policy.requireIdentityBinding && !identityVerified) {
201
+ return { allowed: false, reason: 'IDENTITY_FAIL: DNS Binding could not be verified.', identityVerified: false };
202
+ }
203
+ // 4. Policy Check: Purpose
204
+ if (request.header.purpose === types_1.AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
205
+ return { allowed: false, reason: 'POLICY_DENIED: Training not allowed.', identityVerified };
62
206
  }
63
- return { allowed: true, reason: 'OK' };
207
+ if (request.header.purpose === types_1.AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
208
+ return { allowed: false, reason: 'POLICY_DENIED: RAG not allowed.', identityVerified };
209
+ }
210
+ // 5. Policy Check: Economics
211
+ if (this.policy.requiresPayment) {
212
+ const isAdExempt = this.policy.allowAdSupportedAccess && request.header.context.ads_displayed;
213
+ if (!isAdExempt) {
214
+ return { allowed: false, reason: 'PAYMENT_REQUIRED: Content requires payment or ads.', identityVerified };
215
+ }
216
+ }
217
+ return { allowed: true, reason: 'OK', identityVerified };
64
218
  }
65
- /**
66
- * Verifies a Feedback Signal (Spam Report) from an Agent.
67
- * Part of the AAMP Immune System.
68
- */
69
- async verifyFeedback(signal, signature, agentPublicKey) {
70
- const signableString = JSON.stringify(signal);
71
- return await (0, crypto_1.verifySignature)(agentPublicKey, signableString, signature);
219
+ async verifyDnsBinding(domain, requestKeySpki) {
220
+ try {
221
+ const url = `https://${domain}${constants_1.WELL_KNOWN_AGENT_PATH}`;
222
+ // In production, we need a short timeout to prevent hanging
223
+ const controller = new AbortController();
224
+ const timeoutId = setTimeout(() => controller.abort(), 1500); // 1.5s max for DNS check
225
+ const response = await fetch(url, { signal: controller.signal });
226
+ clearTimeout(timeoutId);
227
+ if (!response.ok)
228
+ return false;
229
+ const manifest = await response.json();
230
+ return manifest.agent_id === domain && manifest.public_key === requestKeySpki;
231
+ }
232
+ catch {
233
+ return false;
234
+ }
235
+ }
236
+ isDomain(s) {
237
+ return /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(s);
72
238
  }
73
239
  async generateResponseHeaders(origin) {
74
240
  if (!this.keyPair)