@footfall/next 0.1.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/LICENSE +21 -0
- package/dist/beacon-Ht4IlMXQ.d.ts +38 -0
- package/dist/beacon.d.ts +2 -0
- package/dist/beacon.js +1 -0
- package/dist/chunk-W6AJB5JH.js +57 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +624 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nitin Murali
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { IngestPayload, Correction } from '@footfall/core/ingest';
|
|
2
|
+
|
|
3
|
+
/** Runtime config, from env vars only (no code changes to toggle behaviour). */
|
|
4
|
+
interface FootfallConfig {
|
|
5
|
+
/** Per-site write token. No token → middleware is inert. */
|
|
6
|
+
token: string | null;
|
|
7
|
+
/** Where to POST events. */
|
|
8
|
+
ingestUrl: string;
|
|
9
|
+
/** Kill switch: FOOTFALL_DISABLED=1 stops all emission. */
|
|
10
|
+
disabled: boolean;
|
|
11
|
+
/** Server-side salt base for IP hashing; combined with the date for daily rotation. */
|
|
12
|
+
ipSalt: string;
|
|
13
|
+
/** Per-instance daily soft cap; 0 = uncapped. Above it, events are sampled. */
|
|
14
|
+
dailyCap: number;
|
|
15
|
+
/** Hard timeout on the ingest send (ms). */
|
|
16
|
+
timeoutMs: number;
|
|
17
|
+
}
|
|
18
|
+
declare function readConfig(env?: Record<string, string | undefined>): FootfallConfig;
|
|
19
|
+
/** Whether the middleware should emit at all (token present + not killed). */
|
|
20
|
+
declare function isActive(cfg: FootfallConfig): boolean;
|
|
21
|
+
|
|
22
|
+
type FetchLike = typeof fetch;
|
|
23
|
+
/**
|
|
24
|
+
* Fire-and-forget send with a hard timeout and one retry. NEVER throws and never
|
|
25
|
+
* rejects — all failures are swallowed so the site is never affected by ingest health.
|
|
26
|
+
* Returns whether the send eventually succeeded (for tests/metrics only).
|
|
27
|
+
*/
|
|
28
|
+
declare function sendBatch(url: string, token: string, batch: IngestPayload, timeoutMs: number, fetchImpl?: FetchLike): Promise<boolean>;
|
|
29
|
+
|
|
30
|
+
/** POST a status correction. Fire-and-forget: never throws, swallows all failures. */
|
|
31
|
+
declare function sendCorrection(cfg: FootfallConfig, correction: Correction, fetchImpl?: FetchLike): Promise<boolean>;
|
|
32
|
+
/** Call from `instrumentation.ts` `onRequestError` to emit a 5xx correction. */
|
|
33
|
+
declare function footfallOnRequestError(_error: unknown, request: {
|
|
34
|
+
headers?: Headers | Record<string, string | undefined>;
|
|
35
|
+
path?: string;
|
|
36
|
+
}): Promise<void>;
|
|
37
|
+
|
|
38
|
+
export { type FootfallConfig as F, type FetchLike as a, sendCorrection as b, footfallOnRequestError as f, isActive as i, readConfig as r, sendBatch as s };
|
package/dist/beacon.d.ts
ADDED
package/dist/beacon.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { footfallOnRequestError, sendCorrection } from './chunk-W6AJB5JH.js';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
function truthy(v) {
|
|
3
|
+
return v === "1" || v === "true" || v === "yes";
|
|
4
|
+
}
|
|
5
|
+
function readConfig(env = process.env) {
|
|
6
|
+
return {
|
|
7
|
+
token: env.FOOTFALL_TOKEN ?? null,
|
|
8
|
+
ingestUrl: env.FOOTFALL_INGEST_URL ?? "",
|
|
9
|
+
disabled: truthy(env.FOOTFALL_DISABLED),
|
|
10
|
+
ipSalt: env.FOOTFALL_IP_SALT ?? "footfall",
|
|
11
|
+
dailyCap: Number.parseInt(env.FOOTFALL_DAILY_CAP ?? "0", 10) || 0,
|
|
12
|
+
timeoutMs: Number.parseInt(env.FOOTFALL_TIMEOUT_MS ?? "500", 10) || 500
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function isActive(cfg) {
|
|
16
|
+
return !cfg.disabled && cfg.token !== null && cfg.token !== "" && cfg.ingestUrl !== "";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/beacon.ts
|
|
20
|
+
async function sendCorrection(cfg, correction, fetchImpl = fetch) {
|
|
21
|
+
const token = cfg.token;
|
|
22
|
+
if (!isActive(cfg) || !token) return false;
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetchImpl(cfg.ingestUrl, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
29
|
+
body: JSON.stringify({ site: token, corrections: [correction] }),
|
|
30
|
+
signal: controller.signal,
|
|
31
|
+
keepalive: true
|
|
32
|
+
});
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
return res.ok;
|
|
35
|
+
} catch {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function readHeader(h, key) {
|
|
41
|
+
if (typeof h.get === "function") return h.get(key);
|
|
42
|
+
return h[key] ?? null;
|
|
43
|
+
}
|
|
44
|
+
async function footfallOnRequestError(_error, request) {
|
|
45
|
+
const cfg = readConfig();
|
|
46
|
+
if (!isActive(cfg) || !request.headers) return;
|
|
47
|
+
const requestId = readHeader(request.headers, "x-footfall-id");
|
|
48
|
+
if (!requestId) return;
|
|
49
|
+
await sendCorrection(cfg, {
|
|
50
|
+
request_id: requestId,
|
|
51
|
+
status: 500,
|
|
52
|
+
path: request.path ?? readHeader(request.headers, "x-footfall-path"),
|
|
53
|
+
ts: Date.now()
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { footfallOnRequestError, isActive, readConfig, sendCorrection };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { NextRequest, NextFetchEvent } from 'next/server';
|
|
2
|
+
import { F as FootfallConfig } from './beacon-Ht4IlMXQ.js';
|
|
3
|
+
export { a as FetchLike, f as footfallOnRequestError, i as isActive, r as readConfig, s as sendBatch, b as sendCorrection } from './beacon-Ht4IlMXQ.js';
|
|
4
|
+
import { Event } from '@footfall/core/schema';
|
|
5
|
+
import '@footfall/core/ingest';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build an Event from a request. Middleware runs before the response, so `status`,
|
|
9
|
+
* `resp_bytes`, `content_type`, and `duration_ms` are null here — they're filled by
|
|
10
|
+
* server-side beacons (404/5xx) or left for the coverage matrix to handle.
|
|
11
|
+
*/
|
|
12
|
+
declare function buildEvent(req: Request, opts: {
|
|
13
|
+
site: string;
|
|
14
|
+
ipSalt: string;
|
|
15
|
+
nowMs: number;
|
|
16
|
+
}): Promise<Event>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Edge-runtime IP hashing (Web Crypto, not node:crypto). The salt rotates daily so
|
|
20
|
+
* sessions stitch within a day but the raw IP is never stored or reversible across days.
|
|
21
|
+
*/
|
|
22
|
+
/** The salt in effect for the day containing `nowMs`. */
|
|
23
|
+
declare function dailySalt(saltBase: string, nowMs: number): string;
|
|
24
|
+
/** SHA-256 of (dailySalt + ip), hex, truncated to 32 chars. Async (crypto.subtle). */
|
|
25
|
+
declare function hashIp(ip: string, saltBase: string, nowMs: number): Promise<string>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Best-effort per-instance daily cap with graceful sampling above it. Edge instances
|
|
29
|
+
* are stateless/distributed, so this is a soft guard; the authoritative cap is enforced
|
|
30
|
+
* at ingest (Phase 6). Crucially, dropped counts are never silent — the next emitted
|
|
31
|
+
* batch carries the accumulated `dropped` so truncation is always visible.
|
|
32
|
+
*/
|
|
33
|
+
declare class Sampler {
|
|
34
|
+
private readonly cap;
|
|
35
|
+
/** Above the cap, keep 1 in `keepEvery`. */
|
|
36
|
+
private readonly keepEvery;
|
|
37
|
+
private day;
|
|
38
|
+
private count;
|
|
39
|
+
private dropped;
|
|
40
|
+
constructor(cap: number,
|
|
41
|
+
/** Above the cap, keep 1 in `keepEvery`. */
|
|
42
|
+
keepEvery?: number);
|
|
43
|
+
private roll;
|
|
44
|
+
private takeDropped;
|
|
45
|
+
/** Decide whether to emit this request; when emitting, hand back accumulated drops. */
|
|
46
|
+
next(nowMs: number): {
|
|
47
|
+
emit: boolean;
|
|
48
|
+
dropped: number;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A Next.js middleware function (what `middleware.ts` exports). */
|
|
53
|
+
type MiddlewareFn = (req: NextRequest, ev: NextFetchEvent) => Response | Promise<Response | undefined> | undefined;
|
|
54
|
+
interface WithFootfallOptions {
|
|
55
|
+
/** Override env config (mainly for tests). */
|
|
56
|
+
config?: FootfallConfig;
|
|
57
|
+
/** Inject fetch (mainly for tests). */
|
|
58
|
+
fetchImpl?: typeof fetch;
|
|
59
|
+
/** Inject the clock (mainly for tests). */
|
|
60
|
+
now?: () => number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Wrap a site's middleware (or nothing) with observe-only event capture. The wrapped
|
|
64
|
+
* middleware's response is returned unchanged; capture happens in `event.waitUntil`,
|
|
65
|
+
* so ingest health can never affect the site.
|
|
66
|
+
*/
|
|
67
|
+
declare function withFootfall(existing?: MiddlewareFn, opts?: WithFootfallOptions): MiddlewareFn;
|
|
68
|
+
|
|
69
|
+
export { FootfallConfig, type MiddlewareFn, Sampler, type WithFootfallOptions, buildEvent, dailySalt, hashIp, withFootfall };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import { readConfig, isActive } from './chunk-W6AJB5JH.js';
|
|
2
|
+
export { footfallOnRequestError, isActive, readConfig, sendCorrection } from './chunk-W6AJB5JH.js';
|
|
3
|
+
import { NextResponse } from 'next/server';
|
|
4
|
+
|
|
5
|
+
// ../core/src/verify/cidr.ts
|
|
6
|
+
function parseV4(s) {
|
|
7
|
+
const m = s.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
8
|
+
if (!m) return null;
|
|
9
|
+
let v = 0n;
|
|
10
|
+
for (let i = 1; i <= 4; i++) {
|
|
11
|
+
const o = Number(m[i]);
|
|
12
|
+
if (o > 255) return null;
|
|
13
|
+
v = v << 8n | BigInt(o);
|
|
14
|
+
}
|
|
15
|
+
return v;
|
|
16
|
+
}
|
|
17
|
+
function parseV6(input) {
|
|
18
|
+
let s = input;
|
|
19
|
+
if (s.includes(".")) {
|
|
20
|
+
const idx = s.lastIndexOf(":");
|
|
21
|
+
const v4 = parseV4(s.slice(idx + 1));
|
|
22
|
+
if (v4 === null) return null;
|
|
23
|
+
const hi = Number(v4 >> 16n & 0xffffn);
|
|
24
|
+
const lo = Number(v4 & 0xffffn);
|
|
25
|
+
s = `${s.slice(0, idx + 1)}${hi.toString(16)}:${lo.toString(16)}`;
|
|
26
|
+
}
|
|
27
|
+
const halves = s.split("::");
|
|
28
|
+
if (halves.length > 2) return null;
|
|
29
|
+
const head = halves[0] ? halves[0].split(":") : [];
|
|
30
|
+
const tail = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
|
|
31
|
+
const missing = 8 - head.length - tail.length;
|
|
32
|
+
if (halves.length === 1 && head.length !== 8) return null;
|
|
33
|
+
if (missing < 0) return null;
|
|
34
|
+
const groups = [
|
|
35
|
+
...head,
|
|
36
|
+
...halves.length === 2 ? Array(missing).fill("0") : [],
|
|
37
|
+
...tail
|
|
38
|
+
];
|
|
39
|
+
if (groups.length !== 8) return null;
|
|
40
|
+
let v = 0n;
|
|
41
|
+
for (const g of groups) {
|
|
42
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(g)) return null;
|
|
43
|
+
v = v << 16n | BigInt(Number.parseInt(g, 16));
|
|
44
|
+
}
|
|
45
|
+
return v;
|
|
46
|
+
}
|
|
47
|
+
function ipInCidr(ip, cidr) {
|
|
48
|
+
const slash = cidr.lastIndexOf("/");
|
|
49
|
+
if (slash < 0) return false;
|
|
50
|
+
const base = cidr.slice(0, slash);
|
|
51
|
+
const prefix = Number(cidr.slice(slash + 1));
|
|
52
|
+
const isV6 = cidr.includes(":");
|
|
53
|
+
const bits = isV6 ? 128 : 32;
|
|
54
|
+
if (!Number.isInteger(prefix) || prefix < 0 || prefix > bits) return false;
|
|
55
|
+
const ipVal = isV6 ? parseV6(ip) : parseV4(ip);
|
|
56
|
+
const baseVal = isV6 ? parseV6(base) : parseV4(base);
|
|
57
|
+
if (ipVal === null || baseVal === null) return false;
|
|
58
|
+
const mask = prefix === 0 ? 0n : (1n << BigInt(bits)) - 1n ^ (1n << BigInt(bits - prefix)) - 1n;
|
|
59
|
+
return (ipVal & mask) === (baseVal & mask);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ../core/src/verify/operators.ts
|
|
63
|
+
var OPERATORS = [
|
|
64
|
+
{ operator: "googlebot", family: "googlebot", class: "crawler", ua: /Googlebot/i },
|
|
65
|
+
{ operator: "bingbot", family: "bingbot", class: "crawler", ua: /bingbot/i },
|
|
66
|
+
{ operator: "gptbot", family: "gptbot", class: "crawler", ua: /GPTBot/i }
|
|
67
|
+
];
|
|
68
|
+
function matchOperator(ua) {
|
|
69
|
+
if (!ua) return null;
|
|
70
|
+
return OPERATORS.find((o) => o.ua.test(ua)) ?? null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ../core/src/verify/ranges.ts
|
|
74
|
+
var OPERATOR_RANGES = {
|
|
75
|
+
googlebot: [
|
|
76
|
+
"2001:4860:4801:10::/64",
|
|
77
|
+
"2001:4860:4801:11::/64",
|
|
78
|
+
"2001:4860:4801:12::/64",
|
|
79
|
+
"2001:4860:4801:13::/64",
|
|
80
|
+
"2001:4860:4801:14::/64",
|
|
81
|
+
"2001:4860:4801:15::/64",
|
|
82
|
+
"2001:4860:4801:16::/64",
|
|
83
|
+
"2001:4860:4801:17::/64",
|
|
84
|
+
"2001:4860:4801:18::/64",
|
|
85
|
+
"2001:4860:4801:19::/64",
|
|
86
|
+
"2001:4860:4801:1a::/64",
|
|
87
|
+
"2001:4860:4801:1b::/64",
|
|
88
|
+
"2001:4860:4801:1c::/64",
|
|
89
|
+
"2001:4860:4801:1d::/64",
|
|
90
|
+
"2001:4860:4801:1e::/64",
|
|
91
|
+
"2001:4860:4801:1f::/64",
|
|
92
|
+
"2001:4860:4801:20::/64",
|
|
93
|
+
"2001:4860:4801:21::/64",
|
|
94
|
+
"2001:4860:4801:22::/64",
|
|
95
|
+
"2001:4860:4801:23::/64",
|
|
96
|
+
"2001:4860:4801:24::/64",
|
|
97
|
+
"2001:4860:4801:25::/64",
|
|
98
|
+
"2001:4860:4801:26::/64",
|
|
99
|
+
"2001:4860:4801:27::/64",
|
|
100
|
+
"2001:4860:4801:28::/64",
|
|
101
|
+
"2001:4860:4801:29::/64",
|
|
102
|
+
"2001:4860:4801:2::/64",
|
|
103
|
+
"2001:4860:4801:2a::/64",
|
|
104
|
+
"2001:4860:4801:2b::/64",
|
|
105
|
+
"2001:4860:4801:2c::/64",
|
|
106
|
+
"2001:4860:4801:2d::/64",
|
|
107
|
+
"2001:4860:4801:2e::/64",
|
|
108
|
+
"2001:4860:4801:2f::/64",
|
|
109
|
+
"2001:4860:4801:30::/64",
|
|
110
|
+
"2001:4860:4801:31::/64",
|
|
111
|
+
"2001:4860:4801:32::/64",
|
|
112
|
+
"2001:4860:4801:33::/64",
|
|
113
|
+
"2001:4860:4801:34::/64",
|
|
114
|
+
"2001:4860:4801:35::/64",
|
|
115
|
+
"2001:4860:4801:36::/64",
|
|
116
|
+
"2001:4860:4801:37::/64",
|
|
117
|
+
"2001:4860:4801:38::/64",
|
|
118
|
+
"2001:4860:4801:39::/64",
|
|
119
|
+
"2001:4860:4801:3a::/64",
|
|
120
|
+
"2001:4860:4801:3b::/64",
|
|
121
|
+
"2001:4860:4801:3c::/64",
|
|
122
|
+
"2001:4860:4801:3d::/64",
|
|
123
|
+
"2001:4860:4801:3e::/64",
|
|
124
|
+
"2001:4860:4801:3f::/64",
|
|
125
|
+
"2001:4860:4801:40::/64",
|
|
126
|
+
"2001:4860:4801:41::/64",
|
|
127
|
+
"2001:4860:4801:42::/64",
|
|
128
|
+
"2001:4860:4801:44::/64",
|
|
129
|
+
"2001:4860:4801:45::/64",
|
|
130
|
+
"2001:4860:4801:46::/64",
|
|
131
|
+
"2001:4860:4801:47::/64",
|
|
132
|
+
"2001:4860:4801:48::/64",
|
|
133
|
+
"2001:4860:4801:49::/64",
|
|
134
|
+
"2001:4860:4801:4a::/64",
|
|
135
|
+
"2001:4860:4801:4b::/64",
|
|
136
|
+
"2001:4860:4801:4c::/64",
|
|
137
|
+
"2001:4860:4801:4d::/64",
|
|
138
|
+
"2001:4860:4801:4e::/64",
|
|
139
|
+
"2001:4860:4801:50::/64",
|
|
140
|
+
"2001:4860:4801:51::/64",
|
|
141
|
+
"2001:4860:4801:52::/64",
|
|
142
|
+
"2001:4860:4801:53::/64",
|
|
143
|
+
"2001:4860:4801:54::/64",
|
|
144
|
+
"2001:4860:4801:55::/64",
|
|
145
|
+
"2001:4860:4801:56::/64",
|
|
146
|
+
"2001:4860:4801:57::/64",
|
|
147
|
+
"2001:4860:4801:58::/64",
|
|
148
|
+
"2001:4860:4801:59::/64",
|
|
149
|
+
"2001:4860:4801:60::/64",
|
|
150
|
+
"2001:4860:4801:61::/64",
|
|
151
|
+
"2001:4860:4801:62::/64",
|
|
152
|
+
"2001:4860:4801:63::/64",
|
|
153
|
+
"2001:4860:4801:64::/64",
|
|
154
|
+
"2001:4860:4801:65::/64",
|
|
155
|
+
"2001:4860:4801:66::/64",
|
|
156
|
+
"2001:4860:4801:67::/64",
|
|
157
|
+
"2001:4860:4801:68::/64",
|
|
158
|
+
"2001:4860:4801:69::/64",
|
|
159
|
+
"2001:4860:4801:6a::/64",
|
|
160
|
+
"2001:4860:4801:6b::/64",
|
|
161
|
+
"2001:4860:4801:6c::/64",
|
|
162
|
+
"2001:4860:4801:6d::/64",
|
|
163
|
+
"2001:4860:4801:6e::/64",
|
|
164
|
+
"2001:4860:4801:6f::/64",
|
|
165
|
+
"2001:4860:4801:70::/64",
|
|
166
|
+
"2001:4860:4801:71::/64",
|
|
167
|
+
"2001:4860:4801:72::/64",
|
|
168
|
+
"2001:4860:4801:73::/64",
|
|
169
|
+
"2001:4860:4801:74::/64",
|
|
170
|
+
"2001:4860:4801:75::/64",
|
|
171
|
+
"2001:4860:4801:76::/64",
|
|
172
|
+
"2001:4860:4801:77::/64",
|
|
173
|
+
"2001:4860:4801:78::/64",
|
|
174
|
+
"2001:4860:4801:79::/64",
|
|
175
|
+
"2001:4860:4801:7a::/64",
|
|
176
|
+
"2001:4860:4801:7b::/64",
|
|
177
|
+
"2001:4860:4801:7c::/64",
|
|
178
|
+
"2001:4860:4801:7d::/64",
|
|
179
|
+
"2001:4860:4801:7e::/64",
|
|
180
|
+
"2001:4860:4801:7f::/64",
|
|
181
|
+
"2001:4860:4801:80::/64",
|
|
182
|
+
"2001:4860:4801:81::/64",
|
|
183
|
+
"2001:4860:4801:82::/64",
|
|
184
|
+
"2001:4860:4801:83::/64",
|
|
185
|
+
"2001:4860:4801:84::/64",
|
|
186
|
+
"2001:4860:4801:85::/64",
|
|
187
|
+
"2001:4860:4801:86::/64",
|
|
188
|
+
"2001:4860:4801:87::/64",
|
|
189
|
+
"2001:4860:4801:88::/64",
|
|
190
|
+
"2001:4860:4801:90::/64",
|
|
191
|
+
"2001:4860:4801:91::/64",
|
|
192
|
+
"2001:4860:4801:92::/64",
|
|
193
|
+
"2001:4860:4801:93::/64",
|
|
194
|
+
"2001:4860:4801:94::/64",
|
|
195
|
+
"2001:4860:4801:95::/64",
|
|
196
|
+
"2001:4860:4801:96::/64",
|
|
197
|
+
"2001:4860:4801:97::/64",
|
|
198
|
+
"2001:4860:4801:a0::/64",
|
|
199
|
+
"2001:4860:4801:a1::/64",
|
|
200
|
+
"2001:4860:4801:a2::/64",
|
|
201
|
+
"2001:4860:4801:a3::/64",
|
|
202
|
+
"2001:4860:4801:a4::/64",
|
|
203
|
+
"2001:4860:4801:a5::/64",
|
|
204
|
+
"2001:4860:4801:a6::/64",
|
|
205
|
+
"2001:4860:4801:a7::/64",
|
|
206
|
+
"2001:4860:4801:a8::/64",
|
|
207
|
+
"2001:4860:4801:a9::/64",
|
|
208
|
+
"2001:4860:4801:aa::/64",
|
|
209
|
+
"2001:4860:4801:ab::/64",
|
|
210
|
+
"2001:4860:4801:ac::/64",
|
|
211
|
+
"2001:4860:4801:ad::/64",
|
|
212
|
+
"2001:4860:4801:ae::/64",
|
|
213
|
+
"2001:4860:4801:b0::/64",
|
|
214
|
+
"2001:4860:4801:b1::/64",
|
|
215
|
+
"2001:4860:4801:b2::/64",
|
|
216
|
+
"2001:4860:4801:b3::/64",
|
|
217
|
+
"2001:4860:4801:b4::/64",
|
|
218
|
+
"2001:4860:4801:b5::/64",
|
|
219
|
+
"2001:4860:4801:b6::/64",
|
|
220
|
+
"2001:4860:4801:c::/64",
|
|
221
|
+
"2001:4860:4801:f::/64",
|
|
222
|
+
"192.178.4.0/27",
|
|
223
|
+
"192.178.4.128/27",
|
|
224
|
+
"192.178.4.160/27",
|
|
225
|
+
"192.178.4.192/27",
|
|
226
|
+
"192.178.4.224/27",
|
|
227
|
+
"192.178.4.32/27",
|
|
228
|
+
"192.178.4.64/27",
|
|
229
|
+
"192.178.4.96/27",
|
|
230
|
+
"192.178.5.0/27",
|
|
231
|
+
"192.178.6.0/27",
|
|
232
|
+
"192.178.6.128/27",
|
|
233
|
+
"192.178.6.160/27",
|
|
234
|
+
"192.178.6.192/27",
|
|
235
|
+
"192.178.6.224/27",
|
|
236
|
+
"192.178.6.32/27",
|
|
237
|
+
"192.178.6.64/27",
|
|
238
|
+
"192.178.6.96/27",
|
|
239
|
+
"192.178.7.0/27",
|
|
240
|
+
"192.178.7.128/27",
|
|
241
|
+
"192.178.7.160/27",
|
|
242
|
+
"192.178.7.192/27",
|
|
243
|
+
"192.178.7.224/27",
|
|
244
|
+
"192.178.7.32/27",
|
|
245
|
+
"192.178.7.64/27",
|
|
246
|
+
"192.178.7.96/27",
|
|
247
|
+
"34.100.182.96/28",
|
|
248
|
+
"34.101.50.144/28",
|
|
249
|
+
"34.118.254.0/28",
|
|
250
|
+
"34.118.66.0/28",
|
|
251
|
+
"34.126.178.96/28",
|
|
252
|
+
"34.146.150.144/28",
|
|
253
|
+
"34.147.110.144/28",
|
|
254
|
+
"34.151.74.144/28",
|
|
255
|
+
"34.152.50.64/28",
|
|
256
|
+
"34.154.114.144/28",
|
|
257
|
+
"34.155.98.32/28",
|
|
258
|
+
"34.165.18.176/28",
|
|
259
|
+
"34.175.160.64/28",
|
|
260
|
+
"34.176.130.16/28",
|
|
261
|
+
"34.22.85.0/27",
|
|
262
|
+
"34.64.82.64/28",
|
|
263
|
+
"34.65.242.112/28",
|
|
264
|
+
"34.80.50.80/28",
|
|
265
|
+
"34.88.194.0/28",
|
|
266
|
+
"34.89.10.80/28",
|
|
267
|
+
"34.89.198.80/28",
|
|
268
|
+
"34.96.162.48/28",
|
|
269
|
+
"35.247.243.240/28",
|
|
270
|
+
"66.249.64.0/27",
|
|
271
|
+
"66.249.64.128/27",
|
|
272
|
+
"66.249.64.160/27",
|
|
273
|
+
"66.249.64.192/27",
|
|
274
|
+
"66.249.64.224/27",
|
|
275
|
+
"66.249.64.32/27",
|
|
276
|
+
"66.249.64.64/27",
|
|
277
|
+
"66.249.64.96/27",
|
|
278
|
+
"66.249.65.0/27",
|
|
279
|
+
"66.249.65.128/27",
|
|
280
|
+
"66.249.65.160/27",
|
|
281
|
+
"66.249.65.192/27",
|
|
282
|
+
"66.249.65.224/27",
|
|
283
|
+
"66.249.65.32/27",
|
|
284
|
+
"66.249.65.64/27",
|
|
285
|
+
"66.249.65.96/27",
|
|
286
|
+
"66.249.66.0/27",
|
|
287
|
+
"66.249.66.128/27",
|
|
288
|
+
"66.249.66.160/27",
|
|
289
|
+
"66.249.66.192/27",
|
|
290
|
+
"66.249.66.224/27",
|
|
291
|
+
"66.249.66.32/27",
|
|
292
|
+
"66.249.66.64/27",
|
|
293
|
+
"66.249.66.96/27",
|
|
294
|
+
"66.249.67.0/27",
|
|
295
|
+
"66.249.67.32/27",
|
|
296
|
+
"66.249.67.64/27",
|
|
297
|
+
"66.249.68.0/27",
|
|
298
|
+
"66.249.68.128/27",
|
|
299
|
+
"66.249.68.160/27",
|
|
300
|
+
"66.249.68.192/27",
|
|
301
|
+
"66.249.68.32/27",
|
|
302
|
+
"66.249.68.64/27",
|
|
303
|
+
"66.249.68.96/27",
|
|
304
|
+
"66.249.69.0/27",
|
|
305
|
+
"66.249.69.128/27",
|
|
306
|
+
"66.249.69.160/27",
|
|
307
|
+
"66.249.69.192/27",
|
|
308
|
+
"66.249.69.224/27",
|
|
309
|
+
"66.249.69.32/27",
|
|
310
|
+
"66.249.69.64/27",
|
|
311
|
+
"66.249.69.96/27",
|
|
312
|
+
"66.249.70.0/27",
|
|
313
|
+
"66.249.70.128/27",
|
|
314
|
+
"66.249.70.160/27",
|
|
315
|
+
"66.249.70.192/27",
|
|
316
|
+
"66.249.70.224/27",
|
|
317
|
+
"66.249.70.32/27",
|
|
318
|
+
"66.249.70.64/27",
|
|
319
|
+
"66.249.70.96/27",
|
|
320
|
+
"66.249.71.0/27",
|
|
321
|
+
"66.249.71.128/27",
|
|
322
|
+
"66.249.71.160/27",
|
|
323
|
+
"66.249.71.192/27",
|
|
324
|
+
"66.249.71.224/27",
|
|
325
|
+
"66.249.71.32/27",
|
|
326
|
+
"66.249.71.64/27",
|
|
327
|
+
"66.249.71.96/27",
|
|
328
|
+
"66.249.72.0/27",
|
|
329
|
+
"66.249.72.128/27",
|
|
330
|
+
"66.249.72.160/27",
|
|
331
|
+
"66.249.72.192/27",
|
|
332
|
+
"66.249.72.224/27",
|
|
333
|
+
"66.249.72.32/27",
|
|
334
|
+
"66.249.72.64/27",
|
|
335
|
+
"66.249.72.96/27",
|
|
336
|
+
"66.249.73.0/27",
|
|
337
|
+
"66.249.73.128/27",
|
|
338
|
+
"66.249.73.160/27",
|
|
339
|
+
"66.249.73.192/27",
|
|
340
|
+
"66.249.73.224/27",
|
|
341
|
+
"66.249.73.32/27",
|
|
342
|
+
"66.249.73.64/27",
|
|
343
|
+
"66.249.73.96/27",
|
|
344
|
+
"66.249.74.0/27",
|
|
345
|
+
"66.249.74.128/27",
|
|
346
|
+
"66.249.74.160/27",
|
|
347
|
+
"66.249.74.192/27",
|
|
348
|
+
"66.249.74.224/27",
|
|
349
|
+
"66.249.74.32/27",
|
|
350
|
+
"66.249.74.64/27",
|
|
351
|
+
"66.249.74.96/27",
|
|
352
|
+
"66.249.75.0/27",
|
|
353
|
+
"66.249.75.128/27",
|
|
354
|
+
"66.249.75.160/27",
|
|
355
|
+
"66.249.75.192/27",
|
|
356
|
+
"66.249.75.224/27",
|
|
357
|
+
"66.249.75.32/27",
|
|
358
|
+
"66.249.75.64/27",
|
|
359
|
+
"66.249.75.96/27",
|
|
360
|
+
"66.249.76.0/27",
|
|
361
|
+
"66.249.76.128/27",
|
|
362
|
+
"66.249.76.160/27",
|
|
363
|
+
"66.249.76.192/27",
|
|
364
|
+
"66.249.76.224/27",
|
|
365
|
+
"66.249.76.32/27",
|
|
366
|
+
"66.249.76.64/27",
|
|
367
|
+
"66.249.76.96/27",
|
|
368
|
+
"66.249.77.0/27",
|
|
369
|
+
"66.249.77.128/27",
|
|
370
|
+
"66.249.77.160/27",
|
|
371
|
+
"66.249.77.192/27",
|
|
372
|
+
"66.249.77.224/27",
|
|
373
|
+
"66.249.77.32/27",
|
|
374
|
+
"66.249.77.64/27",
|
|
375
|
+
"66.249.77.96/27",
|
|
376
|
+
"66.249.78.0/27",
|
|
377
|
+
"66.249.78.128/27",
|
|
378
|
+
"66.249.78.160/27",
|
|
379
|
+
"66.249.78.192/27",
|
|
380
|
+
"66.249.78.224/27",
|
|
381
|
+
"66.249.78.32/27",
|
|
382
|
+
"66.249.78.64/27",
|
|
383
|
+
"66.249.78.96/27",
|
|
384
|
+
"66.249.79.0/27",
|
|
385
|
+
"66.249.79.128/27",
|
|
386
|
+
"66.249.79.160/27",
|
|
387
|
+
"66.249.79.192/27",
|
|
388
|
+
"66.249.79.224/27",
|
|
389
|
+
"66.249.79.32/27",
|
|
390
|
+
"66.249.79.64/27"
|
|
391
|
+
],
|
|
392
|
+
bingbot: [
|
|
393
|
+
"157.55.39.0/24",
|
|
394
|
+
"207.46.13.0/24",
|
|
395
|
+
"40.77.167.0/24",
|
|
396
|
+
"13.66.139.0/24",
|
|
397
|
+
"13.66.144.0/24",
|
|
398
|
+
"52.167.144.0/24",
|
|
399
|
+
"13.67.10.16/28",
|
|
400
|
+
"13.69.66.240/28",
|
|
401
|
+
"13.71.172.224/28",
|
|
402
|
+
"139.217.52.0/28",
|
|
403
|
+
"191.233.204.224/28",
|
|
404
|
+
"20.36.108.32/28",
|
|
405
|
+
"20.43.120.16/28",
|
|
406
|
+
"40.79.131.208/28",
|
|
407
|
+
"40.79.186.176/28",
|
|
408
|
+
"52.231.148.0/28",
|
|
409
|
+
"20.79.107.240/28",
|
|
410
|
+
"51.105.67.0/28",
|
|
411
|
+
"20.125.163.80/28",
|
|
412
|
+
"40.77.188.0/22",
|
|
413
|
+
"65.55.210.0/24",
|
|
414
|
+
"199.30.24.0/23",
|
|
415
|
+
"40.77.202.0/24",
|
|
416
|
+
"40.77.139.0/25",
|
|
417
|
+
"20.74.197.0/28",
|
|
418
|
+
"20.15.133.160/27",
|
|
419
|
+
"40.77.177.0/24",
|
|
420
|
+
"40.77.178.0/23"
|
|
421
|
+
],
|
|
422
|
+
gptbot: [
|
|
423
|
+
"132.196.86.0/24",
|
|
424
|
+
"172.182.202.0/25",
|
|
425
|
+
"172.182.204.0/24",
|
|
426
|
+
"172.182.207.0/25",
|
|
427
|
+
"172.182.214.0/24",
|
|
428
|
+
"172.182.215.0/24",
|
|
429
|
+
"20.125.66.80/28",
|
|
430
|
+
"20.171.206.0/24",
|
|
431
|
+
"20.171.207.0/24",
|
|
432
|
+
"4.227.36.0/25",
|
|
433
|
+
"52.230.152.0/24",
|
|
434
|
+
"74.7.175.128/25",
|
|
435
|
+
"74.7.227.0/25",
|
|
436
|
+
"74.7.227.128/25",
|
|
437
|
+
"74.7.228.0/25",
|
|
438
|
+
"74.7.230.0/25",
|
|
439
|
+
"74.7.241.0/25",
|
|
440
|
+
"74.7.241.128/25",
|
|
441
|
+
"74.7.242.0/25",
|
|
442
|
+
"74.7.243.128/25",
|
|
443
|
+
"74.7.244.0/25"
|
|
444
|
+
]
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// ../core/src/verify/index.ts
|
|
448
|
+
function verifyBot(ua, ip) {
|
|
449
|
+
const op = matchOperator(ua);
|
|
450
|
+
if (!op) return { operator: null, result: "unverifiable" };
|
|
451
|
+
const ranges = OPERATOR_RANGES[op.operator];
|
|
452
|
+
if (!ranges || !ip || ip === "unknown") return { operator: op.operator, result: "unverifiable" };
|
|
453
|
+
const inRange = ranges.some((cidr) => ipInCidr(ip, cidr));
|
|
454
|
+
return { operator: op.operator, result: inRange ? "verified" : "spoofed" };
|
|
455
|
+
}
|
|
456
|
+
function botVerified(ua, ip) {
|
|
457
|
+
const { result } = verifyBot(ua, ip);
|
|
458
|
+
return result === "unverifiable" ? null : result;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ../core/src/version.ts
|
|
462
|
+
var EVENT_VERSION = 1;
|
|
463
|
+
|
|
464
|
+
// src/hash.ts
|
|
465
|
+
function dailySalt(saltBase, nowMs) {
|
|
466
|
+
const day = new Date(nowMs).toISOString().slice(0, 10);
|
|
467
|
+
return `${saltBase}:${day}`;
|
|
468
|
+
}
|
|
469
|
+
async function hashIp(ip, saltBase, nowMs) {
|
|
470
|
+
const data = new TextEncoder().encode(`${dailySalt(saltBase, nowMs)}:${ip}`);
|
|
471
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
472
|
+
let hex = "";
|
|
473
|
+
for (const b of new Uint8Array(digest)) hex += b.toString(16).padStart(2, "0");
|
|
474
|
+
return hex.slice(0, 32);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/event.ts
|
|
478
|
+
var ASSET_EXT = /\.(css|js|mjs|map|svg|png|jpe?g|gif|webp|avif|ico|woff2?|ttf|otf|eot)$/i;
|
|
479
|
+
function isAsset(path) {
|
|
480
|
+
return path.startsWith("/_next/static") || ASSET_EXT.test(path);
|
|
481
|
+
}
|
|
482
|
+
function firstForwardedIp(headers) {
|
|
483
|
+
const xff = headers.get("x-forwarded-for");
|
|
484
|
+
if (xff) return xff.split(",")[0]?.trim() || "unknown";
|
|
485
|
+
return headers.get("x-real-ip") ?? "unknown";
|
|
486
|
+
}
|
|
487
|
+
function refererHost(referer) {
|
|
488
|
+
if (!referer) return null;
|
|
489
|
+
try {
|
|
490
|
+
return new URL(referer).host;
|
|
491
|
+
} catch {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async function buildEvent(req, opts) {
|
|
496
|
+
const url = new URL(req.url);
|
|
497
|
+
const h = req.headers;
|
|
498
|
+
const ip = firstForwardedIp(h);
|
|
499
|
+
const verified = botVerified(h.get("user-agent"), ip);
|
|
500
|
+
return {
|
|
501
|
+
v: EVENT_VERSION,
|
|
502
|
+
ts: opts.nowMs,
|
|
503
|
+
site: opts.site,
|
|
504
|
+
method: req.method,
|
|
505
|
+
path: url.pathname,
|
|
506
|
+
has_query: url.search !== "",
|
|
507
|
+
status: null,
|
|
508
|
+
resp_bytes: null,
|
|
509
|
+
content_type: null,
|
|
510
|
+
duration_ms: null,
|
|
511
|
+
ua: h.get("user-agent"),
|
|
512
|
+
accept: h.get("accept"),
|
|
513
|
+
sec_fetch_mode: h.get("sec-fetch-mode"),
|
|
514
|
+
referer_host: refererHost(h.get("referer")),
|
|
515
|
+
conditional: h.has("if-none-match") || h.has("if-modified-since"),
|
|
516
|
+
ip_hash: await hashIp(ip, opts.ipSalt, opts.nowMs),
|
|
517
|
+
asset: isAsset(url.pathname),
|
|
518
|
+
bot_verified: verified
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/sampling.ts
|
|
523
|
+
var Sampler = class {
|
|
524
|
+
constructor(cap, keepEvery = 10) {
|
|
525
|
+
this.cap = cap;
|
|
526
|
+
this.keepEvery = keepEvery;
|
|
527
|
+
}
|
|
528
|
+
cap;
|
|
529
|
+
keepEvery;
|
|
530
|
+
day = "";
|
|
531
|
+
count = 0;
|
|
532
|
+
dropped = 0;
|
|
533
|
+
roll(nowMs) {
|
|
534
|
+
const d = new Date(nowMs).toISOString().slice(0, 10);
|
|
535
|
+
if (d !== this.day) {
|
|
536
|
+
this.day = d;
|
|
537
|
+
this.count = 0;
|
|
538
|
+
this.dropped = 0;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
takeDropped() {
|
|
542
|
+
const d = this.dropped;
|
|
543
|
+
this.dropped = 0;
|
|
544
|
+
return d;
|
|
545
|
+
}
|
|
546
|
+
/** Decide whether to emit this request; when emitting, hand back accumulated drops. */
|
|
547
|
+
next(nowMs) {
|
|
548
|
+
this.roll(nowMs);
|
|
549
|
+
this.count += 1;
|
|
550
|
+
if (this.cap <= 0 || this.count <= this.cap) {
|
|
551
|
+
return { emit: true, dropped: this.takeDropped() };
|
|
552
|
+
}
|
|
553
|
+
const over = this.count - this.cap;
|
|
554
|
+
if (over % this.keepEvery === 0) {
|
|
555
|
+
return { emit: true, dropped: this.takeDropped() };
|
|
556
|
+
}
|
|
557
|
+
this.dropped += 1;
|
|
558
|
+
return { emit: false, dropped: 0 };
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// src/send.ts
|
|
563
|
+
async function sendBatch(url, token, batch, timeoutMs, fetchImpl = fetch) {
|
|
564
|
+
const body = JSON.stringify(batch);
|
|
565
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
566
|
+
const controller = new AbortController();
|
|
567
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
568
|
+
try {
|
|
569
|
+
const res = await fetchImpl(url, {
|
|
570
|
+
method: "POST",
|
|
571
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
572
|
+
body,
|
|
573
|
+
signal: controller.signal,
|
|
574
|
+
keepalive: true
|
|
575
|
+
});
|
|
576
|
+
clearTimeout(timer);
|
|
577
|
+
if (res.ok) return true;
|
|
578
|
+
} catch {
|
|
579
|
+
clearTimeout(timer);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/index.ts
|
|
586
|
+
function withFootfall(existing, opts = {}) {
|
|
587
|
+
const cfg = opts.config ?? readConfig();
|
|
588
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
589
|
+
const now = opts.now ?? Date.now;
|
|
590
|
+
const sampler = new Sampler(cfg.dailyCap);
|
|
591
|
+
const token = cfg.token;
|
|
592
|
+
return (req, ev) => {
|
|
593
|
+
if (!isActive(cfg) || !token) {
|
|
594
|
+
return existing ? existing(req, ev) : void 0;
|
|
595
|
+
}
|
|
596
|
+
const requestId = crypto.randomUUID();
|
|
597
|
+
const nowMs = now();
|
|
598
|
+
const decision = sampler.next(nowMs);
|
|
599
|
+
if (decision.emit) {
|
|
600
|
+
const capture = (async () => {
|
|
601
|
+
try {
|
|
602
|
+
const event = await buildEvent(req, { site: token, ipSalt: cfg.ipSalt, nowMs });
|
|
603
|
+
event.request_id = requestId;
|
|
604
|
+
await sendBatch(
|
|
605
|
+
cfg.ingestUrl,
|
|
606
|
+
token,
|
|
607
|
+
{ site: token, events: [event], dropped: decision.dropped },
|
|
608
|
+
cfg.timeoutMs,
|
|
609
|
+
fetchImpl
|
|
610
|
+
);
|
|
611
|
+
} catch {
|
|
612
|
+
}
|
|
613
|
+
})();
|
|
614
|
+
ev.waitUntil(capture);
|
|
615
|
+
}
|
|
616
|
+
if (existing) return existing(req, ev);
|
|
617
|
+
const headers = new Headers(req.headers);
|
|
618
|
+
headers.set("x-footfall-id", requestId);
|
|
619
|
+
headers.set("x-footfall-path", new URL(req.url).pathname);
|
|
620
|
+
return NextResponse.next({ request: { headers } });
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export { Sampler, buildEvent, dailySalt, hashIp, sendBatch, withFootfall };
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@footfall/next",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Observe-only, fail-open Next.js middleware that mirrors request events to Footfall.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./beacon": {
|
|
15
|
+
"types": "./dist/beacon.d.ts",
|
|
16
|
+
"import": "./dist/beacon.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"nextjs",
|
|
27
|
+
"middleware",
|
|
28
|
+
"analytics",
|
|
29
|
+
"ai-agents",
|
|
30
|
+
"observability"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/nitinm21/footfall.git",
|
|
35
|
+
"directory": "packages/middleware"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/nitinm21/footfall",
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"next": ">=14"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.0.0",
|
|
43
|
+
"next": "^15.5.0",
|
|
44
|
+
"typescript": "^5.9.0",
|
|
45
|
+
"vitest": "^3.2.0",
|
|
46
|
+
"@footfall/core": "0.0.0"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"build": "tsup"
|
|
52
|
+
}
|
|
53
|
+
}
|