@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.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/package.json +1 -1
  3. package/src/index.js +100 -16
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # paywalls-net/filter
2
2
 
3
- SDK for integrating paywalls.net authorization services with CDN or edge environments.
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
@@ -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.2.1",
6
+ "version": "1.2.3",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
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
- for (const [key, value] of request.headers.entries()) {
15
- headers[key] = value;
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
- // check if the URL has a query parameter to always test as a bot
128
- const url = new URL(request.url);
129
- const uaParam = url.searchParams.get("user-agent");
130
- return uaParam && uaParam.includes("bot");
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.get('PAYWALLS_CLOUD_API_HOST'),
248
- paywallsAPIKey: config.get('PAYWALLS_API_KEY'),
249
- paywallsPublisherId: config.get('PAYWALLS_PUBLISHER_ID')
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(request) {
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
- await logAccess(paywallsConfig, request, authz);
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 = {}) {