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,137 @@
1
+ // src/dashboard/routes/api-key-route.ts
2
+ import { NextResponse as NextResponse2 } from "next/server";
3
+
4
+ // src/dashboard/auth.ts
5
+ import { NextResponse } from "next/server";
6
+
7
+ // src/dashboard/admin-store.ts
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import crypto from "crypto";
11
+ function adminFilePath() {
12
+ const dataDir = process.env.TA_DATA_DIR ?? "data";
13
+ return path.join(process.cwd(), dataDir, "ta-admin.json");
14
+ }
15
+ function loadAdmin() {
16
+ const filePath = adminFilePath();
17
+ if (!fs.existsSync(filePath)) return null;
18
+ try {
19
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+ function saveAdmin(record) {
25
+ const filePath = adminFilePath();
26
+ const dir = path.dirname(filePath);
27
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
28
+ fs.writeFileSync(filePath, JSON.stringify(record, null, 2), "utf-8");
29
+ }
30
+ var CIPHER = "aes-256-gcm";
31
+ function getEncryptionKey() {
32
+ const secret = process.env.THIRD_AUDIENCE_SECRET ?? "ta-fallback-key-change-me";
33
+ return crypto.createHash("sha256").update(secret).digest();
34
+ }
35
+ function encryptApiKey(plaintext) {
36
+ const iv = crypto.randomBytes(12);
37
+ const key = getEncryptionKey();
38
+ const cipher = crypto.createCipheriv(CIPHER, key, iv);
39
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
40
+ const tag = cipher.getAuthTag();
41
+ return iv.toString("hex") + tag.toString("hex") + encrypted.toString("hex");
42
+ }
43
+ function decryptApiKey(encoded) {
44
+ try {
45
+ const iv = Buffer.from(encoded.slice(0, 24), "hex");
46
+ const tag = Buffer.from(encoded.slice(24, 56), "hex");
47
+ const encrypted = Buffer.from(encoded.slice(56), "hex");
48
+ const key = getEncryptionKey();
49
+ const decipher = crypto.createDecipheriv(CIPHER, key, iv);
50
+ decipher.setAuthTag(tag);
51
+ return decipher.update(encrypted) + decipher.final("utf8");
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+ function generateApiKey() {
57
+ return "ta_" + crypto.randomBytes(24).toString("hex");
58
+ }
59
+ function getApiKey() {
60
+ const record = loadAdmin();
61
+ if (!record?.apiKey) return null;
62
+ return decryptApiKey(record.apiKey);
63
+ }
64
+ function rotateApiKey() {
65
+ const record = loadAdmin();
66
+ if (!record) throw new Error("Admin store not initialised");
67
+ const newKey = generateApiKey();
68
+ saveAdmin({ ...record, apiKey: encryptApiKey(newKey) });
69
+ return newKey;
70
+ }
71
+ function verifyApiKey(key) {
72
+ const stored = getApiKey();
73
+ if (!stored) return false;
74
+ if (key.length !== stored.length) return false;
75
+ return crypto.timingSafeEqual(Buffer.from(key), Buffer.from(stored));
76
+ }
77
+ function verifySession(token) {
78
+ const lastDot = token.lastIndexOf(".");
79
+ if (lastDot === -1) return false;
80
+ const payload = token.slice(0, lastDot);
81
+ const sig = token.slice(lastDot + 1);
82
+ const expected = crypto.createHmac("sha256", process.env.THIRD_AUDIENCE_SECRET ?? "ta-salt").update(payload).digest("hex");
83
+ if (sig.length !== expected.length) return false;
84
+ return crypto.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
85
+ }
86
+
87
+ // src/dashboard/auth.ts
88
+ var SESSION_COOKIE = "ta_session";
89
+ function checkApiAuth(req) {
90
+ const apiKeyHeader = req.headers.get("x-ta-api-key");
91
+ if (apiKeyHeader) return verifyApiKey(apiKeyHeader);
92
+ const auth = req.headers.get("authorization") ?? "";
93
+ if (auth.startsWith("Bearer ")) {
94
+ const token = auth.slice(7);
95
+ return verifyApiKey(token);
96
+ }
97
+ const session = req.cookies.get(SESSION_COOKIE)?.value;
98
+ if (session) return verifySession(session);
99
+ return false;
100
+ }
101
+ function unauthorizedResponse() {
102
+ return NextResponse.json(
103
+ { error: "Unauthorized. Provide X-TA-Api-Key header or a valid session cookie." },
104
+ {
105
+ status: 401,
106
+ headers: { "WWW-Authenticate": 'Bearer realm="Third Audience API"' }
107
+ }
108
+ );
109
+ }
110
+
111
+ // src/dashboard/routes/api-key-route.ts
112
+ async function GET(req) {
113
+ if (!checkApiAuth(req)) return unauthorizedResponse();
114
+ const key = getApiKey();
115
+ if (!key) {
116
+ return NextResponse2.json({ key: null, masked: null });
117
+ }
118
+ 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);
119
+ return NextResponse2.json({ masked, prefix: key.slice(0, 3) });
120
+ }
121
+ async function POST(req) {
122
+ if (!checkApiAuth(req)) return unauthorizedResponse();
123
+ const body = await req.json().catch(() => ({}));
124
+ if (body.action !== "rotate") {
125
+ return NextResponse2.json({ error: 'Send { action: "rotate" }' }, { status: 400 });
126
+ }
127
+ const newKey = rotateApiKey();
128
+ return NextResponse2.json({
129
+ key: newKey,
130
+ message: "API key rotated. Copy it now \u2014 it will not be shown again."
131
+ });
132
+ }
133
+ export {
134
+ GET,
135
+ POST
136
+ };
137
+ //# sourceMappingURL=api-key-route.mjs.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":"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","names":["NextResponse","NextResponse"]}
@@ -0,0 +1,14 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ /**
4
+ * POST /api/third-audience/citation
5
+ * Accepts client-side citation reports from citation-tracker.js
6
+ */
7
+ declare function POST(req: NextRequest): Promise<NextResponse<unknown>>;
8
+ /**
9
+ * GET /api/third-audience/citation
10
+ * Server-side detection — call from page route handlers if needed.
11
+ */
12
+ declare function GET(req: NextRequest): Promise<NextResponse<unknown>>;
13
+
14
+ export { GET, POST };
@@ -0,0 +1,14 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ /**
4
+ * POST /api/third-audience/citation
5
+ * Accepts client-side citation reports from citation-tracker.js
6
+ */
7
+ declare function POST(req: NextRequest): Promise<NextResponse<unknown>>;
8
+ /**
9
+ * GET /api/third-audience/citation
10
+ * Server-side detection — call from page route handlers if needed.
11
+ */
12
+ declare function GET(req: NextRequest): Promise<NextResponse<unknown>>;
13
+
14
+ export { GET, POST };
@@ -0,0 +1,202 @@
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/citation-route.ts
31
+ var citation_route_exports = {};
32
+ __export(citation_route_exports, {
33
+ GET: () => GET,
34
+ POST: () => POST
35
+ });
36
+ module.exports = __toCommonJS(citation_route_exports);
37
+ var import_server = require("next/server");
38
+
39
+ // src/citations/citation-tracker.ts
40
+ var import_fs = __toESM(require("fs"));
41
+ var import_path = __toESM(require("path"));
42
+ var AI_PLATFORMS = [
43
+ { name: "ChatGPT", patterns: [/chat\.openai\.com/i, /chatgpt\.com/i], queryParam: "q" },
44
+ { name: "Perplexity", patterns: [/perplexity\.ai/i], queryParam: "q" },
45
+ { name: "Claude", patterns: [/claude\.ai/i] },
46
+ { name: "Gemini", patterns: [/gemini\.google\.com/i, /bard\.google\.com/i] },
47
+ { name: "Copilot", patterns: [/copilot\.microsoft\.com/i, /bing\.com\/chat/i], queryParam: "q" },
48
+ { name: "YouChat", patterns: [/you\.com/i], queryParam: "q" }
49
+ ];
50
+ function detectAiPlatform(referer) {
51
+ if (!referer) return null;
52
+ let url;
53
+ try {
54
+ url = new URL(referer);
55
+ } catch {
56
+ return null;
57
+ }
58
+ for (const p of AI_PLATFORMS) {
59
+ if (p.patterns.some((rx) => rx.test(referer))) {
60
+ const query = p.queryParam ? url.searchParams.get(p.queryParam) : null;
61
+ return { platform: p.name, query };
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+ var CitationTracker = class {
67
+ constructor(dataDir = process.env.TA_DATA_DIR ?? "data") {
68
+ this.dataDir = dataDir;
69
+ }
70
+ /** Call from an API route or middleware to record a citation visit. */
71
+ record(req) {
72
+ const referer = req.headers.get("referer") ?? "";
73
+ const detection = detectAiPlatform(referer);
74
+ if (!detection) return null;
75
+ const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? req.headers.get("x-real-ip") ?? "unknown";
76
+ const record = {
77
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
78
+ platform: detection.platform,
79
+ query: detection.query,
80
+ url: req.nextUrl.pathname,
81
+ ip,
82
+ user_agent: req.headers.get("user-agent") ?? "",
83
+ referer
84
+ };
85
+ this.append(record);
86
+ return record;
87
+ }
88
+ /** Also handles client-side POSTs from citation-tracker.js */
89
+ recordFromBody(body) {
90
+ if (!body.platform || !body.url) return;
91
+ const record = {
92
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
93
+ platform: body.platform,
94
+ query: body.query ?? null,
95
+ url: body.url,
96
+ ip: body.ip ?? "client",
97
+ user_agent: body.user_agent ?? "",
98
+ referer: body.referer ?? ""
99
+ };
100
+ this.append(record);
101
+ }
102
+ append(record) {
103
+ try {
104
+ const filePath = import_path.default.join(this.dataDir, "ta-citations.jsonl");
105
+ import_fs.default.mkdirSync(this.dataDir, { recursive: true });
106
+ import_fs.default.appendFileSync(filePath, JSON.stringify(record) + "\n", "utf-8");
107
+ } catch {
108
+ }
109
+ }
110
+ };
111
+
112
+ // src/citations/citation-alerts.ts
113
+ var import_fs2 = __toESM(require("fs"));
114
+ var import_path2 = __toESM(require("path"));
115
+ var CitationAlerts = class {
116
+ constructor(dataDir = process.env.TA_DATA_DIR ?? "data") {
117
+ this.dataDir = dataDir;
118
+ }
119
+ /** Call after appending a new citation record. Returns any triggered alerts. */
120
+ check(newRecord) {
121
+ const alerts2 = [];
122
+ const history = this.loadRecent(24);
123
+ const platformHistory = this.loadAll().filter((r) => r.platform === newRecord.platform);
124
+ if (platformHistory.length === 1) {
125
+ alerts2.push({
126
+ type: "first_citation",
127
+ platform: newRecord.platform,
128
+ url: newRecord.url,
129
+ message: `First citation from ${newRecord.platform}!`,
130
+ timestamp: newRecord.timestamp
131
+ });
132
+ }
133
+ const recentPlatforms = new Set(this.loadRecent(30 * 24).map((r) => r.platform));
134
+ if (!recentPlatforms.has(newRecord.platform) && platformHistory.length > 1) {
135
+ alerts2.push({
136
+ type: "new_platform",
137
+ platform: newRecord.platform,
138
+ message: `${newRecord.platform} is citing your content again after a long absence.`,
139
+ timestamp: newRecord.timestamp
140
+ });
141
+ }
142
+ const hourly = history.filter((r) => r.platform === newRecord.platform);
143
+ const baseline = hourly.length > 0 ? hourly.length / 24 : 0;
144
+ const lastHourCount = history.filter(
145
+ (r) => r.platform === newRecord.platform && new Date(r.timestamp).getTime() > Date.now() - 36e5
146
+ ).length;
147
+ if (baseline > 2 && lastHourCount > baseline * 3) {
148
+ alerts2.push({
149
+ type: "citation_spike",
150
+ platform: newRecord.platform,
151
+ url: newRecord.url,
152
+ message: `Citation spike from ${newRecord.platform}: ${lastHourCount} in last hour (baseline: ${Math.round(baseline)}/hr)`,
153
+ timestamp: newRecord.timestamp
154
+ });
155
+ }
156
+ return alerts2;
157
+ }
158
+ loadAll() {
159
+ return this.loadLines(Infinity);
160
+ }
161
+ loadRecent(hours) {
162
+ return this.loadLines(hours);
163
+ }
164
+ loadLines(hours) {
165
+ const filePath = import_path2.default.join(this.dataDir, "ta-citations.jsonl");
166
+ if (!import_fs2.default.existsSync(filePath)) return [];
167
+ const cutoff = new Date(Date.now() - hours * 36e5).toISOString();
168
+ return import_fs2.default.readFileSync(filePath, "utf-8").split("\n").filter(Boolean).map((l) => {
169
+ try {
170
+ return JSON.parse(l);
171
+ } catch {
172
+ return null;
173
+ }
174
+ }).filter((r) => r !== null && (hours === Infinity || r.timestamp >= cutoff));
175
+ }
176
+ };
177
+
178
+ // src/dashboard/routes/citation-route.ts
179
+ var tracker = new CitationTracker();
180
+ var alerts = new CitationAlerts();
181
+ async function POST(req) {
182
+ try {
183
+ const body = await req.json();
184
+ tracker.recordFromBody(body);
185
+ return new import_server.NextResponse(null, { status: 204 });
186
+ } catch {
187
+ return new import_server.NextResponse(null, { status: 400 });
188
+ }
189
+ }
190
+ async function GET(req) {
191
+ const record = tracker.record(req);
192
+ if (record) {
193
+ alerts.check(record);
194
+ }
195
+ return new import_server.NextResponse(null, { status: 204 });
196
+ }
197
+ // Annotate the CommonJS export names for ESM import in node:
198
+ 0 && (module.exports = {
199
+ GET,
200
+ POST
201
+ });
202
+ //# sourceMappingURL=citation-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/dashboard/routes/citation-route.ts","../../../src/citations/citation-tracker.ts","../../../src/citations/citation-alerts.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport { CitationTracker } from '../../citations/citation-tracker.js'\nimport { CitationAlerts } from '../../citations/citation-alerts.js'\n\nconst tracker = new CitationTracker()\nconst alerts = new CitationAlerts()\n\n/**\n * POST /api/third-audience/citation\n * Accepts client-side citation reports from citation-tracker.js\n */\nexport async function POST(req: NextRequest) {\n try {\n const body = await req.json()\n tracker.recordFromBody(body)\n return new NextResponse(null, { status: 204 })\n } catch {\n return new NextResponse(null, { status: 400 })\n }\n}\n\n/**\n * GET /api/third-audience/citation\n * Server-side detection — call from page route handlers if needed.\n */\nexport async function GET(req: NextRequest) {\n const record = tracker.record(req)\n if (record) {\n alerts.check(record)\n }\n return new NextResponse(null, { status: 204 })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport type { NextRequest } from 'next/server'\n\nexport interface CitationRecord {\n timestamp: string\n platform: string\n query: string | null\n url: string\n ip: string\n user_agent: string\n referer: string\n}\n\n/** Platform detection from Referer URL */\nconst AI_PLATFORMS: Array<{ name: string; patterns: RegExp[]; queryParam?: string }> = [\n { name: 'ChatGPT', patterns: [/chat\\.openai\\.com/i, /chatgpt\\.com/i], queryParam: 'q' },\n { name: 'Perplexity', patterns: [/perplexity\\.ai/i], queryParam: 'q' },\n { name: 'Claude', patterns: [/claude\\.ai/i] },\n { name: 'Gemini', patterns: [/gemini\\.google\\.com/i, /bard\\.google\\.com/i] },\n { name: 'Copilot', patterns: [/copilot\\.microsoft\\.com/i, /bing\\.com\\/chat/i], queryParam: 'q' },\n { name: 'YouChat', patterns: [/you\\.com/i], queryParam: 'q' },\n]\n\nexport function detectAiPlatform(referer: string): { platform: string; query: string | null } | null {\n if (!referer) return null\n\n let url: URL\n try { url = new URL(referer) } catch { return null }\n\n for (const p of AI_PLATFORMS) {\n if (p.patterns.some(rx => rx.test(referer))) {\n const query = p.queryParam ? url.searchParams.get(p.queryParam) : null\n return { platform: p.name, query }\n }\n }\n return null\n}\n\nexport class CitationTracker {\n private dataDir: string\n\n constructor(dataDir = process.env.TA_DATA_DIR ?? 'data') {\n this.dataDir = dataDir\n }\n\n /** Call from an API route or middleware to record a citation visit. */\n record(req: NextRequest): CitationRecord | null {\n const referer = req.headers.get('referer') ?? ''\n const detection = detectAiPlatform(referer)\n if (!detection) return null\n\n const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim()\n ?? req.headers.get('x-real-ip')\n ?? 'unknown'\n\n const record: CitationRecord = {\n timestamp: new Date().toISOString(),\n platform: detection.platform,\n query: detection.query,\n url: req.nextUrl.pathname,\n ip,\n user_agent: req.headers.get('user-agent') ?? '',\n referer,\n }\n\n this.append(record)\n return record\n }\n\n /** Also handles client-side POSTs from citation-tracker.js */\n recordFromBody(body: Partial<CitationRecord>): void {\n if (!body.platform || !body.url) return\n const record: CitationRecord = {\n timestamp: new Date().toISOString(),\n platform: body.platform,\n query: body.query ?? null,\n url: body.url,\n ip: body.ip ?? 'client',\n user_agent: body.user_agent ?? '',\n referer: body.referer ?? '',\n }\n this.append(record)\n }\n\n private append(record: CitationRecord): void {\n try {\n const filePath = path.join(this.dataDir, 'ta-citations.jsonl')\n fs.mkdirSync(this.dataDir, { recursive: true })\n fs.appendFileSync(filePath, JSON.stringify(record) + '\\n', 'utf-8')\n } catch {\n // Never throw from tracking\n }\n }\n}\n","import fs from 'fs'\nimport path from 'path'\nimport type { CitationRecord } from './citation-tracker.js'\n\nexport interface CitationAlert {\n type: 'first_citation' | 'new_platform' | 'citation_spike' | 'high_performing_page'\n platform: string\n url?: string\n message: string\n timestamp: string\n}\n\nexport class CitationAlerts {\n private dataDir: string\n\n constructor(dataDir = process.env.TA_DATA_DIR ?? 'data') {\n this.dataDir = dataDir\n }\n\n /** Call after appending a new citation record. Returns any triggered alerts. */\n check(newRecord: CitationRecord): CitationAlert[] {\n const alerts: CitationAlert[] = []\n const history = this.loadRecent(24) // last 24h\n\n // First citation from this platform ever\n const platformHistory = this.loadAll().filter(r => r.platform === newRecord.platform)\n if (platformHistory.length === 1) {\n alerts.push({\n type: 'first_citation',\n platform: newRecord.platform,\n url: newRecord.url,\n message: `First citation from ${newRecord.platform}!`,\n timestamp: newRecord.timestamp,\n })\n }\n\n // New platform not seen in last 30 days\n const recentPlatforms = new Set(this.loadRecent(30 * 24).map(r => r.platform))\n if (!recentPlatforms.has(newRecord.platform) && platformHistory.length > 1) {\n alerts.push({\n type: 'new_platform',\n platform: newRecord.platform,\n message: `${newRecord.platform} is citing your content again after a long absence.`,\n timestamp: newRecord.timestamp,\n })\n }\n\n // Citation spike: >3× the hourly baseline\n const hourly = history.filter(r => r.platform === newRecord.platform)\n const baseline = hourly.length > 0 ? hourly.length / 24 : 0\n const lastHourCount = history.filter(r =>\n r.platform === newRecord.platform &&\n new Date(r.timestamp).getTime() > Date.now() - 3_600_000\n ).length\n if (baseline > 2 && lastHourCount > baseline * 3) {\n alerts.push({\n type: 'citation_spike',\n platform: newRecord.platform,\n url: newRecord.url,\n message: `Citation spike from ${newRecord.platform}: ${lastHourCount} in last hour (baseline: ${Math.round(baseline)}/hr)`,\n timestamp: newRecord.timestamp,\n })\n }\n\n return alerts\n }\n\n private loadAll(): CitationRecord[] {\n return this.loadLines(Infinity)\n }\n\n private loadRecent(hours: number): CitationRecord[] {\n return this.loadLines(hours)\n }\n\n private loadLines(hours: number): CitationRecord[] {\n const filePath = path.join(this.dataDir, 'ta-citations.jsonl')\n if (!fs.existsSync(filePath)) return []\n const cutoff = new Date(Date.now() - hours * 3_600_000).toISOString()\n return fs.readFileSync(filePath, 'utf-8')\n .split('\\n')\n .filter(Boolean)\n .map(l => { try { return JSON.parse(l) as CitationRecord } catch { return null } })\n .filter((r): r is CitationRecord => r !== null && (hours === Infinity || r.timestamp >= cutoff))\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAA+C;;;ACA/C,gBAAe;AACf,kBAAiB;AAcjB,IAAM,eAAiF;AAAA,EACrF,EAAE,MAAM,WAAc,UAAU,CAAC,sBAAsB,eAAe,GAAG,YAAY,IAAI;AAAA,EACzF,EAAE,MAAM,cAAc,UAAU,CAAC,iBAAiB,GAAsB,YAAY,IAAI;AAAA,EACxF,EAAE,MAAM,UAAc,UAAU,CAAC,aAAa,EAAE;AAAA,EAChD,EAAE,MAAM,UAAc,UAAU,CAAC,wBAAwB,oBAAoB,EAAE;AAAA,EAC/E,EAAE,MAAM,WAAc,UAAU,CAAC,4BAA4B,kBAAkB,GAAG,YAAY,IAAI;AAAA,EAClG,EAAE,MAAM,WAAc,UAAU,CAAC,WAAW,GAA6B,YAAY,IAAI;AAC3F;AAEO,SAAS,iBAAiB,SAAoE;AACnG,MAAI,CAAC,QAAS,QAAO;AAErB,MAAI;AACJ,MAAI;AAAE,UAAM,IAAI,IAAI,OAAO;AAAA,EAAE,QAAQ;AAAE,WAAO;AAAA,EAAK;AAEnD,aAAW,KAAK,cAAc;AAC5B,QAAI,EAAE,SAAS,KAAK,QAAM,GAAG,KAAK,OAAO,CAAC,GAAG;AAC3C,YAAM,QAAQ,EAAE,aAAa,IAAI,aAAa,IAAI,EAAE,UAAU,IAAI;AAClE,aAAO,EAAE,UAAU,EAAE,MAAM,MAAM;AAAA,IACnC;AAAA,EACF;AACA,SAAO;AACT;AAEO,IAAM,kBAAN,MAAsB;AAAA,EAG3B,YAAY,UAAU,QAAQ,IAAI,eAAe,QAAQ;AACvD,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA,EAGA,OAAO,KAAyC;AAC9C,UAAM,UAAU,IAAI,QAAQ,IAAI,SAAS,KAAK;AAC9C,UAAM,YAAY,iBAAiB,OAAO;AAC1C,QAAI,CAAC,UAAW,QAAO;AAEvB,UAAM,KAAK,IAAI,QAAQ,IAAI,iBAAiB,GAAG,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,KAC9D,IAAI,QAAQ,IAAI,WAAW,KAC3B;AAEL,UAAM,SAAyB;AAAA,MAC7B,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,UAAU,UAAU;AAAA,MACpB,OAAO,UAAU;AAAA,MACjB,KAAK,IAAI,QAAQ;AAAA,MACjB;AAAA,MACA,YAAY,IAAI,QAAQ,IAAI,YAAY,KAAK;AAAA,MAC7C;AAAA,IACF;AAEA,SAAK,OAAO,MAAM;AAClB,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,eAAe,MAAqC;AAClD,QAAI,CAAC,KAAK,YAAY,CAAC,KAAK,IAAK;AACjC,UAAM,SAAyB;AAAA,MAC7B,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,UAAU,KAAK;AAAA,MACf,OAAO,KAAK,SAAS;AAAA,MACrB,KAAK,KAAK;AAAA,MACV,IAAI,KAAK,MAAM;AAAA,MACf,YAAY,KAAK,cAAc;AAAA,MAC/B,SAAS,KAAK,WAAW;AAAA,IAC3B;AACA,SAAK,OAAO,MAAM;AAAA,EACpB;AAAA,EAEQ,OAAO,QAA8B;AAC3C,QAAI;AACF,YAAM,WAAW,YAAAA,QAAK,KAAK,KAAK,SAAS,oBAAoB;AAC7D,gBAAAC,QAAG,UAAU,KAAK,SAAS,EAAE,WAAW,KAAK,CAAC;AAC9C,gBAAAA,QAAG,eAAe,UAAU,KAAK,UAAU,MAAM,IAAI,MAAM,OAAO;AAAA,IACpE,QAAQ;AAAA,IAER;AAAA,EACF;AACF;;;AC9FA,IAAAC,aAAe;AACf,IAAAC,eAAiB;AAWV,IAAM,iBAAN,MAAqB;AAAA,EAG1B,YAAY,UAAU,QAAQ,IAAI,eAAe,QAAQ;AACvD,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA,EAGA,MAAM,WAA4C;AAChD,UAAMC,UAA0B,CAAC;AACjC,UAAM,UAAU,KAAK,WAAW,EAAE;AAGlC,UAAM,kBAAkB,KAAK,QAAQ,EAAE,OAAO,OAAK,EAAE,aAAa,UAAU,QAAQ;AACpF,QAAI,gBAAgB,WAAW,GAAG;AAChC,MAAAA,QAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN,UAAU,UAAU;AAAA,QACpB,KAAK,UAAU;AAAA,QACf,SAAS,uBAAuB,UAAU,QAAQ;AAAA,QAClD,WAAW,UAAU;AAAA,MACvB,CAAC;AAAA,IACH;AAGA,UAAM,kBAAkB,IAAI,IAAI,KAAK,WAAW,KAAK,EAAE,EAAE,IAAI,OAAK,EAAE,QAAQ,CAAC;AAC7E,QAAI,CAAC,gBAAgB,IAAI,UAAU,QAAQ,KAAK,gBAAgB,SAAS,GAAG;AAC1E,MAAAA,QAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN,UAAU,UAAU;AAAA,QACpB,SAAS,GAAG,UAAU,QAAQ;AAAA,QAC9B,WAAW,UAAU;AAAA,MACvB,CAAC;AAAA,IACH;AAGA,UAAM,SAAS,QAAQ,OAAO,OAAK,EAAE,aAAa,UAAU,QAAQ;AACpE,UAAM,WAAW,OAAO,SAAS,IAAI,OAAO,SAAS,KAAK;AAC1D,UAAM,gBAAgB,QAAQ;AAAA,MAAO,OACnC,EAAE,aAAa,UAAU,YACzB,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,IAAI,KAAK,IAAI,IAAI;AAAA,IACjD,EAAE;AACF,QAAI,WAAW,KAAK,gBAAgB,WAAW,GAAG;AAChD,MAAAA,QAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN,UAAU,UAAU;AAAA,QACpB,KAAK,UAAU;AAAA,QACf,SAAS,uBAAuB,UAAU,QAAQ,KAAK,aAAa,4BAA4B,KAAK,MAAM,QAAQ,CAAC;AAAA,QACpH,WAAW,UAAU;AAAA,MACvB,CAAC;AAAA,IACH;AAEA,WAAOA;AAAA,EACT;AAAA,EAEQ,UAA4B;AAClC,WAAO,KAAK,UAAU,QAAQ;AAAA,EAChC;AAAA,EAEQ,WAAW,OAAiC;AAClD,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B;AAAA,EAEQ,UAAU,OAAiC;AACjD,UAAM,WAAW,aAAAC,QAAK,KAAK,KAAK,SAAS,oBAAoB;AAC7D,QAAI,CAAC,WAAAC,QAAG,WAAW,QAAQ,EAAG,QAAO,CAAC;AACtC,UAAM,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,QAAQ,IAAS,EAAE,YAAY;AACpE,WAAO,WAAAA,QAAG,aAAa,UAAU,OAAO,EACrC,MAAM,IAAI,EACV,OAAO,OAAO,EACd,IAAI,OAAK;AAAE,UAAI;AAAE,eAAO,KAAK,MAAM,CAAC;AAAA,MAAoB,QAAQ;AAAE,eAAO;AAAA,MAAK;AAAA,IAAE,CAAC,EACjF,OAAO,CAAC,MAA2B,MAAM,SAAS,UAAU,YAAY,EAAE,aAAa,OAAO;AAAA,EACnG;AACF;;;AFjFA,IAAM,UAAU,IAAI,gBAAgB;AACpC,IAAM,SAAS,IAAI,eAAe;AAMlC,eAAsB,KAAK,KAAkB;AAC3C,MAAI;AACF,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,YAAQ,eAAe,IAAI;AAC3B,WAAO,IAAI,2BAAa,MAAM,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC/C,QAAQ;AACN,WAAO,IAAI,2BAAa,MAAM,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC/C;AACF;AAMA,eAAsB,IAAI,KAAkB;AAC1C,QAAM,SAAS,QAAQ,OAAO,GAAG;AACjC,MAAI,QAAQ;AACV,WAAO,MAAM,MAAM;AAAA,EACrB;AACA,SAAO,IAAI,2BAAa,MAAM,EAAE,QAAQ,IAAI,CAAC;AAC/C;","names":["path","fs","import_fs","import_path","alerts","path","fs"]}
@@ -0,0 +1,166 @@
1
+ // src/dashboard/routes/citation-route.ts
2
+ import { NextResponse } from "next/server";
3
+
4
+ // src/citations/citation-tracker.ts
5
+ import fs from "fs";
6
+ import path from "path";
7
+ var AI_PLATFORMS = [
8
+ { name: "ChatGPT", patterns: [/chat\.openai\.com/i, /chatgpt\.com/i], queryParam: "q" },
9
+ { name: "Perplexity", patterns: [/perplexity\.ai/i], queryParam: "q" },
10
+ { name: "Claude", patterns: [/claude\.ai/i] },
11
+ { name: "Gemini", patterns: [/gemini\.google\.com/i, /bard\.google\.com/i] },
12
+ { name: "Copilot", patterns: [/copilot\.microsoft\.com/i, /bing\.com\/chat/i], queryParam: "q" },
13
+ { name: "YouChat", patterns: [/you\.com/i], queryParam: "q" }
14
+ ];
15
+ function detectAiPlatform(referer) {
16
+ if (!referer) return null;
17
+ let url;
18
+ try {
19
+ url = new URL(referer);
20
+ } catch {
21
+ return null;
22
+ }
23
+ for (const p of AI_PLATFORMS) {
24
+ if (p.patterns.some((rx) => rx.test(referer))) {
25
+ const query = p.queryParam ? url.searchParams.get(p.queryParam) : null;
26
+ return { platform: p.name, query };
27
+ }
28
+ }
29
+ return null;
30
+ }
31
+ var CitationTracker = class {
32
+ constructor(dataDir = process.env.TA_DATA_DIR ?? "data") {
33
+ this.dataDir = dataDir;
34
+ }
35
+ /** Call from an API route or middleware to record a citation visit. */
36
+ record(req) {
37
+ const referer = req.headers.get("referer") ?? "";
38
+ const detection = detectAiPlatform(referer);
39
+ if (!detection) return null;
40
+ const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? req.headers.get("x-real-ip") ?? "unknown";
41
+ const record = {
42
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
43
+ platform: detection.platform,
44
+ query: detection.query,
45
+ url: req.nextUrl.pathname,
46
+ ip,
47
+ user_agent: req.headers.get("user-agent") ?? "",
48
+ referer
49
+ };
50
+ this.append(record);
51
+ return record;
52
+ }
53
+ /** Also handles client-side POSTs from citation-tracker.js */
54
+ recordFromBody(body) {
55
+ if (!body.platform || !body.url) return;
56
+ const record = {
57
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
58
+ platform: body.platform,
59
+ query: body.query ?? null,
60
+ url: body.url,
61
+ ip: body.ip ?? "client",
62
+ user_agent: body.user_agent ?? "",
63
+ referer: body.referer ?? ""
64
+ };
65
+ this.append(record);
66
+ }
67
+ append(record) {
68
+ try {
69
+ const filePath = path.join(this.dataDir, "ta-citations.jsonl");
70
+ fs.mkdirSync(this.dataDir, { recursive: true });
71
+ fs.appendFileSync(filePath, JSON.stringify(record) + "\n", "utf-8");
72
+ } catch {
73
+ }
74
+ }
75
+ };
76
+
77
+ // src/citations/citation-alerts.ts
78
+ import fs2 from "fs";
79
+ import path2 from "path";
80
+ var CitationAlerts = class {
81
+ constructor(dataDir = process.env.TA_DATA_DIR ?? "data") {
82
+ this.dataDir = dataDir;
83
+ }
84
+ /** Call after appending a new citation record. Returns any triggered alerts. */
85
+ check(newRecord) {
86
+ const alerts2 = [];
87
+ const history = this.loadRecent(24);
88
+ const platformHistory = this.loadAll().filter((r) => r.platform === newRecord.platform);
89
+ if (platformHistory.length === 1) {
90
+ alerts2.push({
91
+ type: "first_citation",
92
+ platform: newRecord.platform,
93
+ url: newRecord.url,
94
+ message: `First citation from ${newRecord.platform}!`,
95
+ timestamp: newRecord.timestamp
96
+ });
97
+ }
98
+ const recentPlatforms = new Set(this.loadRecent(30 * 24).map((r) => r.platform));
99
+ if (!recentPlatforms.has(newRecord.platform) && platformHistory.length > 1) {
100
+ alerts2.push({
101
+ type: "new_platform",
102
+ platform: newRecord.platform,
103
+ message: `${newRecord.platform} is citing your content again after a long absence.`,
104
+ timestamp: newRecord.timestamp
105
+ });
106
+ }
107
+ const hourly = history.filter((r) => r.platform === newRecord.platform);
108
+ const baseline = hourly.length > 0 ? hourly.length / 24 : 0;
109
+ const lastHourCount = history.filter(
110
+ (r) => r.platform === newRecord.platform && new Date(r.timestamp).getTime() > Date.now() - 36e5
111
+ ).length;
112
+ if (baseline > 2 && lastHourCount > baseline * 3) {
113
+ alerts2.push({
114
+ type: "citation_spike",
115
+ platform: newRecord.platform,
116
+ url: newRecord.url,
117
+ message: `Citation spike from ${newRecord.platform}: ${lastHourCount} in last hour (baseline: ${Math.round(baseline)}/hr)`,
118
+ timestamp: newRecord.timestamp
119
+ });
120
+ }
121
+ return alerts2;
122
+ }
123
+ loadAll() {
124
+ return this.loadLines(Infinity);
125
+ }
126
+ loadRecent(hours) {
127
+ return this.loadLines(hours);
128
+ }
129
+ loadLines(hours) {
130
+ const filePath = path2.join(this.dataDir, "ta-citations.jsonl");
131
+ if (!fs2.existsSync(filePath)) return [];
132
+ const cutoff = new Date(Date.now() - hours * 36e5).toISOString();
133
+ return fs2.readFileSync(filePath, "utf-8").split("\n").filter(Boolean).map((l) => {
134
+ try {
135
+ return JSON.parse(l);
136
+ } catch {
137
+ return null;
138
+ }
139
+ }).filter((r) => r !== null && (hours === Infinity || r.timestamp >= cutoff));
140
+ }
141
+ };
142
+
143
+ // src/dashboard/routes/citation-route.ts
144
+ var tracker = new CitationTracker();
145
+ var alerts = new CitationAlerts();
146
+ async function POST(req) {
147
+ try {
148
+ const body = await req.json();
149
+ tracker.recordFromBody(body);
150
+ return new NextResponse(null, { status: 204 });
151
+ } catch {
152
+ return new NextResponse(null, { status: 400 });
153
+ }
154
+ }
155
+ async function GET(req) {
156
+ const record = tracker.record(req);
157
+ if (record) {
158
+ alerts.check(record);
159
+ }
160
+ return new NextResponse(null, { status: 204 });
161
+ }
162
+ export {
163
+ GET,
164
+ POST
165
+ };
166
+ //# sourceMappingURL=citation-route.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/dashboard/routes/citation-route.ts","../../../src/citations/citation-tracker.ts","../../../src/citations/citation-alerts.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport { CitationTracker } from '../../citations/citation-tracker.js'\nimport { CitationAlerts } from '../../citations/citation-alerts.js'\n\nconst tracker = new CitationTracker()\nconst alerts = new CitationAlerts()\n\n/**\n * POST /api/third-audience/citation\n * Accepts client-side citation reports from citation-tracker.js\n */\nexport async function POST(req: NextRequest) {\n try {\n const body = await req.json()\n tracker.recordFromBody(body)\n return new NextResponse(null, { status: 204 })\n } catch {\n return new NextResponse(null, { status: 400 })\n }\n}\n\n/**\n * GET /api/third-audience/citation\n * Server-side detection — call from page route handlers if needed.\n */\nexport async function GET(req: NextRequest) {\n const record = tracker.record(req)\n if (record) {\n alerts.check(record)\n }\n return new NextResponse(null, { status: 204 })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport type { NextRequest } from 'next/server'\n\nexport interface CitationRecord {\n timestamp: string\n platform: string\n query: string | null\n url: string\n ip: string\n user_agent: string\n referer: string\n}\n\n/** Platform detection from Referer URL */\nconst AI_PLATFORMS: Array<{ name: string; patterns: RegExp[]; queryParam?: string }> = [\n { name: 'ChatGPT', patterns: [/chat\\.openai\\.com/i, /chatgpt\\.com/i], queryParam: 'q' },\n { name: 'Perplexity', patterns: [/perplexity\\.ai/i], queryParam: 'q' },\n { name: 'Claude', patterns: [/claude\\.ai/i] },\n { name: 'Gemini', patterns: [/gemini\\.google\\.com/i, /bard\\.google\\.com/i] },\n { name: 'Copilot', patterns: [/copilot\\.microsoft\\.com/i, /bing\\.com\\/chat/i], queryParam: 'q' },\n { name: 'YouChat', patterns: [/you\\.com/i], queryParam: 'q' },\n]\n\nexport function detectAiPlatform(referer: string): { platform: string; query: string | null } | null {\n if (!referer) return null\n\n let url: URL\n try { url = new URL(referer) } catch { return null }\n\n for (const p of AI_PLATFORMS) {\n if (p.patterns.some(rx => rx.test(referer))) {\n const query = p.queryParam ? url.searchParams.get(p.queryParam) : null\n return { platform: p.name, query }\n }\n }\n return null\n}\n\nexport class CitationTracker {\n private dataDir: string\n\n constructor(dataDir = process.env.TA_DATA_DIR ?? 'data') {\n this.dataDir = dataDir\n }\n\n /** Call from an API route or middleware to record a citation visit. */\n record(req: NextRequest): CitationRecord | null {\n const referer = req.headers.get('referer') ?? ''\n const detection = detectAiPlatform(referer)\n if (!detection) return null\n\n const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim()\n ?? req.headers.get('x-real-ip')\n ?? 'unknown'\n\n const record: CitationRecord = {\n timestamp: new Date().toISOString(),\n platform: detection.platform,\n query: detection.query,\n url: req.nextUrl.pathname,\n ip,\n user_agent: req.headers.get('user-agent') ?? '',\n referer,\n }\n\n this.append(record)\n return record\n }\n\n /** Also handles client-side POSTs from citation-tracker.js */\n recordFromBody(body: Partial<CitationRecord>): void {\n if (!body.platform || !body.url) return\n const record: CitationRecord = {\n timestamp: new Date().toISOString(),\n platform: body.platform,\n query: body.query ?? null,\n url: body.url,\n ip: body.ip ?? 'client',\n user_agent: body.user_agent ?? '',\n referer: body.referer ?? '',\n }\n this.append(record)\n }\n\n private append(record: CitationRecord): void {\n try {\n const filePath = path.join(this.dataDir, 'ta-citations.jsonl')\n fs.mkdirSync(this.dataDir, { recursive: true })\n fs.appendFileSync(filePath, JSON.stringify(record) + '\\n', 'utf-8')\n } catch {\n // Never throw from tracking\n }\n }\n}\n","import fs from 'fs'\nimport path from 'path'\nimport type { CitationRecord } from './citation-tracker.js'\n\nexport interface CitationAlert {\n type: 'first_citation' | 'new_platform' | 'citation_spike' | 'high_performing_page'\n platform: string\n url?: string\n message: string\n timestamp: string\n}\n\nexport class CitationAlerts {\n private dataDir: string\n\n constructor(dataDir = process.env.TA_DATA_DIR ?? 'data') {\n this.dataDir = dataDir\n }\n\n /** Call after appending a new citation record. Returns any triggered alerts. */\n check(newRecord: CitationRecord): CitationAlert[] {\n const alerts: CitationAlert[] = []\n const history = this.loadRecent(24) // last 24h\n\n // First citation from this platform ever\n const platformHistory = this.loadAll().filter(r => r.platform === newRecord.platform)\n if (platformHistory.length === 1) {\n alerts.push({\n type: 'first_citation',\n platform: newRecord.platform,\n url: newRecord.url,\n message: `First citation from ${newRecord.platform}!`,\n timestamp: newRecord.timestamp,\n })\n }\n\n // New platform not seen in last 30 days\n const recentPlatforms = new Set(this.loadRecent(30 * 24).map(r => r.platform))\n if (!recentPlatforms.has(newRecord.platform) && platformHistory.length > 1) {\n alerts.push({\n type: 'new_platform',\n platform: newRecord.platform,\n message: `${newRecord.platform} is citing your content again after a long absence.`,\n timestamp: newRecord.timestamp,\n })\n }\n\n // Citation spike: >3× the hourly baseline\n const hourly = history.filter(r => r.platform === newRecord.platform)\n const baseline = hourly.length > 0 ? hourly.length / 24 : 0\n const lastHourCount = history.filter(r =>\n r.platform === newRecord.platform &&\n new Date(r.timestamp).getTime() > Date.now() - 3_600_000\n ).length\n if (baseline > 2 && lastHourCount > baseline * 3) {\n alerts.push({\n type: 'citation_spike',\n platform: newRecord.platform,\n url: newRecord.url,\n message: `Citation spike from ${newRecord.platform}: ${lastHourCount} in last hour (baseline: ${Math.round(baseline)}/hr)`,\n timestamp: newRecord.timestamp,\n })\n }\n\n return alerts\n }\n\n private loadAll(): CitationRecord[] {\n return this.loadLines(Infinity)\n }\n\n private loadRecent(hours: number): CitationRecord[] {\n return this.loadLines(hours)\n }\n\n private loadLines(hours: number): CitationRecord[] {\n const filePath = path.join(this.dataDir, 'ta-citations.jsonl')\n if (!fs.existsSync(filePath)) return []\n const cutoff = new Date(Date.now() - hours * 3_600_000).toISOString()\n return fs.readFileSync(filePath, 'utf-8')\n .split('\\n')\n .filter(Boolean)\n .map(l => { try { return JSON.parse(l) as CitationRecord } catch { return null } })\n .filter((r): r is CitationRecord => r !== null && (hours === Infinity || r.timestamp >= cutoff))\n }\n}\n"],"mappings":"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","names":["fs","path","alerts"]}
@@ -0,0 +1,6 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ /** Handler for GET /llms.txt → rewired to /api/third-audience/llms-txt */
4
+ declare function GET(req: NextRequest): Promise<NextResponse<unknown>>;
5
+
6
+ export { GET };
@@ -0,0 +1,6 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ /** Handler for GET /llms.txt → rewired to /api/third-audience/llms-txt */
4
+ declare function GET(req: NextRequest): Promise<NextResponse<unknown>>;
5
+
6
+ export { GET };