third-audience-mdx 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +41 -0
- package/INSTALLATION.md +367 -0
- package/README.md +303 -0
- package/WORKLOG.md +162 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +208 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/index.mjs +185 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/dashboard/auth.d.mts +16 -0
- package/dist/dashboard/auth.d.ts +16 -0
- package/dist/dashboard/auth.js +123 -0
- package/dist/dashboard/auth.js.map +1 -0
- package/dist/dashboard/auth.mjs +87 -0
- package/dist/dashboard/auth.mjs.map +1 -0
- package/dist/dashboard/routes/analytics-api-route.d.mts +6 -0
- package/dist/dashboard/routes/analytics-api-route.d.ts +6 -0
- package/dist/dashboard/routes/analytics-api-route.js +180 -0
- package/dist/dashboard/routes/analytics-api-route.js.map +1 -0
- package/dist/dashboard/routes/analytics-api-route.mjs +145 -0
- package/dist/dashboard/routes/analytics-api-route.mjs.map +1 -0
- package/dist/dashboard/routes/api-key-route.d.mts +8 -0
- package/dist/dashboard/routes/api-key-route.d.ts +8 -0
- package/dist/dashboard/routes/api-key-route.js +173 -0
- package/dist/dashboard/routes/api-key-route.js.map +1 -0
- package/dist/dashboard/routes/api-key-route.mjs +137 -0
- package/dist/dashboard/routes/api-key-route.mjs.map +1 -0
- package/dist/dashboard/routes/citation-route.d.mts +14 -0
- package/dist/dashboard/routes/citation-route.d.ts +14 -0
- package/dist/dashboard/routes/citation-route.js +202 -0
- package/dist/dashboard/routes/citation-route.js.map +1 -0
- package/dist/dashboard/routes/citation-route.mjs +166 -0
- package/dist/dashboard/routes/citation-route.mjs.map +1 -0
- package/dist/dashboard/routes/llms-txt-route.d.mts +6 -0
- package/dist/dashboard/routes/llms-txt-route.d.ts +6 -0
- package/dist/dashboard/routes/llms-txt-route.js +119 -0
- package/dist/dashboard/routes/llms-txt-route.js.map +1 -0
- package/dist/dashboard/routes/llms-txt-route.mjs +84 -0
- package/dist/dashboard/routes/llms-txt-route.mjs.map +1 -0
- package/dist/dashboard/routes/login-route.d.mts +6 -0
- package/dist/dashboard/routes/login-route.d.ts +6 -0
- package/dist/dashboard/routes/login-route.js +313 -0
- package/dist/dashboard/routes/login-route.js.map +1 -0
- package/dist/dashboard/routes/login-route.mjs +284 -0
- package/dist/dashboard/routes/login-route.mjs.map +1 -0
- package/dist/dashboard/routes/markdown-route.d.mts +15 -0
- package/dist/dashboard/routes/markdown-route.d.ts +15 -0
- package/dist/dashboard/routes/markdown-route.js +239 -0
- package/dist/dashboard/routes/markdown-route.js.map +1 -0
- package/dist/dashboard/routes/markdown-route.mjs +204 -0
- package/dist/dashboard/routes/markdown-route.mjs.map +1 -0
- package/dist/dashboard/routes/okf-route.d.mts +13 -0
- package/dist/dashboard/routes/okf-route.d.ts +13 -0
- package/dist/dashboard/routes/okf-route.js +184 -0
- package/dist/dashboard/routes/okf-route.js.map +1 -0
- package/dist/dashboard/routes/okf-route.mjs +149 -0
- package/dist/dashboard/routes/okf-route.mjs.map +1 -0
- package/dist/dashboard/routes/sitemap-ai-route.d.mts +6 -0
- package/dist/dashboard/routes/sitemap-ai-route.d.ts +6 -0
- package/dist/dashboard/routes/sitemap-ai-route.js +134 -0
- package/dist/dashboard/routes/sitemap-ai-route.js.map +1 -0
- package/dist/dashboard/routes/sitemap-ai-route.mjs +99 -0
- package/dist/dashboard/routes/sitemap-ai-route.mjs.map +1 -0
- package/dist/dashboard/ui/components/Sidebar.d.mts +5 -0
- package/dist/dashboard/ui/components/Sidebar.d.ts +5 -0
- package/dist/dashboard/ui/components/Sidebar.js +102 -0
- package/dist/dashboard/ui/components/Sidebar.js.map +1 -0
- package/dist/dashboard/ui/components/Sidebar.mjs +68 -0
- package/dist/dashboard/ui/components/Sidebar.mjs.map +1 -0
- package/dist/dashboard/ui/globals.css +175 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.d.mts +5 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.d.ts +5 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.js +269 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.js.map +1 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.mjs +232 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.mjs.map +1 -0
- package/dist/dashboard/ui/pages/BotManagementPage.d.mts +13 -0
- package/dist/dashboard/ui/pages/BotManagementPage.d.ts +13 -0
- package/dist/dashboard/ui/pages/BotManagementPage.js +177 -0
- package/dist/dashboard/ui/pages/BotManagementPage.js.map +1 -0
- package/dist/dashboard/ui/pages/BotManagementPage.mjs +153 -0
- package/dist/dashboard/ui/pages/BotManagementPage.mjs.map +1 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.d.mts +5 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.d.ts +5 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.js +203 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.js.map +1 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.mjs +168 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.mjs.map +1 -0
- package/dist/dashboard/ui/pages/SettingsPage.d.mts +8 -0
- package/dist/dashboard/ui/pages/SettingsPage.d.ts +8 -0
- package/dist/dashboard/ui/pages/SettingsPage.js +181 -0
- package/dist/dashboard/ui/pages/SettingsPage.js.map +1 -0
- package/dist/dashboard/ui/pages/SettingsPage.mjs +157 -0
- package/dist/dashboard/ui/pages/SettingsPage.mjs.map +1 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.d.mts +5 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.d.ts +5 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.js +183 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.js.map +1 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.mjs +148 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.mjs.map +1 -0
- package/dist/index.d.mts +84 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +372 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +346 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +125 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/cli/commands/init.ts","../../src/cli/commands/health.ts","../../src/cli/commands/export.ts","../../src/cli/index.ts"],"sourcesContent":["import fs from 'fs'\nimport path from 'path'\nimport readline from 'readline'\n\nexport async function init(): Promise<void> {\n const cwd = process.cwd()\n console.log('\\n🎯 third-audience-mdx setup\\n')\n\n // Detect Next.js\n const pkgPath = path.join(cwd, 'package.json')\n if (!fs.existsSync(pkgPath)) {\n console.error('No package.json found. Run this from your Next.js project root.')\n process.exit(1)\n }\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))\n if (!pkg.dependencies?.next && !pkg.devDependencies?.next) {\n console.warn('⚠ next not found in package.json — make sure this is a Next.js project.')\n }\n\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout })\n const ask = (q: string, def: string) => new Promise<string>(r =>\n rl.question(`${q} (${def}): `, ans => r(ans.trim() || def))\n )\n\n const contentDir = await ask('Content directory (where your .mdx files live)', 'content')\n const dataDir = await ask('Data directory (for analytics logs)', 'data')\n const secret = await ask('Dashboard secret (leave blank to disable auth in dev)', '')\n rl.close()\n\n // Write middleware.ts if not exists\n const middlewarePath = path.join(cwd, 'middleware.ts')\n if (!fs.existsSync(middlewarePath)) {\n fs.writeFileSync(middlewarePath, `export { thirdAudienceMiddleware as middleware } from 'third-audience-mdx'\\nexport const config = { matcher: ['/((?!_next|api).*)'] }\\n`)\n console.log('✅ Created middleware.ts')\n } else {\n console.log('⚠ middleware.ts already exists — add thirdAudienceMiddleware manually.')\n }\n\n // Write .env.local additions\n const envPath = path.join(cwd, '.env.local')\n const envLines = [`THIRD_AUDIENCE_SECRET=${secret}`, `NEXT_PUBLIC_SITE_URL=http://localhost:3000`]\n const envContent = envLines.join('\\n') + '\\n'\n if (!fs.existsSync(envPath)) {\n fs.writeFileSync(envPath, envContent)\n console.log('✅ Created .env.local')\n } else {\n fs.appendFileSync(envPath, '\\n# Third Audience\\n' + envContent)\n console.log('✅ Appended to .env.local')\n }\n\n // Create data dir and .gitignore entry\n fs.mkdirSync(path.join(cwd, dataDir, 'ta-cache'), { recursive: true })\n const gitignorePath = path.join(cwd, '.gitignore')\n const gitignoreAdditions = `\\n# Third Audience analytics (local only)\\n${dataDir}/ta-visits.jsonl\\n${dataDir}/ta-citations.jsonl\\n${dataDir}/ta-cache/\\n`\n if (fs.existsSync(gitignorePath)) {\n fs.appendFileSync(gitignorePath, gitignoreAdditions)\n } else {\n fs.writeFileSync(gitignorePath, gitignoreAdditions.trimStart())\n }\n console.log(`✅ Created ${dataDir}/ and updated .gitignore`)\n\n console.log(`\n✅ Setup complete!\n\nNext steps:\n 1. Add to next.config.ts:\n import { withThirdAudience } from 'third-audience-mdx'\n export default withThirdAudience({ contentDir: '${contentDir}', dataDir: '${dataDir}' })\n\n 2. Add API routes in app/api/third-audience/ (see docs)\n\n 3. Visit /third-audience/ for your dashboard\n`)\n}\n","import fs from 'fs'\nimport path from 'path'\n\nexport async function health(): Promise<void> {\n const cwd = process.cwd()\n const checks: Array<{ label: string; ok: boolean; note?: string }> = []\n\n // Node version\n const nodeVersion = process.versions.node\n const [nodeMajor] = nodeVersion.split('.').map(Number)\n checks.push({ label: `Node.js ${nodeVersion}`, ok: nodeMajor >= 18, note: nodeMajor < 18 ? 'requires Node 18+' : undefined })\n\n // package.json\n const pkgPath = path.join(cwd, 'package.json')\n checks.push({ label: 'package.json', ok: fs.existsSync(pkgPath) })\n\n // next installed\n const nextPath = path.join(cwd, 'node_modules', 'next')\n checks.push({ label: 'next installed', ok: fs.existsSync(nextPath) })\n\n // middleware.ts\n const middlewarePath = path.join(cwd, 'middleware.ts')\n checks.push({ label: 'middleware.ts', ok: fs.existsSync(middlewarePath) })\n\n // content dir\n const contentDir = process.env.TA_CONTENT_DIR ?? 'content'\n const contentPath = path.join(cwd, contentDir)\n const contentExists = fs.existsSync(contentPath)\n const mdxCount = contentExists\n ? countFiles(contentPath, ['.mdx', '.md'])\n : 0\n checks.push({ label: `contentDir (${contentDir})`, ok: contentExists, note: contentExists ? `${mdxCount} MDX files` : 'directory not found' })\n\n // data dir\n const dataDir = process.env.TA_DATA_DIR ?? 'data'\n const dataPath = path.join(cwd, dataDir)\n checks.push({ label: `dataDir (${dataDir})`, ok: fs.existsSync(dataPath) })\n\n // JSONL files\n const visitsPath = path.join(cwd, dataDir, 'ta-visits.jsonl')\n const citationsPath = path.join(cwd, dataDir, 'ta-citations.jsonl')\n const visitLines = fs.existsSync(visitsPath) ? countLines(visitsPath) : 0\n const citationLines = fs.existsSync(citationsPath) ? countLines(citationsPath) : 0\n checks.push({ label: 'ta-visits.jsonl', ok: true, note: `${visitLines} records` })\n checks.push({ label: 'ta-citations.jsonl', ok: true, note: `${citationLines} records` })\n\n // dashboard secret\n checks.push({ label: 'THIRD_AUDIENCE_SECRET', ok: !!process.env.THIRD_AUDIENCE_SECRET, note: !process.env.THIRD_AUDIENCE_SECRET ? 'not set (dashboard is open)' : 'set' })\n\n console.log('\\n🏥 third-audience health check\\n')\n for (const c of checks) {\n const icon = c.ok ? '✅' : '❌'\n const note = c.note ? ` (${c.note})` : ''\n console.log(` ${icon} ${c.label}${note}`)\n }\n console.log()\n}\n\nfunction countFiles(dir: string, exts: string[]): number {\n let count = 0\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n if (entry.isDirectory()) count += countFiles(path.join(dir, entry.name), exts)\n else if (exts.some(e => entry.name.endsWith(e))) count++\n }\n return count\n}\n\nfunction countLines(filePath: string): number {\n return fs.readFileSync(filePath, 'utf-8').split('\\n').filter(Boolean).length\n}\n","import fs from 'fs'\nimport path from 'path'\n\nexport async function exportData(args: string[]): Promise<void> {\n const dataDir = process.env.TA_DATA_DIR ?? 'data'\n const type = args[0] ?? 'visits'\n const outPath = args[1] ?? `ta-${type}-export.csv`\n\n const file = type === 'citations' ? 'ta-citations.jsonl' : 'ta-visits.jsonl'\n const jsonlPath = path.join(process.cwd(), dataDir, file)\n\n if (!fs.existsSync(jsonlPath)) {\n console.error(`No data found at ${jsonlPath}`)\n process.exit(1)\n }\n\n const records = fs.readFileSync(jsonlPath, 'utf-8')\n .split('\\n')\n .filter(Boolean)\n .map(l => { try { return JSON.parse(l) } catch { return null } })\n .filter(Boolean)\n\n if (records.length === 0) {\n console.log('No records to export.')\n return\n }\n\n const headers = Object.keys(records[0])\n const csv = [\n headers.join(','),\n ...records.map(r => headers.map(h => csvCell(r[h])).join(',')),\n ].join('\\n') + '\\n'\n\n fs.writeFileSync(outPath, csv)\n console.log(`✅ Exported ${records.length} records to ${outPath}`)\n}\n\nfunction csvCell(val: unknown): string {\n if (val === null || val === undefined) return ''\n const s = String(val)\n if (/[\",\\n]/.test(s)) return `\"${s.replace(/\"/g, '\"\"')}\"`\n return s\n}\n","#!/usr/bin/env node\nimport { init } from './commands/init.js'\nimport { health } from './commands/health.js'\nimport { exportData } from './commands/export.js'\n\nconst [,, command, ...args] = process.argv\n\nswitch (command) {\n case 'init':\n init().catch(console.error)\n break\n case 'health':\n health().catch(console.error)\n break\n case 'export':\n exportData(args).catch(console.error)\n break\n default:\n console.log(`third-audience CLI\n\nCommands:\n init Set up third-audience-mdx in your Next.js project\n health Show system health status\n export Export analytics data as CSV\n\nUsage: npx third-audience <command>`)\n}\n"],"mappings":"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","names":["path","fs","readline","import_fs","import_path","path","fs","import_fs","import_path","args","path","fs"]}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/commands/init.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import readline from "readline";
|
|
7
|
+
async function init() {
|
|
8
|
+
const cwd = process.cwd();
|
|
9
|
+
console.log("\n\u{1F3AF} third-audience-mdx setup\n");
|
|
10
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
11
|
+
if (!fs.existsSync(pkgPath)) {
|
|
12
|
+
console.error("No package.json found. Run this from your Next.js project root.");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
16
|
+
if (!pkg.dependencies?.next && !pkg.devDependencies?.next) {
|
|
17
|
+
console.warn("\u26A0 next not found in package.json \u2014 make sure this is a Next.js project.");
|
|
18
|
+
}
|
|
19
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
20
|
+
const ask = (q, def) => new Promise(
|
|
21
|
+
(r) => rl.question(`${q} (${def}): `, (ans) => r(ans.trim() || def))
|
|
22
|
+
);
|
|
23
|
+
const contentDir = await ask("Content directory (where your .mdx files live)", "content");
|
|
24
|
+
const dataDir = await ask("Data directory (for analytics logs)", "data");
|
|
25
|
+
const secret = await ask("Dashboard secret (leave blank to disable auth in dev)", "");
|
|
26
|
+
rl.close();
|
|
27
|
+
const middlewarePath = path.join(cwd, "middleware.ts");
|
|
28
|
+
if (!fs.existsSync(middlewarePath)) {
|
|
29
|
+
fs.writeFileSync(middlewarePath, `export { thirdAudienceMiddleware as middleware } from 'third-audience-mdx'
|
|
30
|
+
export const config = { matcher: ['/((?!_next|api).*)'] }
|
|
31
|
+
`);
|
|
32
|
+
console.log("\u2705 Created middleware.ts");
|
|
33
|
+
} else {
|
|
34
|
+
console.log("\u26A0 middleware.ts already exists \u2014 add thirdAudienceMiddleware manually.");
|
|
35
|
+
}
|
|
36
|
+
const envPath = path.join(cwd, ".env.local");
|
|
37
|
+
const envLines = [`THIRD_AUDIENCE_SECRET=${secret}`, `NEXT_PUBLIC_SITE_URL=http://localhost:3000`];
|
|
38
|
+
const envContent = envLines.join("\n") + "\n";
|
|
39
|
+
if (!fs.existsSync(envPath)) {
|
|
40
|
+
fs.writeFileSync(envPath, envContent);
|
|
41
|
+
console.log("\u2705 Created .env.local");
|
|
42
|
+
} else {
|
|
43
|
+
fs.appendFileSync(envPath, "\n# Third Audience\n" + envContent);
|
|
44
|
+
console.log("\u2705 Appended to .env.local");
|
|
45
|
+
}
|
|
46
|
+
fs.mkdirSync(path.join(cwd, dataDir, "ta-cache"), { recursive: true });
|
|
47
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
48
|
+
const gitignoreAdditions = `
|
|
49
|
+
# Third Audience analytics (local only)
|
|
50
|
+
${dataDir}/ta-visits.jsonl
|
|
51
|
+
${dataDir}/ta-citations.jsonl
|
|
52
|
+
${dataDir}/ta-cache/
|
|
53
|
+
`;
|
|
54
|
+
if (fs.existsSync(gitignorePath)) {
|
|
55
|
+
fs.appendFileSync(gitignorePath, gitignoreAdditions);
|
|
56
|
+
} else {
|
|
57
|
+
fs.writeFileSync(gitignorePath, gitignoreAdditions.trimStart());
|
|
58
|
+
}
|
|
59
|
+
console.log(`\u2705 Created ${dataDir}/ and updated .gitignore`);
|
|
60
|
+
console.log(`
|
|
61
|
+
\u2705 Setup complete!
|
|
62
|
+
|
|
63
|
+
Next steps:
|
|
64
|
+
1. Add to next.config.ts:
|
|
65
|
+
import { withThirdAudience } from 'third-audience-mdx'
|
|
66
|
+
export default withThirdAudience({ contentDir: '${contentDir}', dataDir: '${dataDir}' })
|
|
67
|
+
|
|
68
|
+
2. Add API routes in app/api/third-audience/ (see docs)
|
|
69
|
+
|
|
70
|
+
3. Visit /third-audience/ for your dashboard
|
|
71
|
+
`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/cli/commands/health.ts
|
|
75
|
+
import fs2 from "fs";
|
|
76
|
+
import path2 from "path";
|
|
77
|
+
async function health() {
|
|
78
|
+
const cwd = process.cwd();
|
|
79
|
+
const checks = [];
|
|
80
|
+
const nodeVersion = process.versions.node;
|
|
81
|
+
const [nodeMajor] = nodeVersion.split(".").map(Number);
|
|
82
|
+
checks.push({ label: `Node.js ${nodeVersion}`, ok: nodeMajor >= 18, note: nodeMajor < 18 ? "requires Node 18+" : void 0 });
|
|
83
|
+
const pkgPath = path2.join(cwd, "package.json");
|
|
84
|
+
checks.push({ label: "package.json", ok: fs2.existsSync(pkgPath) });
|
|
85
|
+
const nextPath = path2.join(cwd, "node_modules", "next");
|
|
86
|
+
checks.push({ label: "next installed", ok: fs2.existsSync(nextPath) });
|
|
87
|
+
const middlewarePath = path2.join(cwd, "middleware.ts");
|
|
88
|
+
checks.push({ label: "middleware.ts", ok: fs2.existsSync(middlewarePath) });
|
|
89
|
+
const contentDir = process.env.TA_CONTENT_DIR ?? "content";
|
|
90
|
+
const contentPath = path2.join(cwd, contentDir);
|
|
91
|
+
const contentExists = fs2.existsSync(contentPath);
|
|
92
|
+
const mdxCount = contentExists ? countFiles(contentPath, [".mdx", ".md"]) : 0;
|
|
93
|
+
checks.push({ label: `contentDir (${contentDir})`, ok: contentExists, note: contentExists ? `${mdxCount} MDX files` : "directory not found" });
|
|
94
|
+
const dataDir = process.env.TA_DATA_DIR ?? "data";
|
|
95
|
+
const dataPath = path2.join(cwd, dataDir);
|
|
96
|
+
checks.push({ label: `dataDir (${dataDir})`, ok: fs2.existsSync(dataPath) });
|
|
97
|
+
const visitsPath = path2.join(cwd, dataDir, "ta-visits.jsonl");
|
|
98
|
+
const citationsPath = path2.join(cwd, dataDir, "ta-citations.jsonl");
|
|
99
|
+
const visitLines = fs2.existsSync(visitsPath) ? countLines(visitsPath) : 0;
|
|
100
|
+
const citationLines = fs2.existsSync(citationsPath) ? countLines(citationsPath) : 0;
|
|
101
|
+
checks.push({ label: "ta-visits.jsonl", ok: true, note: `${visitLines} records` });
|
|
102
|
+
checks.push({ label: "ta-citations.jsonl", ok: true, note: `${citationLines} records` });
|
|
103
|
+
checks.push({ label: "THIRD_AUDIENCE_SECRET", ok: !!process.env.THIRD_AUDIENCE_SECRET, note: !process.env.THIRD_AUDIENCE_SECRET ? "not set (dashboard is open)" : "set" });
|
|
104
|
+
console.log("\n\u{1F3E5} third-audience health check\n");
|
|
105
|
+
for (const c of checks) {
|
|
106
|
+
const icon = c.ok ? "\u2705" : "\u274C";
|
|
107
|
+
const note = c.note ? ` (${c.note})` : "";
|
|
108
|
+
console.log(` ${icon} ${c.label}${note}`);
|
|
109
|
+
}
|
|
110
|
+
console.log();
|
|
111
|
+
}
|
|
112
|
+
function countFiles(dir, exts) {
|
|
113
|
+
let count = 0;
|
|
114
|
+
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
115
|
+
if (entry.isDirectory()) count += countFiles(path2.join(dir, entry.name), exts);
|
|
116
|
+
else if (exts.some((e) => entry.name.endsWith(e))) count++;
|
|
117
|
+
}
|
|
118
|
+
return count;
|
|
119
|
+
}
|
|
120
|
+
function countLines(filePath) {
|
|
121
|
+
return fs2.readFileSync(filePath, "utf-8").split("\n").filter(Boolean).length;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/cli/commands/export.ts
|
|
125
|
+
import fs3 from "fs";
|
|
126
|
+
import path3 from "path";
|
|
127
|
+
async function exportData(args2) {
|
|
128
|
+
const dataDir = process.env.TA_DATA_DIR ?? "data";
|
|
129
|
+
const type = args2[0] ?? "visits";
|
|
130
|
+
const outPath = args2[1] ?? `ta-${type}-export.csv`;
|
|
131
|
+
const file = type === "citations" ? "ta-citations.jsonl" : "ta-visits.jsonl";
|
|
132
|
+
const jsonlPath = path3.join(process.cwd(), dataDir, file);
|
|
133
|
+
if (!fs3.existsSync(jsonlPath)) {
|
|
134
|
+
console.error(`No data found at ${jsonlPath}`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const records = fs3.readFileSync(jsonlPath, "utf-8").split("\n").filter(Boolean).map((l) => {
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(l);
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}).filter(Boolean);
|
|
144
|
+
if (records.length === 0) {
|
|
145
|
+
console.log("No records to export.");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const headers = Object.keys(records[0]);
|
|
149
|
+
const csv = [
|
|
150
|
+
headers.join(","),
|
|
151
|
+
...records.map((r) => headers.map((h) => csvCell(r[h])).join(","))
|
|
152
|
+
].join("\n") + "\n";
|
|
153
|
+
fs3.writeFileSync(outPath, csv);
|
|
154
|
+
console.log(`\u2705 Exported ${records.length} records to ${outPath}`);
|
|
155
|
+
}
|
|
156
|
+
function csvCell(val) {
|
|
157
|
+
if (val === null || val === void 0) return "";
|
|
158
|
+
const s = String(val);
|
|
159
|
+
if (/[",\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
|
|
160
|
+
return s;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/cli/index.ts
|
|
164
|
+
var [, , command, ...args] = process.argv;
|
|
165
|
+
switch (command) {
|
|
166
|
+
case "init":
|
|
167
|
+
init().catch(console.error);
|
|
168
|
+
break;
|
|
169
|
+
case "health":
|
|
170
|
+
health().catch(console.error);
|
|
171
|
+
break;
|
|
172
|
+
case "export":
|
|
173
|
+
exportData(args).catch(console.error);
|
|
174
|
+
break;
|
|
175
|
+
default:
|
|
176
|
+
console.log(`third-audience CLI
|
|
177
|
+
|
|
178
|
+
Commands:
|
|
179
|
+
init Set up third-audience-mdx in your Next.js project
|
|
180
|
+
health Show system health status
|
|
181
|
+
export Export analytics data as CSV
|
|
182
|
+
|
|
183
|
+
Usage: npx third-audience <command>`);
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/cli/commands/init.ts","../../src/cli/commands/health.ts","../../src/cli/commands/export.ts","../../src/cli/index.ts"],"sourcesContent":["import fs from 'fs'\nimport path from 'path'\nimport readline from 'readline'\n\nexport async function init(): Promise<void> {\n const cwd = process.cwd()\n console.log('\\n🎯 third-audience-mdx setup\\n')\n\n // Detect Next.js\n const pkgPath = path.join(cwd, 'package.json')\n if (!fs.existsSync(pkgPath)) {\n console.error('No package.json found. Run this from your Next.js project root.')\n process.exit(1)\n }\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))\n if (!pkg.dependencies?.next && !pkg.devDependencies?.next) {\n console.warn('⚠ next not found in package.json — make sure this is a Next.js project.')\n }\n\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout })\n const ask = (q: string, def: string) => new Promise<string>(r =>\n rl.question(`${q} (${def}): `, ans => r(ans.trim() || def))\n )\n\n const contentDir = await ask('Content directory (where your .mdx files live)', 'content')\n const dataDir = await ask('Data directory (for analytics logs)', 'data')\n const secret = await ask('Dashboard secret (leave blank to disable auth in dev)', '')\n rl.close()\n\n // Write middleware.ts if not exists\n const middlewarePath = path.join(cwd, 'middleware.ts')\n if (!fs.existsSync(middlewarePath)) {\n fs.writeFileSync(middlewarePath, `export { thirdAudienceMiddleware as middleware } from 'third-audience-mdx'\\nexport const config = { matcher: ['/((?!_next|api).*)'] }\\n`)\n console.log('✅ Created middleware.ts')\n } else {\n console.log('⚠ middleware.ts already exists — add thirdAudienceMiddleware manually.')\n }\n\n // Write .env.local additions\n const envPath = path.join(cwd, '.env.local')\n const envLines = [`THIRD_AUDIENCE_SECRET=${secret}`, `NEXT_PUBLIC_SITE_URL=http://localhost:3000`]\n const envContent = envLines.join('\\n') + '\\n'\n if (!fs.existsSync(envPath)) {\n fs.writeFileSync(envPath, envContent)\n console.log('✅ Created .env.local')\n } else {\n fs.appendFileSync(envPath, '\\n# Third Audience\\n' + envContent)\n console.log('✅ Appended to .env.local')\n }\n\n // Create data dir and .gitignore entry\n fs.mkdirSync(path.join(cwd, dataDir, 'ta-cache'), { recursive: true })\n const gitignorePath = path.join(cwd, '.gitignore')\n const gitignoreAdditions = `\\n# Third Audience analytics (local only)\\n${dataDir}/ta-visits.jsonl\\n${dataDir}/ta-citations.jsonl\\n${dataDir}/ta-cache/\\n`\n if (fs.existsSync(gitignorePath)) {\n fs.appendFileSync(gitignorePath, gitignoreAdditions)\n } else {\n fs.writeFileSync(gitignorePath, gitignoreAdditions.trimStart())\n }\n console.log(`✅ Created ${dataDir}/ and updated .gitignore`)\n\n console.log(`\n✅ Setup complete!\n\nNext steps:\n 1. Add to next.config.ts:\n import { withThirdAudience } from 'third-audience-mdx'\n export default withThirdAudience({ contentDir: '${contentDir}', dataDir: '${dataDir}' })\n\n 2. Add API routes in app/api/third-audience/ (see docs)\n\n 3. Visit /third-audience/ for your dashboard\n`)\n}\n","import fs from 'fs'\nimport path from 'path'\n\nexport async function health(): Promise<void> {\n const cwd = process.cwd()\n const checks: Array<{ label: string; ok: boolean; note?: string }> = []\n\n // Node version\n const nodeVersion = process.versions.node\n const [nodeMajor] = nodeVersion.split('.').map(Number)\n checks.push({ label: `Node.js ${nodeVersion}`, ok: nodeMajor >= 18, note: nodeMajor < 18 ? 'requires Node 18+' : undefined })\n\n // package.json\n const pkgPath = path.join(cwd, 'package.json')\n checks.push({ label: 'package.json', ok: fs.existsSync(pkgPath) })\n\n // next installed\n const nextPath = path.join(cwd, 'node_modules', 'next')\n checks.push({ label: 'next installed', ok: fs.existsSync(nextPath) })\n\n // middleware.ts\n const middlewarePath = path.join(cwd, 'middleware.ts')\n checks.push({ label: 'middleware.ts', ok: fs.existsSync(middlewarePath) })\n\n // content dir\n const contentDir = process.env.TA_CONTENT_DIR ?? 'content'\n const contentPath = path.join(cwd, contentDir)\n const contentExists = fs.existsSync(contentPath)\n const mdxCount = contentExists\n ? countFiles(contentPath, ['.mdx', '.md'])\n : 0\n checks.push({ label: `contentDir (${contentDir})`, ok: contentExists, note: contentExists ? `${mdxCount} MDX files` : 'directory not found' })\n\n // data dir\n const dataDir = process.env.TA_DATA_DIR ?? 'data'\n const dataPath = path.join(cwd, dataDir)\n checks.push({ label: `dataDir (${dataDir})`, ok: fs.existsSync(dataPath) })\n\n // JSONL files\n const visitsPath = path.join(cwd, dataDir, 'ta-visits.jsonl')\n const citationsPath = path.join(cwd, dataDir, 'ta-citations.jsonl')\n const visitLines = fs.existsSync(visitsPath) ? countLines(visitsPath) : 0\n const citationLines = fs.existsSync(citationsPath) ? countLines(citationsPath) : 0\n checks.push({ label: 'ta-visits.jsonl', ok: true, note: `${visitLines} records` })\n checks.push({ label: 'ta-citations.jsonl', ok: true, note: `${citationLines} records` })\n\n // dashboard secret\n checks.push({ label: 'THIRD_AUDIENCE_SECRET', ok: !!process.env.THIRD_AUDIENCE_SECRET, note: !process.env.THIRD_AUDIENCE_SECRET ? 'not set (dashboard is open)' : 'set' })\n\n console.log('\\n🏥 third-audience health check\\n')\n for (const c of checks) {\n const icon = c.ok ? '✅' : '❌'\n const note = c.note ? ` (${c.note})` : ''\n console.log(` ${icon} ${c.label}${note}`)\n }\n console.log()\n}\n\nfunction countFiles(dir: string, exts: string[]): number {\n let count = 0\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n if (entry.isDirectory()) count += countFiles(path.join(dir, entry.name), exts)\n else if (exts.some(e => entry.name.endsWith(e))) count++\n }\n return count\n}\n\nfunction countLines(filePath: string): number {\n return fs.readFileSync(filePath, 'utf-8').split('\\n').filter(Boolean).length\n}\n","import fs from 'fs'\nimport path from 'path'\n\nexport async function exportData(args: string[]): Promise<void> {\n const dataDir = process.env.TA_DATA_DIR ?? 'data'\n const type = args[0] ?? 'visits'\n const outPath = args[1] ?? `ta-${type}-export.csv`\n\n const file = type === 'citations' ? 'ta-citations.jsonl' : 'ta-visits.jsonl'\n const jsonlPath = path.join(process.cwd(), dataDir, file)\n\n if (!fs.existsSync(jsonlPath)) {\n console.error(`No data found at ${jsonlPath}`)\n process.exit(1)\n }\n\n const records = fs.readFileSync(jsonlPath, 'utf-8')\n .split('\\n')\n .filter(Boolean)\n .map(l => { try { return JSON.parse(l) } catch { return null } })\n .filter(Boolean)\n\n if (records.length === 0) {\n console.log('No records to export.')\n return\n }\n\n const headers = Object.keys(records[0])\n const csv = [\n headers.join(','),\n ...records.map(r => headers.map(h => csvCell(r[h])).join(',')),\n ].join('\\n') + '\\n'\n\n fs.writeFileSync(outPath, csv)\n console.log(`✅ Exported ${records.length} records to ${outPath}`)\n}\n\nfunction csvCell(val: unknown): string {\n if (val === null || val === undefined) return ''\n const s = String(val)\n if (/[\",\\n]/.test(s)) return `\"${s.replace(/\"/g, '\"\"')}\"`\n return s\n}\n","#!/usr/bin/env node\nimport { init } from './commands/init.js'\nimport { health } from './commands/health.js'\nimport { exportData } from './commands/export.js'\n\nconst [,, command, ...args] = process.argv\n\nswitch (command) {\n case 'init':\n init().catch(console.error)\n break\n case 'health':\n health().catch(console.error)\n break\n case 'export':\n exportData(args).catch(console.error)\n break\n default:\n console.log(`third-audience CLI\n\nCommands:\n init Set up third-audience-mdx in your Next.js project\n health Show system health status\n export Export analytics data as CSV\n\nUsage: npx third-audience <command>`)\n}\n"],"mappings":"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","names":["fs","path","fs","path","args"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Authenticate an API route request. Accepts (in order):
|
|
5
|
+
* 1. X-TA-Api-Key header — for headless/external callers (mirrors WP's approach)
|
|
6
|
+
* 2. Authorization: Bearer <api-key> — same key, different transport
|
|
7
|
+
* 3. Valid ta_session cookie — browser dashboard session
|
|
8
|
+
*/
|
|
9
|
+
declare function checkApiAuth(req: NextRequest): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Returns a 401 JSON response with the correct WWW-Authenticate header.
|
|
12
|
+
* Use as: if (!checkApiAuth(req)) return unauthorizedResponse()
|
|
13
|
+
*/
|
|
14
|
+
declare function unauthorizedResponse(): NextResponse;
|
|
15
|
+
|
|
16
|
+
export { checkApiAuth, unauthorizedResponse };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Authenticate an API route request. Accepts (in order):
|
|
5
|
+
* 1. X-TA-Api-Key header — for headless/external callers (mirrors WP's approach)
|
|
6
|
+
* 2. Authorization: Bearer <api-key> — same key, different transport
|
|
7
|
+
* 3. Valid ta_session cookie — browser dashboard session
|
|
8
|
+
*/
|
|
9
|
+
declare function checkApiAuth(req: NextRequest): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Returns a 401 JSON response with the correct WWW-Authenticate header.
|
|
12
|
+
* Use as: if (!checkApiAuth(req)) return unauthorizedResponse()
|
|
13
|
+
*/
|
|
14
|
+
declare function unauthorizedResponse(): NextResponse;
|
|
15
|
+
|
|
16
|
+
export { checkApiAuth, unauthorizedResponse };
|
|
@@ -0,0 +1,123 @@
|
|
|
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/auth.ts
|
|
31
|
+
var auth_exports = {};
|
|
32
|
+
__export(auth_exports, {
|
|
33
|
+
checkApiAuth: () => checkApiAuth,
|
|
34
|
+
unauthorizedResponse: () => unauthorizedResponse
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(auth_exports);
|
|
37
|
+
var import_server = require("next/server");
|
|
38
|
+
|
|
39
|
+
// src/dashboard/admin-store.ts
|
|
40
|
+
var import_fs = __toESM(require("fs"));
|
|
41
|
+
var import_path = __toESM(require("path"));
|
|
42
|
+
var import_crypto = __toESM(require("crypto"));
|
|
43
|
+
function adminFilePath() {
|
|
44
|
+
const dataDir = process.env.TA_DATA_DIR ?? "data";
|
|
45
|
+
return import_path.default.join(process.cwd(), dataDir, "ta-admin.json");
|
|
46
|
+
}
|
|
47
|
+
function loadAdmin() {
|
|
48
|
+
const filePath = adminFilePath();
|
|
49
|
+
if (!import_fs.default.existsSync(filePath)) return null;
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
var CIPHER = "aes-256-gcm";
|
|
57
|
+
function getEncryptionKey() {
|
|
58
|
+
const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-fallback-key-change-me";
|
|
59
|
+
return import_crypto.default.createHash("sha256").update(secret).digest();
|
|
60
|
+
}
|
|
61
|
+
function decryptApiKey(encoded) {
|
|
62
|
+
try {
|
|
63
|
+
const iv = Buffer.from(encoded.slice(0, 24), "hex");
|
|
64
|
+
const tag = Buffer.from(encoded.slice(24, 56), "hex");
|
|
65
|
+
const encrypted = Buffer.from(encoded.slice(56), "hex");
|
|
66
|
+
const key = getEncryptionKey();
|
|
67
|
+
const decipher = import_crypto.default.createDecipheriv(CIPHER, key, iv);
|
|
68
|
+
decipher.setAuthTag(tag);
|
|
69
|
+
return decipher.update(encrypted) + decipher.final("utf8");
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function getApiKey() {
|
|
75
|
+
const record = loadAdmin();
|
|
76
|
+
if (!record?.apiKey) return null;
|
|
77
|
+
return decryptApiKey(record.apiKey);
|
|
78
|
+
}
|
|
79
|
+
function verifyApiKey(key) {
|
|
80
|
+
const stored = getApiKey();
|
|
81
|
+
if (!stored) return false;
|
|
82
|
+
if (key.length !== stored.length) return false;
|
|
83
|
+
return import_crypto.default.timingSafeEqual(Buffer.from(key), Buffer.from(stored));
|
|
84
|
+
}
|
|
85
|
+
function verifySession(token) {
|
|
86
|
+
const lastDot = token.lastIndexOf(".");
|
|
87
|
+
if (lastDot === -1) return false;
|
|
88
|
+
const payload = token.slice(0, lastDot);
|
|
89
|
+
const sig = token.slice(lastDot + 1);
|
|
90
|
+
const expected = import_crypto.default.createHmac("sha256", process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt").update(payload).digest("hex");
|
|
91
|
+
if (sig.length !== expected.length) return false;
|
|
92
|
+
return import_crypto.default.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/dashboard/auth.ts
|
|
96
|
+
var SESSION_COOKIE = "ta_session";
|
|
97
|
+
function checkApiAuth(req) {
|
|
98
|
+
const apiKeyHeader = req.headers.get("x-ta-api-key");
|
|
99
|
+
if (apiKeyHeader) return verifyApiKey(apiKeyHeader);
|
|
100
|
+
const auth = req.headers.get("authorization") ?? "";
|
|
101
|
+
if (auth.startsWith("Bearer ")) {
|
|
102
|
+
const token = auth.slice(7);
|
|
103
|
+
return verifyApiKey(token);
|
|
104
|
+
}
|
|
105
|
+
const session = req.cookies.get(SESSION_COOKIE)?.value;
|
|
106
|
+
if (session) return verifySession(session);
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
function unauthorizedResponse() {
|
|
110
|
+
return import_server.NextResponse.json(
|
|
111
|
+
{ error: "Unauthorized. Provide X-TA-Api-Key header or a valid session cookie." },
|
|
112
|
+
{
|
|
113
|
+
status: 401,
|
|
114
|
+
headers: { "WWW-Authenticate": 'Bearer realm="Third Audience API"' }
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
119
|
+
0 && (module.exports = {
|
|
120
|
+
checkApiAuth,
|
|
121
|
+
unauthorizedResponse
|
|
122
|
+
});
|
|
123
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/dashboard/auth.ts","../../src/dashboard/admin-store.ts"],"sourcesContent":["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":["path","fs","crypto","crypto","crypto","crypto"]}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// src/dashboard/auth.ts
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
|
|
4
|
+
// src/dashboard/admin-store.ts
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
function adminFilePath() {
|
|
9
|
+
const dataDir = process.env.TA_DATA_DIR ?? "data";
|
|
10
|
+
return path.join(process.cwd(), dataDir, "ta-admin.json");
|
|
11
|
+
}
|
|
12
|
+
function loadAdmin() {
|
|
13
|
+
const filePath = adminFilePath();
|
|
14
|
+
if (!fs.existsSync(filePath)) return null;
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
var CIPHER = "aes-256-gcm";
|
|
22
|
+
function getEncryptionKey() {
|
|
23
|
+
const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-fallback-key-change-me";
|
|
24
|
+
return crypto.createHash("sha256").update(secret).digest();
|
|
25
|
+
}
|
|
26
|
+
function decryptApiKey(encoded) {
|
|
27
|
+
try {
|
|
28
|
+
const iv = Buffer.from(encoded.slice(0, 24), "hex");
|
|
29
|
+
const tag = Buffer.from(encoded.slice(24, 56), "hex");
|
|
30
|
+
const encrypted = Buffer.from(encoded.slice(56), "hex");
|
|
31
|
+
const key = getEncryptionKey();
|
|
32
|
+
const decipher = crypto.createDecipheriv(CIPHER, key, iv);
|
|
33
|
+
decipher.setAuthTag(tag);
|
|
34
|
+
return decipher.update(encrypted) + decipher.final("utf8");
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function getApiKey() {
|
|
40
|
+
const record = loadAdmin();
|
|
41
|
+
if (!record?.apiKey) return null;
|
|
42
|
+
return decryptApiKey(record.apiKey);
|
|
43
|
+
}
|
|
44
|
+
function verifyApiKey(key) {
|
|
45
|
+
const stored = getApiKey();
|
|
46
|
+
if (!stored) return false;
|
|
47
|
+
if (key.length !== stored.length) return false;
|
|
48
|
+
return crypto.timingSafeEqual(Buffer.from(key), Buffer.from(stored));
|
|
49
|
+
}
|
|
50
|
+
function verifySession(token) {
|
|
51
|
+
const lastDot = token.lastIndexOf(".");
|
|
52
|
+
if (lastDot === -1) return false;
|
|
53
|
+
const payload = token.slice(0, lastDot);
|
|
54
|
+
const sig = token.slice(lastDot + 1);
|
|
55
|
+
const expected = crypto.createHmac("sha256", process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt").update(payload).digest("hex");
|
|
56
|
+
if (sig.length !== expected.length) return false;
|
|
57
|
+
return crypto.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/dashboard/auth.ts
|
|
61
|
+
var SESSION_COOKIE = "ta_session";
|
|
62
|
+
function checkApiAuth(req) {
|
|
63
|
+
const apiKeyHeader = req.headers.get("x-ta-api-key");
|
|
64
|
+
if (apiKeyHeader) return verifyApiKey(apiKeyHeader);
|
|
65
|
+
const auth = req.headers.get("authorization") ?? "";
|
|
66
|
+
if (auth.startsWith("Bearer ")) {
|
|
67
|
+
const token = auth.slice(7);
|
|
68
|
+
return verifyApiKey(token);
|
|
69
|
+
}
|
|
70
|
+
const session = req.cookies.get(SESSION_COOKIE)?.value;
|
|
71
|
+
if (session) return verifySession(session);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
function unauthorizedResponse() {
|
|
75
|
+
return NextResponse.json(
|
|
76
|
+
{ error: "Unauthorized. Provide X-TA-Api-Key header or a valid session cookie." },
|
|
77
|
+
{
|
|
78
|
+
status: 401,
|
|
79
|
+
headers: { "WWW-Authenticate": 'Bearer realm="Third Audience API"' }
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
checkApiAuth,
|
|
85
|
+
unauthorizedResponse
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=auth.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/dashboard/auth.ts","../../src/dashboard/admin-store.ts"],"sourcesContent":["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":";AACA,SAAS,oBAAoB;;;ACD7B,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,YAAY;AAUnB,SAAS,gBAAwB;AAC/B,QAAM,UAAU,QAAQ,IAAI,eAAe;AAC3C,SAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,SAAS,eAAe;AAC1D;AAWO,SAAS,YAAgC;AAC9C,QAAM,WAAW,cAAc;AAC/B,MAAI,CAAC,GAAG,WAAW,QAAQ,EAAG,QAAO;AACrC,MAAI;AACF,WAAO,KAAK,MAAM,GAAG,aAAa,UAAU,OAAO,CAAC;AAAA,EACtD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAoDA,IAAM,SAAS;AAEf,SAAS,mBAA2B;AAClC,QAAM,SAAS,QAAQ,IAAI,yBAAyB;AAEpD,SAAO,OAAO,WAAW,QAAQ,EAAE,OAAO,MAAM,EAAE,OAAO;AAC3D;AAYA,SAAS,cAAc,SAAgC;AACrD,MAAI;AACF,UAAM,KAAK,OAAO,KAAK,QAAQ,MAAM,GAAG,EAAE,GAAG,KAAK;AAClD,UAAM,MAAM,OAAO,KAAK,QAAQ,MAAM,IAAI,EAAE,GAAG,KAAK;AACpD,UAAM,YAAY,OAAO,KAAK,QAAQ,MAAM,EAAE,GAAG,KAAK;AACtD,UAAM,MAAM,iBAAiB;AAC7B,UAAM,WAAW,OAAO,iBAAiB,QAAQ,KAAK,EAAE;AACxD,aAAS,WAAW,GAAG;AACvB,WAAO,SAAS,OAAO,SAAS,IAAI,SAAS,MAAM,MAAM;AAAA,EAC3D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,YAA2B;AACzC,QAAM,SAAS,UAAU;AACzB,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAC5B,SAAO,cAAc,OAAO,MAAM;AACpC;AAUO,SAAS,aAAa,KAAsB;AACjD,QAAM,SAAS,UAAU;AACzB,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,IAAI,WAAW,OAAO,OAAQ,QAAO;AACzC,SAAO,OAAO,gBAAgB,OAAO,KAAK,GAAG,GAAG,OAAO,KAAK,MAAM,CAAC;AACrE;AAWO,SAAS,cAAc,OAAwB;AACpD,QAAM,UAAU,MAAM,YAAY,GAAG;AACrC,MAAI,YAAY,GAAI,QAAO;AAC3B,QAAM,UAAU,MAAM,MAAM,GAAG,OAAO;AACtC,QAAM,MAAM,MAAM,MAAM,UAAU,CAAC;AACnC,QAAM,WAAW,OAAO,WAAW,UAAU,QAAQ,IAAI,yBAAyB,SAAS,EACxF,OAAO,OAAO,EAAE,OAAO,KAAK;AAE/B,MAAI,IAAI,WAAW,SAAS,OAAQ,QAAO;AAC3C,SAAO,OAAO,gBAAgB,OAAO,KAAK,KAAK,KAAK,GAAG,OAAO,KAAK,UAAU,KAAK,CAAC;AACrF;;;AD9JA,IAAM,iBAAiB;AAQhB,SAAS,aAAa,KAA2B;AAEtD,QAAM,eAAe,IAAI,QAAQ,IAAI,cAAc;AACnD,MAAI,aAAc,QAAO,aAAa,YAAY;AAGlD,QAAM,OAAO,IAAI,QAAQ,IAAI,eAAe,KAAK;AACjD,MAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,UAAM,QAAQ,KAAK,MAAM,CAAC;AAC1B,WAAO,aAAa,KAAK;AAAA,EAC3B;AAGA,QAAM,UAAU,IAAI,QAAQ,IAAI,cAAc,GAAG;AACjD,MAAI,QAAS,QAAO,cAAc,OAAO;AAEzC,SAAO;AACT;AAMO,SAAS,uBAAqC;AACnD,SAAO,aAAa;AAAA,IAClB,EAAE,OAAO,uEAAuE;AAAA,IAChF;AAAA,MACE,QAAQ;AAAA,MACR,SAAS,EAAE,oBAAoB,oCAAoC;AAAA,IACrE;AAAA,EACF;AACF;","names":[]}
|