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.
Files changed (108) hide show
  1. package/CLAUDE.md +41 -0
  2. package/INSTALLATION.md +367 -0
  3. package/README.md +303 -0
  4. package/WORKLOG.md +162 -0
  5. package/dist/cli/index.d.mts +1 -0
  6. package/dist/cli/index.d.ts +1 -0
  7. package/dist/cli/index.js +208 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/cli/index.mjs +185 -0
  10. package/dist/cli/index.mjs.map +1 -0
  11. package/dist/dashboard/auth.d.mts +16 -0
  12. package/dist/dashboard/auth.d.ts +16 -0
  13. package/dist/dashboard/auth.js +123 -0
  14. package/dist/dashboard/auth.js.map +1 -0
  15. package/dist/dashboard/auth.mjs +87 -0
  16. package/dist/dashboard/auth.mjs.map +1 -0
  17. package/dist/dashboard/routes/analytics-api-route.d.mts +6 -0
  18. package/dist/dashboard/routes/analytics-api-route.d.ts +6 -0
  19. package/dist/dashboard/routes/analytics-api-route.js +180 -0
  20. package/dist/dashboard/routes/analytics-api-route.js.map +1 -0
  21. package/dist/dashboard/routes/analytics-api-route.mjs +145 -0
  22. package/dist/dashboard/routes/analytics-api-route.mjs.map +1 -0
  23. package/dist/dashboard/routes/api-key-route.d.mts +8 -0
  24. package/dist/dashboard/routes/api-key-route.d.ts +8 -0
  25. package/dist/dashboard/routes/api-key-route.js +173 -0
  26. package/dist/dashboard/routes/api-key-route.js.map +1 -0
  27. package/dist/dashboard/routes/api-key-route.mjs +137 -0
  28. package/dist/dashboard/routes/api-key-route.mjs.map +1 -0
  29. package/dist/dashboard/routes/citation-route.d.mts +14 -0
  30. package/dist/dashboard/routes/citation-route.d.ts +14 -0
  31. package/dist/dashboard/routes/citation-route.js +202 -0
  32. package/dist/dashboard/routes/citation-route.js.map +1 -0
  33. package/dist/dashboard/routes/citation-route.mjs +166 -0
  34. package/dist/dashboard/routes/citation-route.mjs.map +1 -0
  35. package/dist/dashboard/routes/llms-txt-route.d.mts +6 -0
  36. package/dist/dashboard/routes/llms-txt-route.d.ts +6 -0
  37. package/dist/dashboard/routes/llms-txt-route.js +119 -0
  38. package/dist/dashboard/routes/llms-txt-route.js.map +1 -0
  39. package/dist/dashboard/routes/llms-txt-route.mjs +84 -0
  40. package/dist/dashboard/routes/llms-txt-route.mjs.map +1 -0
  41. package/dist/dashboard/routes/login-route.d.mts +6 -0
  42. package/dist/dashboard/routes/login-route.d.ts +6 -0
  43. package/dist/dashboard/routes/login-route.js +313 -0
  44. package/dist/dashboard/routes/login-route.js.map +1 -0
  45. package/dist/dashboard/routes/login-route.mjs +284 -0
  46. package/dist/dashboard/routes/login-route.mjs.map +1 -0
  47. package/dist/dashboard/routes/markdown-route.d.mts +15 -0
  48. package/dist/dashboard/routes/markdown-route.d.ts +15 -0
  49. package/dist/dashboard/routes/markdown-route.js +239 -0
  50. package/dist/dashboard/routes/markdown-route.js.map +1 -0
  51. package/dist/dashboard/routes/markdown-route.mjs +204 -0
  52. package/dist/dashboard/routes/markdown-route.mjs.map +1 -0
  53. package/dist/dashboard/routes/okf-route.d.mts +13 -0
  54. package/dist/dashboard/routes/okf-route.d.ts +13 -0
  55. package/dist/dashboard/routes/okf-route.js +184 -0
  56. package/dist/dashboard/routes/okf-route.js.map +1 -0
  57. package/dist/dashboard/routes/okf-route.mjs +149 -0
  58. package/dist/dashboard/routes/okf-route.mjs.map +1 -0
  59. package/dist/dashboard/routes/sitemap-ai-route.d.mts +6 -0
  60. package/dist/dashboard/routes/sitemap-ai-route.d.ts +6 -0
  61. package/dist/dashboard/routes/sitemap-ai-route.js +134 -0
  62. package/dist/dashboard/routes/sitemap-ai-route.js.map +1 -0
  63. package/dist/dashboard/routes/sitemap-ai-route.mjs +99 -0
  64. package/dist/dashboard/routes/sitemap-ai-route.mjs.map +1 -0
  65. package/dist/dashboard/ui/components/Sidebar.d.mts +5 -0
  66. package/dist/dashboard/ui/components/Sidebar.d.ts +5 -0
  67. package/dist/dashboard/ui/components/Sidebar.js +102 -0
  68. package/dist/dashboard/ui/components/Sidebar.js.map +1 -0
  69. package/dist/dashboard/ui/components/Sidebar.mjs +68 -0
  70. package/dist/dashboard/ui/components/Sidebar.mjs.map +1 -0
  71. package/dist/dashboard/ui/globals.css +175 -0
  72. package/dist/dashboard/ui/pages/BotAnalyticsPage.d.mts +5 -0
  73. package/dist/dashboard/ui/pages/BotAnalyticsPage.d.ts +5 -0
  74. package/dist/dashboard/ui/pages/BotAnalyticsPage.js +269 -0
  75. package/dist/dashboard/ui/pages/BotAnalyticsPage.js.map +1 -0
  76. package/dist/dashboard/ui/pages/BotAnalyticsPage.mjs +232 -0
  77. package/dist/dashboard/ui/pages/BotAnalyticsPage.mjs.map +1 -0
  78. package/dist/dashboard/ui/pages/BotManagementPage.d.mts +13 -0
  79. package/dist/dashboard/ui/pages/BotManagementPage.d.ts +13 -0
  80. package/dist/dashboard/ui/pages/BotManagementPage.js +177 -0
  81. package/dist/dashboard/ui/pages/BotManagementPage.js.map +1 -0
  82. package/dist/dashboard/ui/pages/BotManagementPage.mjs +153 -0
  83. package/dist/dashboard/ui/pages/BotManagementPage.mjs.map +1 -0
  84. package/dist/dashboard/ui/pages/LlmTrafficPage.d.mts +5 -0
  85. package/dist/dashboard/ui/pages/LlmTrafficPage.d.ts +5 -0
  86. package/dist/dashboard/ui/pages/LlmTrafficPage.js +203 -0
  87. package/dist/dashboard/ui/pages/LlmTrafficPage.js.map +1 -0
  88. package/dist/dashboard/ui/pages/LlmTrafficPage.mjs +168 -0
  89. package/dist/dashboard/ui/pages/LlmTrafficPage.mjs.map +1 -0
  90. package/dist/dashboard/ui/pages/SettingsPage.d.mts +8 -0
  91. package/dist/dashboard/ui/pages/SettingsPage.d.ts +8 -0
  92. package/dist/dashboard/ui/pages/SettingsPage.js +181 -0
  93. package/dist/dashboard/ui/pages/SettingsPage.js.map +1 -0
  94. package/dist/dashboard/ui/pages/SettingsPage.mjs +157 -0
  95. package/dist/dashboard/ui/pages/SettingsPage.mjs.map +1 -0
  96. package/dist/dashboard/ui/pages/SystemHealthPage.d.mts +5 -0
  97. package/dist/dashboard/ui/pages/SystemHealthPage.d.ts +5 -0
  98. package/dist/dashboard/ui/pages/SystemHealthPage.js +183 -0
  99. package/dist/dashboard/ui/pages/SystemHealthPage.js.map +1 -0
  100. package/dist/dashboard/ui/pages/SystemHealthPage.mjs +148 -0
  101. package/dist/dashboard/ui/pages/SystemHealthPage.mjs.map +1 -0
  102. package/dist/index.d.mts +84 -0
  103. package/dist/index.d.ts +84 -0
  104. package/dist/index.js +372 -0
  105. package/dist/index.js.map +1 -0
  106. package/dist/index.mjs +346 -0
  107. package/dist/index.mjs.map +1 -0
  108. package/package.json +125 -0
