@aamp/protocol 1.1.3 ā 1.1.5
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/crypto.js +4 -1
- package/dist/publisher.d.ts +1 -5
- package/dist/publisher.js +32 -16
- package/package.json +1 -1
- package/src/crypto.ts +6 -1
- package/src/publisher.ts +43 -19
package/dist/crypto.js
CHANGED
|
@@ -23,7 +23,10 @@ async function verifySignature(publicKey, data, signatureHex) {
|
|
|
23
23
|
const encoder = new TextEncoder();
|
|
24
24
|
const encodedData = encoder.encode(data);
|
|
25
25
|
const signatureBytes = hexToBuf(signatureHex);
|
|
26
|
-
|
|
26
|
+
console.log(" š [AAMP Crypto] Verifying ECDSA P-256 Signature...");
|
|
27
|
+
const isValid = await crypto.subtle.verify({ name: "ECDSA", hash: { name: "SHA-256" } }, publicKey, signatureBytes, encodedData);
|
|
28
|
+
console.log(` ${isValid ? "ā
" : "ā"} [AAMP Crypto] Signature Result: ${isValid ? "VALID" : "INVALID"}`);
|
|
29
|
+
return isValid;
|
|
27
30
|
}
|
|
28
31
|
async function exportPublicKey(key) {
|
|
29
32
|
const exported = await crypto.subtle.exportKey("spki", key);
|
package/dist/publisher.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Layer 2: Publisher Middleware
|
|
3
|
-
* Used by content owners to enforce policy, log access, and filter bots.
|
|
4
|
-
*/
|
|
5
|
-
import { AccessPolicy, ContentOrigin, EvaluationResult, UnauthenticatedStrategy, IdentityCache } from './types';
|
|
1
|
+
import { AccessPolicy, ContentOrigin, EvaluationResult, IdentityCache, UnauthenticatedStrategy } from './types';
|
|
6
2
|
export declare class AAMPPublisher {
|
|
7
3
|
private policy;
|
|
8
4
|
private keyPair;
|
package/dist/publisher.js
CHANGED
|
@@ -5,9 +5,9 @@ exports.AAMPPublisher = void 0;
|
|
|
5
5
|
* Layer 2: Publisher Middleware
|
|
6
6
|
* Used by content owners to enforce policy, log access, and filter bots.
|
|
7
7
|
*/
|
|
8
|
-
const types_1 = require("./types");
|
|
9
|
-
const crypto_1 = require("./crypto");
|
|
10
8
|
const constants_1 = require("./constants");
|
|
9
|
+
const crypto_1 = require("./crypto");
|
|
10
|
+
const types_1 = require("./types");
|
|
11
11
|
/**
|
|
12
12
|
* Default In-Memory Cache (Fallback only)
|
|
13
13
|
* NOT recommended for high-traffic Serverless production.
|
|
@@ -55,6 +55,7 @@ class AAMPPublisher {
|
|
|
55
55
|
// 1. Check for AAMP Headers
|
|
56
56
|
const hasAamp = reqHeaders[constants_1.HEADERS.PAYLOAD] && reqHeaders[constants_1.HEADERS.SIGNATURE] && reqHeaders[constants_1.HEADERS.PUBLIC_KEY];
|
|
57
57
|
if (hasAamp) {
|
|
58
|
+
console.log("\nš [AAMP Middleware] Detected Agent Headers. Starting Verification...");
|
|
58
59
|
// It claims to be an Agent. Verify it.
|
|
59
60
|
return await this.handleAgent(reqHeaders, rawPayload);
|
|
60
61
|
}
|
|
@@ -104,32 +105,23 @@ class AAMPPublisher {
|
|
|
104
105
|
const userAgent = headers['user-agent'] || '';
|
|
105
106
|
// A. The "Obvious Bot" Blocklist (Fast Fail)
|
|
106
107
|
const botSignatures = ['python-requests', 'curl', 'wget', 'scrapy', 'bot', 'crawler', 'spider'];
|
|
107
|
-
// Exception: Googlebot (if you want SEO). We'll treat Googlebot as a bot,
|
|
108
|
-
// real implementations might white-list it via IP verification (not possible in just JS headers).
|
|
109
108
|
if (botSignatures.some(sig => userAgent.toLowerCase().includes(sig))) {
|
|
110
109
|
return false;
|
|
111
110
|
}
|
|
112
111
|
// B. Trusted Infrastructure Signals (The Real World Solution)
|
|
113
|
-
// If Cloudflare or Vercel says "This is a real user", we trust them.
|
|
114
|
-
// Cloudflare: 'cf-visitor' exists. 'cf-ipcountry' exists.
|
|
115
112
|
if (headers['cf-visitor'] || headers['cf-ray'])
|
|
116
113
|
return true;
|
|
117
|
-
// Vercel: 'x-vercel-id'
|
|
118
114
|
if (headers['x-vercel-id'])
|
|
119
115
|
return true;
|
|
120
|
-
// AWS CloudFront: 'cloudfront-viewer-address'
|
|
121
116
|
if (headers['cloudfront-viewer-address'])
|
|
122
117
|
return true;
|
|
123
118
|
// C. The "Browser Fingerprint" (Fallback for direct connections)
|
|
124
|
-
// Real browsers almost always send these headers
|
|
125
119
|
const hasAcceptLanguage = !!headers['accept-language'];
|
|
126
120
|
const hasSecFetchDest = !!headers['sec-fetch-dest'];
|
|
127
121
|
const hasUpgradeInsecure = !!headers['upgrade-insecure-requests'];
|
|
128
|
-
// If it has typical browser headers, we allow it.
|
|
129
122
|
if (hasAcceptLanguage && (hasSecFetchDest || hasUpgradeInsecure)) {
|
|
130
123
|
return true;
|
|
131
124
|
}
|
|
132
|
-
// If it has no browser headers and no trusted proxy headers -> It's likely a script.
|
|
133
125
|
return false;
|
|
134
126
|
}
|
|
135
127
|
/**
|
|
@@ -151,6 +143,7 @@ class AAMPPublisher {
|
|
|
151
143
|
// Verify Core Logic
|
|
152
144
|
const result = await this.verifyRequestLogic(signedRequest, agentKey, headerJson);
|
|
153
145
|
if (!result.allowed) {
|
|
146
|
+
console.log(`ā [AAMP Deny] Reason: ${result.reason}`);
|
|
154
147
|
return {
|
|
155
148
|
allowed: false,
|
|
156
149
|
status: 403,
|
|
@@ -158,6 +151,7 @@ class AAMPPublisher {
|
|
|
158
151
|
visitorType: 'VERIFIED_AGENT'
|
|
159
152
|
};
|
|
160
153
|
}
|
|
154
|
+
console.log(`ā
[AAMP Allow] Verified Agent: ${requestHeader.agent_id}`);
|
|
161
155
|
return {
|
|
162
156
|
allowed: true,
|
|
163
157
|
status: 200,
|
|
@@ -167,6 +161,7 @@ class AAMPPublisher {
|
|
|
167
161
|
};
|
|
168
162
|
}
|
|
169
163
|
catch (e) {
|
|
164
|
+
console.error(e);
|
|
170
165
|
return { allowed: false, status: 400, reason: "INVALID_SIGNATURE", visitorType: 'UNIDENTIFIED_BOT' };
|
|
171
166
|
}
|
|
172
167
|
}
|
|
@@ -185,9 +180,11 @@ class AAMPPublisher {
|
|
|
185
180
|
let identityVerified = false;
|
|
186
181
|
const claimedDomain = request.header.agent_id;
|
|
187
182
|
const pubKeyString = await (0, crypto_1.exportPublicKey)(requestPublicKey);
|
|
183
|
+
console.log(` š [AAMP Identity] Verifying DNS Binding for: ${claimedDomain}`);
|
|
188
184
|
// Check Cache First
|
|
189
185
|
const cachedKey = await this.cache.get(claimedDomain);
|
|
190
186
|
if (cachedKey === pubKeyString) {
|
|
187
|
+
console.log(" ā” [AAMP Cache] Identity found in cache.");
|
|
191
188
|
identityVerified = true;
|
|
192
189
|
}
|
|
193
190
|
else if (this.isDomain(claimedDomain)) {
|
|
@@ -218,23 +215,42 @@ class AAMPPublisher {
|
|
|
218
215
|
}
|
|
219
216
|
async verifyDnsBinding(domain, requestKeySpki) {
|
|
220
217
|
try {
|
|
221
|
-
|
|
218
|
+
// Allow HTTP for localhost testing
|
|
219
|
+
const protocol = (domain.includes('localhost') || domain.match(/:\d+$/)) ? 'http' : 'https';
|
|
220
|
+
const url = `${protocol}://${domain}${constants_1.WELL_KNOWN_AGENT_PATH}`;
|
|
221
|
+
console.log(` š [AAMP DNS] Fetching Manifest: ${url} ...`);
|
|
222
222
|
// In production, we need a short timeout to prevent hanging
|
|
223
223
|
const controller = new AbortController();
|
|
224
224
|
const timeoutId = setTimeout(() => controller.abort(), 1500); // 1.5s max for DNS check
|
|
225
225
|
const response = await fetch(url, { signal: controller.signal });
|
|
226
226
|
clearTimeout(timeoutId);
|
|
227
|
-
if (!response.ok)
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
console.log(` ā [AAMP DNS] Fetch Failed: ${response.status}`);
|
|
228
229
|
return false;
|
|
230
|
+
}
|
|
229
231
|
const manifest = await response.json();
|
|
230
|
-
|
|
232
|
+
console.log(` š [AAMP DNS] Manifest received. Agent ID: ${manifest.agent_id}`);
|
|
233
|
+
// CHECK 1: Does the manifest actually belong to the domain?
|
|
234
|
+
if (manifest.agent_id !== domain) {
|
|
235
|
+
console.log(` ā [AAMP DNS] Mismatch: Manifest ID ${manifest.agent_id} != Claimed ${domain}`);
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
// CHECK 2: Does the key match?
|
|
239
|
+
if (manifest.public_key !== requestKeySpki) {
|
|
240
|
+
console.log(` ā [AAMP DNS] Key Mismatch: DNS Key != Request Key`);
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
console.log(` ā
[AAMP DNS] Identity Confirmed.`);
|
|
244
|
+
return true;
|
|
231
245
|
}
|
|
232
|
-
catch {
|
|
246
|
+
catch (e) {
|
|
247
|
+
console.log(` ā [AAMP DNS] Error: ${e.message}`);
|
|
233
248
|
return false;
|
|
234
249
|
}
|
|
235
250
|
}
|
|
236
251
|
isDomain(s) {
|
|
237
|
-
|
|
252
|
+
// Basic regex, allows localhost with ports
|
|
253
|
+
return /^[a-zA-Z0-9.-]+(:\d+)?$/.test(s) || /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(s);
|
|
238
254
|
}
|
|
239
255
|
async generateResponseHeaders(origin) {
|
|
240
256
|
if (!this.keyPair)
|
package/package.json
CHANGED
package/src/crypto.ts
CHANGED
|
@@ -28,12 +28,17 @@ export async function verifySignature(publicKey: CryptoKey, data: string, signat
|
|
|
28
28
|
const encodedData = encoder.encode(data);
|
|
29
29
|
const signatureBytes = hexToBuf(signatureHex);
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
console.log(" š [AAMP Crypto] Verifying ECDSA P-256 Signature...");
|
|
32
|
+
|
|
33
|
+
const isValid = await crypto.subtle.verify(
|
|
32
34
|
{ name: "ECDSA", hash: { name: "SHA-256" } },
|
|
33
35
|
publicKey,
|
|
34
36
|
signatureBytes as any,
|
|
35
37
|
encodedData as any
|
|
36
38
|
);
|
|
39
|
+
|
|
40
|
+
console.log(` ${isValid ? "ā
" : "ā"} [AAMP Crypto] Signature Result: ${isValid ? "VALID" : "INVALID"}`);
|
|
41
|
+
return isValid;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
export async function exportPublicKey(key: CryptoKey): Promise<string> {
|
package/src/publisher.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Layer 2: Publisher Middleware
|
|
3
3
|
* Used by content owners to enforce policy, log access, and filter bots.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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';
|
|
8
8
|
|
|
9
9
|
interface VerificationResult {
|
|
10
10
|
allowed: boolean;
|
|
@@ -76,6 +76,7 @@ export class AAMPPublisher {
|
|
|
76
76
|
const hasAamp = reqHeaders[HEADERS.PAYLOAD] && reqHeaders[HEADERS.SIGNATURE] && reqHeaders[HEADERS.PUBLIC_KEY];
|
|
77
77
|
|
|
78
78
|
if (hasAamp) {
|
|
79
|
+
console.log("\nš [AAMP Middleware] Detected Agent Headers. Starting Verification...");
|
|
79
80
|
// It claims to be an Agent. Verify it.
|
|
80
81
|
return await this.handleAgent(reqHeaders, rawPayload);
|
|
81
82
|
}
|
|
@@ -130,35 +131,24 @@ export class AAMPPublisher {
|
|
|
130
131
|
|
|
131
132
|
// A. The "Obvious Bot" Blocklist (Fast Fail)
|
|
132
133
|
const botSignatures = ['python-requests', 'curl', 'wget', 'scrapy', 'bot', 'crawler', 'spider'];
|
|
133
|
-
// Exception: Googlebot (if you want SEO). We'll treat Googlebot as a bot,
|
|
134
|
-
// real implementations might white-list it via IP verification (not possible in just JS headers).
|
|
135
134
|
if (botSignatures.some(sig => userAgent.toLowerCase().includes(sig))) {
|
|
136
135
|
return false;
|
|
137
136
|
}
|
|
138
137
|
|
|
139
138
|
// B. Trusted Infrastructure Signals (The Real World Solution)
|
|
140
|
-
// If Cloudflare or Vercel says "This is a real user", we trust them.
|
|
141
|
-
// Cloudflare: 'cf-visitor' exists. 'cf-ipcountry' exists.
|
|
142
139
|
if (headers['cf-visitor'] || headers['cf-ray']) return true;
|
|
143
|
-
|
|
144
|
-
// Vercel: 'x-vercel-id'
|
|
145
140
|
if (headers['x-vercel-id']) return true;
|
|
146
|
-
|
|
147
|
-
// AWS CloudFront: 'cloudfront-viewer-address'
|
|
148
141
|
if (headers['cloudfront-viewer-address']) return true;
|
|
149
142
|
|
|
150
143
|
// C. The "Browser Fingerprint" (Fallback for direct connections)
|
|
151
|
-
// Real browsers almost always send these headers
|
|
152
144
|
const hasAcceptLanguage = !!headers['accept-language'];
|
|
153
145
|
const hasSecFetchDest = !!headers['sec-fetch-dest'];
|
|
154
146
|
const hasUpgradeInsecure = !!headers['upgrade-insecure-requests'];
|
|
155
147
|
|
|
156
|
-
// If it has typical browser headers, we allow it.
|
|
157
148
|
if (hasAcceptLanguage && (hasSecFetchDest || hasUpgradeInsecure)) {
|
|
158
149
|
return true;
|
|
159
150
|
}
|
|
160
151
|
|
|
161
|
-
// If it has no browser headers and no trusted proxy headers -> It's likely a script.
|
|
162
152
|
return false;
|
|
163
153
|
}
|
|
164
154
|
|
|
@@ -192,6 +182,7 @@ export class AAMPPublisher {
|
|
|
192
182
|
const result = await this.verifyRequestLogic(signedRequest, agentKey, headerJson);
|
|
193
183
|
|
|
194
184
|
if (!result.allowed) {
|
|
185
|
+
console.log(`ā [AAMP Deny] Reason: ${result.reason}`);
|
|
195
186
|
return {
|
|
196
187
|
allowed: false,
|
|
197
188
|
status: 403,
|
|
@@ -200,6 +191,7 @@ export class AAMPPublisher {
|
|
|
200
191
|
};
|
|
201
192
|
}
|
|
202
193
|
|
|
194
|
+
console.log(`ā
[AAMP Allow] Verified Agent: ${requestHeader.agent_id}`);
|
|
203
195
|
return {
|
|
204
196
|
allowed: true,
|
|
205
197
|
status: 200,
|
|
@@ -209,6 +201,7 @@ export class AAMPPublisher {
|
|
|
209
201
|
};
|
|
210
202
|
|
|
211
203
|
} catch (e) {
|
|
204
|
+
console.error(e);
|
|
212
205
|
return { allowed: false, status: 400, reason: "INVALID_SIGNATURE", visitorType: 'UNIDENTIFIED_BOT' };
|
|
213
206
|
}
|
|
214
207
|
}
|
|
@@ -235,10 +228,13 @@ export class AAMPPublisher {
|
|
|
235
228
|
const claimedDomain = request.header.agent_id;
|
|
236
229
|
const pubKeyString = await exportPublicKey(requestPublicKey);
|
|
237
230
|
|
|
231
|
+
console.log(` š [AAMP Identity] Verifying DNS Binding for: ${claimedDomain}`);
|
|
232
|
+
|
|
238
233
|
// Check Cache First
|
|
239
234
|
const cachedKey = await this.cache.get(claimedDomain);
|
|
240
235
|
|
|
241
236
|
if (cachedKey === pubKeyString) {
|
|
237
|
+
console.log(" ā” [AAMP Cache] Identity found in cache.");
|
|
242
238
|
identityVerified = true;
|
|
243
239
|
} else if (this.isDomain(claimedDomain)) {
|
|
244
240
|
// Cache Miss: Perform DNS Fetch
|
|
@@ -273,7 +269,12 @@ export class AAMPPublisher {
|
|
|
273
269
|
|
|
274
270
|
private async verifyDnsBinding(domain: string, requestKeySpki: string): Promise<boolean> {
|
|
275
271
|
try {
|
|
276
|
-
|
|
272
|
+
// Allow HTTP for localhost testing
|
|
273
|
+
const protocol = (domain.includes('localhost') || domain.match(/:\d+$/)) ? 'http' : 'https';
|
|
274
|
+
const url = `${protocol}://${domain}${WELL_KNOWN_AGENT_PATH}`;
|
|
275
|
+
|
|
276
|
+
console.log(` š [AAMP DNS] Fetching Manifest: ${url} ...`);
|
|
277
|
+
|
|
277
278
|
// In production, we need a short timeout to prevent hanging
|
|
278
279
|
const controller = new AbortController();
|
|
279
280
|
const timeoutId = setTimeout(() => controller.abort(), 1500); // 1.5s max for DNS check
|
|
@@ -281,14 +282,37 @@ export class AAMPPublisher {
|
|
|
281
282
|
const response = await fetch(url, { signal: controller.signal });
|
|
282
283
|
clearTimeout(timeoutId);
|
|
283
284
|
|
|
284
|
-
if (!response.ok)
|
|
285
|
+
if (!response.ok) {
|
|
286
|
+
console.log(` ā [AAMP DNS] Fetch Failed: ${response.status}`);
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
285
290
|
const manifest = await response.json() as AgentIdentityManifest;
|
|
286
|
-
|
|
287
|
-
|
|
291
|
+
console.log(` š [AAMP DNS] Manifest received. Agent ID: ${manifest.agent_id}`);
|
|
292
|
+
|
|
293
|
+
// CHECK 1: Does the manifest actually belong to the domain?
|
|
294
|
+
if (manifest.agent_id !== domain) {
|
|
295
|
+
console.log(` ā [AAMP DNS] Mismatch: Manifest ID ${manifest.agent_id} != Claimed ${domain}`);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// CHECK 2: Does the key match?
|
|
300
|
+
if (manifest.public_key !== requestKeySpki) {
|
|
301
|
+
console.log(` ā [AAMP DNS] Key Mismatch: DNS Key != Request Key`);
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
console.log(` ā
[AAMP DNS] Identity Confirmed.`);
|
|
306
|
+
return true;
|
|
307
|
+
} catch (e: any) {
|
|
308
|
+
console.log(` ā [AAMP DNS] Error: ${e.message}`);
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
288
311
|
}
|
|
289
312
|
|
|
290
313
|
private isDomain(s: string): boolean {
|
|
291
|
-
|
|
314
|
+
// Basic regex, allows localhost with ports
|
|
315
|
+
return /^[a-zA-Z0-9.-]+(:\d+)?$/.test(s) || /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(s);
|
|
292
316
|
}
|
|
293
317
|
|
|
294
318
|
async generateResponseHeaders(origin: ContentOrigin): Promise<Record<string, string>> {
|