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