third-audience-mdx 1.0.6 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dashboard/routes/llms-txt-route.js +16 -4
- package/dist/dashboard/routes/llms-txt-route.js.map +1 -1
- package/dist/dashboard/routes/llms-txt-route.mjs +16 -4
- package/dist/dashboard/routes/llms-txt-route.mjs.map +1 -1
- package/dist/dashboard/routes/markdown-route.js +20 -5
- package/dist/dashboard/routes/markdown-route.js.map +1 -1
- package/dist/dashboard/routes/markdown-route.mjs +20 -5
- package/dist/dashboard/routes/markdown-route.mjs.map +1 -1
- package/dist/dashboard/routes/okf-graph-route.js +16 -4
- package/dist/dashboard/routes/okf-graph-route.js.map +1 -1
- package/dist/dashboard/routes/okf-graph-route.mjs +16 -4
- package/dist/dashboard/routes/okf-graph-route.mjs.map +1 -1
- package/dist/dashboard/routes/okf-route.js +16 -4
- package/dist/dashboard/routes/okf-route.js.map +1 -1
- package/dist/dashboard/routes/okf-route.mjs +16 -4
- package/dist/dashboard/routes/okf-route.mjs.map +1 -1
- package/dist/dashboard/routes/sitemap-ai-route.js +16 -4
- package/dist/dashboard/routes/sitemap-ai-route.js.map +1 -1
- package/dist/dashboard/routes/sitemap-ai-route.mjs +16 -4
- package/dist/dashboard/routes/sitemap-ai-route.mjs.map +1 -1
- package/dist/index.d.mts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -9,14 +9,26 @@ import matter from "gray-matter";
|
|
|
9
9
|
var MdxReader = class {
|
|
10
10
|
constructor(options) {
|
|
11
11
|
this.contentDir = options.contentDir;
|
|
12
|
+
this.stripSegments = options.stripSegments ?? [];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Remove configured URL-only segments from a slug so it maps to the file
|
|
16
|
+
* layout. e.g. stripSegments ['learn'] turns 'en/learn/hydroponics/x' into
|
|
17
|
+
* 'en/hydroponics/x'. Only whole path segments are removed.
|
|
18
|
+
*/
|
|
19
|
+
applyStrip(slug) {
|
|
20
|
+
if (this.stripSegments.length === 0) return slug;
|
|
21
|
+
const drop = new Set(this.stripSegments);
|
|
22
|
+
return slug.split("/").filter((seg) => !drop.has(seg)).join("/");
|
|
12
23
|
}
|
|
13
24
|
/** Read a single MDX file by slug. Returns null if not found. */
|
|
14
25
|
read(slug) {
|
|
26
|
+
const resolved = this.applyStrip(slug);
|
|
15
27
|
const candidates = [
|
|
16
|
-
path.join(this.contentDir, `${
|
|
17
|
-
path.join(this.contentDir, `${
|
|
18
|
-
path.join(this.contentDir,
|
|
19
|
-
path.join(this.contentDir,
|
|
28
|
+
path.join(this.contentDir, `${resolved}.mdx`),
|
|
29
|
+
path.join(this.contentDir, `${resolved}.md`),
|
|
30
|
+
path.join(this.contentDir, resolved, "index.mdx"),
|
|
31
|
+
path.join(this.contentDir, resolved, "index.md")
|
|
20
32
|
];
|
|
21
33
|
for (const filePath of candidates) {
|
|
22
34
|
if (fs.existsSync(filePath)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/dashboard/routes/sitemap-ai-route.ts","../../../src/core/mdx-reader.ts","../../../src/discovery/sitemap-ai.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateAiSitemap } from '../../discovery/sitemap-ai.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/** Handler for GET /sitemap-ai.xml → rewired to /api/third-audience/sitemap-ai */\nexport async function GET(req: NextRequest) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const files = reader.readAll()\n const content = generateAiSitemap(files, baseUrl)\n\n return new NextResponse(content, {\n headers: { 'Content-Type': 'application/xml; charset=utf-8' },\n })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport matter from 'gray-matter'\n\nexport interface MdxFile {\n slug: string // relative path without extension, e.g. 'blog/my-post'\n filePath: string // absolute path to .mdx file\n frontmatter: Record<string, unknown>\n rawContent: string // body after frontmatter\n}\n\nexport interface MdxReaderOptions {\n contentDir: string // absolute path to content directory\n}\n\nexport class MdxReader {\n private contentDir: string\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const candidates = [\n path.join(this.contentDir, `${
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/routes/sitemap-ai-route.ts","../../../src/core/mdx-reader.ts","../../../src/discovery/sitemap-ai.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateAiSitemap } from '../../discovery/sitemap-ai.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/** Handler for GET /sitemap-ai.xml → rewired to /api/third-audience/sitemap-ai */\nexport async function GET(req: NextRequest) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const files = reader.readAll()\n const content = generateAiSitemap(files, baseUrl)\n\n return new NextResponse(content, {\n headers: { 'Content-Type': 'application/xml; charset=utf-8' },\n })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport matter from 'gray-matter'\n\nexport interface MdxFile {\n slug: string // relative path without extension, e.g. 'blog/my-post'\n filePath: string // absolute path to .mdx file\n frontmatter: Record<string, unknown>\n rawContent: string // body after frontmatter\n}\n\nexport interface MdxReaderOptions {\n contentDir: string // absolute path to content directory\n /** URL path segments to drop when mapping a request slug to a file. */\n stripSegments?: string[]\n}\n\nexport class MdxReader {\n private contentDir: string\n private stripSegments: string[]\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n this.stripSegments = options.stripSegments ?? []\n }\n\n /**\n * Remove configured URL-only segments from a slug so it maps to the file\n * layout. e.g. stripSegments ['learn'] turns 'en/learn/hydroponics/x' into\n * 'en/hydroponics/x'. Only whole path segments are removed.\n */\n private applyStrip(slug: string): string {\n if (this.stripSegments.length === 0) return slug\n const drop = new Set(this.stripSegments)\n return slug\n .split('/')\n .filter((seg) => !drop.has(seg))\n .join('/')\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const resolved = this.applyStrip(slug)\n const candidates = [\n path.join(this.contentDir, `${resolved}.mdx`),\n path.join(this.contentDir, `${resolved}.md`),\n path.join(this.contentDir, resolved, 'index.mdx'),\n path.join(this.contentDir, resolved, 'index.md'),\n ]\n\n for (const filePath of candidates) {\n if (fs.existsSync(filePath)) {\n return this.parseFile(slug, filePath)\n }\n }\n\n return null\n }\n\n /** Read all MDX files recursively. */\n readAll(): MdxFile[] {\n if (!fs.existsSync(this.contentDir)) return []\n return this.walkDir(this.contentDir, this.contentDir)\n }\n\n private walkDir(dir: string, root: string): MdxFile[] {\n const results: MdxFile[] = []\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n results.push(...this.walkDir(fullPath, root))\n } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {\n const relative = path.relative(root, fullPath)\n const slug = relative.replace(/\\.(mdx|md)$/, '').replace(/\\/index$/, '')\n results.push(this.parseFile(slug, fullPath))\n }\n }\n return results\n }\n\n private parseFile(slug: string, filePath: string): MdxFile {\n const raw = fs.readFileSync(filePath, 'utf-8')\n const { data: frontmatter, content: rawContent } = matter(raw)\n return { slug, filePath, frontmatter, rawContent }\n }\n}\n","import type { MdxFile } from '../core/mdx-reader.js'\n\n/** Generates /sitemap-ai.xml from MDX files. */\nexport function generateAiSitemap(files: MdxFile[], baseUrl: string): string {\n const base = baseUrl.replace(/\\/$/, '')\n const urls = files.map(file => {\n const fm = file.frontmatter\n const loc = `${base}/${file.slug}`\n const lastmod = fm.date ? new Date(fm.date as string).toISOString().slice(0, 10) : ''\n const title = fm.title ? `\\n <title>${escapeXml(String(fm.title))}</title>` : ''\n const desc = fm.description ? `\\n <description>${escapeXml(String(fm.description))}</description>` : ''\n return [\n ' <url>',\n ` <loc>${escapeXml(loc)}</loc>`,\n lastmod ? ` <lastmod>${lastmod}</lastmod>` : '',\n ` <changefreq>weekly</changefreq>`,\n title,\n desc,\n ' </url>',\n ].filter(Boolean).join('\\n')\n })\n\n return [\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n '<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">',\n ...urls,\n '</urlset>',\n ].join('\\n') + '\\n'\n}\n\nfunction escapeXml(s: string): string {\n return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"')\n}\n"],"mappings":"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","names":["path","path"]}
|
package/dist/index.d.mts
CHANGED
|
@@ -6,6 +6,13 @@ interface ThirdAudienceConfig {
|
|
|
6
6
|
contentDir?: string;
|
|
7
7
|
/** Directory for JSONL data files. Default: 'data' */
|
|
8
8
|
dataDir?: string;
|
|
9
|
+
/**
|
|
10
|
+
* URL path segments to strip when mapping a request slug to a content file.
|
|
11
|
+
* Use when your route structure differs from your file layout — e.g. URLs
|
|
12
|
+
* are /en/learn/hydroponics/x but files live at content/en/hydroponics/x.mdx.
|
|
13
|
+
* Set ['learn'] to drop the 'learn' segment. Default: [] (no rewriting).
|
|
14
|
+
*/
|
|
15
|
+
stripSegments?: string[];
|
|
9
16
|
/** Mount the /third-audience/ dashboard. Default: true */
|
|
10
17
|
dashboard?: boolean;
|
|
11
18
|
/** Secret for dashboard access (HTTP Basic or bearer). Required when dashboard: true */
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,13 @@ interface ThirdAudienceConfig {
|
|
|
6
6
|
contentDir?: string;
|
|
7
7
|
/** Directory for JSONL data files. Default: 'data' */
|
|
8
8
|
dataDir?: string;
|
|
9
|
+
/**
|
|
10
|
+
* URL path segments to strip when mapping a request slug to a content file.
|
|
11
|
+
* Use when your route structure differs from your file layout — e.g. URLs
|
|
12
|
+
* are /en/learn/hydroponics/x but files live at content/en/hydroponics/x.mdx.
|
|
13
|
+
* Set ['learn'] to drop the 'learn' segment. Default: [] (no rewriting).
|
|
14
|
+
*/
|
|
15
|
+
stripSegments?: string[];
|
|
9
16
|
/** Mount the /third-audience/ dashboard. Default: true */
|
|
10
17
|
dashboard?: boolean;
|
|
11
18
|
/** Secret for dashboard access (HTTP Basic or bearer). Required when dashboard: true */
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
30
30
|
var defaultConfig = {
|
|
31
31
|
contentDir: "content",
|
|
32
32
|
dataDir: "data",
|
|
33
|
+
stripSegments: [],
|
|
33
34
|
dashboard: true,
|
|
34
35
|
dashboardSecret: "",
|
|
35
36
|
notifications: {},
|
|
@@ -73,7 +74,8 @@ function withThirdAudience(options = {}, nextConfig = {}) {
|
|
|
73
74
|
...nextConfig.env,
|
|
74
75
|
TA_CONTENT_DIR: config.contentDir,
|
|
75
76
|
TA_DATA_DIR: config.dataDir,
|
|
76
|
-
TA_DASHBOARD_ENABLED: String(config.dashboard)
|
|
77
|
+
TA_DASHBOARD_ENABLED: String(config.dashboard),
|
|
78
|
+
TA_STRIP_SEGMENTS: config.stripSegments.join(",")
|
|
77
79
|
}
|
|
78
80
|
};
|
|
79
81
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/core/config.ts","../src/core/with-third-audience.ts","../src/core/middleware.ts","../src/detection/known-patterns.ts","../src/detection/bot-detection-pipeline.ts"],"sourcesContent":["/**\n * third-audience-mdx\n * Public API surface for the package.\n */\n\nexport { withThirdAudience } from './core/with-third-audience.js'\nexport { thirdAudienceMiddleware } from './core/middleware.js'\nexport { detectBot } from './detection/bot-detection-pipeline.js'\nexport type { ThirdAudienceConfig } from './core/config.js'\nexport type { BotDetectionResult } from './detection/bot-detection-result.js'\n","export interface ThirdAudienceConfig {\n /** Directory containing .mdx files, relative to project root. Default: 'content' */\n contentDir?: string\n /** Directory for JSONL data files. Default: 'data' */\n dataDir?: string\n /** Mount the /third-audience/ dashboard. Default: true */\n dashboard?: boolean\n /** Secret for dashboard access (HTTP Basic or bearer). Required when dashboard: true */\n dashboardSecret?: string\n notifications?: {\n email?: { smtp: string; to: string; from?: string }\n slack?: { webhookUrl: string }\n }\n bots?: {\n allowlist?: string[]\n blocklist?: string[]\n }\n cache?: {\n /** Cache TTL in seconds. Default: 3600 */\n ttl?: number\n /** Max in-memory entries. Default: 500 */\n maxMemoryEntries?: number\n }\n}\n\nexport const defaultConfig: Required<ThirdAudienceConfig> = {\n contentDir: 'content',\n dataDir: 'data',\n dashboard: true,\n dashboardSecret: '',\n notifications: {},\n bots: { allowlist: [], blocklist: [] },\n cache: { ttl: 3600, maxMemoryEntries: 500 },\n}\n\nexport function resolveConfig(partial: ThirdAudienceConfig = {}): Required<ThirdAudienceConfig> {\n return {\n ...defaultConfig,\n ...partial,\n bots: { ...defaultConfig.bots, ...partial.bots },\n cache: { ...defaultConfig.cache, ...partial.cache },\n notifications: { ...defaultConfig.notifications, ...partial.notifications },\n }\n}\n","import type { NextConfig } from 'next'\nimport { resolveConfig, type ThirdAudienceConfig } from './config.js'\n\n/**\n * Wraps next.config.ts to inject Third Audience rewrites and headers.\n *\n * Usage:\n * import { withThirdAudience } from 'third-audience-mdx'\n * export default withThirdAudience({ contentDir: 'content' })\n */\nexport function withThirdAudience(\n options: ThirdAudienceConfig = {},\n nextConfig: NextConfig = {}\n): NextConfig {\n const config = resolveConfig(options)\n\n return {\n ...nextConfig,\n async headers() {\n const existing = await nextConfig.headers?.() ?? []\n return [\n ...existing,\n {\n source: '/:path*.md',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n {\n source: '/llms.txt',\n headers: [{ key: 'Content-Type', value: 'text/plain; charset=utf-8' }],\n },\n {\n source: '/okf/:path*',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n ]\n },\n env: {\n ...nextConfig.env,\n TA_CONTENT_DIR: config.contentDir,\n TA_DATA_DIR: config.dataDir,\n TA_DASHBOARD_ENABLED: String(config.dashboard),\n },\n }\n}\n","import { NextResponse, type NextRequest } from 'next/server'\n\nconst COOKIE_NAME = 'ta_session'\nconst RESET_COOKIE = 'ta_session_reset'\n\n/**\n * Third Audience middleware — Edge-runtime compatible (no Node.js crypto).\n *\n * Auth guard uses cookie presence only; HMAC verification happens in the\n * route handler (Node.js runtime) where crypto is available.\n *\n * Wire up in middleware.ts:\n * export { thirdAudienceMiddleware as middleware } from 'third-audience-mdx'\n * export const config = { matcher: ['/((?!_next|api).*)'] }\n */\nexport function thirdAudienceMiddleware(req: NextRequest): NextResponse | null {\n const { pathname } = req.nextUrl\n const accept = req.headers.get('accept') ?? ''\n\n // Dashboard auth guard — cookie presence check (HMAC verified in route handler)\n if (pathname.startsWith('/third-audience') && !pathname.startsWith('/third-audience/login')) {\n const session = req.cookies.get(COOKIE_NAME)?.value\n if (!session) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // Password reset guard — /third-audience/login?reset=1 requires reset or session cookie\n if (pathname === '/third-audience/login' && req.nextUrl.searchParams.get('reset') === '1') {\n const resetCookie = req.cookies.get(RESET_COOKIE)?.value\n const sessionCookie = req.cookies.get(COOKIE_NAME)?.value\n if (!resetCookie && !sessionCookie) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n loginUrl.search = ''\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // /third-audience/login → rewrite to login route handler (GET/POST)\n if (pathname === '/third-audience/login') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/login'\n return NextResponse.rewrite(url)\n }\n\n // .md URL → rewrite to our internal markdown route handler\n if (pathname.endsWith('.md')) {\n const slug = pathname.slice(0, -3) // strip .md\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${slug}`\n return NextResponse.rewrite(url)\n }\n\n // Accept: text/markdown header → rewrite to markdown route\n if (accept.includes('text/markdown')) {\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${pathname}`\n return NextResponse.rewrite(url)\n }\n\n // /okf or /okf/* → rewrite to OKF bundle handler\n // [...path] catch-all requires at least one segment, so /okf → /okf/index\n if (pathname.startsWith('/okf')) {\n const url = req.nextUrl.clone()\n const rest = pathname.slice(4) // '' or '/something'\n url.pathname = `/api/third-audience/okf${rest || '/index'}`\n return NextResponse.rewrite(url)\n }\n\n // /llms.txt → rewrite to discovery handler\n if (pathname === '/llms.txt') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/llms-txt'\n return NextResponse.rewrite(url)\n }\n\n // /sitemap-ai.xml → rewrite to AI sitemap handler\n if (pathname === '/sitemap-ai.xml') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/sitemap-ai'\n return NextResponse.rewrite(url)\n }\n\n // No match — let the caller's middleware chain continue\n return null\n}\n\n","/** Known AI crawler and search engine user-agent patterns. */\nexport interface KnownBot {\n name: string\n category: 'ai_crawler' | 'search_engine'\n patterns: RegExp[]\n}\n\nexport const KNOWN_BOTS: KnownBot[] = [\n // AI Crawlers\n { name: 'ClaudeBot', category: 'ai_crawler', patterns: [/claudebot/i, /claude-web/i] },\n { name: 'GPTBot', category: 'ai_crawler', patterns: [/gptbot/i] },\n { name: 'ChatGPT-User', category: 'ai_crawler', patterns: [/chatgpt-user/i] },\n { name: 'PerplexityBot', category: 'ai_crawler', patterns: [/perplexitybot/i] },\n { name: 'Googlebot-AI', category: 'ai_crawler', patterns: [/google-extended/i, /googleother/i] },\n { name: 'FacebookBot', category: 'ai_crawler', patterns: [/facebookbot/i] },\n { name: 'Applebot-Extended',category: 'ai_crawler', patterns: [/applebot-extended/i] },\n { name: 'YouBot', category: 'ai_crawler', patterns: [/youbot/i] },\n { name: 'CCBot', category: 'ai_crawler', patterns: [/ccbot/i] },\n { name: 'CohereCrawler', category: 'ai_crawler', patterns: [/cohere-ai/i] },\n { name: 'AI2Bot', category: 'ai_crawler', patterns: [/ai2bot/i] },\n { name: 'Bytespider', category: 'ai_crawler', patterns: [/bytespider/i] },\n { name: 'Diffbot', category: 'ai_crawler', patterns: [/diffbot/i] },\n\n // Search Engines\n { name: 'Googlebot', category: 'search_engine', patterns: [/googlebot/i] },\n { name: 'Bingbot', category: 'search_engine', patterns: [/bingbot/i, /msnbot/i] },\n { name: 'DuckDuckBot', category: 'search_engine', patterns: [/duckduckbot/i] },\n { name: 'Baiduspider', category: 'search_engine', patterns: [/baiduspider/i] },\n { name: 'YandexBot', category: 'search_engine', patterns: [/yandexbot/i] },\n { name: 'Sogou', category: 'search_engine', patterns: [/sogou/i] },\n { name: 'Exabot', category: 'search_engine', patterns: [/exabot/i] },\n { name: 'ia_archiver', category: 'search_engine', patterns: [/ia_archiver/i] },\n]\n","import type { BotDetectionResult } from './bot-detection-result.js'\nimport { KNOWN_BOTS } from './known-patterns.js'\n\nexport interface DetectBotInput {\n userAgent: string\n /** Optional: headers map for heuristic checks */\n headers?: Record<string, string | string[] | undefined>\n /** Optional: IP address */\n ip?: string\n}\n\n/**\n * Three-layer bot detection pipeline:\n * 1. Known pattern matching (O(n) UA string match)\n * 2. Heuristic signals (missing headers, headless indicators)\n * 3. Auto-learner flag (unknown UAs that behave bot-like)\n */\nexport function detectBot(input: DetectBotInput): BotDetectionResult {\n const ua = input.userAgent ?? ''\n\n // Layer 1: known pattern match\n for (const bot of KNOWN_BOTS) {\n for (const pattern of bot.patterns) {\n if (pattern.test(ua)) {\n return {\n isBot: true,\n botName: bot.name,\n confidence: 'high',\n detectionMethod: 'known_pattern',\n category: bot.category,\n rawUserAgent: ua,\n }\n }\n }\n }\n\n // Layer 2: heuristics\n const heuristicResult = checkHeuristics(ua, input.headers ?? {})\n if (heuristicResult) return { ...heuristicResult, rawUserAgent: ua }\n\n // Layer 3: auto-learner — flag suspicious unknown UAs for review\n if (looksLikeBotUa(ua)) {\n return {\n isBot: true,\n botName: null,\n confidence: 'low',\n detectionMethod: 'auto_learned',\n category: 'unknown_bot',\n rawUserAgent: ua,\n }\n }\n\n return {\n isBot: false,\n botName: null,\n confidence: 'high',\n detectionMethod: 'none',\n category: 'human',\n rawUserAgent: ua,\n }\n}\n\nfunction checkHeuristics(\n ua: string,\n headers: Record<string, string | string[] | undefined>\n): Omit<BotDetectionResult, 'rawUserAgent'> | null {\n // Headless Chrome signals\n if (/headlesschrome/i.test(ua)) {\n return { isBot: true, botName: 'HeadlessChrome', confidence: 'medium', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/phantomjs/i.test(ua)) {\n return { isBot: true, botName: 'PhantomJS', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/selenium/i.test(ua)) {\n return { isBot: true, botName: 'Selenium', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Empty or very short UA is suspicious\n if (ua.trim().length < 10) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Missing typical browser headers — only a bot signal when the UA does NOT\n // present itself as a real browser. Genuine browsers occasionally arrive\n // without accept-language/accept-encoding (privacy extensions, proxies,\n // some CDNs), so we must not flag them on missing headers alone.\n const hasAcceptLang = !!headers['accept-language']\n const hasAcceptEncoding = !!headers['accept-encoding']\n const claimsBrowser = /chrome|firefox|safari|edge|opera|gecko|applewebkit/i.test(ua)\n if (!hasAcceptLang && !hasAcceptEncoding && !claimsBrowser) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n return null\n}\n\nfunction looksLikeBotUa(ua: string): boolean {\n return (\n /bot|crawler|spider|scraper|fetch|http|python|curl|java|ruby|go-http|node/i.test(ua) &&\n !/chrome|firefox|safari|edge|opera/i.test(ua)\n )\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/core/config.ts","../src/core/with-third-audience.ts","../src/core/middleware.ts","../src/detection/known-patterns.ts","../src/detection/bot-detection-pipeline.ts"],"sourcesContent":["/**\n * third-audience-mdx\n * Public API surface for the package.\n */\n\nexport { withThirdAudience } from './core/with-third-audience.js'\nexport { thirdAudienceMiddleware } from './core/middleware.js'\nexport { detectBot } from './detection/bot-detection-pipeline.js'\nexport type { ThirdAudienceConfig } from './core/config.js'\nexport type { BotDetectionResult } from './detection/bot-detection-result.js'\n","export interface ThirdAudienceConfig {\n /** Directory containing .mdx files, relative to project root. Default: 'content' */\n contentDir?: string\n /** Directory for JSONL data files. Default: 'data' */\n dataDir?: string\n /**\n * URL path segments to strip when mapping a request slug to a content file.\n * Use when your route structure differs from your file layout — e.g. URLs\n * are /en/learn/hydroponics/x but files live at content/en/hydroponics/x.mdx.\n * Set ['learn'] to drop the 'learn' segment. Default: [] (no rewriting).\n */\n stripSegments?: string[]\n /** Mount the /third-audience/ dashboard. Default: true */\n dashboard?: boolean\n /** Secret for dashboard access (HTTP Basic or bearer). Required when dashboard: true */\n dashboardSecret?: string\n notifications?: {\n email?: { smtp: string; to: string; from?: string }\n slack?: { webhookUrl: string }\n }\n bots?: {\n allowlist?: string[]\n blocklist?: string[]\n }\n cache?: {\n /** Cache TTL in seconds. Default: 3600 */\n ttl?: number\n /** Max in-memory entries. Default: 500 */\n maxMemoryEntries?: number\n }\n}\n\nexport const defaultConfig: Required<ThirdAudienceConfig> = {\n contentDir: 'content',\n dataDir: 'data',\n stripSegments: [],\n dashboard: true,\n dashboardSecret: '',\n notifications: {},\n bots: { allowlist: [], blocklist: [] },\n cache: { ttl: 3600, maxMemoryEntries: 500 },\n}\n\nexport function resolveConfig(partial: ThirdAudienceConfig = {}): Required<ThirdAudienceConfig> {\n return {\n ...defaultConfig,\n ...partial,\n bots: { ...defaultConfig.bots, ...partial.bots },\n cache: { ...defaultConfig.cache, ...partial.cache },\n notifications: { ...defaultConfig.notifications, ...partial.notifications },\n }\n}\n","import type { NextConfig } from 'next'\nimport { resolveConfig, type ThirdAudienceConfig } from './config.js'\n\n/**\n * Wraps next.config.ts to inject Third Audience rewrites and headers.\n *\n * Usage:\n * import { withThirdAudience } from 'third-audience-mdx'\n * export default withThirdAudience({ contentDir: 'content' })\n */\nexport function withThirdAudience(\n options: ThirdAudienceConfig = {},\n nextConfig: NextConfig = {}\n): NextConfig {\n const config = resolveConfig(options)\n\n return {\n ...nextConfig,\n async headers() {\n const existing = await nextConfig.headers?.() ?? []\n return [\n ...existing,\n {\n source: '/:path*.md',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n {\n source: '/llms.txt',\n headers: [{ key: 'Content-Type', value: 'text/plain; charset=utf-8' }],\n },\n {\n source: '/okf/:path*',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n ]\n },\n env: {\n ...nextConfig.env,\n TA_CONTENT_DIR: config.contentDir,\n TA_DATA_DIR: config.dataDir,\n TA_DASHBOARD_ENABLED: String(config.dashboard),\n TA_STRIP_SEGMENTS: config.stripSegments.join(','),\n },\n }\n}\n","import { NextResponse, type NextRequest } from 'next/server'\n\nconst COOKIE_NAME = 'ta_session'\nconst RESET_COOKIE = 'ta_session_reset'\n\n/**\n * Third Audience middleware — Edge-runtime compatible (no Node.js crypto).\n *\n * Auth guard uses cookie presence only; HMAC verification happens in the\n * route handler (Node.js runtime) where crypto is available.\n *\n * Wire up in middleware.ts:\n * export { thirdAudienceMiddleware as middleware } from 'third-audience-mdx'\n * export const config = { matcher: ['/((?!_next|api).*)'] }\n */\nexport function thirdAudienceMiddleware(req: NextRequest): NextResponse | null {\n const { pathname } = req.nextUrl\n const accept = req.headers.get('accept') ?? ''\n\n // Dashboard auth guard — cookie presence check (HMAC verified in route handler)\n if (pathname.startsWith('/third-audience') && !pathname.startsWith('/third-audience/login')) {\n const session = req.cookies.get(COOKIE_NAME)?.value\n if (!session) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // Password reset guard — /third-audience/login?reset=1 requires reset or session cookie\n if (pathname === '/third-audience/login' && req.nextUrl.searchParams.get('reset') === '1') {\n const resetCookie = req.cookies.get(RESET_COOKIE)?.value\n const sessionCookie = req.cookies.get(COOKIE_NAME)?.value\n if (!resetCookie && !sessionCookie) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n loginUrl.search = ''\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // /third-audience/login → rewrite to login route handler (GET/POST)\n if (pathname === '/third-audience/login') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/login'\n return NextResponse.rewrite(url)\n }\n\n // .md URL → rewrite to our internal markdown route handler\n if (pathname.endsWith('.md')) {\n const slug = pathname.slice(0, -3) // strip .md\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${slug}`\n return NextResponse.rewrite(url)\n }\n\n // Accept: text/markdown header → rewrite to markdown route\n if (accept.includes('text/markdown')) {\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${pathname}`\n return NextResponse.rewrite(url)\n }\n\n // /okf or /okf/* → rewrite to OKF bundle handler\n // [...path] catch-all requires at least one segment, so /okf → /okf/index\n if (pathname.startsWith('/okf')) {\n const url = req.nextUrl.clone()\n const rest = pathname.slice(4) // '' or '/something'\n url.pathname = `/api/third-audience/okf${rest || '/index'}`\n return NextResponse.rewrite(url)\n }\n\n // /llms.txt → rewrite to discovery handler\n if (pathname === '/llms.txt') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/llms-txt'\n return NextResponse.rewrite(url)\n }\n\n // /sitemap-ai.xml → rewrite to AI sitemap handler\n if (pathname === '/sitemap-ai.xml') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/sitemap-ai'\n return NextResponse.rewrite(url)\n }\n\n // No match — let the caller's middleware chain continue\n return null\n}\n\n","/** Known AI crawler and search engine user-agent patterns. */\nexport interface KnownBot {\n name: string\n category: 'ai_crawler' | 'search_engine'\n patterns: RegExp[]\n}\n\nexport const KNOWN_BOTS: KnownBot[] = [\n // AI Crawlers\n { name: 'ClaudeBot', category: 'ai_crawler', patterns: [/claudebot/i, /claude-web/i] },\n { name: 'GPTBot', category: 'ai_crawler', patterns: [/gptbot/i] },\n { name: 'ChatGPT-User', category: 'ai_crawler', patterns: [/chatgpt-user/i] },\n { name: 'PerplexityBot', category: 'ai_crawler', patterns: [/perplexitybot/i] },\n { name: 'Googlebot-AI', category: 'ai_crawler', patterns: [/google-extended/i, /googleother/i] },\n { name: 'FacebookBot', category: 'ai_crawler', patterns: [/facebookbot/i] },\n { name: 'Applebot-Extended',category: 'ai_crawler', patterns: [/applebot-extended/i] },\n { name: 'YouBot', category: 'ai_crawler', patterns: [/youbot/i] },\n { name: 'CCBot', category: 'ai_crawler', patterns: [/ccbot/i] },\n { name: 'CohereCrawler', category: 'ai_crawler', patterns: [/cohere-ai/i] },\n { name: 'AI2Bot', category: 'ai_crawler', patterns: [/ai2bot/i] },\n { name: 'Bytespider', category: 'ai_crawler', patterns: [/bytespider/i] },\n { name: 'Diffbot', category: 'ai_crawler', patterns: [/diffbot/i] },\n\n // Search Engines\n { name: 'Googlebot', category: 'search_engine', patterns: [/googlebot/i] },\n { name: 'Bingbot', category: 'search_engine', patterns: [/bingbot/i, /msnbot/i] },\n { name: 'DuckDuckBot', category: 'search_engine', patterns: [/duckduckbot/i] },\n { name: 'Baiduspider', category: 'search_engine', patterns: [/baiduspider/i] },\n { name: 'YandexBot', category: 'search_engine', patterns: [/yandexbot/i] },\n { name: 'Sogou', category: 'search_engine', patterns: [/sogou/i] },\n { name: 'Exabot', category: 'search_engine', patterns: [/exabot/i] },\n { name: 'ia_archiver', category: 'search_engine', patterns: [/ia_archiver/i] },\n]\n","import type { BotDetectionResult } from './bot-detection-result.js'\nimport { KNOWN_BOTS } from './known-patterns.js'\n\nexport interface DetectBotInput {\n userAgent: string\n /** Optional: headers map for heuristic checks */\n headers?: Record<string, string | string[] | undefined>\n /** Optional: IP address */\n ip?: string\n}\n\n/**\n * Three-layer bot detection pipeline:\n * 1. Known pattern matching (O(n) UA string match)\n * 2. Heuristic signals (missing headers, headless indicators)\n * 3. Auto-learner flag (unknown UAs that behave bot-like)\n */\nexport function detectBot(input: DetectBotInput): BotDetectionResult {\n const ua = input.userAgent ?? ''\n\n // Layer 1: known pattern match\n for (const bot of KNOWN_BOTS) {\n for (const pattern of bot.patterns) {\n if (pattern.test(ua)) {\n return {\n isBot: true,\n botName: bot.name,\n confidence: 'high',\n detectionMethod: 'known_pattern',\n category: bot.category,\n rawUserAgent: ua,\n }\n }\n }\n }\n\n // Layer 2: heuristics\n const heuristicResult = checkHeuristics(ua, input.headers ?? {})\n if (heuristicResult) return { ...heuristicResult, rawUserAgent: ua }\n\n // Layer 3: auto-learner — flag suspicious unknown UAs for review\n if (looksLikeBotUa(ua)) {\n return {\n isBot: true,\n botName: null,\n confidence: 'low',\n detectionMethod: 'auto_learned',\n category: 'unknown_bot',\n rawUserAgent: ua,\n }\n }\n\n return {\n isBot: false,\n botName: null,\n confidence: 'high',\n detectionMethod: 'none',\n category: 'human',\n rawUserAgent: ua,\n }\n}\n\nfunction checkHeuristics(\n ua: string,\n headers: Record<string, string | string[] | undefined>\n): Omit<BotDetectionResult, 'rawUserAgent'> | null {\n // Headless Chrome signals\n if (/headlesschrome/i.test(ua)) {\n return { isBot: true, botName: 'HeadlessChrome', confidence: 'medium', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/phantomjs/i.test(ua)) {\n return { isBot: true, botName: 'PhantomJS', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/selenium/i.test(ua)) {\n return { isBot: true, botName: 'Selenium', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Empty or very short UA is suspicious\n if (ua.trim().length < 10) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Missing typical browser headers — only a bot signal when the UA does NOT\n // present itself as a real browser. Genuine browsers occasionally arrive\n // without accept-language/accept-encoding (privacy extensions, proxies,\n // some CDNs), so we must not flag them on missing headers alone.\n const hasAcceptLang = !!headers['accept-language']\n const hasAcceptEncoding = !!headers['accept-encoding']\n const claimsBrowser = /chrome|firefox|safari|edge|opera|gecko|applewebkit/i.test(ua)\n if (!hasAcceptLang && !hasAcceptEncoding && !claimsBrowser) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n return null\n}\n\nfunction looksLikeBotUa(ua: string): boolean {\n return (\n /bot|crawler|spider|scraper|fetch|http|python|curl|java|ruby|go-http|node/i.test(ua) &&\n !/chrome|firefox|safari|edge|opera/i.test(ua)\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACgCO,IAAM,gBAA+C;AAAA,EAC1D,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,eAAe,CAAC;AAAA,EAChB,WAAW;AAAA,EACX,iBAAiB;AAAA,EACjB,eAAe,CAAC;AAAA,EAChB,MAAM,EAAE,WAAW,CAAC,GAAG,WAAW,CAAC,EAAE;AAAA,EACrC,OAAO,EAAE,KAAK,MAAM,kBAAkB,IAAI;AAC5C;AAEO,SAAS,cAAc,UAA+B,CAAC,GAAkC;AAC9F,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAG;AAAA,IACH,MAAM,EAAE,GAAG,cAAc,MAAM,GAAG,QAAQ,KAAK;AAAA,IAC/C,OAAO,EAAE,GAAG,cAAc,OAAO,GAAG,QAAQ,MAAM;AAAA,IAClD,eAAe,EAAE,GAAG,cAAc,eAAe,GAAG,QAAQ,cAAc;AAAA,EAC5E;AACF;;;ACzCO,SAAS,kBACd,UAA+B,CAAC,GAChC,aAAyB,CAAC,GACd;AACZ,QAAM,SAAS,cAAc,OAAO;AAEpC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,MAAM,UAAU;AACd,YAAM,WAAW,MAAM,WAAW,UAAU,KAAK,CAAC;AAClD,aAAO;AAAA,QACL,GAAG;AAAA,QACH;AAAA,UACE,QAAQ;AAAA,UACR,SAAS,CAAC,EAAE,KAAK,gBAAgB,OAAO,+BAA+B,CAAC;AAAA,QAC1E;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,SAAS,CAAC,EAAE,KAAK,gBAAgB,OAAO,4BAA4B,CAAC;AAAA,QACvE;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,SAAS,CAAC,EAAE,KAAK,gBAAgB,OAAO,+BAA+B,CAAC;AAAA,QAC1E;AAAA,MACF;AAAA,IACF;AAAA,IACA,KAAK;AAAA,MACH,GAAG,WAAW;AAAA,MACd,gBAAgB,OAAO;AAAA,MACvB,aAAa,OAAO;AAAA,MACpB,sBAAsB,OAAO,OAAO,SAAS;AAAA,MAC7C,mBAAmB,OAAO,cAAc,KAAK,GAAG;AAAA,IAClD;AAAA,EACF;AACF;;;AC5CA,oBAA+C;AAE/C,IAAM,cAAc;AACpB,IAAM,eAAe;AAYd,SAAS,wBAAwB,KAAuC;AAC7E,QAAM,EAAE,SAAS,IAAI,IAAI;AACzB,QAAM,SAAS,IAAI,QAAQ,IAAI,QAAQ,KAAK;AAG5C,MAAI,SAAS,WAAW,iBAAiB,KAAK,CAAC,SAAS,WAAW,uBAAuB,GAAG;AAC3F,UAAM,UAAU,IAAI,QAAQ,IAAI,WAAW,GAAG;AAC9C,QAAI,CAAC,SAAS;AACZ,YAAM,WAAW,IAAI,QAAQ,MAAM;AACnC,eAAS,WAAW;AACpB,aAAO,2BAAa,SAAS,QAAQ;AAAA,IACvC;AAAA,EACF;AAGA,MAAI,aAAa,2BAA2B,IAAI,QAAQ,aAAa,IAAI,OAAO,MAAM,KAAK;AACzF,UAAM,cAAc,IAAI,QAAQ,IAAI,YAAY,GAAG;AACnD,UAAM,gBAAgB,IAAI,QAAQ,IAAI,WAAW,GAAG;AACpD,QAAI,CAAC,eAAe,CAAC,eAAe;AAClC,YAAM,WAAW,IAAI,QAAQ,MAAM;AACnC,eAAS,WAAW;AACpB,eAAS,SAAS;AAClB,aAAO,2BAAa,SAAS,QAAQ;AAAA,IACvC;AAAA,EACF;AAGA,MAAI,aAAa,yBAAyB;AACxC,UAAM,MAAM,IAAI,QAAQ,MAAM;AAC9B,QAAI,WAAW;AACf,WAAO,2BAAa,QAAQ,GAAG;AAAA,EACjC;AAGA,MAAI,SAAS,SAAS,KAAK,GAAG;AAC5B,UAAM,OAAO,SAAS,MAAM,GAAG,EAAE;AACjC,UAAM,MAAM,IAAI,QAAQ,MAAM;AAC9B,QAAI,WAAW,+BAA+B,IAAI;AAClD,WAAO,2BAAa,QAAQ,GAAG;AAAA,EACjC;AAGA,MAAI,OAAO,SAAS,eAAe,GAAG;AACpC,UAAM,MAAM,IAAI,QAAQ,MAAM;AAC9B,QAAI,WAAW,+BAA+B,QAAQ;AACtD,WAAO,2BAAa,QAAQ,GAAG;AAAA,EACjC;AAIA,MAAI,SAAS,WAAW,MAAM,GAAG;AAC/B,UAAM,MAAM,IAAI,QAAQ,MAAM;AAC9B,UAAM,OAAO,SAAS,MAAM,CAAC;AAC7B,QAAI,WAAW,0BAA0B,QAAQ,QAAQ;AACzD,WAAO,2BAAa,QAAQ,GAAG;AAAA,EACjC;AAGA,MAAI,aAAa,aAAa;AAC5B,UAAM,MAAM,IAAI,QAAQ,MAAM;AAC9B,QAAI,WAAW;AACf,WAAO,2BAAa,QAAQ,GAAG;AAAA,EACjC;AAGA,MAAI,aAAa,mBAAmB;AAClC,UAAM,MAAM,IAAI,QAAQ,MAAM;AAC9B,QAAI,WAAW;AACf,WAAO,2BAAa,QAAQ,GAAG;AAAA,EACjC;AAGA,SAAO;AACT;;;ACjFO,IAAM,aAAyB;AAAA;AAAA,EAEpC,EAAE,MAAM,aAAoB,UAAU,cAAiB,UAAU,CAAC,cAAc,aAAa,EAAE;AAAA,EAC/F,EAAE,MAAM,UAAoB,UAAU,cAAiB,UAAU,CAAC,SAAS,EAAE;AAAA,EAC7E,EAAE,MAAM,gBAAoB,UAAU,cAAiB,UAAU,CAAC,eAAe,EAAE;AAAA,EACnF,EAAE,MAAM,iBAAoB,UAAU,cAAiB,UAAU,CAAC,gBAAgB,EAAE;AAAA,EACpF,EAAE,MAAM,gBAAoB,UAAU,cAAiB,UAAU,CAAC,oBAAoB,cAAc,EAAE;AAAA,EACtG,EAAE,MAAM,eAAoB,UAAU,cAAiB,UAAU,CAAC,cAAc,EAAE;AAAA,EAClF,EAAE,MAAM,qBAAoB,UAAU,cAAiB,UAAU,CAAC,oBAAoB,EAAE;AAAA,EACxF,EAAE,MAAM,UAAoB,UAAU,cAAiB,UAAU,CAAC,SAAS,EAAE;AAAA,EAC7E,EAAE,MAAM,SAAoB,UAAU,cAAiB,UAAU,CAAC,QAAQ,EAAE;AAAA,EAC5E,EAAE,MAAM,iBAAoB,UAAU,cAAiB,UAAU,CAAC,YAAY,EAAE;AAAA,EAChF,EAAE,MAAM,UAAoB,UAAU,cAAiB,UAAU,CAAC,SAAS,EAAE;AAAA,EAC7E,EAAE,MAAM,cAAoB,UAAU,cAAiB,UAAU,CAAC,aAAa,EAAE;AAAA,EACjF,EAAE,MAAM,WAAoB,UAAU,cAAiB,UAAU,CAAC,UAAU,EAAE;AAAA;AAAA,EAG9E,EAAE,MAAM,aAAoB,UAAU,iBAAiB,UAAU,CAAC,YAAY,EAAE;AAAA,EAChF,EAAE,MAAM,WAAoB,UAAU,iBAAiB,UAAU,CAAC,YAAY,SAAS,EAAE;AAAA,EACzF,EAAE,MAAM,eAAoB,UAAU,iBAAiB,UAAU,CAAC,cAAc,EAAE;AAAA,EAClF,EAAE,MAAM,eAAoB,UAAU,iBAAiB,UAAU,CAAC,cAAc,EAAE;AAAA,EAClF,EAAE,MAAM,aAAoB,UAAU,iBAAiB,UAAU,CAAC,YAAY,EAAE;AAAA,EAChF,EAAE,MAAM,SAAoB,UAAU,iBAAiB,UAAU,CAAC,QAAQ,EAAE;AAAA,EAC5E,EAAE,MAAM,UAAoB,UAAU,iBAAiB,UAAU,CAAC,SAAS,EAAE;AAAA,EAC7E,EAAE,MAAM,eAAoB,UAAU,iBAAiB,UAAU,CAAC,cAAc,EAAE;AACpF;;;ACfO,SAAS,UAAU,OAA2C;AACnE,QAAM,KAAK,MAAM,aAAa;AAG9B,aAAW,OAAO,YAAY;AAC5B,eAAW,WAAW,IAAI,UAAU;AAClC,UAAI,QAAQ,KAAK,EAAE,GAAG;AACpB,eAAO;AAAA,UACL,OAAO;AAAA,UACP,SAAS,IAAI;AAAA,UACb,YAAY;AAAA,UACZ,iBAAiB;AAAA,UACjB,UAAU,IAAI;AAAA,UACd,cAAc;AAAA,QAChB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,QAAM,kBAAkB,gBAAgB,IAAI,MAAM,WAAW,CAAC,CAAC;AAC/D,MAAI,gBAAiB,QAAO,EAAE,GAAG,iBAAiB,cAAc,GAAG;AAGnE,MAAI,eAAe,EAAE,GAAG;AACtB,WAAO;AAAA,MACL,OAAO;AAAA,MACP,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,iBAAiB;AAAA,MACjB,UAAU;AAAA,MACV,cAAc;AAAA,IAChB;AAAA,EACF;AAEA,SAAO;AAAA,IACL,OAAO;AAAA,IACP,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,iBAAiB;AAAA,IACjB,UAAU;AAAA,IACV,cAAc;AAAA,EAChB;AACF;AAEA,SAAS,gBACP,IACA,SACiD;AAEjD,MAAI,kBAAkB,KAAK,EAAE,GAAG;AAC9B,WAAO,EAAE,OAAO,MAAM,SAAS,kBAAkB,YAAY,UAAU,iBAAiB,aAAa,UAAU,cAAc;AAAA,EAC/H;AACA,MAAI,aAAa,KAAK,EAAE,GAAG;AACzB,WAAO,EAAE,OAAO,MAAM,SAAS,aAAa,YAAY,QAAQ,iBAAiB,aAAa,UAAU,cAAc;AAAA,EACxH;AACA,MAAI,YAAY,KAAK,EAAE,GAAG;AACxB,WAAO,EAAE,OAAO,MAAM,SAAS,YAAY,YAAY,QAAQ,iBAAiB,aAAa,UAAU,cAAc;AAAA,EACvH;AAGA,MAAI,GAAG,KAAK,EAAE,SAAS,IAAI;AACzB,WAAO,EAAE,OAAO,MAAM,SAAS,MAAM,YAAY,OAAO,iBAAiB,aAAa,UAAU,cAAc;AAAA,EAChH;AAMA,QAAM,gBAAgB,CAAC,CAAC,QAAQ,iBAAiB;AACjD,QAAM,oBAAoB,CAAC,CAAC,QAAQ,iBAAiB;AACrD,QAAM,gBAAgB,sDAAsD,KAAK,EAAE;AACnF,MAAI,CAAC,iBAAiB,CAAC,qBAAqB,CAAC,eAAe;AAC1D,WAAO,EAAE,OAAO,MAAM,SAAS,MAAM,YAAY,OAAO,iBAAiB,aAAa,UAAU,cAAc;AAAA,EAChH;AAEA,SAAO;AACT;AAEA,SAAS,eAAe,IAAqB;AAC3C,SACE,4EAA4E,KAAK,EAAE,KACnF,CAAC,oCAAoC,KAAK,EAAE;AAEhD;","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
var defaultConfig = {
|
|
3
3
|
contentDir: "content",
|
|
4
4
|
dataDir: "data",
|
|
5
|
+
stripSegments: [],
|
|
5
6
|
dashboard: true,
|
|
6
7
|
dashboardSecret: "",
|
|
7
8
|
notifications: {},
|
|
@@ -45,7 +46,8 @@ function withThirdAudience(options = {}, nextConfig = {}) {
|
|
|
45
46
|
...nextConfig.env,
|
|
46
47
|
TA_CONTENT_DIR: config.contentDir,
|
|
47
48
|
TA_DATA_DIR: config.dataDir,
|
|
48
|
-
TA_DASHBOARD_ENABLED: String(config.dashboard)
|
|
49
|
+
TA_DASHBOARD_ENABLED: String(config.dashboard),
|
|
50
|
+
TA_STRIP_SEGMENTS: config.stripSegments.join(",")
|
|
49
51
|
}
|
|
50
52
|
};
|
|
51
53
|
}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/config.ts","../src/core/with-third-audience.ts","../src/core/middleware.ts","../src/detection/known-patterns.ts","../src/detection/bot-detection-pipeline.ts"],"sourcesContent":["export interface ThirdAudienceConfig {\n /** Directory containing .mdx files, relative to project root. Default: 'content' */\n contentDir?: string\n /** Directory for JSONL data files. Default: 'data' */\n dataDir?: string\n /** Mount the /third-audience/ dashboard. Default: true */\n dashboard?: boolean\n /** Secret for dashboard access (HTTP Basic or bearer). Required when dashboard: true */\n dashboardSecret?: string\n notifications?: {\n email?: { smtp: string; to: string; from?: string }\n slack?: { webhookUrl: string }\n }\n bots?: {\n allowlist?: string[]\n blocklist?: string[]\n }\n cache?: {\n /** Cache TTL in seconds. Default: 3600 */\n ttl?: number\n /** Max in-memory entries. Default: 500 */\n maxMemoryEntries?: number\n }\n}\n\nexport const defaultConfig: Required<ThirdAudienceConfig> = {\n contentDir: 'content',\n dataDir: 'data',\n dashboard: true,\n dashboardSecret: '',\n notifications: {},\n bots: { allowlist: [], blocklist: [] },\n cache: { ttl: 3600, maxMemoryEntries: 500 },\n}\n\nexport function resolveConfig(partial: ThirdAudienceConfig = {}): Required<ThirdAudienceConfig> {\n return {\n ...defaultConfig,\n ...partial,\n bots: { ...defaultConfig.bots, ...partial.bots },\n cache: { ...defaultConfig.cache, ...partial.cache },\n notifications: { ...defaultConfig.notifications, ...partial.notifications },\n }\n}\n","import type { NextConfig } from 'next'\nimport { resolveConfig, type ThirdAudienceConfig } from './config.js'\n\n/**\n * Wraps next.config.ts to inject Third Audience rewrites and headers.\n *\n * Usage:\n * import { withThirdAudience } from 'third-audience-mdx'\n * export default withThirdAudience({ contentDir: 'content' })\n */\nexport function withThirdAudience(\n options: ThirdAudienceConfig = {},\n nextConfig: NextConfig = {}\n): NextConfig {\n const config = resolveConfig(options)\n\n return {\n ...nextConfig,\n async headers() {\n const existing = await nextConfig.headers?.() ?? []\n return [\n ...existing,\n {\n source: '/:path*.md',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n {\n source: '/llms.txt',\n headers: [{ key: 'Content-Type', value: 'text/plain; charset=utf-8' }],\n },\n {\n source: '/okf/:path*',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n ]\n },\n env: {\n ...nextConfig.env,\n TA_CONTENT_DIR: config.contentDir,\n TA_DATA_DIR: config.dataDir,\n TA_DASHBOARD_ENABLED: String(config.dashboard),\n },\n }\n}\n","import { NextResponse, type NextRequest } from 'next/server'\n\nconst COOKIE_NAME = 'ta_session'\nconst RESET_COOKIE = 'ta_session_reset'\n\n/**\n * Third Audience middleware — Edge-runtime compatible (no Node.js crypto).\n *\n * Auth guard uses cookie presence only; HMAC verification happens in the\n * route handler (Node.js runtime) where crypto is available.\n *\n * Wire up in middleware.ts:\n * export { thirdAudienceMiddleware as middleware } from 'third-audience-mdx'\n * export const config = { matcher: ['/((?!_next|api).*)'] }\n */\nexport function thirdAudienceMiddleware(req: NextRequest): NextResponse | null {\n const { pathname } = req.nextUrl\n const accept = req.headers.get('accept') ?? ''\n\n // Dashboard auth guard — cookie presence check (HMAC verified in route handler)\n if (pathname.startsWith('/third-audience') && !pathname.startsWith('/third-audience/login')) {\n const session = req.cookies.get(COOKIE_NAME)?.value\n if (!session) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // Password reset guard — /third-audience/login?reset=1 requires reset or session cookie\n if (pathname === '/third-audience/login' && req.nextUrl.searchParams.get('reset') === '1') {\n const resetCookie = req.cookies.get(RESET_COOKIE)?.value\n const sessionCookie = req.cookies.get(COOKIE_NAME)?.value\n if (!resetCookie && !sessionCookie) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n loginUrl.search = ''\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // /third-audience/login → rewrite to login route handler (GET/POST)\n if (pathname === '/third-audience/login') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/login'\n return NextResponse.rewrite(url)\n }\n\n // .md URL → rewrite to our internal markdown route handler\n if (pathname.endsWith('.md')) {\n const slug = pathname.slice(0, -3) // strip .md\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${slug}`\n return NextResponse.rewrite(url)\n }\n\n // Accept: text/markdown header → rewrite to markdown route\n if (accept.includes('text/markdown')) {\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${pathname}`\n return NextResponse.rewrite(url)\n }\n\n // /okf or /okf/* → rewrite to OKF bundle handler\n // [...path] catch-all requires at least one segment, so /okf → /okf/index\n if (pathname.startsWith('/okf')) {\n const url = req.nextUrl.clone()\n const rest = pathname.slice(4) // '' or '/something'\n url.pathname = `/api/third-audience/okf${rest || '/index'}`\n return NextResponse.rewrite(url)\n }\n\n // /llms.txt → rewrite to discovery handler\n if (pathname === '/llms.txt') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/llms-txt'\n return NextResponse.rewrite(url)\n }\n\n // /sitemap-ai.xml → rewrite to AI sitemap handler\n if (pathname === '/sitemap-ai.xml') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/sitemap-ai'\n return NextResponse.rewrite(url)\n }\n\n // No match — let the caller's middleware chain continue\n return null\n}\n\n","/** Known AI crawler and search engine user-agent patterns. */\nexport interface KnownBot {\n name: string\n category: 'ai_crawler' | 'search_engine'\n patterns: RegExp[]\n}\n\nexport const KNOWN_BOTS: KnownBot[] = [\n // AI Crawlers\n { name: 'ClaudeBot', category: 'ai_crawler', patterns: [/claudebot/i, /claude-web/i] },\n { name: 'GPTBot', category: 'ai_crawler', patterns: [/gptbot/i] },\n { name: 'ChatGPT-User', category: 'ai_crawler', patterns: [/chatgpt-user/i] },\n { name: 'PerplexityBot', category: 'ai_crawler', patterns: [/perplexitybot/i] },\n { name: 'Googlebot-AI', category: 'ai_crawler', patterns: [/google-extended/i, /googleother/i] },\n { name: 'FacebookBot', category: 'ai_crawler', patterns: [/facebookbot/i] },\n { name: 'Applebot-Extended',category: 'ai_crawler', patterns: [/applebot-extended/i] },\n { name: 'YouBot', category: 'ai_crawler', patterns: [/youbot/i] },\n { name: 'CCBot', category: 'ai_crawler', patterns: [/ccbot/i] },\n { name: 'CohereCrawler', category: 'ai_crawler', patterns: [/cohere-ai/i] },\n { name: 'AI2Bot', category: 'ai_crawler', patterns: [/ai2bot/i] },\n { name: 'Bytespider', category: 'ai_crawler', patterns: [/bytespider/i] },\n { name: 'Diffbot', category: 'ai_crawler', patterns: [/diffbot/i] },\n\n // Search Engines\n { name: 'Googlebot', category: 'search_engine', patterns: [/googlebot/i] },\n { name: 'Bingbot', category: 'search_engine', patterns: [/bingbot/i, /msnbot/i] },\n { name: 'DuckDuckBot', category: 'search_engine', patterns: [/duckduckbot/i] },\n { name: 'Baiduspider', category: 'search_engine', patterns: [/baiduspider/i] },\n { name: 'YandexBot', category: 'search_engine', patterns: [/yandexbot/i] },\n { name: 'Sogou', category: 'search_engine', patterns: [/sogou/i] },\n { name: 'Exabot', category: 'search_engine', patterns: [/exabot/i] },\n { name: 'ia_archiver', category: 'search_engine', patterns: [/ia_archiver/i] },\n]\n","import type { BotDetectionResult } from './bot-detection-result.js'\nimport { KNOWN_BOTS } from './known-patterns.js'\n\nexport interface DetectBotInput {\n userAgent: string\n /** Optional: headers map for heuristic checks */\n headers?: Record<string, string | string[] | undefined>\n /** Optional: IP address */\n ip?: string\n}\n\n/**\n * Three-layer bot detection pipeline:\n * 1. Known pattern matching (O(n) UA string match)\n * 2. Heuristic signals (missing headers, headless indicators)\n * 3. Auto-learner flag (unknown UAs that behave bot-like)\n */\nexport function detectBot(input: DetectBotInput): BotDetectionResult {\n const ua = input.userAgent ?? ''\n\n // Layer 1: known pattern match\n for (const bot of KNOWN_BOTS) {\n for (const pattern of bot.patterns) {\n if (pattern.test(ua)) {\n return {\n isBot: true,\n botName: bot.name,\n confidence: 'high',\n detectionMethod: 'known_pattern',\n category: bot.category,\n rawUserAgent: ua,\n }\n }\n }\n }\n\n // Layer 2: heuristics\n const heuristicResult = checkHeuristics(ua, input.headers ?? {})\n if (heuristicResult) return { ...heuristicResult, rawUserAgent: ua }\n\n // Layer 3: auto-learner — flag suspicious unknown UAs for review\n if (looksLikeBotUa(ua)) {\n return {\n isBot: true,\n botName: null,\n confidence: 'low',\n detectionMethod: 'auto_learned',\n category: 'unknown_bot',\n rawUserAgent: ua,\n }\n }\n\n return {\n isBot: false,\n botName: null,\n confidence: 'high',\n detectionMethod: 'none',\n category: 'human',\n rawUserAgent: ua,\n }\n}\n\nfunction checkHeuristics(\n ua: string,\n headers: Record<string, string | string[] | undefined>\n): Omit<BotDetectionResult, 'rawUserAgent'> | null {\n // Headless Chrome signals\n if (/headlesschrome/i.test(ua)) {\n return { isBot: true, botName: 'HeadlessChrome', confidence: 'medium', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/phantomjs/i.test(ua)) {\n return { isBot: true, botName: 'PhantomJS', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/selenium/i.test(ua)) {\n return { isBot: true, botName: 'Selenium', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Empty or very short UA is suspicious\n if (ua.trim().length < 10) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Missing typical browser headers — only a bot signal when the UA does NOT\n // present itself as a real browser. Genuine browsers occasionally arrive\n // without accept-language/accept-encoding (privacy extensions, proxies,\n // some CDNs), so we must not flag them on missing headers alone.\n const hasAcceptLang = !!headers['accept-language']\n const hasAcceptEncoding = !!headers['accept-encoding']\n const claimsBrowser = /chrome|firefox|safari|edge|opera|gecko|applewebkit/i.test(ua)\n if (!hasAcceptLang && !hasAcceptEncoding && !claimsBrowser) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n return null\n}\n\nfunction looksLikeBotUa(ua: string): boolean {\n return (\n /bot|crawler|spider|scraper|fetch|http|python|curl|java|ruby|go-http|node/i.test(ua) &&\n !/chrome|firefox|safari|edge|opera/i.test(ua)\n )\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/core/config.ts","../src/core/with-third-audience.ts","../src/core/middleware.ts","../src/detection/known-patterns.ts","../src/detection/bot-detection-pipeline.ts"],"sourcesContent":["export interface ThirdAudienceConfig {\n /** Directory containing .mdx files, relative to project root. Default: 'content' */\n contentDir?: string\n /** Directory for JSONL data files. Default: 'data' */\n dataDir?: string\n /**\n * URL path segments to strip when mapping a request slug to a content file.\n * Use when your route structure differs from your file layout — e.g. URLs\n * are /en/learn/hydroponics/x but files live at content/en/hydroponics/x.mdx.\n * Set ['learn'] to drop the 'learn' segment. Default: [] (no rewriting).\n */\n stripSegments?: string[]\n /** Mount the /third-audience/ dashboard. Default: true */\n dashboard?: boolean\n /** Secret for dashboard access (HTTP Basic or bearer). Required when dashboard: true */\n dashboardSecret?: string\n notifications?: {\n email?: { smtp: string; to: string; from?: string }\n slack?: { webhookUrl: string }\n }\n bots?: {\n allowlist?: string[]\n blocklist?: string[]\n }\n cache?: {\n /** Cache TTL in seconds. Default: 3600 */\n ttl?: number\n /** Max in-memory entries. Default: 500 */\n maxMemoryEntries?: number\n }\n}\n\nexport const defaultConfig: Required<ThirdAudienceConfig> = {\n contentDir: 'content',\n dataDir: 'data',\n stripSegments: [],\n dashboard: true,\n dashboardSecret: '',\n notifications: {},\n bots: { allowlist: [], blocklist: [] },\n cache: { ttl: 3600, maxMemoryEntries: 500 },\n}\n\nexport function resolveConfig(partial: ThirdAudienceConfig = {}): Required<ThirdAudienceConfig> {\n return {\n ...defaultConfig,\n ...partial,\n bots: { ...defaultConfig.bots, ...partial.bots },\n cache: { ...defaultConfig.cache, ...partial.cache },\n notifications: { ...defaultConfig.notifications, ...partial.notifications },\n }\n}\n","import type { NextConfig } from 'next'\nimport { resolveConfig, type ThirdAudienceConfig } from './config.js'\n\n/**\n * Wraps next.config.ts to inject Third Audience rewrites and headers.\n *\n * Usage:\n * import { withThirdAudience } from 'third-audience-mdx'\n * export default withThirdAudience({ contentDir: 'content' })\n */\nexport function withThirdAudience(\n options: ThirdAudienceConfig = {},\n nextConfig: NextConfig = {}\n): NextConfig {\n const config = resolveConfig(options)\n\n return {\n ...nextConfig,\n async headers() {\n const existing = await nextConfig.headers?.() ?? []\n return [\n ...existing,\n {\n source: '/:path*.md',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n {\n source: '/llms.txt',\n headers: [{ key: 'Content-Type', value: 'text/plain; charset=utf-8' }],\n },\n {\n source: '/okf/:path*',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n ]\n },\n env: {\n ...nextConfig.env,\n TA_CONTENT_DIR: config.contentDir,\n TA_DATA_DIR: config.dataDir,\n TA_DASHBOARD_ENABLED: String(config.dashboard),\n TA_STRIP_SEGMENTS: config.stripSegments.join(','),\n },\n }\n}\n","import { NextResponse, type NextRequest } from 'next/server'\n\nconst COOKIE_NAME = 'ta_session'\nconst RESET_COOKIE = 'ta_session_reset'\n\n/**\n * Third Audience middleware — Edge-runtime compatible (no Node.js crypto).\n *\n * Auth guard uses cookie presence only; HMAC verification happens in the\n * route handler (Node.js runtime) where crypto is available.\n *\n * Wire up in middleware.ts:\n * export { thirdAudienceMiddleware as middleware } from 'third-audience-mdx'\n * export const config = { matcher: ['/((?!_next|api).*)'] }\n */\nexport function thirdAudienceMiddleware(req: NextRequest): NextResponse | null {\n const { pathname } = req.nextUrl\n const accept = req.headers.get('accept') ?? ''\n\n // Dashboard auth guard — cookie presence check (HMAC verified in route handler)\n if (pathname.startsWith('/third-audience') && !pathname.startsWith('/third-audience/login')) {\n const session = req.cookies.get(COOKIE_NAME)?.value\n if (!session) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // Password reset guard — /third-audience/login?reset=1 requires reset or session cookie\n if (pathname === '/third-audience/login' && req.nextUrl.searchParams.get('reset') === '1') {\n const resetCookie = req.cookies.get(RESET_COOKIE)?.value\n const sessionCookie = req.cookies.get(COOKIE_NAME)?.value\n if (!resetCookie && !sessionCookie) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n loginUrl.search = ''\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // /third-audience/login → rewrite to login route handler (GET/POST)\n if (pathname === '/third-audience/login') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/login'\n return NextResponse.rewrite(url)\n }\n\n // .md URL → rewrite to our internal markdown route handler\n if (pathname.endsWith('.md')) {\n const slug = pathname.slice(0, -3) // strip .md\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${slug}`\n return NextResponse.rewrite(url)\n }\n\n // Accept: text/markdown header → rewrite to markdown route\n if (accept.includes('text/markdown')) {\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${pathname}`\n return NextResponse.rewrite(url)\n }\n\n // /okf or /okf/* → rewrite to OKF bundle handler\n // [...path] catch-all requires at least one segment, so /okf → /okf/index\n if (pathname.startsWith('/okf')) {\n const url = req.nextUrl.clone()\n const rest = pathname.slice(4) // '' or '/something'\n url.pathname = `/api/third-audience/okf${rest || '/index'}`\n return NextResponse.rewrite(url)\n }\n\n // /llms.txt → rewrite to discovery handler\n if (pathname === '/llms.txt') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/llms-txt'\n return NextResponse.rewrite(url)\n }\n\n // /sitemap-ai.xml → rewrite to AI sitemap handler\n if (pathname === '/sitemap-ai.xml') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/sitemap-ai'\n return NextResponse.rewrite(url)\n }\n\n // No match — let the caller's middleware chain continue\n return null\n}\n\n","/** Known AI crawler and search engine user-agent patterns. */\nexport interface KnownBot {\n name: string\n category: 'ai_crawler' | 'search_engine'\n patterns: RegExp[]\n}\n\nexport const KNOWN_BOTS: KnownBot[] = [\n // AI Crawlers\n { name: 'ClaudeBot', category: 'ai_crawler', patterns: [/claudebot/i, /claude-web/i] },\n { name: 'GPTBot', category: 'ai_crawler', patterns: [/gptbot/i] },\n { name: 'ChatGPT-User', category: 'ai_crawler', patterns: [/chatgpt-user/i] },\n { name: 'PerplexityBot', category: 'ai_crawler', patterns: [/perplexitybot/i] },\n { name: 'Googlebot-AI', category: 'ai_crawler', patterns: [/google-extended/i, /googleother/i] },\n { name: 'FacebookBot', category: 'ai_crawler', patterns: [/facebookbot/i] },\n { name: 'Applebot-Extended',category: 'ai_crawler', patterns: [/applebot-extended/i] },\n { name: 'YouBot', category: 'ai_crawler', patterns: [/youbot/i] },\n { name: 'CCBot', category: 'ai_crawler', patterns: [/ccbot/i] },\n { name: 'CohereCrawler', category: 'ai_crawler', patterns: [/cohere-ai/i] },\n { name: 'AI2Bot', category: 'ai_crawler', patterns: [/ai2bot/i] },\n { name: 'Bytespider', category: 'ai_crawler', patterns: [/bytespider/i] },\n { name: 'Diffbot', category: 'ai_crawler', patterns: [/diffbot/i] },\n\n // Search Engines\n { name: 'Googlebot', category: 'search_engine', patterns: [/googlebot/i] },\n { name: 'Bingbot', category: 'search_engine', patterns: [/bingbot/i, /msnbot/i] },\n { name: 'DuckDuckBot', category: 'search_engine', patterns: [/duckduckbot/i] },\n { name: 'Baiduspider', category: 'search_engine', patterns: [/baiduspider/i] },\n { name: 'YandexBot', category: 'search_engine', patterns: [/yandexbot/i] },\n { name: 'Sogou', category: 'search_engine', patterns: [/sogou/i] },\n { name: 'Exabot', category: 'search_engine', patterns: [/exabot/i] },\n { name: 'ia_archiver', category: 'search_engine', patterns: [/ia_archiver/i] },\n]\n","import type { BotDetectionResult } from './bot-detection-result.js'\nimport { KNOWN_BOTS } from './known-patterns.js'\n\nexport interface DetectBotInput {\n userAgent: string\n /** Optional: headers map for heuristic checks */\n headers?: Record<string, string | string[] | undefined>\n /** Optional: IP address */\n ip?: string\n}\n\n/**\n * Three-layer bot detection pipeline:\n * 1. Known pattern matching (O(n) UA string match)\n * 2. Heuristic signals (missing headers, headless indicators)\n * 3. Auto-learner flag (unknown UAs that behave bot-like)\n */\nexport function detectBot(input: DetectBotInput): BotDetectionResult {\n const ua = input.userAgent ?? ''\n\n // Layer 1: known pattern match\n for (const bot of KNOWN_BOTS) {\n for (const pattern of bot.patterns) {\n if (pattern.test(ua)) {\n return {\n isBot: true,\n botName: bot.name,\n confidence: 'high',\n detectionMethod: 'known_pattern',\n category: bot.category,\n rawUserAgent: ua,\n }\n }\n }\n }\n\n // Layer 2: heuristics\n const heuristicResult = checkHeuristics(ua, input.headers ?? {})\n if (heuristicResult) return { ...heuristicResult, rawUserAgent: ua }\n\n // Layer 3: auto-learner — flag suspicious unknown UAs for review\n if (looksLikeBotUa(ua)) {\n return {\n isBot: true,\n botName: null,\n confidence: 'low',\n detectionMethod: 'auto_learned',\n category: 'unknown_bot',\n rawUserAgent: ua,\n }\n }\n\n return {\n isBot: false,\n botName: null,\n confidence: 'high',\n detectionMethod: 'none',\n category: 'human',\n rawUserAgent: ua,\n }\n}\n\nfunction checkHeuristics(\n ua: string,\n headers: Record<string, string | string[] | undefined>\n): Omit<BotDetectionResult, 'rawUserAgent'> | null {\n // Headless Chrome signals\n if (/headlesschrome/i.test(ua)) {\n return { isBot: true, botName: 'HeadlessChrome', confidence: 'medium', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/phantomjs/i.test(ua)) {\n return { isBot: true, botName: 'PhantomJS', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/selenium/i.test(ua)) {\n return { isBot: true, botName: 'Selenium', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Empty or very short UA is suspicious\n if (ua.trim().length < 10) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Missing typical browser headers — only a bot signal when the UA does NOT\n // present itself as a real browser. Genuine browsers occasionally arrive\n // without accept-language/accept-encoding (privacy extensions, proxies,\n // some CDNs), so we must not flag them on missing headers alone.\n const hasAcceptLang = !!headers['accept-language']\n const hasAcceptEncoding = !!headers['accept-encoding']\n const claimsBrowser = /chrome|firefox|safari|edge|opera|gecko|applewebkit/i.test(ua)\n if (!hasAcceptLang && !hasAcceptEncoding && !claimsBrowser) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n return null\n}\n\nfunction looksLikeBotUa(ua: string): boolean {\n return (\n /bot|crawler|spider|scraper|fetch|http|python|curl|java|ruby|go-http|node/i.test(ua) &&\n !/chrome|firefox|safari|edge|opera/i.test(ua)\n )\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "third-audience-mdx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Serve AI-optimized Markdown to LLM crawlers from MDX content sites. Track bot visits, citations, and AI discoverability.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|