analytics-platform 0.0.0-stage → 0.2.1

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.
@@ -0,0 +1,453 @@
1
+ // Resources module (protocol in sdk/protocols/resources.md): how the machine
2
+ // is doing — CPU, load, memory, swap, disks — sampled every minute and sent to
3
+ // POST /events/resources every few minutes. The companion of the performance
4
+ // module: that says how the requests went, this says whether the box can still
5
+ // serve them, so the dashboard shows what you would otherwise run
6
+ // node_exporter, Prometheus and Grafana for.
7
+ //
8
+ // Nothing is guessed. A reading this machine won't give us is left out of the
9
+ // row, never sent as a zero: the collector stores it as NULL and the dashboard
10
+ // shows "—", because "0% CPU" and "we couldn't measure CPU" are different
11
+ // statements and only one of them means healthy.
12
+ "use strict";
13
+
14
+ const fs = require("node:fs");
15
+ const os = require("node:os");
16
+ const path = require("node:path");
17
+
18
+ // Keep in step with the collector (expresscollect/Analytics/resources.js) and
19
+ // the agent (sdk/vps-agent).
20
+ const SAMPLE_MS = 60 * 1000;
21
+ // Below this, two CPU readings are too close together to divide: the counters
22
+ // move in ticks, so a percentage over a few milliseconds is quantisation noise
23
+ // (0% or 100%, nothing between). A sample taken that soon reports no CPU
24
+ // figure rather than a made-up one.
25
+ const MIN_CPU_MS = 1000;
26
+ const EVERY_S = 5 * 60;
27
+ const RETRY_S = 60;
28
+ // About 8 hours of samples: enough to ride out a collector outage, bounded so
29
+ // a long one can't grow in memory.
30
+ const MAX_ROWS = 500;
31
+ const MAX_DISKS = 20;
32
+ // Nobody watches a sub-gigabyte partition fill up; this drops the EFI
33
+ // partition and other small mounts without needing a list of paths.
34
+ const MIN_DISK_MB = 1024;
35
+ // Real on-disk filesystems only: no tmpfs, devtmpfs, proc, sysfs, cgroup or
36
+ // squashfs (snaps), none of which are the customer's disk. overlay is here so
37
+ // a containerised app still reports something sensible.
38
+ const FSTYPES = new Set(["ext2", "ext3", "ext4", "xfs", "btrfs", "zfs", "f2fs", "jfs",
39
+ "reiserfs", "ufs", "vfat", "exfat", "ntfs", "ntfs3", "overlay", "apfs", "hfs"]);
40
+
41
+ const MB = 1024 * 1024;
42
+ const read = (file) => {
43
+ try {
44
+ return fs.readFileSync(file, "utf8");
45
+ } catch {
46
+ return "";
47
+ }
48
+ };
49
+
50
+ // (busy, total) across every core. idle is the machine doing nothing;
51
+ // everything else, including time stolen by the hypervisor, is time it wasn't
52
+ // available to us. os.cpus() rather than /proc/stat, so this works everywhere.
53
+ function cpuTimes(cpus = os.cpus()) {
54
+ let busy = 0;
55
+ let total = 0;
56
+ for (const cpu of cpus || []) {
57
+ const t = cpu?.times || {};
58
+ const idle = Number(t.idle) || 0;
59
+ const sum = idle + (Number(t.user) || 0) + (Number(t.nice) || 0) + (Number(t.sys) || 0) + (Number(t.irq) || 0);
60
+ busy += sum - idle;
61
+ total += sum;
62
+ }
63
+ return total ? { busy, total } : null;
64
+ }
65
+
66
+ // How busy the machine was between two readings, or null when the pair can't
67
+ // say: the first sample, a counter reset, or no time passed.
68
+ function cpuPercent(before, after) {
69
+ if (!before || !after) return null;
70
+ const busy = after.busy - before.busy;
71
+ const total = after.total - before.total;
72
+ if (total <= 0 || busy < 0) return null;
73
+ return Math.round(Math.min(100, (100 * busy) / total) * 10) / 10;
74
+ }
75
+
76
+ // MemTotal/MemAvailable/Swap from /proc/meminfo, in whole megabytes.
77
+ //
78
+ // Used is total - MemAvailable, never total - MemFree. Page cache is not
79
+ // pressure, and counting it as used makes every healthy Linux box look full —
80
+ // which is also why os.freemem() is not used here: on Linux it is MemFree.
81
+ function memoryMb(text) {
82
+ const values = {};
83
+ for (const line of String(text).split("\n")) {
84
+ const [name, rest] = line.split(":");
85
+ const kb = Number(String(rest || "").trim().split(/\s+/)[0]);
86
+ if (name && Number.isFinite(kb)) values[name] = Math.floor(kb / 1024);
87
+ }
88
+ const out = {};
89
+ if (values.MemTotal) {
90
+ out.mem_total_mb = values.MemTotal;
91
+ if (values.MemAvailable !== undefined) out.mem_used_mb = Math.max(0, values.MemTotal - values.MemAvailable);
92
+ }
93
+ if (values.SwapTotal !== undefined) {
94
+ out.swap_total_mb = values.SwapTotal;
95
+ if (values.SwapFree !== undefined) out.swap_used_mb = Math.max(0, values.SwapTotal - values.SwapFree);
96
+ }
97
+ return out;
98
+ }
99
+
100
+ // Where this machine keeps its filesystems. On Linux that's /proc/mounts; off
101
+ // it, the one filesystem the application is running from, which is the only
102
+ // one we can name without guessing.
103
+ // Whether a mount point is a directory, never throwing: a path this process
104
+ // cannot stat is simply unconfirmed, and mountPoints falls back to the
105
+ // device's shortest mount rather than dropping the disk.
106
+ function defaultIsDir(point) {
107
+ try {
108
+ return fs.statSync(point).isDirectory();
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
114
+ // A mount point has to be a directory. Docker bind-mounts single files, so a
115
+ // container lists /etc/hosts, /etc/hostname and /etc/resolv.conf on the host's
116
+ // own device, and those paths are short enough to win the shortest-path race
117
+ // against the real mount. The test is per-device and advisory: a device whose
118
+ // mounts cannot be confirmed keeps its shortest one, because reporting no
119
+ // disks is worse than reporting an odd mount point.
120
+ function mountPoints(mountsText, platform = process.platform, isDir = defaultIsDir) {
121
+ if (platform !== "linux") return [path.parse(process.cwd()).root || "/"];
122
+ const dirs = new Map();
123
+ const any = new Map();
124
+ for (const line of String(mountsText).split("\n")) {
125
+ const [device, mount, fstype] = line.split(/\s+/);
126
+ if (!device || !mount || !FSTYPES.has(fstype) || !mount.startsWith("/")) continue;
127
+ const point = mount.replace(/\\040/g, " ");
128
+ // The shortest path wins: "/" over "/var" for one device, so a bind mount
129
+ // isn't reported as a second disk.
130
+ if (!any.has(device) || point.length < any.get(device).length) any.set(device, point);
131
+ if (isDir(point) && (!dirs.has(device) || point.length < dirs.get(device).length)) dirs.set(device, point);
132
+ }
133
+ const points = [...any].map(([device, point]) => dirs.get(device) ?? point);
134
+ return [...new Set(points)].sort();
135
+ }
136
+
137
+ // Both figures are df's, so a customer can check them: used is total minus
138
+ // free, and total is used plus what's available to us. The blocks reserved for
139
+ // root are in neither, which is what makes used/total the percentage df prints
140
+ // — and that percentage is what the disk alert is about.
141
+ function diskRows(points, statfs = fs.statfsSync) {
142
+ if (typeof statfs !== "function") return [];
143
+ // One filesystem can sit behind two devices the kernel calls different: in a
144
+ // container / is the overlay and a bind mount exposes the host disk the
145
+ // overlay's upperdir lives on, so both report the same size and the same
146
+ // free space down to the byte. No device-based rule separates them, so the
147
+ // sizes are what gives it away. Raw bytes, not rounded megabytes, so disks
148
+ // that merely round alike stay apart; two separate but identically sized and
149
+ // identically empty filesystems do collapse into one until something is
150
+ // written to either, and being empty they owe the fullness alert nothing.
151
+ const seen = new Set();
152
+ const rows = [];
153
+ for (const mount of points) {
154
+ let st;
155
+ try {
156
+ st = statfs(mount);
157
+ } catch {
158
+ continue;
159
+ }
160
+ const block = Number(st.bsize) || 0;
161
+ const blocks = Number(st.blocks) || 0;
162
+ if (!block || !blocks) continue;
163
+ const used = Math.max(0, blocks - Number(st.bfree || 0)) * block;
164
+ const total = used + Number(st.bavail || 0) * block;
165
+ const row = { mount, total_mb: Math.floor(total / MB), used_mb: Math.floor(used / MB) };
166
+ if (row.total_mb < MIN_DISK_MB) continue;
167
+ // points arrives sorted, so the shortest path wins the collision.
168
+ const key = `${total}:${used}`;
169
+ if (seen.has(key)) continue;
170
+ seen.add(key);
171
+ // A filesystem can refuse a write with gigabytes free once these meet.
172
+ const nodes = Number(st.files) || 0;
173
+ if (nodes) {
174
+ row.inodes_total = nodes;
175
+ row.inodes_used = Math.max(0, nodes - (Number(st.ffree) || 0));
176
+ }
177
+ rows.push(row);
178
+ if (rows.length >= MAX_DISKS) break;
179
+ }
180
+ return rows;
181
+ }
182
+
183
+ // Sectors in /proc/diskstats are always 512 bytes: it is the unit of that
184
+ // interface, not of the drive.
185
+ const IO_SECTOR = 512;
186
+ // Two readings further apart than this say nothing about this minute.
187
+ const IO_MAX_PAIR_MS = 300 * 1000;
188
+ // Not disks: loop and ram devices, optical drives, device-mapper and md, each
189
+ // of which would count the devices underneath it a second time.
190
+ const NOT_A_DISK = /^(?:loop|ram|zram|sr|fd|dm-|md\d)/;
191
+ // Partitions, which must not be counted alongside the disk they sit on.
192
+ const PARTITION = /^(?:(?:sd|vd|xvd|hd)[a-z]+\d+|(?:nvme\d+n\d+|mmcblk\d+)p\d+)$/;
193
+ // Loopback, and the virtual ends of container and VM networking: counting
194
+ // these would count someone else's traffic, or ours twice.
195
+ const NOT_REAL_NET = /^(?:lo|veth|docker|br-|virbr|tap|tun|vnet)/;
196
+
197
+ // The jiffie counters from /proc/stat's first line, or null. In order: user,
198
+ // nice, system, idle, iowait, irq, softirq, steal. Linux only — os.cpus()
199
+ // carries no iowait and no steal, which are the two worth having.
200
+ function cpuFields(text) {
201
+ for (const line of String(text).split("\n")) {
202
+ if (!line.startsWith("cpu ")) continue;
203
+ const fields = line.trim().split(/\s+/).slice(1).map(Number);
204
+ if (fields.length < 4 || fields.some((value) => !Number.isFinite(value))) return null;
205
+ return fields;
206
+ }
207
+ return null;
208
+ }
209
+
210
+ // user folds nice in, as top adds them together. Each is a share of the whole
211
+ // interval, so with idle and the interrupt buckets they come to 100.
212
+ const CPU_SHARES = [
213
+ ["cpu_user_pct", [0, 1]],
214
+ ["cpu_system_pct", [2]],
215
+ ["cpu_iowait_pct", [4]],
216
+ ["cpu_steal_pct", [7]],
217
+ ];
218
+
219
+ // Where the machine's time went between two readings. A column the kernel is
220
+ // too old to carry is left out rather than sent as a zero. steal is the one
221
+ // worth surfacing: time the hypervisor gave to another tenant, which no amount
222
+ // of tuning the app will change.
223
+ function cpuShares(before, after) {
224
+ if (!before || !after) return {};
225
+ const total = after.reduce((sum, v) => sum + v, 0) - before.reduce((sum, v) => sum + v, 0);
226
+ if (total <= 0) return {};
227
+ const out = {};
228
+ for (const [name, columns] of CPU_SHARES) {
229
+ if (columns.some((i) => i >= before.length || i >= after.length)) continue;
230
+ const moved = columns.reduce((sum, i) => sum + (after[i] - before[i]), 0);
231
+ if (moved < 0) return {};
232
+ out[name] = Math.round(Math.min(100, (100 * moved) / total) * 10) / 10;
233
+ }
234
+ return out;
235
+ }
236
+
237
+ // Bytes read and written since boot over the real disks, or null.
238
+ function diskCounters(text) {
239
+ let read_ = 0;
240
+ let written = 0;
241
+ let found = false;
242
+ for (const line of String(text).split("\n")) {
243
+ const fields = line.trim().split(/\s+/);
244
+ if (fields.length < 10) continue;
245
+ const name = fields[2];
246
+ if (!name || NOT_A_DISK.test(name) || PARTITION.test(name)) continue;
247
+ const sectorsRead = Number(fields[5]);
248
+ const sectorsWritten = Number(fields[9]);
249
+ if (!Number.isFinite(sectorsRead) || !Number.isFinite(sectorsWritten)) continue;
250
+ read_ += sectorsRead * IO_SECTOR;
251
+ written += sectorsWritten * IO_SECTOR;
252
+ found = true;
253
+ }
254
+ return found ? [read_, written] : null;
255
+ }
256
+
257
+ // Bytes in and out since boot over the real interfaces, or null.
258
+ function netCounters(text) {
259
+ let rx = 0;
260
+ let tx = 0;
261
+ let found = false;
262
+ for (const line of String(text).split("\n")) {
263
+ const at = line.indexOf(":");
264
+ if (at < 0) continue;
265
+ const name = line.slice(0, at).trim();
266
+ if (!name || NOT_REAL_NET.test(name)) continue;
267
+ const fields = line.slice(at + 1).trim().split(/\s+/).map(Number);
268
+ if (fields.length < 9) continue;
269
+ if (!Number.isFinite(fields[0]) || !Number.isFinite(fields[8])) continue;
270
+ rx += fields[0];
271
+ tx += fields[8];
272
+ found = true;
273
+ }
274
+ return found ? [rx, tx] : null;
275
+ }
276
+
277
+ // Megabytes a second between two byte counters, or null when the pair can't
278
+ // say: no pair, a reboot that reset them, no time, or too long a gap.
279
+ function rateMbs(before, after, ms) {
280
+ if (before === null || before === undefined || after === null || after === undefined) return null;
281
+ if (!(ms > 0) || ms > IO_MAX_PAIR_MS) return null;
282
+ const moved = after - before;
283
+ if (moved < 0) return null;
284
+ return Math.round((moved / (ms / 1000) / MB) * 10) / 10;
285
+ }
286
+
287
+ function createResources(core, options = {}) {
288
+ const log = (...args) => core.log("resources", ...args);
289
+ const serverName = String(options.serverName ?? os.hostname()).slice(0, 100);
290
+ const runtime = `Node ${process.versions.node}`;
291
+ // minute (ISO) -> row, so a minute sampled twice is kept once.
292
+ const rows = new Map();
293
+ let lastCpu = cpuTimes();
294
+ let lastFields = cpuFields(read("/proc/stat"));
295
+ let lastDisk = diskCounters(read("/proc/diskstats"));
296
+ let lastNet = netCounters(read("/proc/net/dev"));
297
+ let lastAt = Date.now();
298
+ let every = EVERY_S;
299
+ let sampleTimer = null;
300
+ let postTimer = null;
301
+ let closed = false;
302
+ let loop = null;
303
+ // The event loop's own delay: how long the application itself was blocked,
304
+ // which no outside measurement of the machine can see.
305
+ try {
306
+ const { monitorEventLoopDelay } = require("node:perf_hooks");
307
+ if (typeof monitorEventLoopDelay === "function") {
308
+ loop = monitorEventLoopDelay({ resolution: 20 });
309
+ loop.enable();
310
+ }
311
+ } catch {
312
+ loop = null;
313
+ }
314
+
315
+ // One sample, covering the time since the last one.
316
+ function sample(at = Date.now()) {
317
+ const cpu = cpuTimes();
318
+ const percent = cpuPercent(lastCpu, cpu);
319
+ const elapsed = at - lastAt;
320
+ // Everything below needs two readings, like cpu_pct does.
321
+ const fields = cpuFields(read("/proc/stat"));
322
+ const disk = diskCounters(read("/proc/diskstats"));
323
+ const net = netCounters(read("/proc/net/dev"));
324
+ // The minute the interval started in, not the one it ended in.
325
+ const row = { minute: new Date(Math.floor(lastAt / 60000) * 60000).toISOString() };
326
+ lastCpu = cpu;
327
+ lastAt = at;
328
+ if (percent !== null && elapsed >= MIN_CPU_MS) row.cpu_pct = percent;
329
+ const cores = os.cpus()?.length;
330
+ if (cores) row.cpu_cores = cores;
331
+ const load = os.loadavg?.() || [];
332
+ // Windows has no load average and reports zeroes; sending them would be a
333
+ // measurement we never took.
334
+ if (load.length === 3 && load.some((value) => value > 0)) {
335
+ row.load1 = Math.round(load[0] * 100) / 100;
336
+ row.load5 = Math.round(load[1] * 100) / 100;
337
+ row.load15 = Math.round(load[2] * 100) / 100;
338
+ }
339
+ const memory = memoryMb(read("/proc/meminfo"));
340
+ if (memory.mem_total_mb) {
341
+ Object.assign(row, memory);
342
+ } else if (os.totalmem()) {
343
+ // Off Linux: os.freemem() is the best this platform offers.
344
+ row.mem_total_mb = Math.floor(os.totalmem() / MB);
345
+ row.mem_used_mb = Math.max(0, Math.floor((os.totalmem() - os.freemem()) / MB));
346
+ }
347
+ const disks = diskRows(mountPoints(read("/proc/mounts")));
348
+ if (disks.length) row.disks = disks;
349
+ Object.assign(row, cpuShares(lastFields, fields));
350
+ const rates = {
351
+ disk_read_mbs: lastDisk && disk ? rateMbs(lastDisk[0], disk[0], elapsed) : null,
352
+ disk_write_mbs: lastDisk && disk ? rateMbs(lastDisk[1], disk[1], elapsed) : null,
353
+ net_rx_mbs: lastNet && net ? rateMbs(lastNet[0], net[0], elapsed) : null,
354
+ net_tx_mbs: lastNet && net ? rateMbs(lastNet[1], net[1], elapsed) : null,
355
+ };
356
+ for (const [name, value] of Object.entries(rates)) {
357
+ if (value !== null) row[name] = value;
358
+ }
359
+ lastFields = fields;
360
+ lastDisk = disk;
361
+ lastNet = net;
362
+ const rss = process.memoryUsage?.().rss;
363
+ if (rss) row.proc_rss_mb = Math.max(1, Math.floor(rss / MB));
364
+ if (loop) {
365
+ row.loop_lag_ms = Math.round((loop.mean / 1e6) * 100) / 100;
366
+ loop.reset();
367
+ }
368
+ const up = os.uptime?.();
369
+ if (Number.isFinite(up) && up > 0) row.host_uptime_s = Math.floor(up);
370
+ rows.set(row.minute, row);
371
+ // Oldest first out: a long outage keeps the recent minutes.
372
+ while (rows.size > MAX_ROWS) rows.delete(rows.keys().next().value);
373
+ return row;
374
+ }
375
+
376
+ // Sends what's buffered. Rows are kept only when the collector couldn't take
377
+ // them and retrying may work.
378
+ async function flush() {
379
+ if (!rows.size) return true;
380
+ const batch = [...rows.keys()].sort();
381
+ const body = { site_id: core.siteId, server_name: serverName, runtime, rows: batch.map((minute) => rows.get(minute)) };
382
+ const res = await core.send("resources", "/events/resources", { auth: "bearer", body });
383
+ if (!res) return false;
384
+ if (res.ok) {
385
+ const data = await res.json().catch(() => null);
386
+ if (Number(data?.every) > 0) every = Math.max(RETRY_S, Number(data.every));
387
+ if (data?.enabled === false) log("server resources are off in the dashboard (Security → Settings → Server package)");
388
+ for (const minute of batch) rows.delete(minute);
389
+ return true;
390
+ }
391
+ // Unreachable or storage down: these rows are still good. A 4xx means
392
+ // resending won't make them acceptable.
393
+ if (res.status >= 500) return false;
394
+ log("collector answered %s to the resources report", res.status);
395
+ for (const minute of batch) rows.delete(minute);
396
+ return true;
397
+ }
398
+
399
+ async function report() {
400
+ let wait = every;
401
+ try {
402
+ if (core.isOn("resources")) {
403
+ if (!(await flush())) wait = RETRY_S;
404
+ } else {
405
+ // Switched off: keep nothing, and ask again later.
406
+ rows.clear();
407
+ wait = Math.max(600, every);
408
+ }
409
+ } catch (error) {
410
+ log("could not send the resources report:", error?.message);
411
+ wait = RETRY_S;
412
+ }
413
+ schedule(wait * 1000);
414
+ }
415
+
416
+ function schedule(ms) {
417
+ if (closed) return;
418
+ postTimer = setTimeout(report, ms);
419
+ postTimer.unref?.();
420
+ }
421
+
422
+ if (core.valid) {
423
+ sampleTimer = setInterval(() => {
424
+ try {
425
+ sample();
426
+ } catch (error) {
427
+ log("could not sample the machine:", error?.message);
428
+ }
429
+ }, SAMPLE_MS);
430
+ sampleTimer.unref?.();
431
+ schedule(EVERY_S * 1000);
432
+ }
433
+
434
+ return {
435
+ // One sample now (for your own checks and tests).
436
+ sample,
437
+ // Send what's buffered now.
438
+ flush,
439
+ report,
440
+ close() {
441
+ closed = true;
442
+ clearInterval(sampleTimer);
443
+ clearTimeout(postTimer);
444
+ loop?.disable?.();
445
+ return flush().catch(() => false);
446
+ },
447
+ };
448
+ }
449
+
450
+ module.exports = {
451
+ createResources, cpuTimes, cpuPercent, memoryMb, mountPoints, diskRows, MIN_DISK_MB,
452
+ cpuFields, cpuShares, diskCounters, netCounters, rateMbs,
453
+ };
package/next.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ export interface NextFirewallOptions {
2
+ siteId: string;
3
+ /** The site secret (site settings). Keep it in an environment variable. */
4
+ secret: string;
5
+ /** The collector's URL. */
6
+ endpoint: string;
7
+ /**
8
+ * How to find the visitor's address. Default on Vercel: "vercel". Self-hosted
9
+ * Next.js has no connection address in middleware: set the number of proxies
10
+ * in X-Forwarded-For, "cloudflare", or a function.
11
+ */
12
+ trustProxy?: "vercel" | "cloudflare" | number | ((request: Request) => string);
13
+ /** Overrides the mode set in the dashboard. */
14
+ mode?: "monitor" | "block";
15
+ /** Return true to leave a request unchecked. */
16
+ skip?: (request: Request) => boolean;
17
+ /** A rate-limit counter shared between instances (e.g. Upstash Redis): returns the hits so far. */
18
+ counter?: (key: string, ttlSeconds: number) => Promise<number> | number;
19
+ debug?: boolean;
20
+ }
21
+
22
+ /** Next.js middleware: returns a Response when blocked, else undefined (the request goes on). */
23
+ export function createNextFirewall(options: NextFirewallOptions):
24
+ (request: Request, event?: { waitUntil(promise: Promise<unknown>): void }) => Promise<Response | undefined>;
25
+
26
+ export interface OnRequestErrorOptions {
27
+ siteId: string;
28
+ secret: string;
29
+ endpoint: string;
30
+ debug?: boolean;
31
+ }
32
+
33
+ /** For instrumentation.ts: export const onRequestError = createOnRequestError({...}). */
34
+ export function createOnRequestError(options: OnRequestErrorOptions):
35
+ (error: unknown, request: { path: string; method: string; headers: Record<string, string | string[] | undefined> },
36
+ context: { routePath?: string; routerKind?: string; routeType?: string }) => Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "analytics-platform",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.1",
4
+ "description": "Firewall, error monitoring and server analytics for your Node server, run from your analytics dashboard.",
5
+ "author": "steviec87",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "firewall",
9
+ "waf",
10
+ "rate-limit",
11
+ "error-monitoring",
12
+ "analytics",
13
+ "server-monitoring",
14
+ "express",
15
+ "nextjs"
16
+ ],
17
+ "main": "index.js",
18
+ "types": "index.d.ts",
19
+ "files": [
20
+ "index.js",
21
+ "index.d.ts",
22
+ "next.d.ts",
23
+ "lib/",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "exports": {
31
+ ".": {
32
+ "types": "./index.d.ts",
33
+ "default": "./index.js"
34
+ },
35
+ "./next": {
36
+ "types": "./next.d.ts",
37
+ "default": "./lib/next.js"
38
+ }
39
+ },
40
+ "scripts": {
41
+ "test": "node --test test/*.test.js"
42
+ }
43
+ }