apptvty 0.3.0 → 0.3.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 @@
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 const isCrawler = crawlerInfo.isAi || aiCrawlerParam || scraperService.isScraperService;\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: crawlerInfo.isAi,\n crawler_type: crawlerInfo.name,\n crawler_organization: crawlerInfo.organization,\n confidence_score: crawlerInfo.confidence,\n scraper_service: scraperService.name,\n };\n\n const isInternalRequest = request.headers.get('x-apptvty-internal') === 'true';\n\n if (!isInternalRequest && !pathname.startsWith(queryPath)) {\n logger.enqueue(entry);\n \n // Ensure logs are flushed before Vercel Edge kills the function\n if (event && typeof event.waitUntil === 'function') {\n event.waitUntil(logger.flush());\n }\n }\n\n // Next.js App Router Native Markdown Translation Proxy\n if (isCrawler && !isInternalRequest && !pathname.startsWith(queryPath)) {\n try {\n const proxyReq = new Request(request.url, {\n headers: new Headers(request.headers)\n });\n proxyReq.headers.set('x-apptvty-internal', 'true');\n \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\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 markdown += `\\n\\n---\\n> **Sponsored:** [${ad.text}](${ad.url}) - ${ad.advertiser}\\n`;\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\n return new NextResponse(markdown, {\n status: res.status,\n headers: {\n 'Content-Type': 'text/markdown',\n 'X-Apptvty-AEO': 'true'\n }\n });\n }\n \n // If it isn't returning HTML natively, passthrough\n return res as NextResponse;\n } catch (err) {\n if (config.debug) console.warn('[apptvty] Markdown proxy 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"]}
package/dist/cli.js CHANGED
@@ -91,7 +91,8 @@ var args = process.argv.slice(2);
91
91
  var rawCmd = args[0];
92
92
  var isMigrate = rawCmd === "migrate";
93
93
  var isCreateSuperuser = rawCmd === "create-superuser";
94
- var cmdArgs = isMigrate || isCreateSuperuser ? args.slice(1) : args;
94
+ var isPush = rawCmd === "push";
95
+ var cmdArgs = isMigrate || isCreateSuperuser || isPush ? args.slice(1) : args;
95
96
  function getFlag(name) {
96
97
  const flag = `--${name}`;
97
98
  const idx = args.indexOf(flag);
@@ -434,6 +435,67 @@ async function runMigrate() {
434
435
  process.exit(1);
435
436
  }
436
437
  }
438
+ async function runPush() {
439
+ const { siteId, apiKey } = loadEnvVars();
440
+ if (!siteId || !apiKey) {
441
+ console.error("\n \u2717 APPTVTY_SITE_ID and APPTVTY_API_KEY are required. Run `npx apptvty init` first or set them in .env.\n");
442
+ process.exit(1);
443
+ }
444
+ const filePath = cmdArgs[0];
445
+ if (!filePath || filePath.startsWith("--")) {
446
+ console.error("\n \u2717 File path is required. Usage: npx apptvty push <file.md>\n");
447
+ process.exit(1);
448
+ }
449
+ const urlFlag = getFlag("url");
450
+ let content = "";
451
+ try {
452
+ content = (0, import_fs.readFileSync)((0, import_path.join)(process.cwd(), filePath), "utf-8");
453
+ } catch (err) {
454
+ console.error(`
455
+ \u2717 Failed to read file ${filePath}: ${err}
456
+ `);
457
+ process.exit(1);
458
+ }
459
+ console.log(`
460
+ apptvty \u2014 pushing knowledge base entry
461
+ `);
462
+ process.stdout.write(` Pushing ${filePath}...`);
463
+ const apiUrl = flagApiUrl || "https://api.apptvty.com";
464
+ try {
465
+ const response = await fetch(`${apiUrl}/v1/sites/${siteId}/knowledge`, {
466
+ method: "POST",
467
+ headers: {
468
+ "Authorization": `Bearer ${apiKey}`,
469
+ "Content-Type": "application/json"
470
+ },
471
+ body: JSON.stringify({
472
+ title: filePath.split("/").pop(),
473
+ content,
474
+ url: urlFlag || `manual-upload://${filePath}`
475
+ })
476
+ });
477
+ const result = await response.json();
478
+ if (!response.ok) {
479
+ console.error(`
480
+
481
+ \u2717 Push failed: ${result.error?.message || result.message || response.statusText}
482
+ `);
483
+ process.exit(1);
484
+ }
485
+ console.log(" done\n");
486
+ console.log(` \u2713 Successfully embedded ${result.chunksEmbedded || 1} chunks into vector space.`);
487
+ if (result.chunksSkipped) {
488
+ console.log(` \u2713 Skipped ${result.chunksSkipped} duplicate chunks.`);
489
+ }
490
+ console.log("\n Content is immediately available for /query agents.\n");
491
+ } catch (err) {
492
+ console.error(`
493
+
494
+ \u2717 Network error: ${err}
495
+ `);
496
+ process.exit(1);
497
+ }
498
+ }
437
499
  async function main() {
438
500
  if (args.includes("--help") || args.includes("-h")) {
439
501
  console.log(`
@@ -442,6 +504,7 @@ async function main() {
442
504
  Usage:
443
505
  npx apptvty init Register site and scaffold integration files
444
506
  npx apptvty migrate Trigger immediate re-crawl/reindex of your site
507
+ npx apptvty push Push a local markdown/text file into the vector DB
445
508
 
446
509
  Options (init):
447
510
  --domain <domain> Your site's domain (e.g. mysite.com)
@@ -462,6 +525,8 @@ async function main() {
462
525
  await runMigrate();
463
526
  } else if (isCreateSuperuser) {
464
527
  await runCreateSuperuser();
528
+ } else if (isPush) {
529
+ await runPush();
465
530
  } else {
466
531
  await runInit();
467
532
  }
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, S as SiteOverviewStats, D as DailyStat, a as RecentActivityItem, b as RecentQueryItem, c as CrawlerBreakdown, d as SiteWalletInfo, e as CreateCampaignParams, f as CampaignRecord, U as UpdateCampaignParams, g as InsufficientBalanceError, h as AgentQueryResponse, i as QueryEndpointDiscovery, j as AgentErrorResponse } from './types-DwAWwqqD.mjs';
2
- export { k as CampaignStatus, l as PageAd, m as QuerySource, n as RelatedResource } from './types-DwAWwqqD.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-07AUBpOl.mjs';
2
+ export { m as CampaignStatus, n as PageAd, o as QuerySource, p as RelatedResource } from './types-07AUBpOl.mjs';
3
3
  export { createNextjsQueryHandler, withApptvty } from './middleware/nextjs.mjs';
4
4
  export { createExpressMiddleware, createExpressQueryHandler } from './middleware/express.mjs';
5
5
  import 'next/server';
@@ -76,6 +76,20 @@ declare class ApptvtyClient {
76
76
  * breaking the query response.
77
77
  */
78
78
  logImpression(impression: ImpressionLog): Promise<void>;
79
+ /**
80
+ * Manually push text or markdown directly into your site's RAG vector database.
81
+ * This instantly embeds the content for AI agents to query, bypassing the
82
+ * need for a scheduled HTML crawl. This is ideal for Client-Side Rendered (CSR)
83
+ * Apps or for injecting private institutional facts/databases.
84
+ *
85
+ * @example
86
+ * const result = await client.pushKnowledge({
87
+ * title: "Capitol Technology University Profile",
88
+ * content: "Here are all the detailed specs, majors, and acceptances...",
89
+ * url: "https://thecollegeradar.com/institution/499" // Optional reference
90
+ * });
91
+ */
92
+ pushKnowledge(params: Omit<IndexDocumentOptions, 'apiKey' | 'siteId' | 'apiUrl'>): Promise<IndexDocumentResult>;
79
93
  /** Get 30-day traffic overview (requests, AI %, crawlers, queries). */
80
94
  getSiteStats(): Promise<SiteOverviewStats>;
81
95
  /** Get day-by-day stats (default 30 days, max 90). */
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, S as SiteOverviewStats, D as DailyStat, a as RecentActivityItem, b as RecentQueryItem, c as CrawlerBreakdown, d as SiteWalletInfo, e as CreateCampaignParams, f as CampaignRecord, U as UpdateCampaignParams, g as InsufficientBalanceError, h as AgentQueryResponse, i as QueryEndpointDiscovery, j as AgentErrorResponse } from './types-DwAWwqqD.js';
2
- export { k as CampaignStatus, l as PageAd, m as QuerySource, n as RelatedResource } from './types-DwAWwqqD.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-07AUBpOl.js';
2
+ export { m as CampaignStatus, n as PageAd, o as QuerySource, p as RelatedResource } from './types-07AUBpOl.js';
3
3
  export { createNextjsQueryHandler, withApptvty } from './middleware/nextjs.js';
4
4
  export { createExpressMiddleware, createExpressQueryHandler } from './middleware/express.js';
5
5
  import 'next/server';
@@ -76,6 +76,20 @@ declare class ApptvtyClient {
76
76
  * breaking the query response.
77
77
  */
78
78
  logImpression(impression: ImpressionLog): Promise<void>;
79
+ /**
80
+ * Manually push text or markdown directly into your site's RAG vector database.
81
+ * This instantly embeds the content for AI agents to query, bypassing the
82
+ * need for a scheduled HTML crawl. This is ideal for Client-Side Rendered (CSR)
83
+ * Apps or for injecting private institutional facts/databases.
84
+ *
85
+ * @example
86
+ * const result = await client.pushKnowledge({
87
+ * title: "Capitol Technology University Profile",
88
+ * content: "Here are all the detailed specs, majors, and acceptances...",
89
+ * url: "https://thecollegeradar.com/institution/499" // Optional reference
90
+ * });
91
+ */
92
+ pushKnowledge(params: Omit<IndexDocumentOptions, 'apiKey' | 'siteId' | 'apiUrl'>): Promise<IndexDocumentResult>;
79
93
  /** Get 30-day traffic overview (requests, AI %, crawlers, queries). */
80
94
  getSiteStats(): Promise<SiteOverviewStats>;
81
95
  /** Get day-by-day stats (default 30 days, max 90). */
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -117,6 +127,28 @@ var ApptvtyClient = class {
117
127
  this.warn("Failed to log impression (billing may be delayed):", err);
118
128
  }
119
129
  }
130
+ // ─── Programmatic Indexing ───────────────────────────────────────────────────
131
+ /**
132
+ * Manually push text or markdown directly into your site's RAG vector database.
133
+ * This instantly embeds the content for AI agents to query, bypassing the
134
+ * need for a scheduled HTML crawl. This is ideal for Client-Side Rendered (CSR)
135
+ * Apps or for injecting private institutional facts/databases.
136
+ *
137
+ * @example
138
+ * const result = await client.pushKnowledge({
139
+ * title: "Capitol Technology University Profile",
140
+ * content: "Here are all the detailed specs, majors, and acceptances...",
141
+ * url: "https://thecollegeradar.com/institution/499" // Optional reference
142
+ * });
143
+ */
144
+ async pushKnowledge(params) {
145
+ const payload = {
146
+ title: params.title,
147
+ content: params.content,
148
+ url: params.url
149
+ };
150
+ return this.post(`/v1/sites/${this.siteId}/knowledge`, payload);
151
+ }
120
152
  // ─── Analytics (for coding agents) ───────────────────────────────────────────
121
153
  // These allow agents to check activity, logs, and errors without a human.
122
154
  /** Get 30-day traffic overview (requests, AI %, crawlers, queries). */
@@ -364,8 +396,12 @@ var ApptvtyClient = class {
364
396
  let dashboardUrl = (typeof process !== "undefined" ? process.env?.DASHBOARD_URL : void 0) ?? "https://dashboard.apptvty.com/login";
365
397
  try {
366
398
  const json = JSON.parse(text);
399
+ if (json?.error?.code === "INSUFFICIENT_BALANCE") {
400
+ throw new ApptvtyApiError(402, path, text);
401
+ }
367
402
  dashboardUrl = json?.error?.details?.dashboard_url ?? dashboardUrl;
368
- } catch {
403
+ } catch (e) {
404
+ if (e instanceof ApptvtyApiError) throw e;
369
405
  }
370
406
  throw new ApptvtyTrialExpiredError(dashboardUrl);
371
407
  }
@@ -773,61 +809,58 @@ function getOrigin(url) {
773
809
  // src/middleware/nextjs.ts
774
810
  var import_server = require("next/server");
775
811
 
776
- // src/ad-injection.ts
777
- var AD_INJECTION_MARKER = "<!-- apptvty-sponsored -->";
778
- function injectIntoHtml(html, ads, isScraperService) {
779
- if (!html || ads.length === 0) return html;
780
- if (html.includes(AD_INJECTION_MARKER)) return html;
781
- let modified = html;
782
- const contentBlock = buildContentStreamBlock(ads);
783
- if (modified.includes("</article>")) {
784
- modified = modified.replace("</article>", `${contentBlock}
785
- </article>`);
786
- } else if (modified.includes("</main>")) {
787
- modified = modified.replace("</main>", `${contentBlock}
788
- </main>`);
789
- } else if (!isScraperService && modified.includes("</body>")) {
790
- modified = modified.replace("</body>", `${contentBlock}
791
- </body>`);
792
- }
793
- if (!isScraperService && modified.includes("</head>")) {
794
- const jsonLdBlock = buildJsonLdBlock(ads);
795
- modified = modified.replace("</head>", `${jsonLdBlock}
796
- </head>`);
812
+ // src/markdown.ts
813
+ var cheerio = __toESM(require("cheerio"));
814
+ function convertHtmlToMarkdown(html) {
815
+ if (!html) return "";
816
+ const $ = cheerio.load(html);
817
+ $("script, style, nav, footer, header, aside, svg, .ad, .sponsor, noscript").remove();
818
+ const main = $('main, article, [role="main"], #content, .content').first();
819
+ const root = main.length ? main : $("body");
820
+ let markdown = "";
821
+ root.find("h1, h2, h3, h4, h5, h6, p, ul, ol").each((_, el) => {
822
+ const $el = $(el);
823
+ const tagName = el.tagName.toLowerCase();
824
+ if (tagName === "ul" || tagName === "ol") {
825
+ $el.find("li").each((_2, li) => {
826
+ const text2 = cleanText($(li).text());
827
+ if (text2) markdown += `- ${text2}
828
+ `;
829
+ });
830
+ markdown += "\n";
831
+ return;
832
+ }
833
+ const text = cleanText($el.text());
834
+ if (!text) return;
835
+ if (tagName === "h1") markdown += `# ${text}
836
+
837
+ `;
838
+ else if (tagName === "h2") markdown += `## ${text}
839
+
840
+ `;
841
+ else if (tagName === "h3") markdown += `### ${text}
842
+
843
+ `;
844
+ else if (tagName === "h4") markdown += `#### ${text}
845
+
846
+ `;
847
+ else if (tagName === "h5") markdown += `##### ${text}
848
+
849
+ `;
850
+ else if (tagName === "h6") markdown += `###### ${text}
851
+
852
+ `;
853
+ else if (tagName === "p") markdown += `${text}
854
+
855
+ `;
856
+ });
857
+ if (markdown.trim().length < 50) {
858
+ markdown = cleanText(root.text()) + "\n\n";
797
859
  }
798
- return modified;
860
+ return markdown.trim();
799
861
  }
800
- function buildSponsoredHeader(ads) {
801
- return JSON.stringify(
802
- ads.map((ad) => ({ text: ad.text, url: ad.url, advertiser: ad.advertiser }))
803
- );
804
- }
805
- function buildContentStreamBlock(ads) {
806
- const paragraphs = ads.map(
807
- (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>`
808
- ).join("\n");
809
- return `${AD_INJECTION_MARKER}
810
- ${paragraphs}`;
811
- }
812
- function buildJsonLdBlock(ads) {
813
- const entries = ads.map((ad) => ({
814
- "@context": "https://schema.org",
815
- "@type": "WPAdBlock",
816
- sponsor: {
817
- "@type": "Organization",
818
- name: ad.advertiser,
819
- url: ad.url
820
- },
821
- description: ad.text
822
- }));
823
- const ld = entries.length === 1 ? entries[0] : entries;
824
- return `<script type="application/ld+json">${JSON.stringify(ld)}</script>`;
825
- }
826
- function escapeHtml(s) {
827
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
828
- }
829
- function escapeAttr(s) {
830
- return s.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
862
+ function cleanText(text) {
863
+ return text.trim().replace(/\s+/g, " ");
831
864
  }
832
865
 
833
866
  // src/middleware/nextjs.ts
@@ -905,27 +938,53 @@ function withApptvty(config, next) {
905
938
  confidence_score: crawlerInfo.confidence,
906
939
  scraper_service: scraperService.name
907
940
  };
908
- logger.enqueue(entry);
909
- if (event && typeof event.waitUntil === "function") {
910
- event.waitUntil(logger.flush());
941
+ const isInternalRequest = request.headers.get("x-apptvty-internal") === "true";
942
+ if (!isInternalRequest && !pathname.startsWith(queryPath)) {
943
+ logger.enqueue(entry);
944
+ if (event && typeof event.waitUntil === "function") {
945
+ event.waitUntil(logger.flush());
946
+ }
911
947
  }
912
- if (isCrawler && response.ok && !pathname.startsWith(queryPath)) {
913
- const contentType = response.headers.get("content-type") ?? "";
914
- if (contentType.includes("text/html")) {
915
- try {
916
- const modified = await injectAdsIntoResponse(
917
- response,
918
- client,
919
- config,
920
- pathname,
921
- userAgent,
922
- getClientIp(headers),
923
- scraperService.isScraperService
924
- );
925
- if (modified) return modified;
926
- } catch (err) {
927
- if (config.debug) console.warn("[apptvty] Ad injection failed:", err);
948
+ if (isCrawler && !isInternalRequest && !pathname.startsWith(queryPath)) {
949
+ try {
950
+ const proxyReq = new Request(request.url, {
951
+ headers: new Headers(request.headers)
952
+ });
953
+ proxyReq.headers.set("x-apptvty-internal", "true");
954
+ const res = await fetch(proxyReq);
955
+ const contentType = res.headers.get("content-type") ?? "";
956
+ if (contentType.includes("text/html")) {
957
+ const html = await res.text();
958
+ let markdown = convertHtmlToMarkdown(html);
959
+ const pageAds = await client.getAdsForPage({ site_id: config.siteId, page_path: pathname });
960
+ if (pageAds.ads && pageAds.ads.length > 0) {
961
+ const ad = pageAds.ads[0];
962
+ markdown += `
963
+
964
+ ---
965
+ > **Sponsored:** [${ad.text}](${ad.url}) - ${ad.advertiser}
966
+ `;
967
+ client.logImpression({
968
+ impression_id: ad.impression_id,
969
+ site_id: config.siteId,
970
+ page_path: pathname,
971
+ agent_ua: userAgent,
972
+ agent_ip: getClientIp(headers),
973
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
974
+ }).catch(() => {
975
+ });
976
+ }
977
+ return new import_server.NextResponse(markdown, {
978
+ status: res.status,
979
+ headers: {
980
+ "Content-Type": "text/markdown",
981
+ "X-Apptvty-AEO": "true"
982
+ }
983
+ });
928
984
  }
985
+ return res;
986
+ } catch (err) {
987
+ if (config.debug) console.warn("[apptvty] Markdown proxy failed:", err);
929
988
  }
930
989
  }
931
990
  return response;
@@ -957,33 +1016,6 @@ function createNextjsQueryHandler(config) {
957
1016
  });
958
1017
  };
959
1018
  }
960
- async function injectAdsIntoResponse(response, client, config, pathname, userAgent, ipAddress, isScraperService) {
961
- const html = await response.text();
962
- if (!html) return null;
963
- const pageAds = await client.getAdsForPage({ site_id: config.siteId, page_path: pathname });
964
- if (!pageAds.ads || pageAds.ads.length === 0) return null;
965
- const modified = injectIntoHtml(html, pageAds.ads, isScraperService);
966
- if (modified === html) return null;
967
- const newHeaders = new Headers(response.headers);
968
- newHeaders.set("X-Sponsored-Content", buildSponsoredHeader(pageAds.ads));
969
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
970
- for (const ad of pageAds.ads) {
971
- client.logImpression({
972
- impression_id: ad.impression_id,
973
- site_id: config.siteId,
974
- page_path: pathname,
975
- agent_ua: userAgent,
976
- agent_ip: ipAddress,
977
- timestamp
978
- }).catch(() => {
979
- });
980
- }
981
- return new import_server.NextResponse(modified, {
982
- status: response.status,
983
- statusText: response.statusText,
984
- headers: newHeaders
985
- });
986
- }
987
1019
  function parseBoolParam(value, defaultValue) {
988
1020
  if (value === null) return defaultValue;
989
1021
  return value === "1" || value === "true" || value === "yes";
@@ -992,6 +1024,63 @@ function shouldSkip(pathname) {
992
1024
  return pathname.startsWith("/_next/") || pathname.startsWith("/api/_") || pathname === "/favicon.ico" || /\.(svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|css|js\.map)$/.test(pathname);
993
1025
  }
994
1026
 
1027
+ // src/ad-injection.ts
1028
+ var AD_INJECTION_MARKER = "<!-- apptvty-sponsored -->";
1029
+ function injectIntoHtml(html, ads, isScraperService) {
1030
+ if (!html || ads.length === 0) return html;
1031
+ if (html.includes(AD_INJECTION_MARKER)) return html;
1032
+ let modified = html;
1033
+ const contentBlock = buildContentStreamBlock(ads);
1034
+ if (modified.includes("</article>")) {
1035
+ modified = modified.replace("</article>", `${contentBlock}
1036
+ </article>`);
1037
+ } else if (modified.includes("</main>")) {
1038
+ modified = modified.replace("</main>", `${contentBlock}
1039
+ </main>`);
1040
+ } else if (!isScraperService && modified.includes("</body>")) {
1041
+ modified = modified.replace("</body>", `${contentBlock}
1042
+ </body>`);
1043
+ }
1044
+ if (!isScraperService && modified.includes("</head>")) {
1045
+ const jsonLdBlock = buildJsonLdBlock(ads);
1046
+ modified = modified.replace("</head>", `${jsonLdBlock}
1047
+ </head>`);
1048
+ }
1049
+ return modified;
1050
+ }
1051
+ function buildSponsoredHeader(ads) {
1052
+ return JSON.stringify(
1053
+ ads.map((ad) => ({ text: ad.text, url: ad.url, advertiser: ad.advertiser }))
1054
+ );
1055
+ }
1056
+ function buildContentStreamBlock(ads) {
1057
+ const paragraphs = ads.map(
1058
+ (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>`
1059
+ ).join("\n");
1060
+ return `${AD_INJECTION_MARKER}
1061
+ ${paragraphs}`;
1062
+ }
1063
+ function buildJsonLdBlock(ads) {
1064
+ const entries = ads.map((ad) => ({
1065
+ "@context": "https://schema.org",
1066
+ "@type": "WPAdBlock",
1067
+ sponsor: {
1068
+ "@type": "Organization",
1069
+ name: ad.advertiser,
1070
+ url: ad.url
1071
+ },
1072
+ description: ad.text
1073
+ }));
1074
+ const ld = entries.length === 1 ? entries[0] : entries;
1075
+ return `<script type="application/ld+json">${JSON.stringify(ld)}</script>`;
1076
+ }
1077
+ function escapeHtml(s) {
1078
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1079
+ }
1080
+ function escapeAttr(s) {
1081
+ return s.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1082
+ }
1083
+
995
1084
  // src/middleware/express.ts
996
1085
  var instances2 = /* @__PURE__ */ new Map();
997
1086
  function getInstance2(config) {