apptvty 0.2.0 → 0.2.1
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/{chunk-OTPVLSG5.mjs → chunk-GMQN6656.mjs} +15 -3
- package/dist/{chunk-OTPVLSG5.mjs.map → chunk-GMQN6656.mjs.map} +1 -1
- package/dist/{chunk-454YBHM2.mjs → chunk-JNM4IJDR.mjs} +4 -3
- package/dist/{chunk-454YBHM2.mjs.map → chunk-JNM4IJDR.mjs.map} +1 -1
- package/dist/{chunk-2KXDQCUZ.mjs → chunk-OZT7PIDN.mjs} +4 -3
- package/dist/{chunk-2KXDQCUZ.mjs.map → chunk-OZT7PIDN.mjs.map} +1 -1
- package/dist/cli.js +51 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -3
- package/dist/middleware/express.d.mts +1 -1
- package/dist/middleware/express.d.ts +1 -1
- package/dist/middleware/express.js +16 -3
- package/dist/middleware/express.js.map +1 -1
- package/dist/middleware/express.mjs +2 -2
- package/dist/middleware/nextjs.d.mts +1 -1
- package/dist/middleware/nextjs.d.ts +1 -1
- package/dist/middleware/nextjs.js +16 -3
- package/dist/middleware/nextjs.js.map +1 -1
- package/dist/middleware/nextjs.mjs +2 -2
- package/dist/{types-D2A_0sPm.d.mts → types-Zt2qHOrW.d.mts} +6 -0
- package/dist/{types-D2A_0sPm.d.ts → types-Zt2qHOrW.d.ts} +6 -0
- package/package.json +1 -1
|
@@ -721,8 +721,20 @@ function getOrigin(url) {
|
|
|
721
721
|
// src/dashboard-handler.ts
|
|
722
722
|
function createDashboardHandler(client, config) {
|
|
723
723
|
return async function handleDashboard(req) {
|
|
724
|
-
const { path, method } = req;
|
|
725
|
-
if (
|
|
724
|
+
const { path, method, authHeader } = req;
|
|
725
|
+
if (config.dashboardSecret) {
|
|
726
|
+
const url = new URL(path, "http://localhost");
|
|
727
|
+
const secretParam = url.searchParams.get("secret");
|
|
728
|
+
const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
|
|
729
|
+
const isAuthorized = secretParam === config.dashboardSecret || bearerToken === config.dashboardSecret;
|
|
730
|
+
if (!isAuthorized) {
|
|
731
|
+
return jsonResponse(401, {
|
|
732
|
+
error: "Unauthorized",
|
|
733
|
+
message: "Dashboard access requires a valid secret. Please set APPTVTY_DASHBOARD_SECRET."
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (path.includes("/api/overview")) {
|
|
726
738
|
const data = await client.getSiteStats();
|
|
727
739
|
return jsonResponse(200, data);
|
|
728
740
|
}
|
|
@@ -1081,4 +1093,4 @@ export {
|
|
|
1081
1093
|
injectIntoHtml,
|
|
1082
1094
|
buildSponsoredHeader
|
|
1083
1095
|
};
|
|
1084
|
-
//# sourceMappingURL=chunk-
|
|
1096
|
+
//# sourceMappingURL=chunk-GMQN6656.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/logger.ts","../src/crawler.ts","../src/query-handler.ts","../src/dashboard-handler.ts","../src/ad-injection.ts"],"sourcesContent":["/**\n * HTTP client for the Apptvty API.\n *\n * Handles:\n * - Batch log ingestion (/v1/logs/batch)\n * - Query processing (/v1/query) — returns answer + optional ad\n * - Impression logging (/v1/impressions) — triggers billing for ads\n */\n\nimport type {\n ApptvtyConfig,\n BackendQueryResponse,\n CampaignRecord,\n CrawlerBreakdown,\n CreateCampaignParams,\n DailyStat,\n ImpressionLog,\n InsufficientBalanceError,\n PageAdsResponse,\n QueryRequest,\n RecentActivityItem,\n RecentQueryItem,\n RequestLogEntry,\n SiteOverviewStats,\n SiteWalletInfo,\n UpdateCampaignParams,\n} from './types.js';\n\nconst DEFAULT_BASE_URL = 'https://api.apptvty.com';\n\nfunction resolveBaseUrl(config: ApptvtyConfig): string {\n const raw: string =\n config.baseUrl ??\n (typeof process !== 'undefined' ? process.env?.APPTVTY_API_URL : undefined) ??\n DEFAULT_BASE_URL;\n return raw.replace(/\\/$/, '');\n}\n\nexport class ApptvtyClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly siteId: string;\n private readonly debug: boolean;\n\n private x402Token?: string;\n\n constructor(config: ApptvtyConfig) {\n this.baseUrl = resolveBaseUrl(config);\n this.apiKey = config.apiKey;\n this.siteId = config.siteId;\n this.debug = config.debug ?? false;\n }\n\n /**\n * Set the X402 (LSAT) token for subsequent requests.\n * This is called after the agent has successfully paid an X402 challenge.\n */\n setX402Token(macaroon: string, preimage: string): void {\n this.x402Token = `LSAT ${macaroon}:${preimage}`;\n }\n\n /**\n * Send a batch of request log entries to the Apptvty ingestion API.\n * Called by the logger's auto-flush — not called directly by user code.\n */\n async sendLogs(logs: RequestLogEntry[]): Promise<void> {\n if (logs.length === 0) return;\n\n try {\n await this.post('/v1/logs/batch', { logs });\n this.log(`Flushed ${logs.length} log(s)`);\n } catch (err) {\n if (err instanceof ApptvtyTrialExpiredError) {\n // Always visible — coding agents and developers need to see this\n console.warn(\n `\\n[apptvty] ⚠ FREE TRIAL EXPIRED\\n` +\n `[apptvty] Log in and upgrade to keep receiving agent analytics.\\n` +\n `[apptvty] Dashboard: ${err.dashboardUrl}\\n`,\n );\n return;\n }\n // Other logging failures must never propagate to the user's request path\n this.warn('Failed to send logs:', err);\n }\n }\n\n /**\n * Send a query to the Apptvty backend.\n * The backend runs RAG against the site's indexed content and,\n * if ads are enabled for the site, attaches a relevant sponsored ad.\n *\n * @throws on non-retryable errors (network errors, 5xx) — callers should catch\n */\n async query(req: QueryRequest): Promise<BackendQueryResponse> {\n // ApptvtyTrialExpiredError propagates to the query handler, which surfaces it to the agent\n const response = await this.post<BackendQueryResponse>('/v1/query', req);\n return response;\n }\n\n /**\n * Get relevant ads for a page (for HTML injection when crawler is detected).\n * Used by middleware to inject ads into HTML responses.\n */\n async getAdsForPage(req: { site_id: string; page_path: string }): Promise<PageAdsResponse> {\n const response = await this.post<PageAdsResponse>('/v1/ads/for-page', req);\n return response;\n }\n\n /**\n * Log an ad impression back to Apptvty.\n *\n * Called after returning a query response that contains a `sponsored` block.\n * This is what triggers the billing cycle:\n * - Advertiser gets charged (debited from their USDC ad budget)\n * - Publisher (the website) gets credited in USDC\n *\n * This call is fire-and-forget. The SDK logs a warning on failure\n * but does not throw — a missed impression log is better than\n * breaking the query response.\n */\n async logImpression(impression: ImpressionLog): Promise<void> {\n try {\n await this.post('/v1/impressions', impression);\n this.log(`Impression logged: ${impression.impression_id}`);\n } catch (err) {\n this.warn('Failed to log impression (billing may be delayed):', err);\n }\n }\n\n // ─── Analytics (for coding agents) ───────────────────────────────────────────\n // These allow agents to check activity, logs, and errors without a human.\n\n /** Get 30-day traffic overview (requests, AI %, crawlers, queries). */\n async getSiteStats(): Promise<SiteOverviewStats> {\n return this.get<SiteOverviewStats>(`/v1/sites/${this.siteId}/stats`);\n }\n\n /** Get day-by-day stats (default 30 days, max 90). */\n async getSiteDailyStats(days = 30): Promise<{ days: number; stats: DailyStat[] }> {\n return this.get(`/v1/sites/${this.siteId}/stats/daily`, { days: String(days) });\n }\n\n /** Get recent activity logs (last 48h). */\n async getRecentActivity(siteId: string, limit = 50, offset = 0): Promise<RecentActivityItem[]> {\n return this.get(`/v1/sites/${siteId}/activity`, {\n limit: String(limit),\n offset: String(offset),\n });\n }\n\n /** Get recent agent queries. */\n async getRecentQueries(siteId: string, limit = 20, offset = 0): Promise<RecentQueryItem[]> {\n return this.get(`/v1/sites/${siteId}/queries`, {\n limit: String(limit),\n offset: String(offset),\n });\n }\n\n /** Get crawler breakdown by type (default 30 days). */\n async getSiteCrawlers(days = 30): Promise<{ days: number; crawlers: CrawlerBreakdown[] }> {\n return this.get(`/v1/sites/${this.siteId}/crawlers`, { days: String(days) });\n }\n\n /** Get recent activity feed (last hour, default 50 items, max 200). */\n async getSiteActivity(limit = 50): Promise<{ activity: RecentActivityItem[] }> {\n return this.get(`/v1/sites/${this.siteId}/activity`, { limit: String(limit) });\n }\n\n /** Get recent agent queries (default 50, max 200). */\n async getSiteQueries(limit = 50): Promise<{ queries: RecentQueryItem[] }> {\n return this.get(`/v1/sites/${this.siteId}/queries`, { limit: String(limit) });\n }\n\n /** Get wallet balance and earnings. */\n async getSiteWallet(): Promise<SiteWalletInfo> {\n return this.get<SiteWalletInfo>(`/v1/sites/${this.siteId}/wallet`);\n }\n\n // ─── Campaign management (for coding agents) ────────────────────────────────\n //\n // A coding agent that installed the SDK can create and manage ad campaigns\n // programmatically — no human dashboard login required.\n //\n // Typical agentic advertiser flow:\n // 1. Check wallet balance: const w = await client.getSiteWallet()\n // 2. Fund wallet if needed: send USDC to w.wallet_address on Base chain\n // 3. Create campaign: await client.createCampaign({ ... })\n // 4. Monitor performance: await client.getCampaign(id)\n // 5. Adjust or pause: await client.updateCampaign(id, { status: 'paused' })\n\n /**\n * Create an ad campaign. The campaign goes live immediately once the wallet\n * has sufficient balance.\n *\n * If the wallet balance is too low, throws `ApptvtyInsufficientBalanceError`\n * which includes deposit instructions so the agent can fund the wallet and retry.\n *\n * @example\n * // Site-based: ad copy derived from your site's crawled content\n * const campaign = await client.createCampaign({\n * name: 'My Kubernetes Blog',\n * advertiser_site_id: 'site_abc123',\n * keywords: ['kubernetes', 'devops', 'containers'],\n * categories: ['technology'],\n * bid_per_view_usdc: 0.001,\n * daily_budget_usdc: 1.0,\n * total_budget_usdc: 20.0,\n * });\n *\n * // Static: manually written ad copy\n * const campaign = await client.createCampaign({\n * name: 'My Blog — Static',\n * ad_text: 'Deep dives on Kubernetes, written by practitioners.',\n * landing_url: 'https://myblog.com',\n * keywords: ['kubernetes', 'devops'],\n * categories: ['technology'],\n * bid_per_view_usdc: 0.001,\n * daily_budget_usdc: 1.0,\n * total_budget_usdc: 10.0,\n * });\n */\n async createCampaign(params: CreateCampaignParams): Promise<CampaignRecord> {\n try {\n return await this.post<CampaignRecord>('/v1/campaigns', params);\n } catch (err) {\n if (err instanceof ApptvtyApiError && err.statusCode === 402) {\n let details: InsufficientBalanceError | undefined;\n try {\n const body = JSON.parse(err.body);\n details = body?.error as InsufficientBalanceError;\n } catch { /* ignore parse errors */ }\n throw new ApptvtyInsufficientBalanceError(details);\n }\n throw err;\n }\n }\n\n /**\n * List all campaigns for this account.\n * Also returns the schema (valid categories, minimum bid) so the agent\n * knows valid field values without guessing.\n */\n async listCampaigns(): Promise<{\n campaigns: CampaignRecord[];\n total: number;\n schema: { valid_categories: string[]; valid_statuses: string[]; bid_per_view_usdc_minimum: number };\n }> {\n return this.get('/v1/campaigns');\n }\n\n /**\n * Get a single campaign by ID, including current spend and impression count.\n * `budget_remaining_usdc` tells the agent how much budget is left before\n * the campaign auto-pauses.\n */\n async getCampaign(campaignId: string): Promise<CampaignRecord> {\n return this.get<CampaignRecord>(`/v1/campaigns/${campaignId}`);\n }\n\n /**\n * Partially update a campaign. Only fields present in `params` are changed.\n *\n * @example\n * // Pause a campaign\n * await client.updateCampaign(id, { status: 'paused' });\n *\n * // Increase bid and daily budget\n * await client.updateCampaign(id, { bid_per_view_usdc: 0.002, daily_budget_usdc: 5.0 });\n */\n async updateCampaign(campaignId: string, params: UpdateCampaignParams): Promise<CampaignRecord> {\n return this.patch<CampaignRecord>(`/v1/campaigns/${campaignId}`, params);\n }\n\n /**\n * Pause a campaign immediately. Equivalent to updateCampaign(id, { status: 'paused' }).\n * Campaigns are never deleted — billing history is retained.\n */\n async pauseCampaign(campaignId: string): Promise<void> {\n await this.delete(`/v1/campaigns/${campaignId}`);\n }\n\n /**\n * Resume a paused campaign.\n */\n async resumeCampaign(campaignId: string): Promise<void> {\n await this.patch(`/v1/campaigns/${campaignId}`, { status: 'active' });\n }\n\n private async get<T>(path: string, params?: Record<string, string>): Promise<T> {\n const url = new URL(`${this.baseUrl}${path}`);\n if (params) {\n Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));\n }\n const response = await fetch(url.toString(), {\n method: 'GET',\n headers: {\n Authorization: this.x402Token ?? `Bearer ${this.apiKey}`,\n 'User-Agent': 'apptvty-sdk/0.1.0',\n },\n signal: AbortSignal.timeout(10_000),\n });\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n throw new ApptvtyApiError(response.status, path, text);\n }\n return response.json() as Promise<T>;\n }\n\n private async patch<T>(path: string, body: unknown): Promise<T> {\n const url = `${this.baseUrl}${path}`;\n const response = await fetch(url, {\n method: 'PATCH',\n headers: {\n 'Authorization': this.x402Token ?? `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n 'User-Agent': 'apptvty-sdk/0.1.0',\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(10_000),\n });\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n throw new ApptvtyApiError(response.status, path, text);\n }\n return response.json() as Promise<T>;\n }\n\n private async delete(path: string): Promise<void> {\n const url = `${this.baseUrl}${path}`;\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n 'Authorization': this.x402Token ?? `Bearer ${this.apiKey}`,\n 'User-Agent': 'apptvty-sdk/0.1.0',\n },\n signal: AbortSignal.timeout(10_000),\n });\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n throw new ApptvtyApiError(response.status, path, text);\n }\n }\n\n /**\n * Settle an X402 challenge from the site's own USDC wallet balance.\n * Called automatically when `post()` receives a 402 with an X402 header.\n * Returns the preimage on success, or throws if the wallet balance is too low.\n */\n private async payX402Challenge(macaroon: string): Promise<string> {\n const url = `${this.baseUrl}/v1/x402/pay`;\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n 'User-Agent': 'apptvty-sdk/0.1.0',\n },\n body: JSON.stringify({ macaroon }),\n signal: AbortSignal.timeout(10_000),\n });\n\n const text = await response.text().catch(() => '');\n if (!response.ok) {\n let details: InsufficientBalanceError | undefined;\n try { details = JSON.parse(text)?.error?.details as InsufficientBalanceError; } catch {}\n throw new ApptvtyInsufficientBalanceError(details);\n }\n\n const json = JSON.parse(text) as { preimage: string };\n return json.preimage;\n }\n\n private async post<T>(path: string, body: unknown): Promise<T> {\n const url = `${this.baseUrl}${path}`;\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Authorization': this.x402Token ?? `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n 'User-Agent': 'apptvty-sdk/0.1.0',\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(10_000),\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n if (response.status === 402) {\n const authHeader = response.headers.get('WWW-Authenticate');\n if (authHeader?.includes('X402') || authHeader?.includes('LSAT')) {\n const macaroon = authHeader.match(/macaroon=\"([^\"]+)\"/)?.[1] || '';\n // Auto-pay from wallet balance and retry once\n const preimage = await this.payX402Challenge(macaroon);\n this.setX402Token(macaroon, preimage);\n this.log('X402 challenge auto-paid from wallet, retrying request');\n return this.post<T>(path, body);\n }\n let dashboardUrl = 'https://dashboard.apptvty.com/login';\n try {\n const json = JSON.parse(text);\n dashboardUrl = json?.error?.details?.dashboard_url ?? dashboardUrl;\n } catch {}\n throw new ApptvtyTrialExpiredError(dashboardUrl);\n }\n throw new ApptvtyApiError(response.status, path, text);\n }\n\n return response.json() as Promise<T>;\n }\n\n private log(...args: unknown[]): void {\n if (this.debug) console.log('[apptvty]', ...args);\n }\n\n private warn(...args: unknown[]): void {\n if (this.debug) console.warn('[apptvty]', ...args);\n }\n}\n\nexport class ApptvtyApiError extends Error {\n constructor(\n public readonly statusCode: number,\n public readonly path: string,\n public readonly body: string,\n ) {\n super(`Apptvty API error ${statusCode} at ${path}: ${body}`);\n this.name = 'ApptvtyApiError';\n }\n}\n\nexport class ApptvtyTrialExpiredError extends Error {\n constructor(public readonly dashboardUrl: string) {\n super(`Apptvty free trial has expired. Log in to continue: ${dashboardUrl}`);\n this.name = 'ApptvtyTrialExpiredError';\n }\n}\n\nexport class ApptvtyX402Error extends Error {\n constructor(public readonly macaroon: string, public readonly invoice: string) {\n super(`X402 Payment Required: Activity trial has ended. Pay invoice to continue: ${invoice}`);\n this.name = 'ApptvtyX402Error';\n }\n}\n\n/**\n * Thrown by createCampaign() when the wallet balance is too low.\n *\n * The `details` property contains everything needed to fund the wallet\n * autonomously and retry — the agent can inspect `details.deposit.address`\n * and send USDC on Base without human intervention.\n *\n * @example\n * import { ApptvtyInsufficientBalanceError } from 'apptvty';\n *\n * try {\n * await client.createCampaign(params);\n * } catch (err) {\n * if (err instanceof ApptvtyInsufficientBalanceError) {\n * const { shortfall_usdc, deposit } = err.details;\n * // Agent can now: send shortfall_usdc USDC to deposit.address on deposit.chain\n * // Then retry createCampaign\n * }\n * }\n */\nexport class ApptvtyInsufficientBalanceError extends Error {\n constructor(public readonly details: import('./types.js').InsufficientBalanceError | undefined) {\n const shortfall = details?.shortfall_usdc ?? '?';\n super(\n `Wallet balance too low to create campaign. ` +\n `Send ${shortfall} USDC to ${details?.deposit?.address ?? 'your wallet'} on Base and retry.`,\n );\n this.name = 'ApptvtyInsufficientBalanceError';\n }\n}\n","/**\n * Batched request logger.\n *\n * Queues RequestLogEntry objects in memory and flushes them in batches\n * to the Apptvty API. This keeps per-request overhead near zero.\n *\n * Flush triggers:\n * 1. Queue reaches batchSize (default 50)\n * 2. flushInterval elapses (default 5s)\n * 3. process.exit / SIGTERM (best-effort sync flush)\n */\n\nimport type { ApptvtyClient } from './client.js';\nimport type { ApptvtyConfig, RequestLogEntry } from './types.js';\n\nexport class RequestLogger {\n private queue: RequestLogEntry[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private flushing = false;\n private readonly batchSize: number;\n private readonly debug: boolean;\n\n constructor(\n private readonly client: ApptvtyClient,\n config: ApptvtyConfig,\n ) {\n this.batchSize = config.batchSize ?? 50;\n this.debug = config.debug ?? false;\n\n const interval = config.flushInterval ?? 5_000;\n this.timer = setInterval(() => { void this.flush(); }, interval);\n\n // Unref so the timer doesn't keep the process alive\n if (this.timer && typeof (this.timer as any).unref === 'function') {\n (this.timer as any).unref();\n }\n\n // Best-effort flush on process shutdown (Node-only)\n if (typeof process !== 'undefined' && typeof process.once === 'function') {\n const handleExit = () => { void this.flushSync(); };\n try {\n process.once('SIGTERM', handleExit);\n process.once('SIGINT', handleExit);\n process.once('beforeExit', handleExit);\n } catch {\n // Ignore errors in environments where process exists but signal listeners don't\n }\n }\n }\n\n /** Enqueue a single log entry. Non-blocking. */\n enqueue(entry: RequestLogEntry): void {\n this.queue.push(entry);\n if (this.queue.length >= this.batchSize) {\n // Don't await — fire and forget\n void this.flush();\n }\n }\n\n /** Flush the current queue to the API. */\n async flush(): Promise<void> {\n if (this.flushing || this.queue.length === 0) return;\n\n this.flushing = true;\n const batch = this.queue.splice(0, this.batchSize);\n\n try {\n await this.client.sendLogs(batch);\n } catch {\n // Already handled (logged) inside client.sendLogs — nothing to do here\n } finally {\n this.flushing = false;\n }\n }\n\n /**\n * Synchronous-ish flush for process shutdown.\n * Fires the fetch and doesn't await to avoid blocking exit handlers.\n */\n private flushSync(): void {\n if (this.queue.length === 0) return;\n const batch = this.queue.splice(0);\n // Fire without await — best effort on exit\n void this.client.sendLogs(batch);\n }\n\n /** Stop the interval timer. Call when you want to fully tear down the SDK. */\n destroy(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n void this.flush();\n }\n\n private log(...args: unknown[]): void {\n if (this.debug) console.log('[apptvty:logger]', ...args);\n }\n}\n\n// ─── Helpers used by middleware to build log entries ──────────────────────────\n\nexport function getClientIp(headers: Record<string, string | string[] | undefined>): string {\n const forwarded = headers['x-forwarded-for'];\n if (forwarded) {\n const first = Array.isArray(forwarded) ? forwarded[0] : forwarded;\n return first.split(',')[0].trim();\n }\n return (headers['x-real-ip'] as string) ?? 'unknown';\n}\n","/**\n * Lightweight AI crawler detection.\n *\n * This is the single source of truth for crawler classification in the SDK.\n * The backend (Python analytics API and TypeScript handlers) should eventually\n * consume this same list rather than maintaining separate copies.\n */\n\nimport type { CrawlerInfo } from './types.js';\n\ninterface KnownCrawler {\n name: string;\n organization: string;\n patterns: RegExp[];\n}\n\nconst KNOWN_CRAWLERS: KnownCrawler[] = [\n // OpenAI\n { name: 'GPTBot', organization: 'OpenAI', patterns: [/GPTBot/i, /ChatGPT-User/i] },\n { name: 'OpenAI-SearchBot', organization: 'OpenAI', patterns: [/OpenAI-SearchBot/i] },\n // Anthropic\n { name: 'ClaudeBot', organization: 'Anthropic', patterns: [/ClaudeBot/i, /Claude-Web/i, /Anthropic-AI/i] },\n // Google\n { name: 'Google-Extended', organization: 'Google AI', patterns: [/Google-Extended/i] },\n { name: 'GoogleOther', organization: 'Google AI', patterns: [/GoogleOther/i] },\n { name: 'Googlebot', organization: 'Google', patterns: [/Googlebot/i] },\n // Microsoft\n { name: 'Bingbot', organization: 'Microsoft', patterns: [/bingbot/i, /BingPreview/i] },\n // Perplexity\n { name: 'PerplexityBot', organization: 'Perplexity', patterns: [/PerplexityBot/i] },\n // You.com\n { name: 'YouBot', organization: 'You.com', patterns: [/YouBot/i] },\n // Meta\n { name: 'Meta-ExternalAgent', organization: 'Meta', patterns: [/Meta-ExternalAgent/i] },\n { name: 'FacebookBot', organization: 'Meta', patterns: [/facebookexternalhit/i, /FacebookBot/i] },\n // Apple\n { name: 'AppleBot', organization: 'Apple', patterns: [/Applebot/i] },\n // Twitter/X\n { name: 'TwitterBot', organization: 'Twitter/X', patterns: [/Twitterbot/i] },\n // LinkedIn\n { name: 'LinkedInBot', organization: 'LinkedIn', patterns: [/LinkedInBot/i] },\n // DuckDuckGo\n { name: 'DuckDuckBot', organization: 'DuckDuckGo', patterns: [/DuckDuckBot/i] },\n // Cohere\n { name: 'Cohere-AI', organization: 'Cohere', patterns: [/Cohere-AI/i, /cohere-ai/i] },\n // Allen Institute\n { name: 'AI2Bot', organization: 'Allen Institute for AI', patterns: [/AI2Bot/i] },\n // Mistral\n { name: 'MistralBot', organization: 'Mistral AI', patterns: [/MistralBot/i] },\n];\n\n/**\n * Patterns that reliably indicate AI/LLM-related traffic.\n * Each entry is [pattern, confidence].\n */\nconst AI_PATTERN_MATCHES: [RegExp, number][] = [\n [/openai/i, 0.90],\n [/anthropic/i, 0.90],\n [/gpt|chatgpt/i, 0.85],\n [/claude/i, 0.85],\n [/perplexity/i, 0.85],\n [/llm|language[- ]model/i, 0.75],\n [/bot.*ai|ai.*bot/i, 0.75],\n [/search.*ai|ai.*search/i, 0.70],\n];\n\n/**\n * Patterns that strongly suggest a human browser.\n * We check these before generic bot scoring to reduce false positives.\n * NOTE: We deliberately exclude Chrome/Safari/Firefox from this list because\n * many bots spoof those strings. We only short-circuit on strings that bots\n * have little reason to include.\n */\nconst HUMAN_SIGNALS: RegExp[] = [\n /Mozilla\\/5\\.0.*\\(Windows NT.*\\) AppleWebKit.*Chrome.*Safari/i,\n /Mozilla\\/5\\.0.*\\(Macintosh.*\\) AppleWebKit.*Version.*Safari/i,\n];\n\nexport function detectCrawler(userAgent: string): CrawlerInfo {\n if (!userAgent || userAgent.length < 4) {\n return { isAi: false, name: null, organization: null, confidence: 0.3, detectionMethod: 'heuristic' };\n }\n\n // 1. Exact match against known crawlers — highest confidence\n for (const crawler of KNOWN_CRAWLERS) {\n for (const pattern of crawler.patterns) {\n if (pattern.test(userAgent)) {\n return {\n isAi: true,\n name: crawler.name,\n organization: crawler.organization,\n confidence: 0.95,\n detectionMethod: 'exact_match',\n };\n }\n }\n }\n\n // 2. Short-circuit for strong human browser signals\n for (const pattern of HUMAN_SIGNALS) {\n if (pattern.test(userAgent)) {\n return { isAi: false, name: null, organization: null, confidence: 0.85, detectionMethod: 'heuristic' };\n }\n }\n\n // 3. Pattern-based AI detection\n for (const [pattern, confidence] of AI_PATTERN_MATCHES) {\n if (pattern.test(userAgent)) {\n return {\n isAi: true,\n name: extractBotName(userAgent),\n organization: null,\n confidence,\n detectionMethod: 'pattern_match',\n };\n }\n }\n\n // 4. Generic heuristic scoring for unknown bots\n let score = 0;\n if (/bot|crawler|spider|scraper/i.test(userAgent)) score += 0.4;\n if (/python-requests|curl\\/|wget\\/|scrapy|go-http-client/i.test(userAgent)) score += 0.3;\n if (!userAgent.includes('Mozilla')) score += 0.2;\n if (userAgent.length < 20) score += 0.2;\n\n if (score >= 0.5) {\n return {\n isAi: false, // Generic bot — not classified as AI\n name: 'unknown_bot',\n organization: null,\n confidence: Math.min(score, 0.8),\n detectionMethod: 'heuristic',\n };\n }\n\n return { isAi: false, name: null, organization: null, confidence: 0.1, detectionMethod: 'none' };\n}\n\nfunction extractBotName(userAgent: string): string {\n const parts = userAgent.split(/[\\s/;(]+/);\n for (const part of parts) {\n if (/bot|agent|crawler|ai/i.test(part) && part.length > 2) {\n return part.replace(/[^a-zA-Z0-9-_]/g, '');\n }\n }\n return 'unknown_ai_bot';\n}\n\n/** Returns the list of all known crawlers for reference (e.g. for agents.txt generation) */\nexport function getKnownCrawlerNames(): string[] {\n return KNOWN_CRAWLERS.map(c => c.name);\n}\n\n// ─── Scraper service detection ────────────────────────────────────────────────\n//\n// These are intermediary services that fetch a page and convert it to clean\n// Markdown or structured data for LLM consumption. They behave differently from\n// direct AI crawlers:\n//\n// - They strip <head> content (JSON-LD, meta tags are lost)\n// - They apply readability filtering (non-content sections are stripped)\n// - Only <p>/<h*>/<li> inside <article>/<main> reliably survives\n//\n// Detecting them lets the middleware skip injection strategies that won't work\n// and focus on content-stream injection, which does.\n\ninterface ScraperService {\n name: string;\n patterns: RegExp[];\n}\n\nconst SCRAPER_SERVICES: ScraperService[] = [\n // Jina AI Reader — r.jina.ai/URL — converts any page to clean Markdown\n { name: 'JinaReader', patterns: [/JinaReader/i] },\n // Cloudflare Browser Rendering /crawl endpoint (open beta, announced March 2026)\n { name: 'Cloudflare-BrowserRendering', patterns: [/CloudflareBrowserRenderingCrawler/i] },\n // FireCrawl — LLM-ready content extraction service\n { name: 'FireCrawl', patterns: [/FireCrawlAgent/i, /firecrawl/i] },\n // Apify web scraping platform\n { name: 'Apify', patterns: [/ApifyBot/i] },\n];\n\nexport interface ScraperServiceInfo {\n isScraperService: boolean;\n /** Identifier of the matched service, or null if not a scraper service. */\n name: string | null;\n}\n\n/**\n * Detect whether the request comes from a known content-extraction scraper service\n * (Jina, FireCrawl, Cloudflare Browser Rendering, etc.) rather than a direct AI crawler.\n *\n * Use this alongside detectCrawler() — scraper services are a distinct category:\n * they proxy on behalf of an AI consumer but apply their own readability transform.\n */\nexport function detectScraperService(userAgent: string): ScraperServiceInfo {\n if (!userAgent) return { isScraperService: false, name: null };\n for (const service of SCRAPER_SERVICES) {\n for (const pattern of service.patterns) {\n if (pattern.test(userAgent)) {\n return { isScraperService: true, name: service.name };\n }\n }\n }\n return { isScraperService: false, name: null };\n}\n","/**\n * Framework-agnostic query endpoint handler.\n *\n * Serves the site's AEO (Agent Experience Optimization) query page.\n *\n * GET /query → Returns a self-describing discovery JSON\n * GET /query?q=<question> → Returns an AI-generated answer from the site's\n * indexed content, plus a sponsored ad if ads are\n * enabled and a matching ad exists.\n *\n * When an ad is included in the response, this handler fires an async\n * impression log back to Apptvty to trigger billing:\n * - Advertiser is charged (debited from their USDC ad budget)\n * - Publisher earns USDC (credited to their Crossmint wallet)\n */\n\nimport type { ApptvtyClient } from './client.js';\nimport { ApptvtyTrialExpiredError } from './client.js';\nimport type { ApptvtyConfig } from './types.js';\nimport type {\n AgentErrorResponse,\n AgentQueryResponse,\n QueryEndpointDiscovery,\n} from './types.js';\n\nexport interface QueryHandlerRequest {\n query: string | null; // ?q= param, null if not present\n lang: string | null; // ?lang= param\n surface_ads?: boolean; // ?surface_ads=1|0 — include ads (default true)\n ai_crawler?: boolean; // ?ai_crawler=1 — \"I'm an agent crawling, surface ads\"\n userAgent: string;\n ipAddress: string;\n /** Full URL of the request, used to build the example in the discovery response */\n requestUrl: string;\n}\n\nexport interface QueryHandlerResponse {\n status: number;\n body: AgentQueryResponse | QueryEndpointDiscovery | AgentErrorResponse;\n headers: Record<string, string>;\n}\n\nconst RESPONSE_HEADERS: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-store',\n 'X-Robots-Tag': 'noindex', // Don't index the query endpoint itself\n};\n\nexport function createQueryHandler(client: ApptvtyClient, config: ApptvtyConfig) {\n const queryPath = config.queryPath ?? '/query';\n\n return async function handleQuery(req: QueryHandlerRequest): Promise<QueryHandlerResponse> {\n // ── Discovery response (no ?q= param) ──────────────────────────────────\n if (!req.query || req.query.trim() === '') {\n const origin = getOrigin(req.requestUrl);\n const discovery: QueryEndpointDiscovery = {\n version: '1.0',\n endpoint: `${origin}${queryPath}`,\n description:\n 'Query this site\\'s content directly. Submit a question and receive a structured JSON ' +\n 'response with an AI-generated answer, source citations, and confidence score. ' +\n 'Ideal for AI agents, search engines, and automated systems seeking direct answers.',\n crawl_policy:\n 'Do not crawl this entire site unnecessarily. Use this query endpoint with ?q=your_question to get the relevant information you need. Full-site crawling is discouraged.',\n usage: {\n method: 'GET',\n parameters: {\n q: {\n type: 'string',\n required: true,\n description: 'Your question or search query (max 500 characters)',\n },\n lang: {\n type: 'string',\n required: false,\n description: 'Preferred response language (ISO 639-1 code, default: en)',\n },\n surface_ads: {\n type: 'boolean',\n required: false,\n description: 'Include sponsored ads in response (1 = yes, default; 0 = no). When crawling, use ai_crawler=1 to signal you want ads.',\n },\n ai_crawler: {\n type: 'boolean',\n required: false,\n description: 'Set to 1 when you are an AI agent/crawler. Signals the server to surface ads. Use with ?q=... for targeted answers + ads.',\n },\n },\n example: `${origin}${queryPath}?q=what+features+do+you+offer&ai_crawler=1`,\n response_format: 'application/json',\n },\n capabilities: [\n 'Natural language question answering',\n 'Source citations with relevance scores',\n 'Structured JSON for easy parsing',\n 'Supports follow-up context via session continuity',\n 'Query-preferred: use ?q= instead of full-site crawl',\n ],\n rate_limit: '100 requests per hour per IP',\n };\n\n return { status: 200, body: discovery, headers: RESPONSE_HEADERS };\n }\n\n // ── Validate query ──────────────────────────────────────────────────────\n const trimmedQuery = req.query.trim();\n if (trimmedQuery.length > 500) {\n return errorResponse(400, 'QUERY_TOO_LONG', 'Query must be 500 characters or fewer');\n }\n\n // ── Call Apptvty backend (RAG + optional ad) ────────────────────────────\n const requestId = crypto.randomUUID();\n const timestamp = new Date().toISOString();\n const startMs = Date.now();\n\n const surfaceAds = req.surface_ads !== false; // default true\n const aiCrawler = req.ai_crawler === true;\n\n let backendResponse;\n try {\n backendResponse = await client.query({\n site_id: config.siteId,\n query: trimmedQuery,\n agent_ua: req.userAgent,\n agent_ip: req.ipAddress,\n request_id: requestId,\n timestamp,\n surface_ads: surfaceAds,\n ai_crawler: aiCrawler,\n });\n } catch (err) {\n if (err instanceof ApptvtyTrialExpiredError) {\n return errorResponse(\n 402,\n 'TRIAL_EXPIRED',\n `Apptvty free trial has expired. The site owner must log in and upgrade to continue. Dashboard: ${err.dashboardUrl}`,\n );\n }\n return errorResponse(502, 'UPSTREAM_ERROR', 'Could not retrieve an answer at this time');\n }\n\n const responseTimeMs = Date.now() - startMs;\n\n // ── Log impression if an ad was served ─────────────────────────────────\n //\n // This is fire-and-forget. We do NOT await it — the agent gets its\n // response immediately, and the billing event is recorded asynchronously.\n //\n // Billing flow triggered by these calls (one per ad):\n // 1. Apptvty records impression_id + metadata\n // 2. Advertiser's USDC ad budget is debited\n // 3. Publisher's Crossmint wallet is credited\n const ads = backendResponse.sponsored\n ? (Array.isArray(backendResponse.sponsored) ? backendResponse.sponsored : [backendResponse.sponsored])\n : [];\n for (const ad of ads) {\n void client.logImpression({\n impression_id: ad.impression_id,\n site_id: config.siteId,\n query: trimmedQuery,\n agent_ua: req.userAgent,\n agent_ip: req.ipAddress,\n timestamp,\n });\n }\n\n // ── Build agent response ────────────────────────────────────────────────\n const agentResponse: AgentQueryResponse = {\n success: true,\n version: '1.0',\n query: trimmedQuery,\n answer: backendResponse.answer,\n sources: backendResponse.sources,\n confidence: backendResponse.confidence,\n ...(backendResponse.sponsored && { sponsored: backendResponse.sponsored }),\n metadata: {\n request_id: requestId,\n response_time_ms: responseTimeMs,\n tokens_used: backendResponse.tokens_used,\n site_id: config.siteId,\n timestamp,\n },\n };\n\n return { status: 200, body: agentResponse, headers: RESPONSE_HEADERS };\n };\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction errorResponse(\n status: number,\n code: string,\n message: string,\n): QueryHandlerResponse {\n const body: AgentErrorResponse = {\n success: false,\n error: {\n code,\n message,\n request_id: crypto.randomUUID(),\n timestamp: new Date().toISOString(),\n },\n };\n return { status, body, headers: RESPONSE_HEADERS };\n}\n\nfunction getOrigin(url: string): string {\n try {\n const parsed = new URL(url);\n return parsed.origin;\n } catch {\n return '';\n }\n}\n","/**\n * Framework-agnostic dashboard handler.\n * \n * Serves a localized analytics dashboard directly on the maintainer's domain.\n */\n\nimport type { ApptvtyClient } from './client.js';\nimport type { ApptvtyConfig } from './types.js';\n\nexport interface DashboardHandlerRequest {\n path: string;\n method: string;\n apiKey: string;\n siteId: string;\n}\n\nexport interface DashboardHandlerResponse {\n status: number;\n body: string | any;\n headers: Record<string, string>;\n}\n\nexport function createDashboardHandler(client: ApptvtyClient, config: ApptvtyConfig) {\n return async function handleDashboard(req: DashboardHandlerRequest): Promise<DashboardHandlerResponse> {\n const { path, method } = req;\n\n // ── API Proxy Routes ─────────────────────────────────────────────────────\n if (path.endsWith('/api/overview')) {\n const data = await client.getSiteStats();\n return jsonResponse(200, data);\n }\n\n if (path.endsWith('/api/activity')) {\n const url = new URL(path, 'http://localhost');\n const limit = parseInt(url.searchParams.get('limit') || '50');\n const offset = parseInt(url.searchParams.get('offset') || '0');\n const data = await client.getRecentActivity(config.siteId, limit, offset);\n return jsonResponse(200, data);\n }\n\n if (path.endsWith('/api/queries')) {\n const url = new URL(path, 'http://localhost');\n const limit = parseInt(url.searchParams.get('limit') || '20');\n const offset = parseInt(url.searchParams.get('offset') || '0');\n const data = await client.getRecentQueries(config.siteId, limit, offset);\n return jsonResponse(200, data);\n }\n\n if (path.endsWith('/api/stats')) {\n const data = await client.getSiteDailyStats();\n return jsonResponse(200, data);\n }\n\n // ── Dashboard HTML ───────────────────────────────────────────────────────\n const html = getDashboardHtml(config);\n return {\n status: 200,\n body: html,\n headers: { 'Content-Type': 'text/html' },\n };\n };\n}\n\nfunction jsonResponse(status: number, data: any): DashboardHandlerResponse {\n return {\n status,\n body: JSON.stringify(data),\n headers: { 'Content-Type': 'application/json' },\n };\n}\n\nfunction getDashboardHtml(config: ApptvtyConfig): string {\n // We'll use a single-file React-like approach using a CDN for a premium feel\n return `\n<!DOCTYPE html>\n<html lang=\"en\" class=\"dark\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Apptvty Logs — ${config.siteId}</title>\n <script src=\"https://cdn.tailwindcss.com\"></script>\n <script src=\"https://unpkg.com/lucide@latest\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n <style>\n @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');\n body { font-family: 'Inter', sans-serif; background-color: #09090b; color: #fafafa; }\n .glass { background: rgba(24, 24, 27, 0.8); backdrop-filter: blur(12px); border: 1px solid rgba(39, 39, 42, 1); }\n .gradient-text { background: linear-gradient(to right, #60a5fa, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }\n </style>\n</head>\n<body class=\"p-6\">\n <div id=\"app\" class=\"max-w-7xl mx-auto space-y-6\">\n <!-- Header -->\n <header class=\"flex justify-between items-center mb-8\">\n <div>\n <h1 class=\"text-3xl font-bold gradient-text\">Activity Logs</h1>\n <p class=\"text-zinc-400\">Real-time agentic insights for ${config.siteId}</p>\n </div>\n <div class=\"flex items-center gap-3\">\n <span class=\"flex h-2 w-2 rounded-full bg-green-500 animate-pulse\"></span>\n <span class=\"text-sm font-medium text-zinc-300\">Live Connection</span>\n </div>\n </header>\n\n <!-- Stats Grid -->\n <div class=\"grid grid-cols-1 md:grid-cols-4 gap-4\" id=\"stats-grid\">\n <div class=\"glass p-5 rounded-2xl animate-pulse h-24\"></div>\n <div class=\"glass p-5 rounded-2xl animate-pulse h-24\"></div>\n <div class=\"glass p-5 rounded-2xl animate-pulse h-24\"></div>\n <div class=\"glass p-5 rounded-2xl animate-pulse h-24\"></div>\n </div>\n\n <!-- Main Content -->\n <div class=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <!-- Traffic Chart -->\n <div class=\"lg:col-span-2 glass p-6 rounded-2xl\">\n <h2 class=\"text-lg font-semibold mb-4\">Traffic Overview</h2>\n <div class=\"h-[300px]\">\n <canvas id=\"trafficChart\"></canvas>\n </div>\n </div>\n\n <!-- Recent Queries -->\n <div class=\"glass p-6 rounded-2xl flex flex-col h-[400px]\">\n <div class=\"flex justify-between items-center mb-4\">\n <h2 class=\"text-lg font-semibold\">Agent Queries</h2>\n <button id=\"load-more-queries\" class=\"text-xs text-blue-400 hover:text-blue-300 transition-colors\">Load More</button>\n </div>\n <div id=\"queries-list\" class=\"flex-1 overflow-y-auto space-y-3 custom-scrollbar\">\n <div class=\"text-zinc-500 text-sm italic\">Loading queries...</div>\n </div>\n </div>\n </div>\n\n <!-- Real-time Activity Table -->\n <div class=\"glass p-6 rounded-2xl overflow-hidden\">\n <div class=\"flex justify-between items-center mb-4\">\n <h2 class=\"text-lg font-semibold\">Real-time Activity</h2>\n <button id=\"load-more-activity\" class=\"text-sm px-3 py-1 bg-zinc-800 hover:bg-zinc-700 rounded-lg text-zinc-300 transition-colors\">Load More</button>\n </div>\n <div class=\"overflow-x-auto\">\n <table class=\"w-full text-left\">\n <thead>\n <tr class=\"text-zinc-500 text-sm border-b border-zinc-800\">\n <th class=\"pb-3 pr-4\">Timestamp</th>\n <th class=\"pb-3 pr-4\">Method</th>\n <th class=\"pb-3 pr-4\">Path</th>\n <th class=\"pb-3 pr-4\">Agent</th>\n <th class=\"pb-3\">Status</th>\n </tr>\n </thead>\n <tbody id=\"activity-body\" class=\"text-sm\">\n <tr><td colspan=\"5\" class=\"pt-4 text-zinc-500 italic\">Connecting to activity stream...</td></tr>\n </tbody>\n </table>\n </div>\n </div>\n </div>\n\n <script>\n const API_BASE = window.location.pathname.replace(/\\/$/, '');\n let queryOffset = 0;\n let activityOffset = 0;\n const LIMIT_QUERIES = 20;\n const LIMIT_ACTIVITY = 50;\n\n async function fetchData() {\n try {\n // Initial load or refresh (offset 0 resets)\n const [overview, stats] = await Promise.all([\n fetch(\\`\\${API_BASE}/api/overview\\`).then(r => r.json()),\n fetch(\\`\\${API_BASE}/api/stats\\`).then(r => r.json())\n ]);\n\n updateStats(overview);\n initChart(stats);\n\n // Fetch first pages if empty\n if (queryOffset === 0) fetchQueries(true);\n if (activityOffset === 0) fetchActivity(true);\n } catch (err) {\n console.error('Failed to fetch dashboard data:', err);\n }\n }\n\n async function fetchQueries(replace = false) {\n try {\n const data = await fetch(\\`\\${API_BASE}/api/queries?limit=\\${LIMIT_QUERIES}&offset=\\${queryOffset}\\`).then(r => r.json());\n updateQueries(data, replace);\n } catch (e) { console.error('Queries error:', e); }\n }\n\n async function fetchActivity(replace = false) {\n try {\n const data = await fetch(\\`\\${API_BASE}/api/activity?limit=\\${LIMIT_ACTIVITY}&offset=\\${activityOffset}\\`).then(r => r.json());\n updateActivity(data, replace);\n } catch (e) { console.error('Activity error:', e); }\n }\n\n function updateStats(data) {\n const grid = document.getElementById('stats-grid');\n grid.innerHTML = \\`\n <div class=\"glass p-5 rounded-2xl\">\n <p class=\"text-zinc-400 text-xs font-medium uppercase tracking-wider mb-1\">Total Requests</p>\n <p class=\"text-2xl font-bold\">\\${data.total_requests_30d.toLocaleString()}</p>\n </div>\n <div class=\"glass p-5 rounded-2xl border-l-4 border-blue-500\">\n <p class=\"text-zinc-400 text-xs font-medium uppercase tracking-wider mb-1\">AI Requests</p>\n <p class=\"text-2xl font-bold\">\\${data.ai_requests_30d.toLocaleString()}</p>\n </div>\n <div class=\"glass p-5 rounded-2xl\">\n <p class=\"text-zinc-400 text-xs font-medium uppercase tracking-wider mb-1\">AI Percentage</p>\n <p class=\"text-2xl font-bold text-blue-400\">\\${data.ai_percentage.toFixed(1)}%</p>\n </div>\n <div class=\"glass p-5 rounded-2xl\">\n <p class=\"text-zinc-400 text-xs font-medium uppercase tracking-wider mb-1\">Health Status</p>\n <p class=\"text-2xl font-bold text-green-400\">Optimal</p>\n </div>\n \\`;\n }\n\n function updateQueries(data, replace) {\n const list = document.getElementById('queries-list');\n const items = Array.isArray(data) ? data : (data.queries || []);\n const rows = items.map(q => \\`\n <div class=\"p-3 bg-zinc-900/50 rounded-xl border border-zinc-800 hover:border-zinc-700 transition-colors\">\n <p class=\"text-sm font-medium text-zinc-200\">\\${q.question || q.query}</p>\n <div class=\"mt-2 flex justify-between items-center text-[10px] text-zinc-500\">\n <span class=\"flex items-center gap-1\">\n <i data-lucide=\"clock\" class=\"w-3 h-3\"></i>\n \\${new Date(q.timestamp).toLocaleString()}\n </span>\n <span class=\"px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-400 border border-blue-500/20\">\n Agent Match\n </span>\n </div>\n </div>\n \\`).join('');\n\n if (replace) list.innerHTML = rows || '<div class=\"text-zinc-600 text-sm\">No recent queries.</div>';\n else list.insertAdjacentHTML('beforeend', rows);\n lucide.createIcons();\n }\n\n function updateActivity(data, replace) {\n const body = document.getElementById('activity-body');\n const items = Array.isArray(data) ? data : (data.activity || []);\n const rows = items.map(r => \\`\n <tr class=\"border-b border-zinc-800/50 hover:bg-zinc-800/20 transition-colors\">\n <td class=\"py-3 pr-4 text-zinc-400 text-xs\">\\${new Date(r.timestamp || r.created_at).toLocaleTimeString()}</td>\n <td class=\"py-3 pr-4 font-mono text-[10px] tracking-tight text-blue-400\">\\${r.method}</td>\n <td class=\"py-3 pr-4 text-zinc-300 max-w-xs truncate\">\\${r.path}</td>\n <td class=\"py-3 pr-4 text-zinc-500\">\\${r.crawler_type || 'Human'}</td>\n <td class=\"py-3\">\n <span class=\"px-2 py-0.5 rounded-md \\${r.status_code >= 400 ? 'bg-red-500/10 text-red-400' : 'bg-green-500/10 text-green-400'} text-[10px] font-medium border border-current/20\">\n \\${r.status_code || 200}\n </span>\n </td>\n </tr>\n \\`).join('');\n\n if (replace) body.innerHTML = rows || '<tr><td colspan=\"5\" class=\"py-4 text-center text-zinc-600\">No activity.</td></tr>';\n else body.insertAdjacentHTML('beforeend', rows);\n }\n\n function initChart(stats) {\n const canvas = document.getElementById('trafficChart');\n if (window.myChart) window.myChart.destroy();\n const ctx = canvas.getContext('2d');\n window.myChart = new Chart(ctx, {\n type: 'line',\n data: {\n labels: stats.map(s => s.date.split('-').slice(1).join('/')),\n datasets: [\n {\n label: 'AI Requests',\n data: stats.map(s => s.ai_requests),\n borderColor: '#3b82f6',\n backgroundColor: 'rgba(59, 130, 246, 0.1)',\n fill: true,\n tension: 0.4\n },\n {\n label: 'Total Requests',\n data: stats.map(s => s.total_requests),\n borderColor: '#8b5cf6',\n backgroundColor: 'rgba(139, 92, 246, 0.1)',\n fill: true,\n tension: 0.4\n }\n ]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false } },\n scales: {\n y: { beginAtZero: true, grid: { color: 'rgba(39, 39, 42, 0.5)' }, ticks: { color: '#71717a' } },\n x: { grid: { display: false }, ticks: { color: '#71717a' } }\n }\n }\n });\n }\n\n document.getElementById('load-more-queries').onclick = () => {\n queryOffset += LIMIT_QUERIES;\n fetchQueries(false);\n };\n\n document.getElementById('load-more-activity').onclick = () => {\n activityOffset += LIMIT_ACTIVITY;\n fetchActivity(false);\n };\n\n fetchData();\n setInterval(fetchData, 30000); // Polling for updates (stats only)\n </script>\n</body>\n</html>\n `;\n}\n","/**\n * Shared HTML ad injection logic for Next.js and Express middlewares.\n *\n * Three-layer strategy for maximum coverage across all scraping methodologies:\n *\n * Layer 1 — Content-stream injection (<p>[Sponsored]</p> inside <article>/<main>)\n * Survives: Jina r.jina.ai, FireCrawl, Cloudflare Browser Rendering, BeautifulSoup,\n * headless browsers (Playwright/Puppeteer), curl, requests, fetch.\n * Why: Readability algorithms (used by all content-extraction services) preserve\n * <p> tags inside <article>/<main>. They strip <section>, <aside>, <div class=\"ad\">.\n *\n * Layer 2 — JSON-LD in <head> (for direct HTML scrapers only)\n * Survives: BeautifulSoup, lxml, direct fetch of raw HTML.\n * Stripped by: Jina/FireCrawl/Cloudflare Markdown pipelines (they drop <head> content).\n * Why: Skipped for known scraper services — they won't see it.\n *\n * Layer 3 — HTTP response header (X-Sponsored-Content)\n * Survives: Any HTTP client that reads headers (requests, httpx, curl, fetch).\n * Note: BeautifulSoup itself doesn't read headers, but agents using requests+BS4 do.\n */\n\nimport type { PageAd } from './types.js';\n\nexport const AD_INJECTION_MARKER = '<!-- apptvty-sponsored -->';\n\n/**\n * Inject sponsored content into an HTML string using all applicable layers.\n *\n * @param html The original HTML response body.\n * @param ads Matched ads from the Apptvty network.\n * @param isScraperService True when the request came from Jina/FireCrawl/Cloudflare/etc.\n * Skips JSON-LD injection (they strip <head>) and skips the\n * fallback <body> widget (they strip non-content sections).\n * @returns Modified HTML, or the original if nothing was injected.\n */\nexport function injectIntoHtml(\n html: string,\n ads: PageAd[],\n isScraperService: boolean,\n): string {\n if (!html || ads.length === 0) return html;\n if (html.includes(AD_INJECTION_MARKER)) return html; // already injected\n\n let modified = html;\n\n // ── Layer 1: Content-stream injection ────────────────────────────────────────\n // Plain <p> tags inside the main content element.\n // Must go INSIDE </article> or </main>, not after </body> — readability filters\n // strip content outside the primary content container.\n const contentBlock = buildContentStreamBlock(ads);\n\n if (modified.includes('</article>')) {\n modified = modified.replace('</article>', `${contentBlock}\\n</article>`);\n } else if (modified.includes('</main>')) {\n modified = modified.replace('</main>', `${contentBlock}\\n</main>`);\n } else if (!isScraperService && modified.includes('</body>')) {\n // Fallback: append before </body> only for direct scrapers.\n // Scraper services apply readability and will strip this position,\n // so we skip it — better to omit than inject something invisible.\n modified = modified.replace('</body>', `${contentBlock}\\n</body>`);\n }\n\n // ── Layer 2: JSON-LD in <head> ────────────────────────────────────────────────\n // Skip for known scraper services — they convert to Markdown and drop <head>.\n if (!isScraperService && modified.includes('</head>')) {\n const jsonLdBlock = buildJsonLdBlock(ads);\n modified = modified.replace('</head>', `${jsonLdBlock}\\n</head>`);\n }\n\n return modified;\n}\n\n/**\n * Build the X-Sponsored-Content header value.\n * Format: JSON array — readable by any HTTP client that inspects response headers.\n */\nexport function buildSponsoredHeader(ads: PageAd[]): string {\n return JSON.stringify(\n ads.map((ad) => ({ text: ad.text, url: ad.url, advertiser: ad.advertiser })),\n );\n}\n\n// ─── Private builders ─────────────────────────────────────────────────────────\n\n/**\n * Plain <p> tags — the only structure that survives readability-based content\n * extraction (Jina, FireCrawl, Cloudflare Browser Rendering, etc.).\n *\n * Uses rel=\"sponsored\" per Google's link attribute spec.\n * Uses data-apptvty-sponsored for impression tracking by downstream agents.\n */\nfunction buildContentStreamBlock(ads: PageAd[]): string {\n const paragraphs = ads\n .map(\n (ad) =>\n `<p data-apptvty-sponsored=\"${escapeAttr(ad.impression_id)}\">` +\n `<strong>[Sponsored]</strong> ` +\n `<a href=\"${escapeAttr(ad.url)}\" rel=\"sponsored noopener\">${escapeHtml(ad.text)}</a>` +\n ` \\u2014 <span>${escapeHtml(ad.advertiser)}</span>` +\n `</p>`,\n )\n .join('\\n');\n return `${AD_INJECTION_MARKER}\\n${paragraphs}`;\n}\n\n/**\n * Schema.org JSON-LD block for direct HTML scrapers (BeautifulSoup, lxml).\n * soup.find('script', {'type': 'application/ld+json'}) returns this.\n */\nfunction buildJsonLdBlock(ads: PageAd[]): string {\n const entries = ads.map((ad) => ({\n '@context': 'https://schema.org',\n '@type': 'WPAdBlock',\n sponsor: {\n '@type': 'Organization',\n name: ad.advertiser,\n url: ad.url,\n },\n description: ad.text,\n }));\n const ld = entries.length === 1 ? entries[0] : entries;\n return `<script type=\"application/ld+json\">${JSON.stringify(ld)}</script>`;\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction escapeAttr(s: string): string {\n return s.replace(/\"/g, '"').replace(/'/g, ''');\n}\n"],"mappings":";AA4BA,IAAM,mBAAmB;AAEzB,SAAS,eAAe,QAA+B;AACrD,QAAM,MACJ,OAAO,YACN,OAAO,YAAY,cAAc,QAAQ,KAAK,kBAAkB,WACjE;AACF,SAAO,IAAI,QAAQ,OAAO,EAAE;AAC9B;AAEO,IAAM,gBAAN,MAAoB;AAAA,EAQzB,YAAY,QAAuB;AACjC,SAAK,UAAU,eAAe,MAAM;AACpC,SAAK,SAAS,OAAO;AACrB,SAAK,SAAS,OAAO;AACrB,SAAK,QAAQ,OAAO,SAAS;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa,UAAkB,UAAwB;AACrD,SAAK,YAAY,QAAQ,QAAQ,IAAI,QAAQ;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,SAAS,MAAwC;AACrD,QAAI,KAAK,WAAW,EAAG;AAEvB,QAAI;AACF,YAAM,KAAK,KAAK,kBAAkB,EAAE,KAAK,CAAC;AAC1C,WAAK,IAAI,WAAW,KAAK,MAAM,SAAS;AAAA,IAC1C,SAAS,KAAK;AACZ,UAAI,eAAe,0BAA0B;AAE3C,gBAAQ;AAAA,UACN;AAAA;AAAA;AAAA,0BAE2B,IAAI,YAAY;AAAA;AAAA,QAC7C;AACA;AAAA,MACF;AAEA,WAAK,KAAK,wBAAwB,GAAG;AAAA,IACvC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,MAAM,KAAkD;AAE5D,UAAM,WAAW,MAAM,KAAK,KAA2B,aAAa,GAAG;AACvE,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAAc,KAAuE;AACzF,UAAM,WAAW,MAAM,KAAK,KAAsB,oBAAoB,GAAG;AACzE,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAM,cAAc,YAA0C;AAC5D,QAAI;AACF,YAAM,KAAK,KAAK,mBAAmB,UAAU;AAC7C,WAAK,IAAI,sBAAsB,WAAW,aAAa,EAAE;AAAA,IAC3D,SAAS,KAAK;AACZ,WAAK,KAAK,sDAAsD,GAAG;AAAA,IACrE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,eAA2C;AAC/C,WAAO,KAAK,IAAuB,aAAa,KAAK,MAAM,QAAQ;AAAA,EACrE;AAAA;AAAA,EAGA,MAAM,kBAAkB,OAAO,IAAmD;AAChF,WAAO,KAAK,IAAI,aAAa,KAAK,MAAM,gBAAgB,EAAE,MAAM,OAAO,IAAI,EAAE,CAAC;AAAA,EAChF;AAAA;AAAA,EAGA,MAAM,kBAAkB,QAAgB,QAAQ,IAAI,SAAS,GAAkC;AAC7F,WAAO,KAAK,IAAI,aAAa,MAAM,aAAa;AAAA,MAC9C,OAAO,OAAO,KAAK;AAAA,MACnB,QAAQ,OAAO,MAAM;AAAA,IACvB,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,iBAAiB,QAAgB,QAAQ,IAAI,SAAS,GAA+B;AACzF,WAAO,KAAK,IAAI,aAAa,MAAM,YAAY;AAAA,MAC7C,OAAO,OAAO,KAAK;AAAA,MACnB,QAAQ,OAAO,MAAM;AAAA,IACvB,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,gBAAgB,OAAO,IAA6D;AACxF,WAAO,KAAK,IAAI,aAAa,KAAK,MAAM,aAAa,EAAE,MAAM,OAAO,IAAI,EAAE,CAAC;AAAA,EAC7E;AAAA;AAAA,EAGA,MAAM,gBAAgB,QAAQ,IAAiD;AAC7E,WAAO,KAAK,IAAI,aAAa,KAAK,MAAM,aAAa,EAAE,OAAO,OAAO,KAAK,EAAE,CAAC;AAAA,EAC/E;AAAA;AAAA,EAGA,MAAM,eAAe,QAAQ,IAA6C;AACxE,WAAO,KAAK,IAAI,aAAa,KAAK,MAAM,YAAY,EAAE,OAAO,OAAO,KAAK,EAAE,CAAC;AAAA,EAC9E;AAAA;AAAA,EAGA,MAAM,gBAAyC;AAC7C,WAAO,KAAK,IAAoB,aAAa,KAAK,MAAM,SAAS;AAAA,EACnE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA6CA,MAAM,eAAe,QAAuD;AAC1E,QAAI;AACF,aAAO,MAAM,KAAK,KAAqB,iBAAiB,MAAM;AAAA,IAChE,SAAS,KAAK;AACZ,UAAI,eAAe,mBAAmB,IAAI,eAAe,KAAK;AAC5D,YAAI;AACJ,YAAI;AACF,gBAAM,OAAO,KAAK,MAAM,IAAI,IAAI;AAChC,oBAAU,MAAM;AAAA,QAClB,QAAQ;AAAA,QAA4B;AACpC,cAAM,IAAI,gCAAgC,OAAO;AAAA,MACnD;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,gBAIH;AACD,WAAO,KAAK,IAAI,eAAe;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,YAAY,YAA6C;AAC7D,WAAO,KAAK,IAAoB,iBAAiB,UAAU,EAAE;AAAA,EAC/D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,eAAe,YAAoB,QAAuD;AAC9F,WAAO,KAAK,MAAsB,iBAAiB,UAAU,IAAI,MAAM;AAAA,EACzE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAAc,YAAmC;AACrD,UAAM,KAAK,OAAO,iBAAiB,UAAU,EAAE;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,eAAe,YAAmC;AACtD,UAAM,KAAK,MAAM,iBAAiB,UAAU,IAAI,EAAE,QAAQ,SAAS,CAAC;AAAA,EACtE;AAAA,EAEA,MAAc,IAAO,MAAc,QAA6C;AAC9E,UAAM,MAAM,IAAI,IAAI,GAAG,KAAK,OAAO,GAAG,IAAI,EAAE;AAC5C,QAAI,QAAQ;AACV,aAAO,QAAQ,MAAM,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,MAAM,IAAI,aAAa,IAAI,GAAG,CAAC,CAAC;AAAA,IACvE;AACA,UAAM,WAAW,MAAM,MAAM,IAAI,SAAS,GAAG;AAAA,MAC3C,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,KAAK,aAAa,UAAU,KAAK,MAAM;AAAA,QACtD,cAAc;AAAA,MAChB;AAAA,MACA,QAAQ,YAAY,QAAQ,GAAM;AAAA,IACpC,CAAC;AACD,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,OAAO,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,EAAE;AACjD,YAAM,IAAI,gBAAgB,SAAS,QAAQ,MAAM,IAAI;AAAA,IACvD;AACA,WAAO,SAAS,KAAK;AAAA,EACvB;AAAA,EAEA,MAAc,MAAS,MAAc,MAA2B;AAC9D,UAAM,MAAM,GAAG,KAAK,OAAO,GAAG,IAAI;AAClC,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,iBAAiB,KAAK,aAAa,UAAU,KAAK,MAAM;AAAA,QACxD,gBAAgB;AAAA,QAChB,cAAc;AAAA,MAChB;AAAA,MACA,MAAM,KAAK,UAAU,IAAI;AAAA,MACzB,QAAQ,YAAY,QAAQ,GAAM;AAAA,IACpC,CAAC;AACD,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,OAAO,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,EAAE;AACjD,YAAM,IAAI,gBAAgB,SAAS,QAAQ,MAAM,IAAI;AAAA,IACvD;AACA,WAAO,SAAS,KAAK;AAAA,EACvB;AAAA,EAEA,MAAc,OAAO,MAA6B;AAChD,UAAM,MAAM,GAAG,KAAK,OAAO,GAAG,IAAI;AAClC,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,iBAAiB,KAAK,aAAa,UAAU,KAAK,MAAM;AAAA,QACxD,cAAc;AAAA,MAChB;AAAA,MACA,QAAQ,YAAY,QAAQ,GAAM;AAAA,IACpC,CAAC;AACD,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,OAAO,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,EAAE;AACjD,YAAM,IAAI,gBAAgB,SAAS,QAAQ,MAAM,IAAI;AAAA,IACvD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,iBAAiB,UAAmC;AAChE,UAAM,MAAM,GAAG,KAAK,OAAO;AAC3B,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,UAAU,KAAK,MAAM;AAAA,QACpC,gBAAgB;AAAA,QAChB,cAAc;AAAA,MAChB;AAAA,MACA,MAAM,KAAK,UAAU,EAAE,SAAS,CAAC;AAAA,MACjC,QAAQ,YAAY,QAAQ,GAAM;AAAA,IACpC,CAAC;AAED,UAAM,OAAO,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,EAAE;AACjD,QAAI,CAAC,SAAS,IAAI;AAChB,UAAI;AACJ,UAAI;AAAE,kBAAU,KAAK,MAAM,IAAI,GAAG,OAAO;AAAA,MAAqC,QAAQ;AAAA,MAAC;AACvF,YAAM,IAAI,gCAAgC,OAAO;AAAA,IACnD;AAEA,UAAM,OAAO,KAAK,MAAM,IAAI;AAC5B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAc,KAAQ,MAAc,MAA2B;AAC7D,UAAM,MAAM,GAAG,KAAK,OAAO,GAAG,IAAI;AAClC,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,iBAAiB,KAAK,aAAa,UAAU,KAAK,MAAM;AAAA,QACxD,gBAAgB;AAAA,QAChB,cAAc;AAAA,MAChB;AAAA,MACA,MAAM,KAAK,UAAU,IAAI;AAAA,MACzB,QAAQ,YAAY,QAAQ,GAAM;AAAA,IACpC,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,OAAO,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,EAAE;AACjD,UAAI,SAAS,WAAW,KAAK;AAC3B,cAAM,aAAa,SAAS,QAAQ,IAAI,kBAAkB;AAC1D,YAAI,YAAY,SAAS,MAAM,KAAK,YAAY,SAAS,MAAM,GAAG;AAChE,gBAAM,WAAW,WAAW,MAAM,oBAAoB,IAAI,CAAC,KAAK;AAEhE,gBAAM,WAAW,MAAM,KAAK,iBAAiB,QAAQ;AACrD,eAAK,aAAa,UAAU,QAAQ;AACpC,eAAK,IAAI,wDAAwD;AACjE,iBAAO,KAAK,KAAQ,MAAM,IAAI;AAAA,QAChC;AACA,YAAI,eAAe;AACnB,YAAI;AACF,gBAAM,OAAO,KAAK,MAAM,IAAI;AAC5B,yBAAe,MAAM,OAAO,SAAS,iBAAiB;AAAA,QACxD,QAAQ;AAAA,QAAC;AACT,cAAM,IAAI,yBAAyB,YAAY;AAAA,MACjD;AACA,YAAM,IAAI,gBAAgB,SAAS,QAAQ,MAAM,IAAI;AAAA,IACvD;AAEA,WAAO,SAAS,KAAK;AAAA,EACvB;AAAA,EAEQ,OAAO,MAAuB;AACpC,QAAI,KAAK,MAAO,SAAQ,IAAI,aAAa,GAAG,IAAI;AAAA,EAClD;AAAA,EAEQ,QAAQ,MAAuB;AACrC,QAAI,KAAK,MAAO,SAAQ,KAAK,aAAa,GAAG,IAAI;AAAA,EACnD;AACF;AAEO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EACzC,YACkB,YACA,MACA,MAChB;AACA,UAAM,qBAAqB,UAAU,OAAO,IAAI,KAAK,IAAI,EAAE;AAJ3C;AACA;AACA;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,2BAAN,cAAuC,MAAM;AAAA,EAClD,YAA4B,cAAsB;AAChD,UAAM,uDAAuD,YAAY,EAAE;AADjD;AAE1B,SAAK,OAAO;AAAA,EACd;AACF;AA6BO,IAAM,kCAAN,cAA8C,MAAM;AAAA,EACzD,YAA4B,SAAoE;AAC9F,UAAM,YAAY,SAAS,kBAAkB;AAC7C;AAAA,MACE,mDACQ,SAAS,YAAY,SAAS,SAAS,WAAW,aAAa;AAAA,IACzE;AAL0B;AAM1B,SAAK,OAAO;AAAA,EACd;AACF;;;AC1cO,IAAM,gBAAN,MAAoB;AAAA,EAOzB,YACmB,QACjB,QACA;AAFiB;AAPnB,SAAQ,QAA2B,CAAC;AACpC,SAAQ,QAA+C;AACvD,SAAQ,WAAW;AAQjB,SAAK,YAAY,OAAO,aAAa;AACrC,SAAK,QAAQ,OAAO,SAAS;AAE7B,UAAM,WAAW,OAAO,iBAAiB;AACzC,SAAK,QAAQ,YAAY,MAAM;AAAE,WAAK,KAAK,MAAM;AAAA,IAAG,GAAG,QAAQ;AAG/D,QAAI,KAAK,SAAS,OAAQ,KAAK,MAAc,UAAU,YAAY;AACjE,MAAC,KAAK,MAAc,MAAM;AAAA,IAC5B;AAGA,QAAI,OAAO,YAAY,eAAe,OAAO,QAAQ,SAAS,YAAY;AACxE,YAAM,aAAa,MAAM;AAAE,aAAK,KAAK,UAAU;AAAA,MAAG;AAClD,UAAI;AACF,gBAAQ,KAAK,WAAW,UAAU;AAClC,gBAAQ,KAAK,UAAU,UAAU;AACjC,gBAAQ,KAAK,cAAc,UAAU;AAAA,MACvC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,QAAQ,OAA8B;AACpC,SAAK,MAAM,KAAK,KAAK;AACrB,QAAI,KAAK,MAAM,UAAU,KAAK,WAAW;AAEvC,WAAK,KAAK,MAAM;AAAA,IAClB;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,QAAuB;AAC3B,QAAI,KAAK,YAAY,KAAK,MAAM,WAAW,EAAG;AAE9C,SAAK,WAAW;AAChB,UAAM,QAAQ,KAAK,MAAM,OAAO,GAAG,KAAK,SAAS;AAEjD,QAAI;AACF,YAAM,KAAK,OAAO,SAAS,KAAK;AAAA,IAClC,QAAQ;AAAA,IAER,UAAE;AACA,WAAK,WAAW;AAAA,IAClB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,YAAkB;AACxB,QAAI,KAAK,MAAM,WAAW,EAAG;AAC7B,UAAM,QAAQ,KAAK,MAAM,OAAO,CAAC;AAEjC,SAAK,KAAK,OAAO,SAAS,KAAK;AAAA,EACjC;AAAA;AAAA,EAGA,UAAgB;AACd,QAAI,KAAK,OAAO;AACd,oBAAc,KAAK,KAAK;AACxB,WAAK,QAAQ;AAAA,IACf;AACA,SAAK,KAAK,MAAM;AAAA,EAClB;AAAA,EAEQ,OAAO,MAAuB;AACpC,QAAI,KAAK,MAAO,SAAQ,IAAI,oBAAoB,GAAG,IAAI;AAAA,EACzD;AACF;AAIO,SAAS,YAAY,SAAgE;AAC1F,QAAM,YAAY,QAAQ,iBAAiB;AAC3C,MAAI,WAAW;AACb,UAAM,QAAQ,MAAM,QAAQ,SAAS,IAAI,UAAU,CAAC,IAAI;AACxD,WAAO,MAAM,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK;AAAA,EAClC;AACA,SAAQ,QAAQ,WAAW,KAAgB;AAC7C;;;AC7FA,IAAM,iBAAiC;AAAA;AAAA,EAErC,EAAE,MAAM,UAAU,cAAc,UAAU,UAAU,CAAC,WAAW,eAAe,EAAE;AAAA,EACjF,EAAE,MAAM,oBAAoB,cAAc,UAAU,UAAU,CAAC,mBAAmB,EAAE;AAAA;AAAA,EAEpF,EAAE,MAAM,aAAa,cAAc,aAAa,UAAU,CAAC,cAAc,eAAe,eAAe,EAAE;AAAA;AAAA,EAEzG,EAAE,MAAM,mBAAmB,cAAc,aAAa,UAAU,CAAC,kBAAkB,EAAE;AAAA,EACrF,EAAE,MAAM,eAAe,cAAc,aAAa,UAAU,CAAC,cAAc,EAAE;AAAA,EAC7E,EAAE,MAAM,aAAa,cAAc,UAAU,UAAU,CAAC,YAAY,EAAE;AAAA;AAAA,EAEtE,EAAE,MAAM,WAAW,cAAc,aAAa,UAAU,CAAC,YAAY,cAAc,EAAE;AAAA;AAAA,EAErF,EAAE,MAAM,iBAAiB,cAAc,cAAc,UAAU,CAAC,gBAAgB,EAAE;AAAA;AAAA,EAElF,EAAE,MAAM,UAAU,cAAc,WAAW,UAAU,CAAC,SAAS,EAAE;AAAA;AAAA,EAEjE,EAAE,MAAM,sBAAsB,cAAc,QAAQ,UAAU,CAAC,qBAAqB,EAAE;AAAA,EACtF,EAAE,MAAM,eAAe,cAAc,QAAQ,UAAU,CAAC,wBAAwB,cAAc,EAAE;AAAA;AAAA,EAEhG,EAAE,MAAM,YAAY,cAAc,SAAS,UAAU,CAAC,WAAW,EAAE;AAAA;AAAA,EAEnE,EAAE,MAAM,cAAc,cAAc,aAAa,UAAU,CAAC,aAAa,EAAE;AAAA;AAAA,EAE3E,EAAE,MAAM,eAAe,cAAc,YAAY,UAAU,CAAC,cAAc,EAAE;AAAA;AAAA,EAE5E,EAAE,MAAM,eAAe,cAAc,cAAc,UAAU,CAAC,cAAc,EAAE;AAAA;AAAA,EAE9E,EAAE,MAAM,aAAa,cAAc,UAAU,UAAU,CAAC,cAAc,YAAY,EAAE;AAAA;AAAA,EAEpF,EAAE,MAAM,UAAU,cAAc,0BAA0B,UAAU,CAAC,SAAS,EAAE;AAAA;AAAA,EAEhF,EAAE,MAAM,cAAc,cAAc,cAAc,UAAU,CAAC,aAAa,EAAE;AAC9E;AAMA,IAAM,qBAAyC;AAAA,EAC7C,CAAC,WAAW,GAAI;AAAA,EAChB,CAAC,cAAc,GAAI;AAAA,EACnB,CAAC,gBAAgB,IAAI;AAAA,EACrB,CAAC,WAAW,IAAI;AAAA,EAChB,CAAC,eAAe,IAAI;AAAA,EACpB,CAAC,0BAA0B,IAAI;AAAA,EAC/B,CAAC,oBAAoB,IAAI;AAAA,EACzB,CAAC,0BAA0B,GAAI;AACjC;AASA,IAAM,gBAA0B;AAAA,EAC9B;AAAA,EACA;AACF;AAEO,SAAS,cAAc,WAAgC;AAC5D,MAAI,CAAC,aAAa,UAAU,SAAS,GAAG;AACtC,WAAO,EAAE,MAAM,OAAO,MAAM,MAAM,cAAc,MAAM,YAAY,KAAK,iBAAiB,YAAY;AAAA,EACtG;AAGA,aAAW,WAAW,gBAAgB;AACpC,eAAW,WAAW,QAAQ,UAAU;AACtC,UAAI,QAAQ,KAAK,SAAS,GAAG;AAC3B,eAAO;AAAA,UACL,MAAM;AAAA,UACN,MAAM,QAAQ;AAAA,UACd,cAAc,QAAQ;AAAA,UACtB,YAAY;AAAA,UACZ,iBAAiB;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,aAAW,WAAW,eAAe;AACnC,QAAI,QAAQ,KAAK,SAAS,GAAG;AAC3B,aAAO,EAAE,MAAM,OAAO,MAAM,MAAM,cAAc,MAAM,YAAY,MAAM,iBAAiB,YAAY;AAAA,IACvG;AAAA,EACF;AAGA,aAAW,CAAC,SAAS,UAAU,KAAK,oBAAoB;AACtD,QAAI,QAAQ,KAAK,SAAS,GAAG;AAC3B,aAAO;AAAA,QACL,MAAM;AAAA,QACN,MAAM,eAAe,SAAS;AAAA,QAC9B,cAAc;AAAA,QACd;AAAA,QACA,iBAAiB;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AAGA,MAAI,QAAQ;AACZ,MAAI,8BAA8B,KAAK,SAAS,EAAG,UAAS;AAC5D,MAAI,uDAAuD,KAAK,SAAS,EAAG,UAAS;AACrF,MAAI,CAAC,UAAU,SAAS,SAAS,EAAG,UAAS;AAC7C,MAAI,UAAU,SAAS,GAAI,UAAS;AAEpC,MAAI,SAAS,KAAK;AAChB,WAAO;AAAA,MACL,MAAM;AAAA;AAAA,MACN,MAAM;AAAA,MACN,cAAc;AAAA,MACd,YAAY,KAAK,IAAI,OAAO,GAAG;AAAA,MAC/B,iBAAiB;AAAA,IACnB;AAAA,EACF;AAEA,SAAO,EAAE,MAAM,OAAO,MAAM,MAAM,cAAc,MAAM,YAAY,KAAK,iBAAiB,OAAO;AACjG;AAEA,SAAS,eAAe,WAA2B;AACjD,QAAM,QAAQ,UAAU,MAAM,UAAU;AACxC,aAAW,QAAQ,OAAO;AACxB,QAAI,wBAAwB,KAAK,IAAI,KAAK,KAAK,SAAS,GAAG;AACzD,aAAO,KAAK,QAAQ,mBAAmB,EAAE;AAAA,IAC3C;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,uBAAiC;AAC/C,SAAO,eAAe,IAAI,OAAK,EAAE,IAAI;AACvC;AAoBA,IAAM,mBAAqC;AAAA;AAAA,EAEzC,EAAE,MAAM,cAAc,UAAU,CAAC,aAAa,EAAE;AAAA;AAAA,EAEhD,EAAE,MAAM,+BAA+B,UAAU,CAAC,oCAAoC,EAAE;AAAA;AAAA,EAExF,EAAE,MAAM,aAAa,UAAU,CAAC,mBAAmB,YAAY,EAAE;AAAA;AAAA,EAEjE,EAAE,MAAM,SAAS,UAAU,CAAC,WAAW,EAAE;AAC3C;AAeO,SAAS,qBAAqB,WAAuC;AAC1E,MAAI,CAAC,UAAW,QAAO,EAAE,kBAAkB,OAAO,MAAM,KAAK;AAC7D,aAAW,WAAW,kBAAkB;AACtC,eAAW,WAAW,QAAQ,UAAU;AACtC,UAAI,QAAQ,KAAK,SAAS,GAAG;AAC3B,eAAO,EAAE,kBAAkB,MAAM,MAAM,QAAQ,KAAK;AAAA,MACtD;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,kBAAkB,OAAO,MAAM,KAAK;AAC/C;;;ACnKA,IAAM,mBAA2C;AAAA,EAC/C,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,gBAAgB;AAAA;AAClB;AAEO,SAAS,mBAAmB,QAAuB,QAAuB;AAC/E,QAAM,YAAY,OAAO,aAAa;AAEtC,SAAO,eAAe,YAAY,KAAyD;AAEzF,QAAI,CAAC,IAAI,SAAS,IAAI,MAAM,KAAK,MAAM,IAAI;AACzC,YAAM,SAAS,UAAU,IAAI,UAAU;AACvC,YAAM,YAAoC;AAAA,QACxC,SAAS;AAAA,QACT,UAAU,GAAG,MAAM,GAAG,SAAS;AAAA,QAC/B,aACE;AAAA,QAGF,cACE;AAAA,QACF,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,YAAY;AAAA,YACV,GAAG;AAAA,cACD,MAAM;AAAA,cACN,UAAU;AAAA,cACV,aAAa;AAAA,YACf;AAAA,YACA,MAAM;AAAA,cACJ,MAAM;AAAA,cACN,UAAU;AAAA,cACV,aAAa;AAAA,YACf;AAAA,YACA,aAAa;AAAA,cACX,MAAM;AAAA,cACN,UAAU;AAAA,cACV,aAAa;AAAA,YACf;AAAA,YACA,YAAY;AAAA,cACV,MAAM;AAAA,cACN,UAAU;AAAA,cACV,aAAa;AAAA,YACf;AAAA,UACF;AAAA,UACA,SAAS,GAAG,MAAM,GAAG,SAAS;AAAA,UAC9B,iBAAiB;AAAA,QACnB;AAAA,QACA,cAAc;AAAA,UACZ;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,QACA,YAAY;AAAA,MACd;AAEA,aAAO,EAAE,QAAQ,KAAK,MAAM,WAAW,SAAS,iBAAiB;AAAA,IACnE;AAGA,UAAM,eAAe,IAAI,MAAM,KAAK;AACpC,QAAI,aAAa,SAAS,KAAK;AAC7B,aAAO,cAAc,KAAK,kBAAkB,uCAAuC;AAAA,IACrF;AAGA,UAAM,YAAY,OAAO,WAAW;AACpC,UAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AACzC,UAAM,UAAU,KAAK,IAAI;AAEzB,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,YAAY,IAAI,eAAe;AAErC,QAAI;AACJ,QAAI;AACF,wBAAkB,MAAM,OAAO,MAAM;AAAA,QACnC,SAAS,OAAO;AAAA,QAChB,OAAO;AAAA,QACP,UAAU,IAAI;AAAA,QACd,UAAU,IAAI;AAAA,QACd,YAAY;AAAA,QACZ;AAAA,QACA,aAAa;AAAA,QACb,YAAY;AAAA,MACd,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,UAAI,eAAe,0BAA0B;AAC3C,eAAO;AAAA,UACL;AAAA,UACA;AAAA,UACA,kGAAkG,IAAI,YAAY;AAAA,QACpH;AAAA,MACF;AACA,aAAO,cAAc,KAAK,kBAAkB,2CAA2C;AAAA,IACzF;AAEA,UAAM,iBAAiB,KAAK,IAAI,IAAI;AAWpC,UAAM,MAAM,gBAAgB,YACvB,MAAM,QAAQ,gBAAgB,SAAS,IAAI,gBAAgB,YAAY,CAAC,gBAAgB,SAAS,IAClG,CAAC;AACL,eAAW,MAAM,KAAK;AACpB,WAAK,OAAO,cAAc;AAAA,QACxB,eAAe,GAAG;AAAA,QAClB,SAAS,OAAO;AAAA,QAChB,OAAO;AAAA,QACP,UAAU,IAAI;AAAA,QACd,UAAU,IAAI;AAAA,QACd;AAAA,MACF,CAAC;AAAA,IACH;AAGA,UAAM,gBAAoC;AAAA,MACxC,SAAS;AAAA,MACT,SAAS;AAAA,MACT,OAAO;AAAA,MACP,QAAQ,gBAAgB;AAAA,MACxB,SAAS,gBAAgB;AAAA,MACzB,YAAY,gBAAgB;AAAA,MAC5B,GAAI,gBAAgB,aAAa,EAAE,WAAW,gBAAgB,UAAU;AAAA,MACxE,UAAU;AAAA,QACR,YAAY;AAAA,QACZ,kBAAkB;AAAA,QAClB,aAAa,gBAAgB;AAAA,QAC7B,SAAS,OAAO;AAAA,QAChB;AAAA,MACF;AAAA,IACF;AAEA,WAAO,EAAE,QAAQ,KAAK,MAAM,eAAe,SAAS,iBAAiB;AAAA,EACvE;AACF;AAIA,SAAS,cACP,QACA,MACA,SACsB;AACtB,QAAM,OAA2B;AAAA,IAC/B,SAAS;AAAA,IACT,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,YAAY,OAAO,WAAW;AAAA,MAC9B,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC;AAAA,EACF;AACA,SAAO,EAAE,QAAQ,MAAM,SAAS,iBAAiB;AACnD;AAEA,SAAS,UAAU,KAAqB;AACtC,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,WAAO,OAAO;AAAA,EAChB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AChMO,SAAS,uBAAuB,QAAuB,QAAuB;AACnF,SAAO,eAAe,gBAAgB,KAAiE;AACrG,UAAM,EAAE,MAAM,OAAO,IAAI;AAGzB,QAAI,KAAK,SAAS,eAAe,GAAG;AAClC,YAAM,OAAO,MAAM,OAAO,aAAa;AACvC,aAAO,aAAa,KAAK,IAAI;AAAA,IAC/B;AAEA,QAAI,KAAK,SAAS,eAAe,GAAG;AAClC,YAAM,MAAM,IAAI,IAAI,MAAM,kBAAkB;AAC5C,YAAM,QAAQ,SAAS,IAAI,aAAa,IAAI,OAAO,KAAK,IAAI;AAC5D,YAAM,SAAS,SAAS,IAAI,aAAa,IAAI,QAAQ,KAAK,GAAG;AAC7D,YAAM,OAAO,MAAM,OAAO,kBAAkB,OAAO,QAAQ,OAAO,MAAM;AACxE,aAAO,aAAa,KAAK,IAAI;AAAA,IAC/B;AAEA,QAAI,KAAK,SAAS,cAAc,GAAG;AACjC,YAAM,MAAM,IAAI,IAAI,MAAM,kBAAkB;AAC5C,YAAM,QAAQ,SAAS,IAAI,aAAa,IAAI,OAAO,KAAK,IAAI;AAC5D,YAAM,SAAS,SAAS,IAAI,aAAa,IAAI,QAAQ,KAAK,GAAG;AAC7D,YAAM,OAAO,MAAM,OAAO,iBAAiB,OAAO,QAAQ,OAAO,MAAM;AACvE,aAAO,aAAa,KAAK,IAAI;AAAA,IAC/B;AAEA,QAAI,KAAK,SAAS,YAAY,GAAG;AAC/B,YAAM,OAAO,MAAM,OAAO,kBAAkB;AAC5C,aAAO,aAAa,KAAK,IAAI;AAAA,IAC/B;AAGA,UAAM,OAAO,iBAAiB,MAAM;AACpC,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,SAAS,EAAE,gBAAgB,YAAY;AAAA,IACzC;AAAA,EACF;AACF;AAEA,SAAS,aAAa,QAAgB,MAAqC;AACzE,SAAO;AAAA,IACL;AAAA,IACA,MAAM,KAAK,UAAU,IAAI;AAAA,IACzB,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,EAChD;AACF;AAEA,SAAS,iBAAiB,QAA+B;AAEvD,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iCAMmB,OAAO,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0EAiBiC,OAAO,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgOvF;;;ACzSO,IAAM,sBAAsB;AAY5B,SAAS,eACd,MACA,KACA,kBACQ;AACR,MAAI,CAAC,QAAQ,IAAI,WAAW,EAAG,QAAO;AACtC,MAAI,KAAK,SAAS,mBAAmB,EAAG,QAAO;AAE/C,MAAI,WAAW;AAMf,QAAM,eAAe,wBAAwB,GAAG;AAEhD,MAAI,SAAS,SAAS,YAAY,GAAG;AACnC,eAAW,SAAS,QAAQ,cAAc,GAAG,YAAY;AAAA,WAAc;AAAA,EACzE,WAAW,SAAS,SAAS,SAAS,GAAG;AACvC,eAAW,SAAS,QAAQ,WAAW,GAAG,YAAY;AAAA,QAAW;AAAA,EACnE,WAAW,CAAC,oBAAoB,SAAS,SAAS,SAAS,GAAG;AAI5D,eAAW,SAAS,QAAQ,WAAW,GAAG,YAAY;AAAA,QAAW;AAAA,EACnE;AAIA,MAAI,CAAC,oBAAoB,SAAS,SAAS,SAAS,GAAG;AACrD,UAAM,cAAc,iBAAiB,GAAG;AACxC,eAAW,SAAS,QAAQ,WAAW,GAAG,WAAW;AAAA,QAAW;AAAA,EAClE;AAEA,SAAO;AACT;AAMO,SAAS,qBAAqB,KAAuB;AAC1D,SAAO,KAAK;AAAA,IACV,IAAI,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,KAAK,GAAG,KAAK,YAAY,GAAG,WAAW,EAAE;AAAA,EAC7E;AACF;AAWA,SAAS,wBAAwB,KAAuB;AACtD,QAAM,aAAa,IAChB;AAAA,IACC,CAAC,OACC,8BAA8B,WAAW,GAAG,aAAa,CAAC,2CAE9C,WAAW,GAAG,GAAG,CAAC,8BAA8B,WAAW,GAAG,IAAI,CAAC,qBAC9D,WAAW,GAAG,UAAU,CAAC;AAAA,EAE9C,EACC,KAAK,IAAI;AACZ,SAAO,GAAG,mBAAmB;AAAA,EAAK,UAAU;AAC9C;AAMA,SAAS,iBAAiB,KAAuB;AAC/C,QAAM,UAAU,IAAI,IAAI,CAAC,QAAQ;AAAA,IAC/B,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,SAAS;AAAA,MACP,SAAS;AAAA,MACT,MAAM,GAAG;AAAA,MACT,KAAK,GAAG;AAAA,IACV;AAAA,IACA,aAAa,GAAG;AAAA,EAClB,EAAE;AACF,QAAM,KAAK,QAAQ,WAAW,IAAI,QAAQ,CAAC,IAAI;AAC/C,SAAO,sCAAsC,KAAK,UAAU,EAAE,CAAC;AACjE;AAEA,SAAS,WAAW,GAAmB;AACrC,SAAO,EACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;AAEA,SAAS,WAAW,GAAmB;AACrC,SAAO,EAAE,QAAQ,MAAM,QAAQ,EAAE,QAAQ,MAAM,OAAO;AACxD;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/logger.ts","../src/crawler.ts","../src/query-handler.ts","../src/dashboard-handler.ts","../src/ad-injection.ts"],"sourcesContent":["/**\n * HTTP client for the Apptvty API.\n *\n * Handles:\n * - Batch log ingestion (/v1/logs/batch)\n * - Query processing (/v1/query) — returns answer + optional ad\n * - Impression logging (/v1/impressions) — triggers billing for ads\n */\n\nimport type {\n ApptvtyConfig,\n BackendQueryResponse,\n CampaignRecord,\n CrawlerBreakdown,\n CreateCampaignParams,\n DailyStat,\n ImpressionLog,\n InsufficientBalanceError,\n PageAdsResponse,\n QueryRequest,\n RecentActivityItem,\n RecentQueryItem,\n RequestLogEntry,\n SiteOverviewStats,\n SiteWalletInfo,\n UpdateCampaignParams,\n} from './types.js';\n\nconst DEFAULT_BASE_URL = 'https://api.apptvty.com';\n\nfunction resolveBaseUrl(config: ApptvtyConfig): string {\n const raw: string =\n config.baseUrl ??\n (typeof process !== 'undefined' ? process.env?.APPTVTY_API_URL : undefined) ??\n DEFAULT_BASE_URL;\n return raw.replace(/\\/$/, '');\n}\n\nexport class ApptvtyClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly siteId: string;\n private readonly debug: boolean;\n\n private x402Token?: string;\n\n constructor(config: ApptvtyConfig) {\n this.baseUrl = resolveBaseUrl(config);\n this.apiKey = config.apiKey;\n this.siteId = config.siteId;\n this.debug = config.debug ?? false;\n }\n\n /**\n * Set the X402 (LSAT) token for subsequent requests.\n * This is called after the agent has successfully paid an X402 challenge.\n */\n setX402Token(macaroon: string, preimage: string): void {\n this.x402Token = `LSAT ${macaroon}:${preimage}`;\n }\n\n /**\n * Send a batch of request log entries to the Apptvty ingestion API.\n * Called by the logger's auto-flush — not called directly by user code.\n */\n async sendLogs(logs: RequestLogEntry[]): Promise<void> {\n if (logs.length === 0) return;\n\n try {\n await this.post('/v1/logs/batch', { logs });\n this.log(`Flushed ${logs.length} log(s)`);\n } catch (err) {\n if (err instanceof ApptvtyTrialExpiredError) {\n // Always visible — coding agents and developers need to see this\n console.warn(\n `\\n[apptvty] ⚠ FREE TRIAL EXPIRED\\n` +\n `[apptvty] Log in and upgrade to keep receiving agent analytics.\\n` +\n `[apptvty] Dashboard: ${err.dashboardUrl}\\n`,\n );\n return;\n }\n // Other logging failures must never propagate to the user's request path\n this.warn('Failed to send logs:', err);\n }\n }\n\n /**\n * Send a query to the Apptvty backend.\n * The backend runs RAG against the site's indexed content and,\n * if ads are enabled for the site, attaches a relevant sponsored ad.\n *\n * @throws on non-retryable errors (network errors, 5xx) — callers should catch\n */\n async query(req: QueryRequest): Promise<BackendQueryResponse> {\n // ApptvtyTrialExpiredError propagates to the query handler, which surfaces it to the agent\n const response = await this.post<BackendQueryResponse>('/v1/query', req);\n return response;\n }\n\n /**\n * Get relevant ads for a page (for HTML injection when crawler is detected).\n * Used by middleware to inject ads into HTML responses.\n */\n async getAdsForPage(req: { site_id: string; page_path: string }): Promise<PageAdsResponse> {\n const response = await this.post<PageAdsResponse>('/v1/ads/for-page', req);\n return response;\n }\n\n /**\n * Log an ad impression back to Apptvty.\n *\n * Called after returning a query response that contains a `sponsored` block.\n * This is what triggers the billing cycle:\n * - Advertiser gets charged (debited from their USDC ad budget)\n * - Publisher (the website) gets credited in USDC\n *\n * This call is fire-and-forget. The SDK logs a warning on failure\n * but does not throw — a missed impression log is better than\n * breaking the query response.\n */\n async logImpression(impression: ImpressionLog): Promise<void> {\n try {\n await this.post('/v1/impressions', impression);\n this.log(`Impression logged: ${impression.impression_id}`);\n } catch (err) {\n this.warn('Failed to log impression (billing may be delayed):', err);\n }\n }\n\n // ─── Analytics (for coding agents) ───────────────────────────────────────────\n // These allow agents to check activity, logs, and errors without a human.\n\n /** Get 30-day traffic overview (requests, AI %, crawlers, queries). */\n async getSiteStats(): Promise<SiteOverviewStats> {\n return this.get<SiteOverviewStats>(`/v1/sites/${this.siteId}/stats`);\n }\n\n /** Get day-by-day stats (default 30 days, max 90). */\n async getSiteDailyStats(days = 30): Promise<{ days: number; stats: DailyStat[] }> {\n return this.get(`/v1/sites/${this.siteId}/stats/daily`, { days: String(days) });\n }\n\n /** Get recent activity logs (last 48h). */\n async getRecentActivity(siteId: string, limit = 50, offset = 0): Promise<RecentActivityItem[]> {\n return this.get(`/v1/sites/${siteId}/activity`, {\n limit: String(limit),\n offset: String(offset),\n });\n }\n\n /** Get recent agent queries. */\n async getRecentQueries(siteId: string, limit = 20, offset = 0): Promise<RecentQueryItem[]> {\n return this.get(`/v1/sites/${siteId}/queries`, {\n limit: String(limit),\n offset: String(offset),\n });\n }\n\n /** Get crawler breakdown by type (default 30 days). */\n async getSiteCrawlers(days = 30): Promise<{ days: number; crawlers: CrawlerBreakdown[] }> {\n return this.get(`/v1/sites/${this.siteId}/crawlers`, { days: String(days) });\n }\n\n /** Get recent activity feed (last hour, default 50 items, max 200). */\n async getSiteActivity(limit = 50): Promise<{ activity: RecentActivityItem[] }> {\n return this.get(`/v1/sites/${this.siteId}/activity`, { limit: String(limit) });\n }\n\n /** Get recent agent queries (default 50, max 200). */\n async getSiteQueries(limit = 50): Promise<{ queries: RecentQueryItem[] }> {\n return this.get(`/v1/sites/${this.siteId}/queries`, { limit: String(limit) });\n }\n\n /** Get wallet balance and earnings. */\n async getSiteWallet(): Promise<SiteWalletInfo> {\n return this.get<SiteWalletInfo>(`/v1/sites/${this.siteId}/wallet`);\n }\n\n // ─── Campaign management (for coding agents) ────────────────────────────────\n //\n // A coding agent that installed the SDK can create and manage ad campaigns\n // programmatically — no human dashboard login required.\n //\n // Typical agentic advertiser flow:\n // 1. Check wallet balance: const w = await client.getSiteWallet()\n // 2. Fund wallet if needed: send USDC to w.wallet_address on Base chain\n // 3. Create campaign: await client.createCampaign({ ... })\n // 4. Monitor performance: await client.getCampaign(id)\n // 5. Adjust or pause: await client.updateCampaign(id, { status: 'paused' })\n\n /**\n * Create an ad campaign. The campaign goes live immediately once the wallet\n * has sufficient balance.\n *\n * If the wallet balance is too low, throws `ApptvtyInsufficientBalanceError`\n * which includes deposit instructions so the agent can fund the wallet and retry.\n *\n * @example\n * // Site-based: ad copy derived from your site's crawled content\n * const campaign = await client.createCampaign({\n * name: 'My Kubernetes Blog',\n * advertiser_site_id: 'site_abc123',\n * keywords: ['kubernetes', 'devops', 'containers'],\n * categories: ['technology'],\n * bid_per_view_usdc: 0.001,\n * daily_budget_usdc: 1.0,\n * total_budget_usdc: 20.0,\n * });\n *\n * // Static: manually written ad copy\n * const campaign = await client.createCampaign({\n * name: 'My Blog — Static',\n * ad_text: 'Deep dives on Kubernetes, written by practitioners.',\n * landing_url: 'https://myblog.com',\n * keywords: ['kubernetes', 'devops'],\n * categories: ['technology'],\n * bid_per_view_usdc: 0.001,\n * daily_budget_usdc: 1.0,\n * total_budget_usdc: 10.0,\n * });\n */\n async createCampaign(params: CreateCampaignParams): Promise<CampaignRecord> {\n try {\n return await this.post<CampaignRecord>('/v1/campaigns', params);\n } catch (err) {\n if (err instanceof ApptvtyApiError && err.statusCode === 402) {\n let details: InsufficientBalanceError | undefined;\n try {\n const body = JSON.parse(err.body);\n details = body?.error as InsufficientBalanceError;\n } catch { /* ignore parse errors */ }\n throw new ApptvtyInsufficientBalanceError(details);\n }\n throw err;\n }\n }\n\n /**\n * List all campaigns for this account.\n * Also returns the schema (valid categories, minimum bid) so the agent\n * knows valid field values without guessing.\n */\n async listCampaigns(): Promise<{\n campaigns: CampaignRecord[];\n total: number;\n schema: { valid_categories: string[]; valid_statuses: string[]; bid_per_view_usdc_minimum: number };\n }> {\n return this.get('/v1/campaigns');\n }\n\n /**\n * Get a single campaign by ID, including current spend and impression count.\n * `budget_remaining_usdc` tells the agent how much budget is left before\n * the campaign auto-pauses.\n */\n async getCampaign(campaignId: string): Promise<CampaignRecord> {\n return this.get<CampaignRecord>(`/v1/campaigns/${campaignId}`);\n }\n\n /**\n * Partially update a campaign. Only fields present in `params` are changed.\n *\n * @example\n * // Pause a campaign\n * await client.updateCampaign(id, { status: 'paused' });\n *\n * // Increase bid and daily budget\n * await client.updateCampaign(id, { bid_per_view_usdc: 0.002, daily_budget_usdc: 5.0 });\n */\n async updateCampaign(campaignId: string, params: UpdateCampaignParams): Promise<CampaignRecord> {\n return this.patch<CampaignRecord>(`/v1/campaigns/${campaignId}`, params);\n }\n\n /**\n * Pause a campaign immediately. Equivalent to updateCampaign(id, { status: 'paused' }).\n * Campaigns are never deleted — billing history is retained.\n */\n async pauseCampaign(campaignId: string): Promise<void> {\n await this.delete(`/v1/campaigns/${campaignId}`);\n }\n\n /**\n * Resume a paused campaign.\n */\n async resumeCampaign(campaignId: string): Promise<void> {\n await this.patch(`/v1/campaigns/${campaignId}`, { status: 'active' });\n }\n\n private async get<T>(path: string, params?: Record<string, string>): Promise<T> {\n const url = new URL(`${this.baseUrl}${path}`);\n if (params) {\n Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));\n }\n const response = await fetch(url.toString(), {\n method: 'GET',\n headers: {\n Authorization: this.x402Token ?? `Bearer ${this.apiKey}`,\n 'User-Agent': 'apptvty-sdk/0.1.0',\n },\n signal: AbortSignal.timeout(10_000),\n });\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n throw new ApptvtyApiError(response.status, path, text);\n }\n return response.json() as Promise<T>;\n }\n\n private async patch<T>(path: string, body: unknown): Promise<T> {\n const url = `${this.baseUrl}${path}`;\n const response = await fetch(url, {\n method: 'PATCH',\n headers: {\n 'Authorization': this.x402Token ?? `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n 'User-Agent': 'apptvty-sdk/0.1.0',\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(10_000),\n });\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n throw new ApptvtyApiError(response.status, path, text);\n }\n return response.json() as Promise<T>;\n }\n\n private async delete(path: string): Promise<void> {\n const url = `${this.baseUrl}${path}`;\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n 'Authorization': this.x402Token ?? `Bearer ${this.apiKey}`,\n 'User-Agent': 'apptvty-sdk/0.1.0',\n },\n signal: AbortSignal.timeout(10_000),\n });\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n throw new ApptvtyApiError(response.status, path, text);\n }\n }\n\n /**\n * Settle an X402 challenge from the site's own USDC wallet balance.\n * Called automatically when `post()` receives a 402 with an X402 header.\n * Returns the preimage on success, or throws if the wallet balance is too low.\n */\n private async payX402Challenge(macaroon: string): Promise<string> {\n const url = `${this.baseUrl}/v1/x402/pay`;\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n 'User-Agent': 'apptvty-sdk/0.1.0',\n },\n body: JSON.stringify({ macaroon }),\n signal: AbortSignal.timeout(10_000),\n });\n\n const text = await response.text().catch(() => '');\n if (!response.ok) {\n let details: InsufficientBalanceError | undefined;\n try { details = JSON.parse(text)?.error?.details as InsufficientBalanceError; } catch {}\n throw new ApptvtyInsufficientBalanceError(details);\n }\n\n const json = JSON.parse(text) as { preimage: string };\n return json.preimage;\n }\n\n private async post<T>(path: string, body: unknown): Promise<T> {\n const url = `${this.baseUrl}${path}`;\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Authorization': this.x402Token ?? `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n 'User-Agent': 'apptvty-sdk/0.1.0',\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(10_000),\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n if (response.status === 402) {\n const authHeader = response.headers.get('WWW-Authenticate');\n if (authHeader?.includes('X402') || authHeader?.includes('LSAT')) {\n const macaroon = authHeader.match(/macaroon=\"([^\"]+)\"/)?.[1] || '';\n // Auto-pay from wallet balance and retry once\n const preimage = await this.payX402Challenge(macaroon);\n this.setX402Token(macaroon, preimage);\n this.log('X402 challenge auto-paid from wallet, retrying request');\n return this.post<T>(path, body);\n }\n let dashboardUrl = 'https://dashboard.apptvty.com/login';\n try {\n const json = JSON.parse(text);\n dashboardUrl = json?.error?.details?.dashboard_url ?? dashboardUrl;\n } catch {}\n throw new ApptvtyTrialExpiredError(dashboardUrl);\n }\n throw new ApptvtyApiError(response.status, path, text);\n }\n\n return response.json() as Promise<T>;\n }\n\n private log(...args: unknown[]): void {\n if (this.debug) console.log('[apptvty]', ...args);\n }\n\n private warn(...args: unknown[]): void {\n if (this.debug) console.warn('[apptvty]', ...args);\n }\n}\n\nexport class ApptvtyApiError extends Error {\n constructor(\n public readonly statusCode: number,\n public readonly path: string,\n public readonly body: string,\n ) {\n super(`Apptvty API error ${statusCode} at ${path}: ${body}`);\n this.name = 'ApptvtyApiError';\n }\n}\n\nexport class ApptvtyTrialExpiredError extends Error {\n constructor(public readonly dashboardUrl: string) {\n super(`Apptvty free trial has expired. Log in to continue: ${dashboardUrl}`);\n this.name = 'ApptvtyTrialExpiredError';\n }\n}\n\nexport class ApptvtyX402Error extends Error {\n constructor(public readonly macaroon: string, public readonly invoice: string) {\n super(`X402 Payment Required: Activity trial has ended. Pay invoice to continue: ${invoice}`);\n this.name = 'ApptvtyX402Error';\n }\n}\n\n/**\n * Thrown by createCampaign() when the wallet balance is too low.\n *\n * The `details` property contains everything needed to fund the wallet\n * autonomously and retry — the agent can inspect `details.deposit.address`\n * and send USDC on Base without human intervention.\n *\n * @example\n * import { ApptvtyInsufficientBalanceError } from 'apptvty';\n *\n * try {\n * await client.createCampaign(params);\n * } catch (err) {\n * if (err instanceof ApptvtyInsufficientBalanceError) {\n * const { shortfall_usdc, deposit } = err.details;\n * // Agent can now: send shortfall_usdc USDC to deposit.address on deposit.chain\n * // Then retry createCampaign\n * }\n * }\n */\nexport class ApptvtyInsufficientBalanceError extends Error {\n constructor(public readonly details: import('./types.js').InsufficientBalanceError | undefined) {\n const shortfall = details?.shortfall_usdc ?? '?';\n super(\n `Wallet balance too low to create campaign. ` +\n `Send ${shortfall} USDC to ${details?.deposit?.address ?? 'your wallet'} on Base and retry.`,\n );\n this.name = 'ApptvtyInsufficientBalanceError';\n }\n}\n","/**\n * Batched request logger.\n *\n * Queues RequestLogEntry objects in memory and flushes them in batches\n * to the Apptvty API. This keeps per-request overhead near zero.\n *\n * Flush triggers:\n * 1. Queue reaches batchSize (default 50)\n * 2. flushInterval elapses (default 5s)\n * 3. process.exit / SIGTERM (best-effort sync flush)\n */\n\nimport type { ApptvtyClient } from './client.js';\nimport type { ApptvtyConfig, RequestLogEntry } from './types.js';\n\nexport class RequestLogger {\n private queue: RequestLogEntry[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private flushing = false;\n private readonly batchSize: number;\n private readonly debug: boolean;\n\n constructor(\n private readonly client: ApptvtyClient,\n config: ApptvtyConfig,\n ) {\n this.batchSize = config.batchSize ?? 50;\n this.debug = config.debug ?? false;\n\n const interval = config.flushInterval ?? 5_000;\n this.timer = setInterval(() => { void this.flush(); }, interval);\n\n // Unref so the timer doesn't keep the process alive\n if (this.timer && typeof (this.timer as any).unref === 'function') {\n (this.timer as any).unref();\n }\n\n // Best-effort flush on process shutdown (Node-only)\n if (typeof process !== 'undefined' && typeof process.once === 'function') {\n const handleExit = () => { void this.flushSync(); };\n try {\n process.once('SIGTERM', handleExit);\n process.once('SIGINT', handleExit);\n process.once('beforeExit', handleExit);\n } catch {\n // Ignore errors in environments where process exists but signal listeners don't\n }\n }\n }\n\n /** Enqueue a single log entry. Non-blocking. */\n enqueue(entry: RequestLogEntry): void {\n this.queue.push(entry);\n if (this.queue.length >= this.batchSize) {\n // Don't await — fire and forget\n void this.flush();\n }\n }\n\n /** Flush the current queue to the API. */\n async flush(): Promise<void> {\n if (this.flushing || this.queue.length === 0) return;\n\n this.flushing = true;\n const batch = this.queue.splice(0, this.batchSize);\n\n try {\n await this.client.sendLogs(batch);\n } catch {\n // Already handled (logged) inside client.sendLogs — nothing to do here\n } finally {\n this.flushing = false;\n }\n }\n\n /**\n * Synchronous-ish flush for process shutdown.\n * Fires the fetch and doesn't await to avoid blocking exit handlers.\n */\n private flushSync(): void {\n if (this.queue.length === 0) return;\n const batch = this.queue.splice(0);\n // Fire without await — best effort on exit\n void this.client.sendLogs(batch);\n }\n\n /** Stop the interval timer. Call when you want to fully tear down the SDK. */\n destroy(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n void this.flush();\n }\n\n private log(...args: unknown[]): void {\n if (this.debug) console.log('[apptvty:logger]', ...args);\n }\n}\n\n// ─── Helpers used by middleware to build log entries ──────────────────────────\n\nexport function getClientIp(headers: Record<string, string | string[] | undefined>): string {\n const forwarded = headers['x-forwarded-for'];\n if (forwarded) {\n const first = Array.isArray(forwarded) ? forwarded[0] : forwarded;\n return first.split(',')[0].trim();\n }\n return (headers['x-real-ip'] as string) ?? 'unknown';\n}\n","/**\n * Lightweight AI crawler detection.\n *\n * This is the single source of truth for crawler classification in the SDK.\n * The backend (Python analytics API and TypeScript handlers) should eventually\n * consume this same list rather than maintaining separate copies.\n */\n\nimport type { CrawlerInfo } from './types.js';\n\ninterface KnownCrawler {\n name: string;\n organization: string;\n patterns: RegExp[];\n}\n\nconst KNOWN_CRAWLERS: KnownCrawler[] = [\n // OpenAI\n { name: 'GPTBot', organization: 'OpenAI', patterns: [/GPTBot/i, /ChatGPT-User/i] },\n { name: 'OpenAI-SearchBot', organization: 'OpenAI', patterns: [/OpenAI-SearchBot/i] },\n // Anthropic\n { name: 'ClaudeBot', organization: 'Anthropic', patterns: [/ClaudeBot/i, /Claude-Web/i, /Anthropic-AI/i] },\n // Google\n { name: 'Google-Extended', organization: 'Google AI', patterns: [/Google-Extended/i] },\n { name: 'GoogleOther', organization: 'Google AI', patterns: [/GoogleOther/i] },\n { name: 'Googlebot', organization: 'Google', patterns: [/Googlebot/i] },\n // Microsoft\n { name: 'Bingbot', organization: 'Microsoft', patterns: [/bingbot/i, /BingPreview/i] },\n // Perplexity\n { name: 'PerplexityBot', organization: 'Perplexity', patterns: [/PerplexityBot/i] },\n // You.com\n { name: 'YouBot', organization: 'You.com', patterns: [/YouBot/i] },\n // Meta\n { name: 'Meta-ExternalAgent', organization: 'Meta', patterns: [/Meta-ExternalAgent/i] },\n { name: 'FacebookBot', organization: 'Meta', patterns: [/facebookexternalhit/i, /FacebookBot/i] },\n // Apple\n { name: 'AppleBot', organization: 'Apple', patterns: [/Applebot/i] },\n // Twitter/X\n { name: 'TwitterBot', organization: 'Twitter/X', patterns: [/Twitterbot/i] },\n // LinkedIn\n { name: 'LinkedInBot', organization: 'LinkedIn', patterns: [/LinkedInBot/i] },\n // DuckDuckGo\n { name: 'DuckDuckBot', organization: 'DuckDuckGo', patterns: [/DuckDuckBot/i] },\n // Cohere\n { name: 'Cohere-AI', organization: 'Cohere', patterns: [/Cohere-AI/i, /cohere-ai/i] },\n // Allen Institute\n { name: 'AI2Bot', organization: 'Allen Institute for AI', patterns: [/AI2Bot/i] },\n // Mistral\n { name: 'MistralBot', organization: 'Mistral AI', patterns: [/MistralBot/i] },\n];\n\n/**\n * Patterns that reliably indicate AI/LLM-related traffic.\n * Each entry is [pattern, confidence].\n */\nconst AI_PATTERN_MATCHES: [RegExp, number][] = [\n [/openai/i, 0.90],\n [/anthropic/i, 0.90],\n [/gpt|chatgpt/i, 0.85],\n [/claude/i, 0.85],\n [/perplexity/i, 0.85],\n [/llm|language[- ]model/i, 0.75],\n [/bot.*ai|ai.*bot/i, 0.75],\n [/search.*ai|ai.*search/i, 0.70],\n];\n\n/**\n * Patterns that strongly suggest a human browser.\n * We check these before generic bot scoring to reduce false positives.\n * NOTE: We deliberately exclude Chrome/Safari/Firefox from this list because\n * many bots spoof those strings. We only short-circuit on strings that bots\n * have little reason to include.\n */\nconst HUMAN_SIGNALS: RegExp[] = [\n /Mozilla\\/5\\.0.*\\(Windows NT.*\\) AppleWebKit.*Chrome.*Safari/i,\n /Mozilla\\/5\\.0.*\\(Macintosh.*\\) AppleWebKit.*Version.*Safari/i,\n];\n\nexport function detectCrawler(userAgent: string): CrawlerInfo {\n if (!userAgent || userAgent.length < 4) {\n return { isAi: false, name: null, organization: null, confidence: 0.3, detectionMethod: 'heuristic' };\n }\n\n // 1. Exact match against known crawlers — highest confidence\n for (const crawler of KNOWN_CRAWLERS) {\n for (const pattern of crawler.patterns) {\n if (pattern.test(userAgent)) {\n return {\n isAi: true,\n name: crawler.name,\n organization: crawler.organization,\n confidence: 0.95,\n detectionMethod: 'exact_match',\n };\n }\n }\n }\n\n // 2. Short-circuit for strong human browser signals\n for (const pattern of HUMAN_SIGNALS) {\n if (pattern.test(userAgent)) {\n return { isAi: false, name: null, organization: null, confidence: 0.85, detectionMethod: 'heuristic' };\n }\n }\n\n // 3. Pattern-based AI detection\n for (const [pattern, confidence] of AI_PATTERN_MATCHES) {\n if (pattern.test(userAgent)) {\n return {\n isAi: true,\n name: extractBotName(userAgent),\n organization: null,\n confidence,\n detectionMethod: 'pattern_match',\n };\n }\n }\n\n // 4. Generic heuristic scoring for unknown bots\n let score = 0;\n if (/bot|crawler|spider|scraper/i.test(userAgent)) score += 0.4;\n if (/python-requests|curl\\/|wget\\/|scrapy|go-http-client/i.test(userAgent)) score += 0.3;\n if (!userAgent.includes('Mozilla')) score += 0.2;\n if (userAgent.length < 20) score += 0.2;\n\n if (score >= 0.5) {\n return {\n isAi: false, // Generic bot — not classified as AI\n name: 'unknown_bot',\n organization: null,\n confidence: Math.min(score, 0.8),\n detectionMethod: 'heuristic',\n };\n }\n\n return { isAi: false, name: null, organization: null, confidence: 0.1, detectionMethod: 'none' };\n}\n\nfunction extractBotName(userAgent: string): string {\n const parts = userAgent.split(/[\\s/;(]+/);\n for (const part of parts) {\n if (/bot|agent|crawler|ai/i.test(part) && part.length > 2) {\n return part.replace(/[^a-zA-Z0-9-_]/g, '');\n }\n }\n return 'unknown_ai_bot';\n}\n\n/** Returns the list of all known crawlers for reference (e.g. for agents.txt generation) */\nexport function getKnownCrawlerNames(): string[] {\n return KNOWN_CRAWLERS.map(c => c.name);\n}\n\n// ─── Scraper service detection ────────────────────────────────────────────────\n//\n// These are intermediary services that fetch a page and convert it to clean\n// Markdown or structured data for LLM consumption. They behave differently from\n// direct AI crawlers:\n//\n// - They strip <head> content (JSON-LD, meta tags are lost)\n// - They apply readability filtering (non-content sections are stripped)\n// - Only <p>/<h*>/<li> inside <article>/<main> reliably survives\n//\n// Detecting them lets the middleware skip injection strategies that won't work\n// and focus on content-stream injection, which does.\n\ninterface ScraperService {\n name: string;\n patterns: RegExp[];\n}\n\nconst SCRAPER_SERVICES: ScraperService[] = [\n // Jina AI Reader — r.jina.ai/URL — converts any page to clean Markdown\n { name: 'JinaReader', patterns: [/JinaReader/i] },\n // Cloudflare Browser Rendering /crawl endpoint (open beta, announced March 2026)\n { name: 'Cloudflare-BrowserRendering', patterns: [/CloudflareBrowserRenderingCrawler/i] },\n // FireCrawl — LLM-ready content extraction service\n { name: 'FireCrawl', patterns: [/FireCrawlAgent/i, /firecrawl/i] },\n // Apify web scraping platform\n { name: 'Apify', patterns: [/ApifyBot/i] },\n];\n\nexport interface ScraperServiceInfo {\n isScraperService: boolean;\n /** Identifier of the matched service, or null if not a scraper service. */\n name: string | null;\n}\n\n/**\n * Detect whether the request comes from a known content-extraction scraper service\n * (Jina, FireCrawl, Cloudflare Browser Rendering, etc.) rather than a direct AI crawler.\n *\n * Use this alongside detectCrawler() — scraper services are a distinct category:\n * they proxy on behalf of an AI consumer but apply their own readability transform.\n */\nexport function detectScraperService(userAgent: string): ScraperServiceInfo {\n if (!userAgent) return { isScraperService: false, name: null };\n for (const service of SCRAPER_SERVICES) {\n for (const pattern of service.patterns) {\n if (pattern.test(userAgent)) {\n return { isScraperService: true, name: service.name };\n }\n }\n }\n return { isScraperService: false, name: null };\n}\n","/**\n * Framework-agnostic query endpoint handler.\n *\n * Serves the site's AEO (Agent Experience Optimization) query page.\n *\n * GET /query → Returns a self-describing discovery JSON\n * GET /query?q=<question> → Returns an AI-generated answer from the site's\n * indexed content, plus a sponsored ad if ads are\n * enabled and a matching ad exists.\n *\n * When an ad is included in the response, this handler fires an async\n * impression log back to Apptvty to trigger billing:\n * - Advertiser is charged (debited from their USDC ad budget)\n * - Publisher earns USDC (credited to their Crossmint wallet)\n */\n\nimport type { ApptvtyClient } from './client.js';\nimport { ApptvtyTrialExpiredError } from './client.js';\nimport type { ApptvtyConfig } from './types.js';\nimport type {\n AgentErrorResponse,\n AgentQueryResponse,\n QueryEndpointDiscovery,\n} from './types.js';\n\nexport interface QueryHandlerRequest {\n query: string | null; // ?q= param, null if not present\n lang: string | null; // ?lang= param\n surface_ads?: boolean; // ?surface_ads=1|0 — include ads (default true)\n ai_crawler?: boolean; // ?ai_crawler=1 — \"I'm an agent crawling, surface ads\"\n userAgent: string;\n ipAddress: string;\n /** Full URL of the request, used to build the example in the discovery response */\n requestUrl: string;\n}\n\nexport interface QueryHandlerResponse {\n status: number;\n body: AgentQueryResponse | QueryEndpointDiscovery | AgentErrorResponse;\n headers: Record<string, string>;\n}\n\nconst RESPONSE_HEADERS: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-store',\n 'X-Robots-Tag': 'noindex', // Don't index the query endpoint itself\n};\n\nexport function createQueryHandler(client: ApptvtyClient, config: ApptvtyConfig) {\n const queryPath = config.queryPath ?? '/query';\n\n return async function handleQuery(req: QueryHandlerRequest): Promise<QueryHandlerResponse> {\n // ── Discovery response (no ?q= param) ──────────────────────────────────\n if (!req.query || req.query.trim() === '') {\n const origin = getOrigin(req.requestUrl);\n const discovery: QueryEndpointDiscovery = {\n version: '1.0',\n endpoint: `${origin}${queryPath}`,\n description:\n 'Query this site\\'s content directly. Submit a question and receive a structured JSON ' +\n 'response with an AI-generated answer, source citations, and confidence score. ' +\n 'Ideal for AI agents, search engines, and automated systems seeking direct answers.',\n crawl_policy:\n 'Do not crawl this entire site unnecessarily. Use this query endpoint with ?q=your_question to get the relevant information you need. Full-site crawling is discouraged.',\n usage: {\n method: 'GET',\n parameters: {\n q: {\n type: 'string',\n required: true,\n description: 'Your question or search query (max 500 characters)',\n },\n lang: {\n type: 'string',\n required: false,\n description: 'Preferred response language (ISO 639-1 code, default: en)',\n },\n surface_ads: {\n type: 'boolean',\n required: false,\n description: 'Include sponsored ads in response (1 = yes, default; 0 = no). When crawling, use ai_crawler=1 to signal you want ads.',\n },\n ai_crawler: {\n type: 'boolean',\n required: false,\n description: 'Set to 1 when you are an AI agent/crawler. Signals the server to surface ads. Use with ?q=... for targeted answers + ads.',\n },\n },\n example: `${origin}${queryPath}?q=what+features+do+you+offer&ai_crawler=1`,\n response_format: 'application/json',\n },\n capabilities: [\n 'Natural language question answering',\n 'Source citations with relevance scores',\n 'Structured JSON for easy parsing',\n 'Supports follow-up context via session continuity',\n 'Query-preferred: use ?q= instead of full-site crawl',\n ],\n rate_limit: '100 requests per hour per IP',\n };\n\n return { status: 200, body: discovery, headers: RESPONSE_HEADERS };\n }\n\n // ── Validate query ──────────────────────────────────────────────────────\n const trimmedQuery = req.query.trim();\n if (trimmedQuery.length > 500) {\n return errorResponse(400, 'QUERY_TOO_LONG', 'Query must be 500 characters or fewer');\n }\n\n // ── Call Apptvty backend (RAG + optional ad) ────────────────────────────\n const requestId = crypto.randomUUID();\n const timestamp = new Date().toISOString();\n const startMs = Date.now();\n\n const surfaceAds = req.surface_ads !== false; // default true\n const aiCrawler = req.ai_crawler === true;\n\n let backendResponse;\n try {\n backendResponse = await client.query({\n site_id: config.siteId,\n query: trimmedQuery,\n agent_ua: req.userAgent,\n agent_ip: req.ipAddress,\n request_id: requestId,\n timestamp,\n surface_ads: surfaceAds,\n ai_crawler: aiCrawler,\n });\n } catch (err) {\n if (err instanceof ApptvtyTrialExpiredError) {\n return errorResponse(\n 402,\n 'TRIAL_EXPIRED',\n `Apptvty free trial has expired. The site owner must log in and upgrade to continue. Dashboard: ${err.dashboardUrl}`,\n );\n }\n return errorResponse(502, 'UPSTREAM_ERROR', 'Could not retrieve an answer at this time');\n }\n\n const responseTimeMs = Date.now() - startMs;\n\n // ── Log impression if an ad was served ─────────────────────────────────\n //\n // This is fire-and-forget. We do NOT await it — the agent gets its\n // response immediately, and the billing event is recorded asynchronously.\n //\n // Billing flow triggered by these calls (one per ad):\n // 1. Apptvty records impression_id + metadata\n // 2. Advertiser's USDC ad budget is debited\n // 3. Publisher's Crossmint wallet is credited\n const ads = backendResponse.sponsored\n ? (Array.isArray(backendResponse.sponsored) ? backendResponse.sponsored : [backendResponse.sponsored])\n : [];\n for (const ad of ads) {\n void client.logImpression({\n impression_id: ad.impression_id,\n site_id: config.siteId,\n query: trimmedQuery,\n agent_ua: req.userAgent,\n agent_ip: req.ipAddress,\n timestamp,\n });\n }\n\n // ── Build agent response ────────────────────────────────────────────────\n const agentResponse: AgentQueryResponse = {\n success: true,\n version: '1.0',\n query: trimmedQuery,\n answer: backendResponse.answer,\n sources: backendResponse.sources,\n confidence: backendResponse.confidence,\n ...(backendResponse.sponsored && { sponsored: backendResponse.sponsored }),\n metadata: {\n request_id: requestId,\n response_time_ms: responseTimeMs,\n tokens_used: backendResponse.tokens_used,\n site_id: config.siteId,\n timestamp,\n },\n };\n\n return { status: 200, body: agentResponse, headers: RESPONSE_HEADERS };\n };\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction errorResponse(\n status: number,\n code: string,\n message: string,\n): QueryHandlerResponse {\n const body: AgentErrorResponse = {\n success: false,\n error: {\n code,\n message,\n request_id: crypto.randomUUID(),\n timestamp: new Date().toISOString(),\n },\n };\n return { status, body, headers: RESPONSE_HEADERS };\n}\n\nfunction getOrigin(url: string): string {\n try {\n const parsed = new URL(url);\n return parsed.origin;\n } catch {\n return '';\n }\n}\n","/**\n * Framework-agnostic dashboard handler.\n * \n * Serves a localized analytics dashboard directly on the maintainer's domain.\n */\n\nimport type { ApptvtyClient } from './client.js';\nimport type { ApptvtyConfig } from './types.js';\n\nexport interface DashboardHandlerRequest {\n path: string;\n method: string;\n apiKey: string;\n siteId: string;\n authHeader?: string | null;\n}\n\nexport interface DashboardHandlerResponse {\n status: number;\n body: string | any;\n headers: Record<string, string>;\n}\n\nexport function createDashboardHandler(client: ApptvtyClient, config: ApptvtyConfig) {\n return async function handleDashboard(req: DashboardHandlerRequest): Promise<DashboardHandlerResponse> {\n const { path, method, authHeader } = req;\n\n // ── Security Check ────────────────────────────────────────────────────────\n if (config.dashboardSecret) {\n const url = new URL(path, 'http://localhost');\n const secretParam = url.searchParams.get('secret');\n const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null;\n \n const isAuthorized = (secretParam === config.dashboardSecret) || (bearerToken === config.dashboardSecret);\n \n if (!isAuthorized) {\n return jsonResponse(401, { \n error: 'Unauthorized', \n message: 'Dashboard access requires a valid secret. Please set APPTVTY_DASHBOARD_SECRET.' \n });\n }\n }\n\n // ── API Proxy Routes ─────────────────────────────────────────────────────\n if (path.includes('/api/overview')) {\n const data = await client.getSiteStats();\n return jsonResponse(200, data);\n }\n\n if (path.endsWith('/api/activity')) {\n const url = new URL(path, 'http://localhost');\n const limit = parseInt(url.searchParams.get('limit') || '50');\n const offset = parseInt(url.searchParams.get('offset') || '0');\n const data = await client.getRecentActivity(config.siteId, limit, offset);\n return jsonResponse(200, data);\n }\n\n if (path.endsWith('/api/queries')) {\n const url = new URL(path, 'http://localhost');\n const limit = parseInt(url.searchParams.get('limit') || '20');\n const offset = parseInt(url.searchParams.get('offset') || '0');\n const data = await client.getRecentQueries(config.siteId, limit, offset);\n return jsonResponse(200, data);\n }\n\n if (path.endsWith('/api/stats')) {\n const data = await client.getSiteDailyStats();\n return jsonResponse(200, data);\n }\n\n // ── Dashboard HTML ───────────────────────────────────────────────────────\n const html = getDashboardHtml(config);\n return {\n status: 200,\n body: html,\n headers: { 'Content-Type': 'text/html' },\n };\n };\n}\n\nfunction jsonResponse(status: number, data: any): DashboardHandlerResponse {\n return {\n status,\n body: JSON.stringify(data),\n headers: { 'Content-Type': 'application/json' },\n };\n}\n\nfunction getDashboardHtml(config: ApptvtyConfig): string {\n // We'll use a single-file React-like approach using a CDN for a premium feel\n return `\n<!DOCTYPE html>\n<html lang=\"en\" class=\"dark\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Apptvty Logs — ${config.siteId}</title>\n <script src=\"https://cdn.tailwindcss.com\"></script>\n <script src=\"https://unpkg.com/lucide@latest\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n <style>\n @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');\n body { font-family: 'Inter', sans-serif; background-color: #09090b; color: #fafafa; }\n .glass { background: rgba(24, 24, 27, 0.8); backdrop-filter: blur(12px); border: 1px solid rgba(39, 39, 42, 1); }\n .gradient-text { background: linear-gradient(to right, #60a5fa, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }\n </style>\n</head>\n<body class=\"p-6\">\n <div id=\"app\" class=\"max-w-7xl mx-auto space-y-6\">\n <!-- Header -->\n <header class=\"flex justify-between items-center mb-8\">\n <div>\n <h1 class=\"text-3xl font-bold gradient-text\">Activity Logs</h1>\n <p class=\"text-zinc-400\">Real-time agentic insights for ${config.siteId}</p>\n </div>\n <div class=\"flex items-center gap-3\">\n <span class=\"flex h-2 w-2 rounded-full bg-green-500 animate-pulse\"></span>\n <span class=\"text-sm font-medium text-zinc-300\">Live Connection</span>\n </div>\n </header>\n\n <!-- Stats Grid -->\n <div class=\"grid grid-cols-1 md:grid-cols-4 gap-4\" id=\"stats-grid\">\n <div class=\"glass p-5 rounded-2xl animate-pulse h-24\"></div>\n <div class=\"glass p-5 rounded-2xl animate-pulse h-24\"></div>\n <div class=\"glass p-5 rounded-2xl animate-pulse h-24\"></div>\n <div class=\"glass p-5 rounded-2xl animate-pulse h-24\"></div>\n </div>\n\n <!-- Main Content -->\n <div class=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <!-- Traffic Chart -->\n <div class=\"lg:col-span-2 glass p-6 rounded-2xl\">\n <h2 class=\"text-lg font-semibold mb-4\">Traffic Overview</h2>\n <div class=\"h-[300px]\">\n <canvas id=\"trafficChart\"></canvas>\n </div>\n </div>\n\n <!-- Recent Queries -->\n <div class=\"glass p-6 rounded-2xl flex flex-col h-[400px]\">\n <div class=\"flex justify-between items-center mb-4\">\n <h2 class=\"text-lg font-semibold\">Agent Queries</h2>\n <button id=\"load-more-queries\" class=\"text-xs text-blue-400 hover:text-blue-300 transition-colors\">Load More</button>\n </div>\n <div id=\"queries-list\" class=\"flex-1 overflow-y-auto space-y-3 custom-scrollbar\">\n <div class=\"text-zinc-500 text-sm italic\">Loading queries...</div>\n </div>\n </div>\n </div>\n\n <!-- Real-time Activity Table -->\n <div class=\"glass p-6 rounded-2xl overflow-hidden\">\n <div class=\"flex justify-between items-center mb-4\">\n <h2 class=\"text-lg font-semibold\">Real-time Activity</h2>\n <button id=\"load-more-activity\" class=\"text-sm px-3 py-1 bg-zinc-800 hover:bg-zinc-700 rounded-lg text-zinc-300 transition-colors\">Load More</button>\n </div>\n <div class=\"overflow-x-auto\">\n <table class=\"w-full text-left\">\n <thead>\n <tr class=\"text-zinc-500 text-sm border-b border-zinc-800\">\n <th class=\"pb-3 pr-4\">Timestamp</th>\n <th class=\"pb-3 pr-4\">Method</th>\n <th class=\"pb-3 pr-4\">Path</th>\n <th class=\"pb-3 pr-4\">Agent</th>\n <th class=\"pb-3\">Status</th>\n </tr>\n </thead>\n <tbody id=\"activity-body\" class=\"text-sm\">\n <tr><td colspan=\"5\" class=\"pt-4 text-zinc-500 italic\">Connecting to activity stream...</td></tr>\n </tbody>\n </table>\n </div>\n </div>\n </div>\n\n <script>\n const API_BASE = window.location.pathname.replace(/\\/$/, '');\n let queryOffset = 0;\n let activityOffset = 0;\n const LIMIT_QUERIES = 20;\n const LIMIT_ACTIVITY = 50;\n\n async function fetchData() {\n try {\n // Initial load or refresh (offset 0 resets)\n const [overview, stats] = await Promise.all([\n fetch(\\`\\${API_BASE}/api/overview\\`).then(r => r.json()),\n fetch(\\`\\${API_BASE}/api/stats\\`).then(r => r.json())\n ]);\n\n updateStats(overview);\n initChart(stats);\n\n // Fetch first pages if empty\n if (queryOffset === 0) fetchQueries(true);\n if (activityOffset === 0) fetchActivity(true);\n } catch (err) {\n console.error('Failed to fetch dashboard data:', err);\n }\n }\n\n async function fetchQueries(replace = false) {\n try {\n const data = await fetch(\\`\\${API_BASE}/api/queries?limit=\\${LIMIT_QUERIES}&offset=\\${queryOffset}\\`).then(r => r.json());\n updateQueries(data, replace);\n } catch (e) { console.error('Queries error:', e); }\n }\n\n async function fetchActivity(replace = false) {\n try {\n const data = await fetch(\\`\\${API_BASE}/api/activity?limit=\\${LIMIT_ACTIVITY}&offset=\\${activityOffset}\\`).then(r => r.json());\n updateActivity(data, replace);\n } catch (e) { console.error('Activity error:', e); }\n }\n\n function updateStats(data) {\n const grid = document.getElementById('stats-grid');\n grid.innerHTML = \\`\n <div class=\"glass p-5 rounded-2xl\">\n <p class=\"text-zinc-400 text-xs font-medium uppercase tracking-wider mb-1\">Total Requests</p>\n <p class=\"text-2xl font-bold\">\\${data.total_requests_30d.toLocaleString()}</p>\n </div>\n <div class=\"glass p-5 rounded-2xl border-l-4 border-blue-500\">\n <p class=\"text-zinc-400 text-xs font-medium uppercase tracking-wider mb-1\">AI Requests</p>\n <p class=\"text-2xl font-bold\">\\${data.ai_requests_30d.toLocaleString()}</p>\n </div>\n <div class=\"glass p-5 rounded-2xl\">\n <p class=\"text-zinc-400 text-xs font-medium uppercase tracking-wider mb-1\">AI Percentage</p>\n <p class=\"text-2xl font-bold text-blue-400\">\\${data.ai_percentage.toFixed(1)}%</p>\n </div>\n <div class=\"glass p-5 rounded-2xl\">\n <p class=\"text-zinc-400 text-xs font-medium uppercase tracking-wider mb-1\">Health Status</p>\n <p class=\"text-2xl font-bold text-green-400\">Optimal</p>\n </div>\n \\`;\n }\n\n function updateQueries(data, replace) {\n const list = document.getElementById('queries-list');\n const items = Array.isArray(data) ? data : (data.queries || []);\n const rows = items.map(q => \\`\n <div class=\"p-3 bg-zinc-900/50 rounded-xl border border-zinc-800 hover:border-zinc-700 transition-colors\">\n <p class=\"text-sm font-medium text-zinc-200\">\\${q.question || q.query}</p>\n <div class=\"mt-2 flex justify-between items-center text-[10px] text-zinc-500\">\n <span class=\"flex items-center gap-1\">\n <i data-lucide=\"clock\" class=\"w-3 h-3\"></i>\n \\${new Date(q.timestamp).toLocaleString()}\n </span>\n <span class=\"px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-400 border border-blue-500/20\">\n Agent Match\n </span>\n </div>\n </div>\n \\`).join('');\n\n if (replace) list.innerHTML = rows || '<div class=\"text-zinc-600 text-sm\">No recent queries.</div>';\n else list.insertAdjacentHTML('beforeend', rows);\n lucide.createIcons();\n }\n\n function updateActivity(data, replace) {\n const body = document.getElementById('activity-body');\n const items = Array.isArray(data) ? data : (data.activity || []);\n const rows = items.map(r => \\`\n <tr class=\"border-b border-zinc-800/50 hover:bg-zinc-800/20 transition-colors\">\n <td class=\"py-3 pr-4 text-zinc-400 text-xs\">\\${new Date(r.timestamp || r.created_at).toLocaleTimeString()}</td>\n <td class=\"py-3 pr-4 font-mono text-[10px] tracking-tight text-blue-400\">\\${r.method}</td>\n <td class=\"py-3 pr-4 text-zinc-300 max-w-xs truncate\">\\${r.path}</td>\n <td class=\"py-3 pr-4 text-zinc-500\">\\${r.crawler_type || 'Human'}</td>\n <td class=\"py-3\">\n <span class=\"px-2 py-0.5 rounded-md \\${r.status_code >= 400 ? 'bg-red-500/10 text-red-400' : 'bg-green-500/10 text-green-400'} text-[10px] font-medium border border-current/20\">\n \\${r.status_code || 200}\n </span>\n </td>\n </tr>\n \\`).join('');\n\n if (replace) body.innerHTML = rows || '<tr><td colspan=\"5\" class=\"py-4 text-center text-zinc-600\">No activity.</td></tr>';\n else body.insertAdjacentHTML('beforeend', rows);\n }\n\n function initChart(stats) {\n const canvas = document.getElementById('trafficChart');\n if (window.myChart) window.myChart.destroy();\n const ctx = canvas.getContext('2d');\n window.myChart = new Chart(ctx, {\n type: 'line',\n data: {\n labels: stats.map(s => s.date.split('-').slice(1).join('/')),\n datasets: [\n {\n label: 'AI Requests',\n data: stats.map(s => s.ai_requests),\n borderColor: '#3b82f6',\n backgroundColor: 'rgba(59, 130, 246, 0.1)',\n fill: true,\n tension: 0.4\n },\n {\n label: 'Total Requests',\n data: stats.map(s => s.total_requests),\n borderColor: '#8b5cf6',\n backgroundColor: 'rgba(139, 92, 246, 0.1)',\n fill: true,\n tension: 0.4\n }\n ]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false } },\n scales: {\n y: { beginAtZero: true, grid: { color: 'rgba(39, 39, 42, 0.5)' }, ticks: { color: '#71717a' } },\n x: { grid: { display: false }, ticks: { color: '#71717a' } }\n }\n }\n });\n }\n\n document.getElementById('load-more-queries').onclick = () => {\n queryOffset += LIMIT_QUERIES;\n fetchQueries(false);\n };\n\n document.getElementById('load-more-activity').onclick = () => {\n activityOffset += LIMIT_ACTIVITY;\n fetchActivity(false);\n };\n\n fetchData();\n setInterval(fetchData, 30000); // Polling for updates (stats only)\n </script>\n</body>\n</html>\n `;\n}\n","/**\n * Shared HTML ad injection logic for Next.js and Express middlewares.\n *\n * Three-layer strategy for maximum coverage across all scraping methodologies:\n *\n * Layer 1 — Content-stream injection (<p>[Sponsored]</p> inside <article>/<main>)\n * Survives: Jina r.jina.ai, FireCrawl, Cloudflare Browser Rendering, BeautifulSoup,\n * headless browsers (Playwright/Puppeteer), curl, requests, fetch.\n * Why: Readability algorithms (used by all content-extraction services) preserve\n * <p> tags inside <article>/<main>. They strip <section>, <aside>, <div class=\"ad\">.\n *\n * Layer 2 — JSON-LD in <head> (for direct HTML scrapers only)\n * Survives: BeautifulSoup, lxml, direct fetch of raw HTML.\n * Stripped by: Jina/FireCrawl/Cloudflare Markdown pipelines (they drop <head> content).\n * Why: Skipped for known scraper services — they won't see it.\n *\n * Layer 3 — HTTP response header (X-Sponsored-Content)\n * Survives: Any HTTP client that reads headers (requests, httpx, curl, fetch).\n * Note: BeautifulSoup itself doesn't read headers, but agents using requests+BS4 do.\n */\n\nimport type { PageAd } from './types.js';\n\nexport const AD_INJECTION_MARKER = '<!-- apptvty-sponsored -->';\n\n/**\n * Inject sponsored content into an HTML string using all applicable layers.\n *\n * @param html The original HTML response body.\n * @param ads Matched ads from the Apptvty network.\n * @param isScraperService True when the request came from Jina/FireCrawl/Cloudflare/etc.\n * Skips JSON-LD injection (they strip <head>) and skips the\n * fallback <body> widget (they strip non-content sections).\n * @returns Modified HTML, or the original if nothing was injected.\n */\nexport function injectIntoHtml(\n html: string,\n ads: PageAd[],\n isScraperService: boolean,\n): string {\n if (!html || ads.length === 0) return html;\n if (html.includes(AD_INJECTION_MARKER)) return html; // already injected\n\n let modified = html;\n\n // ── Layer 1: Content-stream injection ────────────────────────────────────────\n // Plain <p> tags inside the main content element.\n // Must go INSIDE </article> or </main>, not after </body> — readability filters\n // strip content outside the primary content container.\n const contentBlock = buildContentStreamBlock(ads);\n\n if (modified.includes('</article>')) {\n modified = modified.replace('</article>', `${contentBlock}\\n</article>`);\n } else if (modified.includes('</main>')) {\n modified = modified.replace('</main>', `${contentBlock}\\n</main>`);\n } else if (!isScraperService && modified.includes('</body>')) {\n // Fallback: append before </body> only for direct scrapers.\n // Scraper services apply readability and will strip this position,\n // so we skip it — better to omit than inject something invisible.\n modified = modified.replace('</body>', `${contentBlock}\\n</body>`);\n }\n\n // ── Layer 2: JSON-LD in <head> ────────────────────────────────────────────────\n // Skip for known scraper services — they convert to Markdown and drop <head>.\n if (!isScraperService && modified.includes('</head>')) {\n const jsonLdBlock = buildJsonLdBlock(ads);\n modified = modified.replace('</head>', `${jsonLdBlock}\\n</head>`);\n }\n\n return modified;\n}\n\n/**\n * Build the X-Sponsored-Content header value.\n * Format: JSON array — readable by any HTTP client that inspects response headers.\n */\nexport function buildSponsoredHeader(ads: PageAd[]): string {\n return JSON.stringify(\n ads.map((ad) => ({ text: ad.text, url: ad.url, advertiser: ad.advertiser })),\n );\n}\n\n// ─── Private builders ─────────────────────────────────────────────────────────\n\n/**\n * Plain <p> tags — the only structure that survives readability-based content\n * extraction (Jina, FireCrawl, Cloudflare Browser Rendering, etc.).\n *\n * Uses rel=\"sponsored\" per Google's link attribute spec.\n * Uses data-apptvty-sponsored for impression tracking by downstream agents.\n */\nfunction buildContentStreamBlock(ads: PageAd[]): string {\n const paragraphs = ads\n .map(\n (ad) =>\n `<p data-apptvty-sponsored=\"${escapeAttr(ad.impression_id)}\">` +\n `<strong>[Sponsored]</strong> ` +\n `<a href=\"${escapeAttr(ad.url)}\" rel=\"sponsored noopener\">${escapeHtml(ad.text)}</a>` +\n ` \\u2014 <span>${escapeHtml(ad.advertiser)}</span>` +\n `</p>`,\n )\n .join('\\n');\n return `${AD_INJECTION_MARKER}\\n${paragraphs}`;\n}\n\n/**\n * Schema.org JSON-LD block for direct HTML scrapers (BeautifulSoup, lxml).\n * soup.find('script', {'type': 'application/ld+json'}) returns this.\n */\nfunction buildJsonLdBlock(ads: PageAd[]): string {\n const entries = ads.map((ad) => ({\n '@context': 'https://schema.org',\n '@type': 'WPAdBlock',\n sponsor: {\n '@type': 'Organization',\n name: ad.advertiser,\n url: ad.url,\n },\n description: ad.text,\n }));\n const ld = entries.length === 1 ? entries[0] : entries;\n return `<script type=\"application/ld+json\">${JSON.stringify(ld)}</script>`;\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction escapeAttr(s: string): string {\n return s.replace(/\"/g, '"').replace(/'/g, ''');\n}\n"],"mappings":"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","names":[]}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
detectScraperService,
|
|
9
9
|
getClientIp,
|
|
10
10
|
injectIntoHtml
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-GMQN6656.mjs";
|
|
12
12
|
|
|
13
13
|
// src/middleware/nextjs.ts
|
|
14
14
|
import { NextResponse } from "next/server";
|
|
@@ -121,7 +121,8 @@ function createNextjsDashboardHandler(config) {
|
|
|
121
121
|
path: request.nextUrl.pathname + request.nextUrl.search,
|
|
122
122
|
method: request.method,
|
|
123
123
|
apiKey: config.apiKey,
|
|
124
|
-
siteId: config.siteId
|
|
124
|
+
siteId: config.siteId,
|
|
125
|
+
authHeader: request.headers.get("Authorization")
|
|
125
126
|
});
|
|
126
127
|
if (result.headers["Content-Type"] === "text/html") {
|
|
127
128
|
return new NextResponse(result.body, {
|
|
@@ -175,4 +176,4 @@ export {
|
|
|
175
176
|
createNextjsQueryHandler,
|
|
176
177
|
createNextjsDashboardHandler
|
|
177
178
|
};
|
|
178
|
-
//# sourceMappingURL=chunk-
|
|
179
|
+
//# sourceMappingURL=chunk-JNM4IJDR.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/middleware/nextjs.ts"],"sourcesContent":["/**\n * Next.js App Router integration for the Apptvty SDK.\n *\n * Usage — two pieces:\n *\n * 1. Wrap your existing middleware.ts (or use ours standalone):\n *\n * import { withApptvty } from 'apptvty/nextjs';\n * export default withApptvty({ apiKey: 'ak_...', siteId: 'site_...' });\n * // or wrap your existing middleware:\n * export default withApptvty(config, yourMiddleware);\n *\n * 2. Create app/query/route.ts for the AEO query endpoint:\n *\n * import { createNextjsQueryHandler } from 'apptvty/nextjs';\n * export const GET = createNextjsQueryHandler({ apiKey: 'ak_...', siteId: 'site_...' });\n *\n * Ad injection strategy (three layers, applied for all AI/scraper traffic):\n *\n * Layer 1 — <p>[Sponsored]</p> inside <article>/<main>\n * Survives Jina, FireCrawl, Cloudflare Browser Rendering, BeautifulSoup,\n * headless browsers, curl, requests. Applied for all crawler types.\n *\n * Layer 2 — JSON-LD <script> in <head>\n * For direct HTML scrapers (BeautifulSoup, lxml). Skipped for known\n * scraper services (Jina/FireCrawl/Cloudflare) — they drop <head> on\n * Markdown conversion.\n *\n * Layer 3 — X-Sponsored-Content response header\n * For any HTTP client that reads response headers (requests, httpx, curl).\n * Applied for all crawler types.\n */\n\nimport type { NextRequest } from 'next/server';\nimport { NextResponse } from 'next/server';\nimport { ApptvtyClient } from '../client.js';\nimport { detectCrawler, detectScraperService } from '../crawler.js';\nimport { RequestLogger, getClientIp } from '../logger.js';\nimport { createQueryHandler } from '../query-handler.js';\nimport { createDashboardHandler } from '../dashboard-handler.js';\nimport { injectIntoHtml, buildSponsoredHeader } from '../ad-injection.js';\nimport type { ApptvtyConfig, RequestLogEntry } from '../types.js';\n\n/** Convert Next request headers to a plain object (Web API Headers.entries() at runtime). */\nfunction headersToRecord(h: NextRequest['headers']): Record<string, string> {\n const entries = (h as unknown as { entries(): IterableIterator<[string, string]> }).entries();\n return Object.fromEntries(Array.from(entries));\n}\n\n// ─── Shared singleton instances per config (keyed by apiKey) ─────────────────\n\nconst instances = new Map<string, { client: ApptvtyClient; logger: RequestLogger }>();\n\nfunction getInstance(config: ApptvtyConfig) {\n const key = config.apiKey;\n if (!instances.has(key)) {\n const client = new ApptvtyClient(config);\n const logger = new RequestLogger(client, config);\n instances.set(key, { client, logger });\n }\n return instances.get(key)!;\n}\n\n// ─── Middleware wrapper ───────────────────────────────────────────────────────\n\ntype NextMiddleware = (request: NextRequest) => Response | NextResponse | Promise<Response | NextResponse>;\n\n/**\n * Wraps a Next.js middleware function (or creates a passthrough) with\n * Apptvty request logging and multi-layer ad injection for AI/scraper traffic.\n *\n * @example\n * // middleware.ts\n * import { withApptvty } from 'apptvty/nextjs';\n * export default withApptvty({ apiKey: 'ak_...', siteId: 'site_...' });\n */\nexport function withApptvty(\n config: ApptvtyConfig,\n next?: NextMiddleware,\n): NextMiddleware {\n const { client, logger } = getInstance(config);\n const queryPath = config.queryPath ?? '/query';\n\n return async function apptvtyMiddleware(request: NextRequest): Promise<NextResponse> {\n const startMs = Date.now();\n const userAgent = request.headers.get('user-agent') ?? '';\n const crawlerInfo = detectCrawler(userAgent);\n const scraperService = detectScraperService(userAgent);\n const aiCrawlerParam = parseBoolParam(request.nextUrl.searchParams.get('ai_crawler'), false);\n const isCrawler = crawlerInfo.isAi || aiCrawlerParam || scraperService.isScraperService;\n\n // Run the user's middleware (or passthrough)\n let response: Response | NextResponse;\n try {\n response = next ? await next(request) : NextResponse.next();\n } catch (err) {\n throw err;\n }\n\n const responseTimeMs = Date.now() - startMs;\n const { pathname } = request.nextUrl;\n\n if (shouldSkip(pathname)) {\n return response as NextResponse;\n }\n\n const headers = headersToRecord(request.headers);\n const entry: RequestLogEntry = {\n site_id: config.siteId,\n timestamp: new Date().toISOString(),\n method: request.method,\n path: pathname,\n status_code: response.status,\n response_time_ms: responseTimeMs,\n ip_address: getClientIp(headers),\n user_agent: userAgent,\n referer: request.headers.get('referer'),\n is_ai_crawler: crawlerInfo.isAi,\n crawler_type: crawlerInfo.name,\n crawler_organization: crawlerInfo.organization,\n confidence_score: crawlerInfo.confidence,\n scraper_service: scraperService.name,\n };\n\n logger.enqueue(entry);\n\n // Ad injection for all AI crawler and scraper service traffic on HTML pages\n if (isCrawler && response.ok && !pathname.startsWith(queryPath)) {\n const contentType = response.headers.get('content-type') ?? '';\n if (contentType.includes('text/html')) {\n try {\n const modified = await injectAdsIntoResponse(\n response,\n client,\n config,\n pathname,\n userAgent,\n getClientIp(headers),\n scraperService.isScraperService,\n );\n if (modified) return modified;\n } catch (err) {\n if (config.debug) console.warn('[apptvty] Ad injection failed:', err);\n }\n }\n }\n\n return response as NextResponse;\n };\n}\n\n// ─── Query route handler ──────────────────────────────────────────────────────\n\n/**\n * Creates a Next.js App Router GET handler for the AEO query endpoint.\n *\n * @example\n * // app/query/route.ts\n * import { createNextjsQueryHandler } from 'apptvty/nextjs';\n * export const GET = createNextjsQueryHandler({ apiKey: 'ak_...', siteId: 'site_...' });\n */\nexport function createNextjsQueryHandler(config: ApptvtyConfig) {\n const { client } = getInstance(config);\n const handleQuery = createQueryHandler(client, config);\n\n return async function GET(request: NextRequest): Promise<NextResponse> {\n const { searchParams } = request.nextUrl;\n const q = searchParams.get('q');\n const lang = searchParams.get('lang');\n const surfaceAds = parseBoolParam(searchParams.get('surface_ads'), true);\n const aiCrawler = parseBoolParam(searchParams.get('ai_crawler'), false);\n const userAgent = request.headers.get('user-agent') ?? '';\n const headers = headersToRecord(request.headers);\n\n const result = await handleQuery({\n query: q,\n lang,\n surface_ads: surfaceAds,\n ai_crawler: aiCrawler,\n userAgent,\n ipAddress: getClientIp(headers),\n requestUrl: request.url,\n });\n\n return NextResponse.json(result.body, {\n status: result.status,\n headers: result.headers,\n });\n };\n}\n\n/**\n * Next.js route handler for the embedded analytics dashboard.\n *\n * Mount this in app/api/apptvty/logs/route.ts (or your preferred path).\n *\n * @example\n * // app/api/apptvty/logs/route.ts\n * import { createNextjsDashboardHandler } from 'apptvty/nextjs';\n * const config = { apiKey: 'ak_...', siteId: 'site_...' };\n * export const GET = createNextjsDashboardHandler(config);\n */\nexport function createNextjsDashboardHandler(config: ApptvtyConfig) {\n const { client } = getInstance(config);\n const handleDashboard = createDashboardHandler(client, config);\n\n return async function dashboardHandler(request: NextRequest) {\n const result = await handleDashboard({\n path: request.nextUrl.pathname + request.nextUrl.search,\n method: request.method,\n apiKey: config.apiKey,\n siteId: config.siteId,\n });\n\n if (result.headers['Content-Type'] === 'text/html') {\n return new NextResponse(result.body, {\n status: result.status,\n headers: result.headers,\n });\n }\n\n return NextResponse.json(JSON.parse(result.body), {\n status: result.status,\n headers: result.headers,\n });\n };\n}\n\n// ─── Ad injection ─────────────────────────────────────────────────────────────\n\nasync function injectAdsIntoResponse(\n response: Response,\n client: ApptvtyClient,\n config: ApptvtyConfig,\n pathname: string,\n userAgent: string,\n ipAddress: string,\n isScraperService: boolean,\n): Promise<NextResponse | null> {\n const html = await response.text();\n if (!html) return null;\n\n const pageAds = await client.getAdsForPage({ site_id: config.siteId, page_path: pathname });\n if (!pageAds.ads || pageAds.ads.length === 0) return null;\n\n const modified = injectIntoHtml(html, pageAds.ads, isScraperService);\n if (modified === html) return null; // nothing was injected\n\n const newHeaders = new Headers(response.headers);\n\n // Layer 3: HTTP response header — readable by any HTTP client inspecting headers\n newHeaders.set('X-Sponsored-Content', buildSponsoredHeader(pageAds.ads));\n\n // Log impressions fire-and-forget — triggers billing\n const timestamp = new Date().toISOString();\n for (const ad of pageAds.ads) {\n client\n .logImpression({\n impression_id: ad.impression_id,\n site_id: config.siteId,\n page_path: pathname,\n agent_ua: userAgent,\n agent_ip: ipAddress,\n timestamp,\n })\n .catch(() => {});\n }\n\n return new NextResponse(modified, {\n status: response.status,\n statusText: response.statusText,\n headers: newHeaders,\n });\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction parseBoolParam(value: string | null, defaultValue: boolean): boolean {\n if (value === null) return defaultValue;\n return value === '1' || value === 'true' || value === 'yes';\n}\n\nfunction shouldSkip(pathname: string): boolean {\n return (\n pathname.startsWith('/_next/') ||\n pathname.startsWith('/api/_') ||\n pathname === '/favicon.ico' ||\n /\\.(svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|css|js\\.map)$/.test(pathname)\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/middleware/nextjs.ts"],"sourcesContent":["/**\n * Next.js App Router integration for the Apptvty SDK.\n *\n * Usage — two pieces:\n *\n * 1. Wrap your existing middleware.ts (or use ours standalone):\n *\n * import { withApptvty } from 'apptvty/nextjs';\n * export default withApptvty({ apiKey: 'ak_...', siteId: 'site_...' });\n * // or wrap your existing middleware:\n * export default withApptvty(config, yourMiddleware);\n *\n * 2. Create app/query/route.ts for the AEO query endpoint:\n *\n * import { createNextjsQueryHandler } from 'apptvty/nextjs';\n * export const GET = createNextjsQueryHandler({ apiKey: 'ak_...', siteId: 'site_...' });\n *\n * Ad injection strategy (three layers, applied for all AI/scraper traffic):\n *\n * Layer 1 — <p>[Sponsored]</p> inside <article>/<main>\n * Survives Jina, FireCrawl, Cloudflare Browser Rendering, BeautifulSoup,\n * headless browsers, curl, requests. Applied for all crawler types.\n *\n * Layer 2 — JSON-LD <script> in <head>\n * For direct HTML scrapers (BeautifulSoup, lxml). Skipped for known\n * scraper services (Jina/FireCrawl/Cloudflare) — they drop <head> on\n * Markdown conversion.\n *\n * Layer 3 — X-Sponsored-Content response header\n * For any HTTP client that reads response headers (requests, httpx, curl).\n * Applied for all crawler types.\n */\n\nimport type { NextRequest } from 'next/server';\nimport { NextResponse } from 'next/server';\nimport { ApptvtyClient } from '../client.js';\nimport { detectCrawler, detectScraperService } from '../crawler.js';\nimport { RequestLogger, getClientIp } from '../logger.js';\nimport { createQueryHandler } from '../query-handler.js';\nimport { createDashboardHandler } from '../dashboard-handler.js';\nimport { injectIntoHtml, buildSponsoredHeader } from '../ad-injection.js';\nimport type { ApptvtyConfig, RequestLogEntry } from '../types.js';\n\n/** Convert Next request headers to a plain object (Web API Headers.entries() at runtime). */\nfunction headersToRecord(h: NextRequest['headers']): Record<string, string> {\n const entries = (h as unknown as { entries(): IterableIterator<[string, string]> }).entries();\n return Object.fromEntries(Array.from(entries));\n}\n\n// ─── Shared singleton instances per config (keyed by apiKey) ─────────────────\n\nconst instances = new Map<string, { client: ApptvtyClient; logger: RequestLogger }>();\n\nfunction getInstance(config: ApptvtyConfig) {\n const key = config.apiKey;\n if (!instances.has(key)) {\n const client = new ApptvtyClient(config);\n const logger = new RequestLogger(client, config);\n instances.set(key, { client, logger });\n }\n return instances.get(key)!;\n}\n\n// ─── Middleware wrapper ───────────────────────────────────────────────────────\n\ntype NextMiddleware = (request: NextRequest) => Response | NextResponse | Promise<Response | NextResponse>;\n\n/**\n * Wraps a Next.js middleware function (or creates a passthrough) with\n * Apptvty request logging and multi-layer ad injection for AI/scraper traffic.\n *\n * @example\n * // middleware.ts\n * import { withApptvty } from 'apptvty/nextjs';\n * export default withApptvty({ apiKey: 'ak_...', siteId: 'site_...' });\n */\nexport function withApptvty(\n config: ApptvtyConfig,\n next?: NextMiddleware,\n): NextMiddleware {\n const { client, logger } = getInstance(config);\n const queryPath = config.queryPath ?? '/query';\n\n return async function apptvtyMiddleware(request: NextRequest): Promise<NextResponse> {\n const startMs = Date.now();\n const userAgent = request.headers.get('user-agent') ?? '';\n const crawlerInfo = detectCrawler(userAgent);\n const scraperService = detectScraperService(userAgent);\n const aiCrawlerParam = parseBoolParam(request.nextUrl.searchParams.get('ai_crawler'), false);\n const isCrawler = crawlerInfo.isAi || aiCrawlerParam || scraperService.isScraperService;\n\n // Run the user's middleware (or passthrough)\n let response: Response | NextResponse;\n try {\n response = next ? await next(request) : NextResponse.next();\n } catch (err) {\n throw err;\n }\n\n const responseTimeMs = Date.now() - startMs;\n const { pathname } = request.nextUrl;\n\n if (shouldSkip(pathname)) {\n return response as NextResponse;\n }\n\n const headers = headersToRecord(request.headers);\n const entry: RequestLogEntry = {\n site_id: config.siteId,\n timestamp: new Date().toISOString(),\n method: request.method,\n path: pathname,\n status_code: response.status,\n response_time_ms: responseTimeMs,\n ip_address: getClientIp(headers),\n user_agent: userAgent,\n referer: request.headers.get('referer'),\n is_ai_crawler: crawlerInfo.isAi,\n crawler_type: crawlerInfo.name,\n crawler_organization: crawlerInfo.organization,\n confidence_score: crawlerInfo.confidence,\n scraper_service: scraperService.name,\n };\n\n logger.enqueue(entry);\n\n // Ad injection for all AI crawler and scraper service traffic on HTML pages\n if (isCrawler && response.ok && !pathname.startsWith(queryPath)) {\n const contentType = response.headers.get('content-type') ?? '';\n if (contentType.includes('text/html')) {\n try {\n const modified = await injectAdsIntoResponse(\n response,\n client,\n config,\n pathname,\n userAgent,\n getClientIp(headers),\n scraperService.isScraperService,\n );\n if (modified) return modified;\n } catch (err) {\n if (config.debug) console.warn('[apptvty] Ad injection failed:', err);\n }\n }\n }\n\n return response as NextResponse;\n };\n}\n\n// ─── Query route handler ──────────────────────────────────────────────────────\n\n/**\n * Creates a Next.js App Router GET handler for the AEO query endpoint.\n *\n * @example\n * // app/query/route.ts\n * import { createNextjsQueryHandler } from 'apptvty/nextjs';\n * export const GET = createNextjsQueryHandler({ apiKey: 'ak_...', siteId: 'site_...' });\n */\nexport function createNextjsQueryHandler(config: ApptvtyConfig) {\n const { client } = getInstance(config);\n const handleQuery = createQueryHandler(client, config);\n\n return async function GET(request: NextRequest): Promise<NextResponse> {\n const { searchParams } = request.nextUrl;\n const q = searchParams.get('q');\n const lang = searchParams.get('lang');\n const surfaceAds = parseBoolParam(searchParams.get('surface_ads'), true);\n const aiCrawler = parseBoolParam(searchParams.get('ai_crawler'), false);\n const userAgent = request.headers.get('user-agent') ?? '';\n const headers = headersToRecord(request.headers);\n\n const result = await handleQuery({\n query: q,\n lang,\n surface_ads: surfaceAds,\n ai_crawler: aiCrawler,\n userAgent,\n ipAddress: getClientIp(headers),\n requestUrl: request.url,\n });\n\n return NextResponse.json(result.body, {\n status: result.status,\n headers: result.headers,\n });\n };\n}\n\n/**\n * Next.js route handler for the embedded analytics dashboard.\n *\n * Mount this in app/api/apptvty/logs/route.ts (or your preferred path).\n *\n * @example\n * // app/api/apptvty/logs/route.ts\n * import { createNextjsDashboardHandler } from 'apptvty/nextjs';\n * const config = { apiKey: 'ak_...', siteId: 'site_...' };\n * export const GET = createNextjsDashboardHandler(config);\n */\nexport function createNextjsDashboardHandler(config: ApptvtyConfig) {\n const { client } = getInstance(config);\n const handleDashboard = createDashboardHandler(client, config);\n\n return async function dashboardHandler(request: NextRequest) {\n const result = await handleDashboard({\n path: request.nextUrl.pathname + request.nextUrl.search,\n method: request.method,\n apiKey: config.apiKey,\n siteId: config.siteId,\n authHeader: request.headers.get('Authorization'),\n });\n\n if (result.headers['Content-Type'] === 'text/html') {\n return new NextResponse(result.body, {\n status: result.status,\n headers: result.headers,\n });\n }\n\n return NextResponse.json(JSON.parse(result.body), {\n status: result.status,\n headers: result.headers,\n });\n };\n}\n\n// ─── Ad injection ─────────────────────────────────────────────────────────────\n\nasync function injectAdsIntoResponse(\n response: Response,\n client: ApptvtyClient,\n config: ApptvtyConfig,\n pathname: string,\n userAgent: string,\n ipAddress: string,\n isScraperService: boolean,\n): Promise<NextResponse | null> {\n const html = await response.text();\n if (!html) return null;\n\n const pageAds = await client.getAdsForPage({ site_id: config.siteId, page_path: pathname });\n if (!pageAds.ads || pageAds.ads.length === 0) return null;\n\n const modified = injectIntoHtml(html, pageAds.ads, isScraperService);\n if (modified === html) return null; // nothing was injected\n\n const newHeaders = new Headers(response.headers);\n\n // Layer 3: HTTP response header — readable by any HTTP client inspecting headers\n newHeaders.set('X-Sponsored-Content', buildSponsoredHeader(pageAds.ads));\n\n // Log impressions fire-and-forget — triggers billing\n const timestamp = new Date().toISOString();\n for (const ad of pageAds.ads) {\n client\n .logImpression({\n impression_id: ad.impression_id,\n site_id: config.siteId,\n page_path: pathname,\n agent_ua: userAgent,\n agent_ip: ipAddress,\n timestamp,\n })\n .catch(() => {});\n }\n\n return new NextResponse(modified, {\n status: response.status,\n statusText: response.statusText,\n headers: newHeaders,\n });\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction parseBoolParam(value: string | null, defaultValue: boolean): boolean {\n if (value === null) return defaultValue;\n return value === '1' || value === 'true' || value === 'yes';\n}\n\nfunction shouldSkip(pathname: string): boolean {\n return (\n pathname.startsWith('/_next/') ||\n pathname.startsWith('/api/_') ||\n pathname === '/favicon.ico' ||\n /\\.(svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|css|js\\.map)$/.test(pathname)\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
detectScraperService,
|
|
10
10
|
getClientIp,
|
|
11
11
|
injectIntoHtml
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-GMQN6656.mjs";
|
|
13
13
|
|
|
14
14
|
// src/middleware/express.ts
|
|
15
15
|
var instances = /* @__PURE__ */ new Map();
|
|
@@ -152,7 +152,8 @@ function createExpressDashboardHandler(config) {
|
|
|
152
152
|
path: url.pathname + url.search,
|
|
153
153
|
method: req.method ?? "GET",
|
|
154
154
|
apiKey: config.apiKey,
|
|
155
|
-
siteId: config.siteId
|
|
155
|
+
siteId: config.siteId,
|
|
156
|
+
authHeader: req.headers["authorization"] ?? null
|
|
156
157
|
});
|
|
157
158
|
for (const [key, value] of Object.entries(result.headers)) {
|
|
158
159
|
res.setHeader(key, value);
|
|
@@ -174,4 +175,4 @@ export {
|
|
|
174
175
|
createExpressQueryHandler,
|
|
175
176
|
createExpressDashboardHandler
|
|
176
177
|
};
|
|
177
|
-
//# sourceMappingURL=chunk-
|
|
178
|
+
//# sourceMappingURL=chunk-OZT7PIDN.mjs.map
|