@aamp/protocol 1.1.5 → 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.
package/src/proof.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { createRemoteJWKSet, jwtVerify } from 'jose';
2
+
3
+ // In-memory cache for JWKS to avoid repeated fetches
4
+ // Jose's createRemoteJWKSet handles caching/cooldowns internally.
5
+
6
+ /**
7
+ * Verifies a JWT (Proof Token or Payment Credential) using JWKS.
8
+ *
9
+ * @param token The JWT string
10
+ * @param jwksUrl The URL to fetch Public Keys
11
+ * @param issuer The expected issuer
12
+ * @param audience The expected audience range
13
+ */
14
+ export async function verifyJwt(
15
+ token: string,
16
+ jwksUrl: string,
17
+ issuer: string,
18
+ audience?: string
19
+ ): Promise<boolean> {
20
+ try {
21
+ const JWKS = createRemoteJWKSet(new URL(jwksUrl));
22
+
23
+ const { payload } = await jwtVerify(token, JWKS, {
24
+ issuer: issuer,
25
+ audience: audience // specific audience check if provided
26
+ });
27
+
28
+ // Check specific AAMP claims if we standardize them
29
+ // if (payload.type !== 'AD_IMPRESSION') return false;
30
+
31
+ return true;
32
+ } catch (error) {
33
+ // console.error("Ad Proof Verification Failed:", error);
34
+ return false;
35
+ }
36
+ }
package/src/publisher.ts CHANGED
@@ -2,14 +2,17 @@
2
2
  * Layer 2: Publisher Middleware
3
3
  * Used by content owners to enforce policy, log access, and filter bots.
4
4
  */
5
- import { HEADERS, MAX_CLOCK_SKEW_MS, WELL_KNOWN_AGENT_PATH } from './constants';
6
- import { exportPublicKey, signData, verifySignature } from './crypto';
7
- import { AccessPolicy, AccessPurpose, AgentIdentityManifest, ContentOrigin, EvaluationResult, IdentityCache, SignedAccessRequest, UnauthenticatedStrategy } from './types';
5
+ import { HEADERS, MAX_CLOCK_SKEW_MS, WELL_KNOWN_AGENT_PATH } from './constants.js';
6
+ import { exportPublicKey, signData, verifySignature } from './crypto.js';
7
+ import { AccessPolicy, AccessPurpose, AgentIdentityManifest, ContentOrigin, EvaluationResult, IdentityCache, SignedAccessRequest, UnauthenticatedStrategy } from './types.js';
8
+ import { verifyJwt } from './proof.js';
8
9
 
9
10
  interface VerificationResult {
10
11
  allowed: boolean;
11
12
  reason: string;
12
13
  identityVerified: boolean;
14
+ proofUsed?: string; // "WHITELIST", "CREDENTIAL_JWT", "AD_JWT"
15
+ visitorType?: string; // For audit logs
13
16
  }
14
17
 
15
18
  /**
@@ -18,7 +21,7 @@ interface VerificationResult {
18
21
  */
19
22
  class MemoryCache implements IdentityCache {
20
23
  private store = new Map<string, { val: string, exp: number }>();
21
-
24
+
22
25
  async get(key: string): Promise<string | null> {
23
26
  const item = this.store.get(key);
24
27
  if (!item) return null;
@@ -28,11 +31,11 @@ class MemoryCache implements IdentityCache {
28
31
  }
29
32
  return item.val;
30
33
  }
31
-
34
+
32
35
  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
+ this.store.set(key, {
37
+ val: value,
38
+ exp: Date.now() + (ttlSeconds * 1000)
36
39
  });
37
40
  }
38
41
  }
