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
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/dashboard/admin-store.ts
|
|
12
|
+
var admin_store_exports = {};
|
|
13
|
+
__export(admin_store_exports, {
|
|
14
|
+
DEFAULT_PASSWORD: () => DEFAULT_PASSWORD,
|
|
15
|
+
generateApiKey: () => generateApiKey,
|
|
16
|
+
generateDefaultPassword: () => generateDefaultPassword,
|
|
17
|
+
getApiKey: () => getApiKey,
|
|
18
|
+
hashPassword: () => hashPassword,
|
|
19
|
+
initAdmin: () => initAdmin,
|
|
20
|
+
loadAdmin: () => loadAdmin,
|
|
21
|
+
recordLogin: () => recordLogin,
|
|
22
|
+
rotateApiKey: () => rotateApiKey,
|
|
23
|
+
saveAdmin: () => saveAdmin,
|
|
24
|
+
signSession: () => signSession,
|
|
25
|
+
updatePassword: () => updatePassword,
|
|
26
|
+
verifyApiKey: () => verifyApiKey,
|
|
27
|
+
verifyPassword: () => verifyPassword,
|
|
28
|
+
verifySession: () => verifySession
|
|
29
|
+
});
|
|
30
|
+
import fs from "fs";
|
|
31
|
+
import path from "path";
|
|
32
|
+
import crypto from "crypto";
|
|
33
|
+
function adminFilePath() {
|
|
34
|
+
const dataDir = process.env.TA_DATA_DIR ?? "data";
|
|
35
|
+
return path.join(process.cwd(), dataDir, "ta-admin.json");
|
|
36
|
+
}
|
|
37
|
+
function generateDefaultPassword() {
|
|
38
|
+
return crypto.randomBytes(6).toString("hex");
|
|
39
|
+
}
|
|
40
|
+
function hashPassword(password) {
|
|
41
|
+
const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt";
|
|
42
|
+
return crypto.createHash("sha256").update(secret + password).digest("hex");
|
|
43
|
+
}
|
|
44
|
+
function loadAdmin() {
|
|
45
|
+
const filePath = adminFilePath();
|
|
46
|
+
if (!fs.existsSync(filePath)) return null;
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function saveAdmin(record) {
|
|
54
|
+
const filePath = adminFilePath();
|
|
55
|
+
const dir = path.dirname(filePath);
|
|
56
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
57
|
+
fs.writeFileSync(filePath, JSON.stringify(record, null, 2), "utf-8");
|
|
58
|
+
}
|
|
59
|
+
function initAdmin() {
|
|
60
|
+
const existing = loadAdmin();
|
|
61
|
+
if (existing) return { password: "", apiKey: "", isNew: false };
|
|
62
|
+
const apiKey = generateApiKey();
|
|
63
|
+
saveAdmin({
|
|
64
|
+
passwordHash: hashPassword(DEFAULT_PASSWORD),
|
|
65
|
+
isDefaultPassword: true,
|
|
66
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
67
|
+
lastLoginAt: null,
|
|
68
|
+
apiKey: encryptApiKey(apiKey)
|
|
69
|
+
});
|
|
70
|
+
return { password: DEFAULT_PASSWORD, apiKey, isNew: true };
|
|
71
|
+
}
|
|
72
|
+
function verifyPassword(password) {
|
|
73
|
+
const record = loadAdmin();
|
|
74
|
+
if (!record) return false;
|
|
75
|
+
return record.passwordHash === hashPassword(password);
|
|
76
|
+
}
|
|
77
|
+
function updatePassword(newPassword) {
|
|
78
|
+
const record = loadAdmin();
|
|
79
|
+
if (!record) return;
|
|
80
|
+
saveAdmin({
|
|
81
|
+
...record,
|
|
82
|
+
passwordHash: hashPassword(newPassword),
|
|
83
|
+
isDefaultPassword: false
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function recordLogin() {
|
|
87
|
+
const record = loadAdmin();
|
|
88
|
+
if (!record) return;
|
|
89
|
+
saveAdmin({ ...record, lastLoginAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
90
|
+
}
|
|
91
|
+
function getEncryptionKey() {
|
|
92
|
+
const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-fallback-key-change-me";
|
|
93
|
+
return crypto.createHash("sha256").update(secret).digest();
|
|
94
|
+
}
|
|
95
|
+
function encryptApiKey(plaintext) {
|
|
96
|
+
const iv = crypto.randomBytes(12);
|
|
97
|
+
const key = getEncryptionKey();
|
|
98
|
+
const cipher = crypto.createCipheriv(CIPHER, key, iv);
|
|
99
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
100
|
+
const tag = cipher.getAuthTag();
|
|
101
|
+
return iv.toString("hex") + tag.toString("hex") + encrypted.toString("hex");
|
|
102
|
+
}
|
|
103
|
+
function decryptApiKey(encoded) {
|
|
104
|
+
try {
|
|
105
|
+
const iv = Buffer.from(encoded.slice(0, 24), "hex");
|
|
106
|
+
const tag = Buffer.from(encoded.slice(24, 56), "hex");
|
|
107
|
+
const encrypted = Buffer.from(encoded.slice(56), "hex");
|
|
108
|
+
const key = getEncryptionKey();
|
|
109
|
+
const decipher = crypto.createDecipheriv(CIPHER, key, iv);
|
|
110
|
+
decipher.setAuthTag(tag);
|
|
111
|
+
return decipher.update(encrypted) + decipher.final("utf8");
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function generateApiKey() {
|
|
117
|
+
return "ta_" + crypto.randomBytes(24).toString("hex");
|
|
118
|
+
}
|
|
119
|
+
function getApiKey() {
|
|
120
|
+
const record = loadAdmin();
|
|
121
|
+
if (!record?.apiKey) return null;
|
|
122
|
+
return decryptApiKey(record.apiKey);
|
|
123
|
+
}
|
|
124
|
+
function rotateApiKey() {
|
|
125
|
+
const record = loadAdmin();
|
|
126
|
+
if (!record) throw new Error("Admin store not initialised");
|
|
127
|
+
const newKey = generateApiKey();
|
|
128
|
+
saveAdmin({ ...record, apiKey: encryptApiKey(newKey) });
|
|
129
|
+
return newKey;
|
|
130
|
+
}
|
|
131
|
+
function verifyApiKey(key) {
|
|
132
|
+
const stored = getApiKey();
|
|
133
|
+
if (!stored) return false;
|
|
134
|
+
if (key.length !== stored.length) return false;
|
|
135
|
+
return crypto.timingSafeEqual(Buffer.from(key), Buffer.from(stored));
|
|
136
|
+
}
|
|
137
|
+
function signSession(payload) {
|
|
138
|
+
const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt";
|
|
139
|
+
const sig = crypto.createHmac("sha256", secret).update(payload).digest("hex");
|
|
140
|
+
return `${payload}.${sig}`;
|
|
141
|
+
}
|
|
142
|
+
function verifySession(token) {
|
|
143
|
+
const lastDot = token.lastIndexOf(".");
|
|
144
|
+
if (lastDot === -1) return false;
|
|
145
|
+
const payload = token.slice(0, lastDot);
|
|
146
|
+
const sig = token.slice(lastDot + 1);
|
|
147
|
+
const expected = crypto.createHmac("sha256", process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt").update(payload).digest("hex");
|
|
148
|
+
if (sig.length !== expected.length) return false;
|
|
149
|
+
return crypto.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
|
|
150
|
+
}
|
|
151
|
+
var DEFAULT_PASSWORD, CIPHER;
|
|
152
|
+
var init_admin_store = __esm({
|
|
153
|
+
"src/dashboard/admin-store.ts"() {
|
|
154
|
+
"use strict";
|
|
155
|
+
DEFAULT_PASSWORD = "Chang3M3Now!";
|
|
156
|
+
CIPHER = "aes-256-gcm";
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// src/dashboard/routes/login-route.ts
|
|
161
|
+
init_admin_store();
|
|
162
|
+
import { NextResponse } from "next/server";
|
|
163
|
+
var COOKIE_NAME = "ta_session";
|
|
164
|
+
var COOKIE_MAX_AGE = 60 * 60 * 8;
|
|
165
|
+
var LOGIN_HTML = (error, reset) => `<!DOCTYPE html>
|
|
166
|
+
<html lang="en">
|
|
167
|
+
<head>
|
|
168
|
+
<meta charset="UTF-8">
|
|
169
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
170
|
+
<title>${reset ? "Reset Password" : "Third Audience \u2014 Login"}</title>
|
|
171
|
+
<style>
|
|
172
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
173
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f7; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
|
174
|
+
.card { background: #fff; border-radius: 18px; box-shadow: 0 4px 24px rgba(0,0,0,.08); padding: 40px 48px; width: 100%; max-width: 380px; }
|
|
175
|
+
.logo { font-size: 22px; font-weight: 700; color: #1d1d1f; margin-bottom: 8px; }
|
|
176
|
+
.subtitle { font-size: 14px; color: #6e6e73; margin-bottom: 32px; }
|
|
177
|
+
label { display: block; font-size: 13px; font-weight: 500; color: #1d1d1f; margin-bottom: 6px; }
|
|
178
|
+
input[type=password] { width: 100%; padding: 10px 14px; border: 1.5px solid #d2d2d7; border-radius: 10px; font-size: 15px; outline: none; transition: border-color .15s; }
|
|
179
|
+
input[type=password]:focus { border-color: #007aff; }
|
|
180
|
+
.error { background: #fff2f2; border: 1px solid #ffbaba; border-radius: 8px; padding: 10px 14px; font-size: 13px; color: #c0392b; margin-bottom: 20px; }
|
|
181
|
+
.btn { width: 100%; background: #007aff; color: #fff; border: none; border-radius: 10px; padding: 12px; font-size: 15px; font-weight: 600; cursor: pointer; margin-top: 20px; transition: background .15s; }
|
|
182
|
+
.btn:hover { background: #0062cc; }
|
|
183
|
+
.hint { font-size: 12px; color: #6e6e73; text-align: center; margin-top: 16px; }
|
|
184
|
+
</style>
|
|
185
|
+
</head>
|
|
186
|
+
<body>
|
|
187
|
+
<div class="card">
|
|
188
|
+
<div class="logo">Third Audience</div>
|
|
189
|
+
<div class="subtitle">${reset ? "Choose a new password to continue" : "Sign in to your dashboard"}</div>
|
|
190
|
+
${error ? `<div class="error">${error}</div>` : ""}
|
|
191
|
+
<form method="POST">
|
|
192
|
+
${reset ? `
|
|
193
|
+
<div style="margin-bottom:16px">
|
|
194
|
+
<label for="password">New password</label>
|
|
195
|
+
<input id="password" name="password" type="password" autocomplete="new-password" required minlength="8" placeholder="At least 8 characters">
|
|
196
|
+
</div>
|
|
197
|
+
<div>
|
|
198
|
+
<label for="confirm">Confirm password</label>
|
|
199
|
+
<input id="confirm" name="confirm" type="password" autocomplete="new-password" required placeholder="Repeat password">
|
|
200
|
+
</div>
|
|
201
|
+
<input type="hidden" name="action" value="reset">
|
|
202
|
+
<button class="btn" type="submit">Set password</button>
|
|
203
|
+
` : `
|
|
204
|
+
<div>
|
|
205
|
+
<label for="password">Password</label>
|
|
206
|
+
<input id="password" name="password" type="password" autocomplete="current-password" required placeholder="Enter your password">
|
|
207
|
+
</div>
|
|
208
|
+
<button class="btn" type="submit">Sign in</button>
|
|
209
|
+
`}
|
|
210
|
+
<p class="hint">Third Audience Dashboard</p>
|
|
211
|
+
</form>
|
|
212
|
+
</div>
|
|
213
|
+
</body>
|
|
214
|
+
</html>`;
|
|
215
|
+
async function GET(req) {
|
|
216
|
+
const reset = req.nextUrl.searchParams.get("reset") === "1";
|
|
217
|
+
return new NextResponse(LOGIN_HTML(void 0, reset), {
|
|
218
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
async function POST(req) {
|
|
222
|
+
const body = await req.formData();
|
|
223
|
+
const action = body.get("action");
|
|
224
|
+
const password = body.get("password");
|
|
225
|
+
const confirm = body.get("confirm");
|
|
226
|
+
if (action === "reset") {
|
|
227
|
+
if (!password || password.length < 8) {
|
|
228
|
+
return new NextResponse(LOGIN_HTML("Password must be at least 8 characters.", true), {
|
|
229
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (password !== confirm) {
|
|
233
|
+
return new NextResponse(LOGIN_HTML("Passwords do not match.", true), {
|
|
234
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
const { updatePassword: updatePassword2 } = await Promise.resolve().then(() => (init_admin_store(), admin_store_exports));
|
|
238
|
+
updatePassword2(password);
|
|
239
|
+
const token2 = signSession("admin:" + Date.now());
|
|
240
|
+
const res2 = NextResponse.redirect(new URL("/third-audience/", req.nextUrl));
|
|
241
|
+
res2.cookies.set(COOKIE_NAME, token2, {
|
|
242
|
+
httpOnly: true,
|
|
243
|
+
sameSite: "strict",
|
|
244
|
+
secure: process.env.NODE_ENV === "production",
|
|
245
|
+
maxAge: COOKIE_MAX_AGE,
|
|
246
|
+
path: "/"
|
|
247
|
+
});
|
|
248
|
+
return res2;
|
|
249
|
+
}
|
|
250
|
+
if (!password || !verifyPassword(password)) {
|
|
251
|
+
return new NextResponse(LOGIN_HTML("Incorrect password."), {
|
|
252
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
recordLogin();
|
|
256
|
+
const admin = loadAdmin();
|
|
257
|
+
const token = signSession("admin:" + Date.now());
|
|
258
|
+
if (admin?.isDefaultPassword) {
|
|
259
|
+
const res2 = NextResponse.redirect(new URL("/third-audience/login?reset=1", req.nextUrl));
|
|
260
|
+
res2.cookies.set(COOKIE_NAME + "_reset", token, {
|
|
261
|
+
httpOnly: true,
|
|
262
|
+
sameSite: "strict",
|
|
263
|
+
secure: process.env.NODE_ENV === "production",
|
|
264
|
+
maxAge: 300,
|
|
265
|
+
// 5 min to complete reset
|
|
266
|
+
path: "/third-audience/login"
|
|
267
|
+
});
|
|
268
|
+
return res2;
|
|
269
|
+
}
|
|
270
|
+
const res = NextResponse.redirect(new URL("/third-audience/", req.nextUrl));
|
|
271
|
+
res.cookies.set(COOKIE_NAME, token, {
|
|
272
|
+
httpOnly: true,
|
|
273
|
+
sameSite: "strict",
|
|
274
|
+
secure: process.env.NODE_ENV === "production",
|
|
275
|
+
maxAge: COOKIE_MAX_AGE,
|
|
276
|
+
path: "/"
|
|
277
|
+
});
|
|
278
|
+
return res;
|
|
279
|
+
}
|
|
280
|
+
export {
|
|
281
|
+
GET,
|
|
282
|
+
POST
|
|
283
|
+
};
|
|
284
|
+
//# sourceMappingURL=login-route.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/admin-store.ts","../../../src/dashboard/routes/login-route.ts"],"sourcesContent":["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","import { NextResponse, type NextRequest } from 'next/server'\nimport { verifyPassword, signSession, recordLogin, loadAdmin } from '../admin-store.js'\n\nconst COOKIE_NAME = 'ta_session'\nconst COOKIE_MAX_AGE = 60 * 60 * 8 // 8 hours\n\nconst LOGIN_HTML = (error?: string, reset?: boolean) => `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>${reset ? 'Reset Password' : 'Third Audience — Login'}</title>\n <style>\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f7; min-height: 100vh; display: flex; align-items: center; justify-content: center; }\n .card { background: #fff; border-radius: 18px; box-shadow: 0 4px 24px rgba(0,0,0,.08); padding: 40px 48px; width: 100%; max-width: 380px; }\n .logo { font-size: 22px; font-weight: 700; color: #1d1d1f; margin-bottom: 8px; }\n .subtitle { font-size: 14px; color: #6e6e73; margin-bottom: 32px; }\n label { display: block; font-size: 13px; font-weight: 500; color: #1d1d1f; margin-bottom: 6px; }\n input[type=password] { width: 100%; padding: 10px 14px; border: 1.5px solid #d2d2d7; border-radius: 10px; font-size: 15px; outline: none; transition: border-color .15s; }\n input[type=password]:focus { border-color: #007aff; }\n .error { background: #fff2f2; border: 1px solid #ffbaba; border-radius: 8px; padding: 10px 14px; font-size: 13px; color: #c0392b; margin-bottom: 20px; }\n .btn { width: 100%; background: #007aff; color: #fff; border: none; border-radius: 10px; padding: 12px; font-size: 15px; font-weight: 600; cursor: pointer; margin-top: 20px; transition: background .15s; }\n .btn:hover { background: #0062cc; }\n .hint { font-size: 12px; color: #6e6e73; text-align: center; margin-top: 16px; }\n </style>\n</head>\n<body>\n <div class=\"card\">\n <div class=\"logo\">Third Audience</div>\n <div class=\"subtitle\">${reset ? 'Choose a new password to continue' : 'Sign in to your dashboard'}</div>\n ${error ? `<div class=\"error\">${error}</div>` : ''}\n <form method=\"POST\">\n ${reset ? `\n <div style=\"margin-bottom:16px\">\n <label for=\"password\">New password</label>\n <input id=\"password\" name=\"password\" type=\"password\" autocomplete=\"new-password\" required minlength=\"8\" placeholder=\"At least 8 characters\">\n </div>\n <div>\n <label for=\"confirm\">Confirm password</label>\n <input id=\"confirm\" name=\"confirm\" type=\"password\" autocomplete=\"new-password\" required placeholder=\"Repeat password\">\n </div>\n <input type=\"hidden\" name=\"action\" value=\"reset\">\n <button class=\"btn\" type=\"submit\">Set password</button>\n ` : `\n <div>\n <label for=\"password\">Password</label>\n <input id=\"password\" name=\"password\" type=\"password\" autocomplete=\"current-password\" required placeholder=\"Enter your password\">\n </div>\n <button class=\"btn\" type=\"submit\">Sign in</button>\n `}\n <p class=\"hint\">Third Audience Dashboard</p>\n </form>\n </div>\n</body>\n</html>`\n\nexport async function GET(req: NextRequest): Promise<NextResponse> {\n const reset = req.nextUrl.searchParams.get('reset') === '1'\n return new NextResponse(LOGIN_HTML(undefined, reset), {\n headers: { 'Content-Type': 'text/html; charset=utf-8' },\n })\n}\n\nexport async function POST(req: NextRequest): Promise<NextResponse> {\n const body = await req.formData()\n const action = body.get('action') as string | null\n const password = body.get('password') as string | null\n const confirm = body.get('confirm') as string | null\n\n if (action === 'reset') {\n if (!password || password.length < 8) {\n return new NextResponse(LOGIN_HTML('Password must be at least 8 characters.', true), {\n headers: { 'Content-Type': 'text/html; charset=utf-8' },\n })\n }\n if (password !== confirm) {\n return new NextResponse(LOGIN_HTML('Passwords do not match.', true), {\n headers: { 'Content-Type': 'text/html; charset=utf-8' },\n })\n }\n const { updatePassword } = await import('../admin-store.js')\n updatePassword(password)\n // Issue new session after reset\n const token = signSession('admin:' + Date.now())\n const res = NextResponse.redirect(new URL('/third-audience/', req.nextUrl))\n res.cookies.set(COOKIE_NAME, token, {\n httpOnly: true,\n sameSite: 'strict',\n secure: process.env.NODE_ENV === 'production',\n maxAge: COOKIE_MAX_AGE,\n path: '/',\n })\n return res\n }\n\n // Normal login\n if (!password || !verifyPassword(password)) {\n return new NextResponse(LOGIN_HTML('Incorrect password.'), {\n headers: { 'Content-Type': 'text/html; charset=utf-8' },\n })\n }\n\n recordLogin()\n\n const admin = loadAdmin()\n const token = signSession('admin:' + Date.now())\n\n // If this was the default password, force reset before entering dashboard\n if (admin?.isDefaultPassword) {\n const res = NextResponse.redirect(new URL('/third-audience/login?reset=1', req.nextUrl))\n // Set a temporary cookie so reset page knows we're authenticated\n res.cookies.set(COOKIE_NAME + '_reset', token, {\n httpOnly: true,\n sameSite: 'strict',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 300, // 5 min to complete reset\n path: '/third-audience/login',\n })\n return res\n }\n\n const res = NextResponse.redirect(new URL('/third-audience/', req.nextUrl))\n res.cookies.set(COOKIE_NAME, token, {\n httpOnly: true,\n sameSite: 'strict',\n secure: process.env.NODE_ENV === 'production',\n maxAge: COOKIE_MAX_AGE,\n path: '/',\n })\n return res\n}\n"],"mappings":"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","names":["updatePassword","token","res"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handler for GET /api/third-audience/markdown/[...slug]
|
|
5
|
+
*
|
|
6
|
+
* Install in your Next.js app at:
|
|
7
|
+
* app/api/third-audience/markdown/[...slug]/route.ts
|
|
8
|
+
*/
|
|
9
|
+
declare function GET(req: NextRequest, { params }: {
|
|
10
|
+
params: {
|
|
11
|
+
slug: string[];
|
|
12
|
+
};
|
|
13
|
+
}): Promise<NextResponse<unknown>>;
|
|
14
|
+
|
|
15
|
+
export { GET };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handler for GET /api/third-audience/markdown/[...slug]
|
|
5
|
+
*
|
|
6
|
+
* Install in your Next.js app at:
|
|
7
|
+
* app/api/third-audience/markdown/[...slug]/route.ts
|
|
8
|
+
*/
|
|
9
|
+
declare function GET(req: NextRequest, { params }: {
|
|
10
|
+
params: {
|
|
11
|
+
slug: string[];
|
|
12
|
+
};
|
|
13
|
+
}): Promise<NextResponse<unknown>>;
|
|
14
|
+
|
|
15
|
+
export { GET };
|
|
@@ -0,0 +1,239 @@
|
|
|
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 __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/dashboard/routes/markdown-route.ts
|
|
31
|
+
var markdown_route_exports = {};
|
|
32
|
+
__export(markdown_route_exports, {
|
|
33
|
+
GET: () => GET
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(markdown_route_exports);
|
|
36
|
+
var import_server = require("next/server");
|
|
37
|
+
var import_path3 = __toESM(require("path"));
|
|
38
|
+
|
|
39
|
+
// src/core/mdx-reader.ts
|
|
40
|
+
var import_fs = __toESM(require("fs"));
|
|
41
|
+
var import_path = __toESM(require("path"));
|
|
42
|
+
var import_gray_matter = __toESM(require("gray-matter"));
|
|
43
|
+
var MdxReader = class {
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.contentDir = options.contentDir;
|
|
46
|
+
}
|
|
47
|
+
/** Read a single MDX file by slug. Returns null if not found. */
|
|
48
|
+
read(slug) {
|
|
49
|
+
const candidates = [
|
|
50
|
+
import_path.default.join(this.contentDir, `${slug}.mdx`),
|
|
51
|
+
import_path.default.join(this.contentDir, `${slug}.md`),
|
|
52
|
+
import_path.default.join(this.contentDir, slug, "index.mdx"),
|
|
53
|
+
import_path.default.join(this.contentDir, slug, "index.md")
|
|
54
|
+
];
|
|
55
|
+
for (const filePath of candidates) {
|
|
56
|
+
if (import_fs.default.existsSync(filePath)) {
|
|
57
|
+
return this.parseFile(slug, filePath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/** Read all MDX files recursively. */
|
|
63
|
+
readAll() {
|
|
64
|
+
if (!import_fs.default.existsSync(this.contentDir)) return [];
|
|
65
|
+
return this.walkDir(this.contentDir, this.contentDir);
|
|
66
|
+
}
|
|
67
|
+
walkDir(dir, root) {
|
|
68
|
+
const results = [];
|
|
69
|
+
for (const entry of import_fs.default.readdirSync(dir, { withFileTypes: true })) {
|
|
70
|
+
const fullPath = import_path.default.join(dir, entry.name);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
results.push(...this.walkDir(fullPath, root));
|
|
73
|
+
} else if (entry.name.endsWith(".mdx") || entry.name.endsWith(".md")) {
|
|
74
|
+
const relative = import_path.default.relative(root, fullPath);
|
|
75
|
+
const slug = relative.replace(/\.(mdx|md)$/, "").replace(/\/index$/, "");
|
|
76
|
+
results.push(this.parseFile(slug, fullPath));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
81
|
+
parseFile(slug, filePath) {
|
|
82
|
+
const raw = import_fs.default.readFileSync(filePath, "utf-8");
|
|
83
|
+
const { data: frontmatter, content: rawContent } = (0, import_gray_matter.default)(raw);
|
|
84
|
+
return { slug, filePath, frontmatter, rawContent };
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/core/markdown-renderer.ts
|
|
89
|
+
var MarkdownRenderer = class {
|
|
90
|
+
render(file) {
|
|
91
|
+
const header = this.buildFrontmatterHeader(file.frontmatter);
|
|
92
|
+
const body = this.stripJsx(file.rawContent);
|
|
93
|
+
return header ? `${header}
|
|
94
|
+
|
|
95
|
+
${body}` : body;
|
|
96
|
+
}
|
|
97
|
+
buildFrontmatterHeader(fm) {
|
|
98
|
+
const keys = Object.keys(fm);
|
|
99
|
+
if (keys.length === 0) return "";
|
|
100
|
+
const lines = keys.filter((k) => fm[k] !== void 0 && fm[k] !== null).map((k) => `${k}: ${this.yamlValue(fm[k])}`);
|
|
101
|
+
return `---
|
|
102
|
+
${lines.join("\n")}
|
|
103
|
+
---`;
|
|
104
|
+
}
|
|
105
|
+
yamlValue(val) {
|
|
106
|
+
if (typeof val === "string") {
|
|
107
|
+
return /[:#\[\]{},&*?|<>=!%@`]/.test(val) ? `"${val.replace(/"/g, '\\"')}"` : val;
|
|
108
|
+
}
|
|
109
|
+
if (val instanceof Date) return val.toISOString();
|
|
110
|
+
if (Array.isArray(val)) return `[${val.map((v) => this.yamlValue(v)).join(", ")}]`;
|
|
111
|
+
return String(val);
|
|
112
|
+
}
|
|
113
|
+
stripJsx(content) {
|
|
114
|
+
let out = content;
|
|
115
|
+
out = out.replace(/^import\s+.*?['"].*?['"]\s*\n?/gm, "");
|
|
116
|
+
out = out.replace(/^export\s+(?:default\s+)?(?:const|let|var|function|class)\s+[\s\S]*?(?=\n(?=[^{]|\n)|\n{2,})/gm, "");
|
|
117
|
+
out = out.replace(/^export\s*\{[^}]*\}\s*(?:from\s+['"][^'"]*['"])?\s*\n?/gm, "");
|
|
118
|
+
out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*\/>/g, "");
|
|
119
|
+
out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*>[\s\S]*?<\/\1>/g, "");
|
|
120
|
+
out = out.replace(/^\s*\{[^}]+\}\s*\n/gm, "");
|
|
121
|
+
out = out.replace(/\n{3,}/g, "\n\n");
|
|
122
|
+
return out.trim();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// src/cache/cache-manager.ts
|
|
127
|
+
var import_fs2 = __toESM(require("fs"));
|
|
128
|
+
var import_path2 = __toESM(require("path"));
|
|
129
|
+
var import_crypto = __toESM(require("crypto"));
|
|
130
|
+
var CacheManager = class {
|
|
131
|
+
constructor(opts) {
|
|
132
|
+
this.memCache = /* @__PURE__ */ new Map();
|
|
133
|
+
this.cacheDir = opts.cacheDir;
|
|
134
|
+
this.maxMemoryEntries = opts.maxMemoryEntries ?? 500;
|
|
135
|
+
this.defaultTtl = opts.ttl ?? 3600;
|
|
136
|
+
}
|
|
137
|
+
get(key) {
|
|
138
|
+
const mem = this.memCache.get(key);
|
|
139
|
+
if (mem && this.isValid(mem)) return mem.content;
|
|
140
|
+
if (mem) this.memCache.delete(key);
|
|
141
|
+
const file = this.readFileCache(key);
|
|
142
|
+
if (file && this.isValid(file)) {
|
|
143
|
+
this.setMemory(key, file);
|
|
144
|
+
return file.content;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
set(key, content, etag = "", ttl = this.defaultTtl) {
|
|
149
|
+
const entry = { content, etag, cachedAt: Date.now(), ttl };
|
|
150
|
+
this.setMemory(key, entry);
|
|
151
|
+
this.writeFileCache(key, entry);
|
|
152
|
+
}
|
|
153
|
+
/** Invalidate by key prefix — used when source .mdx file changes. */
|
|
154
|
+
invalidate(keyPrefix) {
|
|
155
|
+
for (const k of this.memCache.keys()) {
|
|
156
|
+
if (k.startsWith(keyPrefix)) this.memCache.delete(k);
|
|
157
|
+
}
|
|
158
|
+
const dir = this.cacheDir;
|
|
159
|
+
if (!import_fs2.default.existsSync(dir)) return;
|
|
160
|
+
for (const file of import_fs2.default.readdirSync(dir)) {
|
|
161
|
+
if (file.startsWith(this.hashKey(keyPrefix).slice(0, 8))) {
|
|
162
|
+
import_fs2.default.unlinkSync(import_path2.default.join(dir, file));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
stats() {
|
|
167
|
+
const fsEntries = import_fs2.default.existsSync(this.cacheDir) ? import_fs2.default.readdirSync(this.cacheDir).filter((f) => f.endsWith(".json")).length : 0;
|
|
168
|
+
return { memEntries: this.memCache.size, fsEntries };
|
|
169
|
+
}
|
|
170
|
+
isValid(entry) {
|
|
171
|
+
return Date.now() - entry.cachedAt < entry.ttl * 1e3;
|
|
172
|
+
}
|
|
173
|
+
setMemory(key, entry) {
|
|
174
|
+
if (this.memCache.size >= this.maxMemoryEntries) {
|
|
175
|
+
const firstKey = this.memCache.keys().next().value;
|
|
176
|
+
if (firstKey) this.memCache.delete(firstKey);
|
|
177
|
+
}
|
|
178
|
+
this.memCache.set(key, entry);
|
|
179
|
+
}
|
|
180
|
+
hashKey(key) {
|
|
181
|
+
return import_crypto.default.createHash("sha256").update(key).digest("hex");
|
|
182
|
+
}
|
|
183
|
+
filePath(key) {
|
|
184
|
+
return import_path2.default.join(this.cacheDir, `${this.hashKey(key)}.json`);
|
|
185
|
+
}
|
|
186
|
+
readFileCache(key) {
|
|
187
|
+
const fp = this.filePath(key);
|
|
188
|
+
if (!import_fs2.default.existsSync(fp)) return null;
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(import_fs2.default.readFileSync(fp, "utf-8"));
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
writeFileCache(key, entry) {
|
|
196
|
+
try {
|
|
197
|
+
import_fs2.default.mkdirSync(this.cacheDir, { recursive: true });
|
|
198
|
+
import_fs2.default.writeFileSync(this.filePath(key), JSON.stringify(entry), "utf-8");
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// src/dashboard/routes/markdown-route.ts
|
|
205
|
+
var reader = new MdxReader({ contentDir: import_path3.default.join(process.cwd(), process.env.TA_CONTENT_DIR ?? "content") });
|
|
206
|
+
var renderer = new MarkdownRenderer();
|
|
207
|
+
var cache = new CacheManager({
|
|
208
|
+
cacheDir: import_path3.default.join(process.cwd(), process.env.TA_DATA_DIR ?? "data", "ta-cache")
|
|
209
|
+
});
|
|
210
|
+
async function GET(req, { params }) {
|
|
211
|
+
const slug = params.slug.join("/");
|
|
212
|
+
const cacheKey = `markdown:${slug}`;
|
|
213
|
+
const cached = cache.get(cacheKey);
|
|
214
|
+
if (cached) {
|
|
215
|
+
return new import_server.NextResponse(cached, {
|
|
216
|
+
headers: {
|
|
217
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
218
|
+
"X-Cache": "HIT"
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const file = reader.read(slug);
|
|
223
|
+
if (!file) {
|
|
224
|
+
return new import_server.NextResponse("Not Found", { status: 404 });
|
|
225
|
+
}
|
|
226
|
+
const markdown = renderer.render(file);
|
|
227
|
+
cache.set(cacheKey, markdown);
|
|
228
|
+
return new import_server.NextResponse(markdown, {
|
|
229
|
+
headers: {
|
|
230
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
231
|
+
"X-Cache": "MISS"
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
236
|
+
0 && (module.exports = {
|
|
237
|
+
GET
|
|
238
|
+
});
|
|
239
|
+
//# sourceMappingURL=markdown-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/routes/markdown-route.ts","../../../src/core/mdx-reader.ts","../../../src/core/markdown-renderer.ts","../../../src/cache/cache-manager.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { MarkdownRenderer } from '../../core/markdown-renderer.js'\nimport { CacheManager } from '../../cache/cache-manager.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\nconst renderer = new MarkdownRenderer()\nconst cache = new CacheManager({\n cacheDir: path.join(process.cwd(), process.env.TA_DATA_DIR ?? 'data', 'ta-cache'),\n})\n\n/**\n * Handler for GET /api/third-audience/markdown/[...slug]\n *\n * Install in your Next.js app at:\n * app/api/third-audience/markdown/[...slug]/route.ts\n */\nexport async function GET(req: NextRequest, { params }: { params: { slug: string[] } }) {\n const slug = params.slug.join('/')\n const cacheKey = `markdown:${slug}`\n\n const cached = cache.get(cacheKey)\n if (cached) {\n return new NextResponse(cached, {\n headers: {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'X-Cache': 'HIT',\n },\n })\n }\n\n const file = reader.read(slug)\n if (!file) {\n return new NextResponse('Not Found', { status: 404 })\n }\n\n const markdown = renderer.render(file)\n cache.set(cacheKey, markdown)\n\n return new NextResponse(markdown, {\n headers: {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'X-Cache': 'MISS',\n },\n })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport matter from 'gray-matter'\n\nexport interface MdxFile {\n slug: string // relative path without extension, e.g. 'blog/my-post'\n filePath: string // absolute path to .mdx file\n frontmatter: Record<string, unknown>\n rawContent: string // body after frontmatter\n}\n\nexport interface MdxReaderOptions {\n contentDir: string // absolute path to content directory\n}\n\nexport class MdxReader {\n private contentDir: string\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const candidates = [\n path.join(this.contentDir, `${slug}.mdx`),\n path.join(this.contentDir, `${slug}.md`),\n path.join(this.contentDir, slug, 'index.mdx'),\n path.join(this.contentDir, slug, 'index.md'),\n ]\n\n for (const filePath of candidates) {\n if (fs.existsSync(filePath)) {\n return this.parseFile(slug, filePath)\n }\n }\n\n return null\n }\n\n /** Read all MDX files recursively. */\n readAll(): MdxFile[] {\n if (!fs.existsSync(this.contentDir)) return []\n return this.walkDir(this.contentDir, this.contentDir)\n }\n\n private walkDir(dir: string, root: string): MdxFile[] {\n const results: MdxFile[] = []\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n results.push(...this.walkDir(fullPath, root))\n } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {\n const relative = path.relative(root, fullPath)\n const slug = relative.replace(/\\.(mdx|md)$/, '').replace(/\\/index$/, '')\n results.push(this.parseFile(slug, fullPath))\n }\n }\n return results\n }\n\n private parseFile(slug: string, filePath: string): MdxFile {\n const raw = fs.readFileSync(filePath, 'utf-8')\n const { data: frontmatter, content: rawContent } = matter(raw)\n return { slug, filePath, frontmatter, rawContent }\n }\n}\n","import type { MdxFile } from './mdx-reader.js'\n\n/**\n * Strips JSX from MDX content and returns clean Markdown\n * suitable for AI crawlers.\n *\n * Removes:\n * - import/export statements\n * - JSX component tags (<ComponentName ... /> and <ComponentName>...</ComponentName>)\n * - Inline expressions {variable} that aren't standard Markdown\n *\n * Preserves:\n * - All standard Markdown (headings, lists, code blocks, links, images)\n * - Frontmatter (serialized as YAML header)\n */\nexport class MarkdownRenderer {\n render(file: MdxFile): string {\n const header = this.buildFrontmatterHeader(file.frontmatter)\n const body = this.stripJsx(file.rawContent)\n return header ? `${header}\\n\\n${body}` : body\n }\n\n private buildFrontmatterHeader(fm: Record<string, unknown>): string {\n const keys = Object.keys(fm)\n if (keys.length === 0) return ''\n const lines = keys\n .filter(k => fm[k] !== undefined && fm[k] !== null)\n .map(k => `${k}: ${this.yamlValue(fm[k])}`)\n return `---\\n${lines.join('\\n')}\\n---`\n }\n\n private yamlValue(val: unknown): string {\n if (typeof val === 'string') {\n // Quote strings containing special YAML chars\n return /[:#\\[\\]{},&*?|<>=!%@`]/.test(val) ? `\"${val.replace(/\"/g, '\\\\\"')}\"` : val\n }\n if (val instanceof Date) return val.toISOString()\n if (Array.isArray(val)) return `[${val.map(v => this.yamlValue(v)).join(', ')}]`\n return String(val)\n }\n\n private stripJsx(content: string): string {\n let out = content\n\n // Remove import statements: import Foo from '...' / import { Foo } from '...'\n out = out.replace(/^import\\s+.*?['\"].*?['\"]\\s*\\n?/gm, '')\n\n // Remove export statements at line start (export const, export default, export { })\n out = out.replace(/^export\\s+(?:default\\s+)?(?:const|let|var|function|class)\\s+[\\s\\S]*?(?=\\n(?=[^{]|\\n)|\\n{2,})/gm, '')\n out = out.replace(/^export\\s*\\{[^}]*\\}\\s*(?:from\\s+['\"][^'\"]*['\"])?\\s*\\n?/gm, '')\n\n // Remove self-closing JSX tags: <Component ... />\n // Must not match HTML img/br/hr which are valid Markdown\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*\\/>/g, '')\n\n // Remove JSX block tags: <Component ...>...</Component>\n // Greedy but bounded by matching closing tag\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*>[\\s\\S]*?<\\/\\1>/g, '')\n\n // Remove JSX expression blocks { expression } that span a whole line\n out = out.replace(/^\\s*\\{[^}]+\\}\\s*\\n/gm, '')\n\n // Collapse multiple blank lines to two\n out = out.replace(/\\n{3,}/g, '\\n\\n')\n\n return out.trim()\n }\n}\n","import fs from 'fs'\nimport path from 'path'\nimport crypto from 'crypto'\n\ninterface CacheEntry {\n content: string\n etag: string\n cachedAt: number\n ttl: number\n}\n\n/**\n * Two-tier cache:\n * 1. In-memory LRU (per Node.js process, instant)\n * 2. File-system cache in data/ta-cache/ (survives restarts)\n */\nexport class CacheManager {\n private memCache = new Map<string, CacheEntry>()\n private cacheDir: string\n private maxMemoryEntries: number\n private defaultTtl: number\n\n constructor(opts: { cacheDir: string; maxMemoryEntries?: number; ttl?: number }) {\n this.cacheDir = opts.cacheDir\n this.maxMemoryEntries = opts.maxMemoryEntries ?? 500\n this.defaultTtl = opts.ttl ?? 3600\n }\n\n get(key: string): string | null {\n // Check memory first\n const mem = this.memCache.get(key)\n if (mem && this.isValid(mem)) return mem.content\n if (mem) this.memCache.delete(key)\n\n // Check file cache\n const file = this.readFileCache(key)\n if (file && this.isValid(file)) {\n this.setMemory(key, file)\n return file.content\n }\n\n return null\n }\n\n set(key: string, content: string, etag = '', ttl = this.defaultTtl): void {\n const entry: CacheEntry = { content, etag, cachedAt: Date.now(), ttl }\n this.setMemory(key, entry)\n this.writeFileCache(key, entry)\n }\n\n /** Invalidate by key prefix — used when source .mdx file changes. */\n invalidate(keyPrefix: string): void {\n for (const k of this.memCache.keys()) {\n if (k.startsWith(keyPrefix)) this.memCache.delete(k)\n }\n const dir = this.cacheDir\n if (!fs.existsSync(dir)) return\n for (const file of fs.readdirSync(dir)) {\n if (file.startsWith(this.hashKey(keyPrefix).slice(0, 8))) {\n fs.unlinkSync(path.join(dir, file))\n }\n }\n }\n\n stats(): { memEntries: number; fsEntries: number } {\n const fsEntries = fs.existsSync(this.cacheDir)\n ? fs.readdirSync(this.cacheDir).filter(f => f.endsWith('.json')).length\n : 0\n return { memEntries: this.memCache.size, fsEntries }\n }\n\n private isValid(entry: CacheEntry): boolean {\n return Date.now() - entry.cachedAt < entry.ttl * 1000\n }\n\n private setMemory(key: string, entry: CacheEntry): void {\n if (this.memCache.size >= this.maxMemoryEntries) {\n // Evict oldest entry\n const firstKey = this.memCache.keys().next().value\n if (firstKey) this.memCache.delete(firstKey)\n }\n this.memCache.set(key, entry)\n }\n\n private hashKey(key: string): string {\n return crypto.createHash('sha256').update(key).digest('hex')\n }\n\n private filePath(key: string): string {\n return path.join(this.cacheDir, `${this.hashKey(key)}.json`)\n }\n\n private readFileCache(key: string): CacheEntry | null {\n const fp = this.filePath(key)\n if (!fs.existsSync(fp)) return null\n try {\n return JSON.parse(fs.readFileSync(fp, 'utf-8')) as CacheEntry\n } catch {\n return null\n }\n }\n\n private writeFileCache(key: string, entry: CacheEntry): void {\n try {\n fs.mkdirSync(this.cacheDir, { recursive: true })\n fs.writeFileSync(this.filePath(key), JSON.stringify(entry), 'utf-8')\n } catch {\n // Cache writes must never throw\n }\n }\n}\n"],"mappings":"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","names":["import_path","path","fs","matter","import_fs","import_path","fs","path","crypto","path"]}
|