@aamp/protocol 1.1.4 → 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/publisher.js CHANGED
@@ -1,13 +1,11 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AAMPPublisher = void 0;
4
1
  /**
5
2
  * Layer 2: Publisher Middleware
6
3
  * Used by content owners to enforce policy, log access, and filter bots.
7
4
  */
8
- const constants_1 = require("./constants");
9
- const crypto_1 = require("./crypto");
10
- const types_1 = require("./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 { AccessPurpose } from './types.js';
8
+ import { verifyJwt } from './proof.js';
11
9
  /**
12
10
  * Default In-Memory Cache (Fallback only)
13
11
  * NOT recommended for high-traffic Serverless production.
@@ -33,7 +31,7 @@ class MemoryCache {
33
31
  });
34
32
  }
35
33
  }
36
- class AAMPPublisher {
34
+ export class AAMPPublisher {
37
35
  constructor(policy, strategy = 'PASSIVE', cacheImpl) {
38
36
  this.keyPair = null;
39
37
  // Default TTL: 1 Hour
@@ -53,8 +51,13 @@ class AAMPPublisher {
53
51
  */
54
52
  async evaluateVisitor(reqHeaders, rawPayload) {
55
53
  // 1. Check for AAMP Headers
56
- const hasAamp = reqHeaders[constants_1.HEADERS.PAYLOAD] && reqHeaders[constants_1.HEADERS.SIGNATURE] && reqHeaders[constants_1.HEADERS.PUBLIC_KEY];
54
+ 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
+ }
57
59
  if (hasAamp) {
60
+ console.log("\nšŸ”Ž [AAMP Middleware] Detected Agent Headers. Starting Verification...");
58
61
  // It claims to be an Agent. Verify it.
59
62
  return await this.handleAgent(reqHeaders, rawPayload);
60
63
  }
@@ -104,32 +107,23 @@ class AAMPPublisher {
104
107
  const userAgent = headers['user-agent'] || '';
105
108
  // A. The "Obvious Bot" Blocklist (Fast Fail)
106
109
  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
110
  if (botSignatures.some(sig => userAgent.toLowerCase().includes(sig))) {
110
111
  return false;
111
112
  }
112
113
  // 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
114
  if (headers['cf-visitor'] || headers['cf-ray'])
116
115
  return true;
117
- // Vercel: 'x-vercel-id'
118
116
  if (headers['x-vercel-id'])
119
117
  return true;
120
- // AWS CloudFront: 'cloudfront-viewer-address'
121
118
  if (headers['cloudfront-viewer-address'])
122
119
  return true;
123
120
  // C. The "Browser Fingerprint" (Fallback for direct connections)
124
- // Real browsers almost always send these headers
125
121
  const hasAcceptLanguage = !!headers['accept-language'];
126
122
  const hasSecFetchDest = !!headers['sec-fetch-dest'];
127
123
  const hasUpgradeInsecure = !!headers['upgrade-insecure-requests'];
128
- // If it has typical browser headers, we allow it.
129
124
  if (hasAcceptLanguage && (hasSecFetchDest || hasUpgradeInsecure)) {
130
125
  return true;
131
126
  }
132
- // If it has no browser headers and no trusted proxy headers -> It's likely a script.
133
127
  return false;
134
128
  }
135
129
  /**
@@ -137,9 +131,9 @@ class AAMPPublisher {
137
131
  */
138
132
  async handleAgent(reqHeaders, rawPayload) {
139
133
  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];
134
+ const payloadHeader = reqHeaders[HEADERS.PAYLOAD];
135
+ const sigHeader = reqHeaders[HEADERS.SIGNATURE];
136
+ const keyHeader = reqHeaders[HEADERS.PUBLIC_KEY];
143
137
  const headerJson = atob(payloadHeader);
144
138
  const requestHeader = JSON.parse(headerJson);
145
139
  const signedRequest = {
@@ -148,9 +142,17 @@ class AAMPPublisher {
148
142
  publicKey: keyHeader
149
143
  };
150
144
  const agentKey = await crypto.subtle.importKey("spki", new Uint8Array(atob(keyHeader).split('').map(c => c.charCodeAt(0))), { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]);
145
+ const proofToken = reqHeaders[HEADERS.PROOF_TOKEN];
146
+ const paymentCredential = reqHeaders[HEADERS.PAYMENT_CREDENTIAL];
151
147
  // Verify Core Logic
152
- const result = await this.verifyRequestLogic(signedRequest, agentKey, headerJson);
148
+ const result = await this.verifyRequestLogic(signedRequest, agentKey, proofToken, paymentCredential, headerJson);
153
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}`);
154
156
  return {
155
157
  allowed: false,
156
158
  status: 403,
@@ -158,6 +160,11 @@ class AAMPPublisher {
158
160
  visitorType: 'VERIFIED_AGENT'
159
161
  };
160
162
  }
163
+ console.log(`āœ… [AAMP ALLOW]
164
+ Agent: ${requestHeader.agent_id}
165
+ Reason: AAMP_VERIFIED
166
+ Payment Method: ${result.proofUsed}
167
+ Identity Verified: ${result.identityVerified}`);
161
168
  return {
162
169
  allowed: true,
163
170
  status: 200,
@@ -167,27 +174,30 @@ class AAMPPublisher {
167
174
  };
168
175
  }
169
176
  catch (e) {
177
+ console.error(e);
170
178
  return { allowed: false, status: 400, reason: "INVALID_SIGNATURE", visitorType: 'UNIDENTIFIED_BOT' };
171
179
  }
172
180
  }
173
- async verifyRequestLogic(request, requestPublicKey, rawPayload) {
181
+ async verifyRequestLogic(request, requestPublicKey, proofToken, paymentCredential, rawPayload) {
174
182
  // 1. Replay Attack Prevention
175
183
  const requestTime = new Date(request.header.ts).getTime();
176
- if (Math.abs(Date.now() - requestTime) > constants_1.MAX_CLOCK_SKEW_MS) {
184
+ if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
177
185
  return { allowed: false, reason: 'TIMESTAMP_INVALID', identityVerified: false };
178
186
  }
179
187
  // 2. Crypto Verification
180
188
  const signableString = rawPayload || JSON.stringify(request.header);
181
- const isCryptoValid = await (0, crypto_1.verifySignature)(requestPublicKey, signableString, request.signature);
189
+ const isCryptoValid = await verifySignature(requestPublicKey, signableString, request.signature);
182
190
  if (!isCryptoValid)
183
191
  return { allowed: false, reason: 'CRYPTO_FAIL', identityVerified: false };
184
192
  // 3. Identity Verification (DNS Binding) with Cache
185
193
  let identityVerified = false;
186
194
  const claimedDomain = request.header.agent_id;
187
- const pubKeyString = await (0, crypto_1.exportPublicKey)(requestPublicKey);
195
+ const pubKeyString = await exportPublicKey(requestPublicKey);
196
+ console.log(` šŸ†” [AAMP Identity] Verifying DNS Binding for: ${claimedDomain}`);
188
197
  // Check Cache First
189
198
  const cachedKey = await this.cache.get(claimedDomain);
190
199
  if (cachedKey === pubKeyString) {
200
+ console.log(" ⚔ [AAMP Cache] Identity found in cache.");
191
201
  identityVerified = true;
192
202
  }
193
203
  else if (this.isDomain(claimedDomain)) {
@@ -201,17 +211,56 @@ class AAMPPublisher {
201
211
  return { allowed: false, reason: 'IDENTITY_FAIL: DNS Binding could not be verified.', identityVerified: false };
202
212
  }
203
213
  // 4. Policy Check: Purpose
204
- if (request.header.purpose === types_1.AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
214
+ if (request.header.purpose === AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
205
215
  return { allowed: false, reason: 'POLICY_DENIED: Training not allowed.', identityVerified };
206
216
  }
207
- if (request.header.purpose === types_1.AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
217
+ if (request.header.purpose === AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
208
218
  return { allowed: false, reason: 'POLICY_DENIED: RAG not allowed.', identityVerified };
209
219
  }
210
- // 5. Policy Check: Economics
220
+ // 5. Policy Check: Economics (v1.2)
211
221
  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 };
222
+ let paymentSatisfied = false;
223
+ // Method A: Flexible Payment Callback (DB / Custom Logic)
224
+ if (this.policy.monetization?.checkPayment) {
225
+ const isPaid = await this.policy.monetization.checkPayment(request.header.agent_id, request.header.purpose);
226
+ if (isPaid) {
227
+ console.log(` šŸ’° [AAMP Audit] Whitelist Check Passed via Callback.`);
228
+ return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'WHITELIST_CALLBACK' };
229
+ }
230
+ }
231
+ // Method B: Payment Credentials (Unified JWT)
232
+ if (!paymentSatisfied && this.policy.monetization?.paymentConfig && paymentCredential) {
233
+ const { jwksUrl, issuer } = this.policy.monetization.paymentConfig;
234
+ console.log(` šŸ” [AAMP Audit] Verifying Payment Credential (Issuer: ${issuer})...`);
235
+ const isValidCredential = await verifyJwt(paymentCredential, jwksUrl, issuer);
236
+ if (isValidCredential) {
237
+ console.log(` āœ… [AAMP Audit] Credential Signature VALID.`);
238
+ return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'PAYMENT_CREDENTIAL_JWT' };
239
+ }
240
+ else {
241
+ console.log(` āŒ [AAMP Audit] Credential Signature INVALID.`);
242
+ }
243
+ }
244
+ // Method C: Ad-Supported (Proof Verification)
245
+ if (!paymentSatisfied && this.policy.allowAdSupportedAccess && request.header.context.ads_displayed) {
246
+ if (proofToken && this.policy.monetization?.adNetwork) {
247
+ const { jwksUrl, issuer } = this.policy.monetization.adNetwork;
248
+ console.log(` šŸ“ŗ [AAMP Audit] Verifying Ad Proof (Issuer: ${issuer})...`);
249
+ const isValidProof = await verifyJwt(proofToken, jwksUrl, issuer);
250
+ if (isValidProof) {
251
+ console.log(` āœ… [AAMP Audit] Ad Proof Signature VALID.`);
252
+ return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'AD_PROOF_JWT' };
253
+ }
254
+ else {
255
+ console.log(` āŒ [AAMP Audit] Ad Proof Signature INVALID.`);
256
+ }
257
+ }
258
+ else {
259
+ console.log(` āš ļø [AAMP Audit] Ad Proof MISSING.`);
260
+ }
261
+ }
262
+ if (!paymentSatisfied) {
263
+ return { allowed: false, reason: 'PAYMENT_REQUIRED: Whitelist, Credential, and Ad Proof checks ALL failed.', identityVerified, proofUsed: 'NONE', visitorType: 'VERIFIED_AGENT' };
215
264
  }
216
265
  }
217
266
  return { allowed: true, reason: 'OK', identityVerified };
@@ -220,18 +269,34 @@ class AAMPPublisher {
220
269
  try {
221
270
  // Allow HTTP for localhost testing
222
271
  const protocol = (domain.includes('localhost') || domain.match(/:\d+$/)) ? 'http' : 'https';
223
- const url = `${protocol}://${domain}${constants_1.WELL_KNOWN_AGENT_PATH}`;
272
+ const url = `${protocol}://${domain}${WELL_KNOWN_AGENT_PATH}`;
273
+ console.log(` šŸŒ [AAMP DNS] Fetching Manifest: ${url} ...`);
224
274
  // In production, we need a short timeout to prevent hanging
225
275
  const controller = new AbortController();
226
276
  const timeoutId = setTimeout(() => controller.abort(), 1500); // 1.5s max for DNS check
227
277
  const response = await fetch(url, { signal: controller.signal });
228
278
  clearTimeout(timeoutId);
229
- if (!response.ok)
279
+ if (!response.ok) {
280
+ console.log(` āŒ [AAMP DNS] Fetch Failed: ${response.status}`);
230
281
  return false;
282
+ }
231
283
  const manifest = await response.json();
232
- return manifest.agent_id === domain && manifest.public_key === requestKeySpki;
284
+ console.log(` šŸ“„ [AAMP DNS] Manifest received. Agent ID: ${manifest.agent_id}`);
285
+ // CHECK 1: Does the manifest actually belong to the domain?
286
+ if (manifest.agent_id !== domain) {
287
+ console.log(` āŒ [AAMP DNS] Mismatch: Manifest ID ${manifest.agent_id} != Claimed ${domain}`);
288
+ return false;
289
+ }
290
+ // CHECK 2: Does the key match?
291
+ if (manifest.public_key !== requestKeySpki) {
292
+ console.log(` āŒ [AAMP DNS] Key Mismatch: DNS Key != Request Key`);
293
+ return false;
294
+ }
295
+ console.log(` āœ… [AAMP DNS] Identity Confirmed.`);
296
+ return true;
233
297
  }
