@aamp/protocol 1.1.4 ā 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.js +25 -12
- package/package.json +1 -1
- package/src/crypto.ts +6 -1
- package/src/publisher.ts +34 -14
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.js
CHANGED
|
@@ -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)) {
|
|
@@ -221,17 +218,33 @@ class AAMPPublisher {
|
|
|
221
218
|
// Allow HTTP for localhost testing
|
|
222
219
|
const protocol = (domain.includes('localhost') || domain.match(/:\d+$/)) ? 'http' : 'https';
|
|
223
220
|
const url = `${protocol}://${domain}${constants_1.WELL_KNOWN_AGENT_PATH}`;
|
|
221
|
+
console.log(` š [AAMP DNS] Fetching Manifest: ${url} ...`);
|
|
224
222
|
// In production, we need a short timeout to prevent hanging
|
|
225
223
|
const controller = new AbortController();
|
|
226
224
|
const timeoutId = setTimeout(() => controller.abort(), 1500); // 1.5s max for DNS check
|
|
227
225
|
const response = await fetch(url, { signal: controller.signal });
|
|
228
226
|
clearTimeout(timeoutId);
|
|
229
|
-
if (!response.ok)
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
console.log(` ā [AAMP DNS] Fetch Failed: ${response.status}`);
|
|
230
229
|
return false;
|
|
230
|
+
}
|
|
231
231
|
const manifest = await response.json();
|
|
232
|
-
|
|
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;
|
|
233
245
|
}
|
|
234
|
-
catch {
|
|
246
|
+
catch (e) {
|
|
247
|
+
console.log(` ā [AAMP DNS] Error: ${e.message}`);
|
|
235
248
|
return false;
|
|
236
249
|
}
|
|
237
250
|
}
|
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
|
@@ -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
|
|
@@ -277,6 +273,8 @@ export class AAMPPublisher {
|
|
|
277
273
|
const protocol = (domain.includes('localhost') || domain.match(/:\d+$/)) ? 'http' : 'https';
|
|
278
274
|
const url = `${protocol}://${domain}${WELL_KNOWN_AGENT_PATH}`;
|
|
279
275
|
|
|
276
|
+
console.log(` š [AAMP DNS] Fetching Manifest: ${url} ...`);
|
|
277
|
+
|
|
280
278
|
// In production, we need a short timeout to prevent hanging
|
|
281
279
|
const controller = new AbortController();
|
|
282
280
|
const timeoutId = setTimeout(() => controller.abort(), 1500); // 1.5s max for DNS check
|
|
@@ -284,10 +282,32 @@ export class AAMPPublisher {
|
|
|
284
282
|
const response = await fetch(url, { signal: controller.signal });
|
|
285
283
|
clearTimeout(timeoutId);
|
|
286
284
|
|
|
287
|
-
if (!response.ok)
|
|
285
|
+
if (!response.ok) {
|
|
286
|
+
console.log(` ā [AAMP DNS] Fetch Failed: ${response.status}`);
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
288
290
|
const manifest = await response.json() as AgentIdentityManifest;
|
|
289
|
-
|
|
290
|
-
|
|
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
|
+
}
|
|
291
311
|
}
|
|
292
312
|
|
|
293
313
|
private isDomain(s: string): boolean {
|