@@ -42,14 +45,14 @@ export class AAMPPublisher {
42
45
  private keyPair: CryptoKeyPair | null = null;
43
46
  private unauthenticatedStrategy: UnauthenticatedStrategy;
44
47
  private cache: IdentityCache;
45
-
48
+
46
49
  // Default TTL: 1 Hour
47
- private readonly CACHE_TTL_SECONDS = 3600;
50
+ private readonly CACHE_TTL_SECONDS = 3600;
48
51
 
49
52
  constructor(
50
- policy: AccessPolicy,
53
+ policy: AccessPolicy,
51
54
  strategy: UnauthenticatedStrategy = 'PASSIVE',
52
- cacheImpl?: IdentityCache
55
+ cacheImpl?: IdentityCache
53
56
  ) {
54
57
  this.policy = policy;
55
58
  this.unauthenticatedStrategy = strategy;
@@ -66,58 +69,56 @@ export class AAMPPublisher {
66
69
 
67
70
  /**
68
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)
69
75
  */
70
76
  async evaluateVisitor(
71
- reqHeaders: Record<string, string | undefined>,
77
+ reqHeaders: Record<string, string | undefined>,
72
78
  rawPayload?: string
73
79
  ): Promise<EvaluationResult> {
74
-
75
- // 1. Check for AAMP Headers
80
+ console.log(`\n--- [AAMP LOG START] New Request ---`);
81
+
82
+ // --- STAGE 1: IDENTITY VERIFICATION ---
83
+ console.log(`[IDENTITY] 🔍 Checking Identity Headers...`);
84
+
76
85
  const hasAamp = reqHeaders[HEADERS.PAYLOAD] && reqHeaders[HEADERS.SIGNATURE] && reqHeaders[HEADERS.PUBLIC_KEY];
77
86
 
78
87
  if (hasAamp) {
79
- console.log("\n🔎 [AAMP Middleware] Detected Agent Headers. Starting Verification...");
80
- // It claims to be an Agent. Verify it.
81
- return await this.handleAgent(reqHeaders, rawPayload);
88
+ // It claims to be an Agent. Verify it STRICTLY.
89
+ return await this.handleAgentStrict(reqHeaders, rawPayload);
82
90
  }
83
91
 
84
- // 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.
85
97
  if (this.unauthenticatedStrategy === 'STRICT') {
98
+ console.log(`[IDENTITY] ⛔ BLOCKING. Strategy is STRICT.`);
86
99
  return {
87
100
  allowed: false,
88
101
  status: 401,
89
- reason: "STRICT_MODE: Only AAMP verified agents allowed.",
102
+ reason: "IDENTITY_REQUIRED: Missing AAMP Headers.",
90
103
  visitorType: 'UNIDENTIFIED_BOT'
91
104
  };
92
105
  }
93
106
 
94
- if (this.unauthenticatedStrategy === 'PASSIVE') {
95
- return {
96
- allowed: true,
97
- status: 200,
98
- reason: "PASSIVE_MODE: Allowed without verification.",
99
- visitorType: 'LIKELY_HUMAN'
100
- };
101
- }
102
-
103
- // 3. HYBRID MODE: Heuristic Analysis (The "Lazy Bot" Filter)
107
+ console.log(`[IDENTITY] ⚠️ SKIPPED (Legacy Mode). Checking Browser Heuristics...`);
104
108
  const isHuman = this.performBrowserHeuristics(reqHeaders);
105
-
106
109
  if (isHuman) {
107
- return {
108
- allowed: true,
109
- status: 200,
110
- reason: "BROWSER_VERIFIED: Heuristics passed.",
111
- visitorType: 'LIKELY_HUMAN'
112
- };
113
- } else {
114
- return {
115
- allowed: false,
116
- status: 403,
117
- reason: "BOT_DETECTED: Request lacks browser signatures and AAMP headers.",
118
- visitorType: 'UNIDENTIFIED_BOT'
119
- };
110
+ console.log(`[POLICY] 👤 ALLOWED. Browser Heuristics Passed.`);
111
+ return { allowed: true, status: 200, reason: "BROWSER_VERIFIED", visitorType: 'LIKELY_HUMAN' };
120
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
+ };
121
122
  }
122
123
 
123
124
  /**
@@ -128,11 +129,11 @@ export class AAMPPublisher {
128
129
  */
129
130
  private performBrowserHeuristics(headers: Record<string, string | undefined>): boolean {
130
131
  const userAgent = headers['user-agent'] || '';
131
-
132
+
132
133
  // A. The "Obvious Bot" Blocklist (Fast Fail)
133
134
  const botSignatures = ['python-requests', 'curl', 'wget', 'scrapy', 'bot', 'crawler', 'spider'];
134
135
  if (botSignatures.some(sig => userAgent.toLowerCase().includes(sig))) {
135
- return false;
136
+ return false;
136
137
  }
137
138
 
138
139
  // B. Trusted Infrastructure Signals (The Real World Solution)
@@ -142,7 +143,7 @@ export class AAMPPublisher {
142
143
 
143
144
  // C. The "Browser Fingerprint" (Fallback for direct connections)
144
145
  const hasAcceptLanguage = !!headers['accept-language'];
145
- const hasSecFetchDest = !!headers['sec-fetch-dest'];
146
+ const hasSecFetchDest = !!headers['sec-fetch-dest'];
146
147
  const hasUpgradeInsecure = !!headers['upgrade-insecure-requests'];
147
148
 
148
149
  if (hasAcceptLanguage && (hasSecFetchDest || hasUpgradeInsecure)) {
@@ -153,17 +154,24 @@ export class AAMPPublisher {
153
154
  }
154
155
 
155
156
  /**
156
- * Handle AAMP Protocol Logic
157
+ * Handle AAMP Protocol Logic (Strict Mode)
157
158
  */
158
- 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
+
159
162
  try {
163
+ // 1. Decode Headers
160
164
  const payloadHeader = reqHeaders[HEADERS.PAYLOAD]!;
161
165
  const sigHeader = reqHeaders[HEADERS.SIGNATURE]!;
162
166
  const keyHeader = reqHeaders[HEADERS.PUBLIC_KEY]!;
163
167
 
164
- const headerJson = atob(payloadHeader);
168
+ const headerJson = atob(payloadHeader);
165
169
  const requestHeader = JSON.parse(headerJson);
166
-
170
+ agentId = requestHeader.agent_id;
171
+
172
+ console.log(`[IDENTITY] 🆔 Claimed ID: ${agentId}`);
173
+
174
+ // 2. Crypto & DNS Verification
167
175
  const signedRequest: SignedAccessRequest = {
168
176
  header: requestHeader,
169
177
  signature: sigHeader,
@@ -178,63 +186,158 @@ export class AAMPPublisher {
178
186
  ["verify"]
179
187
  );
180
188
 
181
- // Verify Core Logic
182
- const result = await this.verifyRequestLogic(signedRequest, agentKey, headerJson);
183
-
184
- if (!result.allowed) {
185
- console.log(`⛔ [AAMP Deny] Reason: ${result.reason}`);
186
- return {
187
- allowed: false,
188
- status: 403,
189
- reason: result.reason,
190
- visitorType: 'VERIFIED_AGENT'
191
- };
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' };
192
196
  }
193
197
 
194
- console.log(`✅ [AAMP Allow] Verified Agent: ${requestHeader.agent_id}`);
198
+ console.log(`[IDENTITY] PASSED. DNS Binding Verified.`);
199
+
200
+ // --- STAGE 2: POLICY ENFORCEMENT ---
201
+ console.log(`[POLICY] 📜 Checking Permissions for ${agentId}...`);
202
+
203
+ const proofToken = reqHeaders[HEADERS.PROOF_TOKEN];
204
+ const paymentCredential = reqHeaders[HEADERS.PAYMENT_CREDENTIAL];
205
+
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;
212
+ }
213
+
214
+ // --- STAGE 3: ACCESS GRANT ---
215
+ console.log(`[POLICY] ✅ PASSED. Requirements Met.`);
216
+ console.log(`[ACCESS] 🔓 GRANTED. Unlocking HQ Content.`);
217
+
195
218
  return {
196
219
  allowed: true,
197
220
  status: 200,
198
221
  reason: "AAMP_VERIFIED",
199
222
  visitorType: 'VERIFIED_AGENT',
200
- metadata: requestHeader
223
+ metadata: requestHeader,
224
+ proofUsed: policyResult.proofUsed
201
225
  };
202
226
 
203
227
  } catch (e) {
204
- console.error(e);
228
+ console.error(`[AAMP ERROR]`, e);
205
229
  return { allowed: false, status: 400, reason: "INVALID_SIGNATURE", visitorType: 'UNIDENTIFIED_BOT' };
206
230
  }
207
231
  }
208
232
 
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
+ }
237
+
238
+ /**
239
+ * STAGE 2: POLICY ENFORCEMENT CHECK
240
+ */
241
+ private async checkPolicyStrict(
242
+ requestHeader: any,
243
+ proofToken?: string,
244
+ paymentCredential?: string
245
+ ): Promise<EvaluationResult> {
246
+
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' };
250
+ }
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' };
253
+ }
254
+
255
+ // 2. Policy Check: Economics (v1.2) - Payment & Ads
256
+ if (this.policy.requiresPayment) {
257
+ let paymentSatisfied = false;
258
+
259
+ // Method A: Flexible Payment Callback (DB / Custom Logic)
260
+ if (this.policy.monetization?.checkPayment) {
261
+ const isPaid = await this.policy.monetization.checkPayment(requestHeader.agent_id, requestHeader.purpose);
262
+ if (isPaid) {
263
+ console.log(`[POLICY] 💰 Payment Verified via Callback.`);
264
+ return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'WHITELIST_CALLBACK' };
265
+ }
266
+ }
267
+
268
+ // Method B: Payment Credentials (Unified JWT)
269
+ if (!paymentSatisfied && this.policy.monetization?.paymentConfig && paymentCredential) {
270
+ const { jwksUrl, issuer } = this.policy.monetization.paymentConfig;
271
+ console.log(`[POLICY] 🔐 Verifying Payment Credential (Issuer: ${issuer})...`);
272
+
273
+ const isValidCredential = await verifyJwt(paymentCredential, jwksUrl, issuer);
274
+ if (isValidCredential) {
275
+ console.log(`[POLICY] ✅ Credential Signature VALID.`);
276
+ return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'PAYMENT_CREDENTIAL_JWT' };
277
+ } else {
278
+ console.log(`[POLICY] ❌ Credential Signature INVALID.`);
279
+ }
280
+ }
281
+
282
+ // Method C: Ad-Supported (Proof Verification)
283
+ if (!paymentSatisfied && this.policy.allowAdSupportedAccess && requestHeader.context?.ads_displayed) {
284
+ if (proofToken && this.policy.monetization?.adNetwork) {
285
+ const { jwksUrl, issuer } = this.policy.monetization.adNetwork;
286
+ console.log(`[POLICY] 📺 Verifying Ad Proof (Issuer: ${issuer})...`);
287
+
288
+ const isValidProof = await verifyJwt(proofToken, jwksUrl, issuer);
289
+ if (isValidProof) {
290
+ console.log(`[POLICY] ✅ Ad Proof Signature VALID.`);
291
+ return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'AD_PROOF_JWT' };
292
+ } else {
293
+ console.log(`[POLICY] ❌ Ad Proof Signature INVALID.`);
294
+ }
295
+ } else {
296
+ console.log(`[POLICY] ⚠️ Ad Proof MISSING.`);
297
+ }
298
+ }
299
+
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
+
209
313
  private async verifyRequestLogic(
210
- request: SignedAccessRequest,
314
+ request: SignedAccessRequest,
211
315
  requestPublicKey: CryptoKey,
212
- rawPayload?: string
213
316
  ): Promise<VerificationResult> {
214
-
317
+
215
318
  // 1. Replay Attack Prevention
216
319
  const requestTime = new Date(request.header.ts).getTime();
217
320
  if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
218
- return { allowed: false, reason: 'TIMESTAMP_INVALID', identityVerified: false };
321
+ return { allowed: false, reason: 'TIMESTAMP_INVALID: Clock skew too large.', identityVerified: false };
219
322
  }
220
323
 
221
324
  // 2. Crypto Verification
222
- const signableString = rawPayload || JSON.stringify(request.header);
325
+ const signableString = JSON.stringify(request.header);
223
326
  const isCryptoValid = await verifySignature(requestPublicKey, signableString, request.signature);
224
- if (!isCryptoValid) return { allowed: false, reason: 'CRYPTO_FAIL', identityVerified: false };
327
+ if (!isCryptoValid) return { allowed: false, reason: 'CRYPTO_FAIL: Signature invalid.', identityVerified: false };
225
328
 
226
329
  // 3. Identity Verification (DNS Binding) with Cache
227
330
  let identityVerified = false;
228
331
  const claimedDomain = request.header.agent_id;
229
332
  const pubKeyString = await exportPublicKey(requestPublicKey);
230
-
231
- console.log(` 🆔 [AAMP Identity] Verifying DNS Binding for: ${claimedDomain}`);
333
+
334
+ console.log(`[IDENTITY] 🔍 Verifying DNS Binding for: ${claimedDomain}`);
232
335
 
233
336
  // Check Cache First
234
337
  const cachedKey = await this.cache.get(claimedDomain);
235
-
338
+
236
339
  if (cachedKey === pubKeyString) {
237
- console.log(" [AAMP Cache] Identity found in cache.");
340
+ console.log("[IDENTITY] ⚡ Cache Hit. Identity Verified.");
238
341
  identityVerified = true;
239
342
  } else if (this.isDomain(claimedDomain)) {
240
343
  // Cache Miss: Perform DNS Fetch
@@ -248,23 +351,8 @@ export class AAMPPublisher {
248
351
  return { allowed: false, reason: 'IDENTITY_FAIL: DNS Binding could not be verified.', identityVerified: false };
249
352
  }
250
353
 
251
- // 4. Policy Check: Purpose
252
- if (request.header.purpose === AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
253
- return { allowed: false, reason: 'POLICY_DENIED: Training not allowed.', identityVerified };
254
- }
255
- if (request.header.purpose === AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
256
- return { allowed: false, reason: 'POLICY_DENIED: RAG not allowed.', identityVerified };
257
- }
258
-
259
- // 5. Policy Check: Economics
260
- if (this.policy.requiresPayment) {
261
- const isAdExempt = this.policy.allowAdSupportedAccess && request.header.context.ads_displayed;
262
- if (!isAdExempt) {
263
- return { allowed: false, reason: 'PAYMENT_REQUIRED: Content requires payment or ads.', identityVerified };
264
- }
265
- }
266
-
267
- return { allowed: true, reason: 'OK', identityVerified };
354
+ // Return verified status so handleAgentStrict can proceed to Policy Check
355
+ return { allowed: true, reason: 'OK', identityVerified: true };
268
356
  }
269
357
 
270
358
  private async verifyDnsBinding(domain: string, requestKeySpki: string): Promise<boolean> {
@@ -272,16 +360,16 @@ export class AAMPPublisher {
272
360
  // Allow HTTP for localhost testing
273
361
  const protocol = (domain.includes('localhost') || domain.match(/:\d+$/)) ? 'http' : 'https';
274
362
  const url = `${protocol}://${domain}${WELL_KNOWN_AGENT_PATH}`;
275
-
363
+
276
364
  console.log(` 🌍 [AAMP DNS] Fetching Manifest: ${url} ...`);
277
365
 
278
366
  // In production, we need a short timeout to prevent hanging
279
367
  const controller = new AbortController();
280
368
  const timeoutId = setTimeout(() => controller.abort(), 1500); // 1.5s max for DNS check
281
-
369
+
282
370
  const response = await fetch(url, { signal: controller.signal });
283
371
  clearTimeout(timeoutId);
284
-
372
+
285
373
  if (!response.ok) {
286
374
  console.log(` ❌ [AAMP DNS] Fetch Failed: ${response.status}`);
287
375
  return false;
@@ -298,15 +386,15 @@ export class AAMPPublisher {
298
386
 
299
387
  // CHECK 2: Does the key match?
300
388
  if (manifest.public_key !== requestKeySpki) {
301
- console.log(` ❌ [AAMP DNS] Key Mismatch: DNS Key != Request Key`);
302
- return false;
389
+ console.log(` ❌ [AAMP DNS] Key Mismatch: DNS Key != Request Key`);
390
+ return false;
303
391
  }
304
392
 
305
393
  console.log(` ✅ [AAMP DNS] Identity Confirmed.`);
306
394
  return true;
307
- } catch (e: any) {
308
- console.log(` ❌ [AAMP DNS] Error: ${e.message}`);
309
- return false;
395
+ } catch (e: any) {
396
+ console.log(` ❌ [AAMP DNS] Error: ${e.message}`);
397
+ return false;
310
398
  }
311
399
  }
312
400
 
@@ -324,4 +412,28 @@ export class AAMPPublisher {
324
412
  [HEADERS.PROVENANCE_SIG]: signature
325
413
  };
326
414
  }
415
+
416
+ /**
417
+ * Handling Quality Feedback (The "Dispute" Layer)
418
+ * This runs when an Agent sends 'x-aamp-feedback'.
419
+ */
420
+ private async handleFeedback(token: string, headers: Record<string, string | undefined>) {
421
+ // NOTE: In production, you would fetch the Agent's specific key.
422
+ // For now, we assume standard Discovery or a centralized Key Set (like adNetwork).
423
+ // Ideally, the SDK config should have a 'qualityOracle' key set.
424
+
425
+ // 1. We just Decode it to Log it (Verification is optional but recommended)
426
+ try {
427
+ const parts = token.split('.');
428
+ const payload = JSON.parse(atob(parts[1]));
429
+
430
+ console.log(`\n📢 [AAMP QUALITY ALERT] Feedback Received from ${payload.agent_id}`);
431
+ console.log(` Reason: ${payload.reason} | Score: ${payload.quality_score}`);
432
+ console.log(` Resource: ${payload.url}`);
433
+ console.log(` (Signature available for dispute evidence)`);
434
+
435
+ } catch (e) {
436
+ console.log(` ⚠️ [AAMP Warning] Malformed Feedback Token.`);
437
+ }
438
+ }
327
439
  }
package/src/types.ts CHANGED
@@ -43,14 +43,35 @@ export interface IdentityCache {
43
43
  set(key: string, value: string, ttlSeconds: number): Promise<void>;
44
44
  }
45
45
 
46
+ /**
47
+ * Optional Monetization (The Settlement Layer)
48
+ */
46
49
  /**
47
50
  * Optional Monetization (The Settlement Layer)
48
51
  */
49
52
  export interface MonetizationConfig {
50
- method: 'BROKER' | 'CRYPTO' | 'PRIVATE_TREATY';
51
- location: string;
53
+ // Method 1: Payments (Flexible Callback)
54
+ // Developers implement their own logic (Database check, CMS lookup, etc.)
55
+ // Returns TRUE if the agent is a paid subscriber for this specific purpose.
56
+ checkPayment?: (agentId: string, purpose: string) => boolean | Promise<boolean>;
57
+
58
+ // Method 2: Ads (Proof Verification)
59
+ // Configuration to verify tokens from your Ad Provider (e.g. Google)
60
+ adNetwork?: {
61
+ jwksUrl: string; // e.g. "https://www.googleapis.com/oauth2/v3/certs"
62
+ issuer: string; // e.g. "https://accounts.google.com"
63
+ };
64
+
65
+ // Method 3: Payment Credentials (Unified JWT)
66
+ // Verifies "x-aamp-credential" for Broker or Direct payments.
67
+ paymentConfig?: {
68
+ jwksUrl: string; // e.g. "https://my-site.com/.well-known/jwks.json"
69
+ issuer: string; // e.g. "my-site.com"
70
+ };
52
71
  }
53
72
 
73
+
74
+
54
75
  /**
55
76
  * Handling Non-AAMP Visitors
56
77
  *
@@ -65,15 +86,15 @@ export interface AccessPolicy {
65
86
  allowTraining: boolean;
66
87
  allowRAG: boolean;
67
88
  attributionRequired: boolean;
68
-
89
+
69
90
  // Economic Signals
70
- allowAdSupportedAccess: boolean;
71
- requiresPayment: boolean;
91
+ allowAdSupportedAccess: boolean;
92
+ requiresPayment: boolean;
72
93
  paymentPointer?: string;
73
94
 
74
95
  // Identity Strictness
75
- requireIdentityBinding?: boolean;
76
-
96
+ requireIdentityBinding?: boolean;
97
+
77
98
  // V1.1: Optional Settlement Info
78
99
  monetization?: MonetizationConfig;
79
100
  }
@@ -81,7 +102,7 @@ export interface AccessPolicy {
81
102
  export interface ProtocolHeader {
82
103
  v: '1.1';
83
104
  ts: string;
84
- agent_id: string;
105
+ agent_id: string;
85
106
  resource: string;
86
107
  purpose: AccessPurpose;
87
108
  context: {
@@ -98,16 +119,28 @@ export interface SignedAccessRequest {
98
119
  export interface FeedbackSignal {
99
120
  target_resource: string;
100
121
  agent_id: string;
101
- quality_score: number;
122
+ quality_score: number;
102
123
  flags: QualityFlag[];
103
124
  timestamp: string;
104
125
  }
105
126
 
127
+ // Result of the full evaluation pipeline
106
128
  // Result of the full evaluation pipeline
107
129
  export interface EvaluationResult {
108
130
  allowed: boolean;
109
- status: 200 | 400 | 401 | 403;
131
+ status: 200 | 400 | 401 | 402 | 403;
110
132
  reason: string;
111
133
  visitorType: 'VERIFIED_AGENT' | 'LIKELY_HUMAN' | 'UNIDENTIFIED_BOT';
112
134
  metadata?: any;
135
+ payment_status?: 'PAID_SUBSCRIBER' | 'AD_FUNDED' | 'UNPAID';
136
+ proofUsed?: string;
137
+ }
138
+
139
+ // Signed Quality Feedback (The "Report Card")
140
+ export interface FeedbackSignalToken {
141
+ url: string; // The resource being flagged
142
+ agent_id: string; // Who is flagging it (e.g. "bot.openai.com")
143
+ quality_score: number; // 0.0 to 1.0
144
+ reason: string; // e.g. "SEO_SPAM", "HATE_SPEECH"
145
+ timestamp: number;
113
146
  }
@@ -3,10 +3,10 @@
3
3
  * Run using: npm test
4
4
  * Location: sdk/typescript/test/handshake.spec.ts
5
5
  */
6
- import { AAMPAgent } from '../src/agent';
7
- import { AAMPPublisher } from '../src/publisher';
8
- import { AccessPurpose } from '../src/types';
9
- import { HEADERS } from '../src/constants';
6
+ import { AAMPAgent } from '../src/agent.js';
7
+ import { AAMPPublisher } from '../src/publisher.js';
8
+ import { AccessPurpose } from '../src/types.js';
9
+ import { HEADERS } from '../src/constants.js';
10
10
 
11
11
  async function runTest() {
12
12
  console.log("--- STARTING AAMP HANDSHAKE TEST ---");
@@ -29,7 +29,7 @@ async function runTest() {
29
29
  // TEST CASE A: Requesting RAG without Ads (Should FAIL due to Payment Requirement)
30
30
  console.log("\n[TEST A] Requesting RAG (No Ads)...");
31
31
  const reqA = await agent.createAccessRequest('/doc/1', AccessPurpose.RAG_RETRIEVAL, { adsDisplayed: false });
32
-
32
+
33
33
  const payloadA = JSON.stringify(reqA.header);
34
34
  const headersA = {
35
35
  [HEADERS.PAYLOAD]: btoa(payloadA),
@@ -45,7 +45,7 @@ async function runTest() {
45
45
  // TEST CASE B: Requesting RAG WITH Ads (Should SUCCEED via Exemption)
46
46
  console.log("\n[TEST B] Requesting RAG (With Ads)...");
47
47
  const reqB = await agent.createAccessRequest('/doc/1', AccessPurpose.RAG_RETRIEVAL, { adsDisplayed: true });
48
-
48
+
49
49
  const payloadB = JSON.stringify(reqB.header);
50
50
  const headersB = {
51
51
  [HEADERS.PAYLOAD]: btoa(payloadB),
package/tsconfig.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2020",
4
- "module": "CommonJS",
5
- "lib": ["ES2020", "DOM"],
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": [
7
+ "ES2020",
8
+ "DOM"
9
+ ],
6
10
  "declaration": true,
7
11
  "outDir": "./dist",
8
12
  "rootDir": "./src",
@@ -11,5 +15,7 @@
11
15
  "skipLibCheck": true,
12
16
  "forceConsistentCasingInFileNames": true
13
17
  },
14
- "include": ["src/**/*"]
18
+ "include": [
19
+ "src/**/*"
20
+ ]
15
21
  }