@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 +1 -1
- package/src/index.js +80 -25
- package/src/user-agent-classification.js +2 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -78,9 +78,65 @@ function isVAIRequest(request, vaiPath = '/pw') {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
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
|
-
//
|
|
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
|
-
|
|
113
|
-
|
|
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
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
//
|
|
135
|
-
//
|
|
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:
|
|
200
|
+
headers: response.headers
|
|
146
201
|
});
|
|
147
202
|
} catch (err) {
|
|
148
203
|
console.error(`Error proxying VAI request: ${err.message}`);
|