apptvty 0.1.5 → 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.
@@ -0,0 +1,1096 @@
1
+ // src/client.ts
2
+ var DEFAULT_BASE_URL = "https://api.apptvty.com";
3
+ function resolveBaseUrl(config) {
4
+ const raw = config.baseUrl ?? (typeof process !== "undefined" ? process.env?.APPTVTY_API_URL : void 0) ?? DEFAULT_BASE_URL;
5
+ return raw.replace(/\/$/, "");
6
+ }
7
+ var ApptvtyClient = class {
8
+ constructor(config) {
9
+ this.baseUrl = resolveBaseUrl(config);
10
+ this.apiKey = config.apiKey;
11
+ this.siteId = config.siteId;
12
+ this.debug = config.debug ?? false;
13
+ }
14
+ /**
15
+ * Set the X402 (LSAT) token for subsequent requests.
16
+ * This is called after the agent has successfully paid an X402 challenge.
17
+ */
18
+ setX402Token(macaroon, preimage) {
19
+ this.x402Token = `LSAT ${macaroon}:${preimage}`;
20
+ }
21
+ /**
22
+ * Send a batch of request log entries to the Apptvty ingestion API.
23
+ * Called by the logger's auto-flush — not called directly by user code.
24
+ */
25
+ async sendLogs(logs) {
26
+ if (logs.length === 0) return;
27
+ try {
28
+ await this.post("/v1/logs/batch", { logs });
29
+ this.log(`Flushed ${logs.length} log(s)`);
30
+ } catch (err) {
31
+ if (err instanceof ApptvtyTrialExpiredError) {
32
+ console.warn(
33
+ `
34
+ [apptvty] \u26A0 FREE TRIAL EXPIRED
35
+ [apptvty] Log in and upgrade to keep receiving agent analytics.
36
+ [apptvty] Dashboard: ${err.dashboardUrl}
37
+ `
38
+ );
39
+ return;
40
+ }
41
+ this.warn("Failed to send logs:", err);
42
+ }
43
+ }
44
+ /**
45
+ * Send a query to the Apptvty backend.
46
+ * The backend runs RAG against the site's indexed content and,
47
+ * if ads are enabled for the site, attaches a relevant sponsored ad.
48
+ *
49
+ * @throws on non-retryable errors (network errors, 5xx) — callers should catch
50
+ */
51
+ async query(req) {
52
+ const response = await this.post("/v1/query", req);
53
+ return response;
54
+ }
55
+ /**
56
+ * Get relevant ads for a page (for HTML injection when crawler is detected).
57
+ * Used by middleware to inject ads into HTML responses.
58
+ */
59
+ async getAdsForPage(req) {
60
+ const response = await this.post("/v1/ads/for-page", req);
61
+ return response;
62
+ }
63
+ /**
64
+ * Log an ad impression back to Apptvty.
65
+ *
66
+ * Called after returning a query response that contains a `sponsored` block.
67
+ * This is what triggers the billing cycle:
68
+ * - Advertiser gets charged (debited from their USDC ad budget)
69
+ * - Publisher (the website) gets credited in USDC
70
+ *
71
+ * This call is fire-and-forget. The SDK logs a warning on failure
72
+ * but does not throw — a missed impression log is better than
73
+ * breaking the query response.
74
+ */
75
+ async logImpression(impression) {
76
+ try {
77
+ await this.post("/v1/impressions", impression);
78
+ this.log(`Impression logged: ${impression.impression_id}`);
79
+ } catch (err) {
80
+ this.warn("Failed to log impression (billing may be delayed):", err);
81
+ }
82
+ }
83
+ // ─── Analytics (for coding agents) ───────────────────────────────────────────
84
+ // These allow agents to check activity, logs, and errors without a human.
85
+ /** Get 30-day traffic overview (requests, AI %, crawlers, queries). */
86
+ async getSiteStats() {
87
+ return this.get(`/v1/sites/${this.siteId}/stats`);
88
+ }
89
+ /** Get day-by-day stats (default 30 days, max 90). */
90
+ async getSiteDailyStats(days = 30) {
91
+ return this.get(`/v1/sites/${this.siteId}/stats/daily`, { days: String(days) });
92
+ }
93
+ /** Get recent activity logs (last 48h). */
94
+ async getRecentActivity(siteId, limit = 50, offset = 0) {
95
+ return this.get(`/v1/sites/${siteId}/activity`, {
96
+ limit: String(limit),
97
+ offset: String(offset)
98
+ });
99
+ }
100
+ /** Get recent agent queries. */
101
+ async getRecentQueries(siteId, limit = 20, offset = 0) {
102
+ return this.get(`/v1/sites/${siteId}/queries`, {
103
+ limit: String(limit),
104
+ offset: String(offset)
105
+ });
106
+ }
107
+ /** Get crawler breakdown by type (default 30 days). */
108
+ async getSiteCrawlers(days = 30) {
109
+ return this.get(`/v1/sites/${this.siteId}/crawlers`, { days: String(days) });
110
+ }
111
+ /** Get recent activity feed (last hour, default 50 items, max 200). */
112
+ async getSiteActivity(limit = 50) {
113
+ return this.get(`/v1/sites/${this.siteId}/activity`, { limit: String(limit) });
114
+ }
115
+ /** Get recent agent queries (default 50, max 200). */
116
+ async getSiteQueries(limit = 50) {
117
+ return this.get(`/v1/sites/${this.siteId}/queries`, { limit: String(limit) });
118
+ }
119
+ /** Get wallet balance and earnings. */
120
+ async getSiteWallet() {
121
+ return this.get(`/v1/sites/${this.siteId}/wallet`);
122
+ }
123
+ // ─── Campaign management (for coding agents) ────────────────────────────────
124
+ //
125
+ // A coding agent that installed the SDK can create and manage ad campaigns
126
+ // programmatically — no human dashboard login required.
127
+ //
128
+ // Typical agentic advertiser flow:
129
+ // 1. Check wallet balance: const w = await client.getSiteWallet()
130
+ // 2. Fund wallet if needed: send USDC to w.wallet_address on Base chain
131
+ // 3. Create campaign: await client.createCampaign({ ... })
132
+ // 4. Monitor performance: await client.getCampaign(id)
133
+ // 5. Adjust or pause: await client.updateCampaign(id, { status: 'paused' })
134
+ /**
135
+ * Create an ad campaign. The campaign goes live immediately once the wallet
136
+ * has sufficient balance.
137
+ *
138
+ * If the wallet balance is too low, throws `ApptvtyInsufficientBalanceError`
139
+ * which includes deposit instructions so the agent can fund the wallet and retry.
140
+ *
141
+ * @example
142
+ * // Site-based: ad copy derived from your site's crawled content
143
+ * const campaign = await client.createCampaign({
144
+ * name: 'My Kubernetes Blog',
145
+ * advertiser_site_id: 'site_abc123',
146
+ * keywords: ['kubernetes', 'devops', 'containers'],
147
+ * categories: ['technology'],
148
+ * bid_per_view_usdc: 0.001,
149
+ * daily_budget_usdc: 1.0,
150
+ * total_budget_usdc: 20.0,
151
+ * });
152
+ *
153
+ * // Static: manually written ad copy
154
+ * const campaign = await client.createCampaign({
155
+ * name: 'My Blog — Static',
156
+ * ad_text: 'Deep dives on Kubernetes, written by practitioners.',
157
+ * landing_url: 'https://myblog.com',
158
+ * keywords: ['kubernetes', 'devops'],
159
+ * categories: ['technology'],
160
+ * bid_per_view_usdc: 0.001,
161
+ * daily_budget_usdc: 1.0,
162
+ * total_budget_usdc: 10.0,
163
+ * });
164
+ */
165
+ async createCampaign(params) {
166
+ try {
167
+ return await this.post("/v1/campaigns", params);
168
+ } catch (err) {
169
+ if (err instanceof ApptvtyApiError && err.statusCode === 402) {
170
+ let details;
171
+ try {
172
+ const body = JSON.parse(err.body);
173
+ details = body?.error;
174
+ } catch {
175
+ }
176
+ throw new ApptvtyInsufficientBalanceError(details);
177
+ }
178
+ throw err;
179
+ }
180
+ }
181
+ /**
182
+ * List all campaigns for this account.
183
+ * Also returns the schema (valid categories, minimum bid) so the agent
184
+ * knows valid field values without guessing.
185
+ */
186
+ async listCampaigns() {
187
+ return this.get("/v1/campaigns");
188
+ }
189
+ /**
190
+ * Get a single campaign by ID, including current spend and impression count.
191
+ * `budget_remaining_usdc` tells the agent how much budget is left before
192
+ * the campaign auto-pauses.
193
+ */
194
+ async getCampaign(campaignId) {
195
+ return this.get(`/v1/campaigns/${campaignId}`);
196
+ }
197
+ /**
198
+ * Partially update a campaign. Only fields present in `params` are changed.
199
+ *
200
+ * @example
201
+ * // Pause a campaign
202
+ * await client.updateCampaign(id, { status: 'paused' });
203
+ *
204
+ * // Increase bid and daily budget
205
+ * await client.updateCampaign(id, { bid_per_view_usdc: 0.002, daily_budget_usdc: 5.0 });
206
+ */
207
+ async updateCampaign(campaignId, params) {
208
+ return this.patch(`/v1/campaigns/${campaignId}`, params);
209
+ }
210
+ /**
211
+ * Pause a campaign immediately. Equivalent to updateCampaign(id, { status: 'paused' }).
212
+ * Campaigns are never deleted — billing history is retained.
213
+ */
214
+ async pauseCampaign(campaignId) {
215
+ await this.delete(`/v1/campaigns/${campaignId}`);
216
+ }
217
+ /**
218
+ * Resume a paused campaign.
219
+ */
220
+ async resumeCampaign(campaignId) {
221
+ await this.patch(`/v1/campaigns/${campaignId}`, { status: "active" });
222
+ }
223
+ async get(path, params) {
224
+ const url = new URL(`${this.baseUrl}${path}`);
225
+ if (params) {
226
+ Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
227
+ }
228
+ const response = await fetch(url.toString(), {
229
+ method: "GET",
230
+ headers: {
231
+ Authorization: this.x402Token ?? `Bearer ${this.apiKey}`,
232
+ "User-Agent": "apptvty-sdk/0.1.0"
233
+ },
234
+ signal: AbortSignal.timeout(1e4)
235
+ });
236
+ if (!response.ok) {
237
+ const text = await response.text().catch(() => "");
238
+ throw new ApptvtyApiError(response.status, path, text);
239
+ }
240
+ return response.json();
241
+ }
242
+ async patch(path, body) {
243
+ const url = `${this.baseUrl}${path}`;
244
+ const response = await fetch(url, {
245
+ method: "PATCH",
246
+ headers: {
247
+ "Authorization": this.x402Token ?? `Bearer ${this.apiKey}`,
248
+ "Content-Type": "application/json",
249
+ "User-Agent": "apptvty-sdk/0.1.0"
250
+ },
251
+ body: JSON.stringify(body),
252
+ signal: AbortSignal.timeout(1e4)
253
+ });
254
+ if (!response.ok) {
255
+ const text = await response.text().catch(() => "");
256
+ throw new ApptvtyApiError(response.status, path, text);
257
+ }
258
+ return response.json();
259
+ }
260
+ async delete(path) {
261
+ const url = `${this.baseUrl}${path}`;
262
+ const response = await fetch(url, {
263
+ method: "DELETE",
264
+ headers: {
265
+ "Authorization": this.x402Token ?? `Bearer ${this.apiKey}`,
266
+ "User-Agent": "apptvty-sdk/0.1.0"
267
+ },
268
+ signal: AbortSignal.timeout(1e4)
269
+ });
270
+ if (!response.ok) {
271
+ const text = await response.text().catch(() => "");
272
+ throw new ApptvtyApiError(response.status, path, text);
273
+ }
274
+ }
275
+ /**
276
+ * Settle an X402 challenge from the site's own USDC wallet balance.
277
+ * Called automatically when `post()` receives a 402 with an X402 header.
278
+ * Returns the preimage on success, or throws if the wallet balance is too low.
279
+ */
280
+ async payX402Challenge(macaroon) {
281
+ const url = `${this.baseUrl}/v1/x402/pay`;
282
+ const response = await fetch(url, {
283
+ method: "POST",
284
+ headers: {
285
+ Authorization: `Bearer ${this.apiKey}`,
286
+ "Content-Type": "application/json",
287
+ "User-Agent": "apptvty-sdk/0.1.0"
288
+ },
289
+ body: JSON.stringify({ macaroon }),
290
+ signal: AbortSignal.timeout(1e4)
291
+ });
292
+ const text = await response.text().catch(() => "");
293
+ if (!response.ok) {
294
+ let details;
295
+ try {
296
+ details = JSON.parse(text)?.error?.details;
297
+ } catch {
298
+ }
299
+ throw new ApptvtyInsufficientBalanceError(details);
300
+ }
301
+ const json = JSON.parse(text);
302
+ return json.preimage;
303
+ }
304
+ async post(path, body) {
305
+ const url = `${this.baseUrl}${path}`;
306
+ const response = await fetch(url, {
307
+ method: "POST",
308
+ headers: {
309
+ "Authorization": this.x402Token ?? `Bearer ${this.apiKey}`,
310
+ "Content-Type": "application/json",
311
+ "User-Agent": "apptvty-sdk/0.1.0"
312
+ },
313
+ body: JSON.stringify(body),
314
+ signal: AbortSignal.timeout(1e4)
315
+ });
316
+ if (!response.ok) {
317
+ const text = await response.text().catch(() => "");
318
+ if (response.status === 402) {
319
+ const authHeader = response.headers.get("WWW-Authenticate");
320
+ if (authHeader?.includes("X402") || authHeader?.includes("LSAT")) {
321
+ const macaroon = authHeader.match(/macaroon="([^"]+)"/)?.[1] || "";
322
+ const preimage = await this.payX402Challenge(macaroon);
323
+ this.setX402Token(macaroon, preimage);
324
+ this.log("X402 challenge auto-paid from wallet, retrying request");
325
+ return this.post(path, body);
326
+ }
327
+ let dashboardUrl = "https://dashboard.apptvty.com/login";
328
+ try {
329
+ const json = JSON.parse(text);
330
+ dashboardUrl = json?.error?.details?.dashboard_url ?? dashboardUrl;
331
+ } catch {
332
+ }
333
+ throw new ApptvtyTrialExpiredError(dashboardUrl);
334
+ }
335
+ throw new ApptvtyApiError(response.status, path, text);
336
+ }
337
+ return response.json();
338
+ }
339
+ log(...args) {
340
+ if (this.debug) console.log("[apptvty]", ...args);
341
+ }
342
+ warn(...args) {
343
+ if (this.debug) console.warn("[apptvty]", ...args);
344
+ }
345
+ };
346
+ var ApptvtyApiError = class extends Error {
347
+ constructor(statusCode, path, body) {
348
+ super(`Apptvty API error ${statusCode} at ${path}: ${body}`);
349
+ this.statusCode = statusCode;
350
+ this.path = path;
351
+ this.body = body;
352
+ this.name = "ApptvtyApiError";
353
+ }
354
+ };
355
+ var ApptvtyTrialExpiredError = class extends Error {
356
+ constructor(dashboardUrl) {
357
+ super(`Apptvty free trial has expired. Log in to continue: ${dashboardUrl}`);
358
+ this.dashboardUrl = dashboardUrl;
359
+ this.name = "ApptvtyTrialExpiredError";
360
+ }
361
+ };
362
+ var ApptvtyInsufficientBalanceError = class extends Error {
363
+ constructor(details) {
364
+ const shortfall = details?.shortfall_usdc ?? "?";
365
+ super(
366
+ `Wallet balance too low to create campaign. Send ${shortfall} USDC to ${details?.deposit?.address ?? "your wallet"} on Base and retry.`
367
+ );
368
+ this.details = details;
369
+ this.name = "ApptvtyInsufficientBalanceError";
370
+ }
371
+ };
372
+
373
+ // src/logger.ts
374
+ var RequestLogger = class {
375
+ constructor(client, config) {
376
+ this.client = client;
377
+ this.queue = [];
378
+ this.timer = null;
379
+ this.flushing = false;
380
+ this.batchSize = config.batchSize ?? 50;
381
+ this.debug = config.debug ?? false;
382
+ const interval = config.flushInterval ?? 5e3;
383
+ this.timer = setInterval(() => {
384
+ void this.flush();
385
+ }, interval);
386
+ if (this.timer && typeof this.timer.unref === "function") {
387
+ this.timer.unref();
388
+ }
389
+ if (typeof process !== "undefined" && typeof process.once === "function") {
390
+ const handleExit = () => {
391
+ void this.flushSync();
392
+ };
393
+ try {
394
+ process.once("SIGTERM", handleExit);
395
+ process.once("SIGINT", handleExit);
396
+ process.once("beforeExit", handleExit);
397
+ } catch {
398
+ }
399
+ }
400
+ }
401
+ /** Enqueue a single log entry. Non-blocking. */
402
+ enqueue(entry) {
403
+ this.queue.push(entry);
404
+ if (this.queue.length >= this.batchSize) {
405
+ void this.flush();
406
+ }
407
+ }
408
+ /** Flush the current queue to the API. */
409
+ async flush() {
410
+ if (this.flushing || this.queue.length === 0) return;
411
+ this.flushing = true;
412
+ const batch = this.queue.splice(0, this.batchSize);
413
+ try {
414
+ await this.client.sendLogs(batch);
415
+ } catch {
416
+ } finally {
417
+ this.flushing = false;
418
+ }
419
+ }
420
+ /**
421
+ * Synchronous-ish flush for process shutdown.
422
+ * Fires the fetch and doesn't await to avoid blocking exit handlers.
423
+ */
424
+ flushSync() {
425
+ if (this.queue.length === 0) return;
426
+ const batch = this.queue.splice(0);
427
+ void this.client.sendLogs(batch);
428
+ }
429
+ /** Stop the interval timer. Call when you want to fully tear down the SDK. */
430
+ destroy() {
431
+ if (this.timer) {
432
+ clearInterval(this.timer);
433
+ this.timer = null;
434
+ }
435
+ void this.flush();
436
+ }
437
+ log(...args) {
438
+ if (this.debug) console.log("[apptvty:logger]", ...args);
439
+ }
440
+ };
441
+ function getClientIp(headers) {
442
+ const forwarded = headers["x-forwarded-for"];
443
+ if (forwarded) {
444
+ const first = Array.isArray(forwarded) ? forwarded[0] : forwarded;
445
+ return first.split(",")[0].trim();
446
+ }
447
+ return headers["x-real-ip"] ?? "unknown";
448
+ }
449
+
450
+ // src/crawler.ts
451
+ var KNOWN_CRAWLERS = [
452
+ // OpenAI
453
+ { name: "GPTBot", organization: "OpenAI", patterns: [/GPTBot/i, /ChatGPT-User/i] },
454
+ { name: "OpenAI-SearchBot", organization: "OpenAI", patterns: [/OpenAI-SearchBot/i] },
455
+ // Anthropic
456
+ { name: "ClaudeBot", organization: "Anthropic", patterns: [/ClaudeBot/i, /Claude-Web/i, /Anthropic-AI/i] },
457
+ // Google
458
+ { name: "Google-Extended", organization: "Google AI", patterns: [/Google-Extended/i] },
459
+ { name: "GoogleOther", organization: "Google AI", patterns: [/GoogleOther/i] },
460
+ { name: "Googlebot", organization: "Google", patterns: [/Googlebot/i] },
461
+ // Microsoft
462
+ { name: "Bingbot", organization: "Microsoft", patterns: [/bingbot/i, /BingPreview/i] },
463
+ // Perplexity
464
+ { name: "PerplexityBot", organization: "Perplexity", patterns: [/PerplexityBot/i] },
465
+ // You.com
466
+ { name: "YouBot", organization: "You.com", patterns: [/YouBot/i] },
467
+ // Meta
468
+ { name: "Meta-ExternalAgent", organization: "Meta", patterns: [/Meta-ExternalAgent/i] },
469
+ { name: "FacebookBot", organization: "Meta", patterns: [/facebookexternalhit/i, /FacebookBot/i] },
470
+ // Apple
471
+ { name: "AppleBot", organization: "Apple", patterns: [/Applebot/i] },
472
+ // Twitter/X
473
+ { name: "TwitterBot", organization: "Twitter/X", patterns: [/Twitterbot/i] },
474
+ // LinkedIn
475
+ { name: "LinkedInBot", organization: "LinkedIn", patterns: [/LinkedInBot/i] },
476
+ // DuckDuckGo
477
+ { name: "DuckDuckBot", organization: "DuckDuckGo", patterns: [/DuckDuckBot/i] },
478
+ // Cohere
479
+ { name: "Cohere-AI", organization: "Cohere", patterns: [/Cohere-AI/i, /cohere-ai/i] },
480
+ // Allen Institute
481
+ { name: "AI2Bot", organization: "Allen Institute for AI", patterns: [/AI2Bot/i] },
482
+ // Mistral
483
+ { name: "MistralBot", organization: "Mistral AI", patterns: [/MistralBot/i] }
484
+ ];
485
+ var AI_PATTERN_MATCHES = [
486
+ [/openai/i, 0.9],
487
+ [/anthropic/i, 0.9],
488
+ [/gpt|chatgpt/i, 0.85],
489
+ [/claude/i, 0.85],
490
+ [/perplexity/i, 0.85],
491
+ [/llm|language[- ]model/i, 0.75],
492
+ [/bot.*ai|ai.*bot/i, 0.75],
493
+ [/search.*ai|ai.*search/i, 0.7]
494
+ ];
495
+ var HUMAN_SIGNALS = [
496
+ /Mozilla\/5\.0.*\(Windows NT.*\) AppleWebKit.*Chrome.*Safari/i,
497
+ /Mozilla\/5\.0.*\(Macintosh.*\) AppleWebKit.*Version.*Safari/i
498
+ ];
499
+ function detectCrawler(userAgent) {
500
+ if (!userAgent || userAgent.length < 4) {
501
+ return { isAi: false, name: null, organization: null, confidence: 0.3, detectionMethod: "heuristic" };
502
+ }
503
+ for (const crawler of KNOWN_CRAWLERS) {
504
+ for (const pattern of crawler.patterns) {
505
+ if (pattern.test(userAgent)) {
506
+ return {
507
+ isAi: true,
508
+ name: crawler.name,
509
+ organization: crawler.organization,
510
+ confidence: 0.95,
511
+ detectionMethod: "exact_match"
512
+ };
513
+ }
514
+ }
515
+ }
516
+ for (const pattern of HUMAN_SIGNALS) {
517
+ if (pattern.test(userAgent)) {
518
+ return { isAi: false, name: null, organization: null, confidence: 0.85, detectionMethod: "heuristic" };
519
+ }
520
+ }
521
+ for (const [pattern, confidence] of AI_PATTERN_MATCHES) {
522
+ if (pattern.test(userAgent)) {
523
+ return {
524
+ isAi: true,
525
+ name: extractBotName(userAgent),
526
+ organization: null,
527
+ confidence,
528
+ detectionMethod: "pattern_match"
529
+ };
530
+ }
531
+ }
532
+ let score = 0;
533
+ if (/bot|crawler|spider|scraper/i.test(userAgent)) score += 0.4;
534
+ if (/python-requests|curl\/|wget\/|scrapy|go-http-client/i.test(userAgent)) score += 0.3;
535
+ if (!userAgent.includes("Mozilla")) score += 0.2;
536
+ if (userAgent.length < 20) score += 0.2;
537
+ if (score >= 0.5) {
538
+ return {
539
+ isAi: false,
540
+ // Generic bot — not classified as AI
541
+ name: "unknown_bot",
542
+ organization: null,
543
+ confidence: Math.min(score, 0.8),
544
+ detectionMethod: "heuristic"
545
+ };
546
+ }
547
+ return { isAi: false, name: null, organization: null, confidence: 0.1, detectionMethod: "none" };
548
+ }
549
+ function extractBotName(userAgent) {
550
+ const parts = userAgent.split(/[\s/;(]+/);
551
+ for (const part of parts) {
552
+ if (/bot|agent|crawler|ai/i.test(part) && part.length > 2) {
553
+ return part.replace(/[^a-zA-Z0-9-_]/g, "");
554
+ }
555
+ }
556
+ return "unknown_ai_bot";
557
+ }
558
+ function getKnownCrawlerNames() {
559
+ return KNOWN_CRAWLERS.map((c) => c.name);
560
+ }
561
+ var SCRAPER_SERVICES = [
562
+ // Jina AI Reader — r.jina.ai/URL — converts any page to clean Markdown
563
+ { name: "JinaReader", patterns: [/JinaReader/i] },
564
+ // Cloudflare Browser Rendering /crawl endpoint (open beta, announced March 2026)
565
+ { name: "Cloudflare-BrowserRendering", patterns: [/CloudflareBrowserRenderingCrawler/i] },
566
+ // FireCrawl — LLM-ready content extraction service
567
+ { name: "FireCrawl", patterns: [/FireCrawlAgent/i, /firecrawl/i] },
568
+ // Apify web scraping platform
569
+ { name: "Apify", patterns: [/ApifyBot/i] }
570
+ ];
571
+ function detectScraperService(userAgent) {
572
+ if (!userAgent) return { isScraperService: false, name: null };
573
+ for (const service of SCRAPER_SERVICES) {
574
+ for (const pattern of service.patterns) {
575
+ if (pattern.test(userAgent)) {
576
+ return { isScraperService: true, name: service.name };
577
+ }
578
+ }
579
+ }
580
+ return { isScraperService: false, name: null };
581
+ }
582
+
583
+ // src/query-handler.ts
584
+ var RESPONSE_HEADERS = {
585
+ "Content-Type": "application/json",
586
+ "Cache-Control": "no-store",
587
+ "X-Robots-Tag": "noindex"
588
+ // Don't index the query endpoint itself
589
+ };
590
+ function createQueryHandler(client, config) {
591
+ const queryPath = config.queryPath ?? "/query";
592
+ return async function handleQuery(req) {
593
+ if (!req.query || req.query.trim() === "") {
594
+ const origin = getOrigin(req.requestUrl);
595
+ const discovery = {
596
+ version: "1.0",
597
+ endpoint: `${origin}${queryPath}`,
598
+ 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.",
599
+ 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.",
600
+ usage: {
601
+ method: "GET",
602
+ parameters: {
603
+ q: {
604
+ type: "string",
605
+ required: true,
606
+ description: "Your question or search query (max 500 characters)"
607
+ },
608
+ lang: {
609
+ type: "string",
610
+ required: false,
611
+ description: "Preferred response language (ISO 639-1 code, default: en)"
612
+ },
613
+ surface_ads: {
614
+ type: "boolean",
615
+ required: false,
616
+ description: "Include sponsored ads in response (1 = yes, default; 0 = no). When crawling, use ai_crawler=1 to signal you want ads."
617
+ },
618
+ ai_crawler: {
619
+ type: "boolean",
620
+ required: false,
621
+ description: "Set to 1 when you are an AI agent/crawler. Signals the server to surface ads. Use with ?q=... for targeted answers + ads."
622
+ }
623
+ },
624
+ example: `${origin}${queryPath}?q=what+features+do+you+offer&ai_crawler=1`,
625
+ response_format: "application/json"
626
+ },
627
+ capabilities: [
628
+ "Natural language question answering",
629
+ "Source citations with relevance scores",
630
+ "Structured JSON for easy parsing",
631
+ "Supports follow-up context via session continuity",
632
+ "Query-preferred: use ?q= instead of full-site crawl"
633
+ ],
634
+ rate_limit: "100 requests per hour per IP"
635
+ };
636
+ return { status: 200, body: discovery, headers: RESPONSE_HEADERS };
637
+ }
638
+ const trimmedQuery = req.query.trim();
639
+ if (trimmedQuery.length > 500) {
640
+ return errorResponse(400, "QUERY_TOO_LONG", "Query must be 500 characters or fewer");
641
+ }
642
+ const requestId = crypto.randomUUID();
643
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
644
+ const startMs = Date.now();
645
+ const surfaceAds = req.surface_ads !== false;
646
+ const aiCrawler = req.ai_crawler === true;
647
+ let backendResponse;
648
+ try {
649
+ backendResponse = await client.query({
650
+ site_id: config.siteId,
651
+ query: trimmedQuery,
652
+ agent_ua: req.userAgent,
653
+ agent_ip: req.ipAddress,
654
+ request_id: requestId,
655
+ timestamp,
656
+ surface_ads: surfaceAds,
657
+ ai_crawler: aiCrawler
658
+ });
659
+ } catch (err) {
660
+ if (err instanceof ApptvtyTrialExpiredError) {
661
+ return errorResponse(
662
+ 402,
663
+ "TRIAL_EXPIRED",
664
+ `Apptvty free trial has expired. The site owner must log in and upgrade to continue. Dashboard: ${err.dashboardUrl}`
665
+ );
666
+ }
667
+ return errorResponse(502, "UPSTREAM_ERROR", "Could not retrieve an answer at this time");
668
+ }
669
+ const responseTimeMs = Date.now() - startMs;
670
+ const ads = backendResponse.sponsored ? Array.isArray(backendResponse.sponsored) ? backendResponse.sponsored : [backendResponse.sponsored] : [];
671
+ for (const ad of ads) {
672
+ void client.logImpression({
673
+ impression_id: ad.impression_id,
674
+ site_id: config.siteId,
675
+ query: trimmedQuery,
676
+ agent_ua: req.userAgent,
677
+ agent_ip: req.ipAddress,
678
+ timestamp
679
+ });
680
+ }
681
+ const agentResponse = {
682
+ success: true,
683
+ version: "1.0",
684
+ query: trimmedQuery,
685
+ answer: backendResponse.answer,
686
+ sources: backendResponse.sources,
687
+ confidence: backendResponse.confidence,
688
+ ...backendResponse.sponsored && { sponsored: backendResponse.sponsored },
689
+ metadata: {
690
+ request_id: requestId,
691
+ response_time_ms: responseTimeMs,
692
+ tokens_used: backendResponse.tokens_used,
693
+ site_id: config.siteId,
694
+ timestamp
695
+ }
696
+ };
697
+ return { status: 200, body: agentResponse, headers: RESPONSE_HEADERS };
698
+ };
699
+ }
700
+ function errorResponse(status, code, message) {
701
+ const body = {
702
+ success: false,
703
+ error: {
704
+ code,
705
+ message,
706
+ request_id: crypto.randomUUID(),
707
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
708
+ }
709
+ };
710
+ return { status, body, headers: RESPONSE_HEADERS };
711
+ }
712
+ function getOrigin(url) {
713
+ try {
714
+ const parsed = new URL(url);
715
+ return parsed.origin;
716
+ } catch {
717
+ return "";
718
+ }
719
+ }
720
+
721
+ // src/dashboard-handler.ts
722
+ function createDashboardHandler(client, config) {
723
+ return async function handleDashboard(req) {
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")) {
738
+ const data = await client.getSiteStats();
739
+ return jsonResponse(200, data);
740
+ }
741
+ if (path.endsWith("/api/activity")) {
742
+ const url = new URL(path, "http://localhost");
743
+ const limit = parseInt(url.searchParams.get("limit") || "50");
744
+ const offset = parseInt(url.searchParams.get("offset") || "0");
745
+ const data = await client.getRecentActivity(config.siteId, limit, offset);
746
+ return jsonResponse(200, data);
747
+ }
748
+ if (path.endsWith("/api/queries")) {
749
+ const url = new URL(path, "http://localhost");
750
+ const limit = parseInt(url.searchParams.get("limit") || "20");
751
+ const offset = parseInt(url.searchParams.get("offset") || "0");
752
+ const data = await client.getRecentQueries(config.siteId, limit, offset);
753
+ return jsonResponse(200, data);
754
+ }
755
+ if (path.endsWith("/api/stats")) {
756
+ const data = await client.getSiteDailyStats();
757
+ return jsonResponse(200, data);
758
+ }
759
+ const html = getDashboardHtml(config);
760
+ return {
761
+ status: 200,
762
+ body: html,
763
+ headers: { "Content-Type": "text/html" }
764
+ };
765
+ };
766
+ }
767
+ function jsonResponse(status, data) {
768
+ return {
769
+ status,
770
+ body: JSON.stringify(data),
771
+ headers: { "Content-Type": "application/json" }
772
+ };
773
+ }
774
+ function getDashboardHtml(config) {
775
+ return `
776
+ <!DOCTYPE html>
777
+ <html lang="en" class="dark">
778
+ <head>
779
+ <meta charset="UTF-8">
780
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
781
+ <title>Apptvty Logs \u2014 ${config.siteId}</title>
782
+ <script src="https://cdn.tailwindcss.com"></script>
783
+ <script src="https://unpkg.com/lucide@latest"></script>
784
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
785
+ <style>
786
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
787
+ body { font-family: 'Inter', sans-serif; background-color: #09090b; color: #fafafa; }
788
+ .glass { background: rgba(24, 24, 27, 0.8); backdrop-filter: blur(12px); border: 1px solid rgba(39, 39, 42, 1); }
789
+ .gradient-text { background: linear-gradient(to right, #60a5fa, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
790
+ </style>
791
+ </head>
792
+ <body class="p-6">
793
+ <div id="app" class="max-w-7xl mx-auto space-y-6">
794
+ <!-- Header -->
795
+ <header class="flex justify-between items-center mb-8">
796
+ <div>
797
+ <h1 class="text-3xl font-bold gradient-text">Activity Logs</h1>
798
+ <p class="text-zinc-400">Real-time agentic insights for ${config.siteId}</p>
799
+ </div>
800
+ <div class="flex items-center gap-3">
801
+ <span class="flex h-2 w-2 rounded-full bg-green-500 animate-pulse"></span>
802
+ <span class="text-sm font-medium text-zinc-300">Live Connection</span>
803
+ </div>
804
+ </header>
805
+
806
+ <!-- Stats Grid -->
807
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-4" id="stats-grid">
808
+ <div class="glass p-5 rounded-2xl animate-pulse h-24"></div>
809
+ <div class="glass p-5 rounded-2xl animate-pulse h-24"></div>
810
+ <div class="glass p-5 rounded-2xl animate-pulse h-24"></div>
811
+ <div class="glass p-5 rounded-2xl animate-pulse h-24"></div>
812
+ </div>
813
+
814
+ <!-- Main Content -->
815
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
816
+ <!-- Traffic Chart -->
817
+ <div class="lg:col-span-2 glass p-6 rounded-2xl">
818
+ <h2 class="text-lg font-semibold mb-4">Traffic Overview</h2>
819
+ <div class="h-[300px]">
820
+ <canvas id="trafficChart"></canvas>
821
+ </div>
822
+ </div>
823
+
824
+ <!-- Recent Queries -->
825
+ <div class="glass p-6 rounded-2xl flex flex-col h-[400px]">
826
+ <div class="flex justify-between items-center mb-4">
827
+ <h2 class="text-lg font-semibold">Agent Queries</h2>
828
+ <button id="load-more-queries" class="text-xs text-blue-400 hover:text-blue-300 transition-colors">Load More</button>
829
+ </div>
830
+ <div id="queries-list" class="flex-1 overflow-y-auto space-y-3 custom-scrollbar">
831
+ <div class="text-zinc-500 text-sm italic">Loading queries...</div>
832
+ </div>
833
+ </div>
834
+ </div>
835
+
836
+ <!-- Real-time Activity Table -->
837
+ <div class="glass p-6 rounded-2xl overflow-hidden">
838
+ <div class="flex justify-between items-center mb-4">
839
+ <h2 class="text-lg font-semibold">Real-time Activity</h2>
840
+ <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>
841
+ </div>
842
+ <div class="overflow-x-auto">
843
+ <table class="w-full text-left">
844
+ <thead>
845
+ <tr class="text-zinc-500 text-sm border-b border-zinc-800">
846
+ <th class="pb-3 pr-4">Timestamp</th>
847
+ <th class="pb-3 pr-4">Method</th>
848
+ <th class="pb-3 pr-4">Path</th>
849
+ <th class="pb-3 pr-4">Agent</th>
850
+ <th class="pb-3">Status</th>
851
+ </tr>
852
+ </thead>
853
+ <tbody id="activity-body" class="text-sm">
854
+ <tr><td colspan="5" class="pt-4 text-zinc-500 italic">Connecting to activity stream...</td></tr>
855
+ </tbody>
856
+ </table>
857
+ </div>
858
+ </div>
859
+ </div>
860
+
861
+ <script>
862
+ const API_BASE = window.location.pathname.replace(//$/, '');
863
+ let queryOffset = 0;
864
+ let activityOffset = 0;
865
+ const LIMIT_QUERIES = 20;
866
+ const LIMIT_ACTIVITY = 50;
867
+
868
+ async function fetchData() {
869
+ try {
870
+ // Initial load or refresh (offset 0 resets)
871
+ const [overview, stats] = await Promise.all([
872
+ fetch(\`\${API_BASE}/api/overview\`).then(r => r.json()),
873
+ fetch(\`\${API_BASE}/api/stats\`).then(r => r.json())
874
+ ]);
875
+
876
+ updateStats(overview);
877
+ initChart(stats);
878
+
879
+ // Fetch first pages if empty
880
+ if (queryOffset === 0) fetchQueries(true);
881
+ if (activityOffset === 0) fetchActivity(true);
882
+ } catch (err) {
883
+ console.error('Failed to fetch dashboard data:', err);
884
+ }
885
+ }
886
+
887
+ async function fetchQueries(replace = false) {
888
+ try {
889
+ const data = await fetch(\`\${API_BASE}/api/queries?limit=\${LIMIT_QUERIES}&offset=\${queryOffset}\`).then(r => r.json());
890
+ updateQueries(data, replace);
891
+ } catch (e) { console.error('Queries error:', e); }
892
+ }
893
+
894
+ async function fetchActivity(replace = false) {
895
+ try {
896
+ const data = await fetch(\`\${API_BASE}/api/activity?limit=\${LIMIT_ACTIVITY}&offset=\${activityOffset}\`).then(r => r.json());
897
+ updateActivity(data, replace);
898
+ } catch (e) { console.error('Activity error:', e); }
899
+ }
900
+
901
+ function updateStats(data) {
902
+ const grid = document.getElementById('stats-grid');
903
+ grid.innerHTML = \`
904
+ <div class="glass p-5 rounded-2xl">
905
+ <p class="text-zinc-400 text-xs font-medium uppercase tracking-wider mb-1">Total Requests</p>
906
+ <p class="text-2xl font-bold">\${data.total_requests_30d.toLocaleString()}</p>
907
+ </div>
908
+ <div class="glass p-5 rounded-2xl border-l-4 border-blue-500">
909
+ <p class="text-zinc-400 text-xs font-medium uppercase tracking-wider mb-1">AI Requests</p>
910
+ <p class="text-2xl font-bold">\${data.ai_requests_30d.toLocaleString()}</p>
911
+ </div>
912
+ <div class="glass p-5 rounded-2xl">
913
+ <p class="text-zinc-400 text-xs font-medium uppercase tracking-wider mb-1">AI Percentage</p>
914
+ <p class="text-2xl font-bold text-blue-400">\${data.ai_percentage.toFixed(1)}%</p>
915
+ </div>
916
+ <div class="glass p-5 rounded-2xl">
917
+ <p class="text-zinc-400 text-xs font-medium uppercase tracking-wider mb-1">Health Status</p>
918
+ <p class="text-2xl font-bold text-green-400">Optimal</p>
919
+ </div>
920
+ \`;
921
+ }
922
+
923
+ function updateQueries(data, replace) {
924
+ const list = document.getElementById('queries-list');
925
+ const items = Array.isArray(data) ? data : (data.queries || []);
926
+ const rows = items.map(q => \`
927
+ <div class="p-3 bg-zinc-900/50 rounded-xl border border-zinc-800 hover:border-zinc-700 transition-colors">
928
+ <p class="text-sm font-medium text-zinc-200">\${q.question || q.query}</p>
929
+ <div class="mt-2 flex justify-between items-center text-[10px] text-zinc-500">
930
+ <span class="flex items-center gap-1">
931
+ <i data-lucide="clock" class="w-3 h-3"></i>
932
+ \${new Date(q.timestamp).toLocaleString()}
933
+ </span>
934
+ <span class="px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-400 border border-blue-500/20">
935
+ Agent Match
936
+ </span>
937
+ </div>
938
+ </div>
939
+ \`).join('');
940
+
941
+ if (replace) list.innerHTML = rows || '<div class="text-zinc-600 text-sm">No recent queries.</div>';
942
+ else list.insertAdjacentHTML('beforeend', rows);
943
+ lucide.createIcons();
944
+ }
945
+
946
+ function updateActivity(data, replace) {
947
+ const body = document.getElementById('activity-body');
948
+ const items = Array.isArray(data) ? data : (data.activity || []);
949
+ const rows = items.map(r => \`
950
+ <tr class="border-b border-zinc-800/50 hover:bg-zinc-800/20 transition-colors">
951
+ <td class="py-3 pr-4 text-zinc-400 text-xs">\${new Date(r.timestamp || r.created_at).toLocaleTimeString()}</td>
952
+ <td class="py-3 pr-4 font-mono text-[10px] tracking-tight text-blue-400">\${r.method}</td>
953
+ <td class="py-3 pr-4 text-zinc-300 max-w-xs truncate">\${r.path}</td>
954
+ <td class="py-3 pr-4 text-zinc-500">\${r.crawler_type || 'Human'}</td>
955
+ <td class="py-3">
956
+ <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">
957
+ \${r.status_code || 200}
958
+ </span>
959
+ </td>
960
+ </tr>
961
+ \`).join('');
962
+
963
+ if (replace) body.innerHTML = rows || '<tr><td colspan="5" class="py-4 text-center text-zinc-600">No activity.</td></tr>';
964
+ else body.insertAdjacentHTML('beforeend', rows);
965
+ }
966
+
967
+ function initChart(stats) {
968
+ const canvas = document.getElementById('trafficChart');
969
+ if (window.myChart) window.myChart.destroy();
970
+ const ctx = canvas.getContext('2d');
971
+ window.myChart = new Chart(ctx, {
972
+ type: 'line',
973
+ data: {
974
+ labels: stats.map(s => s.date.split('-').slice(1).join('/')),
975
+ datasets: [
976
+ {
977
+ label: 'AI Requests',
978
+ data: stats.map(s => s.ai_requests),
979
+ borderColor: '#3b82f6',
980
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
981
+ fill: true,
982
+ tension: 0.4
983
+ },
984
+ {
985
+ label: 'Total Requests',
986
+ data: stats.map(s => s.total_requests),
987
+ borderColor: '#8b5cf6',
988
+ backgroundColor: 'rgba(139, 92, 246, 0.1)',
989
+ fill: true,
990
+ tension: 0.4
991
+ }
992
+ ]
993
+ },
994
+ options: {
995
+ responsive: true,
996
+ maintainAspectRatio: false,
997
+ plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false } },
998
+ scales: {
999
+ y: { beginAtZero: true, grid: { color: 'rgba(39, 39, 42, 0.5)' }, ticks: { color: '#71717a' } },
1000
+ x: { grid: { display: false }, ticks: { color: '#71717a' } }
1001
+ }
1002
+ }
1003
+ });
1004
+ }
1005
+
1006
+ document.getElementById('load-more-queries').onclick = () => {
1007
+ queryOffset += LIMIT_QUERIES;
1008
+ fetchQueries(false);
1009
+ };
1010
+
1011
+ document.getElementById('load-more-activity').onclick = () => {
1012
+ activityOffset += LIMIT_ACTIVITY;
1013
+ fetchActivity(false);
1014
+ };
1015
+
1016
+ fetchData();
1017
+ setInterval(fetchData, 30000); // Polling for updates (stats only)
1018
+ </script>
1019
+ </body>
1020
+ </html>
1021
+ `;
1022
+ }
1023
+
1024
+ // src/ad-injection.ts
1025
+ var AD_INJECTION_MARKER = "<!-- apptvty-sponsored -->";
1026
+ function injectIntoHtml(html, ads, isScraperService) {
1027
+ if (!html || ads.length === 0) return html;
1028
+ if (html.includes(AD_INJECTION_MARKER)) return html;
1029
+ let modified = html;
1030
+ const contentBlock = buildContentStreamBlock(ads);
1031
+ if (modified.includes("</article>")) {
1032
+ modified = modified.replace("</article>", `${contentBlock}
1033
+ </article>`);
1034
+ } else if (modified.includes("</main>")) {
1035
+ modified = modified.replace("</main>", `${contentBlock}
1036
+ </main>`);
1037
+ } else if (!isScraperService && modified.includes("</body>")) {
1038
+ modified = modified.replace("</body>", `${contentBlock}
1039
+ </body>`);
1040
+ }
1041
+ if (!isScraperService && modified.includes("</head>")) {
1042
+ const jsonLdBlock = buildJsonLdBlock(ads);
1043
+ modified = modified.replace("</head>", `${jsonLdBlock}
1044
+ </head>`);
1045
+ }
1046
+ return modified;
1047
+ }
1048
+ function buildSponsoredHeader(ads) {
1049
+ return JSON.stringify(
1050
+ ads.map((ad) => ({ text: ad.text, url: ad.url, advertiser: ad.advertiser }))
1051
+ );
1052
+ }
1053
+ function buildContentStreamBlock(ads) {
1054
+ const paragraphs = ads.map(
1055
+ (ad) => `<p data-apptvty-sponsored="${escapeAttr(ad.impression_id)}"><strong>[Sponsored]</strong> <a href="${escapeAttr(ad.url)}" rel="sponsored noopener">${escapeHtml(ad.text)}</a> \u2014 <span>${escapeHtml(ad.advertiser)}</span></p>`
1056
+ ).join("\n");
1057
+ return `${AD_INJECTION_MARKER}
1058
+ ${paragraphs}`;
1059
+ }
1060
+ function buildJsonLdBlock(ads) {
1061
+ const entries = ads.map((ad) => ({
1062
+ "@context": "https://schema.org",
1063
+ "@type": "WPAdBlock",
1064
+ sponsor: {
1065
+ "@type": "Organization",
1066
+ name: ad.advertiser,
1067
+ url: ad.url
1068
+ },
1069
+ description: ad.text
1070
+ }));
1071
+ const ld = entries.length === 1 ? entries[0] : entries;
1072
+ return `<script type="application/ld+json">${JSON.stringify(ld)}</script>`;
1073
+ }
1074
+ function escapeHtml(s) {
1075
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1076
+ }
1077
+ function escapeAttr(s) {
1078
+ return s.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1079
+ }
1080
+
1081
+ export {
1082
+ ApptvtyClient,
1083
+ ApptvtyApiError,
1084
+ ApptvtyInsufficientBalanceError,
1085
+ RequestLogger,
1086
+ getClientIp,
1087
+ detectCrawler,
1088
+ getKnownCrawlerNames,
1089
+ detectScraperService,
1090
+ createQueryHandler,
1091
+ createDashboardHandler,
1092
+ AD_INJECTION_MARKER,
1093
+ injectIntoHtml,
1094
+ buildSponsoredHeader
1095
+ };
1096
+ //# sourceMappingURL=chunk-GMQN6656.mjs.map