@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/dist/index.mjs
CHANGED
|
@@ -1,16 +1,73 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { l as log, F as FEATURE, Z as ZEROAD_NETWORK_PUBLIC_KEY, f as fromBase64, P as PROTOCOL_VERSION, s as setFlags, t as toBase64, e as encodeServerHeader, S as SERVER_HEADER, C as CLIENT_HEADER } from './browser-VjxLaeuu.mjs';
|
|
2
|
+
export { c as CURRENT_PROTOCOL_VERSION, d as decodeServerHeader, a as setLogLevel, b as setLogTransport } from './browser-VjxLaeuu.mjs';
|
|
3
3
|
import { Buffer } from 'node:buffer';
|
|
4
4
|
import { verify as verify$1, randomBytes, sign as sign$1, createPublicKey, createPrivateKey } from 'node:crypto';
|
|
5
5
|
|
|
6
|
+
const DEFAULT_CACHE_CONFIG = {
|
|
7
|
+
enabled: true,
|
|
8
|
+
maxSize: 100,
|
|
9
|
+
ttl: 5e3
|
|
10
|
+
// 5 seconds
|
|
11
|
+
};
|
|
12
|
+
let cacheConfig = { ...DEFAULT_CACHE_CONFIG };
|
|
13
|
+
function configureCaching(config) {
|
|
14
|
+
if (config.ttl !== void 0 && config.ttl < 0) {
|
|
15
|
+
throw new Error("Cache TTL must be >= 0");
|
|
16
|
+
}
|
|
17
|
+
if (config.maxSize !== void 0 && config.maxSize < 1) {
|
|
18
|
+
throw new Error("Cache maxSize must be >= 1");
|
|
19
|
+
}
|
|
20
|
+
cacheConfig = { ...cacheConfig, ...config };
|
|
21
|
+
if (!cacheConfig.enabled) {
|
|
22
|
+
headerCache.clear();
|
|
23
|
+
}
|
|
24
|
+
trimCache();
|
|
25
|
+
log("debug", "Cache configuration updated", cacheConfig);
|
|
26
|
+
}
|
|
27
|
+
function getCacheConfig() {
|
|
28
|
+
return { ...cacheConfig };
|
|
29
|
+
}
|
|
30
|
+
const headerCache = /* @__PURE__ */ new Map();
|
|
31
|
+
function trimCache() {
|
|
32
|
+
if (headerCache.size <= cacheConfig.maxSize) return;
|
|
33
|
+
const entriesToRemove = headerCache.size - cacheConfig.maxSize;
|
|
34
|
+
const entries = Array.from(headerCache.entries());
|
|
35
|
+
entries.sort((a, b) => {
|
|
36
|
+
if (a[1].accessCount !== b[1].accessCount) {
|
|
37
|
+
return a[1].accessCount - b[1].accessCount;
|
|
38
|
+
}
|
|
39
|
+
return a[1].timestamp - b[1].timestamp;
|
|
40
|
+
});
|
|
41
|
+
for (let i = 0; i < entriesToRemove; i++) {
|
|
42
|
+
headerCache.delete(entries[i][0]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function cleanExpiredEntries(now) {
|
|
46
|
+
for (const [key, entry] of headerCache.entries()) {
|
|
47
|
+
if (entry.effectiveExpiry <= now) {
|
|
48
|
+
headerCache.delete(key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
6
53
|
const keyCache = /* @__PURE__ */ new Map();
|
|
7
54
|
function sign(data, privateKey) {
|
|
8
55
|
const key = importPrivateKey(privateKey);
|
|
9
|
-
return
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
sign$1(null, Buffer.from(data), key, (err, result) => {
|
|
58
|
+
if (err) reject(err);
|
|
59
|
+
else resolve(result);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
10
62
|
}
|
|
11
63
|
function verify(data, signature, publicKey) {
|
|
12
64
|
const key = importPublicKey(publicKey);
|
|
13
|
-
return
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
verify$1(null, Buffer.from(data), key, Buffer.from(signature), (err, result) => {
|
|
67
|
+
if (err) reject(err);
|
|
68
|
+
else resolve(result);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
14
71
|
}
|
|
15
72
|
const nonce = (size) => new Uint8Array(randomBytes(size));
|
|
16
73
|
function importPrivateKey(privateKeyBase64) {
|
|
@@ -37,55 +94,132 @@ function importPublicKey(publicKeyBase64) {
|
|
|
37
94
|
const VERSION_BYTES = 1;
|
|
38
95
|
const NONCE_BYTES = 4;
|
|
39
96
|
const SEPARATOR = ".";
|
|
40
|
-
const
|
|
41
|
-
|
|
97
|
+
const UINT32_BYTES = 4;
|
|
98
|
+
const FEATURE_TO_ACTIONS = Object.freeze({
|
|
99
|
+
[FEATURE.CLEAN_WEB]: Object.freeze([
|
|
42
100
|
"HIDE_ADVERTISEMENTS",
|
|
43
101
|
"HIDE_COOKIE_CONSENT_SCREEN",
|
|
44
102
|
"HIDE_MARKETING_DIALOGS",
|
|
45
103
|
"DISABLE_NON_FUNCTIONAL_TRACKING"
|
|
46
|
-
],
|
|
47
|
-
[FEATURE.ONE_PASS]: [
|
|
48
|
-
|
|
49
|
-
|
|
104
|
+
]),
|
|
105
|
+
[FEATURE.ONE_PASS]: Object.freeze([
|
|
106
|
+
"DISABLE_CONTENT_PAYWALL",
|
|
107
|
+
"ENABLE_SUBSCRIPTION_ACCESS"
|
|
108
|
+
])
|
|
109
|
+
});
|
|
110
|
+
const FEATURE_NUMBERS = Object.freeze([FEATURE.CLEAN_WEB, FEATURE.ONE_PASS]);
|
|
111
|
+
const EMPTY_CONTEXT = Object.freeze({
|
|
112
|
+
HIDE_ADVERTISEMENTS: false,
|
|
113
|
+
HIDE_COOKIE_CONSENT_SCREEN: false,
|
|
114
|
+
HIDE_MARKETING_DIALOGS: false,
|
|
115
|
+
DISABLE_NON_FUNCTIONAL_TRACKING: false,
|
|
116
|
+
DISABLE_CONTENT_PAYWALL: false,
|
|
117
|
+
ENABLE_SUBSCRIPTION_ACCESS: false
|
|
118
|
+
});
|
|
119
|
+
function createEmptyContext() {
|
|
120
|
+
return EMPTY_CONTEXT;
|
|
121
|
+
}
|
|
122
|
+
async function parseClientToken(headerValue, options) {
|
|
123
|
+
if (!headerValue || Array.isArray(headerValue) && !headerValue.length) {
|
|
124
|
+
return createEmptyContext();
|
|
125
|
+
}
|
|
50
126
|
const headerValueAsString = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
51
|
-
const
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
if (cacheConfig.enabled && !options.bypassCache) {
|
|
129
|
+
const cached = headerCache.get(headerValueAsString);
|
|
130
|
+
if (cached && cached.effectiveExpiry > now) {
|
|
131
|
+
cached.accessCount++;
|
|
132
|
+
return buildContext(cached.data, options, now);
|
|
133
|
+
} else if (cached) {
|
|
134
|
+
headerCache.delete(headerValueAsString);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const data = await decodeClientHeader(headerValueAsString, options.publicKey || ZEROAD_NETWORK_PUBLIC_KEY);
|
|
138
|
+
if (cacheConfig.enabled && !options.bypassCache) {
|
|
139
|
+
const cacheTTLExpiry = now + cacheConfig.ttl;
|
|
140
|
+
const tokenExpiry = data?.expiresAt.getTime() ?? 0;
|
|
141
|
+
const effectiveExpiry = tokenExpiry > 0 ? Math.min(cacheTTLExpiry, tokenExpiry) : cacheTTLExpiry;
|
|
142
|
+
headerCache.set(headerValueAsString, {
|
|
143
|
+
data,
|
|
144
|
+
timestamp: now,
|
|
145
|
+
accessCount: 1,
|
|
146
|
+
effectiveExpiry
|
|
147
|
+
});
|
|
148
|
+
if (headerCache.size > 0 && headerCache.size % 100 === 0) {
|
|
149
|
+
cleanExpiredEntries(now);
|
|
150
|
+
}
|
|
151
|
+
trimCache();
|
|
152
|
+
}
|
|
153
|
+
return buildContext(data, options, now);
|
|
154
|
+
}
|
|
155
|
+
function buildContext(data, options, now) {
|
|
52
156
|
let flags = 0;
|
|
53
|
-
if (data && data.expiresAt.getTime() >=
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
for (const [feature, actionNames] of Object.entries(FEATURE_TO_ACTIONS)) {
|
|
57
|
-
const decision = options.features.includes(Number(feature)) && hasFlag(Number(feature), flags);
|
|
58
|
-
for (const actionName of actionNames) {
|
|
59
|
-
context.set(actionName, decision);
|
|
157
|
+
if (data && data.expiresAt.getTime() >= now) {
|
|
158
|
+
if (!data.clientId || data.clientId === options.clientId) {
|
|
159
|
+
flags = data.flags;
|
|
60
160
|
}
|
|
61
161
|
}
|
|
62
|
-
|
|
162
|
+
if (!flags) {
|
|
163
|
+
return createEmptyContext();
|
|
164
|
+
}
|
|
165
|
+
const featuresSet = options.features.length <= 2 ? options.features : new Set(options.features);
|
|
166
|
+
const context = {};
|
|
167
|
+
for (let i = 0; i < FEATURE_NUMBERS.length; i++) {
|
|
168
|
+
const feature = FEATURE_NUMBERS[i];
|
|
169
|
+
const actionNames = FEATURE_TO_ACTIONS[feature];
|
|
170
|
+
const isEnabled = (Array.isArray(featuresSet) ? featuresSet.includes(feature) : featuresSet.has(feature)) && (flags & feature) !== 0;
|
|
171
|
+
const len = actionNames.length;
|
|
172
|
+
if (len > 0) context[actionNames[0]] = isEnabled;
|
|
173
|
+
if (len > 1) context[actionNames[1]] = isEnabled;
|
|
174
|
+
if (len > 2) context[actionNames[2]] = isEnabled;
|
|
175
|
+
if (len > 3) context[actionNames[3]] = isEnabled;
|
|
176
|
+
}
|
|
177
|
+
return context;
|
|
63
178
|
}
|
|
64
|
-
function decodeClientHeader(headerValue, publicKey) {
|
|
65
|
-
if (!headerValue?.length) return;
|
|
179
|
+
async function decodeClientHeader(headerValue, publicKey) {
|
|
180
|
+
if (!headerValue?.length) return void 0;
|
|
66
181
|
try {
|
|
67
|
-
const
|
|
182
|
+
const separatorIndex = headerValue.indexOf(SEPARATOR);
|
|
183
|
+
if (separatorIndex === -1) {
|
|
184
|
+
throw new Error("Invalid header format: missing separator");
|
|
185
|
+
}
|
|
186
|
+
const data = headerValue.substring(0, separatorIndex);
|
|
187
|
+
const signature = headerValue.substring(separatorIndex + 1);
|
|
68
188
|
const dataBytes = fromBase64(data);
|
|
69
189
|
const signatureBytes = fromBase64(signature);
|
|
70
|
-
if (!verify(dataBytes.buffer, signatureBytes.buffer, publicKey)) {
|
|
190
|
+
if (!await verify(dataBytes.buffer, signatureBytes.buffer, publicKey)) {
|
|
71
191
|
throw new Error("Forged header value is provided");
|
|
72
192
|
}
|
|
73
193
|
const version = dataBytes[0];
|
|
74
|
-
let clientId;
|
|
75
194
|
if (version === PROTOCOL_VERSION.V_1) {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (dataBytes.byteLength > expectedByteLength) {
|
|
80
|
-
clientId = new TextDecoder().decode(dataBytes.subarray(expectedByteLength));
|
|
195
|
+
const expectedMinLength = VERSION_BYTES + NONCE_BYTES + UINT32_BYTES * 2;
|
|
196
|
+
if (dataBytes.byteLength < expectedMinLength) {
|
|
197
|
+
throw new Error("Invalid data length");
|
|
81
198
|
}
|
|
82
|
-
|
|
199
|
+
const view = new DataView(dataBytes.buffer, dataBytes.byteOffset, dataBytes.byteLength);
|
|
200
|
+
const expiresAtOffset = VERSION_BYTES + NONCE_BYTES;
|
|
201
|
+
const flagsOffset = expiresAtOffset + UINT32_BYTES;
|
|
202
|
+
const expiresAt = view.getUint32(expiresAtOffset, true);
|
|
203
|
+
const flags = view.getUint32(flagsOffset, true);
|
|
204
|
+
let clientId;
|
|
205
|
+
if (dataBytes.byteLength > expectedMinLength) {
|
|
206
|
+
const clientIdBytes = dataBytes.subarray(expectedMinLength);
|
|
207
|
+
clientId = new TextDecoder().decode(clientIdBytes);
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
version,
|
|
211
|
+
expiresAt: new Date(expiresAt * 1e3),
|
|
212
|
+
flags,
|
|
213
|
+
...clientId && { clientId }
|
|
214
|
+
};
|
|
83
215
|
}
|
|
216
|
+
throw new Error(`Unsupported protocol version: ${version}`);
|
|
84
217
|
} catch (err) {
|
|
85
218
|
log("warn", "Could not decode client header value", { reason: err?.message });
|
|
219
|
+
return void 0;
|
|
86
220
|
}
|
|
87
221
|
}
|
|
88
|
-
function encodeClientHeader(data, privateKey) {
|
|
222
|
+
async function encodeClientHeader(data, privateKey) {
|
|
89
223
|
const payload = mergeByteArrays([
|
|
90
224
|
new Uint8Array([data.version]),
|
|
91
225
|
new Uint8Array(nonce(NONCE_BYTES)),
|
|
@@ -93,7 +227,7 @@ function encodeClientHeader(data, privateKey) {
|
|
|
93
227
|
new Uint32Array([setFlags(data.features)]),
|
|
94
228
|
...data.clientId?.length ? [new Uint8Array(new TextEncoder().encode(data.clientId))] : []
|
|
95
229
|
]);
|
|
96
|
-
return [toBase64(payload), toBase64(new Uint8Array(sign(payload.buffer, privateKey)))].join(SEPARATOR);
|
|
230
|
+
return [toBase64(payload), toBase64(new Uint8Array(await sign(payload.buffer, privateKey)))].join(SEPARATOR);
|
|
97
231
|
}
|
|
98
232
|
function mergeByteArrays(arrays) {
|
|
99
233
|
const totalLength = arrays.reduce((sum, a) => sum + a.byteLength, 0);
|
|
@@ -109,14 +243,12 @@ function mergeByteArrays(arrays) {
|
|
|
109
243
|
}
|
|
110
244
|
return data;
|
|
111
245
|
}
|
|
112
|
-
function as32BitNumber(byteArray, begin) {
|
|
113
|
-
const bytes = byteArray.subarray(begin, begin + Uint32Array.BYTES_PER_ELEMENT);
|
|
114
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
115
|
-
return view.getUint32(0, true);
|
|
116
|
-
}
|
|
117
246
|
|
|
118
247
|
function Site(options) {
|
|
119
248
|
const serverHeaderValue = encodeServerHeader(options.clientId, options.features);
|
|
249
|
+
if (options.cacheConfig) {
|
|
250
|
+
configureCaching(options.cacheConfig);
|
|
251
|
+
}
|
|
120
252
|
return {
|
|
121
253
|
parseClientToken: (headerValue) => parseClientToken(headerValue, { clientId: options.clientId, features: options.features }),
|
|
122
254
|
CLIENT_HEADER_NAME: CLIENT_HEADER.HELLO.toLowerCase(),
|
|
@@ -125,4 +257,4 @@ function Site(options) {
|
|
|
125
257
|
};
|
|
126
258
|
}
|
|
127
259
|
|
|
128
|
-
export { CLIENT_HEADER, FEATURE, PROTOCOL_VERSION, SERVER_HEADER, Site, ZEROAD_NETWORK_PUBLIC_KEY, decodeClientHeader, encodeClientHeader, encodeServerHeader, parseClientToken };
|
|
260
|
+
export { CLIENT_HEADER, FEATURE, PROTOCOL_VERSION, SERVER_HEADER, Site, ZEROAD_NETWORK_PUBLIC_KEY, cacheConfig, cleanExpiredEntries, configureCaching, decodeClientHeader, encodeClientHeader, encodeServerHeader, getCacheConfig, headerCache, parseClientToken, trimCache };
|
package/package.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeroad.network/token",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": "github:laurynas-karvelis/zeroad-token-typescript",
|
|
6
6
|
"homepage": "https://zeroad.network",
|
|
7
|
-
"private": false,
|
|
8
7
|
"author": {
|
|
9
8
|
"name": "Laurynas Karvelis <Explosive Brains Ltd>",
|
|
10
9
|
"email": "hello@zeroad.network",
|
|
@@ -70,7 +69,7 @@
|
|
|
70
69
|
"scripts": {
|
|
71
70
|
"keys:generate": "bun run ./src/tools/cli.ts",
|
|
72
71
|
"prettier": "prettier . --write",
|
|
73
|
-
"build": "pkgroll --target=node18",
|
|
72
|
+
"build": "pkgroll --target=esnext --target=node18",
|
|
74
73
|
"test": "bun test ./src"
|
|
75
74
|
},
|
|
76
75
|
"dependencies": {},
|