third-audience-mdx 1.0.6 → 1.0.8

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.
Files changed (51) hide show
  1. package/dist/dashboard/admin-store.d.mts +28 -0
  2. package/dist/dashboard/admin-store.d.ts +28 -0
  3. package/dist/dashboard/admin-store.js +191 -0
  4. package/dist/dashboard/admin-store.js.map +1 -0
  5. package/dist/dashboard/admin-store.mjs +142 -0
  6. package/dist/dashboard/admin-store.mjs.map +1 -0
  7. package/dist/dashboard/routes/llms-txt-route.js +16 -4
  8. package/dist/dashboard/routes/llms-txt-route.js.map +1 -1
  9. package/dist/dashboard/routes/llms-txt-route.mjs +16 -4
  10. package/dist/dashboard/routes/llms-txt-route.mjs.map +1 -1
  11. package/dist/dashboard/routes/markdown-route.js +20 -5
  12. package/dist/dashboard/routes/markdown-route.js.map +1 -1
  13. package/dist/dashboard/routes/markdown-route.mjs +20 -5
  14. package/dist/dashboard/routes/markdown-route.mjs.map +1 -1
  15. package/dist/dashboard/routes/okf-graph-route.js +16 -4
  16. package/dist/dashboard/routes/okf-graph-route.js.map +1 -1
  17. package/dist/dashboard/routes/okf-graph-route.mjs +16 -4
  18. package/dist/dashboard/routes/okf-graph-route.mjs.map +1 -1
  19. package/dist/dashboard/routes/okf-route.js +16 -4
  20. package/dist/dashboard/routes/okf-route.js.map +1 -1
  21. package/dist/dashboard/routes/okf-route.mjs +16 -4
  22. package/dist/dashboard/routes/okf-route.mjs.map +1 -1
  23. package/dist/dashboard/routes/sitemap-ai-route.js +16 -4
  24. package/dist/dashboard/routes/sitemap-ai-route.js.map +1 -1
  25. package/dist/dashboard/routes/sitemap-ai-route.mjs +16 -4
  26. package/dist/dashboard/routes/sitemap-ai-route.mjs.map +1 -1
  27. package/dist/dashboard/ui/components/Card.d.mts +11 -0
  28. package/dist/dashboard/ui/components/Card.d.ts +11 -0
  29. package/dist/dashboard/ui/components/Card.js +40 -0
  30. package/dist/dashboard/ui/components/Card.js.map +1 -0
  31. package/dist/dashboard/ui/components/Card.mjs +15 -0
  32. package/dist/dashboard/ui/components/Card.mjs.map +1 -0
  33. package/dist/dashboard/ui/components/HeroCard.d.mts +13 -0
  34. package/dist/dashboard/ui/components/HeroCard.d.ts +13 -0
  35. package/dist/dashboard/ui/components/HeroCard.js +41 -0
  36. package/dist/dashboard/ui/components/HeroCard.js.map +1 -0
  37. package/dist/dashboard/ui/components/HeroCard.mjs +16 -0
  38. package/dist/dashboard/ui/components/HeroCard.mjs.map +1 -0
  39. package/dist/dashboard/ui/components/VisitsChart.d.mts +13 -0
  40. package/dist/dashboard/ui/components/VisitsChart.d.ts +13 -0
  41. package/dist/dashboard/ui/components/VisitsChart.js +82 -0
  42. package/dist/dashboard/ui/components/VisitsChart.js.map +1 -0
  43. package/dist/dashboard/ui/components/VisitsChart.mjs +58 -0
  44. package/dist/dashboard/ui/components/VisitsChart.mjs.map +1 -0
  45. package/dist/index.d.mts +7 -0
  46. package/dist/index.d.ts +7 -0
  47. package/dist/index.js +3 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/index.mjs +3 -1
  50. package/dist/index.mjs.map +1 -1
  51. package/package.json +2 -2
