@zeroad.network/token 0.13.13 → 0.14.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 +689 -109
- package/dist/{browser-Do--x9Sv.cjs → browser-D3BXUIiT.cjs} +20 -21
- package/dist/{browser-DiH4sEBn.mjs → browser-VjxLaeuu.mjs} +20 -20
- package/dist/browser.mjs +1 -1
- package/dist/index.cjs +174 -36
- package/dist/index.d.cts +28 -6
- package/dist/index.d.mts +28 -6
- package/dist/index.mjs +170 -38
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -1,10 +1,40 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zeroad.network/token
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The official TypeScript/JavaScript module for integrating websites with the [Zero Ad Network](https://zeroad.network) platform.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## What is Zero Ad Network?
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Zero Ad Network is a browser-based platform that creates a better web experience for both users and content creators:
|
|
8
|
+
|
|
9
|
+
**For Users:**
|
|
10
|
+
|
|
11
|
+
- Browse without ads, trackers, cookie consent dialogs, and marketing pop-ups
|
|
12
|
+
- Access paywalled content across multiple sites with a single subscription
|
|
13
|
+
- Support content creators directly through fair revenue distribution
|
|
14
|
+
|
|
15
|
+
**For Publishers:**
|
|
16
|
+
|
|
17
|
+
- Generate revenue from users who would otherwise use ad blockers
|
|
18
|
+
- Provide a cleaner user experience while maintaining income
|
|
19
|
+
- Get paid based on actual user engagement with your content
|
|
20
|
+
|
|
21
|
+
**How It Works:**
|
|
22
|
+
|
|
23
|
+
1. Users subscribe and install the Zero Ad Network browser extension
|
|
24
|
+
2. The extension sends cryptographically signed tokens to partner sites
|
|
25
|
+
3. Partner sites verify tokens and enable premium features (ad-free, paywall-free)
|
|
26
|
+
4. Monthly revenue is distributed to publishers based on user engagement time
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
This module provides:
|
|
31
|
+
|
|
32
|
+
- ✅ **Zero dependencies** - Lightweight and secure
|
|
33
|
+
- ✅ **Full TypeScript support** - Complete type definitions included
|
|
34
|
+
- ✅ **Cryptographic verification** - ED25519 signature validation using Node.js crypto
|
|
35
|
+
- ✅ **Performance optimized** - Async crypto operations with intelligent caching
|
|
36
|
+
- ✅ **Universal runtime support** - Works with Node.js, Bun, and Deno
|
|
37
|
+
- ✅ **ESM & CommonJS** - Supports both module systems
|
|
8
38
|
|
|
9
39
|
## Runtime Compatibility
|
|
10
40
|
|
|
@@ -14,154 +44,704 @@ For detailed guides and implementation instructions, see the [official Zero Ad N
|
|
|
14
44
|
| Bun | 1.1.0+ | ✅ | ✅ |
|
|
15
45
|
| Deno | 2.0.0+ | ✅ | ✅ |
|
|
16
46
|
|
|
17
|
-
##
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# npm
|
|
51
|
+
npm install @zeroad.network/token
|
|
52
|
+
|
|
53
|
+
# yarn
|
|
54
|
+
yarn add @zeroad.network/token
|
|
55
|
+
|
|
56
|
+
# pnpm
|
|
57
|
+
pnpm add @zeroad.network/token
|
|
18
58
|
|
|
19
|
-
|
|
59
|
+
# bun
|
|
60
|
+
bun add @zeroad.network/token
|
|
20
61
|
|
|
21
|
-
|
|
22
|
-
|
|
62
|
+
# deno
|
|
63
|
+
deno add npm:@zeroad.network/token
|
|
64
|
+
```
|
|
23
65
|
|
|
24
|
-
|
|
25
|
-
X-Better-Web-Welcome: "Z2CclA8oXIT1e0QmqTWF8w^1^3"
|
|
26
|
-
```
|
|
66
|
+
## Quick Start
|
|
27
67
|
|
|
28
|
-
|
|
68
|
+
### 1. Register Your Site
|
|
29
69
|
|
|
30
|
-
|
|
31
|
-
X-Better-Web-Hello: "Aav2IXRoh0oKBw==.2yZfC2/pM9DWfgX+von4IgWLmN9t67HJHLiee/gx4+pFIHHurwkC3PCHT1Kaz0yUhx3crUaxST+XLlRtJYacAQ=="
|
|
32
|
-
```
|
|
70
|
+
Before implementing, you need to:
|
|
33
71
|
|
|
34
|
-
|
|
72
|
+
1. [Sign up](https://zeroad.network/login) for a Zero Ad Network account
|
|
73
|
+
2. [Register your site](https://zeroad.network/publisher/sites/add) to receive your unique `Client ID`
|
|
35
74
|
|
|
36
|
-
|
|
75
|
+
### 2. Choose Your Features
|
|
37
76
|
|
|
38
|
-
|
|
39
|
-
- Decodes token payload to extract protocol version, expiration timestamp, and site features.
|
|
40
|
-
- Generates a feature map; expired tokens produce all flags as `false`.
|
|
77
|
+
Decide which features your site will support:
|
|
41
78
|
|
|
42
|
-
|
|
79
|
+
- **`FEATURE.CLEAN_WEB`**: Remove ads, cookie consent screens, trackers, and marketing dialogs
|
|
80
|
+
- **`FEATURE.ONE_PASS`**: Provide free access to paywalled content and base subscription plans
|
|
43
81
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
82
|
+
### 3. Basic Implementation
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import express from "express";
|
|
86
|
+
import { Site, FEATURE } from "@zeroad.network/token";
|
|
87
|
+
|
|
88
|
+
const app = express();
|
|
89
|
+
|
|
90
|
+
// Initialize once at startup - this creates your site instance
|
|
91
|
+
const site = Site({
|
|
92
|
+
clientId: "YOUR_CLIENT_ID_HERE",
|
|
93
|
+
features: [FEATURE.CLEAN_WEB, FEATURE.ONE_PASS],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Middleware: Inject Welcome Header and parse user tokens
|
|
97
|
+
app.use(async (req, res, next) => {
|
|
98
|
+
// Tell the browser extension your site participates
|
|
99
|
+
res.set(site.SERVER_HEADER_NAME, site.SERVER_HEADER_VALUE);
|
|
100
|
+
|
|
101
|
+
// Parse the user's subscription token
|
|
102
|
+
req.tokenContext = await site.parseClientToken(req.get(site.CLIENT_HEADER_NAME));
|
|
103
|
+
|
|
104
|
+
next();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Use token context in your templates
|
|
108
|
+
app.get("/", async (req, res) => {
|
|
109
|
+
res.render("index", {
|
|
110
|
+
// Pass token context to control what appears in templates
|
|
111
|
+
tokenContext: req.tokenContext,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
app.listen(3000);
|
|
53
116
|
```
|
|
54
117
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
118
|
+
### 4. In Your Templates
|
|
119
|
+
|
|
120
|
+
```ejs
|
|
121
|
+
<!-- index.ejs -->
|
|
122
|
+
<!DOCTYPE html>
|
|
123
|
+
<html>
|
|
124
|
+
<head>
|
|
125
|
+
<title>My Site</title>
|
|
126
|
+
</head>
|
|
127
|
+
<body>
|
|
128
|
+
<h1>Welcome to My Site</h1>
|
|
129
|
+
|
|
130
|
+
<!-- Only show ads to non-subscribers -->
|
|
131
|
+
<% if (!tokenContext.HIDE_ADVERTISEMENTS) { %>
|
|
132
|
+
<div class="advertisement">
|
|
133
|
+
<!-- Ad code here -->
|
|
134
|
+
</div>
|
|
135
|
+
<% } %>
|
|
136
|
+
|
|
137
|
+
<!-- Only show cookie consent to non-subscribers -->
|
|
138
|
+
<% if (!tokenContext.HIDE_COOKIE_CONSENT_SCREEN) { %>
|
|
139
|
+
<div class="cookie-consent">
|
|
140
|
+
<!-- Cookie consent dialog -->
|
|
141
|
+
</div>
|
|
142
|
+
<% } %>
|
|
143
|
+
|
|
144
|
+
<!-- Content everyone sees -->
|
|
145
|
+
<article>
|
|
146
|
+
<h2>Article Title</h2>
|
|
147
|
+
|
|
148
|
+
<!-- Show preview or full content based on subscription -->
|
|
149
|
+
<% if (tokenContext.DISABLE_CONTENT_PAYWALL) { %>
|
|
150
|
+
<p>Full article content for Zero Ad Network subscribers...</p>
|
|
151
|
+
<% } else { %>
|
|
152
|
+
<p>Article preview... <a href="/subscribe">Subscribe to read more</a></p>
|
|
153
|
+
<% } %>
|
|
154
|
+
</article>
|
|
155
|
+
</body>
|
|
156
|
+
</html>
|
|
157
|
+
```
|
|
58
158
|
|
|
59
|
-
##
|
|
159
|
+
## Token Context
|
|
60
160
|
|
|
61
|
-
|
|
161
|
+
After parsing, the token context contains boolean flags for each feature:
|
|
62
162
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
163
|
+
```typescript
|
|
164
|
+
interface TokenContext {
|
|
165
|
+
// CLEAN_WEB features
|
|
166
|
+
HIDE_ADVERTISEMENTS: boolean;
|
|
167
|
+
HIDE_COOKIE_CONSENT_SCREEN: boolean;
|
|
168
|
+
HIDE_MARKETING_DIALOGS: boolean;
|
|
169
|
+
DISABLE_NON_FUNCTIONAL_TRACKING: boolean;
|
|
68
170
|
|
|
69
|
-
|
|
171
|
+
// ONE_PASS features
|
|
172
|
+
DISABLE_CONTENT_PAYWALL: boolean;
|
|
173
|
+
ENABLE_SUBSCRIPTION_ACCESS: boolean;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
70
176
|
|
|
71
|
-
|
|
72
|
-
2. [Register your site](https://zeroad.network/publisher/sites/add) to receive your unique `Client ID` value.
|
|
177
|
+
**Important:** All flags default to `false` for:
|
|
73
178
|
|
|
74
|
-
|
|
179
|
+
- Users without subscriptions
|
|
180
|
+
- Expired tokens
|
|
181
|
+
- Invalid/forged tokens
|
|
182
|
+
- Missing tokens
|
|
75
183
|
|
|
76
|
-
##
|
|
184
|
+
## Advanced Configuration
|
|
77
185
|
|
|
78
|
-
|
|
79
|
-
- Supports both ESM (`import`) and CommonJS (`require`). ESM is recommended when possible.
|
|
186
|
+
### Cache Configuration
|
|
80
187
|
|
|
81
|
-
|
|
188
|
+
The module includes intelligent caching to minimize crypto operations. Configure caching when creating your site instance:
|
|
82
189
|
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
npm add @zeroad.network/token
|
|
190
|
+
```typescript
|
|
191
|
+
import { Site, FEATURE } from "@zeroad.network/token";
|
|
86
192
|
|
|
87
|
-
|
|
88
|
-
|
|
193
|
+
const site = Site({
|
|
194
|
+
clientId: process.env.ZERO_AD_CLIENT_ID!,
|
|
195
|
+
features: [FEATURE.CLEAN_WEB],
|
|
196
|
+
cacheConfig: {
|
|
197
|
+
enabled: true,
|
|
198
|
+
ttl: 10000, // Cache tokens for 10 seconds
|
|
199
|
+
maxSize: 500, // Store up to 500 unique tokens
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
```
|
|
89
203
|
|
|
90
|
-
|
|
91
|
-
pnpm add @zeroad.network/token
|
|
204
|
+
**Cache Behavior:**
|
|
92
205
|
|
|
93
|
-
|
|
94
|
-
|
|
206
|
+
- Automatically respects token expiration times
|
|
207
|
+
- Uses LFU+LRU eviction strategy
|
|
208
|
+
- Thread-safe for concurrent requests
|
|
95
209
|
|
|
96
|
-
|
|
97
|
-
|
|
210
|
+
For more control, you can configure caching globally:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import { configureCaching } from "@zeroad.network/token";
|
|
214
|
+
|
|
215
|
+
// Apply to all Site instances
|
|
216
|
+
configureCaching({
|
|
217
|
+
enabled: true,
|
|
218
|
+
ttl: 5000,
|
|
219
|
+
maxSize: 100,
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Security
|
|
224
|
+
|
|
225
|
+
### Token Verification
|
|
226
|
+
|
|
227
|
+
All tokens are cryptographically signed using ED25519 by Zero Ad Network:
|
|
228
|
+
|
|
229
|
+
- **Signature verification** happens locally on your server using Zero Ad Network's official public key
|
|
230
|
+
- **Trusted authority** - Only tokens signed by Zero Ad Network are valid
|
|
231
|
+
- **No external API calls** - verification is instant and offline
|
|
232
|
+
- **Tamper-proof** - modified tokens fail verification automatically
|
|
233
|
+
- **Time-limited** - expired tokens are automatically rejected
|
|
234
|
+
|
|
235
|
+
The module uses a hardcoded public key from Zero Ad Network, ensuring only legitimate subscriber tokens are accepted.
|
|
236
|
+
|
|
237
|
+
### Token Structure
|
|
238
|
+
|
|
239
|
+
Each token contains:
|
|
240
|
+
|
|
241
|
+
1. **Protocol version** - Currently v1
|
|
242
|
+
2. **Expiration timestamp** - Unix timestamp
|
|
243
|
+
3. **Feature flags** - Bitmask of enabled features
|
|
244
|
+
4. **Client ID** (optional) - For developer tokens
|
|
245
|
+
5. **Cryptographic signature** - ED25519 signature
|
|
246
|
+
|
|
247
|
+
Example token:
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
X-Better-Web-Hello: Aav2IXRoh0oKBw==.2yZfC2/pM9DWfgX+von4IgWLmN9t67HJHLiee/gx4+pFIHHurwkC3PCHT1Kaz0yUhx3crUaxST+XLlRtJYacAQ==
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Privacy
|
|
254
|
+
|
|
255
|
+
Tokens contain **no personally identifiable information**:
|
|
256
|
+
|
|
257
|
+
- ❌ No user IDs
|
|
258
|
+
- ❌ No email addresses
|
|
259
|
+
- ❌ No tracking data
|
|
260
|
+
- ✅ Only: expiration date and feature flags
|
|
261
|
+
|
|
262
|
+
## Performance
|
|
263
|
+
|
|
264
|
+
### Benchmarks
|
|
265
|
+
|
|
266
|
+
Typical performance on modern hardware (M1 MacBook Pro):
|
|
267
|
+
|
|
268
|
+
| Operation | Time | Notes |
|
|
269
|
+
| ------------------ | ------ | ---------------------------- |
|
|
270
|
+
| Parse cached token | ~10μs | Cache hit |
|
|
271
|
+
| Parse new token | ~150μs | Includes crypto verification |
|
|
272
|
+
| Verify signature | ~100μs | ED25519 verification |
|
|
273
|
+
| Build context | ~8μs | Feature flag processing |
|
|
274
|
+
|
|
275
|
+
### Optimization Tips
|
|
276
|
+
|
|
277
|
+
1. **Enable caching** - 80-95% performance improvement for repeated tokens
|
|
278
|
+
2. **Use async operations** - Crypto runs in Node.js threadpool (non-blocking)
|
|
279
|
+
3. **Cache at edge** - Consider caching at CDN/proxy level
|
|
280
|
+
4. **Monitor cache hit rate** - Adjust TTL and size based on traffic patterns
|
|
281
|
+
|
|
282
|
+
### High-Traffic Scenarios
|
|
283
|
+
|
|
284
|
+
For sites with >1000 req/sec:
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
configureCaching({
|
|
288
|
+
enabled: true,
|
|
289
|
+
ttl: 30000, // 30 seconds
|
|
290
|
+
maxSize: 5000, // ~2.5MB memory
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The async crypto operations utilize Node.js libuv threadpool (4 threads by default), allowing ~8000 verifications/sec without blocking the event loop.
|
|
295
|
+
|
|
296
|
+
## Framework Examples
|
|
297
|
+
|
|
298
|
+
### Next.js (Pages Router)
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// middleware.ts
|
|
302
|
+
import { NextResponse } from "next/server";
|
|
303
|
+
import type { NextRequest } from "next/server";
|
|
304
|
+
import { Site, FEATURE } from "@zeroad.network/token";
|
|
305
|
+
|
|
306
|
+
// Create site instance once
|
|
307
|
+
const site = Site({
|
|
308
|
+
clientId: process.env.ZERO_AD_CLIENT_ID!,
|
|
309
|
+
features: [FEATURE.CLEAN_WEB, FEATURE.ONE_PASS]
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
export async function middleware(request: NextRequest) {
|
|
313
|
+
const response = NextResponse.next();
|
|
314
|
+
|
|
315
|
+
// Add Welcome Header to response
|
|
316
|
+
response.headers.set(site.SERVER_HEADER_NAME, site.SERVER_HEADER_VALUE);
|
|
317
|
+
|
|
318
|
+
return response;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// pages/article/[id].tsx
|
|
322
|
+
import { GetServerSideProps } from "next";
|
|
323
|
+
import { Site, FEATURE } from "@zeroad.network/token";
|
|
324
|
+
|
|
325
|
+
const site = Site({
|
|
326
|
+
clientId: process.env.ZERO_AD_CLIENT_ID!,
|
|
327
|
+
features: [FEATURE.CLEAN_WEB, FEATURE.ONE_PASS]
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
|
|
331
|
+
// Parse token in server-side code
|
|
332
|
+
const tokenContext = await site.parseClientToken(
|
|
333
|
+
req.headers[site.CLIENT_HEADER_NAME]
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const article = await getArticle();
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
props: {
|
|
340
|
+
article,
|
|
341
|
+
tokenContext
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
export default function Article({ article, tokenContext }) {
|
|
347
|
+
return (
|
|
348
|
+
<div>
|
|
349
|
+
{/* Conditionally render based on token context */}
|
|
350
|
+
{!tokenContext.HIDE_ADVERTISEMENTS && (
|
|
351
|
+
<div className="ad-banner">Ad content</div>
|
|
352
|
+
)}
|
|
353
|
+
|
|
354
|
+
<article>
|
|
355
|
+
<h1>{article.title}</h1>
|
|
356
|
+
{tokenContext.DISABLE_CONTENT_PAYWALL ? (
|
|
357
|
+
<div>{article.fullContent}</div>
|
|
358
|
+
) : (
|
|
359
|
+
<div>{article.preview}</div>
|
|
360
|
+
)}
|
|
361
|
+
</article>
|
|
362
|
+
</div>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
98
365
|
```
|
|
99
366
|
|
|
100
|
-
|
|
367
|
+
### Fastify
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
import Fastify from "fastify";
|
|
371
|
+
import { Site, FEATURE } from "@zeroad.network/token";
|
|
101
372
|
|
|
102
|
-
|
|
373
|
+
const fastify = Fastify();
|
|
374
|
+
|
|
375
|
+
// Create site instance once
|
|
376
|
+
const site = Site({
|
|
377
|
+
clientId: process.env.ZERO_AD_CLIENT_ID!,
|
|
378
|
+
features: [FEATURE.CLEAN_WEB],
|
|
379
|
+
});
|
|
103
380
|
|
|
104
|
-
|
|
381
|
+
fastify.addHook("onRequest", async (request, reply) => {
|
|
382
|
+
reply.header(site.SERVER_HEADER_NAME, site.SERVER_HEADER_VALUE);
|
|
105
383
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
384
|
+
request.tokenContext = await site.parseClientToken(request.headers[site.CLIENT_HEADER_NAME]);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
fastify.get("/", async (request, reply) => {
|
|
388
|
+
return reply.view("index", {
|
|
389
|
+
tokenContext: request.tokenContext,
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
await fastify.listen({ port: 3000 });
|
|
394
|
+
```
|
|
109
395
|
|
|
110
|
-
|
|
396
|
+
### Hono
|
|
111
397
|
|
|
112
|
-
```
|
|
398
|
+
```typescript
|
|
399
|
+
import { Hono } from "hono";
|
|
400
|
+
import { Site, FEATURE } from "@zeroad.network/token";
|
|
401
|
+
|
|
402
|
+
const app = new Hono();
|
|
403
|
+
|
|
404
|
+
// Create site instance once
|
|
405
|
+
const site = Site({
|
|
406
|
+
clientId: process.env.ZERO_AD_CLIENT_ID!,
|
|
407
|
+
features: [FEATURE.CLEAN_WEB, FEATURE.ONE_PASS],
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
app.use("*", async (c, next) => {
|
|
411
|
+
c.header(site.SERVER_HEADER_NAME, site.SERVER_HEADER_VALUE);
|
|
412
|
+
|
|
413
|
+
c.set("tokenContext", await site.parseClientToken(c.req.header(site.CLIENT_HEADER_NAME)));
|
|
414
|
+
|
|
415
|
+
await next();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
app.get("/", (c) => {
|
|
419
|
+
return c.html(
|
|
420
|
+
renderTemplate({
|
|
421
|
+
tokenContext: c.get("tokenContext"),
|
|
422
|
+
})
|
|
423
|
+
);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
export default app;
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
## Complete Usage Example
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
113
432
|
import express from "express";
|
|
114
|
-
import { Site } from "@zeroad.network/token";
|
|
433
|
+
import { Site, FEATURE } from "@zeroad.network/token";
|
|
115
434
|
|
|
116
435
|
const app = express();
|
|
117
436
|
|
|
118
|
-
//
|
|
119
|
-
// Your site's `clientId` value is obtained during site registration on the Zero Ad Network platform (https://zeroad.network).
|
|
120
|
-
const ZERO_AD_NETWORK_CLIENT_ID = "DEMO-Z2CclA8oXIT1e0Qmq";
|
|
437
|
+
// Create your site instance once at startup
|
|
121
438
|
const site = Site({
|
|
122
|
-
clientId:
|
|
439
|
+
clientId: process.env.ZERO_AD_CLIENT_ID!,
|
|
123
440
|
features: [FEATURE.CLEAN_WEB, FEATURE.ONE_PASS],
|
|
441
|
+
cacheConfig: {
|
|
442
|
+
enabled: true,
|
|
443
|
+
ttl: 10000,
|
|
444
|
+
maxSize: 500,
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Global middleware
|
|
449
|
+
app.use(async (req, res, next) => {
|
|
450
|
+
res.set(site.SERVER_HEADER_NAME, site.SERVER_HEADER_VALUE);
|
|
451
|
+
req.tokenContext = await site.parseClientToken(req.get(site.CLIENT_HEADER_NAME));
|
|
452
|
+
next();
|
|
124
453
|
});
|
|
125
454
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
.
|
|
129
|
-
|
|
130
|
-
res.set(site.SERVER_HEADER_NAME, site.SERVER_HEADER_VALUE);
|
|
131
|
-
|
|
132
|
-
// Parse the incoming user token from the request header
|
|
133
|
-
// Attach the parsed token data to the request object for downstream use
|
|
134
|
-
req.tokenContext = site.parseClientToken(req.get(site.CLIENT_HEADER_NAME));
|
|
135
|
-
|
|
136
|
-
next();
|
|
137
|
-
})
|
|
138
|
-
.get("/", (req, res) => {
|
|
139
|
-
// Access parsed token data for this request
|
|
140
|
-
console.log(req.tokenContext);
|
|
141
|
-
|
|
142
|
-
// Example structure of tokenContext:
|
|
143
|
-
req.tokenContext = {
|
|
144
|
-
// If `true`: Hide advertisements on the page
|
|
145
|
-
HIDE_ADVERTISEMENTS: boolean,
|
|
146
|
-
// If `true`: Disable all Cookie Consent screens (headers, footers, or dialogs)
|
|
147
|
-
HIDE_COOKIE_CONSENT_SCREEN: boolean,
|
|
148
|
-
// If `true`: Disable all marketing dialogs or popups (newsletters, promotions, etc.)
|
|
149
|
-
HIDE_MARKETING_DIALOGS: boolean,
|
|
150
|
-
// If `true`: Fully opt out the user of non-functional trackers
|
|
151
|
-
DISABLE_NON_FUNCTIONAL_TRACKING: boolean,
|
|
152
|
-
// If `true`: Provide Free access to content behind a paywall (news, articles, etc.)
|
|
153
|
-
DISABLE_CONTENT_PAYWALL: boolean,
|
|
154
|
-
// If `true`: Provide Free access to your base subscription plan (if subscription model is present)
|
|
155
|
-
ENABLE_SUBSCRIPTION_ACCESS: boolean,
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// Adjust content in your templates based on tokenContext values
|
|
159
|
-
res.render("index.ejs", { tokenContext });
|
|
455
|
+
// Homepage with ads
|
|
456
|
+
app.get("/", async (req, res) => {
|
|
457
|
+
res.render("index", {
|
|
458
|
+
tokenContext: req.tokenContext,
|
|
160
459
|
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Article page with paywall
|
|
463
|
+
app.get("/article/:id", async (req, res) => {
|
|
464
|
+
const article = await db.articles.findById(req.params.id);
|
|
465
|
+
|
|
466
|
+
res.render("article", {
|
|
467
|
+
article,
|
|
468
|
+
tokenContext: req.tokenContext,
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Premium API endpoint
|
|
473
|
+
app.get("/api/premium-data", async (req, res) => {
|
|
474
|
+
if (!req.tokenContext.ENABLE_SUBSCRIPTION_ACCESS) {
|
|
475
|
+
return res.status(403).json({
|
|
476
|
+
error: "Premium subscription required",
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const data = await getPremiumData();
|
|
481
|
+
res.json(data);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
app.listen(3000, () => {
|
|
485
|
+
console.log("Server running on http://localhost:3000");
|
|
486
|
+
});
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**Template Example:**
|
|
490
|
+
|
|
491
|
+
```ejs
|
|
492
|
+
<!-- article.ejs -->
|
|
493
|
+
<!DOCTYPE html>
|
|
494
|
+
<html>
|
|
495
|
+
<head>
|
|
496
|
+
<title><%= article.title %></title>
|
|
497
|
+
</head>
|
|
498
|
+
<body>
|
|
499
|
+
<!-- Ads only for non-subscribers -->
|
|
500
|
+
<% if (!tokenContext.HIDE_ADVERTISEMENTS) { %>
|
|
501
|
+
<div class="ad-banner">
|
|
502
|
+
<!-- Google AdSense or other ad code -->
|
|
503
|
+
</div>
|
|
504
|
+
<% } %>
|
|
505
|
+
|
|
506
|
+
<!-- Cookie consent only for non-subscribers -->
|
|
507
|
+
<% if (!tokenContext.HIDE_COOKIE_CONSENT_SCREEN) { %>
|
|
508
|
+
<div class="cookie-consent">
|
|
509
|
+
<p>We use cookies...</p>
|
|
510
|
+
</div>
|
|
511
|
+
<% } %>
|
|
512
|
+
|
|
513
|
+
<article>
|
|
514
|
+
<h1><%= article.title %></h1>
|
|
515
|
+
|
|
516
|
+
<!-- Full content for subscribers, preview for others -->
|
|
517
|
+
<% if (tokenContext.DISABLE_CONTENT_PAYWALL) { %>
|
|
518
|
+
<div class="full-content">
|
|
519
|
+
<%- article.fullContent %>
|
|
520
|
+
</div>
|
|
521
|
+
<% } else { %>
|
|
522
|
+
<div class="preview">
|
|
523
|
+
<%- article.preview %>
|
|
524
|
+
<div class="paywall">
|
|
525
|
+
<p>Subscribe to read the full article</p>
|
|
526
|
+
<a href="/subscribe">Subscribe Now</a>
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
<% } %>
|
|
530
|
+
</article>
|
|
531
|
+
|
|
532
|
+
<!-- Marketing popup only for non-subscribers -->
|
|
533
|
+
<% if (!tokenContext.HIDE_MARKETING_DIALOGS) { %>
|
|
534
|
+
<div class="newsletter-popup">
|
|
535
|
+
<p>Subscribe to our newsletter!</p>
|
|
536
|
+
</div>
|
|
537
|
+
<% } %>
|
|
538
|
+
|
|
539
|
+
<!-- Analytics tracking (only functional cookies for subscribers) -->
|
|
540
|
+
<% if (!tokenContext.DISABLE_NON_FUNCTIONAL_TRACKING) { %>
|
|
541
|
+
<script>
|
|
542
|
+
// Google Analytics or other tracking code
|
|
543
|
+
</script>
|
|
544
|
+
<% } %>
|
|
545
|
+
</body>
|
|
546
|
+
</html>
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## Implementation Requirements
|
|
550
|
+
|
|
551
|
+
When implementing Zero Ad Network features, you **must** fulfill these requirements to remain in good standing:
|
|
552
|
+
|
|
553
|
+
### CLEAN_WEB Requirements
|
|
554
|
+
|
|
555
|
+
- ✅ Disable **all** advertisements on the page
|
|
556
|
+
- ✅ Disable **all** cookie consent screens (headers, footers, dialogs)
|
|
557
|
+
- ✅ Fully opt out users from **non-functional** trackers
|
|
558
|
+
- ✅ Disable **all** marketing dialogs or popups (newsletters, promotions)
|
|
559
|
+
|
|
560
|
+
### ONE_PASS Requirements
|
|
561
|
+
|
|
562
|
+
- ✅ Provide free access to content behind paywalls
|
|
563
|
+
- ✅ Provide free access to your base subscription plan (if applicable)
|
|
564
|
+
|
|
565
|
+
**⚠️ Failure to comply will result in removal from the Zero Ad Network platform.**
|
|
566
|
+
|
|
567
|
+
## Troubleshooting
|
|
568
|
+
|
|
569
|
+
### Tokens Not Working
|
|
161
570
|
|
|
162
|
-
|
|
571
|
+
```typescript
|
|
572
|
+
import { Site, setLogLevel } from "@zeroad.network/token";
|
|
163
573
|
|
|
164
|
-
|
|
165
|
-
|
|
574
|
+
// Enable debug logging
|
|
575
|
+
setLogLevel("debug");
|
|
576
|
+
|
|
577
|
+
const site = Site({
|
|
578
|
+
clientId: process.env.ZERO_AD_CLIENT_ID!,
|
|
579
|
+
features: [FEATURE.CLEAN_WEB],
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// In your route handler
|
|
583
|
+
app.use(async (req, res, next) => {
|
|
584
|
+
// Check if token header is being received
|
|
585
|
+
const headerValue = req.get(site.CLIENT_HEADER_NAME);
|
|
586
|
+
console.log("Header value:", headerValue);
|
|
587
|
+
|
|
588
|
+
// Parse and verify
|
|
589
|
+
const tokenContext = await site.parseClientToken(headerValue);
|
|
590
|
+
console.log("Token context:", tokenContext);
|
|
591
|
+
|
|
592
|
+
req.tokenContext = tokenContext;
|
|
593
|
+
next();
|
|
166
594
|
});
|
|
167
595
|
```
|
|
596
|
+
|
|
597
|
+
### Cache Issues
|
|
598
|
+
|
|
599
|
+
```typescript
|
|
600
|
+
import { clearHeaderCache, getCacheConfig } from "@zeroad.network/token";
|
|
601
|
+
|
|
602
|
+
// Check current config
|
|
603
|
+
console.log(getCacheConfig());
|
|
604
|
+
|
|
605
|
+
// Clear cache if needed
|
|
606
|
+
clearHeaderCache();
|
|
607
|
+
|
|
608
|
+
// Disable caching for debugging
|
|
609
|
+
configureCaching({ enabled: false });
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### Common Issues
|
|
613
|
+
|
|
614
|
+
1. **All flags are false** - Token is expired, invalid, or missing
|
|
615
|
+
2. **Performance slow** - Enable caching or increase cache size
|
|
616
|
+
3. **Token rejected** - Verify Client ID matches registered site
|
|
617
|
+
4. **Headers not sent** - Ensure middleware runs before routes
|
|
618
|
+
|
|
619
|
+
## API Reference
|
|
620
|
+
|
|
621
|
+
### `Site(options)`
|
|
622
|
+
|
|
623
|
+
Creates a site instance with helper methods. **This is the recommended way to use the module.**
|
|
624
|
+
|
|
625
|
+
```typescript
|
|
626
|
+
const site = Site({
|
|
627
|
+
clientId: "YOUR_CLIENT_ID",
|
|
628
|
+
features: [FEATURE.CLEAN_WEB],
|
|
629
|
+
cacheConfig: {
|
|
630
|
+
// optional
|
|
631
|
+
enabled: true,
|
|
632
|
+
ttl: 5000,
|
|
633
|
+
maxSize: 100,
|
|
634
|
+
},
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// Returns an object with:
|
|
638
|
+
site.parseClientToken(headerValue); // Parse and verify tokens
|
|
639
|
+
site.CLIENT_HEADER_NAME; // "x-better-web-hello"
|
|
640
|
+
site.SERVER_HEADER_NAME; // "X-Better-Web-Welcome"
|
|
641
|
+
site.SERVER_HEADER_VALUE; // Your site's welcome header value
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
**Options:**
|
|
645
|
+
|
|
646
|
+
- `clientId` (string, required) - Your site's Client ID from Zero Ad Network
|
|
647
|
+
- `features` (FEATURE[], required) - Array of enabled features
|
|
648
|
+
- `cacheConfig` (CacheConfig, optional) - Cache configuration
|
|
649
|
+
|
|
650
|
+
### `configureCaching(config)`
|
|
651
|
+
|
|
652
|
+
Configure global cache settings (applies to all Site instances).
|
|
653
|
+
|
|
654
|
+
```typescript
|
|
655
|
+
import { configureCaching } from "@zeroad.network/token";
|
|
656
|
+
|
|
657
|
+
configureCaching({
|
|
658
|
+
enabled: boolean, // Enable/disable caching
|
|
659
|
+
ttl: number, // Time-to-live in milliseconds
|
|
660
|
+
maxSize: number, // Maximum cache entries
|
|
661
|
+
});
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### `clearHeaderCache()`
|
|
665
|
+
|
|
666
|
+
Manually clear the token cache.
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
import { clearHeaderCache } from "@zeroad.network/token";
|
|
670
|
+
|
|
671
|
+
clearHeaderCache();
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### `setLogLevel(level)`
|
|
675
|
+
|
|
676
|
+
Set logging verbosity for debugging.
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
import { setLogLevel } from "@zeroad.network/token";
|
|
680
|
+
|
|
681
|
+
setLogLevel("debug"); // "error" | "warn" | "info" | "debug"
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### `setLogTransport(fn)`
|
|
685
|
+
|
|
686
|
+
Customize where logs are sent (useful for production monitoring).
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
import { setLogTransport } from "@zeroad.network/token";
|
|
690
|
+
|
|
691
|
+
// Send logs to your monitoring service
|
|
692
|
+
setLogTransport((level, ...args) => {
|
|
693
|
+
if (level === "error") {
|
|
694
|
+
yourMonitoringService.captureError(args);
|
|
695
|
+
} else {
|
|
696
|
+
yourLogger.log(level, ...args);
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// Example: Integrate with Winston
|
|
701
|
+
import winston from "winston";
|
|
702
|
+
|
|
703
|
+
const logger = winston.createLogger({
|
|
704
|
+
transports: [new winston.transports.File({ filename: "zeroad.log" })],
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
setLogTransport((level, ...args) => {
|
|
708
|
+
logger.log(level, args.join(" "));
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// Example: Disable all logging in production
|
|
712
|
+
if (process.env.NODE_ENV === "production") {
|
|
713
|
+
setLogTransport(() => {}); // No-op function
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
## Resources
|
|
718
|
+
|
|
719
|
+
- 📖 [Official Documentation](https://docs.zeroad.network)
|
|
720
|
+
- 🌐 [Zero Ad Network Platform](https://zeroad.network)
|
|
721
|
+
- 💻 [Example Implementations](https://github.com/laurynas-karvelis/zeroad-token-typescript/tree/main/examples/)
|
|
722
|
+
- 📝 [Blog](https://docs.zeroad.network/blog)
|
|
723
|
+
|
|
724
|
+
## Contributing
|
|
725
|
+
|
|
726
|
+
This module is open-source. Contributions are welcome! Please ensure:
|
|
727
|
+
|
|
728
|
+
- All tests pass
|
|
729
|
+
- Code follows existing style
|
|
730
|
+
- TypeScript types are complete
|
|
731
|
+
- Documentation is updated
|
|
732
|
+
|
|
733
|
+
## License
|
|
734
|
+
|
|
735
|
+
Apache License 2.0 - see LICENSE file for details
|
|
736
|
+
|
|
737
|
+
## About Zero Ad Network
|
|
738
|
+
|
|
739
|
+
Zero Ad Network is building a fairer internet where:
|
|
740
|
+
|
|
741
|
+
- Users enjoy cleaner, faster browsing
|
|
742
|
+
- Publishers earn sustainable revenue
|
|
743
|
+
- Privacy is respected by default
|
|
744
|
+
|
|
745
|
+
Join thousands of publishers creating a better web experience.
|
|
746
|
+
|
|
747
|
+
[Get Started →](https://zeroad.network/login)
|