@aamp/protocol 1.1.5 → 1.1.6

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/types.js CHANGED
@@ -1,28 +1,25 @@
1
- "use strict";
2
1
  /**
3
2
  * Layer 1: Protocol Definitions
4
3
  * Shared types used by both Agent and Publisher.
5
4
  */
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.QualityFlag = exports.ContentOrigin = exports.AccessPurpose = void 0;
8
- var AccessPurpose;
5
+ export var AccessPurpose;
9
6
  (function (AccessPurpose) {
10
7
  AccessPurpose["CRAWL_TRAINING"] = "CRAWL_TRAINING";
11
8
  AccessPurpose["RAG_RETRIEVAL"] = "RAG_RETRIEVAL";
12
9
  AccessPurpose["SUMMARY"] = "SUMMARY";
13
10
  AccessPurpose["QUOTATION"] = "QUOTATION";
14
11
  AccessPurpose["EMBEDDING"] = "EMBEDDING";
15
- })(AccessPurpose || (exports.AccessPurpose = AccessPurpose = {}));
16
- var ContentOrigin;
12
+ })(AccessPurpose || (AccessPurpose = {}));
13
+ export var ContentOrigin;
17
14
  (function (ContentOrigin) {
18
15
  ContentOrigin["HUMAN"] = "HUMAN";
19
16
  ContentOrigin["SYNTHETIC"] = "SYNTHETIC";
20
17
  ContentOrigin["HYBRID"] = "HYBRID"; // Edited by humans, drafted by AI.
21
- })(ContentOrigin || (exports.ContentOrigin = ContentOrigin = {}));
22
- var QualityFlag;
18
+ })(ContentOrigin || (ContentOrigin = {}));
19
+ export var QualityFlag;
23
20
  (function (QualityFlag) {
24
21
  QualityFlag["SEO_SPAM"] = "SEO_SPAM";
25
22
  QualityFlag["INACCURATE"] = "INACCURATE";
26
23
  QualityFlag["HATE_SPEECH"] = "HATE_SPEECH";
27
24
  QualityFlag["HIGH_QUALITY"] = "HIGH_QUALITY";
28
- })(QualityFlag || (exports.QualityFlag = QualityFlag = {}));
25
+ })(QualityFlag || (QualityFlag = {}));
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@aamp/protocol",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "TypeScript reference implementation of AAMP v1.1",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "type": "module",
7
8
  "scripts": {
8
9
  "build": "tsc",
9
- "test": "ts-node test/handshake.spec.ts",
10
- "prepublishOnly": "npm run test && npm run build"
10
+ "test": "node --loader ts-node/esm test/handshake.spec.ts"
11
11
  },
12
12
  "repository": {
13
13
  "type": "git",
@@ -17,8 +17,13 @@
17
17
  "access": "public"
18
18
  },
19
19
  "devDependencies": {
20
- "typescript": "^5.0.0",
20
+ "@types/node": "^20.0.0",
21
+ "@types/node-fetch": "^2.6.13",
21
22
  "ts-node": "^10.9.0",
22
- "@types/node": "^20.0.0"
23
+ "typescript": "^5.0.0"
24
+ },
25
+ "dependencies": {
26
+ "jose": "^6.1.3",
27
+ "node-fetch": "^2.7.0"
23
28
  }
24
29
  }
package/src/agent.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Layer 2: Agent SDK
3
3
  */
4
- import { AccessPurpose, ProtocolHeader, SignedAccessRequest, FeedbackSignal, QualityFlag, AgentIdentityManifest } from './types';
5
- import { generateKeyPair, signData, exportPublicKey } from './crypto';
6
- import { AAMP_VERSION } from './constants';
4
+ import { AccessPurpose, ProtocolHeader, SignedAccessRequest, FeedbackSignal, QualityFlag, AgentIdentityManifest } from './types.js';
5
+ import { generateKeyPair, signData, exportPublicKey } from './crypto.js';
6
+ import { AAMP_VERSION } from './constants.js';
7
7
 
