@paywalls-net/filter 1.3.8 → 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.8",
6
+ "version": "1.3.9",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
package/src/index.js CHANGED
@@ -77,6 +77,42 @@ function isVAIRequest(request, vaiPath = '/pw') {
77
77
  }
78
78
  }
79
79
 
80
+ /**
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
+
80
116
  /**
81
117
  * Proxy VAI requests to the cloud-api service (Spec §7).
82
118
  *
@@ -92,6 +128,11 @@ function isVAIRequest(request, vaiPath = '/pw') {
92
128
  * - User-Agent, X-Forwarded-For: standard proxy headers
93
129
  * - Authorization: publisher API key (§7.4)
94
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
+ *
95
136
  * Response passthrough (§7.3):
96
137
  * All response headers from cloud-api are returned unchanged — including
97
138
  * Access-Control-*, Vary, Cache-Control. The proxy never injects or
@@ -118,40 +159,29 @@ async function proxyVAIRequest(cfg, request) {
118
159
  'Authorization': `Bearer ${cfg.paywallsAPIKey}`
119
160
  };
120
161
 
121
- // Client IP forwarding
162
+ // Client IP forwarding — dual-source, so handled explicitly
122
163
  if (headers['x-forwarded-for']) {
123
164
  forwardHeaders['X-Forwarded-For'] = headers['x-forwarded-for'];
124
165
  } else if (headers['cf-connecting-ip']) {
125
166
  forwardHeaders['X-Forwarded-For'] = headers['cf-connecting-ip'];
126
167
  }
127
-
128
- // Publisher hostname for domain binding (§7.2, §4)
129
- // Cloud-api gates reading this on the vai_forwarded_host feature flag.
130
- if (headers['host']) {
131
- forwardHeaders['X-Original-Host'] = headers['host'];
132
- }
133
-
134
- // Forward browser Origin via custom header for CORS evaluation (§5, §7.2).
135
- // Cloudflare Workers runtime controls the outbound Origin header on fetch(),
136
- // so we relay the browser's Origin via X-Forwarded-Origin. Cloud-api's
137
- // evaluateCORS() reads this to make the authoritative CORS decision.
138
- if (headers['origin']) {
139
- forwardHeaders['X-Forwarded-Origin'] = headers['origin'];
140
- }
141
-
142
- // Forward preflight headers so cloud-api can evaluate OPTIONS (§5.4, §7.2)
143
- if (headers['access-control-request-method']) {
144
- forwardHeaders['Access-Control-Request-Method'] = headers['access-control-request-method'];
145
- }
146
- if (headers['access-control-request-headers']) {
147
- forwardHeaders['Access-Control-Request-Headers'] = headers['access-control-request-headers'];
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];
148
172
  }
149
-
150
- // Forward cookies for session/identity context7.2)
151
- if (headers['cookie']) {
152
- forwardHeaders['Cookie'] = headers['cookie'];
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
+ }
153
183
  }
154
-
184
+
155
185
  // Forward request to cloud-api
156
186
  const response = await fetch(`${cfg.paywallsAPIHost}${cloudApiPath}`, {
157
187
  method: request.method || 'GET',
@@ -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
  };