234
- catch {
298
+ catch (e) {
299
+ console.log(` āŒ [AAMP DNS] Error: ${e.message}`);
235
300
  return false;
236
301
  }
237
302
  }
@@ -243,11 +308,31 @@ class AAMPPublisher {
243
308
  if (!this.keyPair)
244
309
  throw new Error("Publisher keys not initialized");
245
310
  const payload = JSON.stringify({ origin, ts: Date.now() });
246
- const signature = await (0, crypto_1.signData)(this.keyPair.privateKey, payload);
311
+ const signature = await signData(this.keyPair.privateKey, payload);
247
312
  return {
248
- [constants_1.HEADERS.CONTENT_ORIGIN]: origin,
249
- [constants_1.HEADERS.PROVENANCE_SIG]: signature
313
+ [HEADERS.CONTENT_ORIGIN]: origin,
314
+ [HEADERS.PROVENANCE_SIG]: signature
250
315
  };
251
316
  }
317
+ /**
318
+ * Handling Quality Feedback (The "Dispute" Layer)
319
+ * This runs when an Agent sends 'x-aamp-feedback'.
320
+ */
321
+ async handleFeedback(token, headers) {
322
+ // NOTE: In production, you would fetch the Agent's specific key.
323
+ // For now, we assume standard Discovery or a centralized Key Set (like adNetwork).
324
+ // Ideally, the SDK config should have a 'qualityOracle' key set.
325
+ // 1. We just Decode it to Log it (Verification is optional but recommended)
326
+ try {
327
+ const parts = token.split('.');
328
+ const payload = JSON.parse(atob(parts[1]));
329
+ console.log(`\nšŸ“¢ [AAMP QUALITY ALERT] Feedback Received from ${payload.agent_id}`);
330
+ console.log(` Reason: ${payload.reason} | Score: ${payload.quality_score}`);
331
+ console.log(` Resource: ${payload.url}`);
332
+ console.log(` (Signature available for dispute evidence)`);
333
+ }
334
+ catch (e) {
335
+ console.log(` āš ļø [AAMP Warning] Malformed Feedback Token.`);
336
+ }
337
+ }
252
338
  }