@@ -0,0 +1,180 @@
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/analytics-api-route.ts
31
+ var analytics_api_route_exports = {};
32
+ __export(analytics_api_route_exports, {
33
+ GET: () => GET
34
+ });
35
+ module.exports = __toCommonJS(analytics_api_route_exports);
36
+ var import_server2 = require("next/server");
37
+
38
+ // src/analytics/performance-stats.ts
39
+ var import_fs = __toESM(require("fs"));
40
+ var import_path = __toESM(require("path"));
41
+ var PerformanceStats = class {
42
+ constructor(dataDir = process.env.TA_DATA_DIR ?? "data") {
43
+ this.dataDir = dataDir;
44
+ }
45
+ compute(days = 30) {
46
+ const records = this.loadRecords(days);
47
+ const totalVisits = records.length;
48
+ const uniqueBots = [...new Set(records.map((r) => r.bot_name).filter(Boolean))];
49
+ const withResponseMs = records.filter((r) => r.response_ms !== null);
50
+ const avgResponseMs = withResponseMs.length > 0 ? withResponseMs.reduce((s, r) => s + r.response_ms, 0) / withResponseMs.length : null;
51
+ const cacheHitRate = records.length > 0 ? records.filter((r) => r.cache_hit).length / records.length : null;
52
+ const pageCounts = /* @__PURE__ */ new Map();
53
+ const botCounts = /* @__PURE__ */ new Map();
54
+ const dayCounts = /* @__PURE__ */ new Map();
55
+ for (const r of records) {
56
+ pageCounts.set(r.url, (pageCounts.get(r.url) ?? 0) + 1);
57
+ const name = r.bot_name ?? "unknown";
58
+ botCounts.set(name, (botCounts.get(name) ?? 0) + 1);
59
+ const day = r.timestamp.slice(0, 10);
60
+ dayCounts.set(day, (dayCounts.get(day) ?? 0) + 1);
61
+ }
62
+ const topPages = [...pageCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([url, visits]) => ({ url, visits }));
63
+ const topBots = [...botCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([name, visits]) => ({ name, visits }));
64
+ const visitsByDay = [...dayCounts.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([date, visits]) => ({ date, visits }));
65
+ return { totalVisits, uniqueBots, avgResponseMs, cacheHitRate, topPages, topBots, visitsByDay };
66
+ }
67
+ loadRecords(days) {
68
+ const filePath = import_path.default.join(this.dataDir, "ta-visits.jsonl");
69
+ if (!import_fs.default.existsSync(filePath)) return [];
70
+ const cutoff = /* @__PURE__ */ new Date();
71
+ cutoff.setDate(cutoff.getDate() - days);
72
+ const cutoffStr = cutoff.toISOString();
73
+ return import_fs.default.readFileSync(filePath, "utf-8").split("\n").filter(Boolean).map((line) => {
74
+ try {
75
+ return JSON.parse(line);
76
+ } catch {
77
+ return null;
78
+ }
79
+ }).filter((r) => r !== null && r.timestamp >= cutoffStr);
80
+ }
81
+ };
82
+
83
+ // src/dashboard/auth.ts
84
+ var import_server = require("next/server");
85
+
86
+ // src/dashboard/admin-store.ts
87
+ var import_fs2 = __toESM(require("fs"));
88
+ var import_path2 = __toESM(require("path"));
89
+ var import_crypto = __toESM(require("crypto"));
90
+ function adminFilePath() {
91
+ const dataDir = process.env.TA_DATA_DIR ?? "data";
92
+ return import_path2.default.join(process.cwd(), dataDir, "ta-admin.json");
93
+ }
94
+ function loadAdmin() {
95
+ const filePath = adminFilePath();
96
+ if (!import_fs2.default.existsSync(filePath)) return null;
97
+ try {
98
+ return JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ var CIPHER = "aes-256-gcm";
104
+ function getEncryptionKey() {
105
+ const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-fallback-key-change-me";
106
+ return import_crypto.default.createHash("sha256").update(secret).digest();
107
+ }
108
+ function decryptApiKey(encoded) {
109
+ try {
110
+ const iv = Buffer.from(encoded.slice(0, 24), "hex");
111
+ const tag = Buffer.from(encoded.slice(24, 56), "hex");
112
+ const encrypted = Buffer.from(encoded.slice(56), "hex");
113
+ const key = getEncryptionKey();
114
+ const decipher = import_crypto.default.createDecipheriv(CIPHER, key, iv);
115
+ decipher.setAuthTag(tag);
116
+ return decipher.update(encrypted) + decipher.final("utf8");
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+ function getApiKey() {
122
+ const record = loadAdmin();
123
+ if (!record?.apiKey) return null;
124
+ return decryptApiKey(record.apiKey);
125
+ }
126
+ function verifyApiKey(key) {
127
+ const stored = getApiKey();
128
+ if (!stored) return false;
129
+ if (key.length !== stored.length) return false;
130
+ return import_crypto.default.timingSafeEqual(Buffer.from(key), Buffer.from(stored));
131
+ }
132
+ function verifySession(token) {
133
+ const lastDot = token.lastIndexOf(".");
134
+ if (lastDot === -1) return false;
135
+ const payload = token.slice(0, lastDot);
136
+ const sig = token.slice(lastDot + 1);
137
+ const expected = import_crypto.default.createHmac("sha256", process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt").update(payload).digest("hex");
138
+ if (sig.length !== expected.length) return false;
139
+ return import_crypto.default.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
140
+ }
141
+
142
+ // src/dashboard/auth.ts
143
+ var SESSION_COOKIE = "ta_session";
144
+ function checkApiAuth(req) {
145
+ const apiKeyHeader = req.headers.get("x-ta-api-key");
146
+ if (apiKeyHeader) return verifyApiKey(apiKeyHeader);
147
+ const auth = req.headers.get("authorization") ?? "";
148
+ if (auth.startsWith("Bearer ")) {
149
+ const token = auth.slice(7);
150
+ return verifyApiKey(token);
151
+ }
152
+ const session = req.cookies.get(SESSION_COOKIE)?.value;
153
+ if (session) return verifySession(session);
154
+ return false;
155
+ }
156
+ function unauthorizedResponse() {
157
+ return import_server.NextResponse.json(
158
+ { error: "Unauthorized. Provide X-TA-Api-Key header or a valid session cookie." },
159
+ {
160
+ status: 401,
161
+ headers: { "WWW-Authenticate": 'Bearer realm="Third Audience API"' }
162
+ }
163
+ );
164
+ }
165
+
166
+ // src/dashboard/routes/analytics-api-route.ts
167
+ var stats = new PerformanceStats();
168
+ async function GET(req) {
169
+ if (!checkApiAuth(req)) {
170
+ return unauthorizedResponse();
171
+ }
172
+ const days = parseInt(req.nextUrl.searchParams.get("days") ?? "30", 10);
173
+ const summary = stats.compute(days);
174
+ return import_server2.NextResponse.json(summary);
175
+ }
176
+ // Annotate the CommonJS export names for ESM import in node:
177
+ 0 && (module.exports = {
178
+ GET
179
+ });
180
+ //# sourceMappingURL=analytics-api-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/dashboard/routes/analytics-api-route.ts","../../../src/analytics/performance-stats.ts","../../../src/dashboard/auth.ts","../../../src/dashboard/admin-store.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport { PerformanceStats } from '../../analytics/performance-stats.js'\nimport { checkApiAuth, unauthorizedResponse } from '../auth.js'\n\nconst stats = new PerformanceStats()\n\n/** GET /api/third-audience/analytics?days=30 */\nexport async function GET(req: NextRequest) {\n if (!checkApiAuth(req)) {\n return unauthorizedResponse()\n }\n\n const days = parseInt(req.nextUrl.searchParams.get('days') ?? '30', 10)\n const summary = stats.compute(days)\n\n return NextResponse.json(summary)\n}\n","import fs from 'fs'\nimport path from 'path'\nimport type { VisitRecord } from './visit-tracker.js'\n\nexport interface PerformanceSummary {\n totalVisits: number\n uniqueBots: string[]\n avgResponseMs: number | null\n cacheHitRate: number | null\n topPages: Array<{ url: string; visits: number }>\n topBots: Array<{ name: string; visits: number }>\n visitsByDay: Array<{ date: string; visits: number }>\n}\n\nexport class PerformanceStats {\n private dataDir: string\n\n constructor(dataDir = process.env.TA_DATA_DIR ?? 'data') {\n this.dataDir = dataDir\n }\n\n compute(days = 30): PerformanceSummary {\n const records = this.loadRecords(days)\n\n const totalVisits = records.length\n const uniqueBots = [...new Set(records.map(r => r.bot_name).filter(Boolean))] as string[]\n\n const withResponseMs = records.filter(r => r.response_ms !== null)\n const avgResponseMs = withResponseMs.length > 0\n ? withResponseMs.reduce((s, r) => s + r.response_ms!, 0) / withResponseMs.length\n : null\n\n const cacheHitRate = records.length > 0\n ? records.filter(r => r.cache_hit).length / records.length\n : null\n\n const pageCounts = new Map<string, number>()\n const botCounts = new Map<string, number>()\n const dayCounts = new Map<string, number>()\n\n for (const r of records) {\n pageCounts.set(r.url, (pageCounts.get(r.url) ?? 0) + 1)\n const name = r.bot_name ?? 'unknown'\n botCounts.set(name, (botCounts.get(name) ?? 0) + 1)\n const day = r.timestamp.slice(0, 10)\n dayCounts.set(day, (dayCounts.get(day) ?? 0) + 1)\n }\n\n const topPages = [...pageCounts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 10)\n .map(([url, visits]) => ({ url, visits }))\n\n const topBots = [...botCounts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 10)\n .map(([name, visits]) => ({ name, visits }))\n\n const visitsByDay = [...dayCounts.entries()]\n .sort((a, b) => a[0].localeCompare(b[0]))\n .map(([date, visits]) => ({ date, visits }))\n\n return { totalVisits, uniqueBots, avgResponseMs, cacheHitRate, topPages, topBots, visitsByDay }\n }\n\n private loadRecords(days: number): VisitRecord[] {\n const filePath = path.join(this.dataDir, 'ta-visits.jsonl')\n if (!fs.existsSync(filePath)) return []\n\n const cutoff = new Date()\n cutoff.setDate(cutoff.getDate() - days)\n const cutoffStr = cutoff.toISOString()\n\n return fs.readFileSync(filePath, 'utf-8')\n .split('\\n')\n .filter(Boolean)\n .map(line => { try { return JSON.parse(line) as VisitRecord } catch { return null } })\n .filter((r): r is VisitRecord => r !== null && r.timestamp >= cutoffStr)\n }\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_server","path","fs","import_fs","import_path","path","fs","crypto","crypto","crypto","crypto"]}
@@ -0,0 +1,145 @@
1
+ // src/dashboard/routes/analytics-api-route.ts
2
+ import { NextResponse as NextResponse2 } from "next/server";
3
+
4
+ // src/analytics/performance-stats.ts
5
+ import fs from "fs";
6
+ import path from "path";
7
+ var PerformanceStats = class {
8
+ constructor(dataDir = process.env.TA_DATA_DIR ?? "data") {
9
+ this.dataDir = dataDir;
10
+ }
11
+ compute(days = 30) {
12
+ const records = this.loadRecords(days);
13
+ const totalVisits = records.length;
14
+ const uniqueBots = [...new Set(records.map((r) => r.bot_name).filter(Boolean))];
15
+ const withResponseMs = records.filter((r) => r.response_ms !== null);
16
+ const avgResponseMs = withResponseMs.length > 0 ? withResponseMs.reduce((s, r) => s + r.response_ms, 0) / withResponseMs.length : null;
17
+ const cacheHitRate = records.length > 0 ? records.filter((r) => r.cache_hit).length / records.length : null;
18
+ const pageCounts = /* @__PURE__ */ new Map();
19
+ const botCounts = /* @__PURE__ */ new Map();
20
+ const dayCounts = /* @__PURE__ */ new Map();
21
+ for (const r of records) {
22
+ pageCounts.set(r.url, (pageCounts.get(r.url) ?? 0) + 1);
23
+ const name = r.bot_name ?? "unknown";
24
+ botCounts.set(name, (botCounts.get(name) ?? 0) + 1);
25
+ const day = r.timestamp.slice(0, 10);
26
+ dayCounts.set(day, (dayCounts.get(day) ?? 0) + 1);
27
+ }
28
+ const topPages = [...pageCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([url, visits]) => ({ url, visits }));
29
+ const topBots = [...botCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([name, visits]) => ({ name, visits }));
30
+ const visitsByDay = [...dayCounts.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([date, visits]) => ({ date, visits }));
31
+ return { totalVisits, uniqueBots, avgResponseMs, cacheHitRate, topPages, topBots, visitsByDay };
32
+ }
33
+ loadRecords(days) {
34
+ const filePath = path.join(this.dataDir, "ta-visits.jsonl");
35
+ if (!fs.existsSync(filePath)) return [];
36
+ const cutoff = /* @__PURE__ */ new Date();
37
+ cutoff.setDate(cutoff.getDate() - days);
38
+ const cutoffStr = cutoff.toISOString();
39
+ return fs.readFileSync(filePath, "utf-8").split("\n").filter(Boolean).map((line) => {
40
+ try {
41
+ return JSON.parse(line);
42
+ } catch {
43
+ return null;
44
+ }
45
+ }).filter((r) => r !== null && r.timestamp >= cutoffStr);
46
+ }
47
+ };
48
+
49
+ // src/dashboard/auth.ts
50
+ import { NextResponse } from "next/server";
51
+
52
+ // src/dashboard/admin-store.ts
53
+ import fs2 from "fs";
54
+ import path2 from "path";
55
+ import crypto from "crypto";
56
+ function adminFilePath() {
57
+ const dataDir = process.env.TA_DATA_DIR ?? "data";
58
+ return path2.join(process.cwd(), dataDir, "ta-admin.json");
59
+ }
60
+ function loadAdmin() {
61
+ const filePath = adminFilePath();
62
+ if (!fs2.existsSync(filePath)) return null;
63
+ try {
64
+ return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+ var CIPHER = "aes-256-gcm";
70
+ function getEncryptionKey() {
71
+ const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-fallback-key-change-me";
72
+ return crypto.createHash("sha256").update(secret).digest();
73
+ }
74
+ function decryptApiKey(encoded) {
75
+ try {
76
+ const iv = Buffer.from(encoded.slice(0, 24), "hex");
77
+ const tag = Buffer.from(encoded.slice(24, 56), "hex");
78
+ const encrypted = Buffer.from(encoded.slice(56), "hex");
79
+ const key = getEncryptionKey();
80
+ const decipher = crypto.createDecipheriv(CIPHER, key, iv);
81
+ decipher.setAuthTag(tag);
82
+ return decipher.update(encrypted) + decipher.final("utf8");
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+ function getApiKey() {
88
+ const record = loadAdmin();
89
+ if (!record?.apiKey) return null;
90
+ return decryptApiKey(record.apiKey);
91
+ }
92
+ function verifyApiKey(key) {
93
+ const stored = getApiKey();
94
+ if (!stored) return false;
95
+ if (key.length !== stored.length) return false;
96
+ return crypto.timingSafeEqual(Buffer.from(key), Buffer.from(stored));
97
+ }
98
+ function verifySession(token) {
99
+ const lastDot = token.lastIndexOf(".");
100
+ if (lastDot === -1) return false;
101
+ const payload = token.slice(0, lastDot);
102
+ const sig = token.slice(lastDot + 1);
103
+ const expected = crypto.createHmac("sha256", process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt").update(payload).digest("hex");
104
+ if (sig.length !== expected.length) return false;
105
+ return crypto.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
106
+ }
107
+
108
+ // src/dashboard/auth.ts
109
+ var SESSION_COOKIE = "ta_session";
110
+ function checkApiAuth(req) {
111
+ const apiKeyHeader = req.headers.get("x-ta-api-key");
112
+ if (apiKeyHeader) return verifyApiKey(apiKeyHeader);
113
+ const auth = req.headers.get("authorization") ?? "";
114
+ if (auth.startsWith("Bearer ")) {
115
+ const token = auth.slice(7);
116
+ return verifyApiKey(token);
117
+ }
118
+ const session = req.cookies.get(SESSION_COOKIE)?.value;
119
+ if (session) return verifySession(session);
120
+ return false;
121
+ }
122
+ function unauthorizedResponse() {
123
+ return NextResponse.json(
124
+ { error: "Unauthorized. Provide X-TA-Api-Key header or a valid session cookie." },
125
+ {
126
+ status: 401,
127
+ headers: { "WWW-Authenticate": 'Bearer realm="Third Audience API"' }
128
+ }
129
+ );
130
+ }
131
+
132
+ // src/dashboard/routes/analytics-api-route.ts
133
+ var stats = new PerformanceStats();
134
+ async function GET(req) {
135
+ if (!checkApiAuth(req)) {
136
+ return unauthorizedResponse();
137
+ }
138
+ const days = parseInt(req.nextUrl.searchParams.get("days") ?? "30", 10);
139
+ const summary = stats.compute(days);
140
+ return NextResponse2.json(summary);
141
+ }
142
+ export {
143
+ GET
144
+ };
145
+ //# sourceMappingURL=analytics-api-route.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/dashboard/routes/analytics-api-route.ts","../../../src/analytics/performance-stats.ts","../../../src/dashboard/auth.ts","../../../src/dashboard/admin-store.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport { PerformanceStats } from '../../analytics/performance-stats.js'\nimport { checkApiAuth, unauthorizedResponse } from '../auth.js'\n\nconst stats = new PerformanceStats()\n\n/** GET /api/third-audience/analytics?days=30 */\nexport async function GET(req: NextRequest) {\n if (!checkApiAuth(req)) {\n return unauthorizedResponse()\n }\n\n const days = parseInt(req.nextUrl.searchParams.get('days') ?? '30', 10)\n const summary = stats.compute(days)\n\n return NextResponse.json(summary)\n}\n","import fs from 'fs'\nimport path from 'path'\nimport type { VisitRecord } from './visit-tracker.js'\n\nexport interface PerformanceSummary {\n totalVisits: number\n uniqueBots: string[]\n avgResponseMs: number | null\n cacheHitRate: number | null\n topPages: Array<{ url: string; visits: number }>\n topBots: Array<{ name: string; visits: number }>\n visitsByDay: Array<{ date: string; visits: number }>\n}\n\nexport class PerformanceStats {\n private dataDir: string\n\n constructor(dataDir = process.env.TA_DATA_DIR ?? 'data') {\n this.dataDir = dataDir\n }\n\n compute(days = 30): PerformanceSummary {\n const records = this.loadRecords(days)\n\n const totalVisits = records.length\n const uniqueBots = [...new Set(records.map(r => r.bot_name).filter(Boolean))] as string[]\n\n const withResponseMs = records.filter(r => r.response_ms !== null)\n const avgResponseMs = withResponseMs.length > 0\n ? withResponseMs.reduce((s, r) => s + r.response_ms!, 0) / withResponseMs.length\n : null\n\n const cacheHitRate = records.length > 0\n ? records.filter(r => r.cache_hit).length / records.length\n : null\n\n const pageCounts = new Map<string, number>()\n const botCounts = new Map<string, number>()\n const dayCounts = new Map<string, number>()\n\n for (const r of records) {\n pageCounts.set(r.url, (pageCounts.get(r.url) ?? 0) + 1)\n const name = r.bot_name ?? 'unknown'\n botCounts.set(name, (botCounts.get(name) ?? 0) + 1)\n const day = r.timestamp.slice(0, 10)\n dayCounts.set(day, (dayCounts.get(day) ?? 0) + 1)\n }\n\n const topPages = [...pageCounts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 10)\n .map(([url, visits]) => ({ url, visits }))\n\n const topBots = [...botCounts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 10)\n .map(([name, visits]) => ({ name, visits }))\n\n const visitsByDay = [...dayCounts.entries()]\n .sort((a, b) => a[0].localeCompare(b[0]))\n .map(([date, visits]) => ({ date, visits }))\n\n return { totalVisits, uniqueBots, avgResponseMs, cacheHitRate, topPages, topBots, visitsByDay }\n }\n\n private loadRecords(days: number): VisitRecord[] {\n const filePath = path.join(this.dataDir, 'ta-visits.jsonl')\n if (!fs.existsSync(filePath)) return []\n\n const cutoff = new Date()\n cutoff.setDate(cutoff.getDate() - days)\n const cutoffStr = cutoff.toISOString()\n\n return fs.readFileSync(filePath, 'utf-8')\n .split('\\n')\n .filter(Boolean)\n .map(line => { try { return JSON.parse(line) as VisitRecord } catch { return null } })\n .filter((r): r is VisitRecord => r !== null && r.timestamp >= cutoffStr)\n }\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":["NextResponse","fs","path","fs","NextResponse"]}
@@ -0,0 +1,8 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ /** GET /api/third-audience/api-key — returns masked key for display */
4
+ declare function GET(req: NextRequest): Promise<NextResponse>;
5
+ /** POST /api/third-audience/api-key — rotate (regenerate) the API key */
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/api-key — returns masked key for display */
4
+ declare function GET(req: NextRequest): Promise<NextResponse>;
5
+ /** POST /api/third-audience/api-key — rotate (regenerate) the API key */
6
+ declare function POST(req: NextRequest): Promise<NextResponse>;
7
+
8
+ export { GET, POST };
@@ -0,0 +1,173 @@
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/api-key-route.ts
31
+ var api_key_route_exports = {};
32
+ __export(api_key_route_exports, {
33
+ GET: () => GET,
34
+ POST: () => POST
35
+ });
36
+ module.exports = __toCommonJS(api_key_route_exports);
37
+ var import_server2 = require("next/server");
38
+
39
+ // src/dashboard/auth.ts
40
+ var import_server = require("next/server");
41
+
42
+ // src/dashboard/admin-store.ts
43
+ var import_fs = __toESM(require("fs"));
44
+ var import_path = __toESM(require("path"));
45
+ var import_crypto = __toESM(require("crypto"));
46
+ function adminFilePath() {
47
+ const dataDir = process.env.TA_DATA_DIR ?? "data";
48
+ return import_path.default.join(process.cwd(), dataDir, "ta-admin.json");
49
+ }
50
+ function loadAdmin() {
51
+ const filePath = adminFilePath();
52
+ if (!import_fs.default.existsSync(filePath)) return null;
53
+ try {
54
+ return JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+ function saveAdmin(record) {
60
+ const filePath = adminFilePath();
61
+ const dir = import_path.default.dirname(filePath);
62
+ if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
63
+ import_fs.default.writeFileSync(filePath, JSON.stringify(record, null, 2), "utf-8");
64
+ }
65
+ var CIPHER = "aes-256-gcm";
66
+ function getEncryptionKey() {
67
+ const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-fallback-key-change-me";
68
+ return import_crypto.default.createHash("sha256").update(secret).digest();
69
+ }
70
+ function encryptApiKey(plaintext) {
71
+ const iv = import_crypto.default.randomBytes(12);
72
+ const key = getEncryptionKey();
73
+ const cipher = import_crypto.default.createCipheriv(CIPHER, key, iv);
74
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
75
+ const tag = cipher.getAuthTag();
76
+ return iv.toString("hex") + tag.toString("hex") + encrypted.toString("hex");
77
+ }
78
+ function decryptApiKey(encoded) {
79
+ try {
80
+ const iv = Buffer.from(encoded.slice(0, 24), "hex");
81
+ const tag = Buffer.from(encoded.slice(24, 56), "hex");
82
+ const encrypted = Buffer.from(encoded.slice(56), "hex");
83
+ const key = getEncryptionKey();
84
+ const decipher = import_crypto.default.createDecipheriv(CIPHER, key, iv);
85
+ decipher.setAuthTag(tag);
86
+ return decipher.update(encrypted) + decipher.final("utf8");
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+ function generateApiKey() {
92
+ return "ta_" + import_crypto.default.randomBytes(24).toString("hex");
93
+ }
94
+ function getApiKey() {
95
+ const record = loadAdmin();
96
+ if (!record?.apiKey) return null;
97
+ return decryptApiKey(record.apiKey);
98
+ }
99
+ function rotateApiKey() {
100
+ const record = loadAdmin();
101
+ if (!record) throw new Error("Admin store not initialised");
102
+ const newKey = generateApiKey();
103
+ saveAdmin({ ...record, apiKey: encryptApiKey(newKey) });
104
+ return newKey;
105
+ }
106
+ function verifyApiKey(key) {
107
+ const stored = getApiKey();
108
+ if (!stored) return false;
109
+ if (key.length !== stored.length) return false;
110
+ return import_crypto.default.timingSafeEqual(Buffer.from(key), Buffer.from(stored));
111
+ }
112
+ function verifySession(token) {
113
+ const lastDot = token.lastIndexOf(".");
114
+ if (lastDot === -1) return false;
115
+ const payload = token.slice(0, lastDot);
116
+ const sig = token.slice(lastDot + 1);
117
+ const expected = import_crypto.default.createHmac("sha256", process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt").update(payload).digest("hex");
118
+ if (sig.length !== expected.length) return false;
119
+ return import_crypto.default.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
120
+ }
121
+
122
+ // src/dashboard/auth.ts
123
+ var SESSION_COOKIE = "ta_session";
124
+ function checkApiAuth(req) {
125
+ const apiKeyHeader = req.headers.get("x-ta-api-key");
126
+ if (apiKeyHeader) return verifyApiKey(apiKeyHeader);
127
+ const auth = req.headers.get("authorization") ?? "";
128
+ if (auth.startsWith("Bearer ")) {
129
+ const token = auth.slice(7);
130
+ return verifyApiKey(token);
131
+ }
132
+ const session = req.cookies.get(SESSION_COOKIE)?.value;
133
+ if (session) return verifySession(session);
134
+ return false;
135
+ }
136
+ function unauthorizedResponse() {
137
+ return import_server.NextResponse.json(
138
+ { error: "Unauthorized. Provide X-TA-Api-Key header or a valid session cookie." },
139
+ {
140
+ status: 401,
141
+ headers: { "WWW-Authenticate": 'Bearer realm="Third Audience API"' }
142
+ }
143
+ );
144
+ }
145
+
146
+ // src/dashboard/routes/api-key-route.ts
147
+ async function GET(req) {
148
+ if (!checkApiAuth(req)) return unauthorizedResponse();
149
+ const key = getApiKey();
150
+ if (!key) {
151
+ return import_server2.NextResponse.json({ key: null, masked: null });
152
+ }
153
+ const masked = key.slice(0, 8) + "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" + key.slice(-4);
154
+ return import_server2.NextResponse.json({ masked, prefix: key.slice(0, 3) });
155
+ }
156
+ async function POST(req) {
157
+ if (!checkApiAuth(req)) return unauthorizedResponse();
158
+ const body = await req.json().catch(() => ({}));
159
+ if (body.action !== "rotate") {
160
+ return import_server2.NextResponse.json({ error: 'Send { action: "rotate" }' }, { status: 400 });
161
+ }
162
+ const newKey = rotateApiKey();
163
+ return import_server2.NextResponse.json({
164
+ key: newKey,
165
+ message: "API key rotated. Copy it now \u2014 it will not be shown again."
166
+ });
167
+ }
168
+ // Annotate the CommonJS export names for ESM import in node:
169
+ 0 && (module.exports = {
170
+ GET,
171
+ POST
172
+ });
173
+ //# sourceMappingURL=api-key-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/dashboard/routes/api-key-route.ts","../../../src/dashboard/auth.ts","../../../src/dashboard/admin-store.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport { checkApiAuth, unauthorizedResponse } from '../auth.js'\nimport { getApiKey, rotateApiKey } from '../admin-store.js'\n\n/** GET /api/third-audience/api-key — returns masked key for display */\nexport async function GET(req: NextRequest): Promise<NextResponse> {\n if (!checkApiAuth(req)) return unauthorizedResponse()\n\n const key = getApiKey()\n if (!key) {\n return NextResponse.json({ key: null, masked: null })\n }\n\n // Show first 8 + last 4 chars, mask the middle — same pattern as WP settings page\n const masked = key.slice(0, 8) + '••••••••••••••••••••••••••••••••••••••' + key.slice(-4)\n return NextResponse.json({ masked, prefix: key.slice(0, 3) })\n}\n\n/** POST /api/third-audience/api-key — rotate (regenerate) the API key */\nexport async function POST(req: NextRequest): Promise<NextResponse> {\n if (!checkApiAuth(req)) return unauthorizedResponse()\n\n const body = await req.json().catch(() => ({})) as Record<string, unknown>\n if (body.action !== 'rotate') {\n return NextResponse.json({ error: 'Send { action: \"rotate\" }' }, { status: 400 })\n }\n\n const newKey = rotateApiKey()\n return NextResponse.json({\n key: newKey,\n message: 'API key rotated. Copy it now — it will not be shown again.',\n })\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,iBAA+C;;;ACC/C,oBAA6B;;;ACD7B,gBAAe;AACf,kBAAiB;AACjB,oBAAmB;AAUnB,SAAS,gBAAwB;AAC/B,QAAM,UAAU,QAAQ,IAAI,eAAe;AAC3C,SAAO,YAAAC,QAAK,KAAK,QAAQ,IAAI,GAAG,SAAS,eAAe;AAC1D;AAWO,SAAS,YAAgC;AAC9C,QAAM,WAAW,cAAc;AAC/B,MAAI,CAAC,UAAAC,QAAG,WAAW,QAAQ,EAAG,QAAO;AACrC,MAAI;AACF,WAAO,KAAK,MAAM,UAAAA,QAAG,aAAa,UAAU,OAAO,CAAC;AAAA,EACtD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,UAAU,QAA2B;AACnD,QAAM,WAAW,cAAc;AAC/B,QAAM,MAAM,YAAAC,QAAK,QAAQ,QAAQ;AACjC,MAAI,CAAC,UAAAD,QAAG,WAAW,GAAG,EAAG,WAAAA,QAAG,UAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAC9D,YAAAA,QAAG,cAAc,UAAU,KAAK,UAAU,QAAQ,MAAM,CAAC,GAAG,OAAO;AACrE;AA6CA,IAAM,SAAS;AAEf,SAAS,mBAA2B;AAClC,QAAM,SAAS,QAAQ,IAAI,yBAAyB;AAEpD,SAAO,cAAAE,QAAO,WAAW,QAAQ,EAAE,OAAO,MAAM,EAAE,OAAO;AAC3D;AAEA,SAAS,cAAc,WAA2B;AAChD,QAAM,KAAK,cAAAA,QAAO,YAAY,EAAE;AAChC,QAAM,MAAM,iBAAiB;AAC7B,QAAM,SAAS,cAAAA,QAAO,eAAe,QAAQ,KAAK,EAAE;AACpD,QAAM,YAAY,OAAO,OAAO,CAAC,OAAO,OAAO,WAAW,MAAM,GAAG,OAAO,MAAM,CAAC,CAAC;AAClF,QAAM,MAAM,OAAO,WAAW;AAE9B,SAAO,GAAG,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,IAAI,UAAU,SAAS,KAAK;AAC5E;AAEA,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,cAAAA,QAAO,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;AAEO,SAAS,iBAAyB;AACvC,SAAO,QAAQ,cAAAA,QAAO,YAAY,EAAE,EAAE,SAAS,KAAK;AACtD;AAEO,SAAS,YAA2B;AACzC,QAAM,SAAS,UAAU;AACzB,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAC5B,SAAO,cAAc,OAAO,MAAM;AACpC;AAEO,SAAS,eAAuB;AACrC,QAAM,SAAS,UAAU;AACzB,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,6BAA6B;AAC1D,QAAM,SAAS,eAAe;AAC9B,YAAU,EAAE,GAAG,QAAQ,QAAQ,cAAc,MAAM,EAAE,CAAC;AACtD,SAAO;AACT;AAEO,SAAS,aAAa,KAAsB;AACjD,QAAM,SAAS,UAAU;AACzB,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,IAAI,WAAW,OAAO,OAAQ,QAAO;AACzC,SAAO,cAAAA,QAAO,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,cAAAC,QAAO,WAAW,UAAU,QAAQ,IAAI,yBAAyB,SAAS,EACxF,OAAO,OAAO,EAAE,OAAO,KAAK;AAE/B,MAAI,IAAI,WAAW,SAAS,OAAQ,QAAO;AAC3C,SAAO,cAAAA,QAAO,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,2BAAa;AAAA,IAClB,EAAE,OAAO,uEAAuE;AAAA,IAChF;AAAA,MACE,QAAQ;AAAA,MACR,SAAS,EAAE,oBAAoB,oCAAoC;AAAA,IACrE;AAAA,EACF;AACF;;;ADtCA,eAAsB,IAAI,KAAyC;AACjE,MAAI,CAAC,aAAa,GAAG,EAAG,QAAO,qBAAqB;AAEpD,QAAM,MAAM,UAAU;AACtB,MAAI,CAAC,KAAK;AACR,WAAO,4BAAa,KAAK,EAAE,KAAK,MAAM,QAAQ,KAAK,CAAC;AAAA,EACtD;AAGA,QAAM,SAAS,IAAI,MAAM,GAAG,CAAC,IAAI,yOAA2C,IAAI,MAAM,EAAE;AACxF,SAAO,4BAAa,KAAK,EAAE,QAAQ,QAAQ,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;AAC9D;AAGA,eAAsB,KAAK,KAAyC;AAClE,MAAI,CAAC,aAAa,GAAG,EAAG,QAAO,qBAAqB;AAEpD,QAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAC9C,MAAI,KAAK,WAAW,UAAU;AAC5B,WAAO,4BAAa,KAAK,EAAE,OAAO,4BAA4B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClF;AAEA,QAAM,SAAS,aAAa;AAC5B,SAAO,4BAAa,KAAK;AAAA,IACvB,KAAK;AAAA,IACL,SAAS;AAAA,EACX,CAAC;AACH;","names":["import_server","path","fs","path","crypto","crypto"]}