apptvty 0.1.2

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.
@@ -0,0 +1,658 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/middleware/nextjs.ts
21
+ var nextjs_exports = {};
22
+ __export(nextjs_exports, {
23
+ createNextjsQueryHandler: () => createNextjsQueryHandler,
24
+ withApptvty: () => withApptvty
25
+ });
26
+ module.exports = __toCommonJS(nextjs_exports);
27
+ var import_server = require("next/server");
28
+
29
+ // src/client.ts
30
+ var DEFAULT_BASE_URL = "https://api.apptvty.com";
31
+ function resolveBaseUrl(config) {
32
+ const raw = config.baseUrl ?? (typeof process !== "undefined" ? process.env?.APPTVTY_API_URL : void 0) ?? DEFAULT_BASE_URL;
33
+ return raw.replace(/\/$/, "");
34
+ }
35
+ var ApptvtyClient = class {
36
+ constructor(config) {
37
+ this.baseUrl = resolveBaseUrl(config);
38
+ this.apiKey = config.apiKey;
39
+ this.siteId = config.siteId;
40
+ this.debug = config.debug ?? false;
41
+ }
42
+ /**
43
+ * Send a batch of request log entries to the Apptvty ingestion API.
44
+ * Called by the logger's auto-flush — not called directly by user code.
45
+ */
46
+ async sendLogs(logs) {
47
+ if (logs.length === 0) return;
48
+ try {
49
+ await this.post("/v1/logs/batch", { logs });
50
+ this.log(`Flushed ${logs.length} log(s)`);
51
+ } catch (err) {
52
+ if (err instanceof ApptvtyTrialExpiredError) {
53
+ console.warn(
54
+ `
55
+ [apptvty] \u26A0 FREE TRIAL EXPIRED
56
+ [apptvty] Log in and upgrade to keep receiving agent analytics.
57
+ [apptvty] Dashboard: ${err.dashboardUrl}
58
+ `
59
+ );
60
+ return;
61
+ }
62
+ this.warn("Failed to send logs:", err);
63
+ }
64
+ }
65
+ /**
66
+ * Send a query to the Apptvty backend.
67
+ * The backend runs RAG against the site's indexed content and,
68
+ * if ads are enabled for the site, attaches a relevant sponsored ad.
69
+ *
70
+ * @throws on non-retryable errors (network errors, 5xx) — callers should catch
71
+ */
72
+ async query(req) {
73
+ const response = await this.post("/v1/query", req);
74
+ return response;
75
+ }
76
+ /**
77
+ * Get relevant ads for a page (for HTML injection when crawler is detected).
78
+ * Used by middleware to inject ads into HTML responses.
79
+ */
80
+ async getAdsForPage(req) {
81
+ const response = await this.post("/v1/ads/for-page", req);
82
+ return response;
83
+ }
84
+ /**
85
+ * Log an ad impression back to Apptvty.
86
+ *
87
+ * Called after returning a query response that contains a `sponsored` block.
88
+ * This is what triggers the billing cycle:
89
+ * - Advertiser gets charged (debited from their USDC ad budget)
90
+ * - Publisher (the website) gets credited in USDC
91
+ *
92
+ * This call is fire-and-forget. The SDK logs a warning on failure
93
+ * but does not throw — a missed impression log is better than
94
+ * breaking the query response.
95
+ */
96
+ async logImpression(impression) {
97
+ try {
98
+ await this.post("/v1/impressions", impression);
99
+ this.log(`Impression logged: ${impression.impression_id}`);
100
+ } catch (err) {
101
+ this.warn("Failed to log impression (billing may be delayed):", err);
102
+ }
103
+ }
104
+ // ─── Analytics (for coding agents) ───────────────────────────────────────────
105
+ // These allow agents to check activity, logs, and errors without a human.
106
+ /** Get 30-day traffic overview (requests, AI %, crawlers, queries). */
107
+ async getSiteStats() {
108
+ return this.get(`/v1/sites/${this.siteId}/stats`);
109
+ }
110
+ /** Get day-by-day stats (default 30 days, max 90). */
111
+ async getSiteDailyStats(days = 30) {
112
+ return this.get(`/v1/sites/${this.siteId}/stats/daily`, { days: String(days) });
113
+ }
114
+ /** Get crawler breakdown by type (default 30 days). */
115
+ async getSiteCrawlers(days = 30) {
116
+ return this.get(`/v1/sites/${this.siteId}/crawlers`, { days: String(days) });
117
+ }
118
+ /** Get recent activity feed (last hour, default 50 items, max 200). */
119
+ async getSiteActivity(limit = 50) {
120
+ return this.get(`/v1/sites/${this.siteId}/activity`, { limit: String(limit) });
121
+ }
122
+ /** Get recent agent queries (default 50, max 200). */
123
+ async getSiteQueries(limit = 50) {
124
+ return this.get(`/v1/sites/${this.siteId}/queries`, { limit: String(limit) });
125
+ }
126
+ /** Get wallet balance and earnings. */
127
+ async getSiteWallet() {
128
+ return this.get(`/v1/sites/${this.siteId}/wallet`);
129
+ }
130
+ async get(path, params) {
131
+ const url = new URL(`${this.baseUrl}${path}`);
132
+ if (params) {
133
+ Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
134
+ }
135
+ const response = await fetch(url.toString(), {
136
+ method: "GET",
137
+ headers: {
138
+ Authorization: `Bearer ${this.apiKey}`,
139
+ "User-Agent": "apptvty-sdk/0.1.0"
140
+ },
141
+ signal: AbortSignal.timeout(1e4)
142
+ });
143
+ if (!response.ok) {
144
+ const text = await response.text().catch(() => "");
145
+ throw new ApptvtyApiError(response.status, path, text);
146
+ }
147
+ return response.json();
148
+ }
149
+ async post(path, body) {
150
+ const url = `${this.baseUrl}${path}`;
151
+ const response = await fetch(url, {
152
+ method: "POST",
153
+ headers: {
154
+ "Authorization": `Bearer ${this.apiKey}`,
155
+ "Content-Type": "application/json",
156
+ "User-Agent": "apptvty-sdk/0.1.0"
157
+ },
158
+ body: JSON.stringify(body),
159
+ // Node 18+ fetch: set a reasonable timeout via AbortSignal
160
+ signal: AbortSignal.timeout(1e4)
161
+ });
162
+ if (!response.ok) {
163
+ const text = await response.text().catch(() => "");
164
+ if (response.status === 402) {
165
+ let dashboardUrl = "https://dashboard.apptvty.com/login";
166
+ try {
167
+ const json = JSON.parse(text);
168
+ dashboardUrl = json?.error?.details?.dashboard_url ?? dashboardUrl;
169
+ } catch {
170
+ }
171
+ throw new ApptvtyTrialExpiredError(dashboardUrl);
172
+ }
173
+ throw new ApptvtyApiError(response.status, path, text);
174
+ }
175
+ return response.json();
176
+ }
177
+ log(...args) {
178
+ if (this.debug) console.log("[apptvty]", ...args);
179
+ }
180
+ warn(...args) {
181
+ if (this.debug) console.warn("[apptvty]", ...args);
182
+ }
183
+ };
184
+ var ApptvtyApiError = class extends Error {
185
+ constructor(statusCode, path, body) {
186
+ super(`Apptvty API error ${statusCode} at ${path}: ${body}`);
187
+ this.statusCode = statusCode;
188
+ this.path = path;
189
+ this.body = body;
190
+ this.name = "ApptvtyApiError";
191
+ }
192
+ };
193
+ var ApptvtyTrialExpiredError = class extends Error {
194
+ constructor(dashboardUrl) {
195
+ super(`Apptvty free trial has expired. Log in to continue: ${dashboardUrl}`);
196
+ this.dashboardUrl = dashboardUrl;
197
+ this.name = "ApptvtyTrialExpiredError";
198
+ }
199
+ };
200
+
201
+ // src/crawler.ts
202
+ var KNOWN_CRAWLERS = [
203
+ // OpenAI
204
+ { name: "GPTBot", organization: "OpenAI", patterns: [/GPTBot/i, /ChatGPT-User/i] },
205
+ { name: "OpenAI-SearchBot", organization: "OpenAI", patterns: [/OpenAI-SearchBot/i] },
206
+ // Anthropic
207
+ { name: "ClaudeBot", organization: "Anthropic", patterns: [/ClaudeBot/i, /Claude-Web/i, /Anthropic-AI/i] },
208
+ // Google
209
+ { name: "Google-Extended", organization: "Google AI", patterns: [/Google-Extended/i] },
210
+ { name: "GoogleOther", organization: "Google AI", patterns: [/GoogleOther/i] },
211
+ { name: "Googlebot", organization: "Google", patterns: [/Googlebot/i] },
212
+ // Microsoft
213
+ { name: "Bingbot", organization: "Microsoft", patterns: [/bingbot/i, /BingPreview/i] },
214
+ // Perplexity
215
+ { name: "PerplexityBot", organization: "Perplexity", patterns: [/PerplexityBot/i] },
216
+ // You.com
217
+ { name: "YouBot", organization: "You.com", patterns: [/YouBot/i] },
218
+ // Meta
219
+ { name: "Meta-ExternalAgent", organization: "Meta", patterns: [/Meta-ExternalAgent/i] },
220
+ { name: "FacebookBot", organization: "Meta", patterns: [/facebookexternalhit/i, /FacebookBot/i] },
221
+ // Apple
222
+ { name: "AppleBot", organization: "Apple", patterns: [/Applebot/i] },
223
+ // Twitter/X
224
+ { name: "TwitterBot", organization: "Twitter/X", patterns: [/Twitterbot/i] },
225
+ // LinkedIn
226
+ { name: "LinkedInBot", organization: "LinkedIn", patterns: [/LinkedInBot/i] },
227
+ // DuckDuckGo
228
+ { name: "DuckDuckBot", organization: "DuckDuckGo", patterns: [/DuckDuckBot/i] },
229
+ // Cohere
230
+ { name: "Cohere-AI", organization: "Cohere", patterns: [/Cohere-AI/i, /cohere-ai/i] },
231
+ // Allen Institute
232
+ { name: "AI2Bot", organization: "Allen Institute for AI", patterns: [/AI2Bot/i] },
233
+ // Mistral
234
+ { name: "MistralBot", organization: "Mistral AI", patterns: [/MistralBot/i] }
235
+ ];
236
+ var AI_PATTERN_MATCHES = [
237
+ [/openai/i, 0.9],
238
+ [/anthropic/i, 0.9],
239
+ [/gpt|chatgpt/i, 0.85],
240
+ [/claude/i, 0.85],
241
+ [/perplexity/i, 0.85],
242
+ [/llm|language[- ]model/i, 0.75],
243
+ [/bot.*ai|ai.*bot/i, 0.75],
244
+ [/search.*ai|ai.*search/i, 0.7]
245
+ ];
246
+ var HUMAN_SIGNALS = [
247
+ /Mozilla\/5\.0.*\(Windows NT.*\) AppleWebKit.*Chrome.*Safari/i,
248
+ /Mozilla\/5\.0.*\(Macintosh.*\) AppleWebKit.*Version.*Safari/i
249
+ ];
250
+ function detectCrawler(userAgent) {
251
+ if (!userAgent || userAgent.length < 4) {
252
+ return { isAi: false, name: null, organization: null, confidence: 0.3, detectionMethod: "heuristic" };
253
+ }
254
+ for (const crawler of KNOWN_CRAWLERS) {
255
+ for (const pattern of crawler.patterns) {
256
+ if (pattern.test(userAgent)) {
257
+ return {
258
+ isAi: true,
259
+ name: crawler.name,
260
+ organization: crawler.organization,
261
+ confidence: 0.95,
262
+ detectionMethod: "exact_match"
263
+ };
264
+ }
265
+ }
266
+ }
267
+ for (const pattern of HUMAN_SIGNALS) {
268
+ if (pattern.test(userAgent)) {
269
+ return { isAi: false, name: null, organization: null, confidence: 0.85, detectionMethod: "heuristic" };
270
+ }
271
+ }
272
+ for (const [pattern, confidence] of AI_PATTERN_MATCHES) {
273
+ if (pattern.test(userAgent)) {
274
+ return {
275
+ isAi: true,
276
+ name: extractBotName(userAgent),
277
+ organization: null,
278
+ confidence,
279
+ detectionMethod: "pattern_match"
280
+ };
281
+ }
282
+ }
283
+ let score = 0;
284
+ if (/bot|crawler|spider|scraper/i.test(userAgent)) score += 0.4;
285
+ if (/python-requests|curl\/|wget\/|scrapy|go-http-client/i.test(userAgent)) score += 0.3;
286
+ if (!userAgent.includes("Mozilla")) score += 0.2;
287
+ if (userAgent.length < 20) score += 0.2;
288
+ if (score >= 0.5) {
289
+ return {
290
+ isAi: false,
291
+ // Generic bot — not classified as AI
292
+ name: "unknown_bot",
293
+ organization: null,
294
+ confidence: Math.min(score, 0.8),
295
+ detectionMethod: "heuristic"
296
+ };
297
+ }
298
+ return { isAi: false, name: null, organization: null, confidence: 0.1, detectionMethod: "none" };
299
+ }
300
+ function extractBotName(userAgent) {
301
+ const parts = userAgent.split(/[\s/;(]+/);
302
+ for (const part of parts) {
303
+ if (/bot|agent|crawler|ai/i.test(part) && part.length > 2) {
304
+ return part.replace(/[^a-zA-Z0-9-_]/g, "");
305
+ }
306
+ }
307
+ return "unknown_ai_bot";
308
+ }
309
+
310
+ // src/logger.ts
311
+ var RequestLogger = class {
312
+ constructor(client, config) {
313
+ this.client = client;
314
+ this.queue = [];
315
+ this.timer = null;
316
+ this.flushing = false;
317
+ this.batchSize = config.batchSize ?? 50;
318
+ this.debug = config.debug ?? false;
319
+ const interval = config.flushInterval ?? 5e3;
320
+ this.timer = setInterval(() => {
321
+ void this.flush();
322
+ }, interval);
323
+ if (this.timer.unref) this.timer.unref();
324
+ const handleExit = () => {
325
+ void this.flushSync();
326
+ };
327
+ process.once("SIGTERM", handleExit);
328
+ process.once("SIGINT", handleExit);
329
+ process.once("beforeExit", handleExit);
330
+ }
331
+ /** Enqueue a single log entry. Non-blocking. */
332
+ enqueue(entry) {
333
+ this.queue.push(entry);
334
+ if (this.queue.length >= this.batchSize) {
335
+ void this.flush();
336
+ }
337
+ }
338
+ /** Flush the current queue to the API. */
339
+ async flush() {
340
+ if (this.flushing || this.queue.length === 0) return;
341
+ this.flushing = true;
342
+ const batch = this.queue.splice(0, this.batchSize);
343
+ try {
344
+ await this.client.sendLogs(batch);
345
+ } catch {
346
+ } finally {
347
+ this.flushing = false;
348
+ }
349
+ }
350
+ /**
351
+ * Synchronous-ish flush for process shutdown.
352
+ * Fires the fetch and doesn't await to avoid blocking exit handlers.
353
+ */
354
+ flushSync() {
355
+ if (this.queue.length === 0) return;
356
+ const batch = this.queue.splice(0);
357
+ void this.client.sendLogs(batch);
358
+ }
359
+ /** Stop the interval timer. Call when you want to fully tear down the SDK. */
360
+ destroy() {
361
+ if (this.timer) {
362
+ clearInterval(this.timer);
363
+ this.timer = null;
364
+ }
365
+ void this.flush();
366
+ }
367
+ log(...args) {
368
+ if (this.debug) console.log("[apptvty:logger]", ...args);
369
+ }
370
+ };
371
+ function getClientIp(headers) {
372
+ const forwarded = headers["x-forwarded-for"];
373
+ if (forwarded) {
374
+ const first = Array.isArray(forwarded) ? forwarded[0] : forwarded;
375
+ return first.split(",")[0].trim();
376
+ }
377
+ return headers["x-real-ip"] ?? "unknown";
378
+ }
379
+
380
+ // src/query-handler.ts
381
+ var RESPONSE_HEADERS = {
382
+ "Content-Type": "application/json",
383
+ "Cache-Control": "no-store",
384
+ "X-Robots-Tag": "noindex"
385
+ // Don't index the query endpoint itself
386
+ };
387
+ function createQueryHandler(client, config) {
388
+ const queryPath = config.queryPath ?? "/query";
389
+ return async function handleQuery(req) {
390
+ if (!req.query || req.query.trim() === "") {
391
+ const origin = getOrigin(req.requestUrl);
392
+ const discovery = {
393
+ version: "1.0",
394
+ endpoint: `${origin}${queryPath}`,
395
+ description: "Query this site's content directly. Submit a question and receive a structured JSON response with an AI-generated answer, source citations, and confidence score. Ideal for AI agents, search engines, and automated systems seeking direct answers.",
396
+ crawl_policy: "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.",
397
+ usage: {
398
+ method: "GET",
399
+ parameters: {
400
+ q: {
401
+ type: "string",
402
+ required: true,
403
+ description: "Your question or search query (max 500 characters)"
404
+ },
405
+ lang: {
406
+ type: "string",
407
+ required: false,
408
+ description: "Preferred response language (ISO 639-1 code, default: en)"
409
+ },
410
+ surface_ads: {
411
+ type: "boolean",
412
+ required: false,
413
+ description: "Include sponsored ads in response (1 = yes, default; 0 = no). When crawling, use ai_crawler=1 to signal you want ads."
414
+ },
415
+ ai_crawler: {
416
+ type: "boolean",
417
+ required: false,
418
+ description: "Set to 1 when you are an AI agent/crawler. Signals the server to surface ads. Use with ?q=... for targeted answers + ads."
419
+ }
420
+ },
421
+ example: `${origin}${queryPath}?q=what+features+do+you+offer&ai_crawler=1`,
422
+ response_format: "application/json"
423
+ },
424
+ capabilities: [
425
+ "Natural language question answering",
426
+ "Source citations with relevance scores",
427
+ "Structured JSON for easy parsing",
428
+ "Supports follow-up context via session continuity",
429
+ "Query-preferred: use ?q= instead of full-site crawl"
430
+ ],
431
+ rate_limit: "100 requests per hour per IP"
432
+ };
433
+ return { status: 200, body: discovery, headers: RESPONSE_HEADERS };
434
+ }
435
+ const trimmedQuery = req.query.trim();
436
+ if (trimmedQuery.length > 500) {
437
+ return errorResponse(400, "QUERY_TOO_LONG", "Query must be 500 characters or fewer");
438
+ }
439
+ const requestId = crypto.randomUUID();
440
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
441
+ const startMs = Date.now();
442
+ const surfaceAds = req.surface_ads !== false;
443
+ const aiCrawler = req.ai_crawler === true;
444
+ let backendResponse;
445
+ try {
446
+ backendResponse = await client.query({
447
+ site_id: config.siteId,
448
+ query: trimmedQuery,
449
+ agent_ua: req.userAgent,
450
+ agent_ip: req.ipAddress,
451
+ request_id: requestId,
452
+ timestamp,
453
+ surface_ads: surfaceAds,
454
+ ai_crawler: aiCrawler
455
+ });
456
+ } catch (err) {
457
+ if (err instanceof ApptvtyTrialExpiredError) {
458
+ return errorResponse(
459
+ 402,
460
+ "TRIAL_EXPIRED",
461
+ `Apptvty free trial has expired. The site owner must log in and upgrade to continue. Dashboard: ${err.dashboardUrl}`
462
+ );
463
+ }
464
+ return errorResponse(502, "UPSTREAM_ERROR", "Could not retrieve an answer at this time");
465
+ }
466
+ const responseTimeMs = Date.now() - startMs;
467
+ const ads = backendResponse.sponsored ? Array.isArray(backendResponse.sponsored) ? backendResponse.sponsored : [backendResponse.sponsored] : [];
468
+ for (const ad of ads) {
469
+ void client.logImpression({
470
+ impression_id: ad.impression_id,
471
+ site_id: config.siteId,
472
+ query: trimmedQuery,
473
+ agent_ua: req.userAgent,
474
+ agent_ip: req.ipAddress,
475
+ timestamp
476
+ });
477
+ }
478
+ const agentResponse = {
479
+ success: true,
480
+ version: "1.0",
481
+ query: trimmedQuery,
482
+ answer: backendResponse.answer,
483
+ sources: backendResponse.sources,
484
+ confidence: backendResponse.confidence,
485
+ ...backendResponse.sponsored && { sponsored: backendResponse.sponsored },
486
+ metadata: {
487
+ request_id: requestId,
488
+ response_time_ms: responseTimeMs,
489
+ tokens_used: backendResponse.tokens_used,
490
+ site_id: config.siteId,
491
+ timestamp
492
+ }
493
+ };
494
+ return { status: 200, body: agentResponse, headers: RESPONSE_HEADERS };
495
+ };
496
+ }
497
+ function errorResponse(status, code, message) {
498
+ const body = {
499
+ success: false,
500
+ error: {
501
+ code,
502
+ message,
503
+ request_id: crypto.randomUUID(),
504
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
505
+ }
506
+ };
507
+ return { status, body, headers: RESPONSE_HEADERS };
508
+ }
509
+ function getOrigin(url) {
510
+ try {
511
+ const parsed = new URL(url);
512
+ return parsed.origin;
513
+ } catch {
514
+ return "";
515
+ }
516
+ }
517
+
518
+ // src/middleware/nextjs.ts
519
+ function headersToRecord(h) {
520
+ const entries = h.entries();
521
+ return Object.fromEntries(Array.from(entries));
522
+ }
523
+ var instances = /* @__PURE__ */ new Map();
524
+ function getInstance(config) {
525
+ const key = config.apiKey;
526
+ if (!instances.has(key)) {
527
+ const client = new ApptvtyClient(config);
528
+ const logger = new RequestLogger(client, config);
529
+ instances.set(key, { client, logger });
530
+ }
531
+ return instances.get(key);
532
+ }
533
+ function withApptvty(config, next) {
534
+ const { client, logger } = getInstance(config);
535
+ const queryPath = config.queryPath ?? "/query";
536
+ return async function apptvtyMiddleware(request) {
537
+ const startMs = Date.now();
538
+ const userAgent = request.headers.get("user-agent") ?? "";
539
+ const crawlerInfo = detectCrawler(userAgent);
540
+ const aiCrawlerParam = parseBoolParam(request.nextUrl.searchParams.get("ai_crawler"), false);
541
+ const isCrawler = crawlerInfo.isAi || aiCrawlerParam;
542
+ let response;
543
+ try {
544
+ response = next ? await next(request) : import_server.NextResponse.next();
545
+ } catch (err) {
546
+ throw err;
547
+ }
548
+ const responseTimeMs = Date.now() - startMs;
549
+ const { pathname } = request.nextUrl;
550
+ if (shouldSkip(pathname)) {
551
+ return response;
552
+ }
553
+ const headers = headersToRecord(request.headers);
554
+ const entry = {
555
+ site_id: config.siteId,
556
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
557
+ method: request.method,
558
+ path: pathname,
559
+ status_code: response.status,
560
+ response_time_ms: responseTimeMs,
561
+ ip_address: getClientIp(headers),
562
+ user_agent: userAgent,
563
+ referer: request.headers.get("referer"),
564
+ is_ai_crawler: crawlerInfo.isAi,
565
+ crawler_type: crawlerInfo.name,
566
+ crawler_organization: crawlerInfo.organization,
567
+ confidence_score: crawlerInfo.confidence
568
+ };
569
+ logger.enqueue(entry);
570
+ if (isCrawler && response.ok && !pathname.startsWith(queryPath)) {
571
+ const contentType = response.headers.get("content-type") ?? "";
572
+ if (contentType.includes("text/html")) {
573
+ try {
574
+ const modified = await injectAdsIntoHtml(response, client, config.siteId, pathname);
575
+ if (modified) return modified;
576
+ } catch (err) {
577
+ if (config.debug) console.warn("[apptvty] Ad injection failed:", err);
578
+ }
579
+ }
580
+ }
581
+ return response;
582
+ };
583
+ }
584
+ function createNextjsQueryHandler(config) {
585
+ const { client } = getInstance(config);
586
+ const handleQuery = createQueryHandler(client, config);
587
+ return async function GET(request) {
588
+ const { searchParams } = request.nextUrl;
589
+ const q = searchParams.get("q");
590
+ const lang = searchParams.get("lang");
591
+ const surfaceAds = parseBoolParam(searchParams.get("surface_ads"), true);
592
+ const aiCrawler = parseBoolParam(searchParams.get("ai_crawler"), false);
593
+ const userAgent = request.headers.get("user-agent") ?? "";
594
+ const headers = headersToRecord(request.headers);
595
+ const result = await handleQuery({
596
+ query: q,
597
+ lang,
598
+ surface_ads: surfaceAds,
599
+ ai_crawler: aiCrawler,
600
+ userAgent,
601
+ ipAddress: getClientIp(headers),
602
+ requestUrl: request.url
603
+ });
604
+ return import_server.NextResponse.json(result.body, {
605
+ status: result.status,
606
+ headers: result.headers
607
+ });
608
+ };
609
+ }
610
+ var AD_INJECTION_MARKER = "<!-- apptvty-sponsored -->";
611
+ function buildAdBlock(ads) {
612
+ const items = ads.map(
613
+ (ad) => `<li><a href="${escapeHtml(ad.url)}" rel="nofollow">${escapeHtml(ad.text)}</a> <small>\u2014 ${escapeHtml(ad.advertiser)}</small></li>`
614
+ ).join("\n");
615
+ return `
616
+ <section aria-label="Sponsored" data-sponsored ${AD_INJECTION_MARKER}>
617
+ <h3>Sponsored</h3>
618
+ <ul>${items}</ul>
619
+ </section>`;
620
+ }
621
+ function escapeHtml(s) {
622
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
623
+ }
624
+ async function injectAdsIntoHtml(response, client, siteId, pathname) {
625
+ const html = await response.text();
626
+ if (!html || html.includes(AD_INJECTION_MARKER)) return null;
627
+ const pageAds = await client.getAdsForPage({ site_id: siteId, page_path: pathname });
628
+ if (!pageAds.ads || pageAds.ads.length === 0) return null;
629
+ const adBlock = buildAdBlock(pageAds.ads);
630
+ let modified;
631
+ if (html.includes("</body>")) {
632
+ modified = html.replace("</body>", `${adBlock}
633
+ </body>`);
634
+ } else if (html.includes("</html>")) {
635
+ modified = html.replace("</html>", `${adBlock}
636
+ </html>`);
637
+ } else {
638
+ modified = html + adBlock;
639
+ }
640
+ return new import_server.NextResponse(modified, {
641
+ status: response.status,
642
+ statusText: response.statusText,
643
+ headers: new Headers(response.headers)
644
+ });
645
+ }
646
+ function parseBoolParam(value, defaultValue) {
647
+ if (value === null) return defaultValue;
648
+ return value === "1" || value === "true" || value === "yes";
649
+ }
650
+ function shouldSkip(pathname) {
651
+ return pathname.startsWith("/_next/") || pathname.startsWith("/api/_") || pathname === "/favicon.ico" || /\.(svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|css|js\.map)$/.test(pathname);
652
+ }
653
+ // Annotate the CommonJS export names for ESM import in node:
654
+ 0 && (module.exports = {
655
+ createNextjsQueryHandler,
656
+ withApptvty
657
+ });
658
+ //# sourceMappingURL=nextjs.js.map