@paywalls-net/filter 1.2.1 → 1.2.3
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/README.md +3 -2
- package/package.json +1 -1
- package/src/index.js +100 -16
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# paywalls-net/filter
|
|
2
2
|
|
|
3
|
-
SDK for
|
|
3
|
+
SDK for paywalls.net authorization and real-time licensing services. For use with CDN or edge environments.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -10,10 +10,11 @@ npm install @paywalls-net/filter
|
|
|
10
10
|
|
|
11
11
|
## Environment Variables
|
|
12
12
|
- `PAYWALLS_PUBLISHER_ID`: The unique identifier for the publisher using paywalls.net services.
|
|
13
|
-
- `PAYWALLS_CLOUD_API_HOST`: The host for the paywalls.net API. eg `https://cloud-api.paywalls.net`.
|
|
14
13
|
- `PAYWALLS_CLOUD_API_KEY`: The API key for accessing paywalls.net services. NOTE: This key should be treated like a password and kept secret and stored in a secure secrets vault or environment variable.
|
|
15
14
|
|
|
16
15
|
## Usage
|
|
16
|
+
The following is an example of using the SDK with Cloudflare Workers:
|
|
17
|
+
|
|
17
18
|
```javascript
|
|
18
19
|
import { init } from '@paywalls-net/filter';
|
|
19
20
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -2,18 +2,56 @@
|
|
|
2
2
|
* Example publisher-hosted client code for a Cloudflare Worker that
|
|
3
3
|
* filters bot-like requests by using paywalls.net authorization services.
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
const sdk_version = "1.2.x";
|
|
6
6
|
import { classifyUserAgent, loadAgentPatterns } from './user-agent-classification.js';
|
|
7
7
|
|
|
8
|
+
const PAYWALLS_CLOUD_API_HOST = "https://cloud-api.paywalls.net";
|
|
9
|
+
|
|
10
|
+
function detectRuntime() {
|
|
11
|
+
if (typeof process !== "undefined" && process.versions && process.versions.node) {
|
|
12
|
+
return `Node.js/${process.versions.node}`;
|
|
13
|
+
} else if (typeof navigator !== "undefined" && navigator.userAgent) {
|
|
14
|
+
return `Browser/${navigator.userAgent}`;
|
|
15
|
+
} else if (typeof globalThis !== "undefined" && globalThis.Deno && Deno.version) {
|
|
16
|
+
return `Deno/${Deno.version.deno}`;
|
|
17
|
+
} else if (typeof globalThis !== "undefined" && globalThis.Bun && Bun.version) {
|
|
18
|
+
return `Bun/${Bun.version}`;
|
|
19
|
+
}
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function detectFetchVersion() {
|
|
24
|
+
if (typeof fetch !== "undefined" && fetch.name) {
|
|
25
|
+
return fetch.name;
|
|
26
|
+
} else if (typeof globalThis !== "undefined" && globalThis.fetch) {
|
|
27
|
+
return "native";
|
|
28
|
+
} else {
|
|
29
|
+
return "unavailable";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let runtime = detectRuntime();
|
|
34
|
+
let fetchVersion = detectFetchVersion();
|
|
35
|
+
const sdkUserAgent = `pw-filter-sdk/${sdk_version} (${runtime}; fetch/${fetchVersion})`;
|
|
36
|
+
|
|
8
37
|
async function logAccess(cfg, request, access) {
|
|
9
38
|
// Separate html from the status in the access object.
|
|
10
39
|
const { response, ...status } = access;
|
|
11
40
|
|
|
12
41
|
// Get all headers as a plain object (name-value pairs)
|
|
13
42
|
let headers = {};
|
|
14
|
-
|
|
15
|
-
|
|
43
|
+
if (typeof request.headers.entries === "function") {
|
|
44
|
+
// Standard Headers object (e.g., in Cloudflare Workers)
|
|
45
|
+
for (const [key, value] of request.headers.entries()) {
|
|
46
|
+
headers[key] = value;
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
// CloudFront headers object
|
|
50
|
+
for (const key in request.headers) {
|
|
51
|
+
headers[key] = request.headers[key][0]?.value || "";
|
|
52
|
+
}
|
|
16
53
|
}
|
|
54
|
+
|
|
17
55
|
const url = new URL(request.url);
|
|
18
56
|
let body = {
|
|
19
57
|
account_id: cfg.paywallsPublisherId,
|
|
@@ -29,6 +67,7 @@ async function logAccess(cfg, request, access) {
|
|
|
29
67
|
method: "POST",
|
|
30
68
|
headers: {
|
|
31
69
|
"Content-Type": "application/json",
|
|
70
|
+
"User-Agent": sdkUserAgent,
|
|
32
71
|
Authorization: `Bearer ${cfg.paywallsAPIKey}`
|
|
33
72
|
},
|
|
34
73
|
body: JSON.stringify(body)
|
|
@@ -82,6 +121,7 @@ async function checkAgentStatus(cfg, request) {
|
|
|
82
121
|
method: "POST",
|
|
83
122
|
headers: {
|
|
84
123
|
"Content-Type": "application/json",
|
|
124
|
+
"User-Agent": sdkUserAgent,
|
|
85
125
|
Authorization: `Bearer ${cfg.paywallsAPIKey}`
|
|
86
126
|
},
|
|
87
127
|
body: body
|
|
@@ -124,10 +164,14 @@ function isCloudflareKnownBot(request) {
|
|
|
124
164
|
}
|
|
125
165
|
|
|
126
166
|
function isTestBot(request) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
167
|
+
try {
|
|
168
|
+
// check if the URL has a query parameter to always test as a bot
|
|
169
|
+
const url = new URL(request.url || request.uri);
|
|
170
|
+
const uaParam = url.searchParams.get("user-agent");
|
|
171
|
+
return uaParam && uaParam.includes("bot");
|
|
172
|
+
} catch (err) {
|
|
173
|
+
throw new Error(`test bot failed: ${request.url} | ${request.uri} | ${err.message}`);
|
|
174
|
+
}
|
|
131
175
|
}
|
|
132
176
|
async function isPaywallsKnownBot(cfg, request) {
|
|
133
177
|
const userAgent = request.headers.get("User-Agent");
|
|
@@ -140,6 +184,11 @@ async function isRecognizedBot(cfg, request) {
|
|
|
140
184
|
}
|
|
141
185
|
|
|
142
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Create response in format used by most CDNs
|
|
189
|
+
* @param {*} authz
|
|
190
|
+
* @returns
|
|
191
|
+
*/
|
|
143
192
|
function setHeaders(authz) {
|
|
144
193
|
let headers = {
|
|
145
194
|
"Content-Type": "text/html",
|
|
@@ -157,6 +206,11 @@ function setHeaders(authz) {
|
|
|
157
206
|
});
|
|
158
207
|
}
|
|
159
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Create CloudFront format response
|
|
211
|
+
* @param {*} authz
|
|
212
|
+
* @returns
|
|
213
|
+
*/
|
|
160
214
|
function setCloudFrontHeaders(authz) {
|
|
161
215
|
const headers = {};
|
|
162
216
|
|
|
@@ -200,7 +254,7 @@ async function cloudflare(config = null) {
|
|
|
200
254
|
|
|
201
255
|
return async function handle(request, env, ctx) {
|
|
202
256
|
const paywallsConfig = {
|
|
203
|
-
paywallsAPIHost: env.PAYWALLS_CLOUD_API_HOST,
|
|
257
|
+
paywallsAPIHost: env.PAYWALLS_CLOUD_API_HOST || PAYWALLS_CLOUD_API_HOST,
|
|
204
258
|
paywallsAPIKey: env.PAYWALLS_CLOUD_API_KEY,
|
|
205
259
|
paywallsPublisherId: env.PAYWALLS_PUBLISHER_ID
|
|
206
260
|
};
|
|
@@ -223,7 +277,7 @@ async function cloudflare(config = null) {
|
|
|
223
277
|
|
|
224
278
|
async function fastly(config) {
|
|
225
279
|
const paywallsConfig = {
|
|
226
|
-
paywallsAPIHost: config.get('PAYWALLS_CLOUD_API_HOST'),
|
|
280
|
+
paywallsAPIHost: config.get('PAYWALLS_CLOUD_API_HOST') || PAYWALLS_CLOUD_API_HOST,
|
|
227
281
|
paywallsAPIKey: config.get('PAYWALLS_API_KEY'),
|
|
228
282
|
paywallsPublisherId: config.get('PAYWALLS_PUBLISHER_ID')
|
|
229
283
|
};
|
|
@@ -241,34 +295,64 @@ async function fastly(config) {
|
|
|
241
295
|
}
|
|
242
296
|
};
|
|
243
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Adapt to CloudFront format
|
|
300
|
+
* Lambda@Edge events see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html#lambda-event-structure-request
|
|
301
|
+
* CloudFront events see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-event-structure.html#functions-event-structure-example
|
|
302
|
+
* @param {*} request
|
|
303
|
+
* @returns
|
|
304
|
+
*/
|
|
305
|
+
function requestShim(request) {
|
|
306
|
+
if (!request.headers.get) {
|
|
307
|
+
// add get() to headers object to adapt to CloudFront
|
|
308
|
+
request.headers.get = (name) => {
|
|
309
|
+
const header = request.headers[name.toLowerCase()];
|
|
310
|
+
return header ? header[0].value : null;
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// combine the CloudFront host, request.uri and request.querystring into request.url
|
|
315
|
+
if (!request.url && request.uri) {
|
|
316
|
+
let host = request.headers.get('host');
|
|
317
|
+
request.url = `http://${host}${request.uri}`;
|
|
318
|
+
if (request.querystring) {
|
|
319
|
+
request.url += `?${request.querystring}`;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return request;
|
|
324
|
+
}
|
|
244
325
|
|
|
245
326
|
async function cloudfront(config) {
|
|
246
327
|
const paywallsConfig = {
|
|
247
|
-
paywallsAPIHost: config.
|
|
248
|
-
paywallsAPIKey: config.
|
|
249
|
-
paywallsPublisherId: config.
|
|
328
|
+
paywallsAPIHost: config.PAYWALLS_CLOUD_API_HOST || PAYWALLS_CLOUD_API_HOST,
|
|
329
|
+
paywallsAPIKey: config.PAYWALLS_API_KEY,
|
|
330
|
+
paywallsPublisherId: config.PAYWALLS_PUBLISHER_ID
|
|
250
331
|
};
|
|
251
332
|
await loadAgentPatterns(paywallsConfig);
|
|
252
333
|
|
|
253
|
-
return async function handle(
|
|
334
|
+
return async function handle(event, ctx) {
|
|
335
|
+
let request = event.Records[0].cf.request;
|
|
336
|
+
request = requestShim(request);
|
|
254
337
|
if (await isRecognizedBot(paywallsConfig, request)) {
|
|
255
338
|
const authz = await checkAgentStatus(paywallsConfig, request);
|
|
256
339
|
|
|
257
|
-
|
|
340
|
+
// log the result asynchronously
|
|
341
|
+
ctx.callbackWaitsForEmptyEventLoop = false;
|
|
342
|
+
logAccess(paywallsConfig, request, authz);
|
|
258
343
|
|
|
259
344
|
if (authz.access === 'deny') {
|
|
260
345
|
return setCloudFrontHeaders(authz);
|
|
261
346
|
}
|
|
262
347
|
}
|
|
263
|
-
|
|
264
348
|
};
|
|
265
349
|
}
|
|
266
350
|
|
|
267
351
|
|
|
268
|
-
|
|
269
352
|
/**
|
|
270
353
|
* Initializes the appropriate handler based on the CDN.
|
|
271
354
|
* @param {string} cdn - The name of the CDN (e.g., 'cloudflare', 'fastly', 'cloudfront').
|
|
355
|
+
* @param {Object} [config={}] - Optional configuration object for the CDN handler.
|
|
272
356
|
* @returns {Function} - The handler function for the specified CDN.
|
|
273
357
|
*/
|
|
274
358
|
export async function init(cdn, config = {}) {
|