253
- exports.AAMPPublisher = AAMPPublisher;
package/dist/types.d.ts CHANGED
@@ -37,12 +37,22 @@ export interface IdentityCache {
37
37
  get(key: string): Promise<string | null>;
38
38
  set(key: string, value: string, ttlSeconds: number): Promise<void>;
39
39
  }
40
+ /**
41
+ * Optional Monetization (The Settlement Layer)
42
+ */
40
43
  /**
41
44
  * Optional Monetization (The Settlement Layer)
42
45
  */
43
46
  export interface MonetizationConfig {
44
- method: 'BROKER' | 'CRYPTO' | 'PRIVATE_TREATY';
45
- location: string;
47
+ checkPayment?: (agentId: string, purpose: string) => boolean | Promise<boolean>;
48
+ adNetwork?: {
49
+ jwksUrl: string;
50
+ issuer: string;
51
+ };
52
+ paymentConfig?: {
53
+ jwksUrl: string;
54
+ issuer: string;
55
+ };
46
56
  }
47
57
  /**
48
58
  * Handling Non-AAMP Visitors
@@ -91,4 +101,13 @@ export interface EvaluationResult {
91
101
  reason: string;
92
102
  visitorType: 'VERIFIED_AGENT' | 'LIKELY_HUMAN' | 'UNIDENTIFIED_BOT';
93
103
  metadata?: any;
104
+ payment_status?: 'PAID_SUBSCRIBER' | 'AD_FUNDED' | 'UNPAID';
105
+ proofUsed?: string;
106
+ }
107
+ export interface FeedbackSignalToken {
108
+ url: string;
109
+ agent_id: string;
110
+ quality_score: number;
111
+ reason: string;
112
+ timestamp: number;
94
113
  }
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.4",
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/crypto.ts CHANGED
@@ -28,12 +28,17 @@ export async function verifySignature(publicKey: CryptoKey, data: string, signat
28
28
  const encodedData = encoder.encode(data);
29
29
  const signatureBytes = hexToBuf(signatureHex);
30
30
 
31
- return await crypto.subtle.verify(
31
+ console.log(" šŸ” [AAMP Crypto] Verifying ECDSA P-256 Signature...");
32
+
33
+ const isValid = await crypto.subtle.verify(
32
34
  { name: "ECDSA", hash: { name: "SHA-256" } },
33
35
  publicKey,
34
36
  signatureBytes as any,
35
37
  encodedData as any
36
38
  );
39
+
40
+ console.log(` ${isValid ? "āœ…" : "āŒ"} [AAMP Crypto] Signature Result: ${isValid ? "VALID" : "INVALID"}`);
41
+ return isValid;
37
42
  }
38
43
 
39
44
  export async function exportPublicKey(key: CryptoKey): Promise<string> {
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
+ }