@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/aamp-protocol-1.1.5.tgz +0 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +9 -13
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +12 -9
- package/dist/crypto.js +9 -13
- package/dist/express.d.ts +1 -1
- package/dist/express.js +9 -12
- package/dist/index.d.ts +7 -7
- package/dist/index.js +7 -25
- package/dist/nextjs.d.ts +1 -1
- package/dist/nextjs.js +9 -12
- package/dist/proof.d.ts +9 -0
- package/dist/proof.js +27 -0
- package/dist/publisher.d.ts +6 -1
- package/dist/publisher.js +124 -39
- package/dist/types.d.ts +21 -2
- package/dist/types.js +6 -9
- package/package.json +10 -5
- package/src/agent.ts +8 -8
- package/src/constants.ts +13 -4
- package/src/crypto.ts +6 -1
- package/src/express.ts +8 -7
- package/src/index.ts +7 -7
- package/src/nextjs.ts +11 -10
- package/src/proof.ts +36 -0
- package/src/publisher.ts +157 -47
- package/src/types.ts +42 -9
- package/test/handshake.spec.ts +6 -6
- package/tsconfig.json +9 -3
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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[
|
|
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[
|
|
141
|
-
const sigHeader = reqHeaders[
|
|
142
|
-
const keyHeader = reqHeaders[
|
|
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) >
|
|
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
|
|
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
|
|
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 ===
|
|
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 ===
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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}${
|
|
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
|
-
|
|
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
|
|
311
|
+
const signature = await signData(this.keyPair.privateKey, payload);
|
|
247
312
|
return {
|
|
248
|
-
[
|
|
249
|
-
[
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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 || (
|
|
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 || (
|
|
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 || (
|
|
25
|
+
})(QualityFlag || (QualityFlag = {}));
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aamp/protocol",
|
|
3
|
-
"version": "1.1.
|
|
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
|
-
"
|
|
20
|
+
"@types/node": "^20.0.0",
|
|
21
|
+
"@types/node-fetch": "^2.6.13",
|
|
21
22
|
"ts-node": "^10.9.0",
|
|
22
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
+
}
|