8
8
  export interface AccessOptions {
9
9
  adsDisplayed?: boolean;
@@ -24,8 +24,8 @@ export class AAMPAgent {
24
24
  }
25
25
 
26
26
  async createAccessRequest(
27
- resource: string,
28
- purpose: AccessPurpose,
27
+ resource: string,
28
+ purpose: AccessPurpose,
29
29
  options: AccessOptions = {}
30
30
  ): Promise<SignedAccessRequest> {
31
31
  if (!this.keyPair) throw new Error("Agent not initialized. Call initialize() first.");
@@ -53,9 +53,9 @@ export class AAMPAgent {
53
53
  */
54
54
  async getIdentityManifest(contactEmail?: string): Promise<AgentIdentityManifest> {
55
55
  if (!this.keyPair) throw new Error("Agent not initialized.");
56
-
56
+
57
57
  const publicKey = await exportPublicKey(this.keyPair.publicKey);
58
-
58
+
59
59
  return {
60
60
  agent_id: this.agentId,
61
61
  public_key: publicKey,
@@ -73,7 +73,7 @@ export class AAMPAgent {
73
73
  flags: QualityFlag[]
74
74
  ): Promise<{ signal: FeedbackSignal, signature: string }> {
75
75
  if (!this.keyPair) throw new Error("Agent not initialized.");
76
-
76
+
77
77
  const signal: FeedbackSignal = {
78
78
  target_resource: resource,
79
79
  agent_id: this.agentId,
package/src/constants.ts CHANGED
@@ -17,15 +17,24 @@ export const HEADERS = {
17
17
  SIGNATURE: 'x-aamp-signature',
18
18
  // Transport: The Agent's Public Key (Base64 SPKI)
19
19
  PUBLIC_KEY: 'x-aamp-public-key',
20
-
20
+
21
21
  // Informational / Legacy (Optional if Payload is present)
22
22
  AGENT_ID: 'x-aamp-agent-id',
23
23
  TIMESTAMP: 'x-aamp-timestamp',
24
- ALGORITHM: 'x-aamp-alg',
25
-
24
+ ALGORITHM: 'x-aamp-alg',
25
+
26
26
  // v1.1 Addition: Provenance (Server to Agent)
27
27
  CONTENT_ORIGIN: 'x-aamp-content-origin',
28
- PROVENANCE_SIG: 'x-aamp-provenance-sig'
28
+ PROVENANCE_SIG: 'x-aamp-provenance-sig',
29
+
30
+ // v1.2 Proof of Value
31
+ PROOF_TOKEN: 'x-aamp-proof',
32
+
33
+ // v1.2 Payment Credential (The "Digital Receipt")
34
+ PAYMENT_CREDENTIAL: 'x-aamp-credential',
35
+
36
+ // v1.2 Quality Feedback (The "Dispute Token")
37
+ FEEDBACK: 'x-aamp-feedback'
29
38
  } as const;
30
39
 
31
40
  // Cryptographic Settings
package/src/express.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  * Layer 3: Framework Adapters
3
3
  * Zero-friction integration for Express/Node.js.
4
4
  */
5
- import { AAMPPublisher } from './publisher';
6
- import { AccessPolicy, ContentOrigin, UnauthenticatedStrategy, IdentityCache } from './types';
7
- import { generateKeyPair } from './crypto';
5
+ import { AAMPPublisher } from './publisher.js';
6
+ import { AccessPolicy, ContentOrigin, UnauthenticatedStrategy, IdentityCache } from './types.js';
7
+ import { generateKeyPair } from './crypto.js';
8
8
 
9
9
  export interface AAMPConfig {
10
10
  policy: Omit<AccessPolicy, 'version'>;
@@ -28,9 +28,9 @@ export class AAMP {
28
28
  config.strategy || 'PASSIVE',
29
29
  config.cache
30
30
  );
31
-
31
+
32
32
  this.origin = ContentOrigin[config.meta.origin];
33
-
33
+
34
34
  this.ready = generateKeyPair().then(keys => {
35
35
  return this.publisher.initialize(keys);
36
36
  });
@@ -62,9 +62,10 @@ export class AAMP {
62
62
 
63
63
  // Enforce Decision
64
64
  if (!result.allowed) {
65
- res.status(result.status).json({
65
+ res.status(result.status).json({
66
66
  error: result.reason,
67
- visitor_type: result.visitorType
67
+ visitor_type: result.visitorType,
68
+ proof_used: result.proofUsed
68
69
  });
69
70
  return;
70
71
  }
package/src/index.ts CHANGED
@@ -4,10 +4,10 @@
4
4
  * This is the main entry point for the library.
5
5
  */
6
6
 
7
- export * from './types';
8
- export * from './constants';
9
- export * from './agent';
10
- export * from './publisher';
11
- export * from './crypto';
12
- export * from './express'; // Node.js / Express Adapter
13
- export { AAMPNext } from './nextjs'; // Serverless / Next.js Adapter
7
+ export * from './types.js';
8
+ export * from './constants.js';
9
+ export * from './agent.js';
10
+ export * from './publisher.js';
11
+ export * from './crypto.js';
12
+ export * from './express.js'; // Node.js / Express Adapter
13
+ export { AAMPNext } from './nextjs.js'; // Serverless / Next.js Adapter
package/src/nextjs.ts CHANGED
@@ -2,12 +2,12 @@
2
2
  * Layer 3: Framework Adapters
3
3
  * Serverless integration for Next.js (App Router & API Routes).
4
4
  */
5
- import { AAMPPublisher } from './publisher';
6
- import { AccessPolicy, ContentOrigin, UnauthenticatedStrategy, IdentityCache } from './types';
7
- import { generateKeyPair } from './crypto';
5
+ import { AAMPPublisher } from './publisher.js';
6
+ import { AccessPolicy, ContentOrigin, UnauthenticatedStrategy, IdentityCache } from './types.js';
7
+ import { generateKeyPair } from './crypto.js';
8
8
 
9
- type NextRequest = any;
10
- type NextResponse = any;
9
+ type NextRequest = any;
10
+ type NextResponse = any;
11
11
 
12
12
  const createJsonResponse = (body: any, status = 200) => {
13
13
  return new Response(JSON.stringify(body), {
@@ -63,9 +63,10 @@ export class AAMPNext {
63
63
  const result = await this.publisher.evaluateVisitor(headers, headers['x-aamp-payload']);
64
64
 
65
65
  if (!result.allowed) {
66
- return createJsonResponse({
66
+ return createJsonResponse({
67
67
  error: result.reason,
68
- visitor_type: result.visitorType
68
+ visitor_type: result.visitorType,
69
+ proof_used: result.proofUsed
69
70
  }, result.status);
70
71
  }
71
72
 
@@ -75,9 +76,9 @@ export class AAMPNext {
75
76
  // Inject Provenance
76
77
  const aampHeaders = await this.publisher.generateResponseHeaders(this.origin);
77
78
  if (response && response.headers) {
78
- Object.entries(aampHeaders).forEach(([k, v]) => {
79
- response.headers.set(k, v);
80
- });
79
+ Object.entries(aampHeaders).forEach(([k, v]) => {
80
+ response.headers.set(k, v);
81
+ });
81
82
  }
82
83
 
83
84
  return response;
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;
@@ -68,13 +71,18 @@ export class AAMPPublisher {
68
71
  * Main Entry Point: Evaluate ANY visitor (Human, Bot, or Agent)
69
72
  */
70
73
  async evaluateVisitor(
71
- reqHeaders: Record<string, string | undefined>,
74
+ reqHeaders: Record<string, string | undefined>,
72
75
  rawPayload?: string
73
76
  ): Promise<EvaluationResult> {
74
-
77
+
75
78
  // 1. Check for AAMP Headers
76
79
  const hasAamp = reqHeaders[HEADERS.PAYLOAD] && reqHeaders[HEADERS.SIGNATURE] && reqHeaders[HEADERS.PUBLIC_KEY];
77
80
 
81
+ const feedbackToken = reqHeaders[HEADERS.FEEDBACK];
82
+ if (feedbackToken) {
83
+ await this.handleFeedback(feedbackToken, reqHeaders);
84
+ }
85
+
78
86
  if (hasAamp) {
79
87
  console.log("\nšŸ”Ž [AAMP Middleware] Detected Agent Headers. Starting Verification...");
80
88
  // It claims to be an Agent. Verify it.
@@ -96,13 +104,13 @@ export class AAMPPublisher {
96
104
  allowed: true,
97
105
  status: 200,
98
106
  reason: "PASSIVE_MODE: Allowed without verification.",
99
- visitorType: 'LIKELY_HUMAN'
107
+ visitorType: 'LIKELY_HUMAN'
100
108
  };
101
109
  }
102
110
 
103
111
  // 3. HYBRID MODE: Heuristic Analysis (The "Lazy Bot" Filter)
104
112
  const isHuman = this.performBrowserHeuristics(reqHeaders);
105
-
113
+
106
114
  if (isHuman) {
107
115
  return {
108
116
  allowed: true,
@@ -128,11 +136,11 @@ export class AAMPPublisher {
128
136
  */
129
137
  private performBrowserHeuristics(headers: Record<string, string | undefined>): boolean {
130
138
  const userAgent = headers['user-agent'] || '';
131
-
139
+
132
140
  // A. The "Obvious Bot" Blocklist (Fast Fail)
133
141
  const botSignatures = ['python-requests', 'curl', 'wget', 'scrapy', 'bot', 'crawler', 'spider'];
134
142
  if (botSignatures.some(sig => userAgent.toLowerCase().includes(sig))) {
135
- return false;
143
+ return false;
136
144
  }
137
145
 
138
146
  // B. Trusted Infrastructure Signals (The Real World Solution)
@@ -142,7 +150,7 @@ export class AAMPPublisher {
142
150
 
143
151
  // C. The "Browser Fingerprint" (Fallback for direct connections)
144
152
  const hasAcceptLanguage = !!headers['accept-language'];
145
- const hasSecFetchDest = !!headers['sec-fetch-dest'];
153
+ const hasSecFetchDest = !!headers['sec-fetch-dest'];
146
154
  const hasUpgradeInsecure = !!headers['upgrade-insecure-requests'];
147
155
 
148
156
  if (hasAcceptLanguage && (hasSecFetchDest || hasUpgradeInsecure)) {
@@ -161,9 +169,9 @@ export class AAMPPublisher {
161
169
  const sigHeader = reqHeaders[HEADERS.SIGNATURE]!;
162
170
  const keyHeader = reqHeaders[HEADERS.PUBLIC_KEY]!;
163
171
 
164
- const headerJson = atob(payloadHeader);
172
+ const headerJson = atob(payloadHeader);
165
173
  const requestHeader = JSON.parse(headerJson);
166
-
174
+
167
175
  const signedRequest: SignedAccessRequest = {
168
176
  header: requestHeader,
169
177
  signature: sigHeader,
@@ -178,11 +186,20 @@ export class AAMPPublisher {
178
186
  ["verify"]
179
187
  );
180
188
 
189
+ const proofToken = reqHeaders[HEADERS.PROOF_TOKEN];
190
+ const paymentCredential = reqHeaders[HEADERS.PAYMENT_CREDENTIAL];
191
+
181
192
  // Verify Core Logic
182
- const result = await this.verifyRequestLogic(signedRequest, agentKey, headerJson);
193
+ const result = await this.verifyRequestLogic(signedRequest, agentKey, proofToken, paymentCredential, headerJson);
183
194
 
184
195
  if (!result.allowed) {
185
- console.log(`ā›” [AAMP Deny] Reason: ${result.reason}`);
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
+
186
203
  return {
187
204
  allowed: false,
188
205
  status: 403,
@@ -191,7 +208,12 @@ export class AAMPPublisher {
191
208
  };
192
209
  }
193
210
 
194
- console.log(`āœ… [AAMP Allow] Verified Agent: ${requestHeader.agent_id}`);
211
+ console.log(`āœ… [AAMP ALLOW]
212
+ Agent: ${requestHeader.agent_id}
213
+ Reason: AAMP_VERIFIED
214
+ Payment Method: ${result.proofUsed}
215
+ Identity Verified: ${result.identityVerified}`);
216
+
195
217
  return {
196
218
  allowed: true,
197
219
  status: 200,
@@ -207,11 +229,13 @@ export class AAMPPublisher {
207
229
  }
208
230
 
209
231
  private async verifyRequestLogic(
210
- request: SignedAccessRequest,
232
+ request: SignedAccessRequest,
211
233
  requestPublicKey: CryptoKey,
234
+ proofToken?: string,
235
+ paymentCredential?: string,
212
236
  rawPayload?: string
213
237
  ): Promise<VerificationResult> {
214
-
238
+
215
239
  // 1. Replay Attack Prevention
216
240
  const requestTime = new Date(request.header.ts).getTime();
217
241
  if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
@@ -227,12 +251,12 @@ export class AAMPPublisher {
227
251
  let identityVerified = false;
228
252
  const claimedDomain = request.header.agent_id;
229
253
  const pubKeyString = await exportPublicKey(requestPublicKey);
230
-
254
+
231
255
  console.log(` šŸ†” [AAMP Identity] Verifying DNS Binding for: ${claimedDomain}`);
232
256
 
233
257
  // Check Cache First
234
258
  const cachedKey = await this.cache.get(claimedDomain);
235
-
259
+
236
260
  if (cachedKey === pubKeyString) {
237
261
  console.log(" ⚔ [AAMP Cache] Identity found in cache.");
238
262
  identityVerified = true;
@@ -256,11 +280,53 @@ export class AAMPPublisher {
256
280
  return { allowed: false, reason: 'POLICY_DENIED: RAG not allowed.', identityVerified };
257
281
  }
258
282
 
259
- // 5. Policy Check: Economics
283
+ // 5. Policy Check: Economics (v1.2)
260
284
  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 };
285
+ let paymentSatisfied = false;
286
+
287
+ // Method A: Flexible Payment Callback (DB / Custom Logic)
288
+ if (this.policy.monetization?.checkPayment) {
289
+ const isPaid = await this.policy.monetization.checkPayment(request.header.agent_id, request.header.purpose);
290
+ if (isPaid) {
291
+ console.log(` šŸ’° [AAMP Audit] Whitelist Check Passed via Callback.`);
292
+ return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'WHITELIST_CALLBACK' };
293
+ }
294
+ }
295
+
296
+ // Method B: Payment Credentials (Unified JWT)
297
+ if (!paymentSatisfied && this.policy.monetization?.paymentConfig && paymentCredential) {
298
+ const { jwksUrl, issuer } = this.policy.monetization.paymentConfig;
299
+ console.log(` šŸ” [AAMP Audit] Verifying Payment Credential (Issuer: ${issuer})...`);
300
+
301
+ const isValidCredential = await verifyJwt(paymentCredential, jwksUrl, issuer);
302
+ if (isValidCredential) {
303
+ console.log(` āœ… [AAMP Audit] Credential Signature VALID.`);
304
+ return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'PAYMENT_CREDENTIAL_JWT' };
305
+ } else {
306
+ console.log(` āŒ [AAMP Audit] Credential Signature INVALID.`);
307
+ }
308
+ }
309
+
310
+ // Method C: Ad-Supported (Proof Verification)
311
+ if (!paymentSatisfied && this.policy.allowAdSupportedAccess && request.header.context.ads_displayed) {
312
+ if (proofToken && this.policy.monetization?.adNetwork) {
313
+ const { jwksUrl, issuer } = this.policy.monetization.adNetwork;
314
+ console.log(` šŸ“ŗ [AAMP Audit] Verifying Ad Proof (Issuer: ${issuer})...`);
315
+
316
+ const isValidProof = await verifyJwt(proofToken, jwksUrl, issuer);
317
+ if (isValidProof) {
318
+ console.log(` āœ… [AAMP Audit] Ad Proof Signature VALID.`);
319
+ return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'AD_PROOF_JWT' };
320
+ } else {
321
+ console.log(` āŒ [AAMP Audit] Ad Proof Signature INVALID.`);
322
+ }
323
+ } else {
324
+ console.log(` āš ļø [AAMP Audit] Ad Proof MISSING.`);
325
+ }
326
+ }
327
+
328
+ if (!paymentSatisfied) {
329
+ return { allowed: false, reason: 'PAYMENT_REQUIRED: Whitelist, Credential, and Ad Proof checks ALL failed.', identityVerified, proofUsed: 'NONE', visitorType: 'VERIFIED_AGENT' };
264
330
  }
265
331
  }
266
332
 
@@ -272,16 +338,16 @@ export class AAMPPublisher {
272
338
  // Allow HTTP for localhost testing
273
339
  const protocol = (domain.includes('localhost') || domain.match(/:\d+$/)) ? 'http' : 'https';
274
340
  const url = `${protocol}://${domain}${WELL_KNOWN_AGENT_PATH}`;
275
-
341
+
276
342
  console.log(` šŸŒ [AAMP DNS] Fetching Manifest: ${url} ...`);
277
343
 
278
344
  // In production, we need a short timeout to prevent hanging
279
345
  const controller = new AbortController();
280
346
  const timeoutId = setTimeout(() => controller.abort(), 1500); // 1.5s max for DNS check
281
-
347
+
282
348
  const response = await fetch(url, { signal: controller.signal });
283
349
  clearTimeout(timeoutId);
284
-
350
+
285
351
  if (!response.ok) {
286
352
  console.log(` āŒ [AAMP DNS] Fetch Failed: ${response.status}`);
287
353
  return false;
@@ -298,15 +364,15 @@ export class AAMPPublisher {
298
364
 
299
365
  // CHECK 2: Does the key match?
300
366
  if (manifest.public_key !== requestKeySpki) {
301
- console.log(` āŒ [AAMP DNS] Key Mismatch: DNS Key != Request Key`);
302
- return false;
367
+ console.log(` āŒ [AAMP DNS] Key Mismatch: DNS Key != Request Key`);
368
+ return false;
303
369
  }
304
370
 
305
371
  console.log(` āœ… [AAMP DNS] Identity Confirmed.`);
306
372
  return true;
307
- } catch (e: any) {
308
- console.log(` āŒ [AAMP DNS] Error: ${e.message}`);
309
- return false;
373
+ } catch (e: any) {
374
+ console.log(` āŒ [AAMP DNS] Error: ${e.message}`);
375
+ return false;
310
376
  }
311
377
  }
312
378
 
@@ -324,4 +390,28 @@ export class AAMPPublisher {
324
390
  [HEADERS.PROVENANCE_SIG]: signature
325
391
  };
326
392
  }
393
+
394
+ /**
395
+ * Handling Quality Feedback (The "Dispute" Layer)
396
+ * This runs when an Agent sends 'x-aamp-feedback'.
397
+ */
398
+ private async handleFeedback(token: string, headers: Record<string, string | undefined>) {
399
+ // NOTE: In production, you would fetch the Agent's specific key.
400
+ // For now, we assume standard Discovery or a centralized Key Set (like adNetwork).
401
+ // Ideally, the SDK config should have a 'qualityOracle' key set.
402
+
403
+ // 1. We just Decode it to Log it (Verification is optional but recommended)
404
+ try {
405
+ const parts = token.split('.');
406
+ const payload = JSON.parse(atob(parts[1]));
407
+
408
+ console.log(`\nšŸ“¢ [AAMP QUALITY ALERT] Feedback Received from ${payload.agent_id}`);
409
+ console.log(` Reason: ${payload.reason} | Score: ${payload.quality_score}`);
410
+ console.log(` Resource: ${payload.url}`);
411
+ console.log(` (Signature available for dispute evidence)`);
412
+
413
+ } catch (e) {
414
+ console.log(` āš ļø [AAMP Warning] Malformed Feedback Token.`);
415
+ }
416
+ }
327
417
  }