@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/src/proof.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
2
|
+
|
|
3
|
+
// In-memory cache for JWKS to avoid repeated fetches
|
|
4
|
+
// Jose's createRemoteJWKSet handles caching/cooldowns internally.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Verifies a JWT (Proof Token or Payment Credential) using JWKS.
|
|
8
|
+
*
|
|
9
|
+
* @param token The JWT string
|
|
10
|
+
* @param jwksUrl The URL to fetch Public Keys
|
|
11
|
+
* @param issuer The expected issuer
|
|
12
|
+
* @param audience The expected audience range
|
|
13
|
+
*/
|
|
14
|
+
export async function verifyJwt(
|
|
15
|
+
token: string,
|
|
16
|
+
jwksUrl: string,
|
|
17
|
+
issuer: string,
|
|
18
|
+
audience?: string
|
|
19
|
+
): Promise<boolean> {
|
|
20
|
+
try {
|
|
21
|
+
const JWKS = createRemoteJWKSet(new URL(jwksUrl));
|
|
22
|
+
|
|
23
|
+
const { payload } = await jwtVerify(token, JWKS, {
|
|
24
|
+
issuer: issuer,
|
|
25
|
+
audience: audience // specific audience check if provided
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Check specific AAMP claims if we standardize them
|
|
29
|
+
// if (payload.type !== 'AD_IMPRESSION') return false;
|
|
30
|
+
|
|
31
|
+
return true;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// console.error("Ad Proof Verification Failed:", error);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/publisher.ts
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
* Layer 2: Publisher Middleware
|
|
3
3
|
* Used by content owners to enforce policy, log access, and filter bots.
|
|
4
4
|
*/
|
|
5
|
-
import { HEADERS, MAX_CLOCK_SKEW_MS, WELL_KNOWN_AGENT_PATH } from './constants';
|
|
6
|
-
import { exportPublicKey, signData, verifySignature } from './crypto';
|
|
7
|
-
import { AccessPolicy, AccessPurpose, AgentIdentityManifest, ContentOrigin, EvaluationResult, IdentityCache, SignedAccessRequest, UnauthenticatedStrategy } from './types';
|
|
5
|
+
import { HEADERS, MAX_CLOCK_SKEW_MS, WELL_KNOWN_AGENT_PATH } from './constants.js';
|
|
6
|
+
import { exportPublicKey, signData, verifySignature } from './crypto.js';
|
|
7
|
+
import { AccessPolicy, AccessPurpose, AgentIdentityManifest, ContentOrigin, EvaluationResult, IdentityCache, SignedAccessRequest, UnauthenticatedStrategy } from './types.js';
|
|
8
|
+
import { verifyJwt } from './proof.js';
|
|
8
9
|
|
|
9
10
|
interface VerificationResult {
|
|
10
11
|
allowed: boolean;
|
|
11
12
|
reason: string;
|
|
12
13
|
identityVerified: boolean;
|
|
14
|
+
proofUsed?: string; // "WHITELIST", "CREDENTIAL_JWT", "AD_JWT"
|
|
15
|
+
visitorType?: string; // For audit logs
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
/**
|
|
@@ -18,7 +21,7 @@ interface VerificationResult {
|
|
|
18
21
|
*/
|
|
19
22
|
class MemoryCache implements IdentityCache {
|
|
20
23
|
private store = new Map<string, { val: string, exp: number }>();
|
|
21
|
-
|
|
24
|
+
|
|
22
25
|
async get(key: string): Promise<string | null> {
|
|
23
26
|
const item = this.store.get(key);
|
|
24
27
|
if (!item) return null;
|
|
@@ -28,11 +31,11 @@ class MemoryCache implements IdentityCache {
|
|
|
28
31
|
}
|
|
29
32
|
return item.val;
|
|
30
33
|
}
|
|
31
|
-
|
|
34
|
+
|
|
32
35
|
async set(key: string, value: string, ttlSeconds: number): Promise<void> {
|
|
33
|
-
this.store.set(key, {
|
|
34
|
-
val: value,
|
|
35
|
-
exp: Date.now() + (ttlSeconds * 1000)
|
|
36
|
+
this.store.set(key, {
|
|
37
|
+
val: value,
|
|
38
|
+
exp: Date.now() + (ttlSeconds * 1000)
|
|
36
39
|
});
|
|
37
40
|
}
|
|
38
41
|
}
|
|
@@ -42,14 +45,14 @@ export class AAMPPublisher {
|
|
|
42
45
|
private keyPair: CryptoKeyPair | null = null;
|
|
43
46
|
private unauthenticatedStrategy: UnauthenticatedStrategy;
|
|
44
47
|
private cache: IdentityCache;
|
|
45
|
-
|
|
48
|
+
|
|
46
49
|
// Default TTL: 1 Hour
|
|
47
|
-
private readonly CACHE_TTL_SECONDS = 3600;
|
|
50
|
+
private readonly CACHE_TTL_SECONDS = 3600;
|
|
48
51
|
|
|
49
52
|
constructor(
|
|
50
|
-
policy: AccessPolicy,
|
|
53
|
+
policy: AccessPolicy,
|
|
51
54
|
strategy: UnauthenticatedStrategy = 'PASSIVE',
|
|
52
|
-
cacheImpl?: IdentityCache
|
|
55
|
+
cacheImpl?: IdentityCache
|
|
53
56
|
) {
|
|
54
57
|
this.policy = policy;
|
|
55
58
|
this.unauthenticatedStrategy = strategy;
|
|
@@ -66,58 +69,56 @@ export class AAMPPublisher {
|
|
|
66
69
|
|
|
67
70
|
/**
|
|
68
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)
|
|
69
75
|
*/
|
|
70
76
|
async evaluateVisitor(
|
|
71
|
-
reqHeaders: Record<string, string | undefined>,
|
|
77
|
+
reqHeaders: Record<string, string | undefined>,
|
|
72
78
|
rawPayload?: string
|
|
73
79
|
): Promise<EvaluationResult> {
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
console.log(`\n--- [AAMP LOG START] New Request ---`);
|
|
81
|
+
|
|
82
|
+
// --- STAGE 1: IDENTITY VERIFICATION ---
|
|
83
|
+
console.log(`[IDENTITY] 🔍 Checking Identity Headers...`);
|
|
84
|
+
|
|
76
85
|
const hasAamp = reqHeaders[HEADERS.PAYLOAD] && reqHeaders[HEADERS.SIGNATURE] && reqHeaders[HEADERS.PUBLIC_KEY];
|
|
77
86
|
|
|
78
87
|
if (hasAamp) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return await this.handleAgent(reqHeaders, rawPayload);
|
|
88
|
+
// It claims to be an Agent. Verify it STRICTLY.
|
|
89
|
+
return await this.handleAgentStrict(reqHeaders, rawPayload);
|
|
82
90
|
}
|
|
83
91
|
|
|
84
|
-
//
|
|
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.
|
|
85
97
|
if (this.unauthenticatedStrategy === 'STRICT') {
|
|
98
|
+
console.log(`[IDENTITY] ⛔ BLOCKING. Strategy is STRICT.`);
|
|
86
99
|
return {
|
|
87
100
|
allowed: false,
|
|
88
101
|
status: 401,
|
|
89
|
-
reason: "
|
|
102
|
+
reason: "IDENTITY_REQUIRED: Missing AAMP Headers.",
|
|
90
103
|
visitorType: 'UNIDENTIFIED_BOT'
|
|
91
104
|
};
|
|
92
105
|
}
|
|
93
106
|
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
allowed: true,
|
|
97
|
-
status: 200,
|
|
98
|
-
reason: "PASSIVE_MODE: Allowed without verification.",
|
|
99
|
-
visitorType: 'LIKELY_HUMAN'
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 3. HYBRID MODE: Heuristic Analysis (The "Lazy Bot" Filter)
|
|
107
|
+
console.log(`[IDENTITY] ⚠️ SKIPPED (Legacy Mode). Checking Browser Heuristics...`);
|
|
104
108
|
const isHuman = this.performBrowserHeuristics(reqHeaders);
|
|
105
|
-
|
|
106
109
|
if (isHuman) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
status: 200,
|
|
110
|
-
reason: "BROWSER_VERIFIED: Heuristics passed.",
|
|
111
|
-
visitorType: 'LIKELY_HUMAN'
|
|
112
|
-
};
|
|
113
|
-
} else {
|
|
114
|
-
return {
|
|
115
|
-
allowed: false,
|
|
116
|
-
status: 403,
|
|
117
|
-
reason: "BOT_DETECTED: Request lacks browser signatures and AAMP headers.",
|
|
118
|
-
visitorType: 'UNIDENTIFIED_BOT'
|
|
119
|
-
};
|
|
110
|
+
console.log(`[POLICY] 👤 ALLOWED. Browser Heuristics Passed.`);
|
|
111
|
+
return { allowed: true, status: 200, reason: "BROWSER_VERIFIED", visitorType: 'LIKELY_HUMAN' };
|
|
120
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
|
+
};
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
/**
|
|
@@ -128,11 +129,11 @@ export class AAMPPublisher {
|
|
|
128
129
|
*/
|
|
129
130
|
private performBrowserHeuristics(headers: Record<string, string | undefined>): boolean {
|
|
130
131
|
const userAgent = headers['user-agent'] || '';
|
|
131
|
-
|
|
132
|
+
|
|
132
133
|
// A. The "Obvious Bot" Blocklist (Fast Fail)
|
|
133
134
|
const botSignatures = ['python-requests', 'curl', 'wget', 'scrapy', 'bot', 'crawler', 'spider'];
|
|
134
135
|
if (botSignatures.some(sig => userAgent.toLowerCase().includes(sig))) {
|
|
135
|
-
|
|
136
|
+
return false;
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
// B. Trusted Infrastructure Signals (The Real World Solution)
|
|
@@ -142,7 +143,7 @@ export class AAMPPublisher {
|
|
|
142
143
|
|
|
143
144
|
// C. The "Browser Fingerprint" (Fallback for direct connections)
|
|
144
145
|
const hasAcceptLanguage = !!headers['accept-language'];
|
|
145
|
-
const hasSecFetchDest = !!headers['sec-fetch-dest'];
|
|
146
|
+
const hasSecFetchDest = !!headers['sec-fetch-dest'];
|
|
146
147
|
const hasUpgradeInsecure = !!headers['upgrade-insecure-requests'];
|
|
147
148
|
|
|
148
149
|
if (hasAcceptLanguage && (hasSecFetchDest || hasUpgradeInsecure)) {
|
|
@@ -153,17 +154,24 @@ export class AAMPPublisher {
|
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
/**
|
|
156
|
-
* Handle AAMP Protocol Logic
|
|
157
|
+
* Handle AAMP Protocol Logic (Strict Mode)
|
|
157
158
|
*/
|
|
158
|
-
private async
|
|
159
|
+
private async handleAgentStrict(reqHeaders: Record<string, string | undefined>, rawPayload?: string): Promise<EvaluationResult> {
|
|
160
|
+
let agentId = "UNKNOWN";
|
|
161
|
+
|
|
159
162
|
try {
|
|
163
|
+
// 1. Decode Headers
|
|
160
164
|
const payloadHeader = reqHeaders[HEADERS.PAYLOAD]!;
|
|
161
165
|
const sigHeader = reqHeaders[HEADERS.SIGNATURE]!;
|
|
162
166
|
const keyHeader = reqHeaders[HEADERS.PUBLIC_KEY]!;
|
|
163
167
|
|
|
164
|
-
const headerJson = atob(payloadHeader);
|
|
168
|
+
const headerJson = atob(payloadHeader);
|
|
165
169
|
const requestHeader = JSON.parse(headerJson);
|
|
166
|
-
|
|
170
|
+
agentId = requestHeader.agent_id;
|
|
171
|
+
|
|
172
|
+
console.log(`[IDENTITY] 🆔 Claimed ID: ${agentId}`);
|
|
173
|
+
|
|
174
|
+
// 2. Crypto & DNS Verification
|
|
167
175
|
const signedRequest: SignedAccessRequest = {
|
|
168
176
|
header: requestHeader,
|
|
169
177
|
signature: sigHeader,
|
|
@@ -178,63 +186,158 @@ export class AAMPPublisher {
|
|
|
178
186
|
["verify"]
|
|
179
187
|
);
|
|
180
188
|
|
|
181
|
-
// Verify Core Logic
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
if (!
|
|
185
|
-
console.log(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
status: 403,
|
|
189
|
-
reason: result.reason,
|
|
190
|
-
visitorType: 'VERIFIED_AGENT'
|
|
191
|
-
};
|
|
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' };
|
|
192
196
|
}
|
|
193
197
|
|
|
194
|
-
console.log(
|
|
198
|
+
console.log(`[IDENTITY] ✅ PASSED. DNS Binding Verified.`);
|
|
199
|
+
|
|
200
|
+
// --- STAGE 2: POLICY ENFORCEMENT ---
|
|
201
|
+
console.log(`[POLICY] 📜 Checking Permissions for ${agentId}...`);
|
|
202
|
+
|
|
203
|
+
const proofToken = reqHeaders[HEADERS.PROOF_TOKEN];
|
|
204
|
+
const paymentCredential = reqHeaders[HEADERS.PAYMENT_CREDENTIAL];
|
|
205
|
+
|
|
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;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// --- STAGE 3: ACCESS GRANT ---
|
|
215
|
+
console.log(`[POLICY] ✅ PASSED. Requirements Met.`);
|
|
216
|
+
console.log(`[ACCESS] 🔓 GRANTED. Unlocking HQ Content.`);
|
|
217
|
+
|
|
195
218
|
return {
|
|
196
219
|
allowed: true,
|
|
197
220
|
status: 200,
|
|
198
221
|
reason: "AAMP_VERIFIED",
|
|
199
222
|
visitorType: 'VERIFIED_AGENT',
|
|
200
|
-
metadata: requestHeader
|
|
223
|
+
metadata: requestHeader,
|
|
224
|
+
proofUsed: policyResult.proofUsed
|
|
201
225
|
};
|
|
202
226
|
|
|
203
227
|
} catch (e) {
|
|
204
|
-
console.error(e);
|
|
228
|
+
console.error(`[AAMP ERROR]`, e);
|
|
205
229
|
return { allowed: false, status: 400, reason: "INVALID_SIGNATURE", visitorType: 'UNIDENTIFIED_BOT' };
|
|
206
230
|
}
|
|
207
231
|
}
|
|
208
232
|
|
|
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
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* STAGE 2: POLICY ENFORCEMENT CHECK
|
|
240
|
+
*/
|
|
241
|
+
private async checkPolicyStrict(
|
|
242
|
+
requestHeader: any,
|
|
243
|
+
proofToken?: string,
|
|
244
|
+
paymentCredential?: string
|
|
245
|
+
): Promise<EvaluationResult> {
|
|
246
|
+
|
|
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' };
|
|
250
|
+
}
|
|
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' };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 2. Policy Check: Economics (v1.2) - Payment & Ads
|
|
256
|
+
if (this.policy.requiresPayment) {
|
|
257
|
+
let paymentSatisfied = false;
|
|
258
|
+
|
|
259
|
+
// Method A: Flexible Payment Callback (DB / Custom Logic)
|
|
260
|
+
if (this.policy.monetization?.checkPayment) {
|
|
261
|
+
const isPaid = await this.policy.monetization.checkPayment(requestHeader.agent_id, requestHeader.purpose);
|
|
262
|
+
if (isPaid) {
|
|
263
|
+
console.log(`[POLICY] 💰 Payment Verified via Callback.`);
|
|
264
|
+
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'WHITELIST_CALLBACK' };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Method B: Payment Credentials (Unified JWT)
|
|
269
|
+
if (!paymentSatisfied && this.policy.monetization?.paymentConfig && paymentCredential) {
|
|
270
|
+
const { jwksUrl, issuer } = this.policy.monetization.paymentConfig;
|
|
271
|
+
console.log(`[POLICY] 🔐 Verifying Payment Credential (Issuer: ${issuer})...`);
|
|
272
|
+
|
|
273
|
+
const isValidCredential = await verifyJwt(paymentCredential, jwksUrl, issuer);
|
|
274
|
+
if (isValidCredential) {
|
|
275
|
+
console.log(`[POLICY] ✅ Credential Signature VALID.`);
|
|
276
|
+
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'PAYMENT_CREDENTIAL_JWT' };
|
|
277
|
+
} else {
|
|
278
|
+
console.log(`[POLICY] ❌ Credential Signature INVALID.`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Method C: Ad-Supported (Proof Verification)
|
|
283
|
+
if (!paymentSatisfied && this.policy.allowAdSupportedAccess && requestHeader.context?.ads_displayed) {
|
|
284
|
+
if (proofToken && this.policy.monetization?.adNetwork) {
|
|
285
|
+
const { jwksUrl, issuer } = this.policy.monetization.adNetwork;
|
|
286
|
+
console.log(`[POLICY] 📺 Verifying Ad Proof (Issuer: ${issuer})...`);
|
|
287
|
+
|
|
288
|
+
const isValidProof = await verifyJwt(proofToken, jwksUrl, issuer);
|
|
289
|
+
if (isValidProof) {
|
|
290
|
+
console.log(`[POLICY] ✅ Ad Proof Signature VALID.`);
|
|
291
|
+
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'AD_PROOF_JWT' };
|
|
292
|
+
} else {
|
|
293
|
+
console.log(`[POLICY] ❌ Ad Proof Signature INVALID.`);
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
console.log(`[POLICY] ⚠️ Ad Proof MISSING.`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
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
|
+
|
|
209
313
|
private async verifyRequestLogic(
|
|
210
|
-
request: SignedAccessRequest,
|
|
314
|
+
request: SignedAccessRequest,
|
|
211
315
|
requestPublicKey: CryptoKey,
|
|
212
|
-
rawPayload?: string
|
|
213
316
|
): Promise<VerificationResult> {
|
|
214
|
-
|
|
317
|
+
|
|
215
318
|
// 1. Replay Attack Prevention
|
|
216
319
|
const requestTime = new Date(request.header.ts).getTime();
|
|
217
320
|
if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
|
|
218
|
-
return { allowed: false, reason: 'TIMESTAMP_INVALID', identityVerified: false };
|
|
321
|
+
return { allowed: false, reason: 'TIMESTAMP_INVALID: Clock skew too large.', identityVerified: false };
|
|
219
322
|
}
|
|
220
323
|
|
|
221
324
|
// 2. Crypto Verification
|
|
222
|
-
const signableString =
|
|
325
|
+
const signableString = JSON.stringify(request.header);
|
|
223
326
|
const isCryptoValid = await verifySignature(requestPublicKey, signableString, request.signature);
|
|
224
|
-
if (!isCryptoValid) return { allowed: false, reason: 'CRYPTO_FAIL', identityVerified: false };
|
|
327
|
+
if (!isCryptoValid) return { allowed: false, reason: 'CRYPTO_FAIL: Signature invalid.', identityVerified: false };
|
|
225
328
|
|
|
226
329
|
// 3. Identity Verification (DNS Binding) with Cache
|
|
227
330
|
let identityVerified = false;
|
|
228
331
|
const claimedDomain = request.header.agent_id;
|
|
229
332
|
const pubKeyString = await exportPublicKey(requestPublicKey);
|
|
230
|
-
|
|
231
|
-
console.log(`
|
|
333
|
+
|
|
334
|
+
console.log(`[IDENTITY] 🔍 Verifying DNS Binding for: ${claimedDomain}`);
|
|
232
335
|
|
|
233
336
|
// Check Cache First
|
|
234
337
|
const cachedKey = await this.cache.get(claimedDomain);
|
|
235
|
-
|
|
338
|
+
|
|
236
339
|
if (cachedKey === pubKeyString) {
|
|
237
|
-
console.log("
|
|
340
|
+
console.log("[IDENTITY] ⚡ Cache Hit. Identity Verified.");
|
|
238
341
|
identityVerified = true;
|
|
239
342
|
} else if (this.isDomain(claimedDomain)) {
|
|
240
343
|
// Cache Miss: Perform DNS Fetch
|
|
@@ -248,23 +351,8 @@ export class AAMPPublisher {
|
|
|
248
351
|
return { allowed: false, reason: 'IDENTITY_FAIL: DNS Binding could not be verified.', identityVerified: false };
|
|
249
352
|
}
|
|
250
353
|
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
return { allowed: false, reason: 'POLICY_DENIED: Training not allowed.', identityVerified };
|
|
254
|
-
}
|
|
255
|
-
if (request.header.purpose === AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
|
|
256
|
-
return { allowed: false, reason: 'POLICY_DENIED: RAG not allowed.', identityVerified };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// 5. Policy Check: Economics
|
|
260
|
-
if (this.policy.requiresPayment) {
|
|
261
|
-
const isAdExempt = this.policy.allowAdSupportedAccess && request.header.context.ads_displayed;
|
|
262
|
-
if (!isAdExempt) {
|
|
263
|
-
return { allowed: false, reason: 'PAYMENT_REQUIRED: Content requires payment or ads.', identityVerified };
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return { allowed: true, reason: 'OK', identityVerified };
|
|
354
|
+
// Return verified status so handleAgentStrict can proceed to Policy Check
|
|
355
|
+
return { allowed: true, reason: 'OK', identityVerified: true };
|
|
268
356
|
}
|
|
269
357
|
|
|
270
358
|
private async verifyDnsBinding(domain: string, requestKeySpki: string): Promise<boolean> {
|
|
@@ -272,16 +360,16 @@ export class AAMPPublisher {
|
|
|
272
360
|
// Allow HTTP for localhost testing
|
|
273
361
|
const protocol = (domain.includes('localhost') || domain.match(/:\d+$/)) ? 'http' : 'https';
|
|
274
362
|
const url = `${protocol}://${domain}${WELL_KNOWN_AGENT_PATH}`;
|
|
275
|
-
|
|
363
|
+
|
|
276
364
|
console.log(` 🌍 [AAMP DNS] Fetching Manifest: ${url} ...`);
|
|
277
365
|
|
|
278
366
|
// In production, we need a short timeout to prevent hanging
|
|
279
367
|
const controller = new AbortController();
|
|
280
368
|
const timeoutId = setTimeout(() => controller.abort(), 1500); // 1.5s max for DNS check
|
|
281
|
-
|
|
369
|
+
|
|
282
370
|
const response = await fetch(url, { signal: controller.signal });
|
|
283
371
|
clearTimeout(timeoutId);
|
|
284
|
-
|
|
372
|
+
|
|
285
373
|
if (!response.ok) {
|
|
286
374
|
console.log(` ❌ [AAMP DNS] Fetch Failed: ${response.status}`);
|
|
287
375
|
return false;
|
|
@@ -298,15 +386,15 @@ export class AAMPPublisher {
|
|
|
298
386
|
|
|
299
387
|
// CHECK 2: Does the key match?
|
|
300
388
|
if (manifest.public_key !== requestKeySpki) {
|
|
301
|
-
|
|
302
|
-
|
|
389
|
+
console.log(` ❌ [AAMP DNS] Key Mismatch: DNS Key != Request Key`);
|
|
390
|
+
return false;
|
|
303
391
|
}
|
|
304
392
|
|
|
305
393
|
console.log(` ✅ [AAMP DNS] Identity Confirmed.`);
|
|
306
394
|
return true;
|
|
307
|
-
} catch (e: any) {
|
|
308
|
-
|
|
309
|
-
|
|
395
|
+
} catch (e: any) {
|
|
396
|
+
console.log(` ❌ [AAMP DNS] Error: ${e.message}`);
|
|
397
|
+
return false;
|
|
310
398
|
}
|
|
311
399
|
}
|
|
312
400
|
|
|
@@ -324,4 +412,28 @@ export class AAMPPublisher {
|
|
|
324
412
|
[HEADERS.PROVENANCE_SIG]: signature
|
|
325
413
|
};
|
|
326
414
|
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Handling Quality Feedback (The "Dispute" Layer)
|
|
418
|
+
* This runs when an Agent sends 'x-aamp-feedback'.
|
|
419
|
+
*/
|
|
420
|
+
private async handleFeedback(token: string, headers: Record<string, string | undefined>) {
|
|
421
|
+
// NOTE: In production, you would fetch the Agent's specific key.
|
|
422
|
+
// For now, we assume standard Discovery or a centralized Key Set (like adNetwork).
|
|
423
|
+
// Ideally, the SDK config should have a 'qualityOracle' key set.
|
|
424
|
+
|
|
425
|
+
// 1. We just Decode it to Log it (Verification is optional but recommended)
|
|
426
|
+
try {
|
|
427
|
+
const parts = token.split('.');
|
|
428
|
+
const payload = JSON.parse(atob(parts[1]));
|
|
429
|
+
|
|
430
|
+
console.log(`\n📢 [AAMP QUALITY ALERT] Feedback Received from ${payload.agent_id}`);
|
|
431
|
+
console.log(` Reason: ${payload.reason} | Score: ${payload.quality_score}`);
|
|
432
|
+
console.log(` Resource: ${payload.url}`);
|
|
433
|
+
console.log(` (Signature available for dispute evidence)`);
|
|
434
|
+
|
|
435
|
+
} catch (e) {
|
|
436
|
+
console.log(` ⚠️ [AAMP Warning] Malformed Feedback Token.`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
327
439
|
}
|
package/src/types.ts
CHANGED
|
@@ -43,14 +43,35 @@ export interface IdentityCache {
|
|
|
43
43
|
set(key: string, value: string, ttlSeconds: number): Promise<void>;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Optional Monetization (The Settlement Layer)
|
|
48
|
+
*/
|
|
46
49
|
/**
|
|
47
50
|
* Optional Monetization (The Settlement Layer)
|
|
48
51
|
*/
|
|
49
52
|
export interface MonetizationConfig {
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
// Method 1: Payments (Flexible Callback)
|
|
54
|
+
// Developers implement their own logic (Database check, CMS lookup, etc.)
|
|
55
|
+
// Returns TRUE if the agent is a paid subscriber for this specific purpose.
|
|
56
|
+
checkPayment?: (agentId: string, purpose: string) => boolean | Promise<boolean>;
|
|
57
|
+
|
|
58
|
+
// Method 2: Ads (Proof Verification)
|
|
59
|
+
// Configuration to verify tokens from your Ad Provider (e.g. Google)
|
|
60
|
+
adNetwork?: {
|
|
61
|
+
jwksUrl: string; // e.g. "https://www.googleapis.com/oauth2/v3/certs"
|
|
62
|
+
issuer: string; // e.g. "https://accounts.google.com"
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Method 3: Payment Credentials (Unified JWT)
|
|
66
|
+
// Verifies "x-aamp-credential" for Broker or Direct payments.
|
|
67
|
+
paymentConfig?: {
|
|
68
|
+
jwksUrl: string; // e.g. "https://my-site.com/.well-known/jwks.json"
|
|
69
|
+
issuer: string; // e.g. "my-site.com"
|
|
70
|
+
};
|
|
52
71
|
}
|
|
53
72
|
|
|
73
|
+
|
|
74
|
+
|
|
54
75
|
/**
|
|
55
76
|
* Handling Non-AAMP Visitors
|
|
56
77
|
*
|
|
@@ -65,15 +86,15 @@ export interface AccessPolicy {
|
|
|
65
86
|
allowTraining: boolean;
|
|
66
87
|
allowRAG: boolean;
|
|
67
88
|
attributionRequired: boolean;
|
|
68
|
-
|
|
89
|
+
|
|
69
90
|
// Economic Signals
|
|
70
|
-
allowAdSupportedAccess: boolean;
|
|
71
|
-
requiresPayment: boolean;
|
|
91
|
+
allowAdSupportedAccess: boolean;
|
|
92
|
+
requiresPayment: boolean;
|
|
72
93
|
paymentPointer?: string;
|
|
73
94
|
|
|
74
95
|
// Identity Strictness
|
|
75
|
-
requireIdentityBinding?: boolean;
|
|
76
|
-
|
|
96
|
+
requireIdentityBinding?: boolean;
|
|
97
|
+
|
|
77
98
|
// V1.1: Optional Settlement Info
|
|
78
99
|
monetization?: MonetizationConfig;
|
|
79
100
|
}
|
|
@@ -81,7 +102,7 @@ export interface AccessPolicy {
|
|
|
81
102
|
export interface ProtocolHeader {
|
|
82
103
|
v: '1.1';
|
|
83
104
|
ts: string;
|
|
84
|
-
agent_id: string;
|
|
105
|
+
agent_id: string;
|
|
85
106
|
resource: string;
|
|
86
107
|
purpose: AccessPurpose;
|
|
87
108
|
context: {
|
|
@@ -98,16 +119,28 @@ export interface SignedAccessRequest {
|
|
|
98
119
|
export interface FeedbackSignal {
|
|
99
120
|
target_resource: string;
|
|
100
121
|
agent_id: string;
|
|
101
|
-
quality_score: number;
|
|
122
|
+
quality_score: number;
|
|
102
123
|
flags: QualityFlag[];
|
|
103
124
|
timestamp: string;
|
|
104
125
|
}
|
|
105
126
|
|
|
127
|
+
// Result of the full evaluation pipeline
|
|
106
128
|
// Result of the full evaluation pipeline
|
|
107
129
|
export interface EvaluationResult {
|
|
108
130
|
allowed: boolean;
|
|
109
|
-
status: 200 | 400 | 401 | 403;
|
|
131
|
+
status: 200 | 400 | 401 | 402 | 403;
|
|
110
132
|
reason: string;
|
|
111
133
|
visitorType: 'VERIFIED_AGENT' | 'LIKELY_HUMAN' | 'UNIDENTIFIED_BOT';
|
|
112
134
|
metadata?: any;
|
|
135
|
+
payment_status?: 'PAID_SUBSCRIBER' | 'AD_FUNDED' | 'UNPAID';
|
|
136
|
+
proofUsed?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Signed Quality Feedback (The "Report Card")
|
|
140
|
+
export interface FeedbackSignalToken {
|
|
141
|
+
url: string; // The resource being flagged
|
|
142
|
+
agent_id: string; // Who is flagging it (e.g. "bot.openai.com")
|
|
143
|
+
quality_score: number; // 0.0 to 1.0
|
|
144
|
+
reason: string; // e.g. "SEO_SPAM", "HATE_SPEECH"
|
|
145
|
+
timestamp: number;
|
|
113
146
|
}
|
package/test/handshake.spec.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Run using: npm test
|
|
4
4
|
* Location: sdk/typescript/test/handshake.spec.ts
|
|
5
5
|
*/
|
|
6
|
-
import { AAMPAgent } from '../src/agent';
|
|
7
|
-
import { AAMPPublisher } from '../src/publisher';
|
|
8
|
-
import { AccessPurpose } from '../src/types';
|
|
9
|
-
import { HEADERS } from '../src/constants';
|
|
6
|
+
import { AAMPAgent } from '../src/agent.js';
|
|
7
|
+
import { AAMPPublisher } from '../src/publisher.js';
|
|
8
|
+
import { AccessPurpose } from '../src/types.js';
|
|
9
|
+
import { HEADERS } from '../src/constants.js';
|
|
10
10
|
|
|
11
11
|
async function runTest() {
|
|
12
12
|
console.log("--- STARTING AAMP HANDSHAKE TEST ---");
|
|
@@ -29,7 +29,7 @@ async function runTest() {
|
|
|
29
29
|
// TEST CASE A: Requesting RAG without Ads (Should FAIL due to Payment Requirement)
|
|
30
30
|
console.log("\n[TEST A] Requesting RAG (No Ads)...");
|
|
31
31
|
const reqA = await agent.createAccessRequest('/doc/1', AccessPurpose.RAG_RETRIEVAL, { adsDisplayed: false });
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
const payloadA = JSON.stringify(reqA.header);
|
|
34
34
|
const headersA = {
|
|
35
35
|
[HEADERS.PAYLOAD]: btoa(payloadA),
|
|
@@ -45,7 +45,7 @@ async function runTest() {
|
|
|
45
45
|
// TEST CASE B: Requesting RAG WITH Ads (Should SUCCEED via Exemption)
|
|
46
46
|
console.log("\n[TEST B] Requesting RAG (With Ads)...");
|
|
47
47
|
const reqB = await agent.createAccessRequest('/doc/1', AccessPurpose.RAG_RETRIEVAL, { adsDisplayed: true });
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
const payloadB = JSON.stringify(reqB.header);
|
|
50
50
|
const headersB = {
|
|
51
51
|
[HEADERS.PAYLOAD]: btoa(payloadB),
|
package/tsconfig.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2020",
|
|
4
|
-
"module": "
|
|
5
|
-
"
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": [
|
|
7
|
+
"ES2020",
|
|
8
|
+
"DOM"
|
|
9
|
+
],
|
|
6
10
|
"declaration": true,
|
|
7
11
|
"outDir": "./dist",
|
|
8
12
|
"rootDir": "./src",
|
|
@@ -11,5 +15,7 @@
|
|
|
11
15
|
"skipLibCheck": true,
|
|
12
16
|
"forceConsistentCasingInFileNames": true
|
|
13
17
|
},
|
|
14
|
-
"include": [
|
|
18
|
+
"include": [
|
|
19
|
+
"src/**/*"
|
|
20
|
+
]
|
|
15
21
|
}
|