@@ -0,0 +1,28 @@
1
+ interface AdminRecord {
2
+ passwordHash: string;
3
+ isDefaultPassword: boolean;
4
+ createdAt: string;
5
+ lastLoginAt: string | null;
6
+ apiKey?: string;
7
+ }
8
+ declare function generateDefaultPassword(): string;
9
+ declare function hashPassword(password: string): string;
10
+ declare function loadAdmin(): AdminRecord | null;
11
+ declare function saveAdmin(record: AdminRecord): void;
12
+ declare const DEFAULT_PASSWORD = "Chang3M3Now!";
13
+ declare function initAdmin(): {
14
+ password: string;
15
+ apiKey: string;
16
+ isNew: boolean;
17
+ };
18
+ declare function verifyPassword(password: string): boolean;
19
+ declare function updatePassword(newPassword: string): void;
20
+ declare function recordLogin(): void;
21
+ declare function generateApiKey(): string;
22
+ declare function getApiKey(): string | null;
23
+ declare function rotateApiKey(): string;
24
+ declare function verifyApiKey(key: string): boolean;
25
+ declare function signSession(payload: string): string;
26
+ declare function verifySession(token: string): boolean;
27
+
28
+ export { type AdminRecord, DEFAULT_PASSWORD, generateApiKey, generateDefaultPassword, getApiKey, hashPassword, initAdmin, loadAdmin, recordLogin, rotateApiKey, saveAdmin, signSession, updatePassword, verifyApiKey, verifyPassword, verifySession };
@@ -0,0 +1,28 @@
1
+ interface AdminRecord {
2
+ passwordHash: string;
3
+ isDefaultPassword: boolean;
4
+ createdAt: string;
5
+ lastLoginAt: string | null;
6
+ apiKey?: string;
7
+ }
8
+ declare function generateDefaultPassword(): string;
9
+ declare function hashPassword(password: string): string;
10
+ declare function loadAdmin(): AdminRecord | null;
11
+ declare function saveAdmin(record: AdminRecord): void;
12
+ declare const DEFAULT_PASSWORD = "Chang3M3Now!";
13
+ declare function initAdmin(): {
14
+ password: string;
15
+ apiKey: string;
16
+ isNew: boolean;
17
+ };
18
+ declare function verifyPassword(password: string): boolean;
19
+ declare function updatePassword(newPassword: string): void;
20
+ declare function recordLogin(): void;
21
+ declare function generateApiKey(): string;
22
+ declare function getApiKey(): string | null;
23
+ declare function rotateApiKey(): string;
24
+ declare function verifyApiKey(key: string): boolean;
25
+ declare function signSession(payload: string): string;
26
+ declare function verifySession(token: string): boolean;
27
+
28
+ export { type AdminRecord, DEFAULT_PASSWORD, generateApiKey, generateDefaultPassword, getApiKey, hashPassword, initAdmin, loadAdmin, recordLogin, rotateApiKey, saveAdmin, signSession, updatePassword, verifyApiKey, verifyPassword, verifySession };
@@ -0,0 +1,191 @@
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/admin-store.ts
31
+ var admin_store_exports = {};
32
+ __export(admin_store_exports, {
33
+ DEFAULT_PASSWORD: () => DEFAULT_PASSWORD,
34
+ generateApiKey: () => generateApiKey,
35
+ generateDefaultPassword: () => generateDefaultPassword,
36
+ getApiKey: () => getApiKey,
37
+ hashPassword: () => hashPassword,
38
+ initAdmin: () => initAdmin,
39
+ loadAdmin: () => loadAdmin,
40
+ recordLogin: () => recordLogin,
41
+ rotateApiKey: () => rotateApiKey,
42
+ saveAdmin: () => saveAdmin,
43
+ signSession: () => signSession,
44
+ updatePassword: () => updatePassword,
45
+ verifyApiKey: () => verifyApiKey,
46
+ verifyPassword: () => verifyPassword,
47
+ verifySession: () => verifySession
48
+ });
49
+ module.exports = __toCommonJS(admin_store_exports);
50
+ var import_fs = __toESM(require("fs"));
51
+ var import_path = __toESM(require("path"));
52
+ var import_crypto = __toESM(require("crypto"));
53
+ function adminFilePath() {
54
+ const dataDir = process.env.TA_DATA_DIR ?? "data";
55
+ return import_path.default.join(process.cwd(), dataDir, "ta-admin.json");
56
+ }
57
+ function generateDefaultPassword() {
58
+ return import_crypto.default.randomBytes(6).toString("hex");
59
+ }
60
+ function hashPassword(password) {
61
+ const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt";
62
+ return import_crypto.default.createHash("sha256").update(secret + password).digest("hex");
63
+ }
64
+ function loadAdmin() {
65
+ const filePath = adminFilePath();
66
+ if (!import_fs.default.existsSync(filePath)) return null;
67
+ try {
68
+ return JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+ function saveAdmin(record) {
74
+ const filePath = adminFilePath();
75
+ const dir = import_path.default.dirname(filePath);
76
+ if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
77
+ import_fs.default.writeFileSync(filePath, JSON.stringify(record, null, 2), "utf-8");
78
+ }
79
+ var DEFAULT_PASSWORD = "Chang3M3Now!";
80
+ function initAdmin() {
81
+ const existing = loadAdmin();
82
+ if (existing) return { password: "", apiKey: "", isNew: false };
83
+ const apiKey = generateApiKey();
84
+ saveAdmin({
85
+ passwordHash: hashPassword(DEFAULT_PASSWORD),
86
+ isDefaultPassword: true,
87
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
88
+ lastLoginAt: null,
89
+ apiKey: encryptApiKey(apiKey)
90
+ });
91
+ return { password: DEFAULT_PASSWORD, apiKey, isNew: true };
92
+ }
93
+ function verifyPassword(password) {
94
+ const record = loadAdmin();
95
+ if (!record) return false;
96
+ return record.passwordHash === hashPassword(password);
97
+ }
98
+ function updatePassword(newPassword) {
99
+ const record = loadAdmin();
100
+ if (!record) return;
101
+ saveAdmin({
102
+ ...record,
103
+ passwordHash: hashPassword(newPassword),
104
+ isDefaultPassword: false
105
+ });
106
+ }
107
+ function recordLogin() {
108
+ const record = loadAdmin();
109
+ if (!record) return;
110
+ saveAdmin({ ...record, lastLoginAt: (/* @__PURE__ */ new Date()).toISOString() });
111
+ }
112
+ var CIPHER = "aes-256-gcm";
113
+ function getEncryptionKey() {
114
+ const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-fallback-key-change-me";
115
+ return import_crypto.default.createHash("sha256").update(secret).digest();
116
+ }
117
+ function encryptApiKey(plaintext) {
118
+ const iv = import_crypto.default.randomBytes(12);
119
+ const key = getEncryptionKey();
120
+ const cipher = import_crypto.default.createCipheriv(CIPHER, key, iv);
121
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
122
+ const tag = cipher.getAuthTag();
123
+ return iv.toString("hex") + tag.toString("hex") + encrypted.toString("hex");
124
+ }
125
+ function decryptApiKey(encoded) {
126
+ try {
127
+ const iv = Buffer.from(encoded.slice(0, 24), "hex");
128
+ const tag = Buffer.from(encoded.slice(24, 56), "hex");
129
+ const encrypted = Buffer.from(encoded.slice(56), "hex");
130
+ const key = getEncryptionKey();
131
+ const decipher = import_crypto.default.createDecipheriv(CIPHER, key, iv);
132
+ decipher.setAuthTag(tag);
133
+ return decipher.update(encrypted) + decipher.final("utf8");
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+ function generateApiKey() {
139
+ return "ta_" + import_crypto.default.randomBytes(24).toString("hex");
140
+ }
141
+ function getApiKey() {
142
+ const record = loadAdmin();
143
+ if (!record?.apiKey) return null;
144
+ return decryptApiKey(record.apiKey);
145
+ }
146
+ function rotateApiKey() {
147
+ const record = loadAdmin();
148
+ if (!record) throw new Error("Admin store not initialised");
149
+ const newKey = generateApiKey();
150
+ saveAdmin({ ...record, apiKey: encryptApiKey(newKey) });
151
+ return newKey;
152
+ }
153
+ function verifyApiKey(key) {
154
+ const stored = getApiKey();
155
+ if (!stored) return false;
156
+ if (key.length !== stored.length) return false;
157
+ return import_crypto.default.timingSafeEqual(Buffer.from(key), Buffer.from(stored));
158
+ }
159
+ function signSession(payload) {
160
+ const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt";
161
+ const sig = import_crypto.default.createHmac("sha256", secret).update(payload).digest("hex");
162
+ return `${payload}.${sig}`;
163
+ }
164
+ function verifySession(token) {
165
+ const lastDot = token.lastIndexOf(".");
166
+ if (lastDot === -1) return false;
167
+ const payload = token.slice(0, lastDot);
168
+ const sig = token.slice(lastDot + 1);
169
+ const expected = import_crypto.default.createHmac("sha256", process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt").update(payload).digest("hex");
170
+ if (sig.length !== expected.length) return false;
171
+ return import_crypto.default.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
172
+ }
173
+ // Annotate the CommonJS export names for ESM import in node:
174
+ 0 && (module.exports = {
175
+ DEFAULT_PASSWORD,
176
+ generateApiKey,
177
+ generateDefaultPassword,
178
+ getApiKey,
179
+ hashPassword,
180
+ initAdmin,
181
+ loadAdmin,
182
+ recordLogin,
183
+ rotateApiKey,
184
+ saveAdmin,
185
+ signSession,
186
+ updatePassword,
187
+ verifyApiKey,
188
+ verifyPassword,
189
+ verifySession
190
+ });
191
+ //# sourceMappingURL=admin-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/dashboard/admin-store.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"],"mappings":"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","names":["path","crypto","fs"]}
@@ -0,0 +1,142 @@
1
+ // src/dashboard/admin-store.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import crypto from "crypto";
5
+ function adminFilePath() {
6
+ const dataDir = process.env.TA_DATA_DIR ?? "data";
7
+ return path.join(process.cwd(), dataDir, "ta-admin.json");
8
+ }
9
+ function generateDefaultPassword() {
10
+ return crypto.randomBytes(6).toString("hex");
11
+ }
12
+ function hashPassword(password) {
13
+ const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt";
14
+ return crypto.createHash("sha256").update(secret + password).digest("hex");
15
+ }
16
+ function loadAdmin() {
17
+ const filePath = adminFilePath();
18
+ if (!fs.existsSync(filePath)) return null;
19
+ try {
20
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ function saveAdmin(record) {
26
+ const filePath = adminFilePath();
27
+ const dir = path.dirname(filePath);
28
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
29
+ fs.writeFileSync(filePath, JSON.stringify(record, null, 2), "utf-8");
30
+ }
31
+ var DEFAULT_PASSWORD = "Chang3M3Now!";
32
+ function initAdmin() {
33
+ const existing = loadAdmin();
34
+ if (existing) return { password: "", apiKey: "", isNew: false };
35
+ const apiKey = generateApiKey();
36
+ saveAdmin({
37
+ passwordHash: hashPassword(DEFAULT_PASSWORD),
38
+ isDefaultPassword: true,
39
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
40
+ lastLoginAt: null,
41
+ apiKey: encryptApiKey(apiKey)
42
+ });
43
+ return { password: DEFAULT_PASSWORD, apiKey, isNew: true };
44
+ }
45
+ function verifyPassword(password) {
46
+ const record = loadAdmin();
47
+ if (!record) return false;
48
+ return record.passwordHash === hashPassword(password);
49
+ }
50
+ function updatePassword(newPassword) {
51
+ const record = loadAdmin();
52
+ if (!record) return;
53
+ saveAdmin({
54
+ ...record,
55
+ passwordHash: hashPassword(newPassword),
56
+ isDefaultPassword: false
57
+ });
58
+ }
59
+ function recordLogin() {
60
+ const record = loadAdmin();
61
+ if (!record) return;
62
+ saveAdmin({ ...record, lastLoginAt: (/* @__PURE__ */ new Date()).toISOString() });
63
+ }
64
+ var CIPHER = "aes-256-gcm";
65
+ function getEncryptionKey() {
66
+ const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-fallback-key-change-me";
67
+ return crypto.createHash("sha256").update(secret).digest();
68
+ }
69
+ function encryptApiKey(plaintext) {
70
+ const iv = crypto.randomBytes(12);
71
+ const key = getEncryptionKey();
72
+ const cipher = crypto.createCipheriv(CIPHER, key, iv);
73
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
74
+ const tag = cipher.getAuthTag();
75
+ return iv.toString("hex") + tag.toString("hex") + encrypted.toString("hex");
76
+ }
77
+ function decryptApiKey(encoded) {
78
+ try {
79
+ const iv = Buffer.from(encoded.slice(0, 24), "hex");
80
+ const tag = Buffer.from(encoded.slice(24, 56), "hex");
81
+ const encrypted = Buffer.from(encoded.slice(56), "hex");
82
+ const key = getEncryptionKey();
83
+ const decipher = crypto.createDecipheriv(CIPHER, key, iv);
84
+ decipher.setAuthTag(tag);
85
+ return decipher.update(encrypted) + decipher.final("utf8");
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+ function generateApiKey() {
91
+ return "ta_" + crypto.randomBytes(24).toString("hex");
92
+ }
93
+ function getApiKey() {
94
+ const record = loadAdmin();
95
+ if (!record?.apiKey) return null;
96
+ return decryptApiKey(record.apiKey);
97
+ }
98
+ function rotateApiKey() {
99
+ const record = loadAdmin();
100
+ if (!record) throw new Error("Admin store not initialised");
101
+ const newKey = generateApiKey();
102
+ saveAdmin({ ...record, apiKey: encryptApiKey(newKey) });
103
+ return newKey;
104
+ }
105
+ function verifyApiKey(key) {
106
+ const stored = getApiKey();
107
+ if (!stored) return false;
108
+ if (key.length !== stored.length) return false;
109
+ return crypto.timingSafeEqual(Buffer.from(key), Buffer.from(stored));
110
+ }
111
+ function signSession(payload) {
112
+ const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt";
113
+ const sig = crypto.createHmac("sha256", secret).update(payload).digest("hex");
114
+ return `${payload}.${sig}`;
115
+ }
116
+ function verifySession(token) {
117
+ const lastDot = token.lastIndexOf(".");
118
+ if (lastDot === -1) return false;
119
+ const payload = token.slice(0, lastDot);
120
+ const sig = token.slice(lastDot + 1);
121
+ const expected = crypto.createHmac("sha256", process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt").update(payload).digest("hex");
122
+ if (sig.length !== expected.length) return false;
123
+ return crypto.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
124
+ }
125
+ export {
126
+ DEFAULT_PASSWORD,
127
+ generateApiKey,
128
+ generateDefaultPassword,
129
+ getApiKey,
130
+ hashPassword,
131
+ initAdmin,
132
+ loadAdmin,
133
+ recordLogin,
134
+ rotateApiKey,
135
+ saveAdmin,
136
+ signSession,
137
+ updatePassword,
138
+ verifyApiKey,
139
+ verifyPassword,
140
+ verifySession
141
+ };
142
+ //# sourceMappingURL=admin-store.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/dashboard/admin-store.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"],"mappings":"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","names":[]}
@@ -43,14 +43,26 @@ var import_gray_matter = __toESM(require("gray-matter"));
43
43
  var MdxReader = class {
44
44
  constructor(options) {
45
45
  this.contentDir = options.contentDir;
46
+ this.stripSegments = options.stripSegments ?? [];
47
+ }
48
+ /**
49
+ * Remove configured URL-only segments from a slug so it maps to the file
50
+ * layout. e.g. stripSegments ['learn'] turns 'en/learn/hydroponics/x' into
51
+ * 'en/hydroponics/x'. Only whole path segments are removed.
52
+ */
53
+ applyStrip(slug) {
54
+ if (this.stripSegments.length === 0) return slug;
55
+ const drop = new Set(this.stripSegments);
56
+ return slug.split("/").filter((seg) => !drop.has(seg)).join("/");
46
57
  }
47
58
  /** Read a single MDX file by slug. Returns null if not found. */
48
59
  read(slug) {
60
+ const resolved = this.applyStrip(slug);
49
61
  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")
62
+ import_path.default.join(this.contentDir, `${resolved}.mdx`),
63
+ import_path.default.join(this.contentDir, `${resolved}.md`),
64
+ import_path.default.join(this.contentDir, resolved, "index.mdx"),
65
+ import_path.default.join(this.contentDir, resolved, "index.md")
54
66
  ];
55
67
  for (const filePath of candidates) {
56
68
  if (import_fs.default.existsSync(filePath)) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/dashboard/routes/llms-txt-route.ts","../../../src/core/mdx-reader.ts","../../../src/discovery/llms-txt.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateLlmsTxt } from '../../discovery/llms-txt.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/** Handler for GET /llms.txt → rewired to /api/third-audience/llms-txt */\nexport async function GET(req: NextRequest) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const files = reader.readAll()\n const content = generateLlmsTxt(files, baseUrl)\n\n return new NextResponse(content, {\n headers: { 'Content-Type': 'text/plain; charset=utf-8' },\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 '../core/mdx-reader.js'\n\n/**\n * Generates /llms.txt content from MDX frontmatter.\n * Format: one entry per line — URL, title, description.\n */\nexport function generateLlmsTxt(files: MdxFile[], baseUrl: string): string {\n const lines: string[] = [\n '# LLMs.txt — AI-readable content index',\n `# Generated by third-audience-mdx`,\n '',\n ]\n\n for (const file of files) {\n const fm = file.frontmatter\n const url = `${baseUrl.replace(/\\/$/, '')}/${file.slug}`\n const title = String(fm.title ?? file.slug)\n const desc = fm.description ? ` — ${String(fm.description)}` : ''\n lines.push(`- [${title}](${url})${desc}`)\n }\n\n return lines.join('\\n') + '\\n'\n}\n"],"mappings":"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","names":["import_path","path","fs","matter","path"]}
1
+ {"version":3,"sources":["../../../src/dashboard/routes/llms-txt-route.ts","../../../src/core/mdx-reader.ts","../../../src/discovery/llms-txt.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateLlmsTxt } from '../../discovery/llms-txt.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/** Handler for GET /llms.txt → rewired to /api/third-audience/llms-txt */\nexport async function GET(req: NextRequest) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const files = reader.readAll()\n const content = generateLlmsTxt(files, baseUrl)\n\n return new NextResponse(content, {\n headers: { 'Content-Type': 'text/plain; charset=utf-8' },\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 /** URL path segments to drop when mapping a request slug to a file. */\n stripSegments?: string[]\n}\n\nexport class MdxReader {\n private contentDir: string\n private stripSegments: string[]\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n this.stripSegments = options.stripSegments ?? []\n }\n\n /**\n * Remove configured URL-only segments from a slug so it maps to the file\n * layout. e.g. stripSegments ['learn'] turns 'en/learn/hydroponics/x' into\n * 'en/hydroponics/x'. Only whole path segments are removed.\n */\n private applyStrip(slug: string): string {\n if (this.stripSegments.length === 0) return slug\n const drop = new Set(this.stripSegments)\n return slug\n .split('/')\n .filter((seg) => !drop.has(seg))\n .join('/')\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const resolved = this.applyStrip(slug)\n const candidates = [\n path.join(this.contentDir, `${resolved}.mdx`),\n path.join(this.contentDir, `${resolved}.md`),\n path.join(this.contentDir, resolved, 'index.mdx'),\n path.join(this.contentDir, resolved, '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 '../core/mdx-reader.js'\n\n/**\n * Generates /llms.txt content from MDX frontmatter.\n * Format: one entry per line — URL, title, description.\n */\nexport function generateLlmsTxt(files: MdxFile[], baseUrl: string): string {\n const lines: string[] = [\n '# LLMs.txt — AI-readable content index',\n `# Generated by third-audience-mdx`,\n '',\n ]\n\n for (const file of files) {\n const fm = file.frontmatter\n const url = `${baseUrl.replace(/\\/$/, '')}/${file.slug}`\n const title = String(fm.title ?? file.slug)\n const desc = fm.description ? ` — ${String(fm.description)}` : ''\n lines.push(`- [${title}](${url})${desc}`)\n }\n\n return lines.join('\\n') + '\\n'\n}\n"],"mappings":"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","names":["import_path","path","fs","matter","path"]}
@@ -9,14 +9,26 @@ import matter from "gray-matter";
9
9
  var MdxReader = class {
10
10
  constructor(options) {
11
11
  this.contentDir = options.contentDir;
12
+ this.stripSegments = options.stripSegments ?? [];
13
+ }
14
+ /**
15
+ * Remove configured URL-only segments from a slug so it maps to the file
16
+ * layout. e.g. stripSegments ['learn'] turns 'en/learn/hydroponics/x' into
17
+ * 'en/hydroponics/x'. Only whole path segments are removed.
18
+ */
19
+ applyStrip(slug) {
20
+ if (this.stripSegments.length === 0) return slug;
21
+ const drop = new Set(this.stripSegments);
22
+ return slug.split("/").filter((seg) => !drop.has(seg)).join("/");
12
23
  }
13
24
  /** Read a single MDX file by slug. Returns null if not found. */
14
25
  read(slug) {
26
+ const resolved = this.applyStrip(slug);
15
27
  const candidates = [
16
- path.join(this.contentDir, `${slug}.mdx`),
17
- path.join(this.contentDir, `${slug}.md`),
18
- path.join(this.contentDir, slug, "index.mdx"),
19
- path.join(this.contentDir, slug, "index.md")
28
+ path.join(this.contentDir, `${resolved}.mdx`),
29
+ path.join(this.contentDir, `${resolved}.md`),
30
+ path.join(this.contentDir, resolved, "index.mdx"),
31
+ path.join(this.contentDir, resolved, "index.md")
20
32
  ];
21
33
  for (const filePath of candidates) {
22
34
  if (fs.existsSync(filePath)) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/dashboard/routes/llms-txt-route.ts","../../../src/core/mdx-reader.ts","../../../src/discovery/llms-txt.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateLlmsTxt } from '../../discovery/llms-txt.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/** Handler for GET /llms.txt → rewired to /api/third-audience/llms-txt */\nexport async function GET(req: NextRequest) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const files = reader.readAll()\n const content = generateLlmsTxt(files, baseUrl)\n\n return new NextResponse(content, {\n headers: { 'Content-Type': 'text/plain; charset=utf-8' },\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 '../core/mdx-reader.js'\n\n/**\n * Generates /llms.txt content from MDX frontmatter.\n * Format: one entry per line — URL, title, description.\n */\nexport function generateLlmsTxt(files: MdxFile[], baseUrl: string): string {\n const lines: string[] = [\n '# LLMs.txt — AI-readable content index',\n `# Generated by third-audience-mdx`,\n '',\n ]\n\n for (const file of files) {\n const fm = file.frontmatter\n const url = `${baseUrl.replace(/\\/$/, '')}/${file.slug}`\n const title = String(fm.title ?? file.slug)\n const desc = fm.description ? ` — ${String(fm.description)}` : ''\n lines.push(`- [${title}](${url})${desc}`)\n }\n\n return lines.join('\\n') + '\\n'\n}\n"],"mappings":";AAAA,SAAS,oBAAsC;AAC/C,OAAOA,WAAU;;;ACDjB,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,YAAY;AAaZ,IAAM,YAAN,MAAgB;AAAA,EAGrB,YAAY,SAA2B;AACrC,SAAK,aAAa,QAAQ;AAAA,EAC5B;AAAA;AAAA,EAGA,KAAK,MAA8B;AACjC,UAAM,aAAa;AAAA,MACjB,KAAK,KAAK,KAAK,YAAY,GAAG,IAAI,MAAM;AAAA,MACxC,KAAK,KAAK,KAAK,YAAY,GAAG,IAAI,KAAK;AAAA,MACvC,KAAK,KAAK,KAAK,YAAY,MAAM,WAAW;AAAA,MAC5C,KAAK,KAAK,KAAK,YAAY,MAAM,UAAU;AAAA,IAC7C;AAEA,eAAW,YAAY,YAAY;AACjC,UAAI,GAAG,WAAW,QAAQ,GAAG;AAC3B,eAAO,KAAK,UAAU,MAAM,QAAQ;AAAA,MACtC;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,UAAqB;AACnB,QAAI,CAAC,GAAG,WAAW,KAAK,UAAU,EAAG,QAAO,CAAC;AAC7C,WAAO,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU;AAAA,EACtD;AAAA,EAEQ,QAAQ,KAAa,MAAyB;AACpD,UAAM,UAAqB,CAAC;AAC5B,eAAW,SAAS,GAAG,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC,GAAG;AAChE,YAAM,WAAW,KAAK,KAAK,KAAK,MAAM,IAAI;AAC1C,UAAI,MAAM,YAAY,GAAG;AACvB,gBAAQ,KAAK,GAAG,KAAK,QAAQ,UAAU,IAAI,CAAC;AAAA,MAC9C,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK,MAAM,KAAK,SAAS,KAAK,GAAG;AACpE,cAAM,WAAW,KAAK,SAAS,MAAM,QAAQ;AAC7C,cAAM,OAAO,SAAS,QAAQ,eAAe,EAAE,EAAE,QAAQ,YAAY,EAAE;AACvE,gBAAQ,KAAK,KAAK,UAAU,MAAM,QAAQ,CAAC;AAAA,MAC7C;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,UAAU,MAAc,UAA2B;AACzD,UAAM,MAAM,GAAG,aAAa,UAAU,OAAO;AAC7C,UAAM,EAAE,MAAM,aAAa,SAAS,WAAW,IAAI,OAAO,GAAG;AAC7D,WAAO,EAAE,MAAM,UAAU,aAAa,WAAW;AAAA,EACnD;AACF;;;AC5DO,SAAS,gBAAgB,OAAkB,SAAyB;AACzE,QAAM,QAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,aAAW,QAAQ,OAAO;AACxB,UAAM,KAAK,KAAK;AAChB,UAAM,MAAM,GAAG,QAAQ,QAAQ,OAAO,EAAE,CAAC,IAAI,KAAK,IAAI;AACtD,UAAM,QAAQ,OAAO,GAAG,SAAS,KAAK,IAAI;AAC1C,UAAM,OAAO,GAAG,cAAc,WAAM,OAAO,GAAG,WAAW,CAAC,KAAK;AAC/D,UAAM,KAAK,MAAM,KAAK,KAAK,GAAG,IAAI,IAAI,EAAE;AAAA,EAC1C;AAEA,SAAO,MAAM,KAAK,IAAI,IAAI;AAC5B;;;AFjBA,IAAM,SAAS,IAAI,UAAU,EAAE,YAAYC,MAAK,KAAK,QAAQ,IAAI,GAAG,QAAQ,IAAI,kBAAkB,SAAS,EAAE,CAAC;AAG9G,eAAsB,IAAI,KAAkB;AAC1C,QAAM,UAAU,QAAQ,IAAI,wBACvB,GAAG,IAAI,QAAQ,QAAQ,KAAK,IAAI,QAAQ,IAAI;AAEjD,QAAM,QAAQ,OAAO,QAAQ;AAC7B,QAAM,UAAU,gBAAgB,OAAO,OAAO;AAE9C,SAAO,IAAI,aAAa,SAAS;AAAA,IAC/B,SAAS,EAAE,gBAAgB,4BAA4B;AAAA,EACzD,CAAC;AACH;","names":["path","path"]}
1
+ {"version":3,"sources":["../../../src/dashboard/routes/llms-txt-route.ts","../../../src/core/mdx-reader.ts","../../../src/discovery/llms-txt.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateLlmsTxt } from '../../discovery/llms-txt.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/** Handler for GET /llms.txt → rewired to /api/third-audience/llms-txt */\nexport async function GET(req: NextRequest) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const files = reader.readAll()\n const content = generateLlmsTxt(files, baseUrl)\n\n return new NextResponse(content, {\n headers: { 'Content-Type': 'text/plain; charset=utf-8' },\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 /** URL path segments to drop when mapping a request slug to a file. */\n stripSegments?: string[]\n}\n\nexport class MdxReader {\n private contentDir: string\n private stripSegments: string[]\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n this.stripSegments = options.stripSegments ?? []\n }\n\n /**\n * Remove configured URL-only segments from a slug so it maps to the file\n * layout. e.g. stripSegments ['learn'] turns 'en/learn/hydroponics/x' into\n * 'en/hydroponics/x'. Only whole path segments are removed.\n */\n private applyStrip(slug: string): string {\n if (this.stripSegments.length === 0) return slug\n const drop = new Set(this.stripSegments)\n return slug\n .split('/')\n .filter((seg) => !drop.has(seg))\n .join('/')\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const resolved = this.applyStrip(slug)\n const candidates = [\n path.join(this.contentDir, `${resolved}.mdx`),\n path.join(this.contentDir, `${resolved}.md`),\n path.join(this.contentDir, resolved, 'index.mdx'),\n path.join(this.contentDir, resolved, '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 '../core/mdx-reader.js'\n\n/**\n * Generates /llms.txt content from MDX frontmatter.\n * Format: one entry per line — URL, title, description.\n */\nexport function generateLlmsTxt(files: MdxFile[], baseUrl: string): string {\n const lines: string[] = [\n '# LLMs.txt — AI-readable content index',\n `# Generated by third-audience-mdx`,\n '',\n ]\n\n for (const file of files) {\n const fm = file.frontmatter\n const url = `${baseUrl.replace(/\\/$/, '')}/${file.slug}`\n const title = String(fm.title ?? file.slug)\n const desc = fm.description ? ` — ${String(fm.description)}` : ''\n lines.push(`- [${title}](${url})${desc}`)\n }\n\n return lines.join('\\n') + '\\n'\n}\n"],"mappings":"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","names":["path","path"]}
@@ -43,14 +43,26 @@ var import_gray_matter = __toESM(require("gray-matter"));
43
43
  var MdxReader = class {
44
44
  constructor(options) {
45
45
  this.contentDir = options.contentDir;
46
+ this.stripSegments = options.stripSegments ?? [];
47
+ }
48
+ /**
49
+ * Remove configured URL-only segments from a slug so it maps to the file
50
+ * layout. e.g. stripSegments ['learn'] turns 'en/learn/hydroponics/x' into
51
+ * 'en/hydroponics/x'. Only whole path segments are removed.
52
+ */
53
+ applyStrip(slug) {
54
+ if (this.stripSegments.length === 0) return slug;
55
+ const drop = new Set(this.stripSegments);
56
+ return slug.split("/").filter((seg) => !drop.has(seg)).join("/");
46
57
  }
47
58
  /** Read a single MDX file by slug. Returns null if not found. */
48
59
  read(slug) {
60
+ const resolved = this.applyStrip(slug);
49
61
  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")
62
+ import_path.default.join(this.contentDir, `${resolved}.mdx`),
63
+ import_path.default.join(this.contentDir, `${resolved}.md`),
64
+ import_path.default.join(this.contentDir, resolved, "index.mdx"),
65
+ import_path.default.join(this.contentDir, resolved, "index.md")
54
66
  ];
55
67
  for (const filePath of candidates) {
56
68
  if (import_fs.default.existsSync(filePath)) {
@@ -368,7 +380,10 @@ _VisitTracker.instance = null;
368
380
  var VisitTracker = _VisitTracker;
369
381
 
370
382
  // src/dashboard/routes/markdown-route.ts
371
- var reader = new MdxReader({ contentDir: import_path4.default.join(process.cwd(), process.env.TA_CONTENT_DIR ?? "content") });
383
+ var reader = new MdxReader({
384
+ contentDir: import_path4.default.join(process.cwd(), process.env.TA_CONTENT_DIR ?? "content"),
385
+ stripSegments: (process.env.TA_STRIP_SEGMENTS ?? "").split(",").map((s) => s.trim()).filter(Boolean)
386
+ });
372
387
  var renderer = new MarkdownRenderer();
373
388
  var cache = new CacheManager({
374
389
  cacheDir: import_path4.default.join(process.cwd(), process.env.TA_DATA_DIR ?? "data", "ta-cache")