@zeroad.network/token 0.11.2 → 0.12.0

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 CHANGED
@@ -1,8 +1,8 @@
1
1
  ## Introduction
2
2
 
3
- **@zeroad.network/token** is a module meant to be used by partnering sites of [Zero Ad Network](https://zeroad.network) platform. It's a lightweight module that works on Nodejs 18 and above, and Bun 1.3 and above runtimes.
3
+ **@zeroad.network/token** is a TypeScript ready module meant to be used by partnering sites of [Zero Ad Network](https://zeroad.network) platform. It's a lightweight module that works on Nodejs 18 and above, and Bun 1.3 and above runtimes.
4
4
 
5
- This node module allows a Zero Ad Network program partnering sites and Web APIs to verify determine if incoming web requests are coming from our browser extension users with active subscription.
5
+ This module allows a Zero Ad Network program partnering sites and Web APIs to verify determine if incoming web requests are coming from our browser extension users with active subscription.
6
6
 
7
7
  Their browser extension will send the `X-Better-Web-Hello` Request Header which will let our module to verify it's our actively subscribed user and will allow your site to make a decision whether to disable ads, paywalls or enable access to otherwise paid content of yours.
8
8
 
@@ -20,7 +20,7 @@ In the second step of the Site registration process you'll be presented with you
20
20
 
21
21
  ## Module Installation
22
22
 
23
- This package works well within `mjs` (ESM) and `cjs` (CJS - older node versions) environments.
23
+ As it is written in TypeScript, all types and interfaces are available. Also, this package works well in `mjs` (ESM) and `cjs` (CJS - older node versions) environments. You choose to either use `import` or `require()` statements. Imports are always preferred.
24
24
 
25
25
  Install this package using your favourite package manager:
26
26
 
@@ -28,10 +28,10 @@ Install this package using your favourite package manager:
28
28
  # npm
29
29
  npm add @zeroad.network/token
30
30
 
31
- # yarn
31
+ # or yarn
32
32
  yarn add @zeroad.network/token
33
33
 
34
- # pnpm
34
+ # or pnpm
35
35
  pnpm add @zeroad.network/token
36
36
 
37
37
  # or Bun
@@ -40,60 +40,62 @@ bun add @zeroad.network/token
40
40
 
41
41
  # Examples
42
42
 
43
- Take the example as a reference only. The most basic, and honestly, quite complete use with `express` v.5 could look similar to this:
43
+ Take the simplest JavaScript example as a reference. The most basic, and honestly, quite complete use with `express` v.5 could look similar to this:
44
44
 
45
45
  ```js
46
46
  import express from "express";
47
- import {
48
- init,
49
- getServerHeaderName,
50
- getServerHeaderValue,
51
- processRequest,
52
- getClientHeaderName,
53
- } from "@zeroad.network/token";
47
+ import { ZeroAdNetwork } from "@zeroad.network/token";
54
48
 
55
49
  const app = express();
56
- const port = 3000;
57
50
 
51
+ // Initialize your Zero Ad Network module
58
52
  // Welcome Header Value acquired during Site Registration process at Zero Ad Network platform
59
53
  const ZERO_AD_NETWORK_WELCOME_HEADER_VALUE = "AZqnKU56eIC7vCD1PPlwHg^1^3";
60
-
61
- // Initialize your Zero Ad Network module
62
- init({ value: ZERO_AD_NETWORK_WELCOME_HEADER_VALUE });
54
+ const zeroAd = ZeroAdNetwork(ZERO_AD_NETWORK_WELCOME_HEADER_VALUE);
63
55
 
64
56
  app
65
57
  .use((req, res, next) => {
66
58
  // X-Better-Web-Welcome header injection can could have it's own simple middleware like this:
67
- res.set(getServerHeaderName(), getServerHeaderValue());
59
+ res.set(zeroAd.SERVER_HEADER_NAME, zeroAd.SERVER_HEADER_VALUE);
68
60
 
69
- next();
70
- })
71
- .use((req, res, next) => {
72
- const tokenContext = processRequest(req.get(getClientHeaderName()));
73
- res.locals.tokenContext = tokenContext;
61
+ // Process request token from incoming client token header value.
62
+ // And attach processed token info to request for downstream usage.
63
+ req.tokenContext = zeroAd.parseToken(req.get[zeroAd.CLIENT_HEADER_NAME]);
74
64
 
75
65
  next();
76
66
  })
77
67
  .get("/", (req, res) => {
78
- // The "locals.disableAds" variable can now be used to suppress rendering
79
- // of ads and 3rd party non-functional trackers.
80
-
81
- // If "locals.shouldRemoveAds" value is true, the ads should be disabled in the template.
82
-
83
- // If "locals.shouldEnablePremiumContentAccess" value is true, the access to paywalled content should be enabled. Depending on site subscription pricing, the basic subscription access could be enabled without forcing visitor to pay.
84
-
85
- // If "locals.shouldEnableVipExperience" value is true, the next tier subscription access should be enabled without forcing visitor to pay.
86
- res.render("index.ejs");
68
+ // For example:
69
+ console.log(req.tokenContext);
70
+
71
+ // Will produce the following structure:
72
+ req.tokenContext = {
73
+ // If set to true: Render no advertisements anywhere on the page
74
+ ADS_OFF: boolean,
75
+ // If set to true: Render no Cookie Consent screens (headers, footers or dialogs) on the page with complete OPT-OUT for non-functional trackers
76
+ COOKIE_CONSENT_OFF: boolean,
77
+ // If set to true: Render no marketing dialogs or popups such as newsletter, promotion etc. on the page
78
+ MARKETING_DIALOG_OFF: boolean,
79
+ // If set to true: Provide automatic access to otherwise paywalled content such as articles, news etc.
80
+ CONTENT_PAYWALL_OFF: boolean,
81
+ // If set to true: Provide automatic access to site features provided behind a SaaS at least the basic subscription plan
82
+ SUBSCRIPTION_ACCESS_ON: boolean,
83
+ };
84
+
85
+ // In your template adjust your content depending on tokenContext values
86
+ res.render("index.ejs", { tokenContext });
87
87
  });
88
88
 
89
+ const port = 3000;
90
+
89
91
  app.listen(port, () => {
90
92
  console.log(`Server listening at http://localhost:${port}`);
91
93
  });
