@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/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 +5 -12
- 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 +15 -2
- package/dist/publisher.js +168 -84
- package/dist/types.d.ts +22 -3
- 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/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 +212 -100
- package/src/types.ts +43 -10
- 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
|
|
@@ -50,50 +48,46 @@ class AAMPPublisher {
|
|
|
50
48
|
}
|
|
51
49
|
/**
|
|
52
50
|
* Main Entry Point: Evaluate ANY visitor (Human, Bot, or Agent)
|
|
51
|
+
* STAGE 1: IDENTITY (Strict)
|
|
52
|
+
* STAGE 2: POLICY (Permissions)
|
|
53
|
+
* STAGE 3: ACCESS (HQ Content)
|
|
53
54
|
*/
|
|
54
55
|
async evaluateVisitor(reqHeaders, rawPayload) {
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
console.log(`\n--- [AAMP LOG START] New Request ---`);
|
|
57
|
+
// --- STAGE 1: IDENTITY VERIFICATION ---
|
|
58
|
+
console.log(`[IDENTITY] š Checking Identity Headers...`);
|
|
59
|
+
const hasAamp = reqHeaders[HEADERS.PAYLOAD] && reqHeaders[HEADERS.SIGNATURE] && reqHeaders[HEADERS.PUBLIC_KEY];
|
|
57
60
|
if (hasAamp) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return await this.handleAgent(reqHeaders, rawPayload);
|
|
61
|
+
// It claims to be an Agent. Verify it STRICTLY.
|
|
62
|
+
return await this.handleAgentStrict(reqHeaders, rawPayload);
|
|
61
63
|
}
|
|
62
|
-
//
|
|
64
|
+
// If NO AAMP Headers -> FAIL IDENTITY immediately.
|
|
65
|
+
console.log(`[IDENTITY] ā FAILED. No AAMP Headers found.`);
|
|
66
|
+
// For now, retaining the legacy "Passive/Hybrid" switch just to avoid breaking browser demos completely
|
|
67
|
+
// BUT logging it as a specific "Identity Fail" flow.
|
|
63
68
|
if (this.unauthenticatedStrategy === 'STRICT') {
|
|
69
|
+
console.log(`[IDENTITY] ā BLOCKING. Strategy is STRICT.`);
|
|
64
70
|
return {
|
|
65
71
|
allowed: false,
|
|
66
72
|
status: 401,
|
|
67
|
-
reason: "
|
|
73
|
+
reason: "IDENTITY_REQUIRED: Missing AAMP Headers.",
|
|
68
74
|
visitorType: 'UNIDENTIFIED_BOT'
|
|
69
75
|
};
|
|
70
76
|
}
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
allowed: true,
|
|
74
|
-
status: 200,
|
|
75
|
-
reason: "PASSIVE_MODE: Allowed without verification.",
|
|
76
|
-
visitorType: 'LIKELY_HUMAN'
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
// 3. HYBRID MODE: Heuristic Analysis (The "Lazy Bot" Filter)
|
|
77
|
+
console.log(`[IDENTITY] ā ļø SKIPPED (Legacy Mode). Checking Browser Heuristics...`);
|
|
80
78
|
const isHuman = this.performBrowserHeuristics(reqHeaders);
|
|
81
79
|
if (isHuman) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
status: 200,
|
|
85
|
-
reason: "BROWSER_VERIFIED: Heuristics passed.",
|
|
86
|
-
visitorType: 'LIKELY_HUMAN'
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
return {
|
|
91
|
-
allowed: false,
|
|
92
|
-
status: 403,
|
|
93
|
-
reason: "BOT_DETECTED: Request lacks browser signatures and AAMP headers.",
|
|
94
|
-
visitorType: 'UNIDENTIFIED_BOT'
|
|
95
|
-
};
|
|
80
|
+
console.log(`[POLICY] š¤ ALLOWED. Browser Heuristics Passed.`);
|
|
81
|
+
return { allowed: true, status: 200, reason: "BROWSER_VERIFIED", visitorType: 'LIKELY_HUMAN' };
|
|
96
82
|
}
|
|
83
|
+
console.log(`[IDENTITY] ā FAILED. Not a Browser, No Headers.`);
|
|
84
|
+
console.log(`[ACCESS] ā BLOCKED.`);
|
|
85
|
+
return {
|
|
86
|
+
allowed: false,
|
|
87
|
+
status: 403,
|
|
88
|
+
reason: "IDENTITY_FAIL: No Identity, No Browser.",
|
|
89
|
+
visitorType: 'UNIDENTIFIED_BOT'
|
|
90
|
+
};
|
|
97
91
|
}
|
|
98
92
|
/**
|
|
99
93
|
* Browser Heuristics (Hardened)
|
|
@@ -125,66 +119,149 @@ class AAMPPublisher {
|
|
|
125
119
|
return false;
|
|
126
120
|
}
|
|
127
121
|
/**
|
|
128
|
-
* Handle AAMP Protocol Logic
|
|
122
|
+
* Handle AAMP Protocol Logic (Strict Mode)
|
|
129
123
|
*/
|
|
130
|
-
async
|
|
124
|
+
async handleAgentStrict(reqHeaders, rawPayload) {
|
|
125
|
+
let agentId = "UNKNOWN";
|
|
131
126
|
try {
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
const
|
|
127
|
+
// 1. Decode Headers
|
|
128
|
+
const payloadHeader = reqHeaders[HEADERS.PAYLOAD];
|
|
129
|
+
const sigHeader = reqHeaders[HEADERS.SIGNATURE];
|
|
130
|
+
const keyHeader = reqHeaders[HEADERS.PUBLIC_KEY];
|
|
135
131
|
const headerJson = atob(payloadHeader);
|
|
136
132
|
const requestHeader = JSON.parse(headerJson);
|
|
133
|
+
agentId = requestHeader.agent_id;
|
|
134
|
+
console.log(`[IDENTITY] š Claimed ID: ${agentId}`);
|
|
135
|
+
// 2. Crypto & DNS Verification
|
|
137
136
|
const signedRequest = {
|
|
138
137
|
header: requestHeader,
|
|
139
138
|
signature: sigHeader,
|
|
140
139
|
publicKey: keyHeader
|
|
141
140
|
};
|
|
142
141
|
const agentKey = await crypto.subtle.importKey("spki", new Uint8Array(atob(keyHeader).split('').map(c => c.charCodeAt(0))), { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]);
|
|
143
|
-
// Verify Core Logic
|
|
144
|
-
const
|
|
145
|
-
if (!
|
|
146
|
-
console.log(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
142
|
+
// Verify Core Logic (DNS + Crypto)
|
|
143
|
+
const verification = await this.verifyRequestLogic(signedRequest, agentKey);
|
|
144
|
+
if (!verification.identityVerified) {
|
|
145
|
+
console.log(`[IDENTITY] ā FAILED. Reason: ${verification.reason}`);
|
|
146
|
+
console.log(`[ACCESS] ā BLOCKED.`);
|
|
147
|
+
return { allowed: false, status: 403, reason: verification.reason, visitorType: 'UNIDENTIFIED_BOT' };
|
|
148
|
+
}
|
|
149
|
+
console.log(`[IDENTITY] ā
PASSED. DNS Binding Verified.`);
|
|
150
|
+
// --- STAGE 2: POLICY ENFORCEMENT ---
|
|
151
|
+
console.log(`[POLICY] š Checking Permissions for ${agentId}...`);
|
|
152
|
+
const proofToken = reqHeaders[HEADERS.PROOF_TOKEN];
|
|
153
|
+
const paymentCredential = reqHeaders[HEADERS.PAYMENT_CREDENTIAL];
|
|
154
|
+
const policyResult = await this.checkPolicyStrict(requestHeader, proofToken, paymentCredential);
|
|
155
|
+
if (!policyResult.allowed) {
|
|
156
|
+
console.log(`[POLICY] ā DENIED. Reason: ${policyResult.reason}`);
|
|
157
|
+
console.log(`[ACCESS] ā BLOCKED.`);
|
|
158
|
+
return policyResult;
|
|
153
159
|
}
|
|
154
|
-
|
|
160
|
+
// --- STAGE 3: ACCESS GRANT ---
|
|
161
|
+
console.log(`[POLICY] ā
PASSED. Requirements Met.`);
|
|
162
|
+
console.log(`[ACCESS] š GRANTED. Unlocking HQ Content.`);
|
|
155
163
|
return {
|
|
156
164
|
allowed: true,
|
|
157
165
|
status: 200,
|
|
158
166
|
reason: "AAMP_VERIFIED",
|
|
159
167
|
visitorType: 'VERIFIED_AGENT',
|
|
160
|
-
metadata: requestHeader
|
|
168
|
+
metadata: requestHeader,
|
|
169
|
+
proofUsed: policyResult.proofUsed
|
|
161
170
|
};
|
|
162
171
|
}
|
|
163
172
|
catch (e) {
|
|
164
|
-
console.error(e);
|
|
173
|
+
console.error(`[AAMP ERROR]`, e);
|
|
165
174
|
return { allowed: false, status: 400, reason: "INVALID_SIGNATURE", visitorType: 'UNIDENTIFIED_BOT' };
|
|
166
175
|
}
|
|
167
176
|
}
|
|
168
|
-
|
|
177
|
+
// Legacy handler kept for interface compatibility (deprecated)
|
|
178
|
+
async handleAgent(reqHeaders, rawPayload) {
|
|
179
|
+
return this.handleAgentStrict(reqHeaders, rawPayload);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* STAGE 2: POLICY ENFORCEMENT CHECK
|
|
183
|
+
*/
|
|
184
|
+
async checkPolicyStrict(requestHeader, proofToken, paymentCredential) {
|
|
185
|
+
// 1. Policy Check: Purpose Ban (e.g. No Training)
|
|
186
|
+
if (requestHeader.purpose === AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
|
|
187
|
+
return { allowed: false, status: 403, reason: 'POLICY_DENIED: Training not allowed.', visitorType: 'VERIFIED_AGENT' };
|
|
188
|
+
}
|
|
189
|
+
if (requestHeader.purpose === AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
|
|
190
|
+
return { allowed: false, status: 403, reason: 'POLICY_DENIED: RAG not allowed.', visitorType: 'VERIFIED_AGENT' };
|
|
191
|
+
}
|
|
192
|
+
// 2. Policy Check: Economics (v1.2) - Payment & Ads
|
|
193
|
+
if (this.policy.requiresPayment) {
|
|
194
|
+
let paymentSatisfied = false;
|
|
195
|
+
// Method A: Flexible Payment Callback (DB / Custom Logic)
|
|
196
|
+
if (this.policy.monetization?.checkPayment) {
|
|
197
|
+
const isPaid = await this.policy.monetization.checkPayment(requestHeader.agent_id, requestHeader.purpose);
|
|
198
|
+
if (isPaid) {
|
|
199
|
+
console.log(`[POLICY] š° Payment Verified via Callback.`);
|
|
200
|
+
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'WHITELIST_CALLBACK' };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Method B: Payment Credentials (Unified JWT)
|
|
204
|
+
if (!paymentSatisfied && this.policy.monetization?.paymentConfig && paymentCredential) {
|
|
205
|
+
const { jwksUrl, issuer } = this.policy.monetization.paymentConfig;
|
|
206
|
+
console.log(`[POLICY] š Verifying Payment Credential (Issuer: ${issuer})...`);
|
|
207
|
+
const isValidCredential = await verifyJwt(paymentCredential, jwksUrl, issuer);
|
|
208
|
+
if (isValidCredential) {
|
|
209
|
+
console.log(`[POLICY] ā
Credential Signature VALID.`);
|
|
210
|
+
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'PAYMENT_CREDENTIAL_JWT' };
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
console.log(`[POLICY] ā Credential Signature INVALID.`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Method C: Ad-Supported (Proof Verification)
|
|
217
|
+
if (!paymentSatisfied && this.policy.allowAdSupportedAccess && requestHeader.context?.ads_displayed) {
|
|
218
|
+
if (proofToken && this.policy.monetization?.adNetwork) {
|
|
219
|
+
const { jwksUrl, issuer } = this.policy.monetization.adNetwork;
|
|
220
|
+
console.log(`[POLICY] šŗ Verifying Ad Proof (Issuer: ${issuer})...`);
|
|
221
|
+
const isValidProof = await verifyJwt(proofToken, jwksUrl, issuer);
|
|
222
|
+
if (isValidProof) {
|
|
223
|
+
console.log(`[POLICY] ā
Ad Proof Signature VALID.`);
|
|
224
|
+
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'AD_PROOF_JWT' };
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
console.log(`[POLICY] ā Ad Proof Signature INVALID.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
console.log(`[POLICY] ā ļø Ad Proof MISSING.`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
allowed: false,
|
|
236
|
+
status: 402,
|
|
237
|
+
reason: 'PAYMENT_REQUIRED: Whitelist, Credential, and Ad Proof checks ALL failed.',
|
|
238
|
+
visitorType: 'VERIFIED_AGENT',
|
|
239
|
+
proofUsed: 'NONE'
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// If no payment required, allow.
|
|
243
|
+
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT' };
|
|
244
|
+
}
|
|
245
|
+
async verifyRequestLogic(request, requestPublicKey) {
|
|
169
246
|
// 1. Replay Attack Prevention
|
|
170
247
|
const requestTime = new Date(request.header.ts).getTime();
|
|
171
|
-
if (Math.abs(Date.now() - requestTime) >
|
|
172
|
-
return { allowed: false, reason: 'TIMESTAMP_INVALID', identityVerified: false };
|
|
248
|
+
if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
|
|
249
|
+
return { allowed: false, reason: 'TIMESTAMP_INVALID: Clock skew too large.', identityVerified: false };
|
|
173
250
|
}
|
|
174
251
|
// 2. Crypto Verification
|
|
175
|
-
const signableString =
|
|
176
|
-
const isCryptoValid = await
|
|
252
|
+
const signableString = JSON.stringify(request.header);
|
|
253
|
+
const isCryptoValid = await verifySignature(requestPublicKey, signableString, request.signature);
|
|
177
254
|
if (!isCryptoValid)
|
|
178
|
-
return { allowed: false, reason: 'CRYPTO_FAIL', identityVerified: false };
|
|
255
|
+
return { allowed: false, reason: 'CRYPTO_FAIL: Signature invalid.', identityVerified: false };
|
|
179
256
|
// 3. Identity Verification (DNS Binding) with Cache
|
|
180
257
|
let identityVerified = false;
|
|
181
258
|
const claimedDomain = request.header.agent_id;
|
|
182
|
-
const pubKeyString = await
|
|
183
|
-
console.log(`
|
|
259
|
+
const pubKeyString = await exportPublicKey(requestPublicKey);
|
|
260
|
+
console.log(`[IDENTITY] š Verifying DNS Binding for: ${claimedDomain}`);
|
|
184
261
|
// Check Cache First
|
|
185
262
|
const cachedKey = await this.cache.get(claimedDomain);
|
|
186
263
|
if (cachedKey === pubKeyString) {
|
|
187
|
-
console.log("
|
|
264
|
+
console.log("[IDENTITY] ā” Cache Hit. Identity Verified.");
|
|
188
265
|
identityVerified = true;
|
|
189
266
|
}
|
|
190
267
|
else if (this.isDomain(claimedDomain)) {
|
|
@@ -197,27 +274,14 @@ class AAMPPublisher {
|
|
|
197
274
|
if (this.policy.requireIdentityBinding && !identityVerified) {
|
|
198
275
|
return { allowed: false, reason: 'IDENTITY_FAIL: DNS Binding could not be verified.', identityVerified: false };
|
|
199
276
|
}
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
return { allowed: false, reason: 'POLICY_DENIED: Training not allowed.', identityVerified };
|
|
203
|
-
}
|
|
204
|
-
if (request.header.purpose === types_1.AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
|
|
205
|
-
return { allowed: false, reason: 'POLICY_DENIED: RAG not allowed.', identityVerified };
|
|
206
|
-
}
|
|
207
|
-
// 5. Policy Check: Economics
|
|
208
|
-
if (this.policy.requiresPayment) {
|
|
209
|
-
const isAdExempt = this.policy.allowAdSupportedAccess && request.header.context.ads_displayed;
|
|
210
|
-
if (!isAdExempt) {
|
|
211
|
-
return { allowed: false, reason: 'PAYMENT_REQUIRED: Content requires payment or ads.', identityVerified };
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return { allowed: true, reason: 'OK', identityVerified };
|
|
277
|
+
// Return verified status so handleAgentStrict can proceed to Policy Check
|
|
278
|
+
return { allowed: true, reason: 'OK', identityVerified: true };
|
|
215
279
|
}
|
|
216
280
|
async verifyDnsBinding(domain, requestKeySpki) {
|
|
217
281
|
try {
|
|
218
282
|
// Allow HTTP for localhost testing
|
|
219
283
|
const protocol = (domain.includes('localhost') || domain.match(/:\d+$/)) ? 'http' : 'https';
|
|
220
|
-
const url = `${protocol}://${domain}${
|
|
284
|
+
const url = `${protocol}://${domain}${WELL_KNOWN_AGENT_PATH}`;
|
|
221
285
|
console.log(` š [AAMP DNS] Fetching Manifest: ${url} ...`);
|
|
222
286
|
// In production, we need a short timeout to prevent hanging
|
|
223
287
|
const controller = new AbortController();
|
|
@@ -256,11 +320,31 @@ class AAMPPublisher {
|
|
|
256
320
|
if (!this.keyPair)
|
|
257
321
|
throw new Error("Publisher keys not initialized");
|
|
258
322
|
const payload = JSON.stringify({ origin, ts: Date.now() });
|
|
259
|
-
const signature = await
|
|
323
|
+
const signature = await signData(this.keyPair.privateKey, payload);
|
|
260
324
|
return {
|
|
261
|
-
[
|
|
262
|
-
[
|
|
325
|
+
[HEADERS.CONTENT_ORIGIN]: origin,
|
|
326
|
+
[HEADERS.PROVENANCE_SIG]: signature
|
|
263
327
|
};
|
|
264
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Handling Quality Feedback (The "Dispute" Layer)
|
|
331
|
+
* This runs when an Agent sends 'x-aamp-feedback'.
|
|
332
|
+
*/
|
|
333
|
+
async handleFeedback(token, headers) {
|
|
334
|
+
// NOTE: In production, you would fetch the Agent's specific key.
|
|
335
|
+
// For now, we assume standard Discovery or a centralized Key Set (like adNetwork).
|
|
336
|
+
// Ideally, the SDK config should have a 'qualityOracle' key set.
|
|
337
|
+
// 1. We just Decode it to Log it (Verification is optional but recommended)
|
|
338
|
+
try {
|
|
339
|
+
const parts = token.split('.');
|
|
340
|
+
const payload = JSON.parse(atob(parts[1]));
|
|
341
|
+
console.log(`\nš¢ [AAMP QUALITY ALERT] Feedback Received from ${payload.agent_id}`);
|
|
342
|
+
console.log(` Reason: ${payload.reason} | Score: ${payload.quality_score}`);
|
|
343
|
+
console.log(` Resource: ${payload.url}`);
|
|
344
|
+
console.log(` (Signature available for dispute evidence)`);
|
|
345
|
+
}
|
|
346
|
+
catch (e) {
|
|
347
|
+
console.log(` ā ļø [AAMP Warning] Malformed Feedback Token.`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
265
350
|
}
|
|
266
|
-
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
|
|
@@ -87,8 +97,17 @@ export interface FeedbackSignal {
|
|
|
87
97
|
}
|
|
88
98
|
export interface EvaluationResult {
|
|
89
99
|
allowed: boolean;
|
|
90
|
-
status: 200 | 400 | 401 | 403;
|
|
100
|
+
status: 200 | 400 | 401 | 402 | 403;
|
|
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.7",
|
|
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/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;
|