third-audience-mdx 1.0.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/CLAUDE.md +41 -0
- package/INSTALLATION.md +367 -0
- package/README.md +303 -0
- package/WORKLOG.md +162 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +208 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/index.mjs +185 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/dashboard/auth.d.mts +16 -0
- package/dist/dashboard/auth.d.ts +16 -0
- package/dist/dashboard/auth.js +123 -0
- package/dist/dashboard/auth.js.map +1 -0
- package/dist/dashboard/auth.mjs +87 -0
- package/dist/dashboard/auth.mjs.map +1 -0
- package/dist/dashboard/routes/analytics-api-route.d.mts +6 -0
- package/dist/dashboard/routes/analytics-api-route.d.ts +6 -0
- package/dist/dashboard/routes/analytics-api-route.js +180 -0
- package/dist/dashboard/routes/analytics-api-route.js.map +1 -0
- package/dist/dashboard/routes/analytics-api-route.mjs +145 -0
- package/dist/dashboard/routes/analytics-api-route.mjs.map +1 -0
- package/dist/dashboard/routes/api-key-route.d.mts +8 -0
- package/dist/dashboard/routes/api-key-route.d.ts +8 -0
- package/dist/dashboard/routes/api-key-route.js +173 -0
- package/dist/dashboard/routes/api-key-route.js.map +1 -0
- package/dist/dashboard/routes/api-key-route.mjs +137 -0
- package/dist/dashboard/routes/api-key-route.mjs.map +1 -0
- package/dist/dashboard/routes/citation-route.d.mts +14 -0
- package/dist/dashboard/routes/citation-route.d.ts +14 -0
- package/dist/dashboard/routes/citation-route.js +202 -0
- package/dist/dashboard/routes/citation-route.js.map +1 -0
- package/dist/dashboard/routes/citation-route.mjs +166 -0
- package/dist/dashboard/routes/citation-route.mjs.map +1 -0
- package/dist/dashboard/routes/llms-txt-route.d.mts +6 -0
- package/dist/dashboard/routes/llms-txt-route.d.ts +6 -0
- package/dist/dashboard/routes/llms-txt-route.js +119 -0
- package/dist/dashboard/routes/llms-txt-route.js.map +1 -0
- package/dist/dashboard/routes/llms-txt-route.mjs +84 -0
- package/dist/dashboard/routes/llms-txt-route.mjs.map +1 -0
- package/dist/dashboard/routes/login-route.d.mts +6 -0
- package/dist/dashboard/routes/login-route.d.ts +6 -0
- package/dist/dashboard/routes/login-route.js +313 -0
- package/dist/dashboard/routes/login-route.js.map +1 -0
- package/dist/dashboard/routes/login-route.mjs +284 -0
- package/dist/dashboard/routes/login-route.mjs.map +1 -0
- package/dist/dashboard/routes/markdown-route.d.mts +15 -0
- package/dist/dashboard/routes/markdown-route.d.ts +15 -0
- package/dist/dashboard/routes/markdown-route.js +239 -0
- package/dist/dashboard/routes/markdown-route.js.map +1 -0
- package/dist/dashboard/routes/markdown-route.mjs +204 -0
- package/dist/dashboard/routes/markdown-route.mjs.map +1 -0
- package/dist/dashboard/routes/okf-route.d.mts +13 -0
- package/dist/dashboard/routes/okf-route.d.ts +13 -0
- package/dist/dashboard/routes/okf-route.js +184 -0
- package/dist/dashboard/routes/okf-route.js.map +1 -0
- package/dist/dashboard/routes/okf-route.mjs +149 -0
- package/dist/dashboard/routes/okf-route.mjs.map +1 -0
- package/dist/dashboard/routes/sitemap-ai-route.d.mts +6 -0
- package/dist/dashboard/routes/sitemap-ai-route.d.ts +6 -0
- package/dist/dashboard/routes/sitemap-ai-route.js +134 -0
- package/dist/dashboard/routes/sitemap-ai-route.js.map +1 -0
- package/dist/dashboard/routes/sitemap-ai-route.mjs +99 -0
- package/dist/dashboard/routes/sitemap-ai-route.mjs.map +1 -0
- package/dist/dashboard/ui/components/Sidebar.d.mts +5 -0
- package/dist/dashboard/ui/components/Sidebar.d.ts +5 -0
- package/dist/dashboard/ui/components/Sidebar.js +102 -0
- package/dist/dashboard/ui/components/Sidebar.js.map +1 -0
- package/dist/dashboard/ui/components/Sidebar.mjs +68 -0
- package/dist/dashboard/ui/components/Sidebar.mjs.map +1 -0
- package/dist/dashboard/ui/globals.css +175 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.d.mts +5 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.d.ts +5 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.js +269 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.js.map +1 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.mjs +232 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.mjs.map +1 -0
- package/dist/dashboard/ui/pages/BotManagementPage.d.mts +13 -0
- package/dist/dashboard/ui/pages/BotManagementPage.d.ts +13 -0
- package/dist/dashboard/ui/pages/BotManagementPage.js +177 -0
- package/dist/dashboard/ui/pages/BotManagementPage.js.map +1 -0
- package/dist/dashboard/ui/pages/BotManagementPage.mjs +153 -0
- package/dist/dashboard/ui/pages/BotManagementPage.mjs.map +1 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.d.mts +5 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.d.ts +5 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.js +203 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.js.map +1 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.mjs +168 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.mjs.map +1 -0
- package/dist/dashboard/ui/pages/SettingsPage.d.mts +8 -0
- package/dist/dashboard/ui/pages/SettingsPage.d.ts +8 -0
- package/dist/dashboard/ui/pages/SettingsPage.js +181 -0
- package/dist/dashboard/ui/pages/SettingsPage.js.map +1 -0
- package/dist/dashboard/ui/pages/SettingsPage.mjs +157 -0
- package/dist/dashboard/ui/pages/SettingsPage.mjs.map +1 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.d.mts +5 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.d.ts +5 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.js +183 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.js.map +1 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.mjs +148 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.mjs.map +1 -0
- package/dist/index.d.mts +84 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +372 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +346 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +125 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
|
|
33
|
+
// src/detection/known-patterns.ts
|
|
34
|
+
var KNOWN_BOTS;
|
|
35
|
+
var init_known_patterns = __esm({
|
|
36
|
+
"src/detection/known-patterns.ts"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
KNOWN_BOTS = [
|
|
39
|
+
// AI Crawlers
|
|
40
|
+
{ name: "ClaudeBot", category: "ai_crawler", patterns: [/claudebot/i, /claude-web/i] },
|
|
41
|
+
{ name: "GPTBot", category: "ai_crawler", patterns: [/gptbot/i] },
|
|
42
|
+
{ name: "ChatGPT-User", category: "ai_crawler", patterns: [/chatgpt-user/i] },
|
|
43
|
+
{ name: "PerplexityBot", category: "ai_crawler", patterns: [/perplexitybot/i] },
|
|
44
|
+
{ name: "Googlebot-AI", category: "ai_crawler", patterns: [/google-extended/i, /googleother/i] },
|
|
45
|
+
{ name: "FacebookBot", category: "ai_crawler", patterns: [/facebookbot/i] },
|
|
46
|
+
{ name: "Applebot-Extended", category: "ai_crawler", patterns: [/applebot-extended/i] },
|
|
47
|
+
{ name: "YouBot", category: "ai_crawler", patterns: [/youbot/i] },
|
|
48
|
+
{ name: "CCBot", category: "ai_crawler", patterns: [/ccbot/i] },
|
|
49
|
+
{ name: "CohereCrawler", category: "ai_crawler", patterns: [/cohere-ai/i] },
|
|
50
|
+
{ name: "AI2Bot", category: "ai_crawler", patterns: [/ai2bot/i] },
|
|
51
|
+
{ name: "Bytespider", category: "ai_crawler", patterns: [/bytespider/i] },
|
|
52
|
+
{ name: "Diffbot", category: "ai_crawler", patterns: [/diffbot/i] },
|
|
53
|
+
// Search Engines
|
|
54
|
+
{ name: "Googlebot", category: "search_engine", patterns: [/googlebot/i] },
|
|
55
|
+
{ name: "Bingbot", category: "search_engine", patterns: [/bingbot/i, /msnbot/i] },
|
|
56
|
+
{ name: "DuckDuckBot", category: "search_engine", patterns: [/duckduckbot/i] },
|
|
57
|
+
{ name: "Baiduspider", category: "search_engine", patterns: [/baiduspider/i] },
|
|
58
|
+
{ name: "YandexBot", category: "search_engine", patterns: [/yandexbot/i] },
|
|
59
|
+
{ name: "Sogou", category: "search_engine", patterns: [/sogou/i] },
|
|
60
|
+
{ name: "Exabot", category: "search_engine", patterns: [/exabot/i] },
|
|
61
|
+
{ name: "ia_archiver", category: "search_engine", patterns: [/ia_archiver/i] }
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// src/detection/bot-detection-pipeline.ts
|
|
67
|
+
function detectBot(input) {
|
|
68
|
+
const ua = input.userAgent ?? "";
|
|
69
|
+
for (const bot of KNOWN_BOTS) {
|
|
70
|
+
for (const pattern of bot.patterns) {
|
|
71
|
+
if (pattern.test(ua)) {
|
|
72
|
+
return {
|
|
73
|
+
isBot: true,
|
|
74
|
+
botName: bot.name,
|
|
75
|
+
confidence: "high",
|
|
76
|
+
detectionMethod: "known_pattern",
|
|
77
|
+
category: bot.category,
|
|
78
|
+
rawUserAgent: ua
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const heuristicResult = checkHeuristics(ua, input.headers ?? {});
|
|
84
|
+
if (heuristicResult) return { ...heuristicResult, rawUserAgent: ua };
|
|
85
|
+
if (looksLikeBotUa(ua)) {
|
|
86
|
+
return {
|
|
87
|
+
isBot: true,
|
|
88
|
+
botName: null,
|
|
89
|
+
confidence: "low",
|
|
90
|
+
detectionMethod: "auto_learned",
|
|
91
|
+
category: "unknown_bot",
|
|
92
|
+
rawUserAgent: ua
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
isBot: false,
|
|
97
|
+
botName: null,
|
|
98
|
+
confidence: "high",
|
|
99
|
+
detectionMethod: "none",
|
|
100
|
+
category: "human",
|
|
101
|
+
rawUserAgent: ua
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function checkHeuristics(ua, headers) {
|
|
105
|
+
if (/headlesschrome/i.test(ua)) {
|
|
106
|
+
return { isBot: true, botName: "HeadlessChrome", confidence: "medium", detectionMethod: "heuristic", category: "unknown_bot" };
|
|
107
|
+
}
|
|
108
|
+
if (/phantomjs/i.test(ua)) {
|
|
109
|
+
return { isBot: true, botName: "PhantomJS", confidence: "high", detectionMethod: "heuristic", category: "unknown_bot" };
|
|
110
|
+
}
|
|
111
|
+
if (/selenium/i.test(ua)) {
|
|
112
|
+
return { isBot: true, botName: "Selenium", confidence: "high", detectionMethod: "heuristic", category: "unknown_bot" };
|
|
113
|
+
}
|
|
114
|
+
if (ua.trim().length < 10) {
|
|
115
|
+
return { isBot: true, botName: null, confidence: "low", detectionMethod: "heuristic", category: "unknown_bot" };
|
|
116
|
+
}
|
|
117
|
+
const hasAcceptLang = !!headers["accept-language"];
|
|
118
|
+
const hasAcceptEncoding = !!headers["accept-encoding"];
|
|
119
|
+
if (!hasAcceptLang && !hasAcceptEncoding) {
|
|
120
|
+
return { isBot: true, botName: null, confidence: "low", detectionMethod: "heuristic", category: "unknown_bot" };
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
function looksLikeBotUa(ua) {
|
|
125
|
+
return /bot|crawler|spider|scraper|fetch|http|python|curl|java|ruby|go-http|node/i.test(ua) && !/chrome|firefox|safari|edge|opera/i.test(ua);
|
|
126
|
+
}
|
|
127
|
+
var init_bot_detection_pipeline = __esm({
|
|
128
|
+
"src/detection/bot-detection-pipeline.ts"() {
|
|
129
|
+
"use strict";
|
|
130
|
+
init_known_patterns();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// src/analytics/geolocation.ts
|
|
135
|
+
function loadGeoip() {
|
|
136
|
+
if (geoip) return geoip;
|
|
137
|
+
try {
|
|
138
|
+
geoip = require("geoip-lite");
|
|
139
|
+
} catch {
|
|
140
|
+
geoip = null;
|
|
141
|
+
}
|
|
142
|
+
return geoip;
|
|
143
|
+
}
|
|
144
|
+
function getCountry(ip) {
|
|
145
|
+
if (!ip || ip === "unknown" || ip === "127.0.0.1" || ip.startsWith("::")) return null;
|
|
146
|
+
const geo = loadGeoip();
|
|
147
|
+
if (!geo) return null;
|
|
148
|
+
try {
|
|
149
|
+
const result = geo.lookup(ip);
|
|
150
|
+
return result?.country ?? null;
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
var geoip;
|
|
156
|
+
var init_geolocation = __esm({
|
|
157
|
+
"src/analytics/geolocation.ts"() {
|
|
158
|
+
"use strict";
|
|
159
|
+
geoip = null;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// src/analytics/visit-tracker.ts
|
|
164
|
+
var visit_tracker_exports = {};
|
|
165
|
+
__export(visit_tracker_exports, {
|
|
166
|
+
VisitTracker: () => VisitTracker
|
|
167
|
+
});
|
|
168
|
+
var import_fs, import_path, _VisitTracker, VisitTracker;
|
|
169
|
+
var init_visit_tracker = __esm({
|
|
170
|
+
"src/analytics/visit-tracker.ts"() {
|
|
171
|
+
"use strict";
|
|
172
|
+
import_fs = __toESM(require("fs"));
|
|
173
|
+
import_path = __toESM(require("path"));
|
|
174
|
+
init_bot_detection_pipeline();
|
|
175
|
+
init_geolocation();
|
|
176
|
+
_VisitTracker = class _VisitTracker {
|
|
177
|
+
constructor(dataDir) {
|
|
178
|
+
this.dataDir = dataDir;
|
|
179
|
+
}
|
|
180
|
+
static getInstance(dataDir = process.env.TA_DATA_DIR ?? "data") {
|
|
181
|
+
if (!_VisitTracker.instance) {
|
|
182
|
+
_VisitTracker.instance = new _VisitTracker(dataDir);
|
|
183
|
+
}
|
|
184
|
+
return _VisitTracker.instance;
|
|
185
|
+
}
|
|
186
|
+
record(req, meta = {}) {
|
|
187
|
+
const ua = req.headers.get("user-agent") ?? "";
|
|
188
|
+
const result = detectBot({ userAgent: ua, headers: Object.fromEntries(req.headers) });
|
|
189
|
+
if (!result.isBot) return;
|
|
190
|
+
const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? req.headers.get("x-real-ip") ?? "unknown";
|
|
191
|
+
const record = {
|
|
192
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
193
|
+
bot_name: result.botName,
|
|
194
|
+
bot_category: result.category,
|
|
195
|
+
detection_method: result.detectionMethod,
|
|
196
|
+
confidence: result.confidence,
|
|
197
|
+
url: req.nextUrl.pathname,
|
|
198
|
+
ip,
|
|
199
|
+
country: getCountry(ip),
|
|
200
|
+
user_agent: ua,
|
|
201
|
+
referer: req.headers.get("referer"),
|
|
202
|
+
response_ms: meta.responseMs ?? null,
|
|
203
|
+
cache_hit: meta.cacheHit ?? false,
|
|
204
|
+
content_length: meta.contentLength ?? null
|
|
205
|
+
};
|
|
206
|
+
this.append("ta-visits.jsonl", record);
|
|
207
|
+
}
|
|
208
|
+
append(filename, record) {
|
|
209
|
+
try {
|
|
210
|
+
const filePath = import_path.default.join(this.dataDir, filename);
|
|
211
|
+
import_fs.default.mkdirSync(this.dataDir, { recursive: true });
|
|
212
|
+
import_fs.default.appendFileSync(filePath, JSON.stringify(record) + "\n", "utf-8");
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
_VisitTracker.instance = null;
|
|
218
|
+
VisitTracker = _VisitTracker;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// src/index.ts
|
|
223
|
+
var index_exports = {};
|
|
224
|
+
__export(index_exports, {
|
|
225
|
+
detectBot: () => detectBot,
|
|
226
|
+
thirdAudienceMiddleware: () => thirdAudienceMiddleware,
|
|
227
|
+
withThirdAudience: () => withThirdAudience
|
|
228
|
+
});
|
|
229
|
+
module.exports = __toCommonJS(index_exports);
|
|
230
|
+
|
|
231
|
+
// src/core/config.ts
|
|
232
|
+
var defaultConfig = {
|
|
233
|
+
contentDir: "content",
|
|
234
|
+
dataDir: "data",
|
|
235
|
+
dashboard: true,
|
|
236
|
+
dashboardSecret: "",
|
|
237
|
+
notifications: {},
|
|
238
|
+
bots: { allowlist: [], blocklist: [] },
|
|
239
|
+
cache: { ttl: 3600, maxMemoryEntries: 500 }
|
|
240
|
+
};
|
|
241
|
+
function resolveConfig(partial = {}) {
|
|
242
|
+
return {
|
|
243
|
+
...defaultConfig,
|
|
244
|
+
...partial,
|
|
245
|
+
bots: { ...defaultConfig.bots, ...partial.bots },
|
|
246
|
+
cache: { ...defaultConfig.cache, ...partial.cache },
|
|
247
|
+
notifications: { ...defaultConfig.notifications, ...partial.notifications }
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/core/with-third-audience.ts
|
|
252
|
+
function withThirdAudience(options = {}, nextConfig = {}) {
|
|
253
|
+
const config = resolveConfig(options);
|
|
254
|
+
return {
|
|
255
|
+
...nextConfig,
|
|
256
|
+
async headers() {
|
|
257
|
+
const existing = await nextConfig.headers?.() ?? [];
|
|
258
|
+
return [
|
|
259
|
+
...existing,
|
|
260
|
+
{
|
|
261
|
+
source: "/:path*.md",
|
|
262
|
+
headers: [{ key: "Content-Type", value: "text/markdown; charset=utf-8" }]
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
source: "/llms.txt",
|
|
266
|
+
headers: [{ key: "Content-Type", value: "text/plain; charset=utf-8" }]
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
source: "/okf/:path*",
|
|
270
|
+
headers: [{ key: "Content-Type", value: "text/markdown; charset=utf-8" }]
|
|
271
|
+
}
|
|
272
|
+
];
|
|
273
|
+
},
|
|
274
|
+
env: {
|
|
275
|
+
...nextConfig.env,
|
|
276
|
+
TA_CONTENT_DIR: config.contentDir,
|
|
277
|
+
TA_DATA_DIR: config.dataDir,
|
|
278
|
+
TA_DASHBOARD_ENABLED: String(config.dashboard)
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/core/middleware.ts
|
|
284
|
+
var import_server = require("next/server");
|
|
285
|
+
|
|
286
|
+
// src/dashboard/admin-store.ts
|
|
287
|
+
var import_crypto = __toESM(require("crypto"));
|
|
288
|
+
function verifySession(token) {
|
|
289
|
+
const lastDot = token.lastIndexOf(".");
|
|
290
|
+
if (lastDot === -1) return false;
|
|
291
|
+
const payload = token.slice(0, lastDot);
|
|
292
|
+
const sig = token.slice(lastDot + 1);
|
|
293
|
+
const expected = import_crypto.default.createHmac("sha256", process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt").update(payload).digest("hex");
|
|
294
|
+
if (sig.length !== expected.length) return false;
|
|
295
|
+
return import_crypto.default.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/core/middleware.ts
|
|
299
|
+
var COOKIE_NAME = "ta_session";
|
|
300
|
+
var RESET_COOKIE = "ta_session_reset";
|
|
301
|
+
async function thirdAudienceMiddleware(req) {
|
|
302
|
+
const { pathname } = req.nextUrl;
|
|
303
|
+
const accept = req.headers.get("accept") ?? "";
|
|
304
|
+
if (pathname.startsWith("/third-audience") && !pathname.startsWith("/third-audience/login")) {
|
|
305
|
+
const session = req.cookies.get(COOKIE_NAME)?.value;
|
|
306
|
+
if (!session || !verifySession(session)) {
|
|
307
|
+
const loginUrl = req.nextUrl.clone();
|
|
308
|
+
loginUrl.pathname = "/third-audience/login";
|
|
309
|
+
return import_server.NextResponse.redirect(loginUrl);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (pathname === "/third-audience/login" && req.nextUrl.searchParams.get("reset") === "1") {
|
|
313
|
+
const resetCookie = req.cookies.get(RESET_COOKIE)?.value;
|
|
314
|
+
const sessionCookie = req.cookies.get(COOKIE_NAME)?.value;
|
|
315
|
+
if ((!resetCookie || !verifySession(resetCookie)) && (!sessionCookie || !verifySession(sessionCookie))) {
|
|
316
|
+
const loginUrl = req.nextUrl.clone();
|
|
317
|
+
loginUrl.pathname = "/third-audience/login";
|
|
318
|
+
loginUrl.search = "";
|
|
319
|
+
return import_server.NextResponse.redirect(loginUrl);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (pathname === "/third-audience/login") {
|
|
323
|
+
const url = req.nextUrl.clone();
|
|
324
|
+
url.pathname = "/api/third-audience/login";
|
|
325
|
+
return import_server.NextResponse.rewrite(url);
|
|
326
|
+
}
|
|
327
|
+
if (pathname.endsWith(".md")) {
|
|
328
|
+
const slug = pathname.slice(0, -3);
|
|
329
|
+
const url = req.nextUrl.clone();
|
|
330
|
+
url.pathname = `/api/third-audience/markdown${slug}`;
|
|
331
|
+
return import_server.NextResponse.rewrite(url);
|
|
332
|
+
}
|
|
333
|
+
if (accept.includes("text/markdown")) {
|
|
334
|
+
const url = req.nextUrl.clone();
|
|
335
|
+
url.pathname = `/api/third-audience/markdown${pathname}`;
|
|
336
|
+
return import_server.NextResponse.rewrite(url);
|
|
337
|
+
}
|
|
338
|
+
if (pathname.startsWith("/okf")) {
|
|
339
|
+
const url = req.nextUrl.clone();
|
|
340
|
+
url.pathname = `/api/third-audience/okf${pathname.slice(4)}`;
|
|
341
|
+
return import_server.NextResponse.rewrite(url);
|
|
342
|
+
}
|
|
343
|
+
if (pathname === "/llms.txt") {
|
|
344
|
+
const url = req.nextUrl.clone();
|
|
345
|
+
url.pathname = "/api/third-audience/llms-txt";
|
|
346
|
+
return import_server.NextResponse.rewrite(url);
|
|
347
|
+
}
|
|
348
|
+
if (pathname === "/sitemap-ai.xml") {
|
|
349
|
+
const url = req.nextUrl.clone();
|
|
350
|
+
url.pathname = "/api/third-audience/sitemap-ai";
|
|
351
|
+
return import_server.NextResponse.rewrite(url);
|
|
352
|
+
}
|
|
353
|
+
const response = import_server.NextResponse.next();
|
|
354
|
+
trackVisitAsync(req);
|
|
355
|
+
return response;
|
|
356
|
+
}
|
|
357
|
+
function trackVisitAsync(req) {
|
|
358
|
+
void Promise.resolve().then(() => (init_visit_tracker(), visit_tracker_exports)).then(({ VisitTracker: VisitTracker2 }) => {
|
|
359
|
+
VisitTracker2.getInstance().record(req);
|
|
360
|
+
}).catch(() => {
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/index.ts
|
|
365
|
+
init_bot_detection_pipeline();
|
|
366
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
367
|
+
0 && (module.exports = {
|
|
368
|
+
detectBot,
|
|
369
|
+
thirdAudienceMiddleware,
|
|
370
|
+
withThirdAudience
|
|
371
|
+
});
|
|
372
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/detection/known-patterns.ts","../src/detection/bot-detection-pipeline.ts","../src/analytics/geolocation.ts","../src/analytics/visit-tracker.ts","../src/index.ts","../src/core/config.ts","../src/core/with-third-audience.ts","../src/core/middleware.ts","../src/dashboard/admin-store.ts"],"sourcesContent":["/** Known AI crawler and search engine user-agent patterns. */\nexport interface KnownBot {\n name: string\n category: 'ai_crawler' | 'search_engine'\n patterns: RegExp[]\n}\n\nexport const KNOWN_BOTS: KnownBot[] = [\n // AI Crawlers\n { name: 'ClaudeBot', category: 'ai_crawler', patterns: [/claudebot/i, /claude-web/i] },\n { name: 'GPTBot', category: 'ai_crawler', patterns: [/gptbot/i] },\n { name: 'ChatGPT-User', category: 'ai_crawler', patterns: [/chatgpt-user/i] },\n { name: 'PerplexityBot', category: 'ai_crawler', patterns: [/perplexitybot/i] },\n { name: 'Googlebot-AI', category: 'ai_crawler', patterns: [/google-extended/i, /googleother/i] },\n { name: 'FacebookBot', category: 'ai_crawler', patterns: [/facebookbot/i] },\n { name: 'Applebot-Extended',category: 'ai_crawler', patterns: [/applebot-extended/i] },\n { name: 'YouBot', category: 'ai_crawler', patterns: [/youbot/i] },\n { name: 'CCBot', category: 'ai_crawler', patterns: [/ccbot/i] },\n { name: 'CohereCrawler', category: 'ai_crawler', patterns: [/cohere-ai/i] },\n { name: 'AI2Bot', category: 'ai_crawler', patterns: [/ai2bot/i] },\n { name: 'Bytespider', category: 'ai_crawler', patterns: [/bytespider/i] },\n { name: 'Diffbot', category: 'ai_crawler', patterns: [/diffbot/i] },\n\n // Search Engines\n { name: 'Googlebot', category: 'search_engine', patterns: [/googlebot/i] },\n { name: 'Bingbot', category: 'search_engine', patterns: [/bingbot/i, /msnbot/i] },\n { name: 'DuckDuckBot', category: 'search_engine', patterns: [/duckduckbot/i] },\n { name: 'Baiduspider', category: 'search_engine', patterns: [/baiduspider/i] },\n { name: 'YandexBot', category: 'search_engine', patterns: [/yandexbot/i] },\n { name: 'Sogou', category: 'search_engine', patterns: [/sogou/i] },\n { name: 'Exabot', category: 'search_engine', patterns: [/exabot/i] },\n { name: 'ia_archiver', category: 'search_engine', patterns: [/ia_archiver/i] },\n]\n","import type { BotDetectionResult } from './bot-detection-result.js'\nimport { KNOWN_BOTS } from './known-patterns.js'\n\nexport interface DetectBotInput {\n userAgent: string\n /** Optional: headers map for heuristic checks */\n headers?: Record<string, string | string[] | undefined>\n /** Optional: IP address */\n ip?: string\n}\n\n/**\n * Three-layer bot detection pipeline:\n * 1. Known pattern matching (O(n) UA string match)\n * 2. Heuristic signals (missing headers, headless indicators)\n * 3. Auto-learner flag (unknown UAs that behave bot-like)\n */\nexport function detectBot(input: DetectBotInput): BotDetectionResult {\n const ua = input.userAgent ?? ''\n\n // Layer 1: known pattern match\n for (const bot of KNOWN_BOTS) {\n for (const pattern of bot.patterns) {\n if (pattern.test(ua)) {\n return {\n isBot: true,\n botName: bot.name,\n confidence: 'high',\n detectionMethod: 'known_pattern',\n category: bot.category,\n rawUserAgent: ua,\n }\n }\n }\n }\n\n // Layer 2: heuristics\n const heuristicResult = checkHeuristics(ua, input.headers ?? {})\n if (heuristicResult) return { ...heuristicResult, rawUserAgent: ua }\n\n // Layer 3: auto-learner — flag suspicious unknown UAs for review\n if (looksLikeBotUa(ua)) {\n return {\n isBot: true,\n botName: null,\n confidence: 'low',\n detectionMethod: 'auto_learned',\n category: 'unknown_bot',\n rawUserAgent: ua,\n }\n }\n\n return {\n isBot: false,\n botName: null,\n confidence: 'high',\n detectionMethod: 'none',\n category: 'human',\n rawUserAgent: ua,\n }\n}\n\nfunction checkHeuristics(\n ua: string,\n headers: Record<string, string | string[] | undefined>\n): Omit<BotDetectionResult, 'rawUserAgent'> | null {\n // Headless Chrome signals\n if (/headlesschrome/i.test(ua)) {\n return { isBot: true, botName: 'HeadlessChrome', confidence: 'medium', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/phantomjs/i.test(ua)) {\n return { isBot: true, botName: 'PhantomJS', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/selenium/i.test(ua)) {\n return { isBot: true, botName: 'Selenium', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Empty or very short UA is suspicious\n if (ua.trim().length < 10) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Missing typical browser headers\n const hasAcceptLang = !!headers['accept-language']\n const hasAcceptEncoding = !!headers['accept-encoding']\n if (!hasAcceptLang && !hasAcceptEncoding) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n return null\n}\n\nfunction looksLikeBotUa(ua: string): boolean {\n return (\n /bot|crawler|spider|scraper|fetch|http|python|curl|java|ruby|go-http|node/i.test(ua) &&\n !/chrome|firefox|safari|edge|opera/i.test(ua)\n )\n}\n","let geoip: typeof import('geoip-lite') | null = null\n\nfunction loadGeoip() {\n if (geoip) return geoip\n try {\n geoip = require('geoip-lite') as typeof import('geoip-lite')\n } catch {\n geoip = null\n }\n return geoip\n}\n\n/** Returns ISO 3166-1 alpha-2 country code, or null if lookup fails. */\nexport function getCountry(ip: string): string | null {\n if (!ip || ip === 'unknown' || ip === '127.0.0.1' || ip.startsWith('::')) return null\n const geo = loadGeoip()\n if (!geo) return null\n try {\n const result = geo.lookup(ip)\n return result?.country ?? null\n } catch {\n return null\n }\n}\n","import fs from 'fs'\nimport path from 'path'\nimport type { NextRequest } from 'next/server'\nimport { detectBot } from '../detection/bot-detection-pipeline.js'\nimport { getCountry } from './geolocation.js'\n\nexport interface VisitRecord {\n timestamp: string\n bot_name: string | null\n bot_category: string\n detection_method: string\n confidence: string\n url: string\n ip: string\n country: string | null\n user_agent: string\n referer: string | null\n response_ms: number | null\n cache_hit: boolean\n content_length: number | null\n}\n\nexport class VisitTracker {\n private static instance: VisitTracker | null = null\n private dataDir: string\n\n private constructor(dataDir: string) {\n this.dataDir = dataDir\n }\n\n static getInstance(dataDir = process.env.TA_DATA_DIR ?? 'data'): VisitTracker {\n if (!VisitTracker.instance) {\n VisitTracker.instance = new VisitTracker(dataDir)\n }\n return VisitTracker.instance\n }\n\n record(req: NextRequest, meta: { responseMs?: number; cacheHit?: boolean; contentLength?: number } = {}): void {\n const ua = req.headers.get('user-agent') ?? ''\n const result = detectBot({ userAgent: ua, headers: Object.fromEntries(req.headers) })\n\n if (!result.isBot) return // only track bots\n\n const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim()\n ?? req.headers.get('x-real-ip')\n ?? 'unknown'\n\n const record: VisitRecord = {\n timestamp: new Date().toISOString(),\n bot_name: result.botName,\n bot_category: result.category,\n detection_method: result.detectionMethod,\n confidence: result.confidence,\n url: req.nextUrl.pathname,\n ip,\n country: getCountry(ip),\n user_agent: ua,\n referer: req.headers.get('referer'),\n response_ms: meta.responseMs ?? null,\n cache_hit: meta.cacheHit ?? false,\n content_length: meta.contentLength ?? null,\n }\n\n this.append('ta-visits.jsonl', record)\n }\n\n private append(filename: string, record: VisitRecord): void {\n try {\n const filePath = path.join(this.dataDir, filename)\n fs.mkdirSync(this.dataDir, { recursive: true })\n fs.appendFileSync(filePath, JSON.stringify(record) + '\\n', 'utf-8')\n } catch {\n // Tracking must never throw\n }\n }\n}\n","/**\n * third-audience-mdx\n * Public API surface for the package.\n */\n\nexport { withThirdAudience } from './core/with-third-audience.js'\nexport { thirdAudienceMiddleware } from './core/middleware.js'\nexport { detectBot } from './detection/bot-detection-pipeline.js'\nexport type { ThirdAudienceConfig } from './core/config.js'\nexport type { BotDetectionResult } from './detection/bot-detection-result.js'\n","export interface ThirdAudienceConfig {\n /** Directory containing .mdx files, relative to project root. Default: 'content' */\n contentDir?: string\n /** Directory for JSONL data files. Default: 'data' */\n dataDir?: string\n /** Mount the /third-audience/ dashboard. Default: true */\n dashboard?: boolean\n /** Secret for dashboard access (HTTP Basic or bearer). Required when dashboard: true */\n dashboardSecret?: string\n notifications?: {\n email?: { smtp: string; to: string; from?: string }\n slack?: { webhookUrl: string }\n }\n bots?: {\n allowlist?: string[]\n blocklist?: string[]\n }\n cache?: {\n /** Cache TTL in seconds. Default: 3600 */\n ttl?: number\n /** Max in-memory entries. Default: 500 */\n maxMemoryEntries?: number\n }\n}\n\nexport const defaultConfig: Required<ThirdAudienceConfig> = {\n contentDir: 'content',\n dataDir: 'data',\n dashboard: true,\n dashboardSecret: '',\n notifications: {},\n bots: { allowlist: [], blocklist: [] },\n cache: { ttl: 3600, maxMemoryEntries: 500 },\n}\n\nexport function resolveConfig(partial: ThirdAudienceConfig = {}): Required<ThirdAudienceConfig> {\n return {\n ...defaultConfig,\n ...partial,\n bots: { ...defaultConfig.bots, ...partial.bots },\n cache: { ...defaultConfig.cache, ...partial.cache },\n notifications: { ...defaultConfig.notifications, ...partial.notifications },\n }\n}\n","import type { NextConfig } from 'next'\nimport { resolveConfig, type ThirdAudienceConfig } from './config.js'\n\n/**\n * Wraps next.config.ts to inject Third Audience rewrites and headers.\n *\n * Usage:\n * import { withThirdAudience } from 'third-audience-mdx'\n * export default withThirdAudience({ contentDir: 'content' })\n */\nexport function withThirdAudience(\n options: ThirdAudienceConfig = {},\n nextConfig: NextConfig = {}\n): NextConfig {\n const config = resolveConfig(options)\n\n return {\n ...nextConfig,\n async headers() {\n const existing = await nextConfig.headers?.() ?? []\n return [\n ...existing,\n {\n source: '/:path*.md',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n {\n source: '/llms.txt',\n headers: [{ key: 'Content-Type', value: 'text/plain; charset=utf-8' }],\n },\n {\n source: '/okf/:path*',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n ]\n },\n env: {\n ...nextConfig.env,\n TA_CONTENT_DIR: config.contentDir,\n TA_DATA_DIR: config.dataDir,\n TA_DASHBOARD_ENABLED: String(config.dashboard),\n },\n }\n}\n","import { NextResponse, type NextRequest } from 'next/server'\nimport { verifySession } from '../dashboard/admin-store.js'\n\nconst COOKIE_NAME = 'ta_session'\nconst RESET_COOKIE = 'ta_session_reset'\n\n/**\n * Third Audience middleware.\n *\n * Handles:\n * - Dashboard auth: /third-audience/* requires valid session cookie\n * - .md URL requests → serve Markdown of matching MDX file\n * - Accept: text/markdown header → serve Markdown of current page\n * - Bot visit tracking (non-blocking, fire-and-forget)\n * - Citation detection via Referer header\n *\n * Wire up in middleware.ts:\n * export { thirdAudienceMiddleware as middleware } from 'third-audience-mdx'\n * export const config = { matcher: ['/((?!_next|api).*)'] }\n */\nexport async function thirdAudienceMiddleware(req: NextRequest): Promise<NextResponse> {\n const { pathname } = req.nextUrl\n const accept = req.headers.get('accept') ?? ''\n\n // Dashboard auth guard — all /third-audience/* except /login\n if (pathname.startsWith('/third-audience') && !pathname.startsWith('/third-audience/login')) {\n const session = req.cookies.get(COOKIE_NAME)?.value\n if (!session || !verifySession(session)) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // Password reset guard — /third-audience/login?reset=1 requires reset cookie\n if (pathname === '/third-audience/login' && req.nextUrl.searchParams.get('reset') === '1') {\n const resetCookie = req.cookies.get(RESET_COOKIE)?.value\n const sessionCookie = req.cookies.get(COOKIE_NAME)?.value\n // Allow if they have either a valid session or a valid reset token\n if ((!resetCookie || !verifySession(resetCookie)) && (!sessionCookie || !verifySession(sessionCookie))) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n loginUrl.search = ''\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // /third-audience/login → rewrite to login route handler (GET/POST)\n if (pathname === '/third-audience/login') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/login'\n return NextResponse.rewrite(url)\n }\n\n // .md URL → rewrite to our internal markdown route handler\n if (pathname.endsWith('.md')) {\n const slug = pathname.slice(0, -3) // strip .md\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${slug}`\n return NextResponse.rewrite(url)\n }\n\n // Accept: text/markdown header → rewrite to markdown route\n if (accept.includes('text/markdown')) {\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${pathname}`\n return NextResponse.rewrite(url)\n }\n\n // /okf/ → rewrite to OKF bundle handler\n if (pathname.startsWith('/okf')) {\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/okf${pathname.slice(4)}`\n return NextResponse.rewrite(url)\n }\n\n // /llms.txt → rewrite to discovery handler\n if (pathname === '/llms.txt') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/llms-txt'\n return NextResponse.rewrite(url)\n }\n\n // /sitemap-ai.xml → rewrite to AI sitemap handler\n if (pathname === '/sitemap-ai.xml') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/sitemap-ai'\n return NextResponse.rewrite(url)\n }\n\n const response = NextResponse.next()\n\n // Fire-and-forget: track bot visits and citations (non-blocking)\n trackVisitAsync(req)\n\n return response\n}\n\nfunction trackVisitAsync(req: NextRequest): void {\n // Dynamically import to avoid loading analytics on every request sync path.\n // Uses void to intentionally not await — tracking must never block response.\n void import('../analytics/visit-tracker.js').then(({ VisitTracker }) => {\n VisitTracker.getInstance().record(req)\n }).catch(() => { /* never throw from tracking */ })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport crypto from 'crypto'\n\nexport interface AdminRecord {\n passwordHash: string // sha256(secret + password)\n isDefaultPassword: boolean\n createdAt: string\n lastLoginAt: string | null\n apiKey?: string // AES-256-GCM encrypted, for headless/external API callers\n}\n\nfunction adminFilePath(): string {\n const dataDir = process.env.TA_DATA_DIR ?? 'data'\n return path.join(process.cwd(), dataDir, 'ta-admin.json')\n}\n\nexport function generateDefaultPassword(): string {\n return crypto.randomBytes(6).toString('hex') // 12-char hex, easy to type\n}\n\nexport function hashPassword(password: string): string {\n const secret = process.env.THIRD_AUDIENCE_SECRET ?? 'ta-salt'\n return crypto.createHash('sha256').update(secret + password).digest('hex')\n}\n\nexport function loadAdmin(): AdminRecord | null {\n const filePath = adminFilePath()\n if (!fs.existsSync(filePath)) return null\n try {\n return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as AdminRecord\n } catch {\n return null\n }\n}\n\nexport function saveAdmin(record: AdminRecord): void {\n const filePath = adminFilePath()\n const dir = path.dirname(filePath)\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })\n fs.writeFileSync(filePath, JSON.stringify(record, null, 2), 'utf-8')\n}\n\nexport const DEFAULT_PASSWORD = 'Chang3M3Now!'\n\nexport function initAdmin(): { password: string; apiKey: string; isNew: boolean } {\n const existing = loadAdmin()\n if (existing) return { password: '', apiKey: '', isNew: false }\n\n const apiKey = generateApiKey()\n saveAdmin({\n passwordHash: hashPassword(DEFAULT_PASSWORD),\n isDefaultPassword: true,\n createdAt: new Date().toISOString(),\n lastLoginAt: null,\n apiKey: encryptApiKey(apiKey),\n })\n return { password: DEFAULT_PASSWORD, apiKey, isNew: true }\n}\n\nexport function verifyPassword(password: string): boolean {\n const record = loadAdmin()\n if (!record) return false\n return record.passwordHash === hashPassword(password)\n}\n\nexport function updatePassword(newPassword: string): void {\n const record = loadAdmin()\n if (!record) return\n saveAdmin({\n ...record,\n passwordHash: hashPassword(newPassword),\n isDefaultPassword: false,\n })\n}\n\nexport function recordLogin(): void {\n const record = loadAdmin()\n if (!record) return\n saveAdmin({ ...record, lastLoginAt: new Date().toISOString() })\n}\n\n// ---------------------------------------------------------------------------\n// API key — AES-256-GCM encrypted at rest, mirroring WP's SECURE_AUTH_KEY approach\n// ---------------------------------------------------------------------------\n\nconst CIPHER = 'aes-256-gcm'\n\nfunction getEncryptionKey(): Buffer {\n const secret = process.env.THIRD_AUDIENCE_SECRET ?? 'ta-fallback-key-change-me'\n // Derive a 32-byte key from the secret using SHA-256\n return crypto.createHash('sha256').update(secret).digest()\n}\n\nfunction encryptApiKey(plaintext: string): string {\n const iv = crypto.randomBytes(12)\n const key = getEncryptionKey()\n const cipher = crypto.createCipheriv(CIPHER, key, iv) as crypto.CipherGCM\n const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])\n const tag = cipher.getAuthTag()\n // Format: iv(24 hex) + tag(32 hex) + encrypted(hex)\n return iv.toString('hex') + tag.toString('hex') + encrypted.toString('hex')\n}\n\nfunction decryptApiKey(encoded: string): string | null {\n try {\n const iv = Buffer.from(encoded.slice(0, 24), 'hex')\n const tag = Buffer.from(encoded.slice(24, 56), 'hex')\n const encrypted = Buffer.from(encoded.slice(56), 'hex')\n const key = getEncryptionKey()\n const decipher = crypto.createDecipheriv(CIPHER, key, iv) as crypto.DecipherGCM\n decipher.setAuthTag(tag)\n return decipher.update(encrypted) + decipher.final('utf8')\n } catch {\n return null\n }\n}\n\nexport function generateApiKey(): string {\n return 'ta_' + crypto.randomBytes(24).toString('hex') // 51-char key\n}\n\nexport function getApiKey(): string | null {\n const record = loadAdmin()\n if (!record?.apiKey) return null\n return decryptApiKey(record.apiKey)\n}\n\nexport function rotateApiKey(): string {\n const record = loadAdmin()\n if (!record) throw new Error('Admin store not initialised')\n const newKey = generateApiKey()\n saveAdmin({ ...record, apiKey: encryptApiKey(newKey) })\n return newKey\n}\n\nexport function verifyApiKey(key: string): boolean {\n const stored = getApiKey()\n if (!stored) return false\n if (key.length !== stored.length) return false\n return crypto.timingSafeEqual(Buffer.from(key), Buffer.from(stored))\n}\n\n// ---------------------------------------------------------------------------\n// Session cookie: HMAC-SHA256(secret, userId + timestamp) — stateless, no DB\n// ---------------------------------------------------------------------------\nexport function signSession(payload: string): string {\n const secret = process.env.THIRD_AUDIENCE_SECRET ?? 'ta-salt'\n const sig = crypto.createHmac('sha256', secret).update(payload).digest('hex')\n return `${payload}.${sig}`\n}\n\nexport function verifySession(token: string): boolean {\n const lastDot = token.lastIndexOf('.')\n if (lastDot === -1) return false\n const payload = token.slice(0, lastDot)\n const sig = token.slice(lastDot + 1)\n const expected = crypto.createHmac('sha256', process.env.THIRD_AUDIENCE_SECRET ?? 'ta-salt')\n .update(payload).digest('hex')\n // Constant-time comparison\n if (sig.length !== expected.length) return false\n return crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'))\n}\n"],"mappings":"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","names":["path","fs","crypto","VisitTracker"]}
|