@aamp/protocol 1.1.6 ā 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/dist/publisher.d.ts +9 -1
- package/dist/publisher.js +123 -111
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/publisher.ts +147 -125
- package/src/types.ts +1 -1
package/dist/publisher.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export declare class AAMPPublisher {
|
|
|
10
10
|
getPolicy(): AccessPolicy;
|
|
11
11
|
/**
|
|
12
12
|
* Main Entry Point: Evaluate ANY visitor (Human, Bot, or Agent)
|
|
13
|
+
* STAGE 1: IDENTITY (Strict)
|
|
14
|
+
* STAGE 2: POLICY (Permissions)
|
|
15
|
+
* STAGE 3: ACCESS (HQ Content)
|
|
13
16
|
*/
|
|
14
17
|
evaluateVisitor(reqHeaders: Record<string, string | undefined>, rawPayload?: string): Promise<EvaluationResult>;
|
|
15
18
|
/**
|
|
@@ -20,9 +23,14 @@ export declare class AAMPPublisher {
|
|
|
20
23
|
*/
|
|
21
24
|
private performBrowserHeuristics;
|
|
22
25
|
/**
|
|
23
|
-
* Handle AAMP Protocol Logic
|
|
26
|
+
* Handle AAMP Protocol Logic (Strict Mode)
|
|
24
27
|
*/
|
|
28
|
+
private handleAgentStrict;
|
|
25
29
|
private handleAgent;
|
|
30
|
+
/**
|
|
31
|
+
* STAGE 2: POLICY ENFORCEMENT CHECK
|
|
32
|
+
*/
|
|
33
|
+
private checkPolicyStrict;
|
|
26
34
|
private verifyRequestLogic;
|
|
27
35
|
private verifyDnsBinding;
|
|
28
36
|
private isDomain;
|
package/dist/publisher.js
CHANGED
|
@@ -48,54 +48,46 @@ export class AAMPPublisher {
|
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
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)
|
|
51
54
|
*/
|
|
52
55
|
async evaluateVisitor(reqHeaders, rawPayload) {
|
|
53
|
-
|
|
56
|
+
console.log(`\n--- [AAMP LOG START] New Request ---`);
|
|
57
|
+
// --- STAGE 1: IDENTITY VERIFICATION ---
|
|
58
|
+
console.log(`[IDENTITY] š Checking Identity Headers...`);
|
|
54
59
|
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
|
-
}
|
|
59
60
|
if (hasAamp) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return await this.handleAgent(reqHeaders, rawPayload);
|
|
61
|
+
// It claims to be an Agent. Verify it STRICTLY.
|
|
62
|
+
return await this.handleAgentStrict(reqHeaders, rawPayload);
|
|
63
63
|
}
|
|
64
|
-
//
|
|
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.
|
|
65
68
|
if (this.unauthenticatedStrategy === 'STRICT') {
|
|
69
|
+
console.log(`[IDENTITY] ā BLOCKING. Strategy is STRICT.`);
|
|
66
70
|
return {
|
|
67
71
|
allowed: false,
|
|
68
72
|
status: 401,
|
|
69
|
-
reason: "
|
|
73
|
+
reason: "IDENTITY_REQUIRED: Missing AAMP Headers.",
|
|
70
74
|
visitorType: 'UNIDENTIFIED_BOT'
|
|
71
75
|
};
|
|
72
76
|
}
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
allowed: true,
|
|
76
|
-
status: 200,
|
|
77
|
-
reason: "PASSIVE_MODE: Allowed without verification.",
|
|
78
|
-
visitorType: 'LIKELY_HUMAN'
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
// 3. HYBRID MODE: Heuristic Analysis (The "Lazy Bot" Filter)
|
|
77
|
+
console.log(`[IDENTITY] ā ļø SKIPPED (Legacy Mode). Checking Browser Heuristics...`);
|
|
82
78
|
const isHuman = this.performBrowserHeuristics(reqHeaders);
|
|
83
79
|
if (isHuman) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
status: 200,
|
|
87
|
-
reason: "BROWSER_VERIFIED: Heuristics passed.",
|
|
88
|
-
visitorType: 'LIKELY_HUMAN'
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
return {
|
|
93
|
-
allowed: false,
|
|
94
|
-
status: 403,
|
|
95
|
-
reason: "BOT_DETECTED: Request lacks browser signatures and AAMP headers.",
|
|
96
|
-
visitorType: 'UNIDENTIFIED_BOT'
|
|
97
|
-
};
|
|
80
|
+
console.log(`[POLICY] š¤ ALLOWED. Browser Heuristics Passed.`);
|
|
81
|
+
return { allowed: true, status: 200, reason: "BROWSER_VERIFIED", visitorType: 'LIKELY_HUMAN' };
|
|
98
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
|
+
};
|
|
99
91
|
}
|
|
100
92
|
/**
|
|
101
93
|
* Browser Heuristics (Hardened)
|
|
@@ -127,143 +119,163 @@ export class AAMPPublisher {
|
|
|
127
119
|
return false;
|
|
128
120
|
}
|
|
129
121
|
/**
|
|
130
|
-
* Handle AAMP Protocol Logic
|
|
122
|
+
* Handle AAMP Protocol Logic (Strict Mode)
|
|
131
123
|
*/
|
|
132
|
-
async
|
|
124
|
+
async handleAgentStrict(reqHeaders, rawPayload) {
|
|
125
|
+
let agentId = "UNKNOWN";
|
|
133
126
|
try {
|
|
127
|
+
// 1. Decode Headers
|
|
134
128
|
const payloadHeader = reqHeaders[HEADERS.PAYLOAD];
|
|
135
129
|
const sigHeader = reqHeaders[HEADERS.SIGNATURE];
|
|
136
130
|
const keyHeader = reqHeaders[HEADERS.PUBLIC_KEY];
|
|
137
131
|
const headerJson = atob(payloadHeader);
|
|
138
132
|
const requestHeader = JSON.parse(headerJson);
|
|
133
|
+
agentId = requestHeader.agent_id;
|
|
134
|
+
console.log(`[IDENTITY] š Claimed ID: ${agentId}`);
|
|
135
|
+
// 2. Crypto & DNS Verification
|
|
139
136
|
const signedRequest = {
|
|
140
137
|
header: requestHeader,
|
|
141
138
|
signature: sigHeader,
|
|
142
139
|
publicKey: keyHeader
|
|
143
140
|
};
|
|
144
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"]);
|
|
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}...`);
|
|
145
152
|
const proofToken = reqHeaders[HEADERS.PROOF_TOKEN];
|
|
146
153
|
const paymentCredential = reqHeaders[HEADERS.PAYMENT_CREDENTIAL];
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
console.log(
|
|
151
|
-
|
|
152
|
-
Reason: ${result.reason}
|
|
153
|
-
VisitorType: ${result.visitorType}
|
|
154
|
-
Proof: ${result.proofUsed || 'None'}
|
|
155
|
-
Identity Verified: ${result.identityVerified}`);
|
|
156
|
-
return {
|
|
157
|
-
allowed: false,
|
|
158
|
-
status: 403,
|
|
159
|
-
reason: result.reason,
|
|
160
|
-
visitorType: 'VERIFIED_AGENT'
|
|
161
|
-
};
|
|
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;
|
|
162
159
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
Payment Method: ${result.proofUsed}
|
|
167
|
-
Identity Verified: ${result.identityVerified}`);
|
|
160
|
+
// --- STAGE 3: ACCESS GRANT ---
|
|
161
|
+
console.log(`[POLICY] ā
PASSED. Requirements Met.`);
|
|
162
|
+
console.log(`[ACCESS] š GRANTED. Unlocking HQ Content.`);
|
|
168
163
|
return {
|
|
169
164
|
allowed: true,
|
|
170
165
|
status: 200,
|
|
171
166
|
reason: "AAMP_VERIFIED",
|
|
172
167
|
visitorType: 'VERIFIED_AGENT',
|
|
173
|
-
metadata: requestHeader
|
|
168
|
+
metadata: requestHeader,
|
|
169
|
+
proofUsed: policyResult.proofUsed
|
|
174
170
|
};
|
|
175
171
|
}
|
|
176
172
|
catch (e) {
|
|
177
|
-
console.error(e);
|
|
173
|
+
console.error(`[AAMP ERROR]`, e);
|
|
178
174
|
return { allowed: false, status: 400, reason: "INVALID_SIGNATURE", visitorType: 'UNIDENTIFIED_BOT' };
|
|
179
175
|
}
|
|
180
176
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (!
|
|
191
|
-
return { allowed: false, reason: '
|
|
192
|
-
// 3. Identity Verification (DNS Binding) with Cache
|
|
193
|
-
let identityVerified = false;
|
|
194
|
-
const claimedDomain = request.header.agent_id;
|
|
195
|
-
const pubKeyString = await exportPublicKey(requestPublicKey);
|
|
196
|
-
console.log(` š [AAMP Identity] Verifying DNS Binding for: ${claimedDomain}`);
|
|
197
|
-
// Check Cache First
|
|
198
|
-
const cachedKey = await this.cache.get(claimedDomain);
|
|
199
|
-
if (cachedKey === pubKeyString) {
|
|
200
|
-
console.log(" ā” [AAMP Cache] Identity found in cache.");
|
|
201
|
-
identityVerified = true;
|
|
202
|
-
}
|
|
203
|
-
else if (this.isDomain(claimedDomain)) {
|
|
204
|
-
// Cache Miss: Perform DNS Fetch
|
|
205
|
-
identityVerified = await this.verifyDnsBinding(claimedDomain, pubKeyString);
|
|
206
|
-
if (identityVerified) {
|
|
207
|
-
await this.cache.set(claimedDomain, pubKeyString, this.CACHE_TTL_SECONDS);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
if (this.policy.requireIdentityBinding && !identityVerified) {
|
|
211
|
-
return { allowed: false, reason: 'IDENTITY_FAIL: DNS Binding could not be verified.', identityVerified: false };
|
|
212
|
-
}
|
|
213
|
-
// 4. Policy Check: Purpose
|
|
214
|
-
if (request.header.purpose === AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
|
|
215
|
-
return { allowed: false, reason: 'POLICY_DENIED: Training not allowed.', identityVerified };
|
|
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' };
|
|
216
188
|
}
|
|
217
|
-
if (
|
|
218
|
-
return { allowed: false, reason: 'POLICY_DENIED: RAG not allowed.',
|
|
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' };
|
|
219
191
|
}
|
|
220
|
-
//
|
|
192
|
+
// 2. Policy Check: Economics (v1.2) - Payment & Ads
|
|
221
193
|
if (this.policy.requiresPayment) {
|
|
222
194
|
let paymentSatisfied = false;
|
|
223
195
|
// Method A: Flexible Payment Callback (DB / Custom Logic)
|
|
224
196
|
if (this.policy.monetization?.checkPayment) {
|
|
225
|
-
const isPaid = await this.policy.monetization.checkPayment(
|
|
197
|
+
const isPaid = await this.policy.monetization.checkPayment(requestHeader.agent_id, requestHeader.purpose);
|
|
226
198
|
if (isPaid) {
|
|
227
|
-
console.log(`
|
|
228
|
-
return { allowed: true, reason: 'OK',
|
|
199
|
+
console.log(`[POLICY] š° Payment Verified via Callback.`);
|
|
200
|
+
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'WHITELIST_CALLBACK' };
|
|
229
201
|
}
|
|
230
202
|
}
|
|
231
203
|
// Method B: Payment Credentials (Unified JWT)
|
|
232
204
|
if (!paymentSatisfied && this.policy.monetization?.paymentConfig && paymentCredential) {
|
|
233
205
|
const { jwksUrl, issuer } = this.policy.monetization.paymentConfig;
|
|
234
|
-
console.log(`
|
|
206
|
+
console.log(`[POLICY] š Verifying Payment Credential (Issuer: ${issuer})...`);
|
|
235
207
|
const isValidCredential = await verifyJwt(paymentCredential, jwksUrl, issuer);
|
|
236
208
|
if (isValidCredential) {
|
|
237
|
-
console.log(`
|
|
238
|
-
return { allowed: true, reason: 'OK',
|
|
209
|
+
console.log(`[POLICY] ā
Credential Signature VALID.`);
|
|
210
|
+
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'PAYMENT_CREDENTIAL_JWT' };
|
|
239
211
|
}
|
|
240
212
|
else {
|
|
241
|
-
console.log(`
|
|
213
|
+
console.log(`[POLICY] ā Credential Signature INVALID.`);
|
|
242
214
|
}
|
|
243
215
|
}
|
|
244
216
|
// Method C: Ad-Supported (Proof Verification)
|
|
245
|
-
if (!paymentSatisfied && this.policy.allowAdSupportedAccess &&
|
|
217
|
+
if (!paymentSatisfied && this.policy.allowAdSupportedAccess && requestHeader.context?.ads_displayed) {
|
|
246
218
|
if (proofToken && this.policy.monetization?.adNetwork) {
|
|
247
219
|
const { jwksUrl, issuer } = this.policy.monetization.adNetwork;
|
|
248
|
-
console.log(`
|
|
220
|
+
console.log(`[POLICY] šŗ Verifying Ad Proof (Issuer: ${issuer})...`);
|
|
249
221
|
const isValidProof = await verifyJwt(proofToken, jwksUrl, issuer);
|
|
250
222
|
if (isValidProof) {
|
|
251
|
-
console.log(`
|
|
252
|
-
return { allowed: true, reason: 'OK',
|
|
223
|
+
console.log(`[POLICY] ā
Ad Proof Signature VALID.`);
|
|
224
|
+
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'AD_PROOF_JWT' };
|
|
253
225
|
}
|
|
254
226
|
else {
|
|
255
|
-
console.log(`
|
|
227
|
+
console.log(`[POLICY] ā Ad Proof Signature INVALID.`);
|
|
256
228
|
}
|
|
257
229
|
}
|
|
258
230
|
else {
|
|
259
|
-
console.log(`
|
|
231
|
+
console.log(`[POLICY] ā ļø Ad Proof MISSING.`);
|
|
260
232
|
}
|
|
261
233
|
}
|
|
262
|
-
|
|
263
|
-
|
|
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) {
|
|
246
|
+
// 1. Replay Attack Prevention
|
|
247
|
+
const requestTime = new Date(request.header.ts).getTime();
|
|
248
|
+
if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
|
|
249
|
+
return { allowed: false, reason: 'TIMESTAMP_INVALID: Clock skew too large.', identityVerified: false };
|
|
250
|
+
}
|
|
251
|
+
// 2. Crypto Verification
|
|
252
|
+
const signableString = JSON.stringify(request.header);
|
|
253
|
+
const isCryptoValid = await verifySignature(requestPublicKey, signableString, request.signature);
|
|
254
|
+
if (!isCryptoValid)
|
|
255
|
+
return { allowed: false, reason: 'CRYPTO_FAIL: Signature invalid.', identityVerified: false };
|
|
256
|
+
// 3. Identity Verification (DNS Binding) with Cache
|
|
257
|
+
let identityVerified = false;
|
|
258
|
+
const claimedDomain = request.header.agent_id;
|
|
259
|
+
const pubKeyString = await exportPublicKey(requestPublicKey);
|
|
260
|
+
console.log(`[IDENTITY] š Verifying DNS Binding for: ${claimedDomain}`);
|
|
261
|
+
// Check Cache First
|
|
262
|
+
const cachedKey = await this.cache.get(claimedDomain);
|
|
263
|
+
if (cachedKey === pubKeyString) {
|
|
264
|
+
console.log("[IDENTITY] ā” Cache Hit. Identity Verified.");
|
|
265
|
+
identityVerified = true;
|
|
266
|
+
}
|
|
267
|
+
else if (this.isDomain(claimedDomain)) {
|
|
268
|
+
// Cache Miss: Perform DNS Fetch
|
|
269
|
+
identityVerified = await this.verifyDnsBinding(claimedDomain, pubKeyString);
|
|
270
|
+
if (identityVerified) {
|
|
271
|
+
await this.cache.set(claimedDomain, pubKeyString, this.CACHE_TTL_SECONDS);
|
|
264
272
|
}
|
|
265
273
|
}
|
|
266
|
-
|
|
274
|
+
if (this.policy.requireIdentityBinding && !identityVerified) {
|
|
275
|
+
return { allowed: false, reason: 'IDENTITY_FAIL: DNS Binding could not be verified.', identityVerified: false };
|
|
276
|
+
}
|
|
277
|
+
// Return verified status so handleAgentStrict can proceed to Policy Check
|
|
278
|
+
return { allowed: true, reason: 'OK', identityVerified: true };
|
|
267
279
|
}
|
|
268
280
|
async verifyDnsBinding(domain, requestKeySpki) {
|
|
269
281
|
try {
|
package/dist/types.d.ts
CHANGED
|
@@ -97,7 +97,7 @@ export interface FeedbackSignal {
|
|
|
97
97
|
}
|
|
98
98
|
export interface EvaluationResult {
|
|
99
99
|
allowed: boolean;
|
|
100
|
-
status: 200 | 400 | 401 | 403;
|
|
100
|
+
status: 200 | 400 | 401 | 402 | 403;
|
|
101
101
|
reason: string;
|
|
102
102
|
visitorType: 'VERIFIED_AGENT' | 'LIKELY_HUMAN' | 'UNIDENTIFIED_BOT';
|
|
103
103
|
metadata?: any;
|
package/package.json
CHANGED
package/src/publisher.ts
CHANGED
|
@@ -69,63 +69,56 @@ export class AAMPPublisher {
|
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* Main Entry Point: Evaluate ANY visitor (Human, Bot, or Agent)
|
|
72
|
+
* STAGE 1: IDENTITY (Strict)
|
|
73
|
+
* STAGE 2: POLICY (Permissions)
|
|
74
|
+
* STAGE 3: ACCESS (HQ Content)
|
|
72
75
|
*/
|
|
73
76
|
async evaluateVisitor(
|
|
74
77
|
reqHeaders: Record<string, string | undefined>,
|
|
75
78
|
rawPayload?: string
|
|
76
79
|
): Promise<EvaluationResult> {
|
|
80
|
+
console.log(`\n--- [AAMP LOG START] New Request ---`);
|
|
77
81
|
|
|
78
|
-
// 1
|
|
79
|
-
|
|
82
|
+
// --- STAGE 1: IDENTITY VERIFICATION ---
|
|
83
|
+
console.log(`[IDENTITY] š Checking Identity Headers...`);
|
|
80
84
|
|
|
81
|
-
const
|
|
82
|
-
if (feedbackToken) {
|
|
83
|
-
await this.handleFeedback(feedbackToken, reqHeaders);
|
|
84
|
-
}
|
|
85
|
+
const hasAamp = reqHeaders[HEADERS.PAYLOAD] && reqHeaders[HEADERS.SIGNATURE] && reqHeaders[HEADERS.PUBLIC_KEY];
|
|
85
86
|
|
|
86
87
|
if (hasAamp) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return await this.handleAgent(reqHeaders, rawPayload);
|
|
88
|
+
// It claims to be an Agent. Verify it STRICTLY.
|
|
89
|
+
return await this.handleAgentStrict(reqHeaders, rawPayload);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
//
|
|
92
|
+
// If NO AAMP Headers -> FAIL IDENTITY immediately.
|
|
93
|
+
console.log(`[IDENTITY] ā FAILED. No AAMP Headers found.`);
|
|
94
|
+
|
|
95
|
+
// For now, retaining the legacy "Passive/Hybrid" switch just to avoid breaking browser demos completely
|
|
96
|
+
// BUT logging it as a specific "Identity Fail" flow.
|
|
93
97
|
if (this.unauthenticatedStrategy === 'STRICT') {
|
|
98
|
+
console.log(`[IDENTITY] ā BLOCKING. Strategy is STRICT.`);
|
|
94
99
|
return {
|
|
95
100
|
allowed: false,
|
|
96
101
|
status: 401,
|
|
97
|
-
reason: "
|
|
102
|
+
reason: "IDENTITY_REQUIRED: Missing AAMP Headers.",
|
|
98
103
|
visitorType: 'UNIDENTIFIED_BOT'
|
|
99
104
|
};
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
allowed: true,
|
|
105
|
-
status: 200,
|
|
106
|
-
reason: "PASSIVE_MODE: Allowed without verification.",
|
|
107
|
-
visitorType: 'LIKELY_HUMAN'
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// 3. HYBRID MODE: Heuristic Analysis (The "Lazy Bot" Filter)
|
|
107
|
+
console.log(`[IDENTITY] ā ļø SKIPPED (Legacy Mode). Checking Browser Heuristics...`);
|
|
112
108
|
const isHuman = this.performBrowserHeuristics(reqHeaders);
|
|
113
|
-
|
|
114
109
|
if (isHuman) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
status: 200,
|
|
118
|
-
reason: "BROWSER_VERIFIED: Heuristics passed.",
|
|
119
|
-
visitorType: 'LIKELY_HUMAN'
|
|
120
|
-
};
|
|
121
|
-
} else {
|
|
122
|
-
return {
|
|
123
|
-
allowed: false,
|
|
124
|
-
status: 403,
|
|
125
|
-
reason: "BOT_DETECTED: Request lacks browser signatures and AAMP headers.",
|
|
126
|
-
visitorType: 'UNIDENTIFIED_BOT'
|
|
127
|
-
};
|
|
110
|
+
console.log(`[POLICY] š¤ ALLOWED. Browser Heuristics Passed.`);
|
|
111
|
+
return { allowed: true, status: 200, reason: "BROWSER_VERIFIED", visitorType: 'LIKELY_HUMAN' };
|
|
128
112
|
}
|
|
113
|
+
|
|
114
|
+
console.log(`[IDENTITY] ā FAILED. Not a Browser, No Headers.`);
|
|
115
|
+
console.log(`[ACCESS] ā BLOCKED.`);
|
|
116
|
+
return {
|
|
117
|
+
allowed: false,
|
|
118
|
+
status: 403,
|
|
119
|
+
reason: "IDENTITY_FAIL: No Identity, No Browser.",
|
|
120
|
+
visitorType: 'UNIDENTIFIED_BOT'
|
|
121
|
+
};
|
|
129
122
|
}
|
|
130
123
|
|
|
131
124
|
/**
|
|
@@ -161,17 +154,24 @@ export class AAMPPublisher {
|
|
|
161
154
|
}
|
|
162
155
|
|
|
163
156
|
/**
|
|
164
|
-
* Handle AAMP Protocol Logic
|
|
157
|
+
* Handle AAMP Protocol Logic (Strict Mode)
|
|
165
158
|
*/
|
|
166
|
-
private async
|
|
159
|
+
private async handleAgentStrict(reqHeaders: Record<string, string | undefined>, rawPayload?: string): Promise<EvaluationResult> {
|
|
160
|
+
let agentId = "UNKNOWN";
|
|
161
|
+
|
|
167
162
|
try {
|
|
163
|
+
// 1. Decode Headers
|
|
168
164
|
const payloadHeader = reqHeaders[HEADERS.PAYLOAD]!;
|
|
169
165
|
const sigHeader = reqHeaders[HEADERS.SIGNATURE]!;
|
|
170
166
|
const keyHeader = reqHeaders[HEADERS.PUBLIC_KEY]!;
|
|
171
167
|
|
|
172
168
|
const headerJson = atob(payloadHeader);
|
|
173
169
|
const requestHeader = JSON.parse(headerJson);
|
|
170
|
+
agentId = requestHeader.agent_id;
|
|
174
171
|
|
|
172
|
+
console.log(`[IDENTITY] š Claimed ID: ${agentId}`);
|
|
173
|
+
|
|
174
|
+
// 2. Crypto & DNS Verification
|
|
175
175
|
const signedRequest: SignedAccessRequest = {
|
|
176
176
|
header: requestHeader,
|
|
177
177
|
signature: sigHeader,
|
|
@@ -186,151 +186,173 @@ export class AAMPPublisher {
|
|
|
186
186
|
["verify"]
|
|
187
187
|
);
|
|
188
188
|
|
|
189
|
+
// Verify Core Logic (DNS + Crypto)
|
|
190
|
+
const verification = await this.verifyRequestLogic(signedRequest, agentKey);
|
|
191
|
+
|
|
192
|
+
if (!verification.identityVerified) {
|
|
193
|
+
console.log(`[IDENTITY] ā FAILED. Reason: ${verification.reason}`);
|
|
194
|
+
console.log(`[ACCESS] ā BLOCKED.`);
|
|
195
|
+
return { allowed: false, status: 403, reason: verification.reason, visitorType: 'UNIDENTIFIED_BOT' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log(`[IDENTITY] ā
PASSED. DNS Binding Verified.`);
|
|
199
|
+
|
|
200
|
+
// --- STAGE 2: POLICY ENFORCEMENT ---
|
|
201
|
+
console.log(`[POLICY] š Checking Permissions for ${agentId}...`);
|
|
202
|
+
|
|
189
203
|
const proofToken = reqHeaders[HEADERS.PROOF_TOKEN];
|
|
190
204
|
const paymentCredential = reqHeaders[HEADERS.PAYMENT_CREDENTIAL];
|
|
191
205
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
console.log(
|
|
197
|
-
|
|
198
|
-
Reason: ${result.reason}
|
|
199
|
-
VisitorType: ${result.visitorType}
|
|
200
|
-
Proof: ${result.proofUsed || 'None'}
|
|
201
|
-
Identity Verified: ${result.identityVerified}`);
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
allowed: false,
|
|
205
|
-
status: 403,
|
|
206
|
-
reason: result.reason,
|
|
207
|
-
visitorType: 'VERIFIED_AGENT'
|
|
208
|
-
};
|
|
206
|
+
const policyResult = await this.checkPolicyStrict(requestHeader, proofToken, paymentCredential);
|
|
207
|
+
|
|
208
|
+
if (!policyResult.allowed) {
|
|
209
|
+
console.log(`[POLICY] ā DENIED. Reason: ${policyResult.reason}`);
|
|
210
|
+
console.log(`[ACCESS] ā BLOCKED.`);
|
|
211
|
+
return policyResult;
|
|
209
212
|
}
|
|
210
213
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
Payment Method: ${result.proofUsed}
|
|
215
|
-
Identity Verified: ${result.identityVerified}`);
|
|
214
|
+
// --- STAGE 3: ACCESS GRANT ---
|
|
215
|
+
console.log(`[POLICY] ā
PASSED. Requirements Met.`);
|
|
216
|
+
console.log(`[ACCESS] š GRANTED. Unlocking HQ Content.`);
|
|
216
217
|
|
|
217
218
|
return {
|
|
218
219
|
allowed: true,
|
|
219
220
|
status: 200,
|
|
220
221
|
reason: "AAMP_VERIFIED",
|
|
221
222
|
visitorType: 'VERIFIED_AGENT',
|
|
222
|
-
metadata: requestHeader
|
|
223
|
+
metadata: requestHeader,
|
|
224
|
+
proofUsed: policyResult.proofUsed
|
|
223
225
|
};
|
|
224
226
|
|
|
225
227
|
} catch (e) {
|
|
226
|
-
console.error(e);
|
|
228
|
+
console.error(`[AAMP ERROR]`, e);
|
|
227
229
|
return { allowed: false, status: 400, reason: "INVALID_SIGNATURE", visitorType: 'UNIDENTIFIED_BOT' };
|
|
228
230
|
}
|
|
229
231
|
}
|
|
230
232
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
paymentCredential?: string,
|
|
236
|
-
rawPayload?: string
|
|
237
|
-
): Promise<VerificationResult> {
|
|
238
|
-
|
|
239
|
-
// 1. Replay Attack Prevention
|
|
240
|
-
const requestTime = new Date(request.header.ts).getTime();
|
|
241
|
-
if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
|
|
242
|
-
return { allowed: false, reason: 'TIMESTAMP_INVALID', identityVerified: false };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// 2. Crypto Verification
|
|
246
|
-
const signableString = rawPayload || JSON.stringify(request.header);
|
|
247
|
-
const isCryptoValid = await verifySignature(requestPublicKey, signableString, request.signature);
|
|
248
|
-
if (!isCryptoValid) return { allowed: false, reason: 'CRYPTO_FAIL', identityVerified: false };
|
|
249
|
-
|
|
250
|
-
// 3. Identity Verification (DNS Binding) with Cache
|
|
251
|
-
let identityVerified = false;
|
|
252
|
-
const claimedDomain = request.header.agent_id;
|
|
253
|
-
const pubKeyString = await exportPublicKey(requestPublicKey);
|
|
254
|
-
|
|
255
|
-
console.log(` š [AAMP Identity] Verifying DNS Binding for: ${claimedDomain}`);
|
|
256
|
-
|
|
257
|
-
// Check Cache First
|
|
258
|
-
const cachedKey = await this.cache.get(claimedDomain);
|
|
259
|
-
|
|
260
|
-
if (cachedKey === pubKeyString) {
|
|
261
|
-
console.log(" ā” [AAMP Cache] Identity found in cache.");
|
|
262
|
-
identityVerified = true;
|
|
263
|
-
} else if (this.isDomain(claimedDomain)) {
|
|
264
|
-
// Cache Miss: Perform DNS Fetch
|
|
265
|
-
identityVerified = await this.verifyDnsBinding(claimedDomain, pubKeyString);
|
|
266
|
-
if (identityVerified) {
|
|
267
|
-
await this.cache.set(claimedDomain, pubKeyString, this.CACHE_TTL_SECONDS);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
233
|
+
// Legacy handler kept for interface compatibility (deprecated)
|
|
234
|
+
private async handleAgent(reqHeaders: Record<string, string | undefined>, rawPayload?: string): Promise<EvaluationResult> {
|
|
235
|
+
return this.handleAgentStrict(reqHeaders, rawPayload);
|
|
236
|
+
}
|
|
270
237
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
238
|
+
/**
|
|
239
|
+
* STAGE 2: POLICY ENFORCEMENT CHECK
|
|
240
|
+
*/
|
|
241
|
+
private async checkPolicyStrict(
|
|
242
|
+
requestHeader: any,
|
|
243
|
+
proofToken?: string,
|
|
244
|
+
paymentCredential?: string
|
|
245
|
+
): Promise<EvaluationResult> {
|
|
274
246
|
|
|
275
|
-
//
|
|
276
|
-
if (
|
|
277
|
-
return { allowed: false, reason: 'POLICY_DENIED: Training not allowed.',
|
|
247
|
+
// 1. Policy Check: Purpose Ban (e.g. No Training)
|
|
248
|
+
if (requestHeader.purpose === AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
|
|
249
|
+
return { allowed: false, status: 403, reason: 'POLICY_DENIED: Training not allowed.', visitorType: 'VERIFIED_AGENT' };
|
|
278
250
|
}
|
|
279
|
-
if (
|
|
280
|
-
return { allowed: false, reason: 'POLICY_DENIED: RAG not allowed.',
|
|
251
|
+
if (requestHeader.purpose === AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
|
|
252
|
+
return { allowed: false, status: 403, reason: 'POLICY_DENIED: RAG not allowed.', visitorType: 'VERIFIED_AGENT' };
|
|
281
253
|
}
|
|
282
254
|
|
|
283
|
-
//
|
|
255
|
+
// 2. Policy Check: Economics (v1.2) - Payment & Ads
|
|
284
256
|
if (this.policy.requiresPayment) {
|
|
285
257
|
let paymentSatisfied = false;
|
|
286
258
|
|
|
287
259
|
// Method A: Flexible Payment Callback (DB / Custom Logic)
|
|
288
260
|
if (this.policy.monetization?.checkPayment) {
|
|
289
|
-
const isPaid = await this.policy.monetization.checkPayment(
|
|
261
|
+
const isPaid = await this.policy.monetization.checkPayment(requestHeader.agent_id, requestHeader.purpose);
|
|
290
262
|
if (isPaid) {
|
|
291
|
-
console.log(`
|
|
292
|
-
return { allowed: true, reason: 'OK',
|
|
263
|
+
console.log(`[POLICY] š° Payment Verified via Callback.`);
|
|
264
|
+
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'WHITELIST_CALLBACK' };
|
|
293
265
|
}
|
|
294
266
|
}
|
|
295
267
|
|
|
296
268
|
// Method B: Payment Credentials (Unified JWT)
|
|
297
269
|
if (!paymentSatisfied && this.policy.monetization?.paymentConfig && paymentCredential) {
|
|
298
270
|
const { jwksUrl, issuer } = this.policy.monetization.paymentConfig;
|
|
299
|
-
console.log(`
|
|
271
|
+
console.log(`[POLICY] š Verifying Payment Credential (Issuer: ${issuer})...`);
|
|
300
272
|
|
|
301
273
|
const isValidCredential = await verifyJwt(paymentCredential, jwksUrl, issuer);
|
|
302
274
|
if (isValidCredential) {
|
|
303
|
-
console.log(`
|
|
304
|
-
return { allowed: true, reason: 'OK',
|
|
275
|
+
console.log(`[POLICY] ā
Credential Signature VALID.`);
|
|
276
|
+
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'PAYMENT_CREDENTIAL_JWT' };
|
|
305
277
|
} else {
|
|
306
|
-
console.log(`
|
|
278
|
+
console.log(`[POLICY] ā Credential Signature INVALID.`);
|
|
307
279
|
}
|
|
308
280
|
}
|
|
309
281
|
|
|
310
282
|
// Method C: Ad-Supported (Proof Verification)
|
|
311
|
-
if (!paymentSatisfied && this.policy.allowAdSupportedAccess &&
|
|
283
|
+
if (!paymentSatisfied && this.policy.allowAdSupportedAccess && requestHeader.context?.ads_displayed) {
|
|
312
284
|
if (proofToken && this.policy.monetization?.adNetwork) {
|
|
313
285
|
const { jwksUrl, issuer } = this.policy.monetization.adNetwork;
|
|
314
|
-
console.log(`
|
|
286
|
+
console.log(`[POLICY] šŗ Verifying Ad Proof (Issuer: ${issuer})...`);
|
|
315
287
|
|
|
316
288
|
const isValidProof = await verifyJwt(proofToken, jwksUrl, issuer);
|
|
317
289
|
if (isValidProof) {
|
|
318
|
-
console.log(`
|
|
319
|
-
return { allowed: true, reason: 'OK',
|
|
290
|
+
console.log(`[POLICY] ā
Ad Proof Signature VALID.`);
|
|
291
|
+
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'AD_PROOF_JWT' };
|
|
320
292
|
} else {
|
|
321
|
-
console.log(`
|
|
293
|
+
console.log(`[POLICY] ā Ad Proof Signature INVALID.`);
|
|
322
294
|
}
|
|
323
295
|
} else {
|
|
324
|
-
console.log(`
|
|
296
|
+
console.log(`[POLICY] ā ļø Ad Proof MISSING.`);
|
|
325
297
|
}
|
|
326
298
|
}
|
|
327
299
|
|
|
328
|
-
|
|
329
|
-
|
|
300
|
+
return {
|
|
301
|
+
allowed: false,
|
|
302
|
+
status: 402,
|
|
303
|
+
reason: 'PAYMENT_REQUIRED: Whitelist, Credential, and Ad Proof checks ALL failed.',
|
|
304
|
+
visitorType: 'VERIFIED_AGENT',
|
|
305
|
+
proofUsed: 'NONE'
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// If no payment required, allow.
|
|
310
|
+
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT' };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private async verifyRequestLogic(
|
|
314
|
+
request: SignedAccessRequest,
|
|
315
|
+
requestPublicKey: CryptoKey,
|
|
316
|
+
): Promise<VerificationResult> {
|
|
317
|
+
|
|
318
|
+
// 1. Replay Attack Prevention
|
|
319
|
+
const requestTime = new Date(request.header.ts).getTime();
|
|
320
|
+
if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
|
|
321
|
+
return { allowed: false, reason: 'TIMESTAMP_INVALID: Clock skew too large.', identityVerified: false };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 2. Crypto Verification
|
|
325
|
+
const signableString = JSON.stringify(request.header);
|
|
326
|
+
const isCryptoValid = await verifySignature(requestPublicKey, signableString, request.signature);
|
|
327
|
+
if (!isCryptoValid) return { allowed: false, reason: 'CRYPTO_FAIL: Signature invalid.', identityVerified: false };
|
|
328
|
+
|
|
329
|
+
// 3. Identity Verification (DNS Binding) with Cache
|
|
330
|
+
let identityVerified = false;
|
|
331
|
+
const claimedDomain = request.header.agent_id;
|
|
332
|
+
const pubKeyString = await exportPublicKey(requestPublicKey);
|
|
333
|
+
|
|
334
|
+
console.log(`[IDENTITY] š Verifying DNS Binding for: ${claimedDomain}`);
|
|
335
|
+
|
|
336
|
+
// Check Cache First
|
|
337
|
+
const cachedKey = await this.cache.get(claimedDomain);
|
|
338
|
+
|
|
339
|
+
if (cachedKey === pubKeyString) {
|
|
340
|
+
console.log("[IDENTITY] ā” Cache Hit. Identity Verified.");
|
|
341
|
+
identityVerified = true;
|
|
342
|
+
} else if (this.isDomain(claimedDomain)) {
|
|
343
|
+
// Cache Miss: Perform DNS Fetch
|
|
344
|
+
identityVerified = await this.verifyDnsBinding(claimedDomain, pubKeyString);
|
|
345
|
+
if (identityVerified) {
|
|
346
|
+
await this.cache.set(claimedDomain, pubKeyString, this.CACHE_TTL_SECONDS);
|
|
330
347
|
}
|
|
331
348
|
}
|
|
332
349
|
|
|
333
|
-
|
|
350
|
+
if (this.policy.requireIdentityBinding && !identityVerified) {
|
|
351
|
+
return { allowed: false, reason: 'IDENTITY_FAIL: DNS Binding could not be verified.', identityVerified: false };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Return verified status so handleAgentStrict can proceed to Policy Check
|
|
355
|
+
return { allowed: true, reason: 'OK', identityVerified: true };
|
|
334
356
|
}
|
|
335
357
|
|
|
336
358
|
private async verifyDnsBinding(domain: string, requestKeySpki: string): Promise<boolean> {
|
package/src/types.ts
CHANGED
|
@@ -128,7 +128,7 @@ export interface FeedbackSignal {
|
|
|
128
128
|
// Result of the full evaluation pipeline
|
|
129
129
|
export interface EvaluationResult {
|
|
130
130
|
allowed: boolean;
|
|
131
|
-
status: 200 | 400 | 401 | 403;
|
|
131
|
+
status: 200 | 400 | 401 | 402 | 403;
|
|
132
132
|
reason: string;
|
|
133
133
|
visitorType: 'VERIFIED_AGENT' | 'LIKELY_HUMAN' | 'UNIDENTIFIED_BOT';
|
|
134
134
|
metadata?: any;
|