@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 ADDED
@@ -0,0 +1,7 @@
1
+ export default {
2
+ testEnvironment: 'node',
3
+ roots: ['<rootDir>/tests'],
4
+ testMatch: ['**/*.test.js'],
5
+ // ESM transform: jest uses --experimental-vm-modules via the npm script
6
+ transform: {},
7
+ };
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.9",
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": "echo \"Error: no test specified\" && exit 1"
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
- * Maps browser signal sources X-PW-* forwarding headers (§5.2).
95
- * Each entry: { from: 'headers'|'cf', src: string, dest: string }
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
- const SIGNAL_HEADER_MAP = [
100
- // Bundle A: Sec-Fetch (3 pts)
101
- { from: 'headers', src: 'sec-fetch-dest', dest: 'X-PW-Sec-Fetch-Dest' },
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 (§5.2):
132
- * Driven by SIGNAL_HEADER_MAP each entry specifies a source ('headers' or 'cf')
133
- * and property name to read, and the X-PW-* destination header to write.
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': headers['user-agent'] || sdkUserAgent,
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
- // Forward browser-provenance signals as X-PW-* headers 5.2).
175
- // Simple passthrough: forward whatever is present, no cross-request state.
164
+ // Signal protocol version7.1)
165
+ forwardHeaders['X-PW-V'] = '2';
166
+
176
167
  const cf = request.cf || {};
177
- const sources = { headers, cf };
178
- for (const { from, src, dest } of SIGNAL_HEADER_MAP) {
179
- const value = sources[from][src];
180
- if (value != null && value !== '') {
181
- forwardHeaders[dest] = String(value);
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
- await loadAgentPatterns(paywallsConfig);
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
- await loadAgentPatterns(paywallsConfig);
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
- await loadAgentPatterns(paywallsConfig);
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;