@paywalls-net/filter 1.2.1 → 1.2.2

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 +70 -15
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.2",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
package/src/index.js CHANGED
@@ -5,15 +5,26 @@
5
5
 
6
6
  import { classifyUserAgent, loadAgentPatterns } from './user-agent-classification.js';
7
7
 
8
+ const PAYWALLS_CLOUD_API_HOST = "https://cloud-api.paywalls.net";
9
+
8
10
  async function logAccess(cfg, request, access) {
9
11
  // Separate html from the status in the access object.
10
12
  const { response, ...status } = access;
11
13
 
12
14
  // Get all headers as a plain object (name-value pairs)
13
15
  let headers = {};
14
- for (const [key, value] of request.headers.entries()) {
15
- headers[key] = value;
16
+ if (typeof request.headers.entries === "function") {
17
+ // Standard Headers object (e.g., in Cloudflare Workers)
18
+ for (const [key, value] of request.headers.entries()) {
19
+ headers[key] = value;
20
+ }
21
+ } else {
22
+ // CloudFront headers object
23
+ for (const key in request.headers) {
24
+ headers[key] = request.headers[key][0]?.value || "";
25
+ }
16
26
  }
27
+
17
28
  const url = new URL(request.url);
18
29
  let body = {
19
30
  account_id: cfg.paywallsPublisherId,
@@ -124,10 +135,14 @@ function isCloudflareKnownBot(request) {
124
135
  }
125
136
 
126
137
  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");
138
+ try {
139
+ // check if the URL has a query parameter to always test as a bot
140
+ const url = new URL(request.url || request.uri);
141
+ const uaParam = url.searchParams.get("user-agent");
142
+ return uaParam && uaParam.includes("bot");
143
+ } catch (err) {
144
+ throw new Error(`test bot failed: ${request.url} | ${request.uri} | ${err.message}`);
145
+ }
131
146
  }
132
147
  async function isPaywallsKnownBot(cfg, request) {
133
148
  const userAgent = request.headers.get("User-Agent");
@@ -140,6 +155,11 @@ async function isRecognizedBot(cfg, request) {
140
155
  }
141
156
 
142
157
 
158
+ /**
159
+ * Create response in format used by most CDNs
160
+ * @param {*} authz
161
+ * @returns
162
+ */
143
163
  function setHeaders(authz) {
144
164
  let headers = {
145
165
  "Content-Type": "text/html",
@@ -157,6 +177,11 @@ function setHeaders(authz) {
157
177
  });
158
178
  }
159
179
 
180
+ /**
181
+ * Create CloudFront format response
182
+ * @param {*} authz
183
+ * @returns
184
+ */
160
185
  function setCloudFrontHeaders(authz) {
161
186
  const headers = {};
162
187
 
@@ -200,7 +225,7 @@ async function cloudflare(config = null) {
200
225
 
201
226
  return async function handle(request, env, ctx) {
202
227
  const paywallsConfig = {
203
- paywallsAPIHost: env.PAYWALLS_CLOUD_API_HOST,
228
+ paywallsAPIHost: env.PAYWALLS_CLOUD_API_HOST || PAYWALLS_CLOUD_API_HOST,
204
229
  paywallsAPIKey: env.PAYWALLS_CLOUD_API_KEY,
205
230
  paywallsPublisherId: env.PAYWALLS_PUBLISHER_ID
206
231
  };
@@ -223,7 +248,7 @@ async function cloudflare(config = null) {
223
248
 
224
249
  async function fastly(config) {
225
250
  const paywallsConfig = {
226
- paywallsAPIHost: config.get('PAYWALLS_CLOUD_API_HOST'),
251
+ paywallsAPIHost: config.get('PAYWALLS_CLOUD_API_HOST') || PAYWALLS_CLOUD_API_HOST,
227
252
  paywallsAPIKey: config.get('PAYWALLS_API_KEY'),
228
253
  paywallsPublisherId: config.get('PAYWALLS_PUBLISHER_ID')
229
254
  };
@@ -241,34 +266,64 @@ async function fastly(config) {
241
266
  }
242
267
  };
243
268
  }
269
+ /**
270
+ * Adapt to CloudFront format
271
+ * Lambda@Edge events see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html#lambda-event-structure-request
272
+ * CloudFront events see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-event-structure.html#functions-event-structure-example
273
+ * @param {*} request
274
+ * @returns
275
+ */
276
+ function requestShim(request) {
277
+ if (!request.headers.get) {
278
+ // add get() to headers object to adapt to CloudFront
279
+ request.headers.get = (name) => {
280
+ const header = request.headers[name.toLowerCase()];
281
+ return header ? header[0].value : null;
282
+ };
283
+ }
284
+
285
+ // combine the CloudFront host, request.uri and request.querystring into request.url
286
+ if (!request.url && request.uri) {
287
+ let host = request.headers.get('host');
288
+ request.url = `http://${host}${request.uri}`;
289
+ if (request.querystring) {
290
+ request.url += `?${request.querystring}`;
291
+ }
292
+ }
293
+
294
+ return request;
295
+ }
244
296
 
245
297
  async function cloudfront(config) {
246
298
  const paywallsConfig = {
247
- paywallsAPIHost: config.get('PAYWALLS_CLOUD_API_HOST'),
248
- paywallsAPIKey: config.get('PAYWALLS_API_KEY'),
249
- paywallsPublisherId: config.get('PAYWALLS_PUBLISHER_ID')
299
+ paywallsAPIHost: config.PAYWALLS_CLOUD_API_HOST || PAYWALLS_CLOUD_API_HOST,
300
+ paywallsAPIKey: config.PAYWALLS_API_KEY,
301
+ paywallsPublisherId: config.PAYWALLS_PUBLISHER_ID
250
302
  };
251
303
  await loadAgentPatterns(paywallsConfig);
252
304
 
253
- return async function handle(request) {
305
+ return async function handle(event, ctx) {
306
+ let request = event.Records[0].cf.request;
307
+ request = requestShim(request);
254
308
  if (await isRecognizedBot(paywallsConfig, request)) {
255
309
  const authz = await checkAgentStatus(paywallsConfig, request);
256
310
 
257
- await logAccess(paywallsConfig, request, authz);
311
+ // log the result asynchronously
312
+ ctx.callbackWaitsForEmptyEventLoop = false;
313
+ logAccess(paywallsConfig, request, authz);
258
314
 
259
315
  if (authz.access === 'deny') {
260
316
  return setCloudFrontHeaders(authz);
261
317
  }
262
318
  }
263
-
264
319
  };
265
320
  }
266
321
 
267
322
 
268
-
269
323
  /**
270
324
  * Initializes the appropriate handler based on the CDN.
271
325
  * @param {string} cdn - The name of the CDN (e.g., 'cloudflare', 'fastly', 'cloudfront').
326
+ * @param {Object} [config={}] - Optional configuration object for the CDN handler.
272
327
  * @returns {Function} - The handler function for the specified CDN.
273
328
  */
274
329
  export async function init(cdn, config = {}) {