apptvty 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +537 -0
- package/dist/chunk-RGUS6IL6.mjs +87 -0
- package/dist/chunk-RGUS6IL6.mjs.map +1 -0
- package/dist/chunk-WATTAPBA.mjs +502 -0
- package/dist/chunk-WATTAPBA.mjs.map +1 -0
- package/dist/chunk-XOWRKLFM.mjs +150 -0
- package/dist/chunk-XOWRKLFM.mjs.map +1 -0
- package/dist/cli.js +321 -0
- package/dist/index.d.mts +170 -0
- package/dist/index.d.ts +170 -0
- package/dist/index.js +751 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +29 -0
- package/dist/index.mjs.map +1 -0
- package/dist/middleware/express.d.mts +46 -0
- package/dist/middleware/express.d.ts +46 -0
- package/dist/middleware/express.js +595 -0
- package/dist/middleware/express.js.map +1 -0
- package/dist/middleware/express.mjs +10 -0
- package/dist/middleware/express.mjs.map +1 -0
- package/dist/middleware/nextjs.d.mts +47 -0
- package/dist/middleware/nextjs.d.ts +47 -0
- package/dist/middleware/nextjs.js +658 -0
- package/dist/middleware/nextjs.js.map +1 -0
- package/dist/middleware/nextjs.mjs +10 -0
- package/dist/middleware/nextjs.mjs.map +1 -0
- package/dist/setup.d.mts +71 -0
- package/dist/setup.d.ts +71 -0
- package/dist/setup.js +110 -0
- package/dist/setup.js.map +1 -0
- package/dist/setup.mjs +82 -0
- package/dist/setup.mjs.map +1 -0
- package/dist/types-C1oUTCsT.d.mts +263 -0
- package/dist/types-C1oUTCsT.d.ts +263 -0
- package/package.json +82 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApptvtyClient,
|
|
3
|
+
RequestLogger,
|
|
4
|
+
createQueryHandler,
|
|
5
|
+
detectCrawler,
|
|
6
|
+
getClientIp
|
|
7
|
+
} from "./chunk-WATTAPBA.mjs";
|
|
8
|
+
|
|
9
|
+
// src/middleware/nextjs.ts
|
|
10
|
+
import { NextResponse } from "next/server";
|
|
11
|
+
function headersToRecord(h) {
|
|
12
|
+
const entries = h.entries();
|
|
13
|
+
return Object.fromEntries(Array.from(entries));
|
|
14
|
+
}
|
|
15
|
+
var instances = /* @__PURE__ */ new Map();
|
|
16
|
+
function getInstance(config) {
|
|
17
|
+
const key = config.apiKey;
|
|
18
|
+
if (!instances.has(key)) {
|
|
19
|
+
const client = new ApptvtyClient(config);
|
|
20
|
+
const logger = new RequestLogger(client, config);
|
|
21
|
+
instances.set(key, { client, logger });
|
|
22
|
+
}
|
|
23
|
+
return instances.get(key);
|
|
24
|
+
}
|
|
25
|
+
function withApptvty(config, next) {
|
|
26
|
+
const { client, logger } = getInstance(config);
|
|
27
|
+
const queryPath = config.queryPath ?? "/query";
|
|
28
|
+
return async function apptvtyMiddleware(request) {
|
|
29
|
+
const startMs = Date.now();
|
|
30
|
+
const userAgent = request.headers.get("user-agent") ?? "";
|
|
31
|
+
const crawlerInfo = detectCrawler(userAgent);
|
|
32
|
+
const aiCrawlerParam = parseBoolParam(request.nextUrl.searchParams.get("ai_crawler"), false);
|
|
33
|
+
const isCrawler = crawlerInfo.isAi || aiCrawlerParam;
|
|
34
|
+
let response;
|
|
35
|
+
try {
|
|
36
|
+
response = next ? await next(request) : NextResponse.next();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
const responseTimeMs = Date.now() - startMs;
|
|
41
|
+
const { pathname } = request.nextUrl;
|
|
42
|
+
if (shouldSkip(pathname)) {
|
|
43
|
+
return response;
|
|
44
|
+
}
|
|
45
|
+
const headers = headersToRecord(request.headers);
|
|
46
|
+
const entry = {
|
|
47
|
+
site_id: config.siteId,
|
|
48
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49
|
+
method: request.method,
|
|
50
|
+
path: pathname,
|
|
51
|
+
status_code: response.status,
|
|
52
|
+
response_time_ms: responseTimeMs,
|
|
53
|
+
ip_address: getClientIp(headers),
|
|
54
|
+
user_agent: userAgent,
|
|
55
|
+
referer: request.headers.get("referer"),
|
|
56
|
+
is_ai_crawler: crawlerInfo.isAi,
|
|
57
|
+
crawler_type: crawlerInfo.name,
|
|
58
|
+
crawler_organization: crawlerInfo.organization,
|
|
59
|
+
confidence_score: crawlerInfo.confidence
|
|
60
|
+
};
|
|
61
|
+
logger.enqueue(entry);
|
|
62
|
+
if (isCrawler && response.ok && !pathname.startsWith(queryPath)) {
|
|
63
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
64
|
+
if (contentType.includes("text/html")) {
|
|
65
|
+
try {
|
|
66
|
+
const modified = await injectAdsIntoHtml(response, client, config.siteId, pathname);
|
|
67
|
+
if (modified) return modified;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (config.debug) console.warn("[apptvty] Ad injection failed:", err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return response;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function createNextjsQueryHandler(config) {
|
|
77
|
+
const { client } = getInstance(config);
|
|
78
|
+
const handleQuery = createQueryHandler(client, config);
|
|
79
|
+
return async function GET(request) {
|
|
80
|
+
const { searchParams } = request.nextUrl;
|
|
81
|
+
const q = searchParams.get("q");
|
|
82
|
+
const lang = searchParams.get("lang");
|
|
83
|
+
const surfaceAds = parseBoolParam(searchParams.get("surface_ads"), true);
|
|
84
|
+
const aiCrawler = parseBoolParam(searchParams.get("ai_crawler"), false);
|
|
85
|
+
const userAgent = request.headers.get("user-agent") ?? "";
|
|
86
|
+
const headers = headersToRecord(request.headers);
|
|
87
|
+
const result = await handleQuery({
|
|
88
|
+
query: q,
|
|
89
|
+
lang,
|
|
90
|
+
surface_ads: surfaceAds,
|
|
91
|
+
ai_crawler: aiCrawler,
|
|
92
|
+
userAgent,
|
|
93
|
+
ipAddress: getClientIp(headers),
|
|
94
|
+
requestUrl: request.url
|
|
95
|
+
});
|
|
96
|
+
return NextResponse.json(result.body, {
|
|
97
|
+
status: result.status,
|
|
98
|
+
headers: result.headers
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
var AD_INJECTION_MARKER = "<!-- apptvty-sponsored -->";
|
|
103
|
+
function buildAdBlock(ads) {
|
|
104
|
+
const items = ads.map(
|
|
105
|
+
(ad) => `<li><a href="${escapeHtml(ad.url)}" rel="nofollow">${escapeHtml(ad.text)}</a> <small>\u2014 ${escapeHtml(ad.advertiser)}</small></li>`
|
|
106
|
+
).join("\n");
|
|
107
|
+
return `
|
|
108
|
+
<section aria-label="Sponsored" data-sponsored ${AD_INJECTION_MARKER}>
|
|
109
|
+
<h3>Sponsored</h3>
|
|
110
|
+
<ul>${items}</ul>
|
|
111
|
+
</section>`;
|
|
112
|
+
}
|
|
113
|
+
function escapeHtml(s) {
|
|
114
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
115
|
+
}
|
|
116
|
+
async function injectAdsIntoHtml(response, client, siteId, pathname) {
|
|
117
|
+
const html = await response.text();
|
|
118
|
+
if (!html || html.includes(AD_INJECTION_MARKER)) return null;
|
|
119
|
+
const pageAds = await client.getAdsForPage({ site_id: siteId, page_path: pathname });
|
|
120
|
+
if (!pageAds.ads || pageAds.ads.length === 0) return null;
|
|
121
|
+
const adBlock = buildAdBlock(pageAds.ads);
|
|
122
|
+
let modified;
|
|
123
|
+
if (html.includes("</body>")) {
|
|
124
|
+
modified = html.replace("</body>", `${adBlock}
|
|
125
|
+
</body>`);
|
|
126
|
+
} else if (html.includes("</html>")) {
|
|
127
|
+
modified = html.replace("</html>", `${adBlock}
|
|
128
|
+
</html>`);
|
|
129
|
+
} else {
|
|
130
|
+
modified = html + adBlock;
|
|
131
|
+
}
|
|
132
|
+
return new NextResponse(modified, {
|
|
133
|
+
status: response.status,
|
|
134
|
+
statusText: response.statusText,
|
|
135
|
+
headers: new Headers(response.headers)
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function parseBoolParam(value, defaultValue) {
|
|
139
|
+
if (value === null) return defaultValue;
|
|
140
|
+
return value === "1" || value === "true" || value === "yes";
|
|
141
|
+
}
|
|
142
|
+
function shouldSkip(pathname) {
|
|
143
|
+
return pathname.startsWith("/_next/") || pathname.startsWith("/api/_") || pathname === "/favicon.ico" || /\.(svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|css|js\.map)$/.test(pathname);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export {
|
|
147
|
+
withApptvty,
|
|
148
|
+
createNextjsQueryHandler
|
|
149
|
+
};
|
|
150
|
+
//# sourceMappingURL=chunk-XOWRKLFM.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/middleware/nextjs.ts"],"sourcesContent":["/**\n * Next.js App Router integration for the Apptvty SDK.\n *\n * Usage — two pieces:\n *\n * 1. Wrap your existing middleware.ts (or use ours standalone):\n *\n * import { withApptvty } from 'apptvty/nextjs';\n * export default withApptvty({ apiKey: 'ak_...', siteId: 'site_...' });\n * // or wrap your existing middleware:\n * export default withApptvty(config, yourMiddleware);\n *\n * 2. Create app/query/route.ts for the AEO query endpoint:\n *\n * import { createNextjsQueryHandler } from 'apptvty/nextjs';\n * export const GET = createNextjsQueryHandler({ apiKey: 'ak_...', siteId: 'site_...' });\n *\n * The SDK logs all requests (especially agentic traffic) to Apptvty\n * and serves structured AI answers + optional sponsored ads on /query.\n */\n\nimport type { NextRequest } from 'next/server';\nimport { NextResponse } from 'next/server';\nimport { ApptvtyClient } from '../client.js';\nimport { detectCrawler } from '../crawler.js';\nimport { RequestLogger, getClientIp } from '../logger.js';\nimport { createQueryHandler } from '../query-handler.js';\nimport type { ApptvtyConfig, RequestLogEntry } from '../types.js';\n\n/** Convert Next request headers to a plain object (Web API Headers.entries() at runtime). */\nfunction headersToRecord(h: NextRequest['headers']): Record<string, string> {\n const entries = (h as unknown as { entries(): IterableIterator<[string, string]> }).entries();\n return Object.fromEntries(Array.from(entries));\n}\n\n// ─── Shared singleton instances per config (keyed by apiKey) ─────────────────\n\nconst instances = new Map<string, { client: ApptvtyClient; logger: RequestLogger }>();\n\nfunction getInstance(config: ApptvtyConfig) {\n const key = config.apiKey;\n if (!instances.has(key)) {\n const client = new ApptvtyClient(config);\n const logger = new RequestLogger(client, config);\n instances.set(key, { client, logger });\n }\n return instances.get(key)!;\n}\n\n// ─── Middleware wrapper ───────────────────────────────────────────────────────\n\ntype NextMiddleware = (request: NextRequest) => Response | NextResponse | Promise<Response | NextResponse>;\n\n/**\n * Wraps a Next.js middleware function (or creates a passthrough) with\n * Apptvty request logging. All requests passing through middleware are\n * logged to Apptvty, with AI crawlers automatically classified.\n *\n * @example\n * // middleware.ts\n * import { withApptvty } from 'apptvty/nextjs';\n * export default withApptvty({ apiKey: 'ak_...', siteId: 'site_...' });\n */\nexport function withApptvty(\n config: ApptvtyConfig,\n next?: NextMiddleware,\n): NextMiddleware {\n const { client, logger } = getInstance(config);\n const queryPath = config.queryPath ?? '/query';\n\n return async function apptvtyMiddleware(request: NextRequest): Promise<NextResponse> {\n const startMs = Date.now();\n const userAgent = request.headers.get('user-agent') ?? '';\n const crawlerInfo = detectCrawler(userAgent);\n const aiCrawlerParam = parseBoolParam(request.nextUrl.searchParams.get('ai_crawler'), false);\n const isCrawler = crawlerInfo.isAi || aiCrawlerParam;\n\n // Run the user's middleware (or passthrough)\n let response: Response | NextResponse;\n try {\n response = next ? await next(request) : NextResponse.next();\n } catch (err) {\n // Don't swallow application errors\n throw err;\n }\n\n const responseTimeMs = Date.now() - startMs;\n\n // Skip logging for Next.js internals and static assets\n const { pathname } = request.nextUrl;\n if (shouldSkip(pathname)) {\n return response as NextResponse;\n }\n\n const headers = headersToRecord(request.headers);\n const entry: RequestLogEntry = {\n site_id: config.siteId,\n timestamp: new Date().toISOString(),\n method: request.method,\n path: pathname,\n status_code: response.status,\n response_time_ms: responseTimeMs,\n ip_address: getClientIp(headers),\n user_agent: userAgent,\n referer: request.headers.get('referer'),\n is_ai_crawler: crawlerInfo.isAi,\n crawler_type: crawlerInfo.name,\n crawler_organization: crawlerInfo.organization,\n confidence_score: crawlerInfo.confidence,\n };\n\n logger.enqueue(entry);\n\n // Ad injection: when crawler hits an HTML page, inject sponsored ads\n if (isCrawler && response.ok && !pathname.startsWith(queryPath)) {\n const contentType = response.headers.get('content-type') ?? '';\n if (contentType.includes('text/html')) {\n try {\n const modified = await injectAdsIntoHtml(response, client, config.siteId, pathname);\n if (modified) return modified;\n } catch (err) {\n // Never break the response for ad injection failures\n if (config.debug) console.warn('[apptvty] Ad injection failed:', err);\n }\n }\n }\n\n return response as NextResponse;\n };\n}\n\n// ─── Query route handler ──────────────────────────────────────────────────────\n\n/**\n * Creates a Next.js App Router GET handler for the AEO query endpoint.\n *\n * @example\n * // app/query/route.ts\n * import { createNextjsQueryHandler } from 'apptvty/nextjs';\n * export const GET = createNextjsQueryHandler({ apiKey: 'ak_...', siteId: 'site_...' });\n */\nexport function createNextjsQueryHandler(config: ApptvtyConfig) {\n const { client } = getInstance(config);\n const handleQuery = createQueryHandler(client, config);\n\n return async function GET(request: NextRequest): Promise<NextResponse> {\n const { searchParams } = request.nextUrl;\n const q = searchParams.get('q');\n const lang = searchParams.get('lang');\n const surfaceAds = parseBoolParam(searchParams.get('surface_ads'), true);\n const aiCrawler = parseBoolParam(searchParams.get('ai_crawler'), false);\n const userAgent = request.headers.get('user-agent') ?? '';\n const headers = headersToRecord(request.headers);\n\n const result = await handleQuery({\n query: q,\n lang,\n surface_ads: surfaceAds,\n ai_crawler: aiCrawler,\n userAgent,\n ipAddress: getClientIp(headers),\n requestUrl: request.url,\n });\n\n return NextResponse.json(result.body, {\n status: result.status,\n headers: result.headers,\n });\n };\n}\n\n// ─── HTML ad injection ────────────────────────────────────────────────────────\n\nconst AD_INJECTION_MARKER = '<!-- apptvty-sponsored -->';\n\nfunction buildAdBlock(ads: Array<{ text: string; url: string; advertiser: string }>): string {\n const items = ads\n .map(\n (ad) =>\n `<li><a href=\"${escapeHtml(ad.url)}\" rel=\"nofollow\">${escapeHtml(ad.text)}</a> <small>— ${escapeHtml(ad.advertiser)}</small></li>`\n )\n .join('\\n');\n return `\n<section aria-label=\"Sponsored\" data-sponsored ${AD_INJECTION_MARKER}>\n <h3>Sponsored</h3>\n <ul>${items}</ul>\n</section>`;\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nasync function injectAdsIntoHtml(\n response: Response,\n client: ApptvtyClient,\n siteId: string,\n pathname: string\n): Promise<NextResponse | null> {\n const html = await response.text();\n if (!html || html.includes(AD_INJECTION_MARKER)) return null;\n\n const pageAds = await client.getAdsForPage({ site_id: siteId, page_path: pathname });\n if (!pageAds.ads || pageAds.ads.length === 0) return null;\n\n const adBlock = buildAdBlock(pageAds.ads);\n let modified: string;\n\n if (html.includes('</body>')) {\n modified = html.replace('</body>', `${adBlock}\\n</body>`);\n } else if (html.includes('</html>')) {\n modified = html.replace('</html>', `${adBlock}\\n</html>`);\n } else {\n modified = html + adBlock;\n }\n\n return new NextResponse(modified, {\n status: response.status,\n statusText: response.statusText,\n headers: new Headers(response.headers),\n });\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction parseBoolParam(value: string | null, defaultValue: boolean): boolean {\n if (value === null) return defaultValue;\n return value === '1' || value === 'true' || value === 'yes';\n}\n\nfunction shouldSkip(pathname: string): boolean {\n return (\n pathname.startsWith('/_next/') ||\n pathname.startsWith('/api/_') ||\n pathname === '/favicon.ico' ||\n /\\.(svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|css|js\\.map)$/.test(pathname)\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
// src/cli.ts
|
|
6
|
+
var import_readline = require("readline");
|
|
7
|
+
var import_fs = require("fs");
|
|
8
|
+
var import_path = require("path");
|
|
9
|
+
|
|
10
|
+
// src/setup.ts
|
|
11
|
+
var DEFAULT_API_URL = (typeof process !== "undefined" ? process.env?.APPTVTY_API_URL : void 0) ?? "https://api.apptvty.com";
|
|
12
|
+
var RegistrationError = class extends Error {
|
|
13
|
+
constructor(code, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.name = "RegistrationError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
async function register(options) {
|
|
20
|
+
const apiUrl = (options.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
21
|
+
const response = await fetch(`${apiUrl}/v1/register`, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: { "Content-Type": "application/json" },
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
domain: options.domain,
|
|
26
|
+
framework: options.framework ?? "other",
|
|
27
|
+
agent_id: options.agentId
|
|
28
|
+
}),
|
|
29
|
+
signal: AbortSignal.timeout(2e4)
|
|
30
|
+
});
|
|
31
|
+
const json = await response.json();
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
throw new RegistrationError(
|
|
34
|
+
json.error?.code ?? "REGISTRATION_FAILED",
|
|
35
|
+
json.error?.message ?? `Registration failed with status ${response.status}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const data = json.data ?? json;
|
|
39
|
+
return {
|
|
40
|
+
siteId: data.site_id,
|
|
41
|
+
apiKey: data.api_key,
|
|
42
|
+
companyId: data.company_id,
|
|
43
|
+
walletAddress: data.wallet_address ?? null,
|
|
44
|
+
dashboardUrl: data.dashboard_url,
|
|
45
|
+
claimTokenExpiresAt: data.claim_token_expires_at,
|
|
46
|
+
trialEndsAt: data.trial_ends_at,
|
|
47
|
+
setup: {
|
|
48
|
+
envVars: data.setup?.env_vars ?? {
|
|
49
|
+
APPTVTY_SITE_ID: data.site_id,
|
|
50
|
+
APPTVTY_API_KEY: data.api_key
|
|
51
|
+
},
|
|
52
|
+
files: data.setup?.files
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
var MigrateError = class extends Error {
|
|
57
|
+
constructor(code, message) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.code = code;
|
|
60
|
+
this.name = "MigrateError";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
async function migrate(options) {
|
|
64
|
+
const apiUrl = (options.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
65
|
+
const response = await fetch(`${apiUrl}/v1/sites/${options.siteId}/reindex`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
69
|
+
"Content-Type": "application/json"
|
|
70
|
+
},
|
|
71
|
+
signal: AbortSignal.timeout(2e4)
|
|
72
|
+
});
|
|
73
|
+
const json = await response.json();
|
|
74
|
+
const data = json.data ?? json;
|
|
75
|
+
const err = json.error;
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new MigrateError(err?.code ?? "MIGRATE_FAILED", err?.message ?? `Migrate failed with status ${response.status}`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
message: data.message ?? "Reindex started.",
|
|
81
|
+
siteId: data.siteId ?? options.siteId,
|
|
82
|
+
domain: data.domain ?? ""
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/cli.ts
|
|
87
|
+
var args = process.argv.slice(2);
|
|
88
|
+
var rawCmd = args[0];
|
|
89
|
+
var isMigrate = rawCmd === "migrate";
|
|
90
|
+
var cmdArgs = isMigrate ? args.slice(1) : args;
|
|
91
|
+
function getFlag(name, from = cmdArgs) {
|
|
92
|
+
const idx = from.indexOf(`--${name}`);
|
|
93
|
+
return idx !== -1 ? from[idx + 1] : void 0;
|
|
94
|
+
}
|
|
95
|
+
var isNonInteractive = cmdArgs.includes("--non-interactive") || cmdArgs.includes("--json");
|
|
96
|
+
var flagDomain = getFlag("domain");
|
|
97
|
+
var flagFramework = getFlag("framework");
|
|
98
|
+
var flagApiUrl = getFlag("api-url");
|
|
99
|
+
function detectFramework() {
|
|
100
|
+
try {
|
|
101
|
+
const pkg = JSON.parse((0, import_fs.readFileSync)((0, import_path.join)(process.cwd(), "package.json"), "utf-8"));
|
|
102
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
103
|
+
if (deps.next) return "nextjs";
|
|
104
|
+
if (deps.express) return "express";
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
return "other";
|
|
108
|
+
}
|
|
109
|
+
function detectDomain() {
|
|
110
|
+
try {
|
|
111
|
+
const pkg = JSON.parse((0, import_fs.readFileSync)((0, import_path.join)(process.cwd(), "package.json"), "utf-8"));
|
|
112
|
+
if (pkg.homepage) {
|
|
113
|
+
try {
|
|
114
|
+
return new URL(pkg.homepage).hostname;
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
function prompt(question) {
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
125
|
+
rl.question(question, (answer) => {
|
|
126
|
+
rl.close();
|
|
127
|
+
resolve(answer.trim());
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function writeFile(relativePath, content, overwrite = false) {
|
|
132
|
+
const fullPath = (0, import_path.join)(process.cwd(), relativePath);
|
|
133
|
+
if ((0, import_fs.existsSync)(fullPath) && !overwrite) return false;
|
|
134
|
+
(0, import_fs.mkdirSync)((0, import_path.dirname)(fullPath), { recursive: true });
|
|
135
|
+
(0, import_fs.writeFileSync)(fullPath, content, "utf-8");
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
function appendEnvFile(path, vars) {
|
|
139
|
+
const fullPath = (0, import_path.join)(process.cwd(), path);
|
|
140
|
+
const lines = Object.entries(vars).map(([k, v]) => `${k}=${v}`).join("\n");
|
|
141
|
+
let existing = "";
|
|
142
|
+
try {
|
|
143
|
+
existing = (0, import_fs.readFileSync)(fullPath, "utf-8");
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
const toAdd = Object.entries(vars).filter(([k]) => !existing.includes(k)).map(([k, v]) => `${k}=${v}`).join("\n");
|
|
147
|
+
if (!toAdd) return;
|
|
148
|
+
(0, import_fs.writeFileSync)(fullPath, existing ? `${existing}
|
|
149
|
+
${toAdd}
|
|
150
|
+
` : `${toAdd}
|
|
151
|
+
`, "utf-8");
|
|
152
|
+
}
|
|
153
|
+
function loadEnvVars() {
|
|
154
|
+
const envFiles = [".env.local", ".env"];
|
|
155
|
+
let content = "";
|
|
156
|
+
for (const f of envFiles) {
|
|
157
|
+
try {
|
|
158
|
+
content = (0, import_fs.readFileSync)((0, import_path.join)(process.cwd(), f), "utf-8");
|
|
159
|
+
break;
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const out = {};
|
|
164
|
+
for (const line of content.split("\n")) {
|
|
165
|
+
const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.+?)\s*$/);
|
|
166
|
+
if (m) out[m[1]] = m[2].replace(/^["']|["']$/g, "").trim();
|
|
167
|
+
}
|
|
168
|
+
return { siteId: out.APPTVTY_SITE_ID, apiKey: out.APPTVTY_API_KEY };
|
|
169
|
+
}
|
|
170
|
+
async function runInit() {
|
|
171
|
+
const framework = flagFramework ?? detectFramework();
|
|
172
|
+
const detectedDomain = detectDomain();
|
|
173
|
+
if (!isNonInteractive) {
|
|
174
|
+
console.log("\n apptvty \u2014 AI traffic analytics\n");
|
|
175
|
+
console.log(` Framework detected: ${framework}`);
|
|
176
|
+
if (detectedDomain) console.log(` Domain detected: ${detectedDomain}
|
|
177
|
+
`);
|
|
178
|
+
}
|
|
179
|
+
let domain = flagDomain ?? detectedDomain ?? "";
|
|
180
|
+
if (!domain && !isNonInteractive) {
|
|
181
|
+
domain = await prompt(" Enter your site domain (e.g. mysite.com): ");
|
|
182
|
+
}
|
|
183
|
+
if (!domain) {
|
|
184
|
+
if (isNonInteractive) {
|
|
185
|
+
process.stdout.write(JSON.stringify({ error: "domain is required (use --domain)" }) + "\n");
|
|
186
|
+
} else {
|
|
187
|
+
console.error("\n \u2717 Domain is required.\n");
|
|
188
|
+
}
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
if (!isNonInteractive) {
|
|
192
|
+
process.stdout.write("\n Registering with Apptvty...");
|
|
193
|
+
}
|
|
194
|
+
let result;
|
|
195
|
+
try {
|
|
196
|
+
result = await register({
|
|
197
|
+
domain,
|
|
198
|
+
framework,
|
|
199
|
+
agentId: "apptvty-cli",
|
|
200
|
+
apiUrl: flagApiUrl
|
|
201
|
+
});
|
|
202
|
+
} catch (err) {
|
|
203
|
+
const msg = err instanceof RegistrationError ? err.message : String(err);
|
|
204
|
+
if (isNonInteractive) {
|
|
205
|
+
process.stdout.write(JSON.stringify({ error: msg }) + "\n");
|
|
206
|
+
} else {
|
|
207
|
+
console.error(`
|
|
208
|
+
|
|
209
|
+
\u2717 ${msg}
|
|
210
|
+
`);
|
|
211
|
+
}
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
if (!isNonInteractive) {
|
|
215
|
+
console.log(" done\n");
|
|
216
|
+
}
|
|
217
|
+
const envFile = framework === "nextjs" ? ".env.local" : ".env";
|
|
218
|
+
appendEnvFile(envFile, result.setup.envVars);
|
|
219
|
+
const scaffolded = [];
|
|
220
|
+
if (result.setup.files && !isNonInteractive) {
|
|
221
|
+
for (const [path, content] of Object.entries(result.setup.files)) {
|
|
222
|
+
if (path === envFile) continue;
|
|
223
|
+
const written = writeFile(path, content);
|
|
224
|
+
if (written) scaffolded.push(path);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (isNonInteractive) {
|
|
228
|
+
process.stdout.write(
|
|
229
|
+
JSON.stringify({
|
|
230
|
+
site_id: result.siteId,
|
|
231
|
+
api_key: result.apiKey,
|
|
232
|
+
company_id: result.companyId,
|
|
233
|
+
wallet_address: result.walletAddress,
|
|
234
|
+
dashboard_url: result.dashboardUrl,
|
|
235
|
+
trial_ends_at: result.trialEndsAt,
|
|
236
|
+
env_file: envFile,
|
|
237
|
+
env_vars: result.setup.envVars
|
|
238
|
+
}) + "\n"
|
|
239
|
+
);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
console.log(` \u2713 Site registered: ${domain}`);
|
|
243
|
+
if (result.walletAddress) {
|
|
244
|
+
console.log(` \u2713 Wallet created: ${result.walletAddress}`);
|
|
245
|
+
}
|
|
246
|
+
console.log(` \u2713 Credentials written: ${envFile}`);
|
|
247
|
+
if (scaffolded.length > 0) {
|
|
248
|
+
for (const f of scaffolded) {
|
|
249
|
+
console.log(` \u2713 Created: ${f}`);
|
|
250
|
+
}
|
|
251
|
+
} else if (framework === "nextjs") {
|
|
252
|
+
console.log("\n Add these files to complete setup:");
|
|
253
|
+
console.log("\n middleware.ts");
|
|
254
|
+
console.log(" import { withApptvty } from 'apptvty/nextjs';");
|
|
255
|
+
console.log(" export default withApptvty({ apiKey: process.env.APPTVTY_API_KEY!, siteId: process.env.APPTVTY_SITE_ID! });");
|
|
256
|
+
console.log(" export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] };");
|
|
257
|
+
console.log("\n app/query/route.ts");
|
|
258
|
+
console.log(" import { createNextjsQueryHandler } from 'apptvty/nextjs';");
|
|
259
|
+
console.log(" export const GET = createNextjsQueryHandler({ apiKey: process.env.APPTVTY_API_KEY!, siteId: process.env.APPTVTY_SITE_ID! });");
|
|
260
|
+
}
|
|
261
|
+
console.log("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
262
|
+
console.log(" Free trial active \u2014 24 hours from now.");
|
|
263
|
+
console.log(` Trial expires: ${result.trialEndsAt}`);
|
|
264
|
+
console.log("");
|
|
265
|
+
console.log(" Open the dashboard to log in before the trial ends:");
|
|
266
|
+
console.log(` ${result.dashboardUrl}`);
|
|
267
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
268
|
+
console.log(" After the trial expires, this site's API key will stop working.");
|
|
269
|
+
console.log(" Log in once to keep your credentials and continue receiving analytics.\n");
|
|
270
|
+
}
|
|
271
|
+
async function runMigrate() {
|
|
272
|
+
const { siteId, apiKey } = loadEnvVars();
|
|
273
|
+
if (!siteId || !apiKey) {
|
|
274
|
+
const msg = "APPTVTY_SITE_ID and APPTVTY_API_KEY are required. Run `npx apptvty init` first, or set them in .env.local / .env.";
|
|
275
|
+
if (isNonInteractive) {
|
|
276
|
+
process.stdout.write(JSON.stringify({ error: msg }) + "\n");
|
|
277
|
+
} else {
|
|
278
|
+
console.error("\n \u2717 " + msg + "\n");
|
|
279
|
+
}
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
if (!isNonInteractive) {
|
|
283
|
+
console.log("\n apptvty \u2014 triggering reindex\n");
|
|
284
|
+
process.stdout.write(" Requesting re-crawl...");
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const result = await migrate({ siteId, apiKey, apiUrl: flagApiUrl });
|
|
288
|
+
if (isNonInteractive) {
|
|
289
|
+
process.stdout.write(
|
|
290
|
+
JSON.stringify({ message: result.message, site_id: result.siteId, domain: result.domain }) + "\n"
|
|
291
|
+
);
|
|
292
|
+
} else {
|
|
293
|
+
console.log(" done\n");
|
|
294
|
+
console.log(` \u2713 ${result.message}`);
|
|
295
|
+
console.log(` \u2713 Site: ${result.domain || result.siteId}`);
|
|
296
|
+
console.log("\n Content will be updated within a few minutes.\n");
|
|
297
|
+
}
|
|
298
|
+
} catch (err) {
|
|
299
|
+
const msg = err instanceof MigrateError ? err.message : String(err);
|
|
300
|
+
if (isNonInteractive) {
|
|
301
|
+
process.stdout.write(JSON.stringify({ error: msg }) + "\n");
|
|
302
|
+
} else {
|
|
303
|
+
console.error(`
|
|
304
|
+
|
|
305
|
+
\u2717 ${msg}
|
|
306
|
+
`);
|
|
307
|
+
}
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function main() {
|
|
312
|
+
if (isMigrate) {
|
|
313
|
+
await runMigrate();
|
|
314
|
+
} else {
|
|
315
|
+
await runInit();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
main().catch((err) => {
|
|
319
|
+
console.error(" Unexpected error:", err);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
});
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { A as ApptvtyConfig, R as RequestLogEntry, Q as QueryRequest, B as BackendQueryResponse, P as PageAdsResponse, I as ImpressionLog, S as SiteOverviewStats, D as DailyStat, C as CrawlerBreakdown, a as RecentActivityItem, b as RecentQueryItem, c as SiteWalletInfo, d as CrawlerInfo, e as AgentQueryResponse, f as QueryEndpointDiscovery, g as AgentErrorResponse } from './types-C1oUTCsT.mjs';
|
|
2
|
+
export { h as PageAd, i as QuerySource, j as SponsoredAd } from './types-C1oUTCsT.mjs';
|
|
3
|
+
export { createNextjsQueryHandler, withApptvty } from './middleware/nextjs.mjs';
|
|
4
|
+
export { createExpressMiddleware, createExpressQueryHandler } from './middleware/express.mjs';
|
|
5
|
+
import 'next/server';
|
|
6
|
+
import 'node:http';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* HTTP client for the Apptvty API.
|
|
10
|
+
*
|
|
11
|
+
* Handles:
|
|
12
|
+
* - Batch log ingestion (/v1/logs/batch)
|
|
13
|
+
* - Query processing (/v1/query) — returns answer + optional ad
|
|
14
|
+
* - Impression logging (/v1/impressions) — triggers billing for ads
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
declare class ApptvtyClient {
|
|
18
|
+
private readonly baseUrl;
|
|
19
|
+
private readonly apiKey;
|
|
20
|
+
private readonly siteId;
|
|
21
|
+
private readonly debug;
|
|
22
|
+
constructor(config: ApptvtyConfig);
|
|
23
|
+
/**
|
|
24
|
+
* Send a batch of request log entries to the Apptvty ingestion API.
|
|
25
|
+
* Called by the logger's auto-flush — not called directly by user code.
|
|
26
|
+
*/
|
|
27
|
+
sendLogs(logs: RequestLogEntry[]): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Send a query to the Apptvty backend.
|
|
30
|
+
* The backend runs RAG against the site's indexed content and,
|
|
31
|
+
* if ads are enabled for the site, attaches a relevant sponsored ad.
|
|
32
|
+
*
|
|
33
|
+
* @throws on non-retryable errors (network errors, 5xx) — callers should catch
|
|
34
|
+
*/
|
|
35
|
+
query(req: QueryRequest): Promise<BackendQueryResponse>;
|
|
36
|
+
/**
|
|
37
|
+
* Get relevant ads for a page (for HTML injection when crawler is detected).
|
|
38
|
+
* Used by middleware to inject ads into HTML responses.
|
|
39
|
+
*/
|
|
40
|
+
getAdsForPage(req: {
|
|
41
|
+
site_id: string;
|
|
42
|
+
page_path: string;
|
|
43
|
+
}): Promise<PageAdsResponse>;
|
|
44
|
+
/**
|
|
45
|
+
* Log an ad impression back to Apptvty.
|
|
46
|
+
*
|
|
47
|
+
* Called after returning a query response that contains a `sponsored` block.
|
|
48
|
+
* This is what triggers the billing cycle:
|
|
49
|
+
* - Advertiser gets charged (debited from their USDC ad budget)
|
|
50
|
+
* - Publisher (the website) gets credited in USDC
|
|
51
|
+
*
|
|
52
|
+
* This call is fire-and-forget. The SDK logs a warning on failure
|
|
53
|
+
* but does not throw — a missed impression log is better than
|
|
54
|
+
* breaking the query response.
|
|
55
|
+
*/
|
|
56
|
+
logImpression(impression: ImpressionLog): Promise<void>;
|
|
57
|
+
/** Get 30-day traffic overview (requests, AI %, crawlers, queries). */
|
|
58
|
+
getSiteStats(): Promise<SiteOverviewStats>;
|
|
59
|
+
/** Get day-by-day stats (default 30 days, max 90). */
|
|
60
|
+
getSiteDailyStats(days?: number): Promise<{
|
|
61
|
+
days: number;
|
|
62
|
+
stats: DailyStat[];
|
|
63
|
+
}>;
|
|
64
|
+
/** Get crawler breakdown by type (default 30 days). */
|
|
65
|
+
getSiteCrawlers(days?: number): Promise<{
|
|
66
|
+
days: number;
|
|
67
|
+
crawlers: CrawlerBreakdown[];
|
|
68
|
+
}>;
|
|
69
|
+
/** Get recent activity feed (last hour, default 50 items, max 200). */
|
|
70
|
+
getSiteActivity(limit?: number): Promise<{
|
|
71
|
+
activity: RecentActivityItem[];
|
|
72
|
+
}>;
|
|
73
|
+
/** Get recent agent queries (default 50, max 200). */
|
|
74
|
+
getSiteQueries(limit?: number): Promise<{
|
|
75
|
+
queries: RecentQueryItem[];
|
|
76
|
+
}>;
|
|
77
|
+
/** Get wallet balance and earnings. */
|
|
78
|
+
getSiteWallet(): Promise<SiteWalletInfo>;
|
|
79
|
+
private get;
|
|
80
|
+
private post;
|
|
81
|
+
private log;
|
|
82
|
+
private warn;
|
|
83
|
+
}
|
|
84
|
+
declare class ApptvtyApiError extends Error {
|
|
85
|
+
readonly statusCode: number;
|
|
86
|
+
readonly path: string;
|
|
87
|
+
readonly body: string;
|
|
88
|
+
constructor(statusCode: number, path: string, body: string);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Batched request logger.
|
|
93
|
+
*
|
|
94
|
+
* Queues RequestLogEntry objects in memory and flushes them in batches
|
|
95
|
+
* to the Apptvty API. This keeps per-request overhead near zero.
|
|
96
|
+
*
|
|
97
|
+
* Flush triggers:
|
|
98
|
+
* 1. Queue reaches batchSize (default 50)
|
|
99
|
+
* 2. flushInterval elapses (default 5s)
|
|
100
|
+
* 3. process.exit / SIGTERM (best-effort sync flush)
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
declare class RequestLogger {
|
|
104
|
+
private readonly client;
|
|
105
|
+
private queue;
|
|
106
|
+
private timer;
|
|
107
|
+
private flushing;
|
|
108
|
+
private readonly batchSize;
|
|
109
|
+
private readonly debug;
|
|
110
|
+
constructor(client: ApptvtyClient, config: ApptvtyConfig);
|
|
111
|
+
/** Enqueue a single log entry. Non-blocking. */
|
|
112
|
+
enqueue(entry: RequestLogEntry): void;
|
|
113
|
+
/** Flush the current queue to the API. */
|
|
114
|
+
flush(): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Synchronous-ish flush for process shutdown.
|
|
117
|
+
* Fires the fetch and doesn't await to avoid blocking exit handlers.
|
|
118
|
+
*/
|
|
119
|
+
private flushSync;
|
|
120
|
+
/** Stop the interval timer. Call when you want to fully tear down the SDK. */
|
|
121
|
+
destroy(): void;
|
|
122
|
+
private log;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Lightweight AI crawler detection.
|
|
127
|
+
*
|
|
128
|
+
* This is the single source of truth for crawler classification in the SDK.
|
|
129
|
+
* The backend (Python analytics API and TypeScript handlers) should eventually
|
|
130
|
+
* consume this same list rather than maintaining separate copies.
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
declare function detectCrawler(userAgent: string): CrawlerInfo;
|
|
134
|
+
/** Returns the list of all known crawlers for reference (e.g. for agents.txt generation) */
|
|
135
|
+
declare function getKnownCrawlerNames(): string[];
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Framework-agnostic query endpoint handler.
|
|
139
|
+
*
|
|
140
|
+
* Serves the site's AEO (Agent Experience Optimization) query page.
|
|
141
|
+
*
|
|
142
|
+
* GET /query → Returns a self-describing discovery JSON
|
|
143
|
+
* GET /query?q=<question> → Returns an AI-generated answer from the site's
|
|
144
|
+
* indexed content, plus a sponsored ad if ads are
|
|
145
|
+
* enabled and a matching ad exists.
|
|
146
|
+
*
|
|
147
|
+
* When an ad is included in the response, this handler fires an async
|
|
148
|
+
* impression log back to Apptvty to trigger billing:
|
|
149
|
+
* - Advertiser is charged (debited from their USDC ad budget)
|
|
150
|
+
* - Publisher earns USDC (credited to their Crossmint wallet)
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
interface QueryHandlerRequest {
|
|
154
|
+
query: string | null;
|
|
155
|
+
lang: string | null;
|
|
156
|
+
surface_ads?: boolean;
|
|
157
|
+
ai_crawler?: boolean;
|
|
158
|
+
userAgent: string;
|
|
159
|
+
ipAddress: string;
|
|
160
|
+
/** Full URL of the request, used to build the example in the discovery response */
|
|
161
|
+
requestUrl: string;
|
|
162
|
+
}
|
|
163
|
+
interface QueryHandlerResponse {
|
|
164
|
+
status: number;
|
|
165
|
+
body: AgentQueryResponse | QueryEndpointDiscovery | AgentErrorResponse;
|
|
166
|
+
headers: Record<string, string>;
|
|
167
|
+
}
|
|
168
|
+
declare function createQueryHandler(client: ApptvtyClient, config: ApptvtyConfig): (req: QueryHandlerRequest) => Promise<QueryHandlerResponse>;
|
|
169
|
+
|
|
170
|
+
export { AgentErrorResponse, AgentQueryResponse, ApptvtyApiError, ApptvtyClient, ApptvtyConfig, CrawlerBreakdown, CrawlerInfo, DailyStat, ImpressionLog, PageAdsResponse, QueryEndpointDiscovery, RecentActivityItem, RecentQueryItem, RequestLogEntry, RequestLogger, SiteOverviewStats, SiteWalletInfo, createQueryHandler, detectCrawler, getKnownCrawlerNames };
|