@aamp/protocol 1.1.6 ā 1.1.8
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/package.json +7 -1
- package/src/publisher.ts +147 -125
- package/src/types.ts +1 -1
- package/aamp-protocol-1.1.5.tgz +0 -0
- package/dist/agent.d.ts +0 -30
- package/dist/agent.js +0 -65
- package/dist/constants.d.ts +0 -25
- package/dist/constants.js +0 -38
- package/dist/crypto.d.ts +0 -9
- package/dist/crypto.js +0 -44
- package/dist/express.d.ts +0 -22
- package/dist/express.js +0 -64
- package/dist/index.d.ts +0 -12
- package/dist/index.js +0 -12
- package/dist/nextjs.d.ts +0 -25
- package/dist/nextjs.js +0 -60
- package/dist/proof.d.ts +0 -9
- package/dist/proof.js +0 -27
- package/dist/publisher.d.ts +0 -35
- package/dist/publisher.js +0 -338
- package/dist/types.d.ts +0 -113
- package/dist/types.js +0 -25
package/dist/express.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layer 3: Framework Adapters
|
|
3
|
-
* Zero-friction integration for Express/Node.js.
|
|
4
|
-
*/
|
|
5
|
-
import { AAMPPublisher } from './publisher.js';
|
|
6
|
-
import { ContentOrigin } from './types.js';
|
|
7
|
-
import { generateKeyPair } from './crypto.js';
|
|
8
|
-
export class AAMP {
|
|
9
|
-
constructor(config) {
|
|
10
|
-
this.publisher = new AAMPPublisher({ version: '1.1', ...config.policy }, config.strategy || 'PASSIVE', config.cache);
|
|
11
|
-
this.origin = ContentOrigin[config.meta.origin];
|
|
12
|
-
this.ready = generateKeyPair().then(keys => {
|
|
13
|
-
return this.publisher.initialize(keys);
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
static init(config) {
|
|
17
|
-
return new AAMP(config);
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Express Middleware
|
|
21
|
-
*/
|
|
22
|
-
middleware() {
|
|
23
|
-
return async (req, res, next) => {
|
|
24
|
-
await this.ready;
|
|
25
|
-
// Normalize headers to lowercase dictionary
|
|
26
|
-
const headers = {};
|
|
27
|
-
Object.keys(req.headers).forEach(key => {
|
|
28
|
-
headers[key.toLowerCase()] = req.headers[key];
|
|
29
|
-
});
|
|
30
|
-
// Retrieve Raw Payload if available (optional but good for crypto)
|
|
31
|
-
// Note: Express body parsing might interfere, so we usually rely on the header content.
|
|
32
|
-
const rawPayload = headers['x-aamp-payload'];
|
|
33
|
-
// Evaluate Visitor
|
|
34
|
-
const result = await this.publisher.evaluateVisitor(headers, rawPayload);
|
|
35
|
-
// Enforce Decision
|
|
36
|
-
if (!result.allowed) {
|
|
37
|
-
res.status(result.status).json({
|
|
38
|
-
error: result.reason,
|
|
39
|
-
visitor_type: result.visitorType,
|
|
40
|
-
proof_used: result.proofUsed
|
|
41
|
-
});
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
// Inject Provenance Headers (For the humans/agents that got through)
|
|
45
|
-
const respHeaders = await this.publisher.generateResponseHeaders(this.origin);
|
|
46
|
-
Object.entries(respHeaders).forEach(([k, v]) => {
|
|
47
|
-
res.setHeader(k, v);
|
|
48
|
-
});
|
|
49
|
-
// Attach metadata to request for downstream use
|
|
50
|
-
req.aamp = {
|
|
51
|
-
verified: result.visitorType === 'VERIFIED_AGENT',
|
|
52
|
-
type: result.visitorType,
|
|
53
|
-
...result.metadata
|
|
54
|
-
};
|
|
55
|
-
next();
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
discoveryHandler() {
|
|
59
|
-
return (req, res) => {
|
|
60
|
-
res.setHeader('Content-Type', 'application/json');
|
|
61
|
-
res.send(JSON.stringify(this.publisher.getPolicy(), null, 2));
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
}
|
package/dist/index.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AAMP SDK Public API
|
|
3
|
-
*
|
|
4
|
-
* This is the main entry point for the library.
|
|
5
|
-
*/
|
|
6
|
-
export * from './types.js';
|
|
7
|
-
export * from './constants.js';
|
|
8
|
-
export * from './agent.js';
|
|
9
|
-
export * from './publisher.js';
|
|
10
|
-
export * from './crypto.js';
|
|
11
|
-
export * from './express.js';
|
|
12
|
-
export { AAMPNext } from './nextjs.js';
|
package/dist/index.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AAMP SDK Public API
|
|
3
|
-
*
|
|
4
|
-
* This is the main entry point for the library.
|
|
5
|
-
*/
|
|
6
|
-
export * from './types.js';
|
|
7
|
-
export * from './constants.js';
|
|
8
|
-
export * from './agent.js';
|
|
9
|
-
export * from './publisher.js';
|
|
10
|
-
export * from './crypto.js';
|
|
11
|
-
export * from './express.js'; // Node.js / Express Adapter
|
|
12
|
-
export { AAMPNext } from './nextjs.js'; // Serverless / Next.js Adapter
|
package/dist/nextjs.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { AccessPolicy, ContentOrigin, UnauthenticatedStrategy, IdentityCache } from './types.js';
|
|
2
|
-
type NextRequest = any;
|
|
3
|
-
type NextResponse = any;
|
|
4
|
-
export interface AAMPConfig {
|
|
5
|
-
policy: Omit<AccessPolicy, 'version'>;
|
|
6
|
-
meta: {
|
|
7
|
-
origin: keyof typeof ContentOrigin;
|
|
8
|
-
paymentPointer?: string;
|
|
9
|
-
};
|
|
10
|
-
strategy?: UnauthenticatedStrategy;
|
|
11
|
-
cache?: IdentityCache;
|
|
12
|
-
}
|
|
13
|
-
export declare class AAMPNext {
|
|
14
|
-
private publisher;
|
|
15
|
-
private origin;
|
|
16
|
-
private ready;
|
|
17
|
-
private constructor();
|
|
18
|
-
static init(config: AAMPConfig): AAMPNext;
|
|
19
|
-
/**
|
|
20
|
-
* Serverless Route Wrapper
|
|
21
|
-
*/
|
|
22
|
-
withProtection(handler: (req: NextRequest) => Promise<NextResponse>): (req: NextRequest) => Promise<any>;
|
|
23
|
-
discoveryHandler(): () => Promise<Response>;
|
|
24
|
-
}
|
|
25
|
-
export {};
|
package/dist/nextjs.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layer 3: Framework Adapters
|
|
3
|
-
* Serverless integration for Next.js (App Router & API Routes).
|
|
4
|
-
*/
|
|
5
|
-
import { AAMPPublisher } from './publisher.js';
|
|
6
|
-
import { ContentOrigin } from './types.js';
|
|
7
|
-
import { generateKeyPair } from './crypto.js';
|
|
8
|
-
const createJsonResponse = (body, status = 200) => {
|
|
9
|
-
return new Response(JSON.stringify(body), {
|
|
10
|
-
status,
|
|
11
|
-
headers: { 'Content-Type': 'application/json' }
|
|
12
|
-
});
|
|
13
|
-
};
|
|
14
|
-
export class AAMPNext {
|
|
15
|
-
constructor(config) {
|
|
16
|
-
this.publisher = new AAMPPublisher({ version: '1.1', ...config.policy }, config.strategy || 'PASSIVE', config.cache);
|
|
17
|
-
this.origin = ContentOrigin[config.meta.origin];
|
|
18
|
-
this.ready = generateKeyPair().then(keys => this.publisher.initialize(keys));
|
|
19
|
-
}
|
|
20
|
-
static init(config) {
|
|
21
|
-
return new AAMPNext(config);
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Serverless Route Wrapper
|
|
25
|
-
*/
|
|
26
|
-
withProtection(handler) {
|
|
27
|
-
return async (req) => {
|
|
28
|
-
await this.ready;
|
|
29
|
-
// Extract Headers map
|
|
30
|
-
const headers = {};
|
|
31
|
-
req.headers.forEach((value, key) => {
|
|
32
|
-
headers[key.toLowerCase()] = value;
|
|
33
|
-
});
|
|
34
|
-
// Evaluate
|
|
35
|
-
const result = await this.publisher.evaluateVisitor(headers, headers['x-aamp-payload']);
|
|
36
|
-
if (!result.allowed) {
|
|
37
|
-
return createJsonResponse({
|
|
38
|
-
error: result.reason,
|
|
39
|
-
visitor_type: result.visitorType,
|
|
40
|
-
proof_used: result.proofUsed
|
|
41
|
-
}, result.status);
|
|
42
|
-
}
|
|
43
|
-
// Execute Handler
|
|
44
|
-
const response = await handler(req);
|
|
45
|
-
// Inject Provenance
|
|
46
|
-
const aampHeaders = await this.publisher.generateResponseHeaders(this.origin);
|
|
47
|
-
if (response && response.headers) {
|
|
48
|
-
Object.entries(aampHeaders).forEach(([k, v]) => {
|
|
49
|
-
response.headers.set(k, v);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
return response;
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
discoveryHandler() {
|
|
56
|
-
return async () => {
|
|
57
|
-
return createJsonResponse(this.publisher.getPolicy());
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
}
|
package/dist/proof.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Verifies a JWT (Proof Token or Payment Credential) using JWKS.
|
|
3
|
-
*
|
|
4
|
-
* @param token The JWT string
|
|
5
|
-
* @param jwksUrl The URL to fetch Public Keys
|
|
6
|
-
* @param issuer The expected issuer
|
|
7
|
-
* @param audience The expected audience range
|
|
8
|
-
*/
|
|
9
|
-
export declare function verifyJwt(token: string, jwksUrl: string, issuer: string, audience?: string): Promise<boolean>;
|
package/dist/proof.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
2
|
-
// In-memory cache for JWKS to avoid repeated fetches
|
|
3
|
-
// Jose's createRemoteJWKSet handles caching/cooldowns internally.
|
|
4
|
-
/**
|
|
5
|
-
* Verifies a JWT (Proof Token or Payment Credential) using JWKS.
|
|
6
|
-
*
|
|
7
|
-
* @param token The JWT string
|
|
8
|
-
* @param jwksUrl The URL to fetch Public Keys
|
|
9
|
-
* @param issuer The expected issuer
|
|
10
|
-
* @param audience The expected audience range
|
|
11
|
-
*/
|
|
12
|
-
export async function verifyJwt(token, jwksUrl, issuer, audience) {
|
|
13
|
-
try {
|
|
14
|
-
const JWKS = createRemoteJWKSet(new URL(jwksUrl));
|
|
15
|
-
const { payload } = await jwtVerify(token, JWKS, {
|
|
16
|
-
issuer: issuer,
|
|
17
|
-
audience: audience // specific audience check if provided
|
|
18
|
-
});
|
|
19
|
-
// Check specific AAMP claims if we standardize them
|
|
20
|
-
// if (payload.type !== 'AD_IMPRESSION') return false;
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
// console.error("Ad Proof Verification Failed:", error);
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
}
|
package/dist/publisher.d.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { AccessPolicy, ContentOrigin, EvaluationResult, IdentityCache, UnauthenticatedStrategy } from './types.js';
|
|
2
|
-
export declare class AAMPPublisher {
|
|
3
|
-
private policy;
|
|
4
|
-
private keyPair;
|
|
5
|
-
private unauthenticatedStrategy;
|
|
6
|
-
private cache;
|
|
7
|
-
private readonly CACHE_TTL_SECONDS;
|
|
8
|
-
constructor(policy: AccessPolicy, strategy?: UnauthenticatedStrategy, cacheImpl?: IdentityCache);
|
|
9
|
-
initialize(keyPair: CryptoKeyPair): Promise<void>;
|
|
10
|
-
getPolicy(): AccessPolicy;
|
|
11
|
-
/**
|
|
12
|
-
* Main Entry Point: Evaluate ANY visitor (Human, Bot, or Agent)
|
|
13
|
-
*/
|
|
14
|
-
evaluateVisitor(reqHeaders: Record<string, string | undefined>, rawPayload?: string): Promise<EvaluationResult>;
|
|
15
|
-
/**
|
|
16
|
-
* Browser Heuristics (Hardened)
|
|
17
|
-
* 1. Checks Known Bot Signatures (Fast Fail)
|
|
18
|
-
* 2. Checks Trusted Upstream Signals (Cloudflare/Vercel)
|
|
19
|
-
* 3. Checks Browser Header Consistency
|
|
20
|
-
*/
|
|
21
|
-
private performBrowserHeuristics;
|
|
22
|
-
/**
|
|
23
|
-
* Handle AAMP Protocol Logic
|
|
24
|
-
*/
|
|
25
|
-
private handleAgent;
|
|
26
|
-
private verifyRequestLogic;
|
|
27
|
-
private verifyDnsBinding;
|
|
28
|
-
private isDomain;
|
|
29
|
-
generateResponseHeaders(origin: ContentOrigin): Promise<Record<string, string>>;
|
|
30
|
-
/**
|
|
31
|
-
* Handling Quality Feedback (The "Dispute" Layer)
|
|
32
|
-
* This runs when an Agent sends 'x-aamp-feedback'.
|
|
33
|
-
*/
|
|
34
|
-
private handleFeedback;
|
|
35
|
-
}
|
package/dist/publisher.js
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layer 2: Publisher Middleware
|
|
3
|
-
* Used by content owners to enforce policy, log access, and filter bots.
|
|
4
|
-
*/
|
|
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';
|
|
9
|
-
/**
|
|
10
|
-
* Default In-Memory Cache (Fallback only)
|
|
11
|
-
* NOT recommended for high-traffic Serverless production.
|
|
12
|
-
*/
|
|
13
|
-
class MemoryCache {
|
|
14
|
-
constructor() {
|
|
15
|
-
this.store = new Map();
|
|
16
|
-
}
|
|
17
|
-
async get(key) {
|
|
18
|
-
const item = this.store.get(key);
|
|
19
|
-
if (!item)
|
|
20
|
-
return null;
|
|
21
|
-
if (Date.now() > item.exp) {
|
|
22
|
-
this.store.delete(key);
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
return item.val;
|
|
26
|
-
}
|
|
27
|
-
async set(key, value, ttlSeconds) {
|
|
28
|
-
this.store.set(key, {
|
|
29
|
-
val: value,
|
|
30
|
-
exp: Date.now() + (ttlSeconds * 1000)
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
export class AAMPPublisher {
|
|
35
|
-
constructor(policy, strategy = 'PASSIVE', cacheImpl) {
|
|
36
|
-
this.keyPair = null;
|
|
37
|
-
// Default TTL: 1 Hour
|
|
38
|
-
this.CACHE_TTL_SECONDS = 3600;
|
|
39
|
-
this.policy = policy;
|
|
40
|
-
this.unauthenticatedStrategy = strategy;
|
|
41
|
-
this.cache = cacheImpl || new MemoryCache();
|
|
42
|
-
}
|
|
43
|
-
async initialize(keyPair) {
|
|
44
|
-
this.keyPair = keyPair;
|
|
45
|
-
}
|
|
46
|
-
getPolicy() {
|
|
47
|
-
return this.policy;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Main Entry Point: Evaluate ANY visitor (Human, Bot, or Agent)
|
|
51
|
-
*/
|
|
52
|
-
async evaluateVisitor(reqHeaders, rawPayload) {
|
|
53
|
-
// 1. Check for AAMP Headers
|
|
54
|
-
const hasAamp = reqHeaders[HEADERS.PAYLOAD] && reqHeaders[HEADERS.SIGNATURE] && reqHeaders[HEADERS.PUBLIC_KEY];
|
|
55
|
-
const feedbackToken = reqHeaders[HEADERS.FEEDBACK];
|
|
56
|
-
if (feedbackToken) {
|
|
57
|
-
await this.handleFeedback(feedbackToken, reqHeaders);
|
|
58
|
-
}
|
|
59
|
-
if (hasAamp) {
|
|
60
|
-
console.log("\nš [AAMP Middleware] Detected Agent Headers. Starting Verification...");
|
|
61
|
-
// It claims to be an Agent. Verify it.
|
|
62
|
-
return await this.handleAgent(reqHeaders, rawPayload);
|
|
63
|
-
}
|
|
64
|
-
// 2. It's not an AAMP Agent. Apply Strategy.
|
|
65
|
-
if (this.unauthenticatedStrategy === 'STRICT') {
|
|
66
|
-
return {
|
|
67
|
-
allowed: false,
|
|
68
|
-
status: 401,
|
|
69
|
-
reason: "STRICT_MODE: Only AAMP verified agents allowed.",
|
|
70
|
-
visitorType: 'UNIDENTIFIED_BOT'
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
if (this.unauthenticatedStrategy === 'PASSIVE') {
|
|
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)
|
|
82
|
-
const isHuman = this.performBrowserHeuristics(reqHeaders);
|
|
83
|
-
if (isHuman) {
|
|
84
|
-
return {
|
|
85
|
-
allowed: true,
|
|
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
|
-
};
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Browser Heuristics (Hardened)
|
|
102
|
-
* 1. Checks Known Bot Signatures (Fast Fail)
|
|
103
|
-
* 2. Checks Trusted Upstream Signals (Cloudflare/Vercel)
|
|
104
|
-
* 3. Checks Browser Header Consistency
|
|
105
|
-
*/
|
|
106
|
-
performBrowserHeuristics(headers) {
|
|
107
|
-
const userAgent = headers['user-agent'] || '';
|
|
108
|
-
// A. The "Obvious Bot" Blocklist (Fast Fail)
|
|
109
|
-
const botSignatures = ['python-requests', 'curl', 'wget', 'scrapy', 'bot', 'crawler', 'spider'];
|
|
110
|
-
if (botSignatures.some(sig => userAgent.toLowerCase().includes(sig))) {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
// B. Trusted Infrastructure Signals (The Real World Solution)
|
|
114
|
-
if (headers['cf-visitor'] || headers['cf-ray'])
|
|
115
|
-
return true;
|
|
116
|
-
if (headers['x-vercel-id'])
|
|
117
|
-
return true;
|
|
118
|
-
if (headers['cloudfront-viewer-address'])
|
|
119
|
-
return true;
|
|
120
|
-
// C. The "Browser Fingerprint" (Fallback for direct connections)
|
|
121
|
-
const hasAcceptLanguage = !!headers['accept-language'];
|
|
122
|
-
const hasSecFetchDest = !!headers['sec-fetch-dest'];
|
|
123
|
-
const hasUpgradeInsecure = !!headers['upgrade-insecure-requests'];
|
|
124
|
-
if (hasAcceptLanguage && (hasSecFetchDest || hasUpgradeInsecure)) {
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Handle AAMP Protocol Logic
|
|
131
|
-
*/
|
|
132
|
-
async handleAgent(reqHeaders, rawPayload) {
|
|
133
|
-
try {
|
|
134
|
-
const payloadHeader = reqHeaders[HEADERS.PAYLOAD];
|
|
135
|
-
const sigHeader = reqHeaders[HEADERS.SIGNATURE];
|
|
136
|
-
const keyHeader = reqHeaders[HEADERS.PUBLIC_KEY];
|
|
137
|
-
const headerJson = atob(payloadHeader);
|
|
138
|
-
const requestHeader = JSON.parse(headerJson);
|
|
139
|
-
const signedRequest = {
|
|
140
|
-
header: requestHeader,
|
|
141
|
-
signature: sigHeader,
|
|
142
|
-
publicKey: keyHeader
|
|
143
|
-
};
|
|
144
|
-
const agentKey = await crypto.subtle.importKey("spki", new Uint8Array(atob(keyHeader).split('').map(c => c.charCodeAt(0))), { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]);
|
|
145
|
-
const proofToken = reqHeaders[HEADERS.PROOF_TOKEN];
|
|
146
|
-
const paymentCredential = reqHeaders[HEADERS.PAYMENT_CREDENTIAL];
|
|
147
|
-
// Verify Core Logic
|
|
148
|
-
const result = await this.verifyRequestLogic(signedRequest, agentKey, proofToken, paymentCredential, headerJson);
|
|
149
|
-
if (!result.allowed) {
|
|
150
|
-
console.log(`ā [AAMP BLOCK]
|
|
151
|
-
Agent: ${requestHeader.agent_id}
|
|
152
|
-
Reason: ${result.reason}
|
|
153
|
-
VisitorType: ${result.visitorType}
|
|
154
|
-
Proof: ${result.proofUsed || 'None'}
|
|
155
|
-
Identity Verified: ${result.identityVerified}`);
|
|
156
|
-
return {
|
|
157
|
-
allowed: false,
|
|
158
|
-
status: 403,
|
|
159
|
-
reason: result.reason,
|
|
160
|
-
visitorType: 'VERIFIED_AGENT'
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
console.log(`ā
[AAMP ALLOW]
|
|
164
|
-
Agent: ${requestHeader.agent_id}
|
|
165
|
-
Reason: AAMP_VERIFIED
|
|
166
|
-
Payment Method: ${result.proofUsed}
|
|
167
|
-
Identity Verified: ${result.identityVerified}`);
|
|
168
|
-
return {
|
|
169
|
-
allowed: true,
|
|
170
|
-
status: 200,
|
|
171
|
-
reason: "AAMP_VERIFIED",
|
|
172
|
-
visitorType: 'VERIFIED_AGENT',
|
|
173
|
-
metadata: requestHeader
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
catch (e) {
|
|
177
|
-
console.error(e);
|
|
178
|
-
return { allowed: false, status: 400, reason: "INVALID_SIGNATURE", visitorType: 'UNIDENTIFIED_BOT' };
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
async verifyRequestLogic(request, requestPublicKey, proofToken, paymentCredential, rawPayload) {
|
|
182
|
-
// 1. Replay Attack Prevention
|
|
183
|
-
const requestTime = new Date(request.header.ts).getTime();
|
|
184
|
-
if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
|
|
185
|
-
return { allowed: false, reason: 'TIMESTAMP_INVALID', identityVerified: false };
|
|
186
|
-
}
|
|
187
|
-
// 2. Crypto Verification
|
|
188
|
-
const signableString = rawPayload || JSON.stringify(request.header);
|
|
189
|
-
const isCryptoValid = await verifySignature(requestPublicKey, signableString, request.signature);
|
|
190
|
-
if (!isCryptoValid)
|
|
191
|
-
return { allowed: false, reason: 'CRYPTO_FAIL', identityVerified: false };
|
|
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 };
|
|
216
|
-
}
|
|
217
|
-
if (request.header.purpose === AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
|
|
218
|
-
return { allowed: false, reason: 'POLICY_DENIED: RAG not allowed.', identityVerified };
|
|
219
|
-
}
|
|
220
|
-
// 5. Policy Check: Economics (v1.2)
|
|
221
|
-
if (this.policy.requiresPayment) {
|
|
222
|
-
let paymentSatisfied = false;
|
|
223
|
-
// Method A: Flexible Payment Callback (DB / Custom Logic)
|
|
224
|
-
if (this.policy.monetization?.checkPayment) {
|
|
225
|
-
const isPaid = await this.policy.monetization.checkPayment(request.header.agent_id, request.header.purpose);
|
|
226
|
-
if (isPaid) {
|
|
227
|
-
console.log(` š° [AAMP Audit] Whitelist Check Passed via Callback.`);
|
|
228
|
-
return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'WHITELIST_CALLBACK' };
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
// Method B: Payment Credentials (Unified JWT)
|
|
232
|
-
if (!paymentSatisfied && this.policy.monetization?.paymentConfig && paymentCredential) {
|
|
233
|
-
const { jwksUrl, issuer } = this.policy.monetization.paymentConfig;
|
|
234
|
-
console.log(` š [AAMP Audit] Verifying Payment Credential (Issuer: ${issuer})...`);
|
|
235
|
-
const isValidCredential = await verifyJwt(paymentCredential, jwksUrl, issuer);
|
|
236
|
-
if (isValidCredential) {
|
|
237
|
-
console.log(` ā
[AAMP Audit] Credential Signature VALID.`);
|
|
238
|
-
return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'PAYMENT_CREDENTIAL_JWT' };
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
241
|
-
console.log(` ā [AAMP Audit] Credential Signature INVALID.`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
// Method C: Ad-Supported (Proof Verification)
|
|
245
|
-
if (!paymentSatisfied && this.policy.allowAdSupportedAccess && request.header.context.ads_displayed) {
|
|
246
|
-
if (proofToken && this.policy.monetization?.adNetwork) {
|
|
247
|
-
const { jwksUrl, issuer } = this.policy.monetization.adNetwork;
|
|
248
|
-
console.log(` šŗ [AAMP Audit] Verifying Ad Proof (Issuer: ${issuer})...`);
|
|
249
|
-
const isValidProof = await verifyJwt(proofToken, jwksUrl, issuer);
|
|
250
|
-
if (isValidProof) {
|
|
251
|
-
console.log(` ā
[AAMP Audit] Ad Proof Signature VALID.`);
|
|
252
|
-
return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'AD_PROOF_JWT' };
|
|
253
|
-
}
|
|
254
|
-
else {
|
|
255
|
-
console.log(` ā [AAMP Audit] Ad Proof Signature INVALID.`);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
else {
|
|
259
|
-
console.log(` ā ļø [AAMP Audit] Ad Proof MISSING.`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (!paymentSatisfied) {
|
|
263
|
-
return { allowed: false, reason: 'PAYMENT_REQUIRED: Whitelist, Credential, and Ad Proof checks ALL failed.', identityVerified, proofUsed: 'NONE', visitorType: 'VERIFIED_AGENT' };
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return { allowed: true, reason: 'OK', identityVerified };
|
|
267
|
-
}
|
|
268
|
-
async verifyDnsBinding(domain, requestKeySpki) {
|
|
269
|
-
try {
|
|
270
|
-
// Allow HTTP for localhost testing
|
|
271
|
-
const protocol = (domain.includes('localhost') || domain.match(/:\d+$/)) ? 'http' : 'https';
|
|
272
|
-
const url = `${protocol}://${domain}${WELL_KNOWN_AGENT_PATH}`;
|
|
273
|
-
console.log(` š [AAMP DNS] Fetching Manifest: ${url} ...`);
|
|
274
|
-
// In production, we need a short timeout to prevent hanging
|
|
275
|
-
const controller = new AbortController();
|
|
276
|
-
const timeoutId = setTimeout(() => controller.abort(), 1500); // 1.5s max for DNS check
|
|
277
|
-
const response = await fetch(url, { signal: controller.signal });
|
|
278
|
-
clearTimeout(timeoutId);
|
|
279
|
-
if (!response.ok) {
|
|
280
|
-
console.log(` ā [AAMP DNS] Fetch Failed: ${response.status}`);
|
|
281
|
-
return false;
|
|
282
|
-
}
|
|
283
|
-
const manifest = await response.json();
|
|
284
|
-
console.log(` š [AAMP DNS] Manifest received. Agent ID: ${manifest.agent_id}`);
|
|
285
|
-
// CHECK 1: Does the manifest actually belong to the domain?
|
|
286
|
-
if (manifest.agent_id !== domain) {
|
|
287
|
-
console.log(` ā [AAMP DNS] Mismatch: Manifest ID ${manifest.agent_id} != Claimed ${domain}`);
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
// CHECK 2: Does the key match?
|
|
291
|
-
if (manifest.public_key !== requestKeySpki) {
|
|
292
|
-
console.log(` ā [AAMP DNS] Key Mismatch: DNS Key != Request Key`);
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
console.log(` ā
[AAMP DNS] Identity Confirmed.`);
|
|
296
|
-
return true;
|
|
297
|
-
}
|
|
298
|
-
catch (e) {
|
|
299
|
-
console.log(` ā [AAMP DNS] Error: ${e.message}`);
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
isDomain(s) {
|
|
304
|
-
// Basic regex, allows localhost with ports
|
|
305
|
-
return /^[a-zA-Z0-9.-]+(:\d+)?$/.test(s) || /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(s);
|
|
306
|
-
}
|
|
307
|
-
async generateResponseHeaders(origin) {
|
|
308
|
-
if (!this.keyPair)
|
|
309
|
-
throw new Error("Publisher keys not initialized");
|
|
310
|
-
const payload = JSON.stringify({ origin, ts: Date.now() });
|
|
311
|
-
const signature = await signData(this.keyPair.privateKey, payload);
|
|
312
|
-
return {
|
|
313
|
-
[HEADERS.CONTENT_ORIGIN]: origin,
|
|
314
|
-
[HEADERS.PROVENANCE_SIG]: signature
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Handling Quality Feedback (The "Dispute" Layer)
|
|
319
|
-
* This runs when an Agent sends 'x-aamp-feedback'.
|
|
320
|
-
*/
|
|
321
|
-
async handleFeedback(token, headers) {
|
|
322
|
-
// NOTE: In production, you would fetch the Agent's specific key.
|
|
323
|
-
// For now, we assume standard Discovery or a centralized Key Set (like adNetwork).
|
|
324
|
-
// Ideally, the SDK config should have a 'qualityOracle' key set.
|
|
325
|
-
// 1. We just Decode it to Log it (Verification is optional but recommended)
|
|
326
|
-
try {
|
|
327
|
-
const parts = token.split('.');
|
|
328
|
-
const payload = JSON.parse(atob(parts[1]));
|
|
329
|
-
console.log(`\nš¢ [AAMP QUALITY ALERT] Feedback Received from ${payload.agent_id}`);
|
|
330
|
-
console.log(` Reason: ${payload.reason} | Score: ${payload.quality_score}`);
|
|
331
|
-
console.log(` Resource: ${payload.url}`);
|
|
332
|
-
console.log(` (Signature available for dispute evidence)`);
|
|
333
|
-
}
|
|
334
|
-
catch (e) {
|
|
335
|
-
console.log(` ā ļø [AAMP Warning] Malformed Feedback Token.`);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|