third-audience-mdx 1.0.2 → 1.0.4
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/dist/dashboard/routes/bots-config-route.d.mts +8 -0
- package/dist/dashboard/routes/bots-config-route.d.ts +8 -0
- package/dist/dashboard/routes/bots-config-route.js +149 -0
- package/dist/dashboard/routes/bots-config-route.js.map +1 -0
- package/dist/dashboard/routes/bots-config-route.mjs +113 -0
- package/dist/dashboard/routes/bots-config-route.mjs.map +1 -0
- package/dist/dashboard/routes/okf-graph-route.d.mts +6 -0
- package/dist/dashboard/routes/okf-graph-route.d.ts +6 -0
- package/dist/dashboard/routes/okf-graph-route.js +266 -0
- package/dist/dashboard/routes/okf-graph-route.js.map +1 -0
- package/dist/dashboard/routes/okf-graph-route.mjs +231 -0
- package/dist/dashboard/routes/okf-graph-route.mjs.map +1 -0
- package/dist/dashboard/routes/okf-route.js +1 -1
- package/dist/dashboard/routes/okf-route.js.map +1 -1
- package/dist/dashboard/routes/okf-route.mjs +1 -1
- package/dist/dashboard/routes/okf-route.mjs.map +1 -1
- package/dist/dashboard/ui/components/Sidebar.js +15 -0
- package/dist/dashboard/ui/components/Sidebar.js.map +1 -1
- package/dist/dashboard/ui/components/Sidebar.mjs +15 -0
- package/dist/dashboard/ui/components/Sidebar.mjs.map +1 -1
- package/dist/dashboard/ui/pages/OkfPage.d.mts +5 -0
- package/dist/dashboard/ui/pages/OkfPage.d.ts +5 -0
- package/dist/dashboard/ui/pages/OkfPage.js +438 -0
- package/dist/dashboard/ui/pages/OkfPage.js.map +1 -0
- package/dist/dashboard/ui/pages/OkfPage.mjs +414 -0
- package/dist/dashboard/ui/pages/OkfPage.mjs.map +1 -0
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
/** GET /api/third-audience/bots-config — returns current bots configuration */
|
|
4
|
+
declare function GET(req: NextRequest): Promise<NextResponse>;
|
|
5
|
+
/** POST /api/third-audience/bots-config — saves bots configuration */
|
|
6
|
+
declare function POST(req: NextRequest): Promise<NextResponse>;
|
|
7
|
+
|
|
8
|
+
export { GET, POST };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
/** GET /api/third-audience/bots-config — returns current bots configuration */
|
|
4
|
+
declare function GET(req: NextRequest): Promise<NextResponse>;
|
|
5
|
+
/** POST /api/third-audience/bots-config — saves bots configuration */
|
|
6
|
+
declare function POST(req: NextRequest): Promise<NextResponse>;
|
|
7
|
+
|
|
8
|
+
export { GET, POST };
|
|
@@ -0,0 +1,149 @@
|
|
|
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/bots-config-route.ts
|
|
31
|
+
var bots_config_route_exports = {};
|
|
32
|
+
__export(bots_config_route_exports, {
|
|
33
|
+
GET: () => GET,
|
|
34
|
+
POST: () => POST
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(bots_config_route_exports);
|
|
37
|
+
var import_fs2 = __toESM(require("fs"));
|
|
38
|
+
var import_path2 = __toESM(require("path"));
|
|
39
|
+
var import_server2 = require("next/server");
|
|
40
|
+
|
|
41
|
+
// src/dashboard/auth.ts
|
|
42
|
+
var import_server = require("next/server");
|
|
43
|
+
|
|
44
|
+
// src/dashboard/admin-store.ts
|
|
45
|
+
var import_fs = __toESM(require("fs"));
|
|
46
|
+
var import_path = __toESM(require("path"));
|
|
47
|
+
var import_crypto = __toESM(require("crypto"));
|
|
48
|
+
function adminFilePath() {
|
|
49
|
+
const dataDir = process.env.TA_DATA_DIR ?? "data";
|
|
50
|
+
return import_path.default.join(process.cwd(), dataDir, "ta-admin.json");
|
|
51
|
+
}
|
|
52
|
+
function loadAdmin() {
|
|
53
|
+
const filePath = adminFilePath();
|
|
54
|
+
if (!import_fs.default.existsSync(filePath)) return null;
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
var CIPHER = "aes-256-gcm";
|
|
62
|
+
function getEncryptionKey() {
|
|
63
|
+
const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-fallback-key-change-me";
|
|
64
|
+
return import_crypto.default.createHash("sha256").update(secret).digest();
|
|
65
|
+
}
|
|
66
|
+
function decryptApiKey(encoded) {
|
|
67
|
+
try {
|
|
68
|
+
const iv = Buffer.from(encoded.slice(0, 24), "hex");
|
|
69
|
+
const tag = Buffer.from(encoded.slice(24, 56), "hex");
|
|
70
|
+
const encrypted = Buffer.from(encoded.slice(56), "hex");
|
|
71
|
+
const key = getEncryptionKey();
|
|
72
|
+
const decipher = import_crypto.default.createDecipheriv(CIPHER, key, iv);
|
|
73
|
+
decipher.setAuthTag(tag);
|
|
74
|
+
return decipher.update(encrypted) + decipher.final("utf8");
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function getApiKey() {
|
|
80
|
+
const record = loadAdmin();
|
|
81
|
+
if (!record?.apiKey) return null;
|
|
82
|
+
return decryptApiKey(record.apiKey);
|
|
83
|
+
}
|
|
84
|
+
function verifyApiKey(key) {
|
|
85
|
+
const stored = getApiKey();
|
|
86
|
+
if (!stored) return false;
|
|
87
|
+
if (key.length !== stored.length) return false;
|
|
88
|
+
return import_crypto.default.timingSafeEqual(Buffer.from(key), Buffer.from(stored));
|
|
89
|
+
}
|
|
90
|
+
function verifySession(token) {
|
|
91
|
+
const lastDot = token.lastIndexOf(".");
|
|
92
|
+
if (lastDot === -1) return false;
|
|
93
|
+
const payload = token.slice(0, lastDot);
|
|
94
|
+
const sig = token.slice(lastDot + 1);
|
|
95
|
+
const expected = import_crypto.default.createHmac("sha256", process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt").update(payload).digest("hex");
|
|
96
|
+
if (sig.length !== expected.length) return false;
|
|
97
|
+
return import_crypto.default.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/dashboard/auth.ts
|
|
101
|
+
var SESSION_COOKIE = "ta_session";
|
|
102
|
+
function checkApiAuth(req) {
|
|
103
|
+
const apiKeyHeader = req.headers.get("x-ta-api-key");
|
|
104
|
+
if (apiKeyHeader) return verifyApiKey(apiKeyHeader);
|
|
105
|
+
const auth = req.headers.get("authorization") ?? "";
|
|
106
|
+
if (auth.startsWith("Bearer ")) {
|
|
107
|
+
const token = auth.slice(7);
|
|
108
|
+
return verifyApiKey(token);
|
|
109
|
+
}
|
|
110
|
+
const session = req.cookies.get(SESSION_COOKIE)?.value;
|
|
111
|
+
if (session) return verifySession(session);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
function unauthorizedResponse() {
|
|
115
|
+
return import_server.NextResponse.json(
|
|
116
|
+
{ error: "Unauthorized. Provide X-TA-Api-Key header or a valid session cookie." },
|
|
117
|
+
{
|
|
118
|
+
status: 401,
|
|
119
|
+
headers: { "WWW-Authenticate": 'Bearer realm="Third Audience API"' }
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/dashboard/routes/bots-config-route.ts
|
|
125
|
+
function botsConfigPath() {
|
|
126
|
+
return import_path2.default.join(process.cwd(), process.env.TA_DATA_DIR ?? "data", "ta-bots-config.json");
|
|
127
|
+
}
|
|
128
|
+
async function GET(req) {
|
|
129
|
+
if (!checkApiAuth(req)) return unauthorizedResponse();
|
|
130
|
+
const filePath = botsConfigPath();
|
|
131
|
+
if (!import_fs2.default.existsSync(filePath)) {
|
|
132
|
+
return import_server2.NextResponse.json({ allowlist: [], blocklist: [], track_unknown: true });
|
|
133
|
+
}
|
|
134
|
+
return import_server2.NextResponse.json(JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8")));
|
|
135
|
+
}
|
|
136
|
+
async function POST(req) {
|
|
137
|
+
if (!checkApiAuth(req)) return unauthorizedResponse();
|
|
138
|
+
const body = await req.json();
|
|
139
|
+
const filePath = botsConfigPath();
|
|
140
|
+
import_fs2.default.mkdirSync(import_path2.default.dirname(filePath), { recursive: true });
|
|
141
|
+
import_fs2.default.writeFileSync(filePath, JSON.stringify(body, null, 2));
|
|
142
|
+
return new import_server2.NextResponse(null, { status: 204 });
|
|
143
|
+
}
|
|
144
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
145
|
+
0 && (module.exports = {
|
|
146
|
+
GET,
|
|
147
|
+
POST
|
|
148
|
+
});
|
|
149
|
+
//# sourceMappingURL=bots-config-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/routes/bots-config-route.ts","../../../src/dashboard/auth.ts","../../../src/dashboard/admin-store.ts"],"sourcesContent":["import fs from 'fs'\nimport path from 'path'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { checkApiAuth, unauthorizedResponse } from '../auth.js'\n\nfunction botsConfigPath(): string {\n return path.join(process.cwd(), process.env.TA_DATA_DIR ?? 'data', 'ta-bots-config.json')\n}\n\n/** GET /api/third-audience/bots-config — returns current bots configuration */\nexport async function GET(req: NextRequest): Promise<NextResponse> {\n if (!checkApiAuth(req)) return unauthorizedResponse()\n\n const filePath = botsConfigPath()\n if (!fs.existsSync(filePath)) {\n return NextResponse.json({ allowlist: [], blocklist: [], track_unknown: true })\n }\n\n return NextResponse.json(JSON.parse(fs.readFileSync(filePath, 'utf-8')))\n}\n\n/** POST /api/third-audience/bots-config — saves bots configuration */\nexport async function POST(req: NextRequest): Promise<NextResponse> {\n if (!checkApiAuth(req)) return unauthorizedResponse()\n\n const body = await req.json()\n const filePath = botsConfigPath()\n fs.mkdirSync(path.dirname(filePath), { recursive: true })\n fs.writeFileSync(filePath, JSON.stringify(body, null, 2))\n\n return new NextResponse(null, { status: 204 })\n}\n","import type { NextRequest } from 'next/server'\nimport { NextResponse } from 'next/server'\nimport { verifySession, verifyApiKey } from './admin-store.js'\n\nconst SESSION_COOKIE = 'ta_session'\n\n/**\n * Authenticate an API route request. Accepts (in order):\n * 1. X-TA-Api-Key header — for headless/external callers (mirrors WP's approach)\n * 2. Authorization: Bearer <api-key> — same key, different transport\n * 3. Valid ta_session cookie — browser dashboard session\n */\nexport function checkApiAuth(req: NextRequest): boolean {\n // 1. X-TA-Api-Key header (WP-style headless key)\n const apiKeyHeader = req.headers.get('x-ta-api-key')\n if (apiKeyHeader) return verifyApiKey(apiKeyHeader)\n\n // 2. Bearer token (treat as api key)\n const auth = req.headers.get('authorization') ?? ''\n if (auth.startsWith('Bearer ')) {\n const token = auth.slice(7)\n return verifyApiKey(token)\n }\n\n // 3. Browser session cookie\n const session = req.cookies.get(SESSION_COOKIE)?.value\n if (session) return verifySession(session)\n\n return false\n}\n\n/**\n * Returns a 401 JSON response with the correct WWW-Authenticate header.\n * Use as: if (!checkApiAuth(req)) return unauthorizedResponse()\n */\nexport function unauthorizedResponse(): NextResponse {\n return NextResponse.json(\n { error: 'Unauthorized. Provide X-TA-Api-Key header or a valid session cookie.' },\n {\n status: 401,\n headers: { 'WWW-Authenticate': 'Bearer realm=\"Third Audience API\"' },\n }\n )\n}\n","import fs from 'fs'\nimport path from 'path'\nimport crypto from 'crypto'\n\nexport interface AdminRecord {\n passwordHash: string // sha256(secret + password)\n isDefaultPassword: boolean\n createdAt: string\n lastLoginAt: string | null\n apiKey?: string // AES-256-GCM encrypted, for headless/external API callers\n}\n\nfunction adminFilePath(): string {\n const dataDir = process.env.TA_DATA_DIR ?? 'data'\n return path.join(process.cwd(), dataDir, 'ta-admin.json')\n}\n\nexport function generateDefaultPassword(): string {\n return crypto.randomBytes(6).toString('hex') // 12-char hex, easy to type\n}\n\nexport function hashPassword(password: string): string {\n const secret = process.env.THIRD_AUDIENCE_SECRET ?? 'ta-salt'\n return crypto.createHash('sha256').update(secret + password).digest('hex')\n}\n\nexport function loadAdmin(): AdminRecord | null {\n const filePath = adminFilePath()\n if (!fs.existsSync(filePath)) return null\n try {\n return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as AdminRecord\n } catch {\n return null\n }\n}\n\nexport function saveAdmin(record: AdminRecord): void {\n const filePath = adminFilePath()\n const dir = path.dirname(filePath)\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })\n fs.writeFileSync(filePath, JSON.stringify(record, null, 2), 'utf-8')\n}\n\nexport const DEFAULT_PASSWORD = 'Chang3M3Now!'\n\nexport function initAdmin(): { password: string; apiKey: string; isNew: boolean } {\n const existing = loadAdmin()\n if (existing) return { password: '', apiKey: '', isNew: false }\n\n const apiKey = generateApiKey()\n saveAdmin({\n passwordHash: hashPassword(DEFAULT_PASSWORD),\n isDefaultPassword: true,\n createdAt: new Date().toISOString(),\n lastLoginAt: null,\n apiKey: encryptApiKey(apiKey),\n })\n return { password: DEFAULT_PASSWORD, apiKey, isNew: true }\n}\n\nexport function verifyPassword(password: string): boolean {\n const record = loadAdmin()\n if (!record) return false\n return record.passwordHash === hashPassword(password)\n}\n\nexport function updatePassword(newPassword: string): void {\n const record = loadAdmin()\n if (!record) return\n saveAdmin({\n ...record,\n passwordHash: hashPassword(newPassword),\n isDefaultPassword: false,\n })\n}\n\nexport function recordLogin(): void {\n const record = loadAdmin()\n if (!record) return\n saveAdmin({ ...record, lastLoginAt: new Date().toISOString() })\n}\n\n// ---------------------------------------------------------------------------\n// API key — AES-256-GCM encrypted at rest, mirroring WP's SECURE_AUTH_KEY approach\n// ---------------------------------------------------------------------------\n\nconst CIPHER = 'aes-256-gcm'\n\nfunction getEncryptionKey(): Buffer {\n const secret = process.env.THIRD_AUDIENCE_SECRET ?? 'ta-fallback-key-change-me'\n // Derive a 32-byte key from the secret using SHA-256\n return crypto.createHash('sha256').update(secret).digest()\n}\n\nfunction encryptApiKey(plaintext: string): string {\n const iv = crypto.randomBytes(12)\n const key = getEncryptionKey()\n const cipher = crypto.createCipheriv(CIPHER, key, iv) as crypto.CipherGCM\n const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])\n const tag = cipher.getAuthTag()\n // Format: iv(24 hex) + tag(32 hex) + encrypted(hex)\n return iv.toString('hex') + tag.toString('hex') + encrypted.toString('hex')\n}\n\nfunction decryptApiKey(encoded: string): string | null {\n try {\n const iv = Buffer.from(encoded.slice(0, 24), 'hex')\n const tag = Buffer.from(encoded.slice(24, 56), 'hex')\n const encrypted = Buffer.from(encoded.slice(56), 'hex')\n const key = getEncryptionKey()\n const decipher = crypto.createDecipheriv(CIPHER, key, iv) as crypto.DecipherGCM\n decipher.setAuthTag(tag)\n return decipher.update(encrypted) + decipher.final('utf8')\n } catch {\n return null\n }\n}\n\nexport function generateApiKey(): string {\n return 'ta_' + crypto.randomBytes(24).toString('hex') // 51-char key\n}\n\nexport function getApiKey(): string | null {\n const record = loadAdmin()\n if (!record?.apiKey) return null\n return decryptApiKey(record.apiKey)\n}\n\nexport function rotateApiKey(): string {\n const record = loadAdmin()\n if (!record) throw new Error('Admin store not initialised')\n const newKey = generateApiKey()\n saveAdmin({ ...record, apiKey: encryptApiKey(newKey) })\n return newKey\n}\n\nexport function verifyApiKey(key: string): boolean {\n const stored = getApiKey()\n if (!stored) return false\n if (key.length !== stored.length) return false\n return crypto.timingSafeEqual(Buffer.from(key), Buffer.from(stored))\n}\n\n// ---------------------------------------------------------------------------\n// Session cookie: HMAC-SHA256(secret, userId + timestamp) — stateless, no DB\n// ---------------------------------------------------------------------------\nexport function signSession(payload: string): string {\n const secret = process.env.THIRD_AUDIENCE_SECRET ?? 'ta-salt'\n const sig = crypto.createHmac('sha256', secret).update(payload).digest('hex')\n return `${payload}.${sig}`\n}\n\nexport function verifySession(token: string): boolean {\n const lastDot = token.lastIndexOf('.')\n if (lastDot === -1) return false\n const payload = token.slice(0, lastDot)\n const sig = token.slice(lastDot + 1)\n const expected = crypto.createHmac('sha256', process.env.THIRD_AUDIENCE_SECRET ?? 'ta-salt')\n .update(payload).digest('hex')\n // Constant-time comparison\n if (sig.length !== expected.length) return false\n return crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'))\n}\n"],"mappings":"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","names":["import_fs","import_path","import_server","path","fs","crypto","crypto","crypto","crypto","path","fs"]}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// src/dashboard/routes/bots-config-route.ts
|
|
2
|
+
import fs2 from "fs";
|
|
3
|
+
import path2 from "path";
|
|
4
|
+
import { NextResponse as NextResponse2 } from "next/server";
|
|
5
|
+
|
|
6
|
+
// src/dashboard/auth.ts
|
|
7
|
+
import { NextResponse } from "next/server";
|
|
8
|
+
|
|
9
|
+
// src/dashboard/admin-store.ts
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import crypto from "crypto";
|
|
13
|
+
function adminFilePath() {
|
|
14
|
+
const dataDir = process.env.TA_DATA_DIR ?? "data";
|
|
15
|
+
return path.join(process.cwd(), dataDir, "ta-admin.json");
|
|
16
|
+
}
|
|
17
|
+
function loadAdmin() {
|
|
18
|
+
const filePath = adminFilePath();
|
|
19
|
+
if (!fs.existsSync(filePath)) return null;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
var CIPHER = "aes-256-gcm";
|
|
27
|
+
function getEncryptionKey() {
|
|
28
|
+
const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-fallback-key-change-me";
|
|
29
|
+
return crypto.createHash("sha256").update(secret).digest();
|
|
30
|
+
}
|
|
31
|
+
function decryptApiKey(encoded) {
|
|
32
|
+
try {
|
|
33
|
+
const iv = Buffer.from(encoded.slice(0, 24), "hex");
|
|
34
|
+
const tag = Buffer.from(encoded.slice(24, 56), "hex");
|
|
35
|
+
const encrypted = Buffer.from(encoded.slice(56), "hex");
|
|
36
|
+
const key = getEncryptionKey();
|
|
37
|
+
const decipher = crypto.createDecipheriv(CIPHER, key, iv);
|
|
38
|
+
decipher.setAuthTag(tag);
|
|
39
|
+
return decipher.update(encrypted) + decipher.final("utf8");
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function getApiKey() {
|
|
45
|
+
const record = loadAdmin();
|
|
46
|
+
if (!record?.apiKey) return null;
|
|
47
|
+
return decryptApiKey(record.apiKey);
|
|
48
|
+
}
|
|
49
|
+
function verifyApiKey(key) {
|
|
50
|
+
const stored = getApiKey();
|
|
51
|
+
if (!stored) return false;
|
|
52
|
+
if (key.length !== stored.length) return false;
|
|
53
|
+
return crypto.timingSafeEqual(Buffer.from(key), Buffer.from(stored));
|
|
54
|
+
}
|
|
55
|
+
function verifySession(token) {
|
|
56
|
+
const lastDot = token.lastIndexOf(".");
|
|
57
|
+
if (lastDot === -1) return false;
|
|
58
|
+
const payload = token.slice(0, lastDot);
|
|
59
|
+
const sig = token.slice(lastDot + 1);
|
|
60
|
+
const expected = crypto.createHmac("sha256", process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt").update(payload).digest("hex");
|
|
61
|
+
if (sig.length !== expected.length) return false;
|
|
62
|
+
return crypto.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/dashboard/auth.ts
|
|
66
|
+
var SESSION_COOKIE = "ta_session";
|
|
67
|
+
function checkApiAuth(req) {
|
|
68
|
+
const apiKeyHeader = req.headers.get("x-ta-api-key");
|
|
69
|
+
if (apiKeyHeader) return verifyApiKey(apiKeyHeader);
|
|
70
|
+
const auth = req.headers.get("authorization") ?? "";
|
|
71
|
+
if (auth.startsWith("Bearer ")) {
|
|
72
|
+
const token = auth.slice(7);
|
|
73
|
+
return verifyApiKey(token);
|
|
74
|
+
}
|
|
75
|
+
const session = req.cookies.get(SESSION_COOKIE)?.value;
|
|
76
|
+
if (session) return verifySession(session);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
function unauthorizedResponse() {
|
|
80
|
+
return NextResponse.json(
|
|
81
|
+
{ error: "Unauthorized. Provide X-TA-Api-Key header or a valid session cookie." },
|
|
82
|
+
{
|
|
83
|
+
status: 401,
|
|
84
|
+
headers: { "WWW-Authenticate": 'Bearer realm="Third Audience API"' }
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/dashboard/routes/bots-config-route.ts
|
|
90
|
+
function botsConfigPath() {
|
|
91
|
+
return path2.join(process.cwd(), process.env.TA_DATA_DIR ?? "data", "ta-bots-config.json");
|
|
92
|
+
}
|
|
93
|
+
async function GET(req) {
|
|
94
|
+
if (!checkApiAuth(req)) return unauthorizedResponse();
|
|
95
|
+
const filePath = botsConfigPath();
|
|
96
|
+
if (!fs2.existsSync(filePath)) {
|
|
97
|
+
return NextResponse2.json({ allowlist: [], blocklist: [], track_unknown: true });
|
|
98
|
+
}
|
|
99
|
+
return NextResponse2.json(JSON.parse(fs2.readFileSync(filePath, "utf-8")));
|
|
100
|
+
}
|
|
101
|
+
async function POST(req) {
|
|
102
|
+
if (!checkApiAuth(req)) return unauthorizedResponse();
|
|
103
|
+
const body = await req.json();
|
|
104
|
+
const filePath = botsConfigPath();
|
|
105
|
+
fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
|
|
106
|
+
fs2.writeFileSync(filePath, JSON.stringify(body, null, 2));
|
|
107
|
+
return new NextResponse2(null, { status: 204 });
|
|
108
|
+
}
|
|
109
|
+
export {
|
|
110
|
+
GET,
|
|
111
|
+
POST
|
|
112
|
+
};
|
|
113
|
+
//# sourceMappingURL=bots-config-route.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/routes/bots-config-route.ts","../../../src/dashboard/auth.ts","../../../src/dashboard/admin-store.ts"],"sourcesContent":["import fs from 'fs'\nimport path from 'path'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { checkApiAuth, unauthorizedResponse } from '../auth.js'\n\nfunction botsConfigPath(): string {\n return path.join(process.cwd(), process.env.TA_DATA_DIR ?? 'data', 'ta-bots-config.json')\n}\n\n/** GET /api/third-audience/bots-config — returns current bots configuration */\nexport async function GET(req: NextRequest): Promise<NextResponse> {\n if (!checkApiAuth(req)) return unauthorizedResponse()\n\n const filePath = botsConfigPath()\n if (!fs.existsSync(filePath)) {\n return NextResponse.json({ allowlist: [], blocklist: [], track_unknown: true })\n }\n\n return NextResponse.json(JSON.parse(fs.readFileSync(filePath, 'utf-8')))\n}\n\n/** POST /api/third-audience/bots-config — saves bots configuration */\nexport async function POST(req: NextRequest): Promise<NextResponse> {\n if (!checkApiAuth(req)) return unauthorizedResponse()\n\n const body = await req.json()\n const filePath = botsConfigPath()\n fs.mkdirSync(path.dirname(filePath), { recursive: true })\n fs.writeFileSync(filePath, JSON.stringify(body, null, 2))\n\n return new NextResponse(null, { status: 204 })\n}\n","import type { NextRequest } from 'next/server'\nimport { NextResponse } from 'next/server'\nimport { verifySession, verifyApiKey } from './admin-store.js'\n\nconst SESSION_COOKIE = 'ta_session'\n\n/**\n * Authenticate an API route request. Accepts (in order):\n * 1. X-TA-Api-Key header — for headless/external callers (mirrors WP's approach)\n * 2. Authorization: Bearer <api-key> — same key, different transport\n * 3. Valid ta_session cookie — browser dashboard session\n */\nexport function checkApiAuth(req: NextRequest): boolean {\n // 1. X-TA-Api-Key header (WP-style headless key)\n const apiKeyHeader = req.headers.get('x-ta-api-key')\n if (apiKeyHeader) return verifyApiKey(apiKeyHeader)\n\n // 2. Bearer token (treat as api key)\n const auth = req.headers.get('authorization') ?? ''\n if (auth.startsWith('Bearer ')) {\n const token = auth.slice(7)\n return verifyApiKey(token)\n }\n\n // 3. Browser session cookie\n const session = req.cookies.get(SESSION_COOKIE)?.value\n if (session) return verifySession(session)\n\n return false\n}\n\n/**\n * Returns a 401 JSON response with the correct WWW-Authenticate header.\n * Use as: if (!checkApiAuth(req)) return unauthorizedResponse()\n */\nexport function unauthorizedResponse(): NextResponse {\n return NextResponse.json(\n { error: 'Unauthorized. Provide X-TA-Api-Key header or a valid session cookie.' },\n {\n status: 401,\n headers: { 'WWW-Authenticate': 'Bearer realm=\"Third Audience API\"' },\n }\n )\n}\n","import fs from 'fs'\nimport path from 'path'\nimport crypto from 'crypto'\n\nexport interface AdminRecord {\n passwordHash: string // sha256(secret + password)\n isDefaultPassword: boolean\n createdAt: string\n lastLoginAt: string | null\n apiKey?: string // AES-256-GCM encrypted, for headless/external API callers\n}\n\nfunction adminFilePath(): string {\n const dataDir = process.env.TA_DATA_DIR ?? 'data'\n return path.join(process.cwd(), dataDir, 'ta-admin.json')\n}\n\nexport function generateDefaultPassword(): string {\n return crypto.randomBytes(6).toString('hex') // 12-char hex, easy to type\n}\n\nexport function hashPassword(password: string): string {\n const secret = process.env.THIRD_AUDIENCE_SECRET ?? 'ta-salt'\n return crypto.createHash('sha256').update(secret + password).digest('hex')\n}\n\nexport function loadAdmin(): AdminRecord | null {\n const filePath = adminFilePath()\n if (!fs.existsSync(filePath)) return null\n try {\n return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as AdminRecord\n } catch {\n return null\n }\n}\n\nexport function saveAdmin(record: AdminRecord): void {\n const filePath = adminFilePath()\n const dir = path.dirname(filePath)\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })\n fs.writeFileSync(filePath, JSON.stringify(record, null, 2), 'utf-8')\n}\n\nexport const DEFAULT_PASSWORD = 'Chang3M3Now!'\n\nexport function initAdmin(): { password: string; apiKey: string; isNew: boolean } {\n const existing = loadAdmin()\n if (existing) return { password: '', apiKey: '', isNew: false }\n\n const apiKey = generateApiKey()\n saveAdmin({\n passwordHash: hashPassword(DEFAULT_PASSWORD),\n isDefaultPassword: true,\n createdAt: new Date().toISOString(),\n lastLoginAt: null,\n apiKey: encryptApiKey(apiKey),\n })\n return { password: DEFAULT_PASSWORD, apiKey, isNew: true }\n}\n\nexport function verifyPassword(password: string): boolean {\n const record = loadAdmin()\n if (!record) return false\n return record.passwordHash === hashPassword(password)\n}\n\nexport function updatePassword(newPassword: string): void {\n const record = loadAdmin()\n if (!record) return\n saveAdmin({\n ...record,\n passwordHash: hashPassword(newPassword),\n isDefaultPassword: false,\n })\n}\n\nexport function recordLogin(): void {\n const record = loadAdmin()\n if (!record) return\n saveAdmin({ ...record, lastLoginAt: new Date().toISOString() })\n}\n\n// ---------------------------------------------------------------------------\n// API key — AES-256-GCM encrypted at rest, mirroring WP's SECURE_AUTH_KEY approach\n// ---------------------------------------------------------------------------\n\nconst CIPHER = 'aes-256-gcm'\n\nfunction getEncryptionKey(): Buffer {\n const secret = process.env.THIRD_AUDIENCE_SECRET ?? 'ta-fallback-key-change-me'\n // Derive a 32-byte key from the secret using SHA-256\n return crypto.createHash('sha256').update(secret).digest()\n}\n\nfunction encryptApiKey(plaintext: string): string {\n const iv = crypto.randomBytes(12)\n const key = getEncryptionKey()\n const cipher = crypto.createCipheriv(CIPHER, key, iv) as crypto.CipherGCM\n const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])\n const tag = cipher.getAuthTag()\n // Format: iv(24 hex) + tag(32 hex) + encrypted(hex)\n return iv.toString('hex') + tag.toString('hex') + encrypted.toString('hex')\n}\n\nfunction decryptApiKey(encoded: string): string | null {\n try {\n const iv = Buffer.from(encoded.slice(0, 24), 'hex')\n const tag = Buffer.from(encoded.slice(24, 56), 'hex')\n const encrypted = Buffer.from(encoded.slice(56), 'hex')\n const key = getEncryptionKey()\n const decipher = crypto.createDecipheriv(CIPHER, key, iv) as crypto.DecipherGCM\n decipher.setAuthTag(tag)\n return decipher.update(encrypted) + decipher.final('utf8')\n } catch {\n return null\n }\n}\n\nexport function generateApiKey(): string {\n return 'ta_' + crypto.randomBytes(24).toString('hex') // 51-char key\n}\n\nexport function getApiKey(): string | null {\n const record = loadAdmin()\n if (!record?.apiKey) return null\n return decryptApiKey(record.apiKey)\n}\n\nexport function rotateApiKey(): string {\n const record = loadAdmin()\n if (!record) throw new Error('Admin store not initialised')\n const newKey = generateApiKey()\n saveAdmin({ ...record, apiKey: encryptApiKey(newKey) })\n return newKey\n}\n\nexport function verifyApiKey(key: string): boolean {\n const stored = getApiKey()\n if (!stored) return false\n if (key.length !== stored.length) return false\n return crypto.timingSafeEqual(Buffer.from(key), Buffer.from(stored))\n}\n\n// ---------------------------------------------------------------------------\n// Session cookie: HMAC-SHA256(secret, userId + timestamp) — stateless, no DB\n// ---------------------------------------------------------------------------\nexport function signSession(payload: string): string {\n const secret = process.env.THIRD_AUDIENCE_SECRET ?? 'ta-salt'\n const sig = crypto.createHmac('sha256', secret).update(payload).digest('hex')\n return `${payload}.${sig}`\n}\n\nexport function verifySession(token: string): boolean {\n const lastDot = token.lastIndexOf('.')\n if (lastDot === -1) return false\n const payload = token.slice(0, lastDot)\n const sig = token.slice(lastDot + 1)\n const expected = crypto.createHmac('sha256', process.env.THIRD_AUDIENCE_SECRET ?? 'ta-salt')\n .update(payload).digest('hex')\n // Constant-time comparison\n if (sig.length !== expected.length) return false\n return crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'))\n}\n"],"mappings":"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","names":["fs","path","NextResponse","path","fs","NextResponse"]}
|
|
@@ -0,0 +1,266 @@
|
|
|
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/okf-graph-route.ts
|
|
31
|
+
var okf_graph_route_exports = {};
|
|
32
|
+
__export(okf_graph_route_exports, {
|
|
33
|
+
GET: () => GET
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(okf_graph_route_exports);
|
|
36
|
+
var import_server2 = 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/okf/okf-bundle.ts
|
|
127
|
+
var renderer = new MarkdownRenderer();
|
|
128
|
+
function buildOkfGraph(files, baseUrl) {
|
|
129
|
+
const base = baseUrl.replace(/\/$/, "");
|
|
130
|
+
const slugSet = new Set(files.map((f) => f.slug));
|
|
131
|
+
const mdMap = /* @__PURE__ */ new Map();
|
|
132
|
+
for (const file of files) {
|
|
133
|
+
mdMap.set(file.slug, renderer.render(file));
|
|
134
|
+
}
|
|
135
|
+
const degrees = new Map(files.map((f) => [f.slug, 0]));
|
|
136
|
+
const rawEdges = [];
|
|
137
|
+
for (const file of files) {
|
|
138
|
+
const md = mdMap.get(file.slug) ?? "";
|
|
139
|
+
const linkRe = /\[([^\]]+)\]\((\/[^)]+)\)/g;
|
|
140
|
+
let m;
|
|
141
|
+
while ((m = linkRe.exec(md)) !== null) {
|
|
142
|
+
const candidate = m[2].replace(/^\//, "").replace(/\.md$/, "");
|
|
143
|
+
if (slugSet.has(candidate) && candidate !== file.slug) {
|
|
144
|
+
rawEdges.push({ source: file.slug, target: candidate });
|
|
145
|
+
degrees.set(file.slug, (degrees.get(file.slug) ?? 0) + 1);
|
|
146
|
+
degrees.set(candidate, (degrees.get(candidate) ?? 0) + 1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const top100 = files.slice().sort((a, b) => (degrees.get(b.slug) ?? 0) - (degrees.get(a.slug) ?? 0)).slice(0, 100);
|
|
151
|
+
const topSet = new Set(top100.map((f) => f.slug));
|
|
152
|
+
const nodes = top100.map((f) => ({
|
|
153
|
+
id: f.slug,
|
|
154
|
+
title: String(f.frontmatter.title ?? f.slug),
|
|
155
|
+
type: String(f.frontmatter.type ?? "WebPage"),
|
|
156
|
+
url: `${base}/${f.slug}`
|
|
157
|
+
}));
|
|
158
|
+
const edges = rawEdges.filter((e) => topSet.has(e.source) && topSet.has(e.target));
|
|
159
|
+
return { nodes, edges };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/dashboard/auth.ts
|
|
163
|
+
var import_server = require("next/server");
|
|
164
|
+
|
|
165
|
+
// src/dashboard/admin-store.ts
|
|
166
|
+
var import_fs2 = __toESM(require("fs"));
|
|
167
|
+
var import_path2 = __toESM(require("path"));
|
|
168
|
+
var import_crypto = __toESM(require("crypto"));
|
|
169
|
+
function adminFilePath() {
|
|
170
|
+
const dataDir = process.env.TA_DATA_DIR ?? "data";
|
|
171
|
+
return import_path2.default.join(process.cwd(), dataDir, "ta-admin.json");
|
|
172
|
+
}
|
|
173
|
+
function loadAdmin() {
|
|
174
|
+
const filePath = adminFilePath();
|
|
175
|
+
if (!import_fs2.default.existsSync(filePath)) return null;
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
var CIPHER = "aes-256-gcm";
|
|
183
|
+
function getEncryptionKey() {
|
|
184
|
+
const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-fallback-key-change-me";
|
|
185
|
+
return import_crypto.default.createHash("sha256").update(secret).digest();
|
|
186
|
+
}
|
|
187
|
+
function decryptApiKey(encoded) {
|
|
188
|
+
try {
|
|
189
|
+
const iv = Buffer.from(encoded.slice(0, 24), "hex");
|
|
190
|
+
const tag = Buffer.from(encoded.slice(24, 56), "hex");
|
|
191
|
+
const encrypted = Buffer.from(encoded.slice(56), "hex");
|
|
192
|
+
const key = getEncryptionKey();
|
|
193
|
+
const decipher = import_crypto.default.createDecipheriv(CIPHER, key, iv);
|
|
194
|
+
decipher.setAuthTag(tag);
|
|
195
|
+
return decipher.update(encrypted) + decipher.final("utf8");
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function getApiKey() {
|
|
201
|
+
const record = loadAdmin();
|
|
202
|
+
if (!record?.apiKey) return null;
|
|
203
|
+
return decryptApiKey(record.apiKey);
|
|
204
|
+
}
|
|
205
|
+
function verifyApiKey(key) {
|
|
206
|
+
const stored = getApiKey();
|
|
207
|
+
if (!stored) return false;
|
|
208
|
+
if (key.length !== stored.length) return false;
|
|
209
|
+
return import_crypto.default.timingSafeEqual(Buffer.from(key), Buffer.from(stored));
|
|
210
|
+
}
|
|
211
|
+
function verifySession(token) {
|
|
212
|
+
const lastDot = token.lastIndexOf(".");
|
|
213
|
+
if (lastDot === -1) return false;
|
|
214
|
+
const payload = token.slice(0, lastDot);
|
|
215
|
+
const sig = token.slice(lastDot + 1);
|
|
216
|
+
const expected = import_crypto.default.createHmac("sha256", process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt").update(payload).digest("hex");
|
|
217
|
+
if (sig.length !== expected.length) return false;
|
|
218
|
+
return import_crypto.default.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/dashboard/auth.ts
|
|
222
|
+
var SESSION_COOKIE = "ta_session";
|
|
223
|
+
function checkApiAuth(req) {
|
|
224
|
+
const apiKeyHeader = req.headers.get("x-ta-api-key");
|
|
225
|
+
if (apiKeyHeader) return verifyApiKey(apiKeyHeader);
|
|
226
|
+
const auth = req.headers.get("authorization") ?? "";
|
|
227
|
+
if (auth.startsWith("Bearer ")) {
|
|
228
|
+
const token = auth.slice(7);
|
|
229
|
+
return verifyApiKey(token);
|
|
230
|
+
}
|
|
231
|
+
const session = req.cookies.get(SESSION_COOKIE)?.value;
|
|
232
|
+
if (session) return verifySession(session);
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
function unauthorizedResponse() {
|
|
236
|
+
return import_server.NextResponse.json(
|
|
237
|
+
{ error: "Unauthorized. Provide X-TA-Api-Key header or a valid session cookie." },
|
|
238
|
+
{
|
|
239
|
+
status: 401,
|
|
240
|
+
headers: { "WWW-Authenticate": 'Bearer realm="Third Audience API"' }
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/dashboard/routes/okf-graph-route.ts
|
|
246
|
+
var reader = new MdxReader({ contentDir: import_path3.default.join(process.cwd(), process.env.TA_CONTENT_DIR ?? "content") });
|
|
247
|
+
async function GET(req) {
|
|
248
|
+
if (!checkApiAuth(req)) return unauthorizedResponse();
|
|
249
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`;
|
|
250
|
+
const files = reader.readAll();
|
|
251
|
+
const graph = buildOkfGraph(files, baseUrl);
|
|
252
|
+
return import_server2.NextResponse.json({
|
|
253
|
+
graph,
|
|
254
|
+
stats: {
|
|
255
|
+
pages: files.length,
|
|
256
|
+
nodes: graph.nodes.length,
|
|
257
|
+
edges: graph.edges.length
|
|
258
|
+
},
|
|
259
|
+
indexUrl: `${baseUrl}/okf`
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
263
|
+
0 && (module.exports = {
|
|
264
|
+
GET
|
|
265
|
+
});
|
|
266
|
+
//# sourceMappingURL=okf-graph-route.js.map
|