@paywalls-net/filter 1.3.7 → 1.3.9

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/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.7",
6
+ "version": "1.3.9",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
package/src/index.js CHANGED
@@ -78,9 +78,65 @@ function isVAIRequest(request, vaiPath = '/pw') {
78
78
  }
79
79
 
80
80
  /**
81
- * Proxy VAI requests to the cloud-api service.
82
- * Proxies the entire request path without endpoint-specific logic,
83
- * allowing new VAI endpoints to work automatically.
81
+ * Clean 1:1 header forwarding map for operational proxy headers (§7.2).
82
+ * Each entry: { src: string (lowercase incoming), dest: string (outgoing) }
83
+ * Headers with fallback logic or multi-source derivation are handled separately.
84
+ */
85
+ const PROXY_HEADER_MAP = [
86
+ { src: 'host', dest: 'X-Original-Host' }, // publisher hostname for domain binding
87
+ { src: 'origin', dest: 'X-Forwarded-Origin' }, // relay for CORS evaluation (§5, §7.2)
88
+ { src: 'access-control-request-method', dest: 'Access-Control-Request-Method' }, // preflight (§5.4)
89
+ { src: 'access-control-request-headers',dest: 'Access-Control-Request-Headers' }, // preflight (§5.4)
90
+ { src: 'cookie', dest: 'Cookie' }, // session/identity context
91
+ ];
92
+
93
+ /**
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
98
+ */
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
+ ];
115
+
116
+ /**
117
+ * Proxy VAI requests to the cloud-api service (Spec §7).
118
+ *
119
+ * Transparent passthrough: cloud-api is authoritative for CORS, domain auth,
120
+ * and security headers. This proxy must NOT inject or modify CORS headers.
121
+ *
122
+ * Header forwarding (§7.2):
123
+ * - X-Forwarded-Origin: browser Origin (Cloudflare Workers control the
124
+ * outbound Origin header, so we relay via a custom header)
125
+ * - X-Original-Host: publisher hostname for domain binding
126
+ * - Access-Control-Request-Method/Headers: for preflight evaluation
127
+ * - Cookie: for session/identity context
128
+ * - User-Agent, X-Forwarded-For: standard proxy headers
129
+ * - Authorization: publisher API key (§7.4)
130
+ *
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.
135
+ *
136
+ * Response passthrough (§7.3):
137
+ * All response headers from cloud-api are returned unchanged — including
138
+ * Access-Control-*, Vary, Cache-Control. The proxy never injects or
139
+ * re-stamps CORS headers.
84
140
  *
85
141
  * @param {Object} cfg - Configuration object with paywallsAPIHost and paywallsAPIKey
86
142
  * @param {Request} request - The incoming request
@@ -96,31 +152,36 @@ async function proxyVAIRequest(cfg, request) {
96
152
  // Get all request headers
97
153
  const headers = getAllHeaders(request);
98
154
 
99
- // Build forwarding headers
155
+ // Build forwarding headers — include everything cloud-api needs
156
+ // for CORS evaluation, domain auth, and request context.
100
157
  const forwardHeaders = {
101
158
  'User-Agent': headers['user-agent'] || sdkUserAgent,
102
159
  'Authorization': `Bearer ${cfg.paywallsAPIKey}`
103
160
  };
104
161
 
105
- // Add forwarding headers if available
162
+ // Client IP forwarding dual-source, so handled explicitly
106
163
  if (headers['x-forwarded-for']) {
107
164
  forwardHeaders['X-Forwarded-For'] = headers['x-forwarded-for'];
108
165
  } else if (headers['cf-connecting-ip']) {
109
166
  forwardHeaders['X-Forwarded-For'] = headers['cf-connecting-ip'];
110
167
  }
111
-
112
- if (headers['host']) {
113
- forwardHeaders['X-Original-Host'] = headers['host'];
168
+
169
+ // Clean 1:1 operational header forwarding (§7.2)
170
+ for (const { src, dest } of PROXY_HEADER_MAP) {
171
+ if (headers[src]) forwardHeaders[dest] = headers[src];
114
172
  }
115
-
116
- // Forward browser Origin via custom header for CORS evaluation (§5).
117
- // Using X-Forwarded-Origin because wrangler dev mangles the standard
118
- // Origin header with port-stacking when proxying between local workers.
119
- const browserOrigin = headers['origin'] || null;
120
- if (browserOrigin) {
121
- forwardHeaders['X-Forwarded-Origin'] = browserOrigin;
173
+
174
+ // Forward browser-provenance signals as X-PW-* headers (§5.2).
175
+ // Simple passthrough: forward whatever is present, no cross-request state.
176
+ 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
+ }
122
183
  }
123
-
184
+
124
185
  // Forward request to cloud-api
125
186
  const response = await fetch(`${cfg.paywallsAPIHost}${cloudApiPath}`, {
126
187
  method: request.method || 'GET',
@@ -131,18 +192,12 @@ async function proxyVAIRequest(cfg, request) {
131
192
  console.error(`VAI proxy error: ${response.status} ${response.statusText}`);
132
193
  }
133
194
 
134
- // Build response, fixing CORS headers that wrangler dev may mangle.
135
- // The cloud-api sets Access-Control-Allow-Origin to the browser's origin
136
- // (via X-Forwarded-Origin), but wrangler can corrupt the value on the
137
- // return path too. Re-stamp it with the captured browser origin.
138
- const responseHeaders = new Headers(response.headers);
139
- if (browserOrigin && responseHeaders.has('Access-Control-Allow-Origin')) {
140
- responseHeaders.set('Access-Control-Allow-Origin', browserOrigin);
141
- }
195
+ // Transparent passthrough (§7.3): return cloud-api response unchanged.
196
+ // Cloud-api is authoritative for CORS headers — do not re-stamp or inject.
142
197
  return new Response(response.body, {
143
198
  status: response.status,
144
199
  statusText: response.statusText,
145
- headers: responseHeaders
200
+ headers: response.headers
146
201
  });
147
202
  } catch (err) {
148
203
  console.error(`Error proxying VAI request: ${err.message}`);
@@ -142,6 +142,8 @@ export async function classifyUserAgent(cfg, userAgent) {
142
142
  const result = {
143
143
  browser,
144
144
  os,
145
+ purpose: 'other',
146
+ purpose_mode: ['other'],
145
147
  vat: 'HUMAN',
146
148
  act: 'ACT-2', // Unmatched UA with detected browser — medium confidence
147
149
  };