92
94
  ```
93
95
 
94
- For more example implementations such as `Express.js`, `Hono` or `Fastify`, please go to [see more examples](https://github.com/laurynas-karvelis/zeroad-token/tree/main/examples/).
96
+ For all example implementations such as `Express.js` (JavaScript), `Hono` and `Fastify` (both are TypeScript), please go to [see more examples](https://github.com/laurynas-karvelis/zeroad-token-ts/tree/main/examples/).
95
97
 
96
- P.S.: Each web request coming from active subscriber using their Zero Ad Network browser extension will incur a tiny fraction of CPU computation cost to verify the token data matches its encrypted signature. On modern web infrastructure a request execution time will increase roughly by ~0.08ms to 0.2ms or so. Mileage might vary, but the impact is minimal.
98
+ P.S.: Each web request coming from active subscriber using their Zero Ad Network browser extension will incur a tiny fraction of CPU computation cost to verify the token data matches its encrypted signature. On modern web infrastructure a request execution time will increase roughly by ~0.06ms to 0.2ms or so. Mileage might vary, but the impact is minimal.
97
99
 
98
100
  # Final thoughts
99
101
 
@@ -1,10 +1,12 @@
1
1
  const ZEROAD_NETWORK_PUBLIC_KEY = "MCowBQYDK2VwAyEAignXRaTQtxEDl4ThULucKNQKEEO2Lo5bEO8qKwjSDVs=";
2
- var SITE_FEATURES = /* @__PURE__ */ ((SITE_FEATURES2) => {
3
- SITE_FEATURES2[SITE_FEATURES2["AD_LESS_EXPERIENCE"] = 1] = "AD_LESS_EXPERIENCE";
4
- SITE_FEATURES2[SITE_FEATURES2["PREMIUM_CONTENT_ACCESS"] = 2] = "PREMIUM_CONTENT_ACCESS";
5
- SITE_FEATURES2[SITE_FEATURES2["VIP_EXPERIENCE"] = 4] = "VIP_EXPERIENCE";
6
- return SITE_FEATURES2;
7
- })(SITE_FEATURES || {});
2
+ var FEATURES = /* @__PURE__ */ ((FEATURES2) => {
3
+ FEATURES2[FEATURES2["ADS_OFF"] = 1] = "ADS_OFF";
4
+ FEATURES2[FEATURES2["COOKIE_CONSENT_OFF"] = 2] = "COOKIE_CONSENT_OFF";
5
+ FEATURES2[FEATURES2["MARKETING_DIALOG_OFF"] = 4] = "MARKETING_DIALOG_OFF";
6
+ FEATURES2[FEATURES2["CONTENT_PAYWALL_OFF"] = 8] = "CONTENT_PAYWALL_OFF";
7
+ FEATURES2[FEATURES2["SUBSCRIPTION_ACCESS_ON"] = 16] = "SUBSCRIPTION_ACCESS_ON";
8
+ return FEATURES2;
9
+ })(FEATURES || {});
8
10
  var SERVER_HEADERS = /* @__PURE__ */ ((SERVER_HEADERS2) => {
9
11
  SERVER_HEADERS2["WELCOME"] = "X-Better-Web-Welcome";
10
12
  return SERVER_HEADERS2;
@@ -19,6 +21,12 @@ var PROTOCOL_VERSION = /* @__PURE__ */ ((PROTOCOL_VERSION2) => {
19
21
  })(PROTOCOL_VERSION || {});
20
22
  const CURRENT_PROTOCOL_VERSION = 1 /* V_1 */;
21
23
 
24
+ let SITE_FEATURES_NATIVE$1;
25
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
26
+ const getSiteFeaturesNative = () => {
27
+ if (SITE_FEATURES_NATIVE$1?.length) return SITE_FEATURES_NATIVE$1;
28
+ return SITE_FEATURES_NATIVE$1 = Object.entries(FEATURES).filter(([key]) => isNaN(Number(key)));
29
+ };
22
30
  const toBase64 = (data) => {
23
31
  if (typeof data.toBase64 === "function") return data.toBase64();
24
32
  if (typeof Buffer !== "undefined") return Buffer.from(data).toString("base64");
@@ -65,15 +73,6 @@ const base64ToUuid = (input) => {
65
73
  hex.slice(20, 20 + 12)
66
74
  ].join("-");
67
75
  };
68
- const unixTimestampToBytes = (date) => {
69
- const buffer = new ArrayBuffer(4);
70
- new DataView(buffer).setUint32(0, Math.floor(date.getTime() / 1e3));
71
- return new Uint8Array(buffer);
72
- };
73
- const bytesToUnixTimestamp = (bytes) => {
74
- const u32int = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0);
75
- return new Date(u32int * 1e3);
76
- };
77
76
  const assert = (value, message) => {
78
77
  if (!value) throw new Error(message);
79
78
  };
@@ -99,15 +98,26 @@ function log(level, ...args) {
99
98
  }
100
99
 
101
100
  const SEPARATOR = "^";
101
+ const SITE_FEATURES_NATIVE = getSiteFeaturesNative();
102
102
  class ServerHeader {
103
- name = SERVER_HEADERS.WELCOME;
104
- value;
103
+ NAME = SERVER_HEADERS.WELCOME.toLowerCase();
104
+ VALUE;
105
105
  constructor(options) {
106
- if ("value" in options) {
107
- this.value = options.value;
108
- } else {
106
+ if (typeof options === "string") {
107
+ if (!options.length) {
108
+ throw new Error("ServerHeader: non-empty welcome header string value must be provided");
109
+ }
110
+ this.VALUE = options;
111
+ } else if (isObject(options) && "siteId" in options && "features" in options) {
109
112
  const { siteId, features } = options;
110
- this.value = this.encode(siteId, features);
113
+ if (typeof siteId !== "string" || !siteId.length || !Array.isArray(features) || !features.length) {
114
+ throw new Error("ServerHeader options must be provided");
115
+ }
116
+ this.VALUE = this.encode(siteId, features);
117
+ } else {
118
+ throw new Error(
119
+ "ServerHeader: non-empty welcome header string value or { siteId, features } options must be provided"
120
+ );
111
121
  }
112
122
  }
113
123
  encode(siteId, features) {
@@ -127,12 +137,14 @@ class ServerHeader {
127
137
  );
128
138
  const siteId = base64ToUuid(encodedSiteId);
129
139
  assert(siteId.length === 36, "Invalid siteId value");
130
- if (PROTOCOL_VERSION.V_1) {
131
- assert([1, 2, 3, 4, 5, 6, 7].includes(Number(flags)), "Invalid flags value");
140
+ assert(Number(flags).toFixed(0).toString() === flags, "Invalid flags number");
141
+ let features = [];
142
+ for (const [feature, shift] of SITE_FEATURES_NATIVE) {
143
+ if (hasFeature(Number(flags), shift)) features.push(feature);
132
144
  }
133
145
  return {
134
146
  version: Number(protocolVersion),
135
- flags: Number(flags),
147
+ features,
136
148
  siteId
137
149
  };
138
150
  } catch (err) {
@@ -141,4 +153,4 @@ class ServerHeader {
141
153
  }
142
154
  }
143
155
 
144
- export { CLIENT_HEADERS as C, PROTOCOL_VERSION as P, SITE_FEATURES as S, ZEROAD_NETWORK_PUBLIC_KEY as Z, ServerHeader as a, bytesToUnixTimestamp as b, setLogLevel as c, SERVER_HEADERS as d, CURRENT_PROTOCOL_VERSION as e, fromBase64 as f, hasFeature as h, log as l, setFeatures as s, toBase64 as t, unixTimestampToBytes as u };
156
+ export { CLIENT_HEADERS as C, FEATURES as F, PROTOCOL_VERSION as P, ServerHeader as S, ZEROAD_NETWORK_PUBLIC_KEY as Z, setLogLevel as a, SERVER_HEADERS as b, CURRENT_PROTOCOL_VERSION as c, fromBase64 as f, getSiteFeaturesNative as g, hasFeature as h, log as l, setFeatures as s, toBase64 as t };
@@ -1,12 +1,14 @@
1
1
  'use strict';
2
2
 
3
3
  const ZEROAD_NETWORK_PUBLIC_KEY = "MCowBQYDK2VwAyEAignXRaTQtxEDl4ThULucKNQKEEO2Lo5bEO8qKwjSDVs=";
4
- var SITE_FEATURES = /* @__PURE__ */ ((SITE_FEATURES2) => {
5
- SITE_FEATURES2[SITE_FEATURES2["AD_LESS_EXPERIENCE"] = 1] = "AD_LESS_EXPERIENCE";
6
- SITE_FEATURES2[SITE_FEATURES2["PREMIUM_CONTENT_ACCESS"] = 2] = "PREMIUM_CONTENT_ACCESS";
7
- SITE_FEATURES2[SITE_FEATURES2["VIP_EXPERIENCE"] = 4] = "VIP_EXPERIENCE";
8
- return SITE_FEATURES2;
9
- })(SITE_FEATURES || {});
4
+ var FEATURES = /* @__PURE__ */ ((FEATURES2) => {
5
+ FEATURES2[FEATURES2["ADS_OFF"] = 1] = "ADS_OFF";
6
+ FEATURES2[FEATURES2["COOKIE_CONSENT_OFF"] = 2] = "COOKIE_CONSENT_OFF";
7
+ FEATURES2[FEATURES2["MARKETING_DIALOG_OFF"] = 4] = "MARKETING_DIALOG_OFF";
8
+ FEATURES2[FEATURES2["CONTENT_PAYWALL_OFF"] = 8] = "CONTENT_PAYWALL_OFF";
9
+ FEATURES2[FEATURES2["SUBSCRIPTION_ACCESS_ON"] = 16] = "SUBSCRIPTION_ACCESS_ON";
10
+ return FEATURES2;
11
+ })(FEATURES || {});
10
12
  var SERVER_HEADERS = /* @__PURE__ */ ((SERVER_HEADERS2) => {
11
13
  SERVER_HEADERS2["WELCOME"] = "X-Better-Web-Welcome";
12
14
  return SERVER_HEADERS2;
@@ -21,6 +23,12 @@ var PROTOCOL_VERSION = /* @__PURE__ */ ((PROTOCOL_VERSION2) => {
21
23
  })(PROTOCOL_VERSION || {});
22
24
  const CURRENT_PROTOCOL_VERSION = 1 /* V_1 */;
23
25
 
26
+ let SITE_FEATURES_NATIVE$1;
27
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
28
+ const getSiteFeaturesNative = () => {
29
+ if (SITE_FEATURES_NATIVE$1?.length) return SITE_FEATURES_NATIVE$1;
30
+ return SITE_FEATURES_NATIVE$1 = Object.entries(FEATURES).filter(([key]) => isNaN(Number(key)));
31
+ };
24
32
  const toBase64 = (data) => {
25
33
  if (typeof data.toBase64 === "function") return data.toBase64();
26
34
  if (typeof Buffer !== "undefined") return Buffer.from(data).toString("base64");
@@ -67,15 +75,6 @@ const base64ToUuid = (input) => {
67
75
  hex.slice(20, 20 + 12)
68
76
  ].join("-");
69
77
  };
70
- const unixTimestampToBytes = (date) => {
71
- const buffer = new ArrayBuffer(4);
72
- new DataView(buffer).setUint32(0, Math.floor(date.getTime() / 1e3));
73
- return new Uint8Array(buffer);
74
- };
75
- const bytesToUnixTimestamp = (bytes) => {
76
- const u32int = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0);
77
- return new Date(u32int * 1e3);
78
- };
79
78
  const assert = (value, message) => {
80
79
  if (!value) throw new Error(message);
81
80
  };
@@ -101,15 +100,26 @@ function log(level, ...args) {
101
100
  }
102
101
 
103
102
  const SEPARATOR = "^";
103
+ const SITE_FEATURES_NATIVE = getSiteFeaturesNative();
104
104
  class ServerHeader {
105
- name = SERVER_HEADERS.WELCOME;
106
- value;
105
+ NAME = SERVER_HEADERS.WELCOME.toLowerCase();
106
+ VALUE;
107
107
  constructor(options) {
108
- if ("value" in options) {
109
- this.value = options.value;
110
- } else {
108
+ if (typeof options === "string") {
109
+ if (!options.length) {
110
+ throw new Error("ServerHeader: non-empty welcome header string value must be provided");
111
+ }
112
+ this.VALUE = options;
113
+ } else if (isObject(options) && "siteId" in options && "features" in options) {
111
114
  const { siteId, features } = options;
112
- this.value = this.encode(siteId, features);
115
+ if (typeof siteId !== "string" || !siteId.length || !Array.isArray(features) || !features.length) {
116
+ throw new Error("ServerHeader options must be provided");
117
+ }
118
+ this.VALUE = this.encode(siteId, features);
119
+ } else {
120
+ throw new Error(
121
+ "ServerHeader: non-empty welcome header string value or { siteId, features } options must be provided"
122
+ );
113
123
  }
114
124
  }
115
125
  encode(siteId, features) {
@@ -129,12 +139,14 @@ class ServerHeader {
129
139
  );
130
140
  const siteId = base64ToUuid(encodedSiteId);
131
141
  assert(siteId.length === 36, "Invalid siteId value");
132
- if (PROTOCOL_VERSION.V_1) {
133
- assert([1, 2, 3, 4, 5, 6, 7].includes(Number(flags)), "Invalid flags value");
142
+ assert(Number(flags).toFixed(0).toString() === flags, "Invalid flags number");
143
+ let features = [];
144
+ for (const [feature, shift] of SITE_FEATURES_NATIVE) {
145
+ if (hasFeature(Number(flags), shift)) features.push(feature);
134
146
  }
135
147
  return {
136
148
  version: Number(protocolVersion),
137
- flags: Number(flags),
149
+ features,
138
150
  siteId
139
151
  };
140
152
  } catch (err) {
@@ -145,16 +157,15 @@ class ServerHeader {
145
157
 
146
158
  exports.CLIENT_HEADERS = CLIENT_HEADERS;
147
159
  exports.CURRENT_PROTOCOL_VERSION = CURRENT_PROTOCOL_VERSION;
160
+ exports.FEATURES = FEATURES;
148
161
  exports.PROTOCOL_VERSION = PROTOCOL_VERSION;
149
162
  exports.SERVER_HEADERS = SERVER_HEADERS;
150
- exports.SITE_FEATURES = SITE_FEATURES;
151
163
  exports.ServerHeader = ServerHeader;
152
164
  exports.ZEROAD_NETWORK_PUBLIC_KEY = ZEROAD_NETWORK_PUBLIC_KEY;
153
- exports.bytesToUnixTimestamp = bytesToUnixTimestamp;
154
165
  exports.fromBase64 = fromBase64;
166
+ exports.getSiteFeaturesNative = getSiteFeaturesNative;
155
167
  exports.hasFeature = hasFeature;
156
168
  exports.log = log;
157
169
  exports.setFeatures = setFeatures;
158
170
  exports.setLogLevel = setLogLevel;
159
171
  exports.toBase64 = toBase64;
160
- exports.unixTimestampToBytes = unixTimestampToBytes;
@@ -3,10 +3,17 @@
3
3
  * Used to verify `X-Better-Web-User` header values are not tampered with.
4
4
  */
5
5
  declare const ZEROAD_NETWORK_PUBLIC_KEY: string;
6
- declare enum SITE_FEATURES {
7
- AD_LESS_EXPERIENCE = 1,
8
- PREMIUM_CONTENT_ACCESS = 2,
9
- VIP_EXPERIENCE = 4
6
+ declare enum FEATURES {
7
+ /** Render no advertisements anywhere on the page */
8
+ ADS_OFF = 1,
9
+ /** Render no Cookie Consent screens (headers, footers or dialogs) on the page with complete OPT-OUT for non-functional trackers */
10
+ COOKIE_CONSENT_OFF = 2,
11
+ /** Render no marketing dialogs or popups such as newsletter, promotion etc. on the page */
12
+ MARKETING_DIALOG_OFF = 4,
13
+ /** Provide automatic access to otherwise paywalled content such as articles, news etc. */
14
+ CONTENT_PAYWALL_OFF = 8,
15
+ /** Provide automatic access to site features provided behind a SaaS at least the basic subscription plan */
16
+ SUBSCRIPTION_ACCESS_ON = 16
10
17
  }
11
18
  type UUID = string;
12
19
  declare enum SERVER_HEADERS {
@@ -19,35 +26,31 @@ declare enum PROTOCOL_VERSION {
19
26
  V_1 = 1
20
27
  }
21
28
  declare const CURRENT_PROTOCOL_VERSION = PROTOCOL_VERSION.V_1;
22
- type ServerHeaderSimpleOptions = {
23
- value: string;
24
- };
25
29
  type ServerHeaderExtendedOptions = {
26
30
  siteId: UUID;
27
- features: SITE_FEATURES[];
31
+ features: FEATURES[];
28
32
  };
29
- type ServerHeaderOptions = ServerHeaderExtendedOptions | ServerHeaderSimpleOptions;
33
+ type ServerHeaderOptions = NonNullable<string | ServerHeaderExtendedOptions>;
30
34
  type WelcomeHeaderParseResult = WelcomeHeader | undefined;
31
35
  type WelcomeHeader = {
32
36
  version: PROTOCOL_VERSION;
37
+ features: (keyof typeof FEATURES)[];
33
38
  siteId: UUID;
34
- flags: number;
35
39
  };
36
40
  type ClientHeaderParseResult = ClientParsedHeader | undefined;
37
41
  type ClientParsedHeader = {
38
42
  version: PROTOCOL_VERSION;
39
43
  expiresAt: Date;
40
- expired: boolean;
41
44
  flags: number;
42
45
  };
43
46
 
44
47
  declare class ServerHeader {
45
- name: SERVER_HEADERS;
46
- value: string;
48
+ NAME: string;
49
+ VALUE: string;
47
50
  constructor(options: ServerHeaderOptions);
48
- encode(siteId: UUID, features: SITE_FEATURES[]): string;
51
+ encode(siteId: UUID, features: FEATURES[]): string;
49
52
  static decode(headerValue: string | undefined): WelcomeHeaderParseResult;
50
53
  }
51
54
 
52
- export { CLIENT_HEADERS, CURRENT_PROTOCOL_VERSION, PROTOCOL_VERSION, SERVER_HEADERS, SITE_FEATURES, ServerHeader, ZEROAD_NETWORK_PUBLIC_KEY };
53
- export type { ClientHeaderParseResult, ClientParsedHeader, ServerHeaderExtendedOptions, ServerHeaderOptions, ServerHeaderSimpleOptions, UUID, WelcomeHeader, WelcomeHeaderParseResult };
55
+ export { CLIENT_HEADERS, CURRENT_PROTOCOL_VERSION, FEATURES, PROTOCOL_VERSION, SERVER_HEADERS, ServerHeader, ZEROAD_NETWORK_PUBLIC_KEY };
56
+ export type { ClientHeaderParseResult, ClientParsedHeader, ServerHeaderExtendedOptions, ServerHeaderOptions, UUID, WelcomeHeader, WelcomeHeaderParseResult };
@@ -3,10 +3,17 @@
3
3
  * Used to verify `X-Better-Web-User` header values are not tampered with.
4
4
  */
5
5
  declare const ZEROAD_NETWORK_PUBLIC_KEY: string;
6
- declare enum SITE_FEATURES {
7
- AD_LESS_EXPERIENCE = 1,
8
- PREMIUM_CONTENT_ACCESS = 2,
9
- VIP_EXPERIENCE = 4
6
+ declare enum FEATURES {
7
+ /** Render no advertisements anywhere on the page */
8
+ ADS_OFF = 1,
9
+ /** Render no Cookie Consent screens (headers, footers or dialogs) on the page with complete OPT-OUT for non-functional trackers */
10
+ COOKIE_CONSENT_OFF = 2,
11
+ /** Render no marketing dialogs or popups such as newsletter, promotion etc. on the page */
12
+ MARKETING_DIALOG_OFF = 4,
13
+ /** Provide automatic access to otherwise paywalled content such as articles, news etc. */
14
+ CONTENT_PAYWALL_OFF = 8,
15
+ /** Provide automatic access to site features provided behind a SaaS at least the basic subscription plan */
16
+ SUBSCRIPTION_ACCESS_ON = 16
10
17
  }
11
18
  type UUID = string;
12
19
  declare enum SERVER_HEADERS {
@@ -19,35 +26,31 @@ declare enum PROTOCOL_VERSION {
19
26
  V_1 = 1
20
27
  }
21
28
  declare const CURRENT_PROTOCOL_VERSION = PROTOCOL_VERSION.V_1;
22
- type ServerHeaderSimpleOptions = {
23
- value: string;
24
- };
25
29
  type ServerHeaderExtendedOptions = {
26
30
  siteId: UUID;
27
- features: SITE_FEATURES[];
31
+ features: FEATURES[];
28
32
  };
29
- type ServerHeaderOptions = ServerHeaderExtendedOptions | ServerHeaderSimpleOptions;
33
+ type ServerHeaderOptions = NonNullable<string | ServerHeaderExtendedOptions>;
30
34
  type WelcomeHeaderParseResult = WelcomeHeader | undefined;
31
35
  type WelcomeHeader = {
32
36
  version: PROTOCOL_VERSION;
37
+ features: (keyof typeof FEATURES)[];
33
38
  siteId: UUID;
34
- flags: number;
35
39
  };
36
40
  type ClientHeaderParseResult = ClientParsedHeader | undefined;
37
41
  type ClientParsedHeader = {
38
42
  version: PROTOCOL_VERSION;
39
43
  expiresAt: Date;
40
- expired: boolean;
41
44
  flags: number;
42
45
  };
43
46
 
44
47
  declare class ServerHeader {
45
- name: SERVER_HEADERS;
46
- value: string;
48
+ NAME: string;
49
+ VALUE: string;
47
50
  constructor(options: ServerHeaderOptions);
48
- encode(siteId: UUID, features: SITE_FEATURES[]): string;
51
+ encode(siteId: UUID, features: FEATURES[]): string;
49
52
  static decode(headerValue: string | undefined): WelcomeHeaderParseResult;
50
53
  }
51
54
 
52
- export { CLIENT_HEADERS, CURRENT_PROTOCOL_VERSION, PROTOCOL_VERSION, SERVER_HEADERS, SITE_FEATURES, ServerHeader, ZEROAD_NETWORK_PUBLIC_KEY };
53
- export type { ClientHeaderParseResult, ClientParsedHeader, ServerHeaderExtendedOptions, ServerHeaderOptions, ServerHeaderSimpleOptions, UUID, WelcomeHeader, WelcomeHeaderParseResult };
55
+ export { CLIENT_HEADERS, CURRENT_PROTOCOL_VERSION, FEATURES, PROTOCOL_VERSION, SERVER_HEADERS, ServerHeader, ZEROAD_NETWORK_PUBLIC_KEY };
56
+ export type { ClientHeaderParseResult, ClientParsedHeader, ServerHeaderExtendedOptions, ServerHeaderOptions, UUID, WelcomeHeader, WelcomeHeaderParseResult };
package/dist/browser.mjs CHANGED
@@ -1 +1 @@
1
- export { C as CLIENT_HEADERS, e as CURRENT_PROTOCOL_VERSION, P as PROTOCOL_VERSION, d as SERVER_HEADERS, S as SITE_FEATURES, a as ServerHeader, Z as ZEROAD_NETWORK_PUBLIC_KEY } from './browser-CEwtQQBM.mjs';
1
+ export { C as CLIENT_HEADERS, c as CURRENT_PROTOCOL_VERSION, F as FEATURES, P as PROTOCOL_VERSION, b as SERVER_HEADERS, S as ServerHeader, Z as ZEROAD_NETWORK_PUBLIC_KEY } from './browser-ByaVwJr6.mjs';
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var browser = require('./browser-DhviIs25.cjs');
3
+ var browser = require('./browser-C50SBRBw.cjs');
4
4
  var crypto = require('crypto');
5
5
 
6
6
  const importPrivateKey = (privateKeyBase64) => {
@@ -30,30 +30,40 @@ const verify = (data, signature, publicKey) => {
30
30
  };
31
31
  const nonce = (size) => new Uint8Array(crypto.randomBytes(size));
32
32
 
33
+ const VERSION_BYTES = 1;
33
34
  const NONCE_BYTES = 4;
34
35
  const SEPARATOR = ".";
36
+ const SITE_FEATURES_NATIVE = browser.getSiteFeaturesNative();
37
+ const as32BitNumber = (byteArray, begin) => {
38
+ const bytes = byteArray.subarray(begin, begin + Uint32Array.BYTES_PER_ELEMENT);
39
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
40
+ return view.getUint32(0, true);
41
+ };
35
42
  class ClientHeader {
36
43
  cryptoPublicKey;
37
44
  cryptoPrivateKey;
38
45
  publicKey;
39
46
  privateKey;
40
- name = browser.CLIENT_HEADERS.HELLO;
47
+ NAME = browser.CLIENT_HEADERS.HELLO.toLowerCase();
41
48
  constructor(publicKey, privateKey) {
42
49
  this.publicKey = publicKey;
43
50
  this.privateKey = privateKey;
44
51
  }
45
- encode(version, expiresAt, features) {
46
- if (!this.privateKey) throw new Error("Private key is required");
47
- const flags = browser.setFeatures(0, features);
48
- const data = mergeByteArrays([
49
- new Uint8Array([version]),
50
- nonce(NONCE_BYTES),
51
- browser.unixTimestampToBytes(expiresAt),
52
- new Uint8Array([flags])
53
- ]);
54
- if (!this.cryptoPrivateKey) this.cryptoPrivateKey = importPrivateKey(this.privateKey);
55
- const signature = sign(data.buffer, this.cryptoPrivateKey);
56
- return [browser.toBase64(data), browser.toBase64(new Uint8Array(signature))].join(SEPARATOR);
52
+ parseToken(headerValue) {
53
+ const headerValueAsString = Array.isArray(headerValue) ? headerValue[0] : headerValue;
54
+ const data = this.decode(headerValueAsString);
55
+ const expired = !data || data.expiresAt.getTime() < Date.now();
56
+ const result = {};
57
+ if (!data || expired) {
58
+ for (const [feature] of SITE_FEATURES_NATIVE) {
59
+ result[feature] = false;
60
+ }
61
+ return result;
62
+ }
63
+ for (const [feature, shift] of SITE_FEATURES_NATIVE) {
64
+ result[feature] = browser.hasFeature(data.flags, shift);
65
+ }
66
+ return result;
57
67
  }
58
68
  decode(headerValue) {
59
69
  if (!headerValue?.length) return;
@@ -67,76 +77,60 @@ class ClientHeader {
67
77
  }
68
78
  const version = dataBytes[0];
69
79
  if (version === browser.PROTOCOL_VERSION.V_1) {
70
- const flagsBytes = dataBytes.subarray(dataBytes.length - 1, dataBytes.length);
71
- const expiresAtBytes = dataBytes.subarray(5, 9);
72
- const expiresAt = browser.bytesToUnixTimestamp(expiresAtBytes);
73
- const expired = expiresAt.getTime() < Date.now();
74
- return { version, expiresAt, expired, flags: flagsBytes[0] };
80
+ const expiresAt = as32BitNumber(dataBytes, VERSION_BYTES + NONCE_BYTES);
81
+ const flags = as32BitNumber(dataBytes, dataBytes.length - Uint32Array.BYTES_PER_ELEMENT);
82
+ return { version, expiresAt: new Date(expiresAt * 1e3), flags };
75
83
  }
76
84
  } catch (err) {
77
85
  browser.log("warn", "Could not decode client header value", { reason: err?.message });
78
86
  }
79
87
  }
80
- processRequest(headerValue) {
81
- const data = this.decode(headerValue);
82
- return {
83
- _raw: data,
84
- get shouldRemoveAds() {
85
- return shouldRemoveAds(data);
86
- },
87
- get shouldEnablePremiumContentAccess() {
88
- return shouldEnablePremiumContentAccess(data);
89
- },
90
- get shouldEnableVipExperience() {
91
- return shouldEnableVipExperience(data);
92
- }
93
- };
88
+ encode(version, expiresAt, features) {
89
+ if (!this.privateKey) throw new Error("Private key is required");
90
+ const data = mergeByteArrays([
91
+ new Uint8Array([version]),
92
+ new Uint8Array(nonce(NONCE_BYTES)),
93
+ new Uint32Array([Math.floor(expiresAt.getTime() / 1e3)]),
94
+ new Uint32Array([browser.setFeatures(0, features)])
95
+ ]);
96
+ if (!this.cryptoPrivateKey) this.cryptoPrivateKey = importPrivateKey(this.privateKey);
97
+ const signature = sign(data.buffer, this.cryptoPrivateKey);
98
+ return [browser.toBase64(data), browser.toBase64(new Uint8Array(signature))].join(SEPARATOR);
94
99
  }
95
100
  }
96
- const test = (data, feature) => {
97
- return data && !data?.expired && browser.hasFeature(data?.flags, feature) || false;
98
- };
99
- const shouldRemoveAds = (data) => test(data, browser.SITE_FEATURES.AD_LESS_EXPERIENCE);
100
- const shouldEnableVipExperience = (data) => test(data, browser.SITE_FEATURES.VIP_EXPERIENCE);
101
- const shouldEnablePremiumContentAccess = (data) => test(data, browser.SITE_FEATURES.PREMIUM_CONTENT_ACCESS);
102
101
  const mergeByteArrays = (arrays) => {
103
- const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
102
+ const totalLength = arrays.reduce((sum, a) => sum + a.byteLength, 0);
104
103
  const data = new Uint8Array(totalLength);
105
104
  let offset = 0;
106
105
  for (const arr of arrays) {
107
- data.set(arr, offset);
108
- offset += arr.length;
106
+ let bytes;
107
+ if (arr instanceof Uint8Array) bytes = arr;
108
+ else if (arr instanceof Uint32Array) bytes = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
109
+ else throw new Error("Unsupported type");
110
+ data.set(bytes, offset);
111
+ offset += bytes.byteLength;
109
112
  }
110
113
  return data;
111
114
  };
112
115
 
113
- class Site {
114
- serverHeader;
115
- clientHeader;
116
- constructor(options) {
117
- this.serverHeader = new browser.ServerHeader(options);
118
- this.clientHeader = new ClientHeader(browser.ZEROAD_NETWORK_PUBLIC_KEY);
119
- }
116
+ function ZeroAdNetwork(options) {
117
+ const serverHeader = new browser.ServerHeader(options);
118
+ const clientHeader = new ClientHeader(browser.ZEROAD_NETWORK_PUBLIC_KEY);
119
+ return {
120
+ setLogLevel: browser.setLogLevel,
121
+ parseToken: clientHeader.parseToken.bind(clientHeader),
122
+ CLIENT_HEADER_NAME: clientHeader.NAME,
123
+ SERVER_HEADER_NAME: serverHeader.NAME,
124
+ SERVER_HEADER_VALUE: serverHeader.VALUE
125
+ };
120
126
  }
121
- let defaultSite;
122
- const init = (options) => defaultSite = new Site(options);
123
- const processRequest = (headerValue) => defaultSite.clientHeader.processRequest(headerValue);
124
- const getClientHeaderName = () => defaultSite.clientHeader.name;
125
- const getServerHeaderName = () => defaultSite.serverHeader.name;
126
- const getServerHeaderValue = () => defaultSite.serverHeader.value;
127
127
 
128
128
  exports.CLIENT_HEADERS = browser.CLIENT_HEADERS;
129
129
  exports.CURRENT_PROTOCOL_VERSION = browser.CURRENT_PROTOCOL_VERSION;
130
+ exports.FEATURES = browser.FEATURES;
130
131
  exports.PROTOCOL_VERSION = browser.PROTOCOL_VERSION;
131
132
  exports.SERVER_HEADERS = browser.SERVER_HEADERS;
132
- exports.SITE_FEATURES = browser.SITE_FEATURES;
133
133
  exports.ServerHeader = browser.ServerHeader;
134
134
  exports.ZEROAD_NETWORK_PUBLIC_KEY = browser.ZEROAD_NETWORK_PUBLIC_KEY;
135
- exports.setLogLevel = browser.setLogLevel;
136
135
  exports.ClientHeader = ClientHeader;
137
- exports.Site = Site;
138
- exports.getClientHeaderName = getClientHeaderName;
139
- exports.getServerHeaderName = getServerHeaderName;
140
- exports.getServerHeaderValue = getServerHeaderValue;
141
- exports.init = init;
142
- exports.processRequest = processRequest;
136
+ exports.ZeroAdNetwork = ZeroAdNetwork;
package/dist/index.d.cts CHANGED
@@ -1,40 +1,27 @@
1
- import { CLIENT_HEADERS, PROTOCOL_VERSION, SITE_FEATURES, ClientHeaderParseResult, ServerHeader, ServerHeaderOptions, SERVER_HEADERS } from './browser.cjs';
2
- export { CURRENT_PROTOCOL_VERSION, ClientParsedHeader, ServerHeaderExtendedOptions, ServerHeaderSimpleOptions, UUID, WelcomeHeader, WelcomeHeaderParseResult, ZEROAD_NETWORK_PUBLIC_KEY } from './browser.cjs';
1
+ import { ClientHeaderParseResult, PROTOCOL_VERSION, FEATURES, ServerHeaderOptions } from './browser.cjs';
2
+ export { CLIENT_HEADERS, CURRENT_PROTOCOL_VERSION, ClientParsedHeader, SERVER_HEADERS, ServerHeader, ServerHeaderExtendedOptions, UUID, WelcomeHeader, WelcomeHeaderParseResult, ZEROAD_NETWORK_PUBLIC_KEY } from './browser.cjs';
3
3
 
4
4
  declare class ClientHeader {
5
5
  private cryptoPublicKey;
6
6
  private cryptoPrivateKey;
7
7
  private publicKey;
8
8
  private privateKey;
9
- name: CLIENT_HEADERS;
9
+ NAME: string;
10
10
  constructor(publicKey: string, privateKey?: string);
11
- encode(version: PROTOCOL_VERSION, expiresAt: Date, features: SITE_FEATURES[]): string;
12
- decode(headerValue: string): ClientHeaderParseResult;
13
- processRequest(headerValue: string | undefined): {
14
- _raw: ClientHeaderParseResult;
15
- readonly shouldRemoveAds: boolean;
16
- readonly shouldEnablePremiumContentAccess: boolean;
17
- readonly shouldEnableVipExperience: boolean;
18
- };
11
+ parseToken(headerValue: string | string[] | undefined): Record<"ADS_OFF" | "COOKIE_CONSENT_OFF" | "MARKETING_DIALOG_OFF" | "CONTENT_PAYWALL_OFF" | "SUBSCRIPTION_ACCESS_ON", boolean>;
12
+ decode(headerValue: string | undefined): ClientHeaderParseResult;
13
+ encode(version: PROTOCOL_VERSION, expiresAt: Date, features: FEATURES[]): string;
19
14
  }
20
15
 
21
16
  type LogLevel = "error" | "warn" | "info" | "debug";
22
17
  declare function setLogLevel(level: LogLevel): void;
23
18
 
24
- declare class Site {
25
- serverHeader: ServerHeader;
26
- clientHeader: ClientHeader;
27
- constructor(options: ServerHeaderOptions);
28
- }
29
- declare const init: (options: ServerHeaderOptions) => Site;
30
- declare const processRequest: (headerValue: string | undefined) => {
31
- _raw: ClientHeaderParseResult;
32
- readonly shouldRemoveAds: boolean;
33
- readonly shouldEnablePremiumContentAccess: boolean;
34
- readonly shouldEnableVipExperience: boolean;
19
+ declare function ZeroAdNetwork(options: ServerHeaderOptions): {
20
+ setLogLevel: typeof setLogLevel;
21
+ parseToken: (headerValue: string | string[] | undefined) => Record<"ADS_OFF" | "COOKIE_CONSENT_OFF" | "MARKETING_DIALOG_OFF" | "CONTENT_PAYWALL_OFF" | "SUBSCRIPTION_ACCESS_ON", boolean>;
22
+ CLIENT_HEADER_NAME: string;
23
+ SERVER_HEADER_NAME: string;
24
+ SERVER_HEADER_VALUE: string;
35
25
  };
36
- declare const getClientHeaderName: () => CLIENT_HEADERS;
37
- declare const getServerHeaderName: () => SERVER_HEADERS;
38
- declare const getServerHeaderValue: () => string;
39
26
 
40
- export { CLIENT_HEADERS, ClientHeader, ClientHeaderParseResult, PROTOCOL_VERSION, SERVER_HEADERS, SITE_FEATURES, ServerHeader, ServerHeaderOptions, Site, getClientHeaderName, getServerHeaderName, getServerHeaderValue, init, processRequest, setLogLevel };
27
+ export { ClientHeader, ClientHeaderParseResult, FEATURES, PROTOCOL_VERSION, ServerHeaderOptions, ZeroAdNetwork };
package/dist/index.d.mts CHANGED
@@ -1,40 +1,27 @@
1
- import { CLIENT_HEADERS, PROTOCOL_VERSION, SITE_FEATURES, ClientHeaderParseResult, ServerHeader, ServerHeaderOptions, SERVER_HEADERS } from './browser.mjs';
2
- export { CURRENT_PROTOCOL_VERSION, ClientParsedHeader, ServerHeaderExtendedOptions, ServerHeaderSimpleOptions, UUID, WelcomeHeader, WelcomeHeaderParseResult, ZEROAD_NETWORK_PUBLIC_KEY } from './browser.mjs';
1
+ import { ClientHeaderParseResult, PROTOCOL_VERSION, FEATURES, ServerHeaderOptions } from './browser.mjs';
2
+ export { CLIENT_HEADERS, CURRENT_PROTOCOL_VERSION, ClientParsedHeader, SERVER_HEADERS, ServerHeader, ServerHeaderExtendedOptions, UUID, WelcomeHeader, WelcomeHeaderParseResult, ZEROAD_NETWORK_PUBLIC_KEY } from './browser.mjs';
3
3
 
4
4
  declare class ClientHeader {
5
5
  private cryptoPublicKey;
6
6
  private cryptoPrivateKey;
7
7
  private publicKey;
8
8
  private privateKey;
9
- name: CLIENT_HEADERS;
9
+ NAME: string;
10
10
  constructor(publicKey: string, privateKey?: string);
11
- encode(version: PROTOCOL_VERSION, expiresAt: Date, features: SITE_FEATURES[]): string;
12
- decode(headerValue: string): ClientHeaderParseResult;
13
- processRequest(headerValue: string | undefined): {
14
- _raw: ClientHeaderParseResult;
15
- readonly shouldRemoveAds: boolean;
16
- readonly shouldEnablePremiumContentAccess: boolean;
17
- readonly shouldEnableVipExperience: boolean;
18
- };
11
+ parseToken(headerValue: string | string[] | undefined): Record<"ADS_OFF" | "COOKIE_CONSENT_OFF" | "MARKETING_DIALOG_OFF" | "CONTENT_PAYWALL_OFF" | "SUBSCRIPTION_ACCESS_ON", boolean>;
12
+ decode(headerValue: string | undefined): ClientHeaderParseResult;
13
+ encode(version: PROTOCOL_VERSION, expiresAt: Date, features: FEATURES[]): string;
19
14
  }
20
15
 
21
16
  type LogLevel = "error" | "warn" | "info" | "debug";
22
17
  declare function setLogLevel(level: LogLevel): void;
23
18
 
24
- declare class Site {
25
- serverHeader: ServerHeader;
26
- clientHeader: ClientHeader;
27
- constructor(options: ServerHeaderOptions);
28
- }
29
- declare const init: (options: ServerHeaderOptions) => Site;
30
- declare const processRequest: (headerValue: string | undefined) => {
31
- _raw: ClientHeaderParseResult;
32
- readonly shouldRemoveAds: boolean;
33
- readonly shouldEnablePremiumContentAccess: boolean;
34
- readonly shouldEnableVipExperience: boolean;
19
+ declare function ZeroAdNetwork(options: ServerHeaderOptions): {
20
+ setLogLevel: typeof setLogLevel;
21
+ parseToken: (headerValue: string | string[] | undefined) => Record<"ADS_OFF" | "COOKIE_CONSENT_OFF" | "MARKETING_DIALOG_OFF" | "CONTENT_PAYWALL_OFF" | "SUBSCRIPTION_ACCESS_ON", boolean>;
22
+ CLIENT_HEADER_NAME: string;
23
+ SERVER_HEADER_NAME: string;
24
+ SERVER_HEADER_VALUE: string;
35
25
  };
36
- declare const getClientHeaderName: () => CLIENT_HEADERS;
37
- declare const getServerHeaderName: () => SERVER_HEADERS;
38
- declare const getServerHeaderValue: () => string;
39
26
 
40
- export { CLIENT_HEADERS, ClientHeader, ClientHeaderParseResult, PROTOCOL_VERSION, SERVER_HEADERS, SITE_FEATURES, ServerHeader, ServerHeaderOptions, Site, getClientHeaderName, getServerHeaderName, getServerHeaderValue, init, processRequest, setLogLevel };
27
+ export { ClientHeader, ClientHeaderParseResult, FEATURES, PROTOCOL_VERSION, ServerHeaderOptions, ZeroAdNetwork };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { C as CLIENT_HEADERS, s as setFeatures, u as unixTimestampToBytes, t as toBase64, f as fromBase64, P as PROTOCOL_VERSION, b as bytesToUnixTimestamp, l as log, S as SITE_FEATURES, h as hasFeature, a as ServerHeader, Z as ZEROAD_NETWORK_PUBLIC_KEY } from './browser-CEwtQQBM.mjs';
2
- export { e as CURRENT_PROTOCOL_VERSION, d as SERVER_HEADERS, c as setLogLevel } from './browser-CEwtQQBM.mjs';
3
- import { randomBytes, createPrivateKey, sign as sign$1, createPublicKey, verify as verify$1 } from 'crypto';
1
+ import { g as getSiteFeaturesNative, C as CLIENT_HEADERS, h as hasFeature, f as fromBase64, P as PROTOCOL_VERSION, l as log, s as setFeatures, t as toBase64, S as ServerHeader, a as setLogLevel, Z as ZEROAD_NETWORK_PUBLIC_KEY } from './browser-ByaVwJr6.mjs';
2
+ export { c as CURRENT_PROTOCOL_VERSION, F as FEATURES, b as SERVER_HEADERS } from './browser-ByaVwJr6.mjs';
3
+ import { createPublicKey, verify as verify$1, randomBytes, createPrivateKey, sign as sign$1 } from 'crypto';
4
4
 
5
5
  const importPrivateKey = (privateKeyBase64) => {
6
6
  const keyBuffer = Buffer.from(privateKeyBase64, "base64");
@@ -29,30 +29,40 @@ const verify = (data, signature, publicKey) => {
29
29
  };
30
30
  const nonce = (size) => new Uint8Array(randomBytes(size));
31
31
 
32
+ const VERSION_BYTES = 1;
32
33
  const NONCE_BYTES = 4;
33
34
  const SEPARATOR = ".";
35
+ const SITE_FEATURES_NATIVE = getSiteFeaturesNative();
36
+ const as32BitNumber = (byteArray, begin) => {
37
+ const bytes = byteArray.subarray(begin, begin + Uint32Array.BYTES_PER_ELEMENT);
38
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
39
+ return view.getUint32(0, true);
40
+ };
34
41
  class ClientHeader {
35
42
  cryptoPublicKey;
36
43
  cryptoPrivateKey;
37
44
  publicKey;
38
45
  privateKey;
39
- name = CLIENT_HEADERS.HELLO;
46
+ NAME = CLIENT_HEADERS.HELLO.toLowerCase();
40
47
  constructor(publicKey, privateKey) {
41
48
  this.publicKey = publicKey;
42
49
  this.privateKey = privateKey;
43
50
  }
44
- encode(version, expiresAt, features) {
45
- if (!this.privateKey) throw new Error("Private key is required");
46
- const flags = setFeatures(0, features);
47
- const data = mergeByteArrays([
48
- new Uint8Array([version]),
49
- nonce(NONCE_BYTES),
50
- unixTimestampToBytes(expiresAt),
51
- new Uint8Array([flags])
52
- ]);
53
- if (!this.cryptoPrivateKey) this.cryptoPrivateKey = importPrivateKey(this.privateKey);
54
- const signature = sign(data.buffer, this.cryptoPrivateKey);
55
- return [toBase64(data), toBase64(new Uint8Array(signature))].join(SEPARATOR);
51
+ parseToken(headerValue) {
52
+ const headerValueAsString = Array.isArray(headerValue) ? headerValue[0] : headerValue;
53
+ const data = this.decode(headerValueAsString);
54
+ const expired = !data || data.expiresAt.getTime() < Date.now();
55
+ const result = {};
56
+ if (!data || expired) {
57
+ for (const [feature] of SITE_FEATURES_NATIVE) {
58
+ result[feature] = false;
59
+ }
60
+ return result;
61
+ }
62
+ for (const [feature, shift] of SITE_FEATURES_NATIVE) {
63
+ result[feature] = hasFeature(data.flags, shift);
64
+ }
65
+ return result;
56
66
  }
57
67
  decode(headerValue) {
58
68
  if (!headerValue?.length) return;
@@ -66,62 +76,52 @@ class ClientHeader {
66
76
  }
67
77
  const version = dataBytes[0];
68
78
  if (version === PROTOCOL_VERSION.V_1) {
69
- const flagsBytes = dataBytes.subarray(dataBytes.length - 1, dataBytes.length);
70
- const expiresAtBytes = dataBytes.subarray(5, 9);
71
- const expiresAt = bytesToUnixTimestamp(expiresAtBytes);
72
- const expired = expiresAt.getTime() < Date.now();
73
- return { version, expiresAt, expired, flags: flagsBytes[0] };
79
+ const expiresAt = as32BitNumber(dataBytes, VERSION_BYTES + NONCE_BYTES);
80
+ const flags = as32BitNumber(dataBytes, dataBytes.length - Uint32Array.BYTES_PER_ELEMENT);
81
+ return { version, expiresAt: new Date(expiresAt * 1e3), flags };
74
82
  }
75
83
  } catch (err) {
76
84
  log("warn", "Could not decode client header value", { reason: err?.message });
77
85
  }
78
86
  }
79
- processRequest(headerValue) {
80
- const data = this.decode(headerValue);
81
- return {
82
- _raw: data,
83
- get shouldRemoveAds() {
84
- return shouldRemoveAds(data);
85
- },
86
- get shouldEnablePremiumContentAccess() {
87
- return shouldEnablePremiumContentAccess(data);
88
- },
89
- get shouldEnableVipExperience() {
90
- return shouldEnableVipExperience(data);
91
- }
92
- };
87
+ encode(version, expiresAt, features) {
88
+ if (!this.privateKey) throw new Error("Private key is required");
89
+ const data = mergeByteArrays([
90
+ new Uint8Array([version]),
91
+ new Uint8Array(nonce(NONCE_BYTES)),
92
+ new Uint32Array([Math.floor(expiresAt.getTime() / 1e3)]),
93
+ new Uint32Array([setFeatures(0, features)])
94
+ ]);
95
+ if (!this.cryptoPrivateKey) this.cryptoPrivateKey = importPrivateKey(this.privateKey);
96
+ const signature = sign(data.buffer, this.cryptoPrivateKey);
97
+ return [toBase64(data), toBase64(new Uint8Array(signature))].join(SEPARATOR);
93
98
  }
94
99
  }
95
- const test = (data, feature) => {
96
- return data && !data?.expired && hasFeature(data?.flags, feature) || false;
97
- };
98
- const shouldRemoveAds = (data) => test(data, SITE_FEATURES.AD_LESS_EXPERIENCE);
99
- const shouldEnableVipExperience = (data) => test(data, SITE_FEATURES.VIP_EXPERIENCE);
100
- const shouldEnablePremiumContentAccess = (data) => test(data, SITE_FEATURES.PREMIUM_CONTENT_ACCESS);
101
100
  const mergeByteArrays = (arrays) => {
102
- const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
101
+ const totalLength = arrays.reduce((sum, a) => sum + a.byteLength, 0);
103
102
  const data = new Uint8Array(totalLength);
104
103
  let offset = 0;
105
104
  for (const arr of arrays) {
106
- data.set(arr, offset);
107
- offset += arr.length;
105
+ let bytes;
106
+ if (arr instanceof Uint8Array) bytes = arr;
107
+ else if (arr instanceof Uint32Array) bytes = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
108
+ else throw new Error("Unsupported type");
109
+ data.set(bytes, offset);
110
+ offset += bytes.byteLength;
108
111
  }
109
112
  return data;
110
113
  };
111
114
 
112
- class Site {
113
- serverHeader;
114
- clientHeader;
115
- constructor(options) {
116
- this.serverHeader = new ServerHeader(options);
117
- this.clientHeader = new ClientHeader(ZEROAD_NETWORK_PUBLIC_KEY);
118
- }
115
+ function ZeroAdNetwork(options) {
116
+ const serverHeader = new ServerHeader(options);
117
+ const clientHeader = new ClientHeader(ZEROAD_NETWORK_PUBLIC_KEY);
118
+ return {
119
+ setLogLevel,
120
+ parseToken: clientHeader.parseToken.bind(clientHeader),
121
+ CLIENT_HEADER_NAME: clientHeader.NAME,
122
+ SERVER_HEADER_NAME: serverHeader.NAME,
123
+ SERVER_HEADER_VALUE: serverHeader.VALUE
124
+ };
119
125
  }
120
- let defaultSite;
121
- const init = (options) => defaultSite = new Site(options);
122
- const processRequest = (headerValue) => defaultSite.clientHeader.processRequest(headerValue);
123
- const getClientHeaderName = () => defaultSite.clientHeader.name;
124
- const getServerHeaderName = () => defaultSite.serverHeader.name;
125
- const getServerHeaderValue = () => defaultSite.serverHeader.value;
126
126
 
127
- export { CLIENT_HEADERS, ClientHeader, PROTOCOL_VERSION, SITE_FEATURES, ServerHeader, Site, ZEROAD_NETWORK_PUBLIC_KEY, getClientHeaderName, getServerHeaderName, getServerHeaderValue, init, processRequest };
127
+ export { CLIENT_HEADERS, ClientHeader, PROTOCOL_VERSION, ServerHeader, ZEROAD_NETWORK_PUBLIC_KEY, ZeroAdNetwork };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@zeroad.network/token",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "license": "Apache-2.0",
5
- "repository": "github:laurynas-karvelis/zeroad-token",
5
+ "repository": "github:laurynas-karvelis/zeroad-token-ts",
6
6
  "homepage": "https://zeroad.network",
7
7
  "private": false,
8
8
  "author": {
@@ -42,17 +42,21 @@
42
42
  "default": "./dist/browser.mjs"
43
43
  }
44
44
  },
45
+ "output": {
46
+ "exports": "named"
47
+ },
45
48
  "scripts": {
46
49
  "keys:generate": "bun run ./src/tools/cli.ts",
47
50
  "prettier": "prettier . --write",
48
- "build": "pkgroll --target=node18"
51
+ "build": "pkgroll --target=node18",
52
+ "test": "bun test"
49
53
  },
50
54
  "dependencies": {},
51
55
  "devDependencies": {
52
56
  "@types/bun": "^1.3.3",
53
57
  "@types/node": "^24.10.0",
54
58
  "pkgroll": "^2.21.4",
55
- "prettier": "^3.7.1",
59
+ "prettier": "^3.7.2",
56
60
  "typescript": "^5.9.3"
57
61
  }
58
62
  }