@paywalls-net/filter 1.3.9 → 1.3.11
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/jest.config.js +7 -0
- package/package.json +6 -2
- package/src/index.js +58 -40
- package/src/signal-extraction.js +524 -0
- package/tests/proxy-vai-request.test.js +379 -0
- package/tests/signal-extraction.test.js +1002 -0
package/jest.config.js
ADDED
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Client SDK for integrating paywalls.net bot filtering and authorization services into your server or CDN.",
|
|
4
4
|
"author": "paywalls.net",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "1.3.
|
|
6
|
+
"version": "1.3.11",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
@@ -17,9 +17,13 @@
|
|
|
17
17
|
".": "./src/index.js"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
|
-
"test": "
|
|
20
|
+
"test": "node --experimental-vm-modules node_modules/.bin/jest --runInBand",
|
|
21
|
+
"test:watch": "node --experimental-vm-modules node_modules/.bin/jest --watch"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"ua-parser-js": "^2.0.4"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"jest": "^30.2.0"
|
|
24
28
|
}
|
|
25
29
|
}
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
const sdk_version = "1.2.x";
|
|
6
6
|
import { classifyUserAgent, loadAgentPatterns } from './user-agent-classification.js';
|
|
7
|
+
import {
|
|
8
|
+
extractAcceptFeatures, extractEncodingFeatures, extractLanguageFeatures,
|
|
9
|
+
extractNetFeatures, extractCHFeatures, extractUAFeatures,
|
|
10
|
+
computeUAHMAC, computeConfidenceToken,
|
|
11
|
+
loadVAIMetadata,
|
|
12
|
+
} from './signal-extraction.js';
|
|
7
13
|
|
|
8
14
|
const PAYWALLS_CLOUD_API_HOST = "https://cloud-api.paywalls.net";
|
|
9
15
|
|
|
@@ -91,27 +97,12 @@ const PROXY_HEADER_MAP = [
|
|
|
91
97
|
];
|
|
92
98
|
|
|
93
99
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* from:'headers' — read from incoming request headers (lowercase)
|
|
97
|
-
* from:'cf' — read from request.cf property
|
|
100
|
+
* Set a header only if the value is non-null (extractor returns null for
|
|
101
|
+
* absent inputs → header omitted entirely, not sent as empty string).
|
|
98
102
|
*/
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
{ from: 'headers', src: 'sec-fetch-mode', dest: 'X-PW-Sec-Fetch-Mode' },
|
|
103
|
-
{ from: 'headers', src: 'sec-fetch-site', dest: 'X-PW-Sec-Fetch-Site' },
|
|
104
|
-
// Bundle B: Accept (2 pts)
|
|
105
|
-
{ from: 'headers', src: 'accept', dest: 'X-PW-Accept' },
|
|
106
|
-
{ from: 'headers', src: 'accept-language', dest: 'X-PW-Accept-Language' },
|
|
107
|
-
{ from: 'headers', src: 'accept-encoding', dest: 'X-PW-Accept-Encoding' },
|
|
108
|
-
// Bundle C: Client Hints (2 pts)
|
|
109
|
-
{ from: 'headers', src: 'sec-ch-ua', dest: 'X-PW-Sec-CH-UA' },
|
|
110
|
-
// Bundle D: CF infrastructure (1 pt) — only valid at first-hop CF Worker
|
|
111
|
-
{ from: 'cf', src: 'tlsVersion', dest: 'X-PW-TLS-Version' },
|
|
112
|
-
{ from: 'cf', src: 'httpProtocol', dest: 'X-PW-HTTP-Protocol' },
|
|
113
|
-
{ from: 'cf', src: 'asn', dest: 'X-PW-ASN' },
|
|
114
|
-
];
|
|
103
|
+
function setIfPresent(obj, key, value) {
|
|
104
|
+
if (value != null) obj[key] = value;
|
|
105
|
+
}
|
|
115
106
|
|
|
116
107
|
/**
|
|
117
108
|
* Proxy VAI requests to the cloud-api service (Spec §7).
|
|
@@ -128,10 +119,9 @@ const SIGNAL_HEADER_MAP = [
|
|
|
128
119
|
* - User-Agent, X-Forwarded-For: standard proxy headers
|
|
129
120
|
* - Authorization: publisher API key (§7.4)
|
|
130
121
|
*
|
|
131
|
-
* Human-confidence signal forwarding (§
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* Simple passthrough: present values forwarded, absent values omitted.
|
|
122
|
+
* Human-confidence signal forwarding (§7.2):
|
|
123
|
+
* Uses signal-extraction module to transform raw browser headers into compact
|
|
124
|
+
* RFC 8941 Structured Field Value strings. Absent inputs → null → header omitted.
|
|
135
125
|
*
|
|
136
126
|
* Response passthrough (§7.3):
|
|
137
127
|
* All response headers from cloud-api are returned unchanged — including
|
|
@@ -155,7 +145,7 @@ async function proxyVAIRequest(cfg, request) {
|
|
|
155
145
|
// Build forwarding headers — include everything cloud-api needs
|
|
156
146
|
// for CORS evaluation, domain auth, and request context.
|
|
157
147
|
const forwardHeaders = {
|
|
158
|
-
'User-Agent':
|
|
148
|
+
'User-Agent': sdkUserAgent,
|
|
159
149
|
'Authorization': `Bearer ${cfg.paywallsAPIKey}`
|
|
160
150
|
};
|
|
161
151
|
|
|
@@ -171,16 +161,29 @@ async function proxyVAIRequest(cfg, request) {
|
|
|
171
161
|
if (headers[src]) forwardHeaders[dest] = headers[src];
|
|
172
162
|
}
|
|
173
163
|
|
|
174
|
-
//
|
|
175
|
-
|
|
164
|
+
// Signal protocol version (§7.1)
|
|
165
|
+
forwardHeaders['X-PW-V'] = '2';
|
|
166
|
+
|
|
176
167
|
const cf = request.cf || {};
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
168
|
+
|
|
169
|
+
// Tier 1: kept raw (§6.1)
|
|
170
|
+
setIfPresent(forwardHeaders, 'X-PW-Sec-Fetch-Dest', headers['sec-fetch-dest']);
|
|
171
|
+
setIfPresent(forwardHeaders, 'X-PW-Sec-Fetch-Mode', headers['sec-fetch-mode']);
|
|
172
|
+
setIfPresent(forwardHeaders, 'X-PW-Sec-Fetch-Site', headers['sec-fetch-site']);
|
|
173
|
+
setIfPresent(forwardHeaders, 'X-PW-TLS-Version', cf.tlsVersion != null ? String(cf.tlsVersion) : null);
|
|
174
|
+
setIfPresent(forwardHeaders, 'X-PW-HTTP-Protocol', cf.httpProtocol != null ? String(cf.httpProtocol) : null);
|
|
175
|
+
|
|
176
|
+
// Tier 2: extract features (§6.2)
|
|
177
|
+
setIfPresent(forwardHeaders, 'X-PW-Accept', extractAcceptFeatures(headers['accept']));
|
|
178
|
+
setIfPresent(forwardHeaders, 'X-PW-Enc', extractEncodingFeatures(headers['accept-encoding']));
|
|
179
|
+
setIfPresent(forwardHeaders, 'X-PW-Lang', extractLanguageFeatures(headers['accept-language']));
|
|
180
|
+
setIfPresent(forwardHeaders, 'X-PW-Net', extractNetFeatures(cf.asn));
|
|
181
|
+
setIfPresent(forwardHeaders, 'X-PW-CH', extractCHFeatures(headers['sec-ch-ua'], headers['user-agent']));
|
|
182
|
+
|
|
183
|
+
// Tier 3: UA features + HMAC (§6.3)
|
|
184
|
+
setIfPresent(forwardHeaders, 'X-PW-UA', extractUAFeatures(headers['user-agent']));
|
|
185
|
+
setIfPresent(forwardHeaders, 'X-PW-UA-HMAC', await computeUAHMAC(headers['user-agent'], cfg.vaiUAHmacKey));
|
|
186
|
+
setIfPresent(forwardHeaders, 'X-PW-CT-FP', await computeConfidenceToken(headers['user-agent'], headers['accept-language'], headers['sec-ch-ua']));
|
|
184
187
|
|
|
185
188
|
// Forward request to cloud-api
|
|
186
189
|
const response = await fetch(`${cfg.paywallsAPIHost}${cloudApiPath}`, {
|
|
@@ -417,7 +420,8 @@ async function cloudflare(config = null) {
|
|
|
417
420
|
paywallsAPIHost: env.PAYWALLS_CLOUD_API_HOST || PAYWALLS_CLOUD_API_HOST,
|
|
418
421
|
paywallsAPIKey: env.PAYWALLS_CLOUD_API_KEY,
|
|
419
422
|
paywallsPublisherId: env.PAYWALLS_PUBLISHER_ID,
|
|
420
|
-
vaiPath: env.PAYWALLS_VAI_PATH || '/pw'
|
|
423
|
+
vaiPath: env.PAYWALLS_VAI_PATH || '/pw',
|
|
424
|
+
vaiUAHmacKey: env.VAI_UA_HMAC_KEY || null,
|
|
421
425
|
};
|
|
422
426
|
|
|
423
427
|
// Check if this is a VAI endpoint request and proxy it
|
|
@@ -425,7 +429,11 @@ async function cloudflare(config = null) {
|
|
|
425
429
|
return await proxyVAIRequest(paywallsConfig, request);
|
|
426
430
|
}
|
|
427
431
|
|
|
428
|
-
|
|
432
|
+
// Load agent patterns + VAI metadata in parallel (both self-cache for 1 hour)
|
|
433
|
+
await Promise.all([
|
|
434
|
+
loadAgentPatterns(paywallsConfig),
|
|
435
|
+
loadVAIMetadata(paywallsConfig),
|
|
436
|
+
]);
|
|
429
437
|
|
|
430
438
|
if (await isRecognizedBot(paywallsConfig, request)) {
|
|
431
439
|
const authz = await checkAgentStatus(paywallsConfig, request);
|
|
@@ -449,7 +457,8 @@ async function fastly() {
|
|
|
449
457
|
paywallsAPIHost: config.get('PAYWALLS_CLOUD_API_HOST') || PAYWALLS_CLOUD_API_HOST,
|
|
450
458
|
paywallsAPIKey: config.get('PAYWALLS_API_KEY'),
|
|
451
459
|
paywallsPublisherId: config.get('PAYWALLS_PUBLISHER_ID'),
|
|
452
|
-
vaiPath: config.get('PAYWALLS_VAI_PATH') || '/pw'
|
|
460
|
+
vaiPath: config.get('PAYWALLS_VAI_PATH') || '/pw',
|
|
461
|
+
vaiUAHmacKey: config.get('VAI_UA_HMAC_KEY') || null,
|
|
453
462
|
};
|
|
454
463
|
|
|
455
464
|
// Check if this is a VAI endpoint request and proxy it
|
|
@@ -457,7 +466,11 @@ async function fastly() {
|
|
|
457
466
|
return await proxyVAIRequest(paywallsConfig, request);
|
|
458
467
|
}
|
|
459
468
|
|
|
460
|
-
|
|
469
|
+
// Load agent patterns + VAI metadata in parallel (both self-cache for 1 hour)
|
|
470
|
+
await Promise.all([
|
|
471
|
+
loadAgentPatterns(paywallsConfig),
|
|
472
|
+
loadVAIMetadata(paywallsConfig),
|
|
473
|
+
]);
|
|
461
474
|
|
|
462
475
|
if (await isRecognizedBot(paywallsConfig, request)) {
|
|
463
476
|
const authz = await checkAgentStatus(paywallsConfig, request);
|
|
@@ -531,9 +544,14 @@ async function cloudfront(config) {
|
|
|
531
544
|
paywallsAPIHost: config.PAYWALLS_CLOUD_API_HOST || PAYWALLS_CLOUD_API_HOST,
|
|
532
545
|
paywallsAPIKey: config.PAYWALLS_API_KEY,
|
|
533
546
|
paywallsPublisherId: config.PAYWALLS_PUBLISHER_ID,
|
|
534
|
-
vaiPath: config.PAYWALLS_VAI_PATH || '/pw'
|
|
547
|
+
vaiPath: config.PAYWALLS_VAI_PATH || '/pw',
|
|
548
|
+
vaiUAHmacKey: config.VAI_UA_HMAC_KEY || null,
|
|
535
549
|
};
|
|
536
|
-
|
|
550
|
+
// Load agent patterns + VAI metadata in parallel (both self-cache for 1 hour)
|
|
551
|
+
await Promise.all([
|
|
552
|
+
loadAgentPatterns(paywallsConfig),
|
|
553
|
+
loadVAIMetadata(paywallsConfig),
|
|
554
|
+
]);
|
|
537
555
|
|
|
538
556
|
return async function handle(event, ctx) {
|
|
539
557
|
let request = event.Records[0].cf.request;
|