@aamp/protocol 1.1.5 ā 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/aamp-protocol-1.1.5.tgz +0 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +9 -13
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +12 -9
- package/dist/crypto.js +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 +6 -1
- package/dist/publisher.js +101 -29
- package/dist/types.d.ts +21 -2
- package/dist/types.js +6 -9
- package/package.json +10 -5
- package/src/agent.ts +8 -8
- package/src/constants.ts +13 -4
- package/src/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 +130 -40
- package/src/types.ts +42 -9
- package/test/handshake.spec.ts +6 -6
- package/tsconfig.json +9 -3
package/dist/types.js
CHANGED
|
@@ -1,28 +1,25 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Layer 1: Protocol Definitions
|
|
4
3
|
* Shared types used by both Agent and Publisher.
|
|
5
4
|
*/
|
|
6
|
-
|
|
7
|
-
exports.QualityFlag = exports.ContentOrigin = exports.AccessPurpose = void 0;
|
|
8
|
-
var AccessPurpose;
|
|
5
|
+
export var AccessPurpose;
|
|
9
6
|
(function (AccessPurpose) {
|
|
10
7
|
AccessPurpose["CRAWL_TRAINING"] = "CRAWL_TRAINING";
|
|
11
8
|
AccessPurpose["RAG_RETRIEVAL"] = "RAG_RETRIEVAL";
|
|
12
9
|
AccessPurpose["SUMMARY"] = "SUMMARY";
|
|
13
10
|
AccessPurpose["QUOTATION"] = "QUOTATION";
|
|
14
11
|
AccessPurpose["EMBEDDING"] = "EMBEDDING";
|
|
15
|
-
})(AccessPurpose || (
|
|
16
|
-
var ContentOrigin;
|
|
12
|
+
})(AccessPurpose || (AccessPurpose = {}));
|
|
13
|
+
export var ContentOrigin;
|
|
17
14
|
(function (ContentOrigin) {
|
|
18
15
|
ContentOrigin["HUMAN"] = "HUMAN";
|
|
19
16
|
ContentOrigin["SYNTHETIC"] = "SYNTHETIC";
|
|
20
17
|
ContentOrigin["HYBRID"] = "HYBRID"; // Edited by humans, drafted by AI.
|
|
21
|
-
})(ContentOrigin || (
|
|
22
|
-
var QualityFlag;
|
|
18
|
+
})(ContentOrigin || (ContentOrigin = {}));
|
|
19
|
+
export var QualityFlag;
|
|
23
20
|
(function (QualityFlag) {
|
|
24
21
|
QualityFlag["SEO_SPAM"] = "SEO_SPAM";
|
|
25
22
|
QualityFlag["INACCURATE"] = "INACCURATE";
|
|
26
23
|
QualityFlag["HATE_SPEECH"] = "HATE_SPEECH";
|
|
27
24
|
QualityFlag["HIGH_QUALITY"] = "HIGH_QUALITY";
|
|
28
|
-
})(QualityFlag || (
|
|
25
|
+
})(QualityFlag || (QualityFlag = {}));
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aamp/protocol",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "TypeScript reference implementation of AAMP v1.1",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
7
8
|
"scripts": {
|
|
8
9
|
"build": "tsc",
|
|
9
|
-
"test": "ts-node test/handshake.spec.ts"
|
|
10
|
-
"prepublishOnly": "npm run test && npm run build"
|
|
10
|
+
"test": "node --loader ts-node/esm test/handshake.spec.ts"
|
|
11
11
|
},
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
@@ -17,8 +17,13 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"
|
|
20
|
+
"@types/node": "^20.0.0",
|
|
21
|
+
"@types/node-fetch": "^2.6.13",
|
|
21
22
|
"ts-node": "^10.9.0",
|
|
22
|
-
"
|
|
23
|
+
"typescript": "^5.0.0"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"jose": "^6.1.3",
|
|
27
|
+
"node-fetch": "^2.7.0"
|
|
23
28
|
}
|
|
24
29
|
}
|
package/src/agent.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Layer 2: Agent SDK
|
|
3
3
|
*/
|
|
4
|
-
import { AccessPurpose, ProtocolHeader, SignedAccessRequest, FeedbackSignal, QualityFlag, AgentIdentityManifest } from './types';
|
|
5
|
-
import { generateKeyPair, signData, exportPublicKey } from './crypto';
|
|
6
|
-
import { AAMP_VERSION } from './constants';
|
|
4
|
+
import { AccessPurpose, ProtocolHeader, SignedAccessRequest, FeedbackSignal, QualityFlag, AgentIdentityManifest } from './types.js';
|
|
5
|
+
import { generateKeyPair, signData, exportPublicKey } from './crypto.js';
|
|
6
|
+
import { AAMP_VERSION } from './constants.js';
|
|
7
7
|
|
|
8
8
|
export interface AccessOptions {
|
|
9
9
|
adsDisplayed?: boolean;
|
|
@@ -24,8 +24,8 @@ export class AAMPAgent {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
async createAccessRequest(
|
|
27
|
-
resource: string,
|
|
28
|
-
purpose: AccessPurpose,
|
|
27
|
+
resource: string,
|
|
28
|
+
purpose: AccessPurpose,
|
|
29
29
|
options: AccessOptions = {}
|
|
30
30
|
): Promise<SignedAccessRequest> {
|
|
31
31
|
if (!this.keyPair) throw new Error("Agent not initialized. Call initialize() first.");
|
|
@@ -53,9 +53,9 @@ export class AAMPAgent {
|
|
|
53
53
|
*/
|
|
54
54
|
async getIdentityManifest(contactEmail?: string): Promise<AgentIdentityManifest> {
|
|
55
55
|
if (!this.keyPair) throw new Error("Agent not initialized.");
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
const publicKey = await exportPublicKey(this.keyPair.publicKey);
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
return {
|
|
60
60
|
agent_id: this.agentId,
|
|
61
61
|
public_key: publicKey,
|
|
@@ -73,7 +73,7 @@ export class AAMPAgent {
|
|
|
73
73
|
flags: QualityFlag[]
|
|
74
74
|
): Promise<{ signal: FeedbackSignal, signature: string }> {
|
|
75
75
|
if (!this.keyPair) throw new Error("Agent not initialized.");
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
const signal: FeedbackSignal = {
|
|
78
78
|
target_resource: resource,
|
|
79
79
|
agent_id: this.agentId,
|
package/src/constants.ts
CHANGED
|
@@ -17,15 +17,24 @@ export const HEADERS = {
|
|
|
17
17
|
SIGNATURE: 'x-aamp-signature',
|
|
18
18
|
// Transport: The Agent's Public Key (Base64 SPKI)
|
|
19
19
|
PUBLIC_KEY: 'x-aamp-public-key',
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
// Informational / Legacy (Optional if Payload is present)
|
|
22
22
|
AGENT_ID: 'x-aamp-agent-id',
|
|
23
23
|
TIMESTAMP: 'x-aamp-timestamp',
|
|
24
|
-
ALGORITHM: 'x-aamp-alg',
|
|
25
|
-
|
|
24
|
+
ALGORITHM: 'x-aamp-alg',
|
|
25
|
+
|
|
26
26
|
// v1.1 Addition: Provenance (Server to Agent)
|
|
27
27
|
CONTENT_ORIGIN: 'x-aamp-content-origin',
|
|
28
|
-
PROVENANCE_SIG: 'x-aamp-provenance-sig'
|
|
28
|
+
PROVENANCE_SIG: 'x-aamp-provenance-sig',
|
|
29
|
+
|
|
30
|
+
// v1.2 Proof of Value
|
|
31
|
+
PROOF_TOKEN: 'x-aamp-proof',
|
|
32
|
+
|
|
33
|
+
// v1.2 Payment Credential (The "Digital Receipt")
|
|
34
|
+
PAYMENT_CREDENTIAL: 'x-aamp-credential',
|
|
35
|
+
|
|
36
|
+
// v1.2 Quality Feedback (The "Dispute Token")
|
|
37
|
+
FEEDBACK: 'x-aamp-feedback'
|
|
29
38
|
} as const;
|
|
30
39
|
|
|
31
40
|
// Cryptographic Settings
|
package/src/express.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Layer 3: Framework Adapters
|
|
3
3
|
* Zero-friction integration for Express/Node.js.
|
|
4
4
|
*/
|
|
5
|
-
import { AAMPPublisher } from './publisher';
|
|
6
|
-
import { AccessPolicy, ContentOrigin, UnauthenticatedStrategy, IdentityCache } from './types';
|
|
7
|
-
import { generateKeyPair } from './crypto';
|
|
5
|
+
import { AAMPPublisher } from './publisher.js';
|
|
6
|
+
import { AccessPolicy, ContentOrigin, UnauthenticatedStrategy, IdentityCache } from './types.js';
|
|
7
|
+
import { generateKeyPair } from './crypto.js';
|
|
8
8
|
|
|
9
9
|
export interface AAMPConfig {
|
|
10
10
|
policy: Omit<AccessPolicy, 'version'>;
|
|
@@ -28,9 +28,9 @@ export class AAMP {
|
|
|
28
28
|
config.strategy || 'PASSIVE',
|
|
29
29
|
config.cache
|
|
30
30
|
);
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
this.origin = ContentOrigin[config.meta.origin];
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
this.ready = generateKeyPair().then(keys => {
|
|
35
35
|
return this.publisher.initialize(keys);
|
|
36
36
|
});
|
|
@@ -62,9 +62,10 @@ export class AAMP {
|
|
|
62
62
|
|
|
63
63
|
// Enforce Decision
|
|
64
64
|
if (!result.allowed) {
|
|
65
|
-
res.status(result.status).json({
|
|
65
|
+
res.status(result.status).json({
|
|
66
66
|
error: result.reason,
|
|
67
|
-
visitor_type: result.visitorType
|
|
67
|
+
visitor_type: result.visitorType,
|
|
68
|
+
proof_used: result.proofUsed
|
|
68
69
|
});
|
|
69
70
|
return;
|
|
70
71
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* This is the main entry point for the library.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export * from './types';
|
|
8
|
-
export * from './constants';
|
|
9
|
-
export * from './agent';
|
|
10
|
-
export * from './publisher';
|
|
11
|
-
export * from './crypto';
|
|
12
|
-
export * from './express'; // Node.js / Express Adapter
|
|
13
|
-
export { AAMPNext } from './nextjs'; // Serverless / Next.js Adapter
|
|
7
|
+
export * from './types.js';
|
|
8
|
+
export * from './constants.js';
|
|
9
|
+
export * from './agent.js';
|
|
10
|
+
export * from './publisher.js';
|
|
11
|
+
export * from './crypto.js';
|
|
12
|
+
export * from './express.js'; // Node.js / Express Adapter
|
|
13
|
+
export { AAMPNext } from './nextjs.js'; // Serverless / Next.js Adapter
|
package/src/nextjs.ts
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Layer 3: Framework Adapters
|
|
3
3
|
* Serverless integration for Next.js (App Router & API Routes).
|
|
4
4
|
*/
|
|
5
|
-
import { AAMPPublisher } from './publisher';
|
|
6
|
-
import { AccessPolicy, ContentOrigin, UnauthenticatedStrategy, IdentityCache } from './types';
|
|
7
|
-
import { generateKeyPair } from './crypto';
|
|
5
|
+
import { AAMPPublisher } from './publisher.js';
|
|
6
|
+
import { AccessPolicy, ContentOrigin, UnauthenticatedStrategy, IdentityCache } from './types.js';
|
|
7
|
+
import { generateKeyPair } from './crypto.js';
|
|
8
8
|
|
|
9
|
-
type NextRequest = any;
|
|
10
|
-
type NextResponse = any;
|
|
9
|
+
type NextRequest = any;
|
|
10
|
+
type NextResponse = any;
|
|
11
11
|
|
|
12
12
|
const createJsonResponse = (body: any, status = 200) => {
|
|
13
13
|
return new Response(JSON.stringify(body), {
|
|
@@ -63,9 +63,10 @@ export class AAMPNext {
|
|
|
63
63
|
const result = await this.publisher.evaluateVisitor(headers, headers['x-aamp-payload']);
|
|
64
64
|
|
|
65
65
|
if (!result.allowed) {
|
|
66
|
-
return createJsonResponse({
|
|
66
|
+
return createJsonResponse({
|
|
67
67
|
error: result.reason,
|
|
68
|
-
visitor_type: result.visitorType
|
|
68
|
+
visitor_type: result.visitorType,
|
|
69
|
+
proof_used: result.proofUsed
|
|
69
70
|
}, result.status);
|
|
70
71
|
}
|
|
71
72
|
|
|
@@ -75,9 +76,9 @@ export class AAMPNext {
|
|
|
75
76
|
// Inject Provenance
|
|
76
77
|
const aampHeaders = await this.publisher.generateResponseHeaders(this.origin);
|
|
77
78
|
if (response && response.headers) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
Object.entries(aampHeaders).forEach(([k, v]) => {
|
|
80
|
+
response.headers.set(k, v);
|
|
81
|
+
});
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
return response;
|
package/src/proof.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
2
|
+
|
|
3
|
+
// In-memory cache for JWKS to avoid repeated fetches
|
|
4
|
+
// Jose's createRemoteJWKSet handles caching/cooldowns internally.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Verifies a JWT (Proof Token or Payment Credential) using JWKS.
|
|
8
|
+
*
|
|
9
|
+
* @param token The JWT string
|
|
10
|
+
* @param jwksUrl The URL to fetch Public Keys
|
|
11
|
+
* @param issuer The expected issuer
|
|
12
|
+
* @param audience The expected audience range
|
|
13
|
+
*/
|
|
14
|
+
export async function verifyJwt(
|
|
15
|
+
token: string,
|
|
16
|
+
jwksUrl: string,
|
|
17
|
+
issuer: string,
|
|
18
|
+
audience?: string
|
|
19
|
+
): Promise<boolean> {
|
|
20
|
+
try {
|
|
21
|
+
const JWKS = createRemoteJWKSet(new URL(jwksUrl));
|
|
22
|
+
|
|
23
|
+
const { payload } = await jwtVerify(token, JWKS, {
|
|
24
|
+
issuer: issuer,
|
|
25
|
+
audience: audience // specific audience check if provided
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Check specific AAMP claims if we standardize them
|
|
29
|
+
// if (payload.type !== 'AD_IMPRESSION') return false;
|
|
30
|
+
|
|
31
|
+
return true;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// console.error("Ad Proof Verification Failed:", error);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
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;
|
|
@@ -68,13 +71,18 @@ export class AAMPPublisher {
|
|
|
68
71
|
* Main Entry Point: Evaluate ANY visitor (Human, Bot, or Agent)
|
|
69
72
|
*/
|
|
70
73
|
async evaluateVisitor(
|
|
71
|
-
reqHeaders: Record<string, string | undefined>,
|
|
74
|
+
reqHeaders: Record<string, string | undefined>,
|
|
72
75
|
rawPayload?: string
|
|
73
76
|
): Promise<EvaluationResult> {
|
|
74
|
-
|
|
77
|
+
|
|
75
78
|
// 1. Check for AAMP Headers
|
|
76
79
|
const hasAamp = reqHeaders[HEADERS.PAYLOAD] && reqHeaders[HEADERS.SIGNATURE] && reqHeaders[HEADERS.PUBLIC_KEY];
|
|
77
80
|
|
|
81
|
+
const feedbackToken = reqHeaders[HEADERS.FEEDBACK];
|
|
82
|
+
if (feedbackToken) {
|
|
83
|
+
await this.handleFeedback(feedbackToken, reqHeaders);
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
if (hasAamp) {
|
|
79
87
|
console.log("\nš [AAMP Middleware] Detected Agent Headers. Starting Verification...");
|
|
80
88
|
// It claims to be an Agent. Verify it.
|
|
@@ -96,13 +104,13 @@ export class AAMPPublisher {
|
|
|
96
104
|
allowed: true,
|
|
97
105
|
status: 200,
|
|
98
106
|
reason: "PASSIVE_MODE: Allowed without verification.",
|
|
99
|
-
visitorType: 'LIKELY_HUMAN'
|
|
107
|
+
visitorType: 'LIKELY_HUMAN'
|
|
100
108
|
};
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
// 3. HYBRID MODE: Heuristic Analysis (The "Lazy Bot" Filter)
|
|
104
112
|
const isHuman = this.performBrowserHeuristics(reqHeaders);
|
|
105
|
-
|
|
113
|
+
|
|
106
114
|
if (isHuman) {
|
|
107
115
|
return {
|
|
108
116
|
allowed: true,
|
|
@@ -128,11 +136,11 @@ export class AAMPPublisher {
|
|
|
128
136
|
*/
|
|
129
137
|
private performBrowserHeuristics(headers: Record<string, string | undefined>): boolean {
|
|
130
138
|
const userAgent = headers['user-agent'] || '';
|
|
131
|
-
|
|
139
|
+
|
|
132
140
|
// A. The "Obvious Bot" Blocklist (Fast Fail)
|
|
133
141
|
const botSignatures = ['python-requests', 'curl', 'wget', 'scrapy', 'bot', 'crawler', 'spider'];
|
|
134
142
|
if (botSignatures.some(sig => userAgent.toLowerCase().includes(sig))) {
|
|
135
|
-
|
|
143
|
+
return false;
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
// B. Trusted Infrastructure Signals (The Real World Solution)
|
|
@@ -142,7 +150,7 @@ export class AAMPPublisher {
|
|
|
142
150
|
|
|
143
151
|
// C. The "Browser Fingerprint" (Fallback for direct connections)
|
|
144
152
|
const hasAcceptLanguage = !!headers['accept-language'];
|
|
145
|
-
const hasSecFetchDest = !!headers['sec-fetch-dest'];
|
|
153
|
+
const hasSecFetchDest = !!headers['sec-fetch-dest'];
|
|
146
154
|
const hasUpgradeInsecure = !!headers['upgrade-insecure-requests'];
|
|
147
155
|
|
|
148
156
|
if (hasAcceptLanguage && (hasSecFetchDest || hasUpgradeInsecure)) {
|
|
@@ -161,9 +169,9 @@ export class AAMPPublisher {
|
|
|
161
169
|
const sigHeader = reqHeaders[HEADERS.SIGNATURE]!;
|
|
162
170
|
const keyHeader = reqHeaders[HEADERS.PUBLIC_KEY]!;
|
|
163
171
|
|
|
164
|
-
const headerJson = atob(payloadHeader);
|
|
172
|
+
const headerJson = atob(payloadHeader);
|
|
165
173
|
const requestHeader = JSON.parse(headerJson);
|
|
166
|
-
|
|
174
|
+
|
|
167
175
|
const signedRequest: SignedAccessRequest = {
|
|
168
176
|
header: requestHeader,
|
|
169
177
|
signature: sigHeader,
|
|
@@ -178,11 +186,20 @@ export class AAMPPublisher {
|
|
|
178
186
|
["verify"]
|
|
179
187
|
);
|
|
180
188
|
|
|
189
|
+
const proofToken = reqHeaders[HEADERS.PROOF_TOKEN];
|
|
190
|
+
const paymentCredential = reqHeaders[HEADERS.PAYMENT_CREDENTIAL];
|
|
191
|
+
|
|
181
192
|
// Verify Core Logic
|
|
182
|
-
const result = await this.verifyRequestLogic(signedRequest, agentKey, headerJson);
|
|
193
|
+
const result = await this.verifyRequestLogic(signedRequest, agentKey, proofToken, paymentCredential, headerJson);
|
|
183
194
|
|
|
184
195
|
if (!result.allowed) {
|
|
185
|
-
console.log(`ā [AAMP
|
|
196
|
+
console.log(`ā [AAMP BLOCK]
|
|
197
|
+
Agent: ${requestHeader.agent_id}
|
|
198
|
+
Reason: ${result.reason}
|
|
199
|
+
VisitorType: ${result.visitorType}
|
|
200
|
+
Proof: ${result.proofUsed || 'None'}
|
|
201
|
+
Identity Verified: ${result.identityVerified}`);
|
|
202
|
+
|
|
186
203
|
return {
|
|
187
204
|
allowed: false,
|
|
188
205
|
status: 403,
|
|
@@ -191,7 +208,12 @@ export class AAMPPublisher {
|
|
|
191
208
|
};
|
|
192
209
|
}
|
|
193
210
|
|
|
194
|
-
console.log(`ā
[AAMP
|
|
211
|
+
console.log(`ā
[AAMP ALLOW]
|
|
212
|
+
Agent: ${requestHeader.agent_id}
|
|
213
|
+
Reason: AAMP_VERIFIED
|
|
214
|
+
Payment Method: ${result.proofUsed}
|
|
215
|
+
Identity Verified: ${result.identityVerified}`);
|
|
216
|
+
|
|
195
217
|
return {
|
|
196
218
|
allowed: true,
|
|
197
219
|
status: 200,
|
|
@@ -207,11 +229,13 @@ export class AAMPPublisher {
|
|
|
207
229
|
}
|
|
208
230
|
|
|
209
231
|
private async verifyRequestLogic(
|
|
210
|
-
request: SignedAccessRequest,
|
|
232
|
+
request: SignedAccessRequest,
|
|
211
233
|
requestPublicKey: CryptoKey,
|
|
234
|
+
proofToken?: string,
|
|
235
|
+
paymentCredential?: string,
|
|
212
236
|
rawPayload?: string
|
|
213
237
|
): Promise<VerificationResult> {
|
|
214
|
-
|
|
238
|
+
|
|
215
239
|
// 1. Replay Attack Prevention
|
|
216
240
|
const requestTime = new Date(request.header.ts).getTime();
|
|
217
241
|
if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
|
|
@@ -227,12 +251,12 @@ export class AAMPPublisher {
|
|
|
227
251
|
let identityVerified = false;
|
|
228
252
|
const claimedDomain = request.header.agent_id;
|
|
229
253
|
const pubKeyString = await exportPublicKey(requestPublicKey);
|
|
230
|
-
|
|
254
|
+
|
|
231
255
|
console.log(` š [AAMP Identity] Verifying DNS Binding for: ${claimedDomain}`);
|
|
232
256
|
|
|
233
257
|
// Check Cache First
|
|
234
258
|
const cachedKey = await this.cache.get(claimedDomain);
|
|
235
|
-
|
|
259
|
+
|
|
236
260
|
if (cachedKey === pubKeyString) {
|
|
237
261
|
console.log(" ā” [AAMP Cache] Identity found in cache.");
|
|
238
262
|
identityVerified = true;
|
|
@@ -256,11 +280,53 @@ export class AAMPPublisher {
|
|
|
256
280
|
return { allowed: false, reason: 'POLICY_DENIED: RAG not allowed.', identityVerified };
|
|
257
281
|
}
|
|
258
282
|
|
|
259
|
-
// 5. Policy Check: Economics
|
|
283
|
+
// 5. Policy Check: Economics (v1.2)
|
|
260
284
|
if (this.policy.requiresPayment) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
285
|
+
let paymentSatisfied = false;
|
|
286
|
+
|
|
287
|
+
// Method A: Flexible Payment Callback (DB / Custom Logic)
|
|
288
|
+
if (this.policy.monetization?.checkPayment) {
|
|
289
|
+
const isPaid = await this.policy.monetization.checkPayment(request.header.agent_id, request.header.purpose);
|
|
290
|
+
if (isPaid) {
|
|
291
|
+
console.log(` š° [AAMP Audit] Whitelist Check Passed via Callback.`);
|
|
292
|
+
return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'WHITELIST_CALLBACK' };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Method B: Payment Credentials (Unified JWT)
|
|
297
|
+
if (!paymentSatisfied && this.policy.monetization?.paymentConfig && paymentCredential) {
|
|
298
|
+
const { jwksUrl, issuer } = this.policy.monetization.paymentConfig;
|
|
299
|
+
console.log(` š [AAMP Audit] Verifying Payment Credential (Issuer: ${issuer})...`);
|
|
300
|
+
|
|
301
|
+
const isValidCredential = await verifyJwt(paymentCredential, jwksUrl, issuer);
|
|
302
|
+
if (isValidCredential) {
|
|
303
|
+
console.log(` ā
[AAMP Audit] Credential Signature VALID.`);
|
|
304
|
+
return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'PAYMENT_CREDENTIAL_JWT' };
|
|
305
|
+
} else {
|
|
306
|
+
console.log(` ā [AAMP Audit] Credential Signature INVALID.`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Method C: Ad-Supported (Proof Verification)
|
|
311
|
+
if (!paymentSatisfied && this.policy.allowAdSupportedAccess && request.header.context.ads_displayed) {
|
|
312
|
+
if (proofToken && this.policy.monetization?.adNetwork) {
|
|
313
|
+
const { jwksUrl, issuer } = this.policy.monetization.adNetwork;
|
|
314
|
+
console.log(` šŗ [AAMP Audit] Verifying Ad Proof (Issuer: ${issuer})...`);
|
|
315
|
+
|
|
316
|
+
const isValidProof = await verifyJwt(proofToken, jwksUrl, issuer);
|
|
317
|
+
if (isValidProof) {
|
|
318
|
+
console.log(` ā
[AAMP Audit] Ad Proof Signature VALID.`);
|
|
319
|
+
return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'AD_PROOF_JWT' };
|
|
320
|
+
} else {
|
|
321
|
+
console.log(` ā [AAMP Audit] Ad Proof Signature INVALID.`);
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
console.log(` ā ļø [AAMP Audit] Ad Proof MISSING.`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!paymentSatisfied) {
|
|
329
|
+
return { allowed: false, reason: 'PAYMENT_REQUIRED: Whitelist, Credential, and Ad Proof checks ALL failed.', identityVerified, proofUsed: 'NONE', visitorType: 'VERIFIED_AGENT' };
|
|
264
330
|
}
|
|
265
331
|
}
|
|
266
332
|
|
|
@@ -272,16 +338,16 @@ export class AAMPPublisher {
|
|
|
272
338
|
// Allow HTTP for localhost testing
|
|
273
339
|
const protocol = (domain.includes('localhost') || domain.match(/:\d+$/)) ? 'http' : 'https';
|
|
274
340
|
const url = `${protocol}://${domain}${WELL_KNOWN_AGENT_PATH}`;
|
|
275
|
-
|
|
341
|
+
|
|
276
342
|
console.log(` š [AAMP DNS] Fetching Manifest: ${url} ...`);
|
|
277
343
|
|
|
278
344
|
// In production, we need a short timeout to prevent hanging
|
|
279
345
|
const controller = new AbortController();
|
|
280
346
|
const timeoutId = setTimeout(() => controller.abort(), 1500); // 1.5s max for DNS check
|
|
281
|
-
|
|
347
|
+
|
|
282
348
|
const response = await fetch(url, { signal: controller.signal });
|
|
283
349
|
clearTimeout(timeoutId);
|
|
284
|
-
|
|
350
|
+
|
|
285
351
|
if (!response.ok) {
|
|
286
352
|
console.log(` ā [AAMP DNS] Fetch Failed: ${response.status}`);
|
|
287
353
|
return false;
|
|
@@ -298,15 +364,15 @@ export class AAMPPublisher {
|
|
|
298
364
|
|
|
299
365
|
// CHECK 2: Does the key match?
|
|
300
366
|
if (manifest.public_key !== requestKeySpki) {
|
|
301
|
-
|
|
302
|
-
|
|
367
|
+
console.log(` ā [AAMP DNS] Key Mismatch: DNS Key != Request Key`);
|
|
368
|
+
return false;
|
|
303
369
|
}
|
|
304
370
|
|
|
305
371
|
console.log(` ā
[AAMP DNS] Identity Confirmed.`);
|
|
306
372
|
return true;
|
|
307
|
-
} catch (e: any) {
|
|
308
|
-
|
|
309
|
-
|
|
373
|
+
} catch (e: any) {
|
|
374
|
+
console.log(` ā [AAMP DNS] Error: ${e.message}`);
|
|
375
|
+
return false;
|
|
310
376
|
}
|
|
311
377
|
}
|
|
312
378
|
|
|
@@ -324,4 +390,28 @@ export class AAMPPublisher {
|
|
|
324
390
|
[HEADERS.PROVENANCE_SIG]: signature
|
|
325
391
|
};
|
|
326
392
|
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Handling Quality Feedback (The "Dispute" Layer)
|
|
396
|
+
* This runs when an Agent sends 'x-aamp-feedback'.
|
|
397
|
+
*/
|
|
398
|
+
private async handleFeedback(token: string, headers: Record<string, string | undefined>) {
|
|
399
|
+
// NOTE: In production, you would fetch the Agent's specific key.
|
|
400
|
+
// For now, we assume standard Discovery or a centralized Key Set (like adNetwork).
|
|
401
|
+
// Ideally, the SDK config should have a 'qualityOracle' key set.
|
|
402
|
+
|
|
403
|
+
// 1. We just Decode it to Log it (Verification is optional but recommended)
|
|
404
|
+
try {
|
|
405
|
+
const parts = token.split('.');
|
|
406
|
+
const payload = JSON.parse(atob(parts[1]));
|
|
407
|
+
|
|
408
|
+
console.log(`\nš¢ [AAMP QUALITY ALERT] Feedback Received from ${payload.agent_id}`);
|
|
409
|
+
console.log(` Reason: ${payload.reason} | Score: ${payload.quality_score}`);
|
|
410
|
+
console.log(` Resource: ${payload.url}`);
|
|
411
|
+
console.log(` (Signature available for dispute evidence)`);
|
|
412
|
+
|
|
413
|
+
} catch (e) {
|
|
414
|
+
console.log(` ā ļø [AAMP Warning] Malformed Feedback Token.`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
327
417
|
}
|