@aamp/protocol 1.1.6 → 1.1.7

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.
@@ -10,6 +10,9 @@ export declare class AAMPPublisher {
10
10
  getPolicy(): AccessPolicy;
11
11
  /**
12
12
  * Main Entry Point: Evaluate ANY visitor (Human, Bot, or Agent)
13
+ * STAGE 1: IDENTITY (Strict)
14
+ * STAGE 2: POLICY (Permissions)
15
+ * STAGE 3: ACCESS (HQ Content)
13
16
  */
14
17
  evaluateVisitor(reqHeaders: Record<string, string | undefined>, rawPayload?: string): Promise<EvaluationResult>;
15
18
  /**
@@ -20,9 +23,14 @@ export declare class AAMPPublisher {
20
23
  */
21
24
  private performBrowserHeuristics;
22
25
  /**
23
- * Handle AAMP Protocol Logic
26
+ * Handle AAMP Protocol Logic (Strict Mode)
24
27
  */
28
+ private handleAgentStrict;
25
29
  private handleAgent;
30
+ /**
31
+ * STAGE 2: POLICY ENFORCEMENT CHECK
32
+ */
33
+ private checkPolicyStrict;
26
34
  private verifyRequestLogic;
27
35
  private verifyDnsBinding;
28
36
  private isDomain;
package/dist/publisher.js CHANGED
@@ -48,54 +48,46 @@ export class AAMPPublisher {
48
48
  }
49
49
  /**
50
50
  * Main Entry Point: Evaluate ANY visitor (Human, Bot, or Agent)
51
+ * STAGE 1: IDENTITY (Strict)
52
+ * STAGE 2: POLICY (Permissions)
53
+ * STAGE 3: ACCESS (HQ Content)
51
54
  */
52
55
  async evaluateVisitor(reqHeaders, rawPayload) {
53
- // 1. Check for AAMP Headers
56
+ console.log(`\n--- [AAMP LOG START] New Request ---`);
57
+ // --- STAGE 1: IDENTITY VERIFICATION ---
58
+ console.log(`[IDENTITY] šŸ” Checking Identity Headers...`);
54
59
  const hasAamp = reqHeaders[HEADERS.PAYLOAD] && reqHeaders[HEADERS.SIGNATURE] && reqHeaders[HEADERS.PUBLIC_KEY];
55
- const feedbackToken = reqHeaders[HEADERS.FEEDBACK];
56
- if (feedbackToken) {
57
- await this.handleFeedback(feedbackToken, reqHeaders);
58
- }
59
60
  if (hasAamp) {
60
- console.log("\nšŸ”Ž [AAMP Middleware] Detected Agent Headers. Starting Verification...");
61
- // It claims to be an Agent. Verify it.
62
- return await this.handleAgent(reqHeaders, rawPayload);
61
+ // It claims to be an Agent. Verify it STRICTLY.
62
+ return await this.handleAgentStrict(reqHeaders, rawPayload);
63
63
  }
64
- // 2. It's not an AAMP Agent. Apply Strategy.
64
+ // If NO AAMP Headers -> FAIL IDENTITY immediately.
65
+ console.log(`[IDENTITY] āŒ FAILED. No AAMP Headers found.`);
66
+ // For now, retaining the legacy "Passive/Hybrid" switch just to avoid breaking browser demos completely
67
+ // BUT logging it as a specific "Identity Fail" flow.
65
68
  if (this.unauthenticatedStrategy === 'STRICT') {
69
+ console.log(`[IDENTITY] ā›” BLOCKING. Strategy is STRICT.`);
66
70
  return {
67
71
  allowed: false,
68
72
  status: 401,
69
- reason: "STRICT_MODE: Only AAMP verified agents allowed.",
73
+ reason: "IDENTITY_REQUIRED: Missing AAMP Headers.",
70
74
  visitorType: 'UNIDENTIFIED_BOT'
71
75
  };
72
76
  }
73
- if (this.unauthenticatedStrategy === 'PASSIVE') {
74
- return {
75
- allowed: true,
76
- status: 200,
77
- reason: "PASSIVE_MODE: Allowed without verification.",
78
- visitorType: 'LIKELY_HUMAN'
79
- };
80
- }
81
- // 3. HYBRID MODE: Heuristic Analysis (The "Lazy Bot" Filter)
77
+ console.log(`[IDENTITY] āš ļø SKIPPED (Legacy Mode). Checking Browser Heuristics...`);
82
78
  const isHuman = this.performBrowserHeuristics(reqHeaders);
83
79
  if (isHuman) {
84
- return {
85
- allowed: true,
86
- status: 200,
87
- reason: "BROWSER_VERIFIED: Heuristics passed.",
88
- visitorType: 'LIKELY_HUMAN'
89
- };
90
- }
91
- else {
92
- return {
93
- allowed: false,
94
- status: 403,
95
- reason: "BOT_DETECTED: Request lacks browser signatures and AAMP headers.",
96
- visitorType: 'UNIDENTIFIED_BOT'
97
- };
80
+ console.log(`[POLICY] šŸ‘¤ ALLOWED. Browser Heuristics Passed.`);
81
+ return { allowed: true, status: 200, reason: "BROWSER_VERIFIED", visitorType: 'LIKELY_HUMAN' };
98
82
  }
83
+ console.log(`[IDENTITY] āŒ FAILED. Not a Browser, No Headers.`);
84
+ console.log(`[ACCESS] ā›” BLOCKED.`);
85
+ return {
86
+ allowed: false,
87
+ status: 403,
88
+ reason: "IDENTITY_FAIL: No Identity, No Browser.",
89
+ visitorType: 'UNIDENTIFIED_BOT'
90
+ };
99
91
  }
100
92
  /**
101
93
  * Browser Heuristics (Hardened)
@@ -127,143 +119,163 @@ export class AAMPPublisher {
127
119
  return false;
128
120
  }
129
121
  /**
130
- * Handle AAMP Protocol Logic
122
+ * Handle AAMP Protocol Logic (Strict Mode)
131
123
  */
132
- async handleAgent(reqHeaders, rawPayload) {
124
+ async handleAgentStrict(reqHeaders, rawPayload) {
125
+ let agentId = "UNKNOWN";
133
126
  try {
127
+ // 1. Decode Headers
134
128
  const payloadHeader = reqHeaders[HEADERS.PAYLOAD];
135
129
  const sigHeader = reqHeaders[HEADERS.SIGNATURE];
136
130
  const keyHeader = reqHeaders[HEADERS.PUBLIC_KEY];
137
131
  const headerJson = atob(payloadHeader);
138
132
  const requestHeader = JSON.parse(headerJson);
133
+ agentId = requestHeader.agent_id;
134
+ console.log(`[IDENTITY] šŸ†” Claimed ID: ${agentId}`);
135
+ // 2. Crypto & DNS Verification
139
136
  const signedRequest = {
140
137
  header: requestHeader,
141
138
  signature: sigHeader,
142
139
  publicKey: keyHeader
143
140
  };
144
141
  const agentKey = await crypto.subtle.importKey("spki", new Uint8Array(atob(keyHeader).split('').map(c => c.charCodeAt(0))), { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]);
142
+ // Verify Core Logic (DNS + Crypto)
143
+ const verification = await this.verifyRequestLogic(signedRequest, agentKey);
144
+ if (!verification.identityVerified) {
145
+ console.log(`[IDENTITY] āŒ FAILED. Reason: ${verification.reason}`);
146
+ console.log(`[ACCESS] ā›” BLOCKED.`);
147
+ return { allowed: false, status: 403, reason: verification.reason, visitorType: 'UNIDENTIFIED_BOT' };
148
+ }
149
+ console.log(`[IDENTITY] āœ… PASSED. DNS Binding Verified.`);
150
+ // --- STAGE 2: POLICY ENFORCEMENT ---
151
+ console.log(`[POLICY] šŸ“œ Checking Permissions for ${agentId}...`);
145
152
  const proofToken = reqHeaders[HEADERS.PROOF_TOKEN];
146
153
  const paymentCredential = reqHeaders[HEADERS.PAYMENT_CREDENTIAL];
147
- // Verify Core Logic
148
- const result = await this.verifyRequestLogic(signedRequest, agentKey, proofToken, paymentCredential, headerJson);
149
- if (!result.allowed) {
150
- console.log(`ā›” [AAMP BLOCK]
151
- Agent: ${requestHeader.agent_id}
152
- Reason: ${result.reason}
153
- VisitorType: ${result.visitorType}
154
- Proof: ${result.proofUsed || 'None'}
155
- Identity Verified: ${result.identityVerified}`);
156
- return {
157
- allowed: false,
158
- status: 403,
159
- reason: result.reason,
160
- visitorType: 'VERIFIED_AGENT'
161
- };
154
+ const policyResult = await this.checkPolicyStrict(requestHeader, proofToken, paymentCredential);
155
+ if (!policyResult.allowed) {
156
+ console.log(`[POLICY] ā›” DENIED. Reason: ${policyResult.reason}`);
157
+ console.log(`[ACCESS] ā›” BLOCKED.`);
158
+ return policyResult;
162
159
  }
163
- console.log(`āœ… [AAMP ALLOW]
164
- Agent: ${requestHeader.agent_id}
165
- Reason: AAMP_VERIFIED
166
- Payment Method: ${result.proofUsed}
167
- Identity Verified: ${result.identityVerified}`);
160
+ // --- STAGE 3: ACCESS GRANT ---
161
+ console.log(`[POLICY] āœ… PASSED. Requirements Met.`);
162
+ console.log(`[ACCESS] šŸ”“ GRANTED. Unlocking HQ Content.`);
168
163
  return {
169
164
  allowed: true,
170
165
  status: 200,
171
166
  reason: "AAMP_VERIFIED",
172
167
  visitorType: 'VERIFIED_AGENT',
173
- metadata: requestHeader
168
+ metadata: requestHeader,
169
+ proofUsed: policyResult.proofUsed
174
170
  };
175
171
  }
176
172
  catch (e) {
177
- console.error(e);
173
+ console.error(`[AAMP ERROR]`, e);
178
174
  return { allowed: false, status: 400, reason: "INVALID_SIGNATURE", visitorType: 'UNIDENTIFIED_BOT' };
179
175
  }
180
176
  }
181
- async verifyRequestLogic(request, requestPublicKey, proofToken, paymentCredential, rawPayload) {
182
- // 1. Replay Attack Prevention
183
- const requestTime = new Date(request.header.ts).getTime();
184
- if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
185
- return { allowed: false, reason: 'TIMESTAMP_INVALID', identityVerified: false };
186
- }
187
- // 2. Crypto Verification
188
- const signableString = rawPayload || JSON.stringify(request.header);
189
- const isCryptoValid = await verifySignature(requestPublicKey, signableString, request.signature);
190
- if (!isCryptoValid)
191
- return { allowed: false, reason: 'CRYPTO_FAIL', identityVerified: false };
192
- // 3. Identity Verification (DNS Binding) with Cache
193
- let identityVerified = false;
194
- const claimedDomain = request.header.agent_id;
195
- const pubKeyString = await exportPublicKey(requestPublicKey);
196
- console.log(` šŸ†” [AAMP Identity] Verifying DNS Binding for: ${claimedDomain}`);
197
- // Check Cache First
198
- const cachedKey = await this.cache.get(claimedDomain);
199
- if (cachedKey === pubKeyString) {
200
- console.log(" ⚔ [AAMP Cache] Identity found in cache.");
201
- identityVerified = true;
202
- }
203
- else if (this.isDomain(claimedDomain)) {
204
- // Cache Miss: Perform DNS Fetch
205
- identityVerified = await this.verifyDnsBinding(claimedDomain, pubKeyString);
206
- if (identityVerified) {
207
- await this.cache.set(claimedDomain, pubKeyString, this.CACHE_TTL_SECONDS);
208
- }
209
- }
210
- if (this.policy.requireIdentityBinding && !identityVerified) {
211
- return { allowed: false, reason: 'IDENTITY_FAIL: DNS Binding could not be verified.', identityVerified: false };
212
- }
213
- // 4. Policy Check: Purpose
214
- if (request.header.purpose === AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
215
- return { allowed: false, reason: 'POLICY_DENIED: Training not allowed.', identityVerified };
177
+ // Legacy handler kept for interface compatibility (deprecated)
178
+ async handleAgent(reqHeaders, rawPayload) {
179
+ return this.handleAgentStrict(reqHeaders, rawPayload);
180
+ }
181
+ /**
182
+ * STAGE 2: POLICY ENFORCEMENT CHECK
183
+ */
184
+ async checkPolicyStrict(requestHeader, proofToken, paymentCredential) {
185
+ // 1. Policy Check: Purpose Ban (e.g. No Training)
186
+ if (requestHeader.purpose === AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
187
+ return { allowed: false, status: 403, reason: 'POLICY_DENIED: Training not allowed.', visitorType: 'VERIFIED_AGENT' };
216
188
  }
217
- if (request.header.purpose === AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
218
- return { allowed: false, reason: 'POLICY_DENIED: RAG not allowed.', identityVerified };
189
+ if (requestHeader.purpose === AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
190
+ return { allowed: false, status: 403, reason: 'POLICY_DENIED: RAG not allowed.', visitorType: 'VERIFIED_AGENT' };
219
191
  }
220
- // 5. Policy Check: Economics (v1.2)
192
+ // 2. Policy Check: Economics (v1.2) - Payment & Ads
221
193
  if (this.policy.requiresPayment) {
222
194
  let paymentSatisfied = false;
223
195
  // Method A: Flexible Payment Callback (DB / Custom Logic)
224
196
  if (this.policy.monetization?.checkPayment) {
225
- const isPaid = await this.policy.monetization.checkPayment(request.header.agent_id, request.header.purpose);
197
+ const isPaid = await this.policy.monetization.checkPayment(requestHeader.agent_id, requestHeader.purpose);
226
198
  if (isPaid) {
227
- console.log(` šŸ’° [AAMP Audit] Whitelist Check Passed via Callback.`);
228
- return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'WHITELIST_CALLBACK' };
199
+ console.log(`[POLICY] šŸ’° Payment Verified via Callback.`);
200
+ return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'WHITELIST_CALLBACK' };
229
201
  }
230
202
  }
231
203
  // Method B: Payment Credentials (Unified JWT)
232
204
  if (!paymentSatisfied && this.policy.monetization?.paymentConfig && paymentCredential) {
233
205
  const { jwksUrl, issuer } = this.policy.monetization.paymentConfig;
234
- console.log(` šŸ” [AAMP Audit] Verifying Payment Credential (Issuer: ${issuer})...`);
206
+ console.log(`[POLICY] šŸ” Verifying Payment Credential (Issuer: ${issuer})...`);
235
207
  const isValidCredential = await verifyJwt(paymentCredential, jwksUrl, issuer);
236
208
  if (isValidCredential) {
237
- console.log(` āœ… [AAMP Audit] Credential Signature VALID.`);
238
- return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'PAYMENT_CREDENTIAL_JWT' };
209
+ console.log(`[POLICY] āœ… Credential Signature VALID.`);
210
+ return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'PAYMENT_CREDENTIAL_JWT' };
239
211
  }
240
212
  else {
241
- console.log(` āŒ [AAMP Audit] Credential Signature INVALID.`);
213
+ console.log(`[POLICY] āŒ Credential Signature INVALID.`);
242
214
  }
243
215
  }
244
216
  // Method C: Ad-Supported (Proof Verification)
245
- if (!paymentSatisfied && this.policy.allowAdSupportedAccess && request.header.context.ads_displayed) {
217
+ if (!paymentSatisfied && this.policy.allowAdSupportedAccess && requestHeader.context?.ads_displayed) {
246
218
  if (proofToken && this.policy.monetization?.adNetwork) {
247
219
  const { jwksUrl, issuer } = this.policy.monetization.adNetwork;
248
- console.log(` šŸ“ŗ [AAMP Audit] Verifying Ad Proof (Issuer: ${issuer})...`);
220
+ console.log(`[POLICY] šŸ“ŗ Verifying Ad Proof (Issuer: ${issuer})...`);
249
221
  const isValidProof = await verifyJwt(proofToken, jwksUrl, issuer);
250
222
  if (isValidProof) {
251
- console.log(` āœ… [AAMP Audit] Ad Proof Signature VALID.`);
252
- return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'AD_PROOF_JWT' };
223
+ console.log(`[POLICY] āœ… Ad Proof Signature VALID.`);
224
+ return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'AD_PROOF_JWT' };
253
225
  }
254
226
  else {
255
- console.log(` āŒ [AAMP Audit] Ad Proof Signature INVALID.`);
227
+ console.log(`[POLICY] āŒ Ad Proof Signature INVALID.`);
256
228
  }
257
229
  }
258
230
  else {
259
- console.log(` āš ļø [AAMP Audit] Ad Proof MISSING.`);
231
+ console.log(`[POLICY] āš ļø Ad Proof MISSING.`);
260
232
  }
261
233
  }
262
- if (!paymentSatisfied) {
263
- return { allowed: false, reason: 'PAYMENT_REQUIRED: Whitelist, Credential, and Ad Proof checks ALL failed.', identityVerified, proofUsed: 'NONE', visitorType: 'VERIFIED_AGENT' };
234
+ return {
235
+ allowed: false,
236
+ status: 402,
237
+ reason: 'PAYMENT_REQUIRED: Whitelist, Credential, and Ad Proof checks ALL failed.',
238
+ visitorType: 'VERIFIED_AGENT',
239
+ proofUsed: 'NONE'
240
+ };
241
+ }
242
+ // If no payment required, allow.
243
+ return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT' };
244
+ }
245
+ async verifyRequestLogic(request, requestPublicKey) {
246
+ // 1. Replay Attack Prevention
247
+ const requestTime = new Date(request.header.ts).getTime();
248
+ if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
249
+ return { allowed: false, reason: 'TIMESTAMP_INVALID: Clock skew too large.', identityVerified: false };
250
+ }
251
+ // 2. Crypto Verification
252
+ const signableString = JSON.stringify(request.header);
253
+ const isCryptoValid = await verifySignature(requestPublicKey, signableString, request.signature);
254
+ if (!isCryptoValid)
255
+ return { allowed: false, reason: 'CRYPTO_FAIL: Signature invalid.', identityVerified: false };
256
+ // 3. Identity Verification (DNS Binding) with Cache
257
+ let identityVerified = false;
258
+ const claimedDomain = request.header.agent_id;
259
+ const pubKeyString = await exportPublicKey(requestPublicKey);
260
+ console.log(`[IDENTITY] šŸ” Verifying DNS Binding for: ${claimedDomain}`);
261
+ // Check Cache First
262
+ const cachedKey = await this.cache.get(claimedDomain);
263
+ if (cachedKey === pubKeyString) {
264
+ console.log("[IDENTITY] ⚔ Cache Hit. Identity Verified.");
265
+ identityVerified = true;
266
+ }
267
+ else if (this.isDomain(claimedDomain)) {
268
+ // Cache Miss: Perform DNS Fetch
269
+ identityVerified = await this.verifyDnsBinding(claimedDomain, pubKeyString);
270
+ if (identityVerified) {
271
+ await this.cache.set(claimedDomain, pubKeyString, this.CACHE_TTL_SECONDS);
264
272
  }
265
273
  }
266
- return { allowed: true, reason: 'OK', identityVerified };
274
+ if (this.policy.requireIdentityBinding && !identityVerified) {
275
+ return { allowed: false, reason: 'IDENTITY_FAIL: DNS Binding could not be verified.', identityVerified: false };
276
+ }
277
+ // Return verified status so handleAgentStrict can proceed to Policy Check
278
+ return { allowed: true, reason: 'OK', identityVerified: true };
267
279
  }
268
280
  async verifyDnsBinding(domain, requestKeySpki) {
269
281
  try {
package/dist/types.d.ts CHANGED
@@ -97,7 +97,7 @@ export interface FeedbackSignal {
97
97
  }
98
98
  export interface EvaluationResult {
99
99
  allowed: boolean;
100
- status: 200 | 400 | 401 | 403;
100
+ status: 200 | 400 | 401 | 402 | 403;
101
101
  reason: string;
102
102
  visitorType: 'VERIFIED_AGENT' | 'LIKELY_HUMAN' | 'UNIDENTIFIED_BOT';
103
103
  metadata?: any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aamp/protocol",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "description": "TypeScript reference implementation of AAMP v1.1",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/publisher.ts CHANGED
@@ -69,63 +69,56 @@ export class AAMPPublisher {
69
69
 
70
70
  /**
71
71
  * Main Entry Point: Evaluate ANY visitor (Human, Bot, or Agent)
72
+ * STAGE 1: IDENTITY (Strict)
73
+ * STAGE 2: POLICY (Permissions)
74
+ * STAGE 3: ACCESS (HQ Content)
72
75
  */
73
76
  async evaluateVisitor(
74
77
  reqHeaders: Record<string, string | undefined>,
75
78
  rawPayload?: string
76
79
  ): Promise<EvaluationResult> {
80
+ console.log(`\n--- [AAMP LOG START] New Request ---`);
77
81
 
78
- // 1. Check for AAMP Headers
79
- const hasAamp = reqHeaders[HEADERS.PAYLOAD] && reqHeaders[HEADERS.SIGNATURE] && reqHeaders[HEADERS.PUBLIC_KEY];
82
+ // --- STAGE 1: IDENTITY VERIFICATION ---
83
+ console.log(`[IDENTITY] šŸ” Checking Identity Headers...`);
80
84
 
81
- const feedbackToken = reqHeaders[HEADERS.FEEDBACK];
82
- if (feedbackToken) {
83
- await this.handleFeedback(feedbackToken, reqHeaders);
84
- }
85
+ const hasAamp = reqHeaders[HEADERS.PAYLOAD] && reqHeaders[HEADERS.SIGNATURE] && reqHeaders[HEADERS.PUBLIC_KEY];
85
86
 
86
87
  if (hasAamp) {
87
- console.log("\nšŸ”Ž [AAMP Middleware] Detected Agent Headers. Starting Verification...");
88
- // It claims to be an Agent. Verify it.
89
- return await this.handleAgent(reqHeaders, rawPayload);
88
+ // It claims to be an Agent. Verify it STRICTLY.
89
+ return await this.handleAgentStrict(reqHeaders, rawPayload);
90
90
  }
91
91
 
92
- // 2. It's not an AAMP Agent. Apply Strategy.
92
+ // If NO AAMP Headers -> FAIL IDENTITY immediately.
93
+ console.log(`[IDENTITY] āŒ FAILED. No AAMP Headers found.`);
94
+
95
+ // For now, retaining the legacy "Passive/Hybrid" switch just to avoid breaking browser demos completely
96
+ // BUT logging it as a specific "Identity Fail" flow.
93
97
  if (this.unauthenticatedStrategy === 'STRICT') {
98
+ console.log(`[IDENTITY] ā›” BLOCKING. Strategy is STRICT.`);
94
99
  return {
95
100
  allowed: false,
96
101
  status: 401,
97
- reason: "STRICT_MODE: Only AAMP verified agents allowed.",
102
+ reason: "IDENTITY_REQUIRED: Missing AAMP Headers.",
98
103
  visitorType: 'UNIDENTIFIED_BOT'
99
104
  };
100
105
  }
101
106
 
102
- if (this.unauthenticatedStrategy === 'PASSIVE') {
103
- return {
104
- allowed: true,
105
- status: 200,
106
- reason: "PASSIVE_MODE: Allowed without verification.",
107
- visitorType: 'LIKELY_HUMAN'
108
- };
109
- }
110
-
111
- // 3. HYBRID MODE: Heuristic Analysis (The "Lazy Bot" Filter)
107
+ console.log(`[IDENTITY] āš ļø SKIPPED (Legacy Mode). Checking Browser Heuristics...`);
112
108
  const isHuman = this.performBrowserHeuristics(reqHeaders);
113
-
114
109
  if (isHuman) {
115
- return {
116
- allowed: true,
117
- status: 200,
118
- reason: "BROWSER_VERIFIED: Heuristics passed.",
119
- visitorType: 'LIKELY_HUMAN'
120
- };
121
- } else {
122
- return {
123
- allowed: false,
124
- status: 403,
125
- reason: "BOT_DETECTED: Request lacks browser signatures and AAMP headers.",
126
- visitorType: 'UNIDENTIFIED_BOT'
127
- };
110
+ console.log(`[POLICY] šŸ‘¤ ALLOWED. Browser Heuristics Passed.`);
111
+ return { allowed: true, status: 200, reason: "BROWSER_VERIFIED", visitorType: 'LIKELY_HUMAN' };
128
112
  }
113
+
114
+ console.log(`[IDENTITY] āŒ FAILED. Not a Browser, No Headers.`);
115
+ console.log(`[ACCESS] ā›” BLOCKED.`);
116
+ return {
117
+ allowed: false,
118
+ status: 403,
119
+ reason: "IDENTITY_FAIL: No Identity, No Browser.",
120
+ visitorType: 'UNIDENTIFIED_BOT'
121
+ };
129
122
  }
130
123
 
131
124
  /**
@@ -161,17 +154,24 @@ export class AAMPPublisher {
161
154
  }
162
155
 
163
156
  /**
164
- * Handle AAMP Protocol Logic
157
+ * Handle AAMP Protocol Logic (Strict Mode)
165
158
  */
166
- private async handleAgent(reqHeaders: Record<string, string | undefined>, rawPayload?: string): Promise<EvaluationResult> {
159
+ private async handleAgentStrict(reqHeaders: Record<string, string | undefined>, rawPayload?: string): Promise<EvaluationResult> {
160
+ let agentId = "UNKNOWN";
161
+
167
162
  try {
163
+ // 1. Decode Headers
168
164
  const payloadHeader = reqHeaders[HEADERS.PAYLOAD]!;
169
165
  const sigHeader = reqHeaders[HEADERS.SIGNATURE]!;
170
166
  const keyHeader = reqHeaders[HEADERS.PUBLIC_KEY]!;
171
167
 
172
168
  const headerJson = atob(payloadHeader);
173
169
  const requestHeader = JSON.parse(headerJson);
170
+ agentId = requestHeader.agent_id;
174
171
 
172
+ console.log(`[IDENTITY] šŸ†” Claimed ID: ${agentId}`);
173
+
174
+ // 2. Crypto & DNS Verification
175
175
  const signedRequest: SignedAccessRequest = {
176
176
  header: requestHeader,
177
177
  signature: sigHeader,
@@ -186,151 +186,173 @@ export class AAMPPublisher {
186
186
  ["verify"]
187
187
  );
188
188
 
189
+ // Verify Core Logic (DNS + Crypto)
190
+ const verification = await this.verifyRequestLogic(signedRequest, agentKey);
191
+
192
+ if (!verification.identityVerified) {
193
+ console.log(`[IDENTITY] āŒ FAILED. Reason: ${verification.reason}`);
194
+ console.log(`[ACCESS] ā›” BLOCKED.`);
195
+ return { allowed: false, status: 403, reason: verification.reason, visitorType: 'UNIDENTIFIED_BOT' };
196
+ }
197
+
198
+ console.log(`[IDENTITY] āœ… PASSED. DNS Binding Verified.`);
199
+
200
+ // --- STAGE 2: POLICY ENFORCEMENT ---
201
+ console.log(`[POLICY] šŸ“œ Checking Permissions for ${agentId}...`);
202
+
189
203
  const proofToken = reqHeaders[HEADERS.PROOF_TOKEN];
190
204
  const paymentCredential = reqHeaders[HEADERS.PAYMENT_CREDENTIAL];
191
205
 
192
- // Verify Core Logic
193
- const result = await this.verifyRequestLogic(signedRequest, agentKey, proofToken, paymentCredential, headerJson);
194
-
195
- if (!result.allowed) {
196
- console.log(`ā›” [AAMP BLOCK]
197
- Agent: ${requestHeader.agent_id}
198
- Reason: ${result.reason}
199
- VisitorType: ${result.visitorType}
200
- Proof: ${result.proofUsed || 'None'}
201
- Identity Verified: ${result.identityVerified}`);
202
-
203
- return {
204
- allowed: false,
205
- status: 403,
206
- reason: result.reason,
207
- visitorType: 'VERIFIED_AGENT'
208
- };
206
+ const policyResult = await this.checkPolicyStrict(requestHeader, proofToken, paymentCredential);
207
+
208
+ if (!policyResult.allowed) {
209
+ console.log(`[POLICY] ā›” DENIED. Reason: ${policyResult.reason}`);
210
+ console.log(`[ACCESS] ā›” BLOCKED.`);
211
+ return policyResult;
209
212
  }
210
213
 
211
- console.log(`āœ… [AAMP ALLOW]
212
- Agent: ${requestHeader.agent_id}
213
- Reason: AAMP_VERIFIED
214
- Payment Method: ${result.proofUsed}
215
- Identity Verified: ${result.identityVerified}`);
214
+ // --- STAGE 3: ACCESS GRANT ---
215
+ console.log(`[POLICY] āœ… PASSED. Requirements Met.`);
216
+ console.log(`[ACCESS] šŸ”“ GRANTED. Unlocking HQ Content.`);
216
217
 
217
218
  return {
218
219
  allowed: true,
219
220
  status: 200,
220
221
  reason: "AAMP_VERIFIED",
221
222
  visitorType: 'VERIFIED_AGENT',
222
- metadata: requestHeader
223
+ metadata: requestHeader,
224
+ proofUsed: policyResult.proofUsed
223
225
  };
224
226
 
225
227
  } catch (e) {
226
- console.error(e);
228
+ console.error(`[AAMP ERROR]`, e);
227
229
  return { allowed: false, status: 400, reason: "INVALID_SIGNATURE", visitorType: 'UNIDENTIFIED_BOT' };
228
230
  }
229
231
  }
230
232
 
231
- private async verifyRequestLogic(
232
- request: SignedAccessRequest,
233
- requestPublicKey: CryptoKey,
234
- proofToken?: string,
235
- paymentCredential?: string,
236
- rawPayload?: string
237
- ): Promise<VerificationResult> {
238
-
239
- // 1. Replay Attack Prevention
240
- const requestTime = new Date(request.header.ts).getTime();
241
- if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
242
- return { allowed: false, reason: 'TIMESTAMP_INVALID', identityVerified: false };
243
- }
244
-
245
- // 2. Crypto Verification
246
- const signableString = rawPayload || JSON.stringify(request.header);
247
- const isCryptoValid = await verifySignature(requestPublicKey, signableString, request.signature);
248
- if (!isCryptoValid) return { allowed: false, reason: 'CRYPTO_FAIL', identityVerified: false };
249
-
250
- // 3. Identity Verification (DNS Binding) with Cache
251
- let identityVerified = false;
252
- const claimedDomain = request.header.agent_id;
253
- const pubKeyString = await exportPublicKey(requestPublicKey);
254
-
255
- console.log(` šŸ†” [AAMP Identity] Verifying DNS Binding for: ${claimedDomain}`);
256
-
257
- // Check Cache First
258
- const cachedKey = await this.cache.get(claimedDomain);
259
-
260
- if (cachedKey === pubKeyString) {
261
- console.log(" ⚔ [AAMP Cache] Identity found in cache.");
262
- identityVerified = true;
263
- } else if (this.isDomain(claimedDomain)) {
264
- // Cache Miss: Perform DNS Fetch
265
- identityVerified = await this.verifyDnsBinding(claimedDomain, pubKeyString);
266
- if (identityVerified) {
267
- await this.cache.set(claimedDomain, pubKeyString, this.CACHE_TTL_SECONDS);
268
- }
269
- }
233
+ // Legacy handler kept for interface compatibility (deprecated)
234
+ private async handleAgent(reqHeaders: Record<string, string | undefined>, rawPayload?: string): Promise<EvaluationResult> {
235
+ return this.handleAgentStrict(reqHeaders, rawPayload);
236
+ }
270
237
 
271
- if (this.policy.requireIdentityBinding && !identityVerified) {
272
- return { allowed: false, reason: 'IDENTITY_FAIL: DNS Binding could not be verified.', identityVerified: false };
273
- }
238
+ /**
239
+ * STAGE 2: POLICY ENFORCEMENT CHECK
240
+ */
241
+ private async checkPolicyStrict(
242
+ requestHeader: any,
243
+ proofToken?: string,
244
+ paymentCredential?: string
245
+ ): Promise<EvaluationResult> {
274
246
 
275
- // 4. Policy Check: Purpose
276
- if (request.header.purpose === AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
277
- return { allowed: false, reason: 'POLICY_DENIED: Training not allowed.', identityVerified };
247
+ // 1. Policy Check: Purpose Ban (e.g. No Training)
248
+ if (requestHeader.purpose === AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
249
+ return { allowed: false, status: 403, reason: 'POLICY_DENIED: Training not allowed.', visitorType: 'VERIFIED_AGENT' };
278
250
  }
279
- if (request.header.purpose === AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
280
- return { allowed: false, reason: 'POLICY_DENIED: RAG not allowed.', identityVerified };
251
+ if (requestHeader.purpose === AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
252
+ return { allowed: false, status: 403, reason: 'POLICY_DENIED: RAG not allowed.', visitorType: 'VERIFIED_AGENT' };
281
253
  }
282
254
 
283
- // 5. Policy Check: Economics (v1.2)
255
+ // 2. Policy Check: Economics (v1.2) - Payment & Ads
284
256
  if (this.policy.requiresPayment) {
285
257
  let paymentSatisfied = false;
286
258
 
287
259
  // Method A: Flexible Payment Callback (DB / Custom Logic)
288
260
  if (this.policy.monetization?.checkPayment) {
289
- const isPaid = await this.policy.monetization.checkPayment(request.header.agent_id, request.header.purpose);
261
+ const isPaid = await this.policy.monetization.checkPayment(requestHeader.agent_id, requestHeader.purpose);
290
262
  if (isPaid) {
291
- console.log(` šŸ’° [AAMP Audit] Whitelist Check Passed via Callback.`);
292
- return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'WHITELIST_CALLBACK' };
263
+ console.log(`[POLICY] šŸ’° Payment Verified via Callback.`);
264
+ return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'WHITELIST_CALLBACK' };
293
265
  }
294
266
  }
295
267
 
296
268
  // Method B: Payment Credentials (Unified JWT)
297
269
  if (!paymentSatisfied && this.policy.monetization?.paymentConfig && paymentCredential) {
298
270
  const { jwksUrl, issuer } = this.policy.monetization.paymentConfig;
299
- console.log(` šŸ” [AAMP Audit] Verifying Payment Credential (Issuer: ${issuer})...`);
271
+ console.log(`[POLICY] šŸ” Verifying Payment Credential (Issuer: ${issuer})...`);
300
272
 
301
273
  const isValidCredential = await verifyJwt(paymentCredential, jwksUrl, issuer);
302
274
  if (isValidCredential) {
303
- console.log(` āœ… [AAMP Audit] Credential Signature VALID.`);
304
- return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'PAYMENT_CREDENTIAL_JWT' };
275
+ console.log(`[POLICY] āœ… Credential Signature VALID.`);
276
+ return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'PAYMENT_CREDENTIAL_JWT' };
305
277
  } else {
306
- console.log(` āŒ [AAMP Audit] Credential Signature INVALID.`);
278
+ console.log(`[POLICY] āŒ Credential Signature INVALID.`);
307
279
  }
308
280
  }
309
281
 
310
282
  // Method C: Ad-Supported (Proof Verification)
311
- if (!paymentSatisfied && this.policy.allowAdSupportedAccess && request.header.context.ads_displayed) {
283
+ if (!paymentSatisfied && this.policy.allowAdSupportedAccess && requestHeader.context?.ads_displayed) {
312
284
  if (proofToken && this.policy.monetization?.adNetwork) {
313
285
  const { jwksUrl, issuer } = this.policy.monetization.adNetwork;
314
- console.log(` šŸ“ŗ [AAMP Audit] Verifying Ad Proof (Issuer: ${issuer})...`);
286
+ console.log(`[POLICY] šŸ“ŗ Verifying Ad Proof (Issuer: ${issuer})...`);
315
287
 
316
288
  const isValidProof = await verifyJwt(proofToken, jwksUrl, issuer);
317
289
  if (isValidProof) {
318
- console.log(` āœ… [AAMP Audit] Ad Proof Signature VALID.`);
319
- return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'AD_PROOF_JWT' };
290
+ console.log(`[POLICY] āœ… Ad Proof Signature VALID.`);
291
+ return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'AD_PROOF_JWT' };
320
292
  } else {
321
- console.log(` āŒ [AAMP Audit] Ad Proof Signature INVALID.`);
293
+ console.log(`[POLICY] āŒ Ad Proof Signature INVALID.`);
322
294
  }
323
295
  } else {
324
- console.log(` āš ļø [AAMP Audit] Ad Proof MISSING.`);
296
+ console.log(`[POLICY] āš ļø Ad Proof MISSING.`);
325
297
  }
326
298
  }
327
299
 
328
- if (!paymentSatisfied) {
329
- return { allowed: false, reason: 'PAYMENT_REQUIRED: Whitelist, Credential, and Ad Proof checks ALL failed.', identityVerified, proofUsed: 'NONE', visitorType: 'VERIFIED_AGENT' };
300
+ return {
301
+ allowed: false,
302
+ status: 402,
303
+ reason: 'PAYMENT_REQUIRED: Whitelist, Credential, and Ad Proof checks ALL failed.',
304
+ visitorType: 'VERIFIED_AGENT',
305
+ proofUsed: 'NONE'
306
+ };
307
+ }
308
+
309
+ // If no payment required, allow.
310
+ return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT' };
311
+ }
312
+
313
+ private async verifyRequestLogic(
314
+ request: SignedAccessRequest,
315
+ requestPublicKey: CryptoKey,
316
+ ): Promise<VerificationResult> {
317
+
318
+ // 1. Replay Attack Prevention
319
+ const requestTime = new Date(request.header.ts).getTime();
320
+ if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
321
+ return { allowed: false, reason: 'TIMESTAMP_INVALID: Clock skew too large.', identityVerified: false };
322
+ }
323
+
324
+ // 2. Crypto Verification
325
+ const signableString = JSON.stringify(request.header);
326
+ const isCryptoValid = await verifySignature(requestPublicKey, signableString, request.signature);
327
+ if (!isCryptoValid) return { allowed: false, reason: 'CRYPTO_FAIL: Signature invalid.', identityVerified: false };
328
+
329
+ // 3. Identity Verification (DNS Binding) with Cache
330
+ let identityVerified = false;
331
+ const claimedDomain = request.header.agent_id;
332
+ const pubKeyString = await exportPublicKey(requestPublicKey);
333
+
334
+ console.log(`[IDENTITY] šŸ” Verifying DNS Binding for: ${claimedDomain}`);
335
+
336
+ // Check Cache First
337
+ const cachedKey = await this.cache.get(claimedDomain);
338
+
339
+ if (cachedKey === pubKeyString) {
340
+ console.log("[IDENTITY] ⚔ Cache Hit. Identity Verified.");
341
+ identityVerified = true;
342
+ } else if (this.isDomain(claimedDomain)) {
343
+ // Cache Miss: Perform DNS Fetch
344
+ identityVerified = await this.verifyDnsBinding(claimedDomain, pubKeyString);
345
+ if (identityVerified) {
346
+ await this.cache.set(claimedDomain, pubKeyString, this.CACHE_TTL_SECONDS);
330
347
  }
331
348
  }
332
349
 
333
- return { allowed: true, reason: 'OK', identityVerified };
350
+ if (this.policy.requireIdentityBinding && !identityVerified) {
351
+ return { allowed: false, reason: 'IDENTITY_FAIL: DNS Binding could not be verified.', identityVerified: false };
352
+ }
353
+
354
+ // Return verified status so handleAgentStrict can proceed to Policy Check
355
+ return { allowed: true, reason: 'OK', identityVerified: true };
334
356
  }
335
357
 
336
358
  private async verifyDnsBinding(domain: string, requestKeySpki: string): Promise<boolean> {
package/src/types.ts CHANGED
@@ -128,7 +128,7 @@ export interface FeedbackSignal {
128
128
  // Result of the full evaluation pipeline
129
129
  export interface EvaluationResult {
130
130
  allowed: boolean;
131
- status: 200 | 400 | 401 | 403;
131
+ status: 200 | 400 | 401 | 402 | 403;
132
132
  reason: string;
133
133
  visitorType: 'VERIFIED_AGENT' | 'LIKELY_HUMAN' | 'UNIDENTIFIED_BOT';
134
134
  metadata?: any;