apptvty 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -89,7 +89,8 @@ function withApptvty(config, next) {
89
89
  const crawlerInfo = detectCrawler(userAgent);
90
90
  const scraperService = detectScraperService(userAgent);
91
91
  const aiCrawlerParam = parseBoolParam(request.nextUrl.searchParams.get("ai_crawler"), false);
92
- const isCrawler = crawlerInfo.isAi || aiCrawlerParam || scraperService.isScraperService;
92
+ const isAi = crawlerInfo.isAi || aiCrawlerParam;
93
+ const isScraper = isAi || scraperService.isScraperService || crawlerInfo.name === "unknown_bot";
93
94
  if (request.nextUrl.pathname === "/api/apptvty/verify") {
94
95
  const challenge = request.nextUrl.searchParams.get("challenge");
95
96
  if (challenge) {
@@ -134,11 +135,12 @@ function withApptvty(config, next) {
134
135
  ip_address: getClientIp(headers),
135
136
  user_agent: userAgent,
136
137
  referrer: request.headers.get("referer"),
137
- is_ai_crawler: crawlerInfo.isAi,
138
+ is_ai_crawler: isAi,
138
139
  crawler_type: crawlerInfo.name,
139
140
  crawler_organization: crawlerInfo.organization,
140
141
  confidence_score: crawlerInfo.confidence,
141
- scraper_service: scraperService.name
142
+ scraper_service: scraperService.name,
143
+ attribution_id: request.nextUrl.searchParams.get("atid")
142
144
  };
143
145
  const isInternalRequest = request.headers.get("x-apptvty-internal") === "true";
144
146
  if (!isInternalRequest && !pathname.startsWith(queryPath)) {
@@ -147,46 +149,73 @@ function withApptvty(config, next) {
147
149
  event.waitUntil(logger.flush());
148
150
  }
149
151
  }
150
- if (isCrawler && !isInternalRequest && !pathname.startsWith(queryPath)) {
152
+ if (!isInternalRequest && !pathname.startsWith(queryPath) && response.status === 200) {
151
153
  try {
152
- const proxyReq = new Request(request.url, {
153
- headers: new Headers(request.headers)
154
- });
155
- proxyReq.headers.set("x-apptvty-internal", "true");
156
- const res = await fetch(proxyReq);
157
- const contentType = res.headers.get("content-type") ?? "";
158
- if (contentType.includes("text/html")) {
159
- const html = await res.text();
160
- let markdown = convertHtmlToMarkdown(html);
161
- const pageAds = await client.getAdsForPage({ site_id: config.siteId, page_path: pathname });
162
- if (pageAds.ads && pageAds.ads.length > 0) {
163
- const ad = pageAds.ads[0];
164
- markdown += `
154
+ const pageAds = await client.getAdsForPage({ site_id: config.siteId, page_path: pathname });
155
+ if (pageAds.ads && pageAds.ads.length > 0) {
156
+ const ad = pageAds.ads[0];
157
+ client.logImpression({
158
+ impression_id: ad.impression_id,
159
+ site_id: config.siteId,
160
+ page_path: pathname,
161
+ agent_ua: userAgent,
162
+ agent_ip: getClientIp(headers),
163
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
164
+ }).catch(() => {
165
+ });
166
+ if (isAi || scraperService.isScraperService) {
167
+ const proxyReq = new Request(request.url, { headers: new Headers(request.headers) });
168
+ proxyReq.headers.set("x-apptvty-internal", "true");
169
+ const res = await fetch(proxyReq);
170
+ const contentType = res.headers.get("content-type") ?? "";
171
+ if (contentType.includes("text/html")) {
172
+ const html = await res.text();
173
+ let markdown = convertHtmlToMarkdown(html);
174
+ markdown += `
165
175
 
166
176
  ---
167
177
  > **Sponsored:** [${ad.text}](${ad.url}) - ${ad.advertiser}
168
178
  `;
169
- client.logImpression({
170
- impression_id: ad.impression_id,
171
- site_id: config.siteId,
172
- page_path: pathname,
173
- agent_ua: userAgent,
174
- agent_ip: getClientIp(headers),
175
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
176
- }).catch(() => {
177
- });
179
+ return new NextResponse(markdown, {
180
+ status: res.status,
181
+ headers: {
182
+ "Content-Type": "text/markdown",
183
+ "X-Apptvty-AEO": "true",
184
+ "X-Sponsored-Content": `${ad.text}; url=${ad.url}`
185
+ }
186
+ });
187
+ }
178
188
  }
179
- return new NextResponse(markdown, {
180
- status: res.status,
181
- headers: {
182
- "Content-Type": "text/markdown",
183
- "X-Apptvty-AEO": "true"
189
+ const originalHeaders = headersToRecord(response.headers);
190
+ if (originalHeaders["content-type"]?.includes("text/html")) {
191
+ const html = await response.text();
192
+ const jsonLd = `
193
+ <script type="application/ld+json">{"@context":"https://schema.org","@type":"CreativeWork","author":{"@type":"Organization","name":"${ad.advertiser}"},"mainEntityOfPage":{"@type":"WebPage","@id":"${ad.url}"},"headline":"Sponsored: ${ad.text}"}</script>
194
+ `;
195
+ const stealthDiv = `
196
+ <div style="display:none !important;visibility:hidden;height:0;width:0;overflow:hidden;" aria-hidden="true" data-apptvty-ad="${ad.impression_id}">Sponsored by ${ad.advertiser}: <a href="${ad.url}">${ad.text}</a></div>
197
+ `;
198
+ let modifiedHtml = html;
199
+ if (html.includes("</head>")) {
200
+ modifiedHtml = html.replace("</head>", `${jsonLd}</head>`);
184
201
  }
185
- });
202
+ if (modifiedHtml.includes("</body>")) {
203
+ modifiedHtml = modifiedHtml.replace("</body>", `${stealthDiv}</body>`);
204
+ } else {
205
+ modifiedHtml += stealthDiv;
206
+ }
207
+ return new NextResponse(modifiedHtml, {
208
+ status: response.status,
209
+ headers: {
210
+ ...originalHeaders,
211
+ "X-Sponsored-Content": `${ad.text}; url=${ad.url}`
212
+ }
213
+ });
214
+ }
215
+ response.headers.set("X-Sponsored-Content", `${ad.text}; url=${ad.url}`);
186
216
  }
187
- return res;
188
217
  } catch (err) {
189
- if (config.debug) console.warn("[apptvty] Markdown proxy failed:", err);
218
+ if (config.debug) console.warn("[apptvty] Stealth injection failed:", err);
190
219
  }
191
220
  }
192
221
  return response;
@@ -254,4 +283,4 @@ export {
254
283
  createNextjsQueryHandler,
255
284
  createNextjsDashboardHandler
256
285
  };
257
- //# sourceMappingURL=chunk-LI2S7OO4.mjs.map
286
+ //# sourceMappingURL=chunk-2UCECX7X.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/middleware/nextjs.ts","../src/markdown.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 { convertHtmlToMarkdown } from '../markdown.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, event?: any) => 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) {\n const { client, logger } = getInstance(config);\n const queryPath = config.queryPath ?? '/query';\n\n return async function apptvtyMiddleware(request: NextRequest, event?: any): 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 \n // broad detection: is it a confirmed AI, a known scraper service, or a suspicious generic bot?\n const isAi = crawlerInfo.isAi || aiCrawlerParam;\n const isScraper = isAi || scraperService.isScraperService || crawlerInfo.name === 'unknown_bot';\n\n // ── Active Handshake Verification (PRIORITY) ──────────────────────────────\n if (request.nextUrl.pathname === '/api/apptvty/verify') {\n const challenge = request.nextUrl.searchParams.get('challenge');\n if (challenge) {\n const encoder = new TextEncoder();\n const keyData = encoder.encode(config.apiKey);\n const cryptoKey = await globalThis.crypto.subtle.importKey(\n 'raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']\n );\n const prefixedChallenge = `apptvty_verify_challenge:${challenge}`;\n const signatureBuffer = await globalThis.crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(prefixedChallenge));\n const signature = Array.from(new Uint8Array(signatureBuffer))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n\n return NextResponse.json({\n site_id: config.siteId,\n verified: true,\n signature\n });\n }\n }\n\n // Run the user's middleware (or passthrough)\n let response: Response | NextResponse;\n try {\n response = next ? await next(request, event) : 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 request_method: request.method,\n request_path: pathname,\n response_status: response.status,\n response_time_ms: responseTimeMs,\n ip_address: getClientIp(headers),\n user_agent: userAgent,\n referrer: request.headers.get('referer'),\n is_ai_crawler: isAi,\n crawler_type: crawlerInfo.name,\n crawler_organization: crawlerInfo.organization,\n confidence_score: crawlerInfo.confidence,\n scraper_service: scraperService.name,\n attribution_id: request.nextUrl.searchParams.get('atid'),\n };\n\n const isInternalRequest = request.headers.get('x-apptvty-internal') === 'true';\n\n if (!isInternalRequest && !pathname.startsWith(queryPath)) {\n logger.enqueue(entry);\n if (event && typeof event.waitUntil === 'function') {\n event.waitUntil(logger.flush());\n }\n }\n\n // ── Stealth Ad Injection Strategy ─────────────────────────────────────────\n // If not an internal request, we check for ads to inject.\n if (!isInternalRequest && !pathname.startsWith(queryPath) && response.status === 200) {\n try {\n const pageAds = await client.getAdsForPage({ site_id: config.siteId, page_path: pathname });\n \n if (pageAds.ads && pageAds.ads.length > 0) {\n const ad = pageAds.ads[0];\n \n // Log impression fire-and-forget\n client.logImpression({\n impression_id: ad.impression_id,\n site_id: config.siteId,\n page_path: pathname,\n agent_ua: userAgent,\n agent_ip: getClientIp(headers),\n timestamp: new Date().toISOString()\n }).catch(() => {});\n\n // Tier 1: Confirm AI Crawler -> Return Markdown (Best for LLMs)\n if (isAi || scraperService.isScraperService) {\n const proxyReq = new Request(request.url, { headers: new Headers(request.headers) });\n proxyReq.headers.set('x-apptvty-internal', 'true');\n const res = await fetch(proxyReq);\n const contentType = res.headers.get('content-type') ?? '';\n \n if (contentType.includes('text/html')) {\n const html = await res.text();\n let markdown = convertHtmlToMarkdown(html);\n markdown += `\\n\\n---\\n> **Sponsored:** [${ad.text}](${ad.url}) - ${ad.advertiser}\\n`;\n \n return new NextResponse(markdown, {\n status: res.status,\n headers: {\n 'Content-Type': 'text/markdown',\n 'X-Apptvty-AEO': 'true',\n 'X-Sponsored-Content': `${ad.text}; url=${ad.url}`\n }\n });\n }\n }\n\n // Tier 2: Grey Area (Suspected Scraper or Human-Spoofer) -> Stealth HTML Injection\n // Also applied to humans, but hidden via JSON-LD + Stealth Div\n const originalHeaders = headersToRecord(response.headers);\n if (originalHeaders['content-type']?.includes('text/html')) {\n const html = await (response as Response).text();\n \n // Inject JSON-LD into head\n const jsonLd = `\\n<script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"CreativeWork\",\"author\":{\"@type\":\"Organization\",\"name\":\"${ad.advertiser}\"},\"mainEntityOfPage\":{\"@type\":\"WebPage\",\"@id\":\"${ad.url}\"},\"headline\":\"Sponsored: ${ad.text}\"}</script>\\n`;\n \n // Inject Invisible Stealth Div into body\n const stealthDiv = `\\n<div style=\"display:none !important;visibility:hidden;height:0;width:0;overflow:hidden;\" aria-hidden=\"true\" data-apptvty-ad=\"${ad.impression_id}\">Sponsored by ${ad.advertiser}: <a href=\"${ad.url}\">${ad.text}</a></div>\\n`;\n \n let modifiedHtml = html;\n if (html.includes('</head>')) {\n modifiedHtml = html.replace('</head>', `${jsonLd}</head>`);\n }\n if (modifiedHtml.includes('</body>')) {\n modifiedHtml = modifiedHtml.replace('</body>', `${stealthDiv}</body>`);\n } else {\n modifiedHtml += stealthDiv;\n }\n\n return new NextResponse(modifiedHtml, {\n status: response.status,\n headers: {\n ...originalHeaders,\n 'X-Sponsored-Content': `${ad.text}; url=${ad.url}`\n }\n });\n }\n \n // Tier 3: Other content types (or if HTML injection skipped) -> Header only\n response.headers.set('X-Sponsored-Content', `${ad.text}; url=${ad.url}`);\n }\n } catch (err) {\n if (config.debug) console.warn('[apptvty] Stealth injection failed:', err);\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\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","import * as cheerio from 'cheerio';\n\n/**\n * A lightweight Edge-compatible HTML to Markdown converter.\n * Perfect for Vercel Edge runtime where DOMParser and 'turndown' don't work reliably.\n */\nexport function convertHtmlToMarkdown(html: string): string {\n if (!html) return '';\n \n const $ = cheerio.load(html);\n \n // Clean up noisy elements\n $('script, style, nav, footer, header, aside, svg, .ad, .sponsor, noscript').remove();\n \n // Scope to main content area if it exists, to avoid extracting menus\n const main = $('main, article, [role=\"main\"], #content, .content').first();\n const root = main.length ? main : $('body');\n \n let markdown = '';\n \n // Extract content pseudo-sequentially\n root.find('h1, h2, h3, h4, h5, h6, p, ul, ol').each((_, el) => {\n const $el = $(el);\n const tagName = el.tagName.toLowerCase();\n \n if (tagName === 'ul' || tagName === 'ol') {\n $el.find('li').each((_, li) => {\n const text = cleanText($(li).text());\n if (text) markdown += `- ${text}\\n`;\n });\n markdown += '\\n';\n return;\n }\n \n const text = cleanText($el.text());\n if (!text) return;\n\n if (tagName === 'h1') markdown += `# ${text}\\n\\n`;\n else if (tagName === 'h2') markdown += `## ${text}\\n\\n`;\n else if (tagName === 'h3') markdown += `### ${text}\\n\\n`;\n else if (tagName === 'h4') markdown += `#### ${text}\\n\\n`;\n else if (tagName === 'h5') markdown += `##### ${text}\\n\\n`;\n else if (tagName === 'h6') markdown += `###### ${text}\\n\\n`;\n else if (tagName === 'p') markdown += `${text}\\n\\n`;\n });\n\n // Fallback if structured HTML is poor and we found almost nothing\n if (markdown.trim().length < 50) {\n markdown = cleanText(root.text()) + '\\n\\n';\n }\n\n return markdown.trim();\n}\n\nfunction cleanText(text: string): string {\n return text.trim().replace(/\\s+/g, ' ');\n}\n"],"mappings":"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","names":["_","text"]}
@@ -84,13 +84,18 @@ function createExpressMiddleware(config) {
84
84
  const crawlerInfo = detectCrawler(userAgent);
85
85
  const scraperService = detectScraperService(userAgent);
86
86
  const path = req.url ?? "/";
87
- const isCrawler = crawlerInfo.isAi || scraperService.isScraperService;
88
87
  const ipAddress = getClientIp(req.headers);
89
- const adsPromise = isCrawler && !shouldSkip(path) ? client.getAdsForPage({ site_id: config.siteId, page_path: path }).catch(() => ({ ads: [] })) : Promise.resolve({ ads: [] });
90
- if (isCrawler && !shouldSkip(path)) {
91
- const chunks = [];
92
- const originalWrite = res.write.bind(res);
93
- const originalEnd = res.end.bind(res);
88
+ const urlObj = new URL(path, `http://${req.headers.host ?? "localhost"}`);
89
+ const aiCrawlerParam = parseBoolParam(urlObj.searchParams.get("ai_crawler"), false);
90
+ const attributionId = urlObj.searchParams.get("atid");
91
+ const isAi = crawlerInfo.isAi || aiCrawlerParam;
92
+ const isScraper = isAi || scraperService.isScraperService || crawlerInfo.name === "unknown_bot";
93
+ const adsPromise = !isInternalRequest(req) && !shouldSkip(path) ? client.getAdsForPage({ site_id: config.siteId, page_path: path }).catch(() => ({ ads: [] })) : Promise.resolve({ ads: [] });
94
+ const chunks = [];
95
+ const originalWrite = res.write.bind(res);
96
+ const originalEnd = res.end.bind(res);
97
+ const shouldBuffer = !isInternalRequest(req) && !shouldSkip(path);
98
+ if (shouldBuffer) {
94
99
  res.write = function(chunk, encodingOrCallback, callback) {
95
100
  if (chunk != null) {
96
101
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
@@ -105,17 +110,12 @@ function createExpressMiddleware(config) {
105
110
  }
106
111
  const contentType = res.getHeader("content-type") ?? "";
107
112
  const isHtml = contentType.includes("text/html");
108
- if (!isHtml || chunks.length === 0) {
113
+ if (!isHtml || chunks.length === 0 || res.statusCode !== 200) {
109
114
  res.write = originalWrite;
110
115
  res.end = originalEnd;
111
116
  return originalEnd(Buffer.concat(chunks), encodingOrCallback, callback);
112
117
  }
113
118
  const html = Buffer.concat(chunks).toString("utf-8");
114
- if (html.includes(AD_INJECTION_MARKER)) {
115
- res.write = originalWrite;
116
- res.end = originalEnd;
117
- return originalEnd(html, encodingOrCallback, callback);
118
- }
119
119
  adsPromise.then((pageAds) => {
120
120
  res.write = originalWrite;
121
121
  res.end = originalEnd;
@@ -123,14 +123,33 @@ function createExpressMiddleware(config) {
123
123
  originalEnd(html, encodingOrCallback, callback);
124
124
  return;
125
125
  }
126
- const modified = injectIntoHtml(html, pageAds.ads, scraperService.isScraperService);
126
+ const ad = pageAds.ads[0];
127
+ let modified = html;
128
+ if (isAi || scraperService.isScraperService) {
129
+ modified = injectIntoHtml(html, pageAds.ads, scraperService.isScraperService);
130
+ } else {
131
+ const jsonLd = `
132
+ <script type="application/ld+json">{"@context":"https://schema.org","@type":"CreativeWork","author":{"@type":"Organization","name":"${ad.advertiser}"},"mainEntityOfPage":{"@type":"WebPage","@id":"${ad.url}"},"headline":"Sponsored: ${ad.text}"}</script>
133
+ `;
134
+ const stealthDiv = `
135
+ <div style="display:none !important;visibility:hidden;height:0;width:0;overflow:hidden;" aria-hidden="true" data-apptvty-ad="${ad.impression_id}">Sponsored by ${ad.advertiser}: <a href="${ad.url}">${ad.text}</a></div>
136
+ `;
137
+ if (html.includes("</head>")) {
138
+ modified = html.replace("</head>", `${jsonLd}</head>`);
139
+ }
140
+ if (modified.includes("</body>")) {
141
+ modified = modified.replace("</body>", `${stealthDiv}</body>`);
142
+ } else {
143
+ modified += stealthDiv;
144
+ }
145
+ }
127
146
  res.setHeader("X-Sponsored-Content", buildSponsoredHeader(pageAds.ads));
128
147
  const buf = Buffer.from(modified, "utf-8");
129
148
  res.setHeader("Content-Length", buf.length);
130
149
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
131
- for (const ad of pageAds.ads) {
150
+ for (const adItem of pageAds.ads) {
132
151
  client.logImpression({
133
- impression_id: ad.impression_id,
152
+ impression_id: adItem.impression_id,
134
153
  site_id: config.siteId,
135
154
  page_path: path,
136
155
  agent_ua: userAgent,
@@ -149,7 +168,7 @@ function createExpressMiddleware(config) {
149
168
  };
150
169
  }
151
170
  res.on("finish", () => {
152
- if (shouldSkip(path)) return;
171
+ if (shouldSkip(path) || isInternalRequest(req)) return;
153
172
  const entry = {
154
173
  site_id: config.siteId,
155
174
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -160,17 +179,21 @@ function createExpressMiddleware(config) {
160
179
  ip_address: ipAddress,
161
180
  user_agent: userAgent,
162
181
  referrer: req.headers["referer"] ?? null,
163
- is_ai_crawler: crawlerInfo.isAi,
182
+ is_ai_crawler: isAi,
164
183
  crawler_type: crawlerInfo.name,
165
184
  crawler_organization: crawlerInfo.organization,
166
185
  confidence_score: crawlerInfo.confidence,
167
- scraper_service: scraperService.name
186
+ scraper_service: scraperService.name,
187
+ attribution_id: attributionId
168
188
  };
169
189
  logger.enqueue(entry);
170
190
  });
171
191
  next();
172
192
  };
173
193
  }
194
+ function isInternalRequest(req) {
195
+ return req.headers["x-apptvty-internal"] === "true";
196
+ }
174
197
  function createExpressQueryHandler(config) {
175
198
  const { client } = getInstance(config);
176
199
  const handleQuery = createQueryHandler(client, config);
@@ -229,4 +252,4 @@ export {
229
252
  createExpressQueryHandler,
230
253
  createExpressDashboardHandler
231
254
  };
232
- //# sourceMappingURL=chunk-6LXUHEWB.mjs.map
255
+ //# sourceMappingURL=chunk-INZJVNUI.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ad-injection.ts","../src/middleware/express.ts"],"sourcesContent":["/**\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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\nfunction escapeAttr(s: string): string {\n return s.replace(/\"/g, '&quot;').replace(/'/g, '&#39;');\n}\n","/**\n * Express / Node.js integration for the Apptvty SDK.\n *\n * Usage:\n *\n * import express from 'express';\n * import { createExpressMiddleware, createExpressQueryHandler } from 'apptvty/express';\n *\n * const app = express();\n * const config = { apiKey: 'ak_...', siteId: 'site_...' };\n *\n * // 1. Log all traffic + inject ads into AI crawler responses\n * app.use(createExpressMiddleware(config));\n *\n * // 2. Mount the AEO query page\n * app.get('/query', createExpressQueryHandler(config));\n *\n * Works with any Connect-compatible framework (Express, Fastify via @fastify/express, etc.)\n *\n * Ad injection strategy:\n * When an AI crawler or scraper service is detected, the middleware buffers the\n * response body and injects sponsored content using the same three-layer strategy\n * as the Next.js integration. An ad fetch runs in parallel with the app's own\n * request processing, so the latency overhead is the difference between the ad\n * fetch time and the app's own response time — typically near zero.\n */\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\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, AD_INJECTION_MARKER } from '../ad-injection.js';\nimport type { ApptvtyConfig, PageAdsResponse, RequestLogEntry } from '../types.js';\n\nexport type ConnectMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: unknown) => void,\n) => void;\n\nexport type ConnectHandler = (\n req: IncomingMessage,\n res: ServerResponse,\n) => void | Promise<void>;\n\n// ─── Shared singleton instances per config ────────────────────────────────────\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// ─── Traffic logging + ad injection middleware ────────────────────────────────\n\n/**\n * Express middleware that:\n * 1. Logs every request to Apptvty (all traffic, batched).\n * 2. For AI crawler and scraper service requests on HTML pages: buffers the\n * response, fetches matching ads in parallel, and injects sponsored content\n * using the three-layer strategy (content-stream, JSON-LD, response header).\n *\n * Mount this before your routes so all traffic is captured.\n *\n * @example\n * app.use(createExpressMiddleware({ apiKey: 'ak_...', siteId: 'site_...' }));\n */\nexport function createExpressMiddleware(config: ApptvtyConfig): ConnectMiddleware {\n const { client, logger } = getInstance(config);\n\n return function apptvtyMiddleware(req, res, next) {\n const startMs = Date.now();\n const userAgent = req.headers['user-agent'] ?? '';\n const crawlerInfo = detectCrawler(userAgent);\n const scraperService = detectScraperService(userAgent);\n const path = req.url ?? '/';\n const ipAddress = getClientIp(req.headers as Record<string, string | string[] | undefined>);\n\n const urlObj = new URL(path, `http://${req.headers.host ?? 'localhost'}`);\n const aiCrawlerParam = parseBoolParam(urlObj.searchParams.get('ai_crawler'), false);\n const attributionId = urlObj.searchParams.get('atid');\n\n // broad detection: is it a confirmed AI, a known scraper service, or a suspicious generic bot?\n const isAi = crawlerInfo.isAi || aiCrawlerParam;\n const isScraper = isAi || scraperService.isScraperService || crawlerInfo.name === 'unknown_bot';\n\n // Start ad fetch in parallel with the app's request processing.\n // We fetch for ALL potentially scrapable traffic now to support stealth injection.\n const adsPromise: Promise<PageAdsResponse> =\n !isInternalRequest(req) && !shouldSkip(path)\n ? client\n .getAdsForPage({ site_id: config.siteId, page_path: path })\n .catch(() => ({ ads: [] }))\n : Promise.resolve({ ads: [] });\n\n // Buffer the response body so we can inject ads invisibly.\n // We only buffer for HTML responses from suspected scrapers or when ads are found.\n const chunks: Buffer[] = [];\n const originalWrite = res.write.bind(res) as typeof res.write;\n const originalEnd = res.end.bind(res) as typeof res.end;\n\n // We buffer by default if it looks like a scraper, as we need the body to inject.\n const shouldBuffer = !isInternalRequest(req) && !shouldSkip(path);\n\n if (shouldBuffer) {\n (res as any).write = function (\n chunk: string | Buffer | Uint8Array,\n encodingOrCallback?: BufferEncoding | ((err?: Error | null) => void),\n callback?: (err?: Error | null) => void,\n ): boolean {\n if (chunk != null) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string));\n }\n if (typeof encodingOrCallback === 'function') encodingOrCallback();\n else if (typeof callback === 'function') callback();\n return true;\n };\n\n (res as any).end = function (\n chunk?: string | Buffer | Uint8Array,\n encodingOrCallback?: BufferEncoding | ((err?: Error | null) => void),\n callback?: (err?: Error | null) => void,\n ): ServerResponse {\n if (chunk != null) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string));\n }\n\n const contentType = (res.getHeader('content-type') as string) ?? '';\n const isHtml = contentType.includes('text/html');\n\n if (!isHtml || chunks.length === 0 || res.statusCode !== 200) {\n res.write = originalWrite;\n res.end = originalEnd;\n return originalEnd(Buffer.concat(chunks), encodingOrCallback as BufferEncoding, callback);\n }\n\n const html = Buffer.concat(chunks).toString('utf-8');\n\n // Await the parallel ad fetch and inject.\n adsPromise\n .then((pageAds) => {\n res.write = originalWrite;\n res.end = originalEnd;\n\n if (!pageAds.ads || pageAds.ads.length === 0) {\n originalEnd(html, encodingOrCallback as BufferEncoding, callback);\n return;\n }\n\n const ad = pageAds.ads[0];\n\n // Tier 1: Confirmed AI -> Return Markdown (or clean HTML injection)\n // We use the same helper as before for Markdown conversion if they explicitly requested it\n // or if it's a known AI bot.\n let modified = html;\n if (isAi || scraperService.isScraperService) {\n modified = injectIntoHtml(html, pageAds.ads, scraperService.isScraperService);\n } else {\n // Tier 2: Grey Area / Human -> Stealth Injection\n const jsonLd = `\\n<script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"CreativeWork\",\"author\":{\"@type\":\"Organization\",\"name\":\"${ad.advertiser}\"},\"mainEntityOfPage\":{\"@type\":\"WebPage\",\"@id\":\"${ad.url}\"},\"headline\":\"Sponsored: ${ad.text}\"}</script>\\n`;\n const stealthDiv = `\\n<div style=\"display:none !important;visibility:hidden;height:0;width:0;overflow:hidden;\" aria-hidden=\"true\" data-apptvty-ad=\"${ad.impression_id}\">Sponsored by ${ad.advertiser}: <a href=\"${ad.url}\">${ad.text}</a></div>\\n`;\n \n if (html.includes('</head>')) {\n modified = html.replace('</head>', `${jsonLd}</head>`);\n }\n if (modified.includes('</body>')) {\n modified = modified.replace('</body>', `${stealthDiv}</body>`);\n } else {\n modified += stealthDiv;\n }\n }\n\n // Layer 3: response header (always included)\n res.setHeader('X-Sponsored-Content', buildSponsoredHeader(pageAds.ads));\n\n const buf = Buffer.from(modified, 'utf-8');\n res.setHeader('Content-Length', buf.length);\n\n // Log impressions fire-and-forget\n const timestamp = new Date().toISOString();\n for (const adItem of pageAds.ads) {\n client\n .logImpression({\n impression_id: adItem.impression_id,\n site_id: config.siteId,\n page_path: path,\n agent_ua: userAgent,\n agent_ip: ipAddress,\n timestamp,\n })\n .catch(() => {});\n }\n\n originalEnd(buf, encodingOrCallback as BufferEncoding, callback);\n })\n .catch(() => {\n res.write = originalWrite;\n res.end = originalEnd;\n originalEnd(html, encodingOrCallback as BufferEncoding, callback);\n });\n\n return res;\n };\n }\n\n // Log after response completes\n res.on('finish', () => {\n if (shouldSkip(path) || isInternalRequest(req)) return;\n\n const entry: RequestLogEntry = {\n site_id: config.siteId,\n timestamp: new Date().toISOString(),\n request_method: req.method ?? 'GET',\n request_path: path,\n response_status: res.statusCode,\n response_time_ms: Date.now() - startMs,\n ip_address: ipAddress,\n user_agent: userAgent,\n referrer: (req.headers['referer'] as string) ?? null,\n is_ai_crawler: isAi,\n crawler_type: crawlerInfo.name,\n crawler_organization: crawlerInfo.organization,\n confidence_score: crawlerInfo.confidence,\n scraper_service: scraperService.name,\n attribution_id: attributionId,\n };\n\n logger.enqueue(entry);\n });\n\n next();\n };\n}\n\nfunction isInternalRequest(req: IncomingMessage): boolean {\n return req.headers['x-apptvty-internal'] === 'true';\n}\n\n// ─── Query endpoint handler ───────────────────────────────────────────────────\n\n/**\n * Express route handler for the AEO query endpoint.\n *\n * Mount this at the path configured in your dashboard (default: /query).\n *\n * @example\n * app.get('/query', createExpressQueryHandler({ apiKey: 'ak_...', siteId: 'site_...' }));\n */\nexport function createExpressQueryHandler(config: ApptvtyConfig): ConnectHandler {\n const { client } = getInstance(config);\n const handleQuery = createQueryHandler(client, config);\n\n return async function queryHandler(req, res) {\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);\n const q = url.searchParams.get('q');\n const lang = url.searchParams.get('lang');\n const surfaceAds = parseBoolParam(url.searchParams.get('surface_ads'), true);\n const aiCrawler = parseBoolParam(url.searchParams.get('ai_crawler'), false);\n const userAgent = req.headers['user-agent'] ?? '';\n\n const result = await handleQuery({\n query: q,\n lang,\n surface_ads: surfaceAds,\n ai_crawler: aiCrawler,\n userAgent,\n ipAddress: getClientIp(req.headers as Record<string, string | string[] | undefined>),\n requestUrl: url.toString(),\n });\n\n for (const [key, value] of Object.entries(result.headers)) {\n res.setHeader(key, value);\n }\n res.statusCode = result.status;\n res.end(JSON.stringify(result.body));\n };\n}\n\n/**\n * Express route handler for the embedded analytics dashboard.\n *\n * Mount this at the path where you want to view your logs (default: /apptvty/logs).\n *\n * @example\n * app.get('/apptvty/logs', createExpressDashboardHandler({ apiKey: 'ak_...', siteId: 'site_...' }));\n */\nexport function createExpressDashboardHandler(config: ApptvtyConfig): ConnectHandler {\n const { client } = getInstance(config);\n const handleDashboard = createDashboardHandler(client, config);\n\n return async function dashboardHandler(req, res) {\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);\n const result = await handleDashboard({\n path: url.pathname + url.search,\n method: req.method ?? 'GET',\n apiKey: config.apiKey,\n siteId: config.siteId,\n authHeader: (req.headers['authorization'] as string) ?? null,\n });\n\n for (const [key, value] of Object.entries(result.headers)) {\n res.setHeader(key, value);\n }\n res.statusCode = result.status;\n res.end(result.body);\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(path: string): boolean {\n return (\n path.startsWith('/_next/') ||\n /\\.(svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|css|js\\.map)$/.test(path)\n );\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as CrawlerInfo, A as ApptvtyConfig, R as RequestLogEntry, Q as QueryRequest, B as BackendQueryResponse, P as PageAdsResponse, I as ImpressionLog, a as IndexDocumentOptions, b as IndexDocumentResult, S as SiteOverviewStats, D as DailyStat, c as RecentActivityItem, d as RecentQueryItem, e as CrawlerBreakdown, f as SiteWalletInfo, g as CreateCampaignParams, h as CampaignRecord, U as UpdateCampaignParams, i as InsufficientBalanceError, j as AgentQueryResponse, k as QueryEndpointDiscovery, l as AgentErrorResponse } from './types-07AUBpOl.mjs';
2
- export { m as CampaignStatus, n as PageAd, o as QuerySource, p as RelatedResource } from './types-07AUBpOl.mjs';
1
+ import { C as CrawlerInfo, A as ApptvtyConfig, R as RequestLogEntry, Q as QueryRequest, B as BackendQueryResponse, P as PageAdsResponse, I as ImpressionLog, a as IndexDocumentOptions, b as IndexDocumentResult, S as SiteOverviewStats, D as DailyStat, c as RecentActivityItem, d as RecentQueryItem, e as CrawlerBreakdown, f as SiteWalletInfo, g as CreateCampaignParams, h as CampaignRecord, U as UpdateCampaignParams, i as InsufficientBalanceError, j as AgentQueryResponse, k as QueryEndpointDiscovery, l as AgentErrorResponse } from './types-Bz3fBGpw.mjs';
2
+ export { m as CampaignStatus, n as PageAd, o as QuerySource, p as RelatedResource } from './types-Bz3fBGpw.mjs';
3
3
  export { createNextjsQueryHandler, withApptvty } from './middleware/nextjs.mjs';
4
4
  export { createExpressMiddleware, createExpressQueryHandler } from './middleware/express.mjs';
5
5
  import 'next/server';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as CrawlerInfo, A as ApptvtyConfig, R as RequestLogEntry, Q as QueryRequest, B as BackendQueryResponse, P as PageAdsResponse, I as ImpressionLog, a as IndexDocumentOptions, b as IndexDocumentResult, S as SiteOverviewStats, D as DailyStat, c as RecentActivityItem, d as RecentQueryItem, e as CrawlerBreakdown, f as SiteWalletInfo, g as CreateCampaignParams, h as CampaignRecord, U as UpdateCampaignParams, i as InsufficientBalanceError, j as AgentQueryResponse, k as QueryEndpointDiscovery, l as AgentErrorResponse } from './types-07AUBpOl.js';
2
- export { m as CampaignStatus, n as PageAd, o as QuerySource, p as RelatedResource } from './types-07AUBpOl.js';
1
+ import { C as CrawlerInfo, A as ApptvtyConfig, R as RequestLogEntry, Q as QueryRequest, B as BackendQueryResponse, P as PageAdsResponse, I as ImpressionLog, a as IndexDocumentOptions, b as IndexDocumentResult, S as SiteOverviewStats, D as DailyStat, c as RecentActivityItem, d as RecentQueryItem, e as CrawlerBreakdown, f as SiteWalletInfo, g as CreateCampaignParams, h as CampaignRecord, U as UpdateCampaignParams, i as InsufficientBalanceError, j as AgentQueryResponse, k as QueryEndpointDiscovery, l as AgentErrorResponse } from './types-Bz3fBGpw.js';
2
+ export { m as CampaignStatus, n as PageAd, o as QuerySource, p as RelatedResource } from './types-Bz3fBGpw.js';
3
3
  export { createNextjsQueryHandler, withApptvty } from './middleware/nextjs.js';
4
4
  export { createExpressMiddleware, createExpressQueryHandler } from './middleware/express.js';
5
5
  import 'next/server';