apptvty 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -17
- package/dist/chunk-2KXDQCUZ.mjs +177 -0
- package/dist/chunk-2KXDQCUZ.mjs.map +1 -0
- package/dist/{chunk-OZAZWZNO.mjs → chunk-454YBHM2.mjs} +62 -34
- package/dist/chunk-454YBHM2.mjs.map +1 -0
- package/dist/chunk-OTPVLSG5.mjs +1084 -0
- package/dist/chunk-OTPVLSG5.mjs.map +1 -0
- package/dist/cli.js +43 -7
- package/dist/index.d.mts +138 -20
- package/dist/index.d.ts +138 -20
- package/dist/index.js +389 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +7 -3
- package/dist/middleware/express.d.mts +24 -5
- package/dist/middleware/express.d.ts +24 -5
- package/dist/middleware/express.js +662 -7
- package/dist/middleware/express.js.map +1 -1
- package/dist/middleware/express.mjs +4 -2
- package/dist/middleware/nextjs.d.mts +29 -6
- package/dist/middleware/nextjs.d.ts +29 -6
- package/dist/middleware/nextjs.js +627 -33
- package/dist/middleware/nextjs.js.map +1 -1
- package/dist/middleware/nextjs.mjs +4 -2
- package/dist/setup.d.mts +9 -0
- package/dist/setup.d.ts +9 -0
- package/dist/setup.js +6 -2
- package/dist/setup.js.map +1 -1
- package/dist/setup.mjs +6 -2
- package/dist/setup.mjs.map +1 -1
- package/dist/{types-C1oUTCsT.d.mts → types-D2A_0sPm.d.mts} +116 -2
- package/dist/{types-C1oUTCsT.d.ts → types-D2A_0sPm.d.ts} +116 -2
- package/package.json +1 -1
- package/dist/chunk-3ITWIW4P.mjs +0 -87
- package/dist/chunk-3ITWIW4P.mjs.map +0 -1
- package/dist/chunk-JVOMOEEL.mjs +0 -509
- package/dist/chunk-JVOMOEEL.mjs.map +0 -1
- package/dist/chunk-OZAZWZNO.mjs.map +0 -1
|
@@ -52,6 +52,12 @@ interface RequestLogEntry {
|
|
|
52
52
|
crawler_type: string | null;
|
|
53
53
|
crawler_organization: string | null;
|
|
54
54
|
confidence_score: number;
|
|
55
|
+
/**
|
|
56
|
+
* Set when the request came from a known content-extraction service
|
|
57
|
+
* (JinaReader, FireCrawl, Cloudflare-BrowserRendering, etc.) rather than
|
|
58
|
+
* a direct AI crawler. Null for direct crawlers and human browsers.
|
|
59
|
+
*/
|
|
60
|
+
scraper_service: string | null;
|
|
55
61
|
}
|
|
56
62
|
/**
|
|
57
63
|
* Sent to the Apptvty backend when an agent queries the site's /query endpoint.
|
|
@@ -194,10 +200,20 @@ interface PageAd {
|
|
|
194
200
|
interface PageAdsResponse {
|
|
195
201
|
ads: PageAd[];
|
|
196
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Sent to Apptvty when an ad is shown, either via the query endpoint or
|
|
205
|
+
* via HTML injection into a scraped page. Triggers advertiser billing and
|
|
206
|
+
* publisher USDC credit.
|
|
207
|
+
*
|
|
208
|
+
* One of `query` or `page_path` must be set to identify the impression surface.
|
|
209
|
+
*/
|
|
197
210
|
interface ImpressionLog {
|
|
198
211
|
impression_id: string;
|
|
199
212
|
site_id: string;
|
|
200
|
-
query
|
|
213
|
+
/** Set when the impression came from the /query endpoint. */
|
|
214
|
+
query?: string;
|
|
215
|
+
/** Set when the impression came from HTML injection on a page scrape. */
|
|
216
|
+
page_path?: string;
|
|
201
217
|
agent_ua: string;
|
|
202
218
|
agent_ip: string;
|
|
203
219
|
timestamp: string;
|
|
@@ -259,5 +275,103 @@ interface SiteWalletInfo {
|
|
|
259
275
|
total_earned_usdc: number;
|
|
260
276
|
total_spent_usdc: number;
|
|
261
277
|
}
|
|
278
|
+
type CampaignStatus = 'active' | 'paused' | 'depleted';
|
|
279
|
+
interface CreateCampaignParams {
|
|
280
|
+
/** Display name for this campaign (e.g. "Kubernetes blog — tech sites"). */
|
|
281
|
+
name: string;
|
|
282
|
+
/**
|
|
283
|
+
* The ad text shown to agents. Max 500 characters.
|
|
284
|
+
* Required for static campaigns (when advertiser_site_id is not set).
|
|
285
|
+
*/
|
|
286
|
+
ad_text?: string;
|
|
287
|
+
/**
|
|
288
|
+
* Where agents land when they follow the ad. Must be a valid URL.
|
|
289
|
+
* Required for static campaigns.
|
|
290
|
+
*/
|
|
291
|
+
landing_url?: string;
|
|
292
|
+
/**
|
|
293
|
+
* Your site ID. Ad copy is generated from your site's indexed content,
|
|
294
|
+
* semantically matched to the query the agent is answering.
|
|
295
|
+
* Use this instead of ad_text/landing_url for content-aware ads.
|
|
296
|
+
*/
|
|
297
|
+
advertiser_site_id?: string;
|
|
298
|
+
/**
|
|
299
|
+
* Keywords to match against agent queries.
|
|
300
|
+
* E.g. ["kubernetes", "devops", "containers"]
|
|
301
|
+
*/
|
|
302
|
+
keywords?: string[];
|
|
303
|
+
/**
|
|
304
|
+
* Site categories to target. Valid values:
|
|
305
|
+
* technology | finance | health | education | travel |
|
|
306
|
+
* ecommerce | media | real-estate | legal | marketing
|
|
307
|
+
*/
|
|
308
|
+
categories?: string[];
|
|
309
|
+
/** Price you pay per agent view (impression). Minimum 0.0001 USDC. */
|
|
310
|
+
bid_per_view_usdc: number;
|
|
311
|
+
/** Maximum spend per calendar day. */
|
|
312
|
+
daily_budget_usdc: number;
|
|
313
|
+
/**
|
|
314
|
+
* Total campaign budget. Campaign pauses when this is exhausted.
|
|
315
|
+
* Must be >= daily_budget_usdc.
|
|
316
|
+
* Must not exceed your current wallet balance.
|
|
317
|
+
*/
|
|
318
|
+
total_budget_usdc: number;
|
|
319
|
+
}
|
|
320
|
+
interface UpdateCampaignParams {
|
|
321
|
+
name?: string;
|
|
322
|
+
ad_text?: string;
|
|
323
|
+
landing_url?: string;
|
|
324
|
+
advertiser_site_id?: string;
|
|
325
|
+
keywords?: string[];
|
|
326
|
+
categories?: string[];
|
|
327
|
+
bid_per_view_usdc?: number;
|
|
328
|
+
daily_budget_usdc?: number;
|
|
329
|
+
total_budget_usdc?: number;
|
|
330
|
+
/** Pause or resume the campaign. */
|
|
331
|
+
status?: 'active' | 'paused';
|
|
332
|
+
}
|
|
333
|
+
interface CampaignRecord {
|
|
334
|
+
id: string;
|
|
335
|
+
name: string;
|
|
336
|
+
status: CampaignStatus;
|
|
337
|
+
ad_text: string;
|
|
338
|
+
landing_url?: string;
|
|
339
|
+
advertiser_site_id?: string;
|
|
340
|
+
keywords: string[];
|
|
341
|
+
categories: string[];
|
|
342
|
+
bid_per_view_usdc: number;
|
|
343
|
+
daily_budget_usdc: number;
|
|
344
|
+
total_budget_usdc: number;
|
|
345
|
+
spent_usdc: number;
|
|
346
|
+
daily_spent_usdc: number;
|
|
347
|
+
impression_count?: number;
|
|
348
|
+
/** How much budget is left before the campaign auto-pauses. */
|
|
349
|
+
budget_remaining_usdc?: number;
|
|
350
|
+
daily_budget_remaining_usdc?: number;
|
|
351
|
+
created_at: string;
|
|
352
|
+
updated_at: string;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Returned when campaign creation fails due to insufficient wallet balance.
|
|
356
|
+
* Contains everything an agent needs to fund the wallet and retry autonomously.
|
|
357
|
+
*/
|
|
358
|
+
interface InsufficientBalanceError {
|
|
359
|
+
code: 'INSUFFICIENT_BALANCE';
|
|
360
|
+
message: string;
|
|
361
|
+
required_usdc: number;
|
|
362
|
+
available_usdc: number;
|
|
363
|
+
shortfall_usdc: number;
|
|
364
|
+
deposit: {
|
|
365
|
+
/** EVM wallet address to send USDC to. */
|
|
366
|
+
address: string | null;
|
|
367
|
+
/** Chain the wallet is on. Currently always "base". */
|
|
368
|
+
chain: 'base';
|
|
369
|
+
/** Token to send. Always "USDC". */
|
|
370
|
+
token: 'USDC';
|
|
371
|
+
/** USDC contract address on Base. */
|
|
372
|
+
contract: string;
|
|
373
|
+
note: string;
|
|
374
|
+
};
|
|
375
|
+
}
|
|
262
376
|
|
|
263
|
-
export type { ApptvtyConfig as A, BackendQueryResponse as B,
|
|
377
|
+
export type { ApptvtyConfig as A, BackendQueryResponse as B, CrawlerInfo as C, DailyStat as D, ImpressionLog as I, PageAdsResponse as P, QueryRequest as Q, RequestLogEntry as R, SiteOverviewStats as S, UpdateCampaignParams as U, RecentActivityItem as a, RecentQueryItem as b, CrawlerBreakdown as c, SiteWalletInfo as d, CreateCampaignParams as e, CampaignRecord as f, InsufficientBalanceError as g, AgentQueryResponse as h, QueryEndpointDiscovery as i, AgentErrorResponse as j, CampaignStatus as k, PageAd as l, QuerySource as m, SponsoredAd as n };
|
|
@@ -52,6 +52,12 @@ interface RequestLogEntry {
|
|
|
52
52
|
crawler_type: string | null;
|
|
53
53
|
crawler_organization: string | null;
|
|
54
54
|
confidence_score: number;
|
|
55
|
+
/**
|
|
56
|
+
* Set when the request came from a known content-extraction service
|
|
57
|
+
* (JinaReader, FireCrawl, Cloudflare-BrowserRendering, etc.) rather than
|
|
58
|
+
* a direct AI crawler. Null for direct crawlers and human browsers.
|
|
59
|
+
*/
|
|
60
|
+
scraper_service: string | null;
|
|
55
61
|
}
|
|
56
62
|
/**
|
|
57
63
|
* Sent to the Apptvty backend when an agent queries the site's /query endpoint.
|
|
@@ -194,10 +200,20 @@ interface PageAd {
|
|
|
194
200
|
interface PageAdsResponse {
|
|
195
201
|
ads: PageAd[];
|
|
196
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Sent to Apptvty when an ad is shown, either via the query endpoint or
|
|
205
|
+
* via HTML injection into a scraped page. Triggers advertiser billing and
|
|
206
|
+
* publisher USDC credit.
|
|
207
|
+
*
|
|
208
|
+
* One of `query` or `page_path` must be set to identify the impression surface.
|
|
209
|
+
*/
|
|
197
210
|
interface ImpressionLog {
|
|
198
211
|
impression_id: string;
|
|
199
212
|
site_id: string;
|
|
200
|
-
query
|
|
213
|
+
/** Set when the impression came from the /query endpoint. */
|
|
214
|
+
query?: string;
|
|
215
|
+
/** Set when the impression came from HTML injection on a page scrape. */
|
|
216
|
+
page_path?: string;
|
|
201
217
|
agent_ua: string;
|
|
202
218
|
agent_ip: string;
|
|
203
219
|
timestamp: string;
|
|
@@ -259,5 +275,103 @@ interface SiteWalletInfo {
|
|
|
259
275
|
total_earned_usdc: number;
|
|
260
276
|
total_spent_usdc: number;
|
|
261
277
|
}
|
|
278
|
+
type CampaignStatus = 'active' | 'paused' | 'depleted';
|
|
279
|
+
interface CreateCampaignParams {
|
|
280
|
+
/** Display name for this campaign (e.g. "Kubernetes blog — tech sites"). */
|
|
281
|
+
name: string;
|
|
282
|
+
/**
|
|
283
|
+
* The ad text shown to agents. Max 500 characters.
|
|
284
|
+
* Required for static campaigns (when advertiser_site_id is not set).
|
|
285
|
+
*/
|
|
286
|
+
ad_text?: string;
|
|
287
|
+
/**
|
|
288
|
+
* Where agents land when they follow the ad. Must be a valid URL.
|
|
289
|
+
* Required for static campaigns.
|
|
290
|
+
*/
|
|
291
|
+
landing_url?: string;
|
|
292
|
+
/**
|
|
293
|
+
* Your site ID. Ad copy is generated from your site's indexed content,
|
|
294
|
+
* semantically matched to the query the agent is answering.
|
|
295
|
+
* Use this instead of ad_text/landing_url for content-aware ads.
|
|
296
|
+
*/
|
|
297
|
+
advertiser_site_id?: string;
|
|
298
|
+
/**
|
|
299
|
+
* Keywords to match against agent queries.
|
|
300
|
+
* E.g. ["kubernetes", "devops", "containers"]
|
|
301
|
+
*/
|
|
302
|
+
keywords?: string[];
|
|
303
|
+
/**
|
|
304
|
+
* Site categories to target. Valid values:
|
|
305
|
+
* technology | finance | health | education | travel |
|
|
306
|
+
* ecommerce | media | real-estate | legal | marketing
|
|
307
|
+
*/
|
|
308
|
+
categories?: string[];
|
|
309
|
+
/** Price you pay per agent view (impression). Minimum 0.0001 USDC. */
|
|
310
|
+
bid_per_view_usdc: number;
|
|
311
|
+
/** Maximum spend per calendar day. */
|
|
312
|
+
daily_budget_usdc: number;
|
|
313
|
+
/**
|
|
314
|
+
* Total campaign budget. Campaign pauses when this is exhausted.
|
|
315
|
+
* Must be >= daily_budget_usdc.
|
|
316
|
+
* Must not exceed your current wallet balance.
|
|
317
|
+
*/
|
|
318
|
+
total_budget_usdc: number;
|
|
319
|
+
}
|
|
320
|
+
interface UpdateCampaignParams {
|
|
321
|
+
name?: string;
|
|
322
|
+
ad_text?: string;
|
|
323
|
+
landing_url?: string;
|
|
324
|
+
advertiser_site_id?: string;
|
|
325
|
+
keywords?: string[];
|
|
326
|
+
categories?: string[];
|
|
327
|
+
bid_per_view_usdc?: number;
|
|
328
|
+
daily_budget_usdc?: number;
|
|
329
|
+
total_budget_usdc?: number;
|
|
330
|
+
/** Pause or resume the campaign. */
|
|
331
|
+
status?: 'active' | 'paused';
|
|
332
|
+
}
|
|
333
|
+
interface CampaignRecord {
|
|
334
|
+
id: string;
|
|
335
|
+
name: string;
|
|
336
|
+
status: CampaignStatus;
|
|
337
|
+
ad_text: string;
|
|
338
|
+
landing_url?: string;
|
|
339
|
+
advertiser_site_id?: string;
|
|
340
|
+
keywords: string[];
|
|
341
|
+
categories: string[];
|
|
342
|
+
bid_per_view_usdc: number;
|
|
343
|
+
daily_budget_usdc: number;
|
|
344
|
+
total_budget_usdc: number;
|
|
345
|
+
spent_usdc: number;
|
|
346
|
+
daily_spent_usdc: number;
|
|
347
|
+
impression_count?: number;
|
|
348
|
+
/** How much budget is left before the campaign auto-pauses. */
|
|
349
|
+
budget_remaining_usdc?: number;
|
|
350
|
+
daily_budget_remaining_usdc?: number;
|
|
351
|
+
created_at: string;
|
|
352
|
+
updated_at: string;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Returned when campaign creation fails due to insufficient wallet balance.
|
|
356
|
+
* Contains everything an agent needs to fund the wallet and retry autonomously.
|
|
357
|
+
*/
|
|
358
|
+
interface InsufficientBalanceError {
|
|
359
|
+
code: 'INSUFFICIENT_BALANCE';
|
|
360
|
+
message: string;
|
|
361
|
+
required_usdc: number;
|
|
362
|
+
available_usdc: number;
|
|
363
|
+
shortfall_usdc: number;
|
|
364
|
+
deposit: {
|
|
365
|
+
/** EVM wallet address to send USDC to. */
|
|
366
|
+
address: string | null;
|
|
367
|
+
/** Chain the wallet is on. Currently always "base". */
|
|
368
|
+
chain: 'base';
|
|
369
|
+
/** Token to send. Always "USDC". */
|
|
370
|
+
token: 'USDC';
|
|
371
|
+
/** USDC contract address on Base. */
|
|
372
|
+
contract: string;
|
|
373
|
+
note: string;
|
|
374
|
+
};
|
|
375
|
+
}
|
|
262
376
|
|
|
263
|
-
export type { ApptvtyConfig as A, BackendQueryResponse as B,
|
|
377
|
+
export type { ApptvtyConfig as A, BackendQueryResponse as B, CrawlerInfo as C, DailyStat as D, ImpressionLog as I, PageAdsResponse as P, QueryRequest as Q, RequestLogEntry as R, SiteOverviewStats as S, UpdateCampaignParams as U, RecentActivityItem as a, RecentQueryItem as b, CrawlerBreakdown as c, SiteWalletInfo as d, CreateCampaignParams as e, CampaignRecord as f, InsufficientBalanceError as g, AgentQueryResponse as h, QueryEndpointDiscovery as i, AgentErrorResponse as j, CampaignStatus as k, PageAd as l, QuerySource as m, SponsoredAd as n };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apptvty",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Server-side analytics and AEO (Agent Experience Optimization) SDK for websites. Logs agentic traffic, exposes a structured query endpoint for AI agents, and serves relevant ads on agent queries.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
package/dist/chunk-3ITWIW4P.mjs
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ApptvtyClient,
|
|
3
|
-
RequestLogger,
|
|
4
|
-
createQueryHandler,
|
|
5
|
-
detectCrawler,
|
|
6
|
-
getClientIp
|
|
7
|
-
} from "./chunk-JVOMOEEL.mjs";
|
|
8
|
-
|
|
9
|
-
// src/middleware/express.ts
|
|
10
|
-
var instances = /* @__PURE__ */ new Map();
|
|
11
|
-
function getInstance(config) {
|
|
12
|
-
const key = config.apiKey;
|
|
13
|
-
if (!instances.has(key)) {
|
|
14
|
-
const client = new ApptvtyClient(config);
|
|
15
|
-
const logger = new RequestLogger(client, config);
|
|
16
|
-
instances.set(key, { client, logger });
|
|
17
|
-
}
|
|
18
|
-
return instances.get(key);
|
|
19
|
-
}
|
|
20
|
-
function createExpressMiddleware(config) {
|
|
21
|
-
const { logger } = getInstance(config);
|
|
22
|
-
return function apptvtyLogger(req, res, next) {
|
|
23
|
-
const startMs = Date.now();
|
|
24
|
-
const userAgent = req.headers["user-agent"] ?? "";
|
|
25
|
-
const crawlerInfo = detectCrawler(userAgent);
|
|
26
|
-
const path = req.url ?? "/";
|
|
27
|
-
res.on("finish", () => {
|
|
28
|
-
if (shouldSkip(path)) return;
|
|
29
|
-
const entry = {
|
|
30
|
-
site_id: config.siteId,
|
|
31
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32
|
-
method: req.method ?? "GET",
|
|
33
|
-
path,
|
|
34
|
-
status_code: res.statusCode,
|
|
35
|
-
response_time_ms: Date.now() - startMs,
|
|
36
|
-
ip_address: getClientIp(req.headers),
|
|
37
|
-
user_agent: userAgent,
|
|
38
|
-
referer: req.headers["referer"] ?? null,
|
|
39
|
-
is_ai_crawler: crawlerInfo.isAi,
|
|
40
|
-
crawler_type: crawlerInfo.name,
|
|
41
|
-
crawler_organization: crawlerInfo.organization,
|
|
42
|
-
confidence_score: crawlerInfo.confidence
|
|
43
|
-
};
|
|
44
|
-
logger.enqueue(entry);
|
|
45
|
-
});
|
|
46
|
-
next();
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function createExpressQueryHandler(config) {
|
|
50
|
-
const { client } = getInstance(config);
|
|
51
|
-
const handleQuery = createQueryHandler(client, config);
|
|
52
|
-
return async function queryHandler(req, res) {
|
|
53
|
-
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
54
|
-
const q = url.searchParams.get("q");
|
|
55
|
-
const lang = url.searchParams.get("lang");
|
|
56
|
-
const surfaceAds = parseBoolParam(url.searchParams.get("surface_ads"), true);
|
|
57
|
-
const aiCrawler = parseBoolParam(url.searchParams.get("ai_crawler"), false);
|
|
58
|
-
const userAgent = req.headers["user-agent"] ?? "";
|
|
59
|
-
const result = await handleQuery({
|
|
60
|
-
query: q,
|
|
61
|
-
lang,
|
|
62
|
-
surface_ads: surfaceAds,
|
|
63
|
-
ai_crawler: aiCrawler,
|
|
64
|
-
userAgent,
|
|
65
|
-
ipAddress: getClientIp(req.headers),
|
|
66
|
-
requestUrl: url.toString()
|
|
67
|
-
});
|
|
68
|
-
for (const [key, value] of Object.entries(result.headers)) {
|
|
69
|
-
res.setHeader(key, value);
|
|
70
|
-
}
|
|
71
|
-
res.statusCode = result.status;
|
|
72
|
-
res.end(JSON.stringify(result.body));
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
function parseBoolParam(value, defaultValue) {
|
|
76
|
-
if (value === null) return defaultValue;
|
|
77
|
-
return value === "1" || value === "true" || value === "yes";
|
|
78
|
-
}
|
|
79
|
-
function shouldSkip(path) {
|
|
80
|
-
return path.startsWith("/_next/") || /\.(svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|css|js\.map)$/.test(path);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export {
|
|
84
|
-
createExpressMiddleware,
|
|
85
|
-
createExpressQueryHandler
|
|
86
|
-
};
|
|
87
|
-
//# sourceMappingURL=chunk-3ITWIW4P.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/middleware/express.ts"],"sourcesContent":["/**\n * Express / Node.js integration for the Apptvty SDK.\n *\n * Usage:\n *\n * import express from 'express';\n * import { createExpressMiddleware, createExpressQueryHandler } from 'apptvty/express';\n *\n * const app = express();\n * const config = { apiKey: 'ak_...', siteId: 'site_...' };\n *\n * // 1. Log all traffic\n * app.use(createExpressMiddleware(config));\n *\n * // 2. Mount the AEO query page\n * app.get('/query', createExpressQueryHandler(config));\n *\n * Works with any Connect-compatible framework (Express, Fastify via @fastify/express, etc.)\n */\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\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\nexport type ConnectMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: unknown) => void,\n) => void;\n\nexport type ConnectHandler = (\n req: IncomingMessage,\n res: ServerResponse,\n) => void | Promise<void>;\n\n// ─── Shared singleton instances per config ────────────────────────────────────\n\nconst instances = new Map<string, { client: ApptvtyClient; logger: RequestLogger }>();\n\nfunction getInstance(config: ApptvtyConfig) {\n const key = config.apiKey;\n if (!instances.has(key)) {\n const client = new ApptvtyClient(config);\n const logger = new RequestLogger(client, config);\n instances.set(key, { client, logger });\n }\n return instances.get(key)!;\n}\n\n// ─── Traffic logging middleware ───────────────────────────────────────────────\n\n/**\n * Express middleware that logs every request to Apptvty.\n * Captures: method, path, status, response time, user-agent, IP, crawler classification.\n *\n * Mount this before your routes so all traffic is captured.\n *\n * @example\n * app.use(createExpressMiddleware({ apiKey: 'ak_...', siteId: 'site_...' }));\n */\nexport function createExpressMiddleware(config: ApptvtyConfig): ConnectMiddleware {\n const { logger } = getInstance(config);\n\n return function apptvtyLogger(req, res, next) {\n const startMs = Date.now();\n const userAgent = req.headers['user-agent'] ?? '';\n const crawlerInfo = detectCrawler(userAgent);\n const path = req.url ?? '/';\n\n // Intercept response finish to capture status code and timing\n res.on('finish', () => {\n if (shouldSkip(path)) return;\n\n const entry: RequestLogEntry = {\n site_id: config.siteId,\n timestamp: new Date().toISOString(),\n method: req.method ?? 'GET',\n path,\n status_code: res.statusCode,\n response_time_ms: Date.now() - startMs,\n ip_address: getClientIp(req.headers as Record<string, string | string[] | undefined>),\n user_agent: userAgent,\n referer: (req.headers['referer'] as string) ?? null,\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\n next();\n };\n}\n\n// ─── Query endpoint handler ───────────────────────────────────────────────────\n\n/**\n * Express route handler for the AEO query endpoint.\n *\n * Mount this at the path configured in your dashboard (default: /query).\n *\n * @example\n * app.get('/query', createExpressQueryHandler({ apiKey: 'ak_...', siteId: 'site_...' }));\n */\nexport function createExpressQueryHandler(config: ApptvtyConfig): ConnectHandler {\n const { client } = getInstance(config);\n const handleQuery = createQueryHandler(client, config);\n\n return async function queryHandler(req, res) {\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);\n const q = url.searchParams.get('q');\n const lang = url.searchParams.get('lang');\n const surfaceAds = parseBoolParam(url.searchParams.get('surface_ads'), true);\n const aiCrawler = parseBoolParam(url.searchParams.get('ai_crawler'), false);\n const userAgent = req.headers['user-agent'] ?? '';\n\n const result = await handleQuery({\n query: q,\n lang,\n surface_ads: surfaceAds,\n ai_crawler: aiCrawler,\n userAgent,\n ipAddress: getClientIp(req.headers as Record<string, string | string[] | undefined>),\n requestUrl: url.toString(),\n });\n\n for (const [key, value] of Object.entries(result.headers)) {\n res.setHeader(key, value);\n }\n res.statusCode = result.status;\n res.end(JSON.stringify(result.body));\n };\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction parseBoolParam(value: string | null, defaultValue: boolean): boolean {\n if (value === null) return defaultValue;\n return value === '1' || value === 'true' || value === 'yes';\n}\n\nfunction shouldSkip(path: string): boolean {\n return (\n path.startsWith('/_next/') ||\n /\\.(svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|css|js\\.map)$/.test(path)\n );\n}\n"],"mappings":";;;;;;;;;AAwCA,IAAM,YAAY,oBAAI,IAA8D;AAEpF,SAAS,YAAY,QAAuB;AAC1C,QAAM,MAAM,OAAO;AACnB,MAAI,CAAC,UAAU,IAAI,GAAG,GAAG;AACvB,UAAM,SAAS,IAAI,cAAc,MAAM;AACvC,UAAM,SAAS,IAAI,cAAc,QAAQ,MAAM;AAC/C,cAAU,IAAI,KAAK,EAAE,QAAQ,OAAO,CAAC;AAAA,EACvC;AACA,SAAO,UAAU,IAAI,GAAG;AAC1B;AAaO,SAAS,wBAAwB,QAA0C;AAChF,QAAM,EAAE,OAAO,IAAI,YAAY,MAAM;AAErC,SAAO,SAAS,cAAc,KAAK,KAAK,MAAM;AAC5C,UAAM,UAAU,KAAK,IAAI;AACzB,UAAM,YAAY,IAAI,QAAQ,YAAY,KAAK;AAC/C,UAAM,cAAc,cAAc,SAAS;AAC3C,UAAM,OAAO,IAAI,OAAO;AAGxB,QAAI,GAAG,UAAU,MAAM;AACrB,UAAI,WAAW,IAAI,EAAG;AAEtB,YAAM,QAAyB;AAAA,QAC7B,SAAS,OAAO;AAAA,QAChB,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QAClC,QAAQ,IAAI,UAAU;AAAA,QACtB;AAAA,QACA,aAAa,IAAI;AAAA,QACjB,kBAAkB,KAAK,IAAI,IAAI;AAAA,QAC/B,YAAY,YAAY,IAAI,OAAwD;AAAA,QACpF,YAAY;AAAA,QACZ,SAAU,IAAI,QAAQ,SAAS,KAAgB;AAAA,QAC/C,eAAe,YAAY;AAAA,QAC3B,cAAc,YAAY;AAAA,QAC1B,sBAAsB,YAAY;AAAA,QAClC,kBAAkB,YAAY;AAAA,MAChC;AAEA,aAAO,QAAQ,KAAK;AAAA,IACtB,CAAC;AAED,SAAK;AAAA,EACP;AACF;AAYO,SAAS,0BAA0B,QAAuC;AAC/E,QAAM,EAAE,OAAO,IAAI,YAAY,MAAM;AACrC,QAAM,cAAc,mBAAmB,QAAQ,MAAM;AAErD,SAAO,eAAe,aAAa,KAAK,KAAK;AAC3C,UAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,IAAI,QAAQ,QAAQ,WAAW,EAAE;AAC/E,UAAM,IAAI,IAAI,aAAa,IAAI,GAAG;AAClC,UAAM,OAAO,IAAI,aAAa,IAAI,MAAM;AACxC,UAAM,aAAa,eAAe,IAAI,aAAa,IAAI,aAAa,GAAG,IAAI;AAC3E,UAAM,YAAY,eAAe,IAAI,aAAa,IAAI,YAAY,GAAG,KAAK;AAC1E,UAAM,YAAY,IAAI,QAAQ,YAAY,KAAK;AAE/C,UAAM,SAAS,MAAM,YAAY;AAAA,MAC/B,OAAO;AAAA,MACP;AAAA,MACA,aAAa;AAAA,MACb,YAAY;AAAA,MACZ;AAAA,MACA,WAAW,YAAY,IAAI,OAAwD;AAAA,MACnF,YAAY,IAAI,SAAS;AAAA,IAC3B,CAAC;AAED,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,OAAO,GAAG;AACzD,UAAI,UAAU,KAAK,KAAK;AAAA,IAC1B;AACA,QAAI,aAAa,OAAO;AACxB,QAAI,IAAI,KAAK,UAAU,OAAO,IAAI,CAAC;AAAA,EACrC;AACF;AAIA,SAAS,eAAe,OAAsB,cAAgC;AAC5E,MAAI,UAAU,KAAM,QAAO;AAC3B,SAAO,UAAU,OAAO,UAAU,UAAU,UAAU;AACxD;AAEA,SAAS,WAAW,MAAuB;AACzC,SACE,KAAK,WAAW,SAAS,KACzB,4DAA4D,KAAK,IAAI;AAEzE;","names":[]}
|