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 ADDED
@@ -0,0 +1,537 @@
1
+ # apptvty
2
+
3
+ **AI traffic analytics and Agent Experience Optimization (AEO) for Node.js websites.**
4
+
5
+ Apptvty makes your website visible to, and queryable by, AI agents — the crawlers and language models (GPTBot, ClaudeBot, Perplexity, etc.) that are increasingly the primary consumers of web content.
6
+
7
+ - **Detects and classifies AI traffic** that standard analytics tools miss entirely
8
+ - **Exposes a `/query` endpoint** so AI agents can get structured, sourced answers from your site
9
+ - **Earns USDC** for your site when sponsored ads are served in query responses
10
+ - **Two lines to integrate** into an existing Next.js or Express app
11
+
12
+ ```
13
+ npm install apptvty
14
+ ```
15
+
16
+ Node.js >= 18 required.
17
+
18
+ ---
19
+
20
+ ## Quick start
21
+
22
+ ### Next.js (App Router)
23
+
24
+ **Step 1 — Log all traffic**
25
+
26
+ ```typescript
27
+ // middleware.ts
28
+ import { withApptvty } from 'apptvty/nextjs';
29
+
30
+ export default withApptvty({
31
+ apiKey: process.env.APPTVTY_API_KEY!,
32
+ siteId: process.env.APPTVTY_SITE_ID!,
33
+ });
34
+
35
+ export const config = {
36
+ matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
37
+ };
38
+ ```
39
+
40
+ **Step 2 — Expose the query endpoint**
41
+
42
+ ```typescript
43
+ // app/query/route.ts
44
+ import { createNextjsQueryHandler } from 'apptvty/nextjs';
45
+
46
+ export const GET = createNextjsQueryHandler({
47
+ apiKey: process.env.APPTVTY_API_KEY!,
48
+ siteId: process.env.APPTVTY_SITE_ID!,
49
+ });
50
+ ```
51
+
52
+ **That's it.** Every request is now logged, AI crawlers are classified, and AI agents can query your site at `https://yoursite.com/query?q=your+question`.
53
+
54
+ ---
55
+
56
+ ### Express
57
+
58
+ ```typescript
59
+ import express from 'express';
60
+ import { createExpressMiddleware, createExpressQueryHandler } from 'apptvty/express';
61
+
62
+ const app = express();
63
+ const config = {
64
+ apiKey: process.env.APPTVTY_API_KEY!,
65
+ siteId: process.env.APPTVTY_SITE_ID!,
66
+ };
67
+
68
+ // Log all requests
69
+ app.use(createExpressMiddleware(config));
70
+
71
+ // Expose the query endpoint
72
+ app.get('/query', createExpressQueryHandler(config));
73
+ ```
74
+
75
+ ---
76
+
77
+ ### CLI setup (humans and agents)
78
+
79
+ The fastest way to get credentials and scaffold files:
80
+
81
+ ```bash
82
+ # Interactive (human)
83
+ npx apptvty init
84
+
85
+ # Non-interactive (agent / CI) — prints JSON to stdout
86
+ npx apptvty init --domain mysite.com --framework nextjs --non-interactive
87
+ ```
88
+
89
+ Trigger a re-crawl after publishing new content:
90
+
91
+ ```bash
92
+ # Reindex your site (reads APPTVTY_SITE_ID and APPTVTY_API_KEY from .env.local / .env)
93
+ npx apptvty migrate
94
+ ```
95
+
96
+ CLI commands:
97
+
98
+ | Command | Description |
99
+ |---------|-------------|
100
+ | `init` | Register site and scaffold integration files (default) |
101
+ | `migrate` | Trigger immediate re-crawl/reindex of your site |
102
+
103
+ CLI flags:
104
+
105
+ | Flag | Description |
106
+ |------|-------------|
107
+ | `--domain` | Your site's domain, e.g. `mysite.com` (init only) |
108
+ | `--framework` | `nextjs`, `express`, or `other` (init only, auto-detected) |
109
+ | `--non-interactive` | No prompts; JSON output for scripting/agents |
110
+ | `--api-url` | Override API base URL (staging / self-hosted) |
111
+
112
+ **Agent-mode JSON output:**
113
+ ```json
114
+ {
115
+ "site_id": "site_abc123",
116
+ "api_key": "ak_live_...",
117
+ "company_id": "co_xyz...",
118
+ "wallet_address": "0x...",
119
+ "dashboard_url": "https://app.apptvty.com/claim?token=...",
120
+ "env_file": ".env.local",
121
+ "env_vars": {
122
+ "APPTVTY_SITE_ID": "site_abc123",
123
+ "APPTVTY_API_KEY": "ak_live_..."
124
+ }
125
+ }
126
+ ```
127
+
128
+ The CLI writes credentials to `.env.local` (Next.js) or `.env` (Express/other) and scaffolds `middleware.ts` and `app/query/route.ts` if they do not already exist.
129
+
130
+ ---
131
+
132
+ ## Package exports
133
+
134
+ | Import | Contents |
135
+ |--------|----------|
136
+ | `apptvty` | Core client, logger, crawler detector, query handler (framework-agnostic) |
137
+ | `apptvty/nextjs` | Next.js App Router middleware and route handler |
138
+ | `apptvty/express` | Express/Connect middleware and route handler |
139
+ | `apptvty/setup` | `register()` and `migrate()` for programmatic setup and reindex |
140
+
141
+ ---
142
+
143
+ ## Configuration
144
+
145
+ All middleware and handler functions accept an `ApptvtyConfig` object:
146
+
147
+ ```typescript
148
+ interface ApptvtyConfig {
149
+ apiKey: string; // Required. Your site's API key (ak_...)
150
+ siteId: string; // Required. Your site ID from the dashboard
151
+ baseUrl?: string; // Optional. Uses APPTVTY_API_URL env if set.
152
+ batchSize?: number; // Default: 50 — logs per flush
153
+ flushInterval?: number; // Default: 5000 — ms between auto-flushes
154
+ debug?: boolean; // Default: false — log errors to console
155
+ queryPath?: string; // Default: '/query' — path of AEO endpoint
156
+ }
157
+ ```
158
+
159
+ Set these from environment variables so credentials are never in source code:
160
+
161
+ ```
162
+ APPTVTY_API_KEY=ak_live_...
163
+ APPTVTY_SITE_ID=site_...
164
+ APPTVTY_API_URL=https://api.apptvty.com # Optional. Default is api.apptvty.com; override for self-hosted only.
165
+ ```
166
+
167
+ See [.env.example](.env.example) for a copy-paste template.
168
+
169
+ **Production:** The SDK uses **https://api.apptvty.com** by default. You do not need to set `APPTVTY_API_URL` for production.
170
+
171
+ ### Using the SDK with a self-hosted backend
172
+
173
+ Only if you run **apptvty-backend** yourself (e.g. Serverless in your AWS account), point the SDK at that API:
174
+
175
+ 1. **Deploy the backend** (from the backend repo):
176
+ ```bash
177
+ cd apptvty-backend
178
+ npx sls deploy --stage dev
179
+ ```
180
+ 2. **Get the API URL** from the stack output:
181
+ ```bash
182
+ npx sls info --stage dev
183
+ ```
184
+ Use the **ApiGatewayRestApiUrl** (e.g. `https://xxxx.execute-api.us-west-2.amazonaws.com/dev`).
185
+ 3. **Override the default (api.apptvty.com)** in one of two ways:
186
+ - **Environment:** Set `APPTVTY_API_URL` to that URL (no trailing slash).
187
+ - **In code:** Pass `baseUrl` (or `apiUrl` for `register`/`migrate`).
188
+
189
+ ---
190
+
191
+ ## The query endpoint
192
+
193
+ When an AI agent visits `https://yoursite.com/query`, the SDK responds with a self-describing discovery page so the agent knows how to use it:
194
+
195
+ ```json
196
+ {
197
+ "version": "1.0",
198
+ "endpoint": "https://yoursite.com/query",
199
+ "description": "Query this site's content using natural language.",
200
+ "usage": {
201
+ "method": "GET",
202
+ "parameters": {
203
+ "q": { "type": "string", "required": true, "description": "Natural language question" },
204
+ "lang": { "type": "string", "required": false, "description": "Preferred language (ISO 639-1)" }
205
+ },
206
+ "example": "https://yoursite.com/query?q=What+does+this+site+do"
207
+ },
208
+ "capabilities": ["rag", "sponsored_ads"],
209
+ "rate_limit": "60 requests per minute"
210
+ }
211
+ ```
212
+
213
+ When called with `?q=<question>`:
214
+
215
+ ```json
216
+ {
217
+ "success": true,
218
+ "version": "1.0",
219
+ "query": "What does this site do?",
220
+ "answer": "Apptvty is an AI traffic analytics platform ...",
221
+ "sources": [
222
+ {
223
+ "url": "https://yoursite.com/about",
224
+ "title": "About Apptvty",
225
+ "snippet": "Apptvty logs and classifies AI crawler traffic ...",
226
+ "relevance": 0.94
227
+ }
228
+ ],
229
+ "confidence": 0.91,
230
+ "sponsored": {
231
+ "label": "Sponsored",
232
+ "text": "Get deeper AI analytics with AEO Pro — 14-day free trial",
233
+ "url": "https://...",
234
+ "advertiser": "AEO Pro",
235
+ "impression_id": "imp_..."
236
+ },
237
+ "metadata": {
238
+ "request_id": "req_...",
239
+ "response_time_ms": 340,
240
+ "tokens_used": 420,
241
+ "site_id": "site_...",
242
+ "timestamp": "2026-03-06T12:00:00.000Z"
243
+ }
244
+ }
245
+ ```
246
+
247
+ The `sponsored` field is present only when ads are enabled for your site and a matching ad exists. Ads are clearly labeled and serve as a revenue stream for site owners — no action required by the agent beyond passing the field through to the end user.
248
+
249
+ **Error response:**
250
+ ```json
251
+ {
252
+ "success": false,
253
+ "error": {
254
+ "code": "QUERY_TOO_LONG",
255
+ "message": "Query exceeds the 500-character limit.",
256
+ "request_id": "req_...",
257
+ "timestamp": "2026-03-06T12:00:00.000Z"
258
+ }
259
+ }
260
+ ```
261
+
262
+ Response headers always include:
263
+ - `Content-Type: application/json`
264
+ - `Cache-Control: no-store`
265
+ - `X-Robots-Tag: noindex`
266
+
267
+ ---
268
+
269
+ ## Analytics for coding agents
270
+
271
+ The SDK exposes methods so coding agents (e.g. Cursor, Claude Code) can fetch logs, activity, and errors without human intervention — similar to how `aws logs tail` works for AWS CLI.
272
+
273
+ ```typescript
274
+ import { ApptvtyClient } from 'apptvty';
275
+
276
+ const client = new ApptvtyClient({
277
+ apiKey: process.env.APPTVTY_API_KEY!,
278
+ siteId: process.env.APPTVTY_SITE_ID!,
279
+ });
280
+
281
+ // 30-day overview
282
+ const stats = await client.getSiteStats();
283
+ console.log(`AI traffic: ${stats.ai_percentage}%`);
284
+
285
+ // Recent activity (last hour)
286
+ const { activity } = await client.getSiteActivity(20);
287
+ activity.forEach(a => console.log(a.path, a.crawler_type, a.status_code));
288
+
289
+ // Recent agent queries
290
+ const { queries } = await client.getSiteQueries(10);
291
+
292
+ // Crawler breakdown
293
+ const { crawlers } = await client.getSiteCrawlers(7);
294
+
295
+ // Daily stats
296
+ const { stats: daily } = await client.getSiteDailyStats(30);
297
+
298
+ // Wallet balance
299
+ const wallet = await client.getSiteWallet();
300
+ ```
301
+
302
+ | Method | Returns |
303
+ |--------|---------|
304
+ | `getSiteStats()` | 30-day overview (requests, AI %, crawlers, queries) |
305
+ | `getSiteDailyStats(days?)` | Per-day breakdown |
306
+ | `getSiteActivity(limit?)` | Recent requests (path, crawler, status) |
307
+ | `getSiteQueries(limit?)` | Recent agent queries |
308
+ | `getSiteCrawlers(days?)` | Crawler type breakdown |
309
+ | `getSiteWallet()` | Balance, earnings, spend |
310
+
311
+ ---
312
+
313
+ ## Crawler detection
314
+
315
+ The SDK identifies 50+ AI crawlers by user-agent string. Access detection directly:
316
+
317
+ ```typescript
318
+ import { detectCrawler, getKnownCrawlerNames } from 'apptvty';
319
+
320
+ const info = detectCrawler('GPTBot/1.1');
321
+ // {
322
+ // isAi: true,
323
+ // name: 'GPTBot',
324
+ // organization: 'OpenAI',
325
+ // confidence: 0.95,
326
+ // detectionMethod: 'exact_match'
327
+ // }
328
+
329
+ const names = getKnownCrawlerNames();
330
+ // ['GPTBot', 'ClaudeBot', 'PerplexityBot', 'BingBot', ...]
331
+ ```
332
+
333
+ **`CrawlerInfo` fields:**
334
+
335
+ | Field | Type | Description |
336
+ |-------|------|-------------|
337
+ | `isAi` | `boolean` | Whether the traffic is from an AI/LLM system |
338
+ | `name` | `string \| null` | Crawler name, e.g. `"GPTBot"` |
339
+ | `organization` | `string \| null` | Organization, e.g. `"OpenAI"` |
340
+ | `confidence` | `number` | Detection certainty, 0.0 – 1.0 |
341
+ | `detectionMethod` | `string` | `exact_match` / `pattern_match` / `heuristic` / `none` |
342
+
343
+ **Crawlers recognized (partial list):** GPTBot, OpenAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-Web, Anthropic-AI, Google-Extended, GoogleOther, Bingbot, PerplexityBot, YouBot, DuckDuckBot, Meta-ExternalAgent, LinkedInBot, AppleBot, TwitterBot, CohereAI, AI2Bot, MistralBot.
344
+
345
+ ---
346
+
347
+ ## Programmatic registration
348
+
349
+ For agents and scripts that need to register a site without the CLI:
350
+
351
+ ```typescript
352
+ import { register, RegistrationError } from 'apptvty/setup';
353
+
354
+ try {
355
+ const result = await register({
356
+ domain: 'mysite.com',
357
+ framework: 'nextjs', // 'nextjs' | 'express' | 'other'
358
+ agentId: 'my-agent', // optional — analytics on which agent registered
359
+ });
360
+
361
+ console.log(result.apiKey); // 'ak_live_...'
362
+ console.log(result.siteId); // 'site_...'
363
+ console.log(result.walletAddress); // '0x...' or null
364
+ console.log(result.dashboardUrl); // Dashboard claim link (valid 30 days)
365
+ console.log(result.setup.envVars); // { APPTVTY_SITE_ID, APPTVTY_API_KEY }
366
+
367
+ } catch (err) {
368
+ if (err instanceof RegistrationError) {
369
+ // err.code: 'DOMAIN_TAKEN' | 'REGISTRATION_FAILED' | ...
370
+ console.error(err.code, err.message);
371
+ }
372
+ }
373
+ ```
374
+
375
+ **`RegisterOptions`:**
376
+
377
+ | Field | Type | Required | Description |
378
+ |-------|------|----------|-------------|
379
+ | `domain` | `string` | Yes | Site domain, e.g. `mysite.com` |
380
+ | `framework` | `string` | No | `nextjs`, `express`, or `other` |
381
+ | `agentId` | `string` | No | Identifies the registering agent (for analytics) |
382
+ | `apiUrl` | `string` | No | Override API base URL for staging/self-hosted |
383
+
384
+ **`RegisterResult`:**
385
+
386
+ | Field | Type | Description |
387
+ |-------|------|-------------|
388
+ | `siteId` | `string` | Site identifier |
389
+ | `apiKey` | `string` | API key for SDK config |
390
+ | `companyId` | `string` | Company identifier |
391
+ | `walletAddress` | `string \| null` | Crossmint wallet for USDC earnings |
392
+ | `dashboardUrl` | `string` | Dashboard claim link (valid 30 days) |
393
+ | `claimTokenExpiresAt` | `string` | ISO timestamp of link expiry |
394
+ | `setup.envVars` | `object` | Env vars to write to your `.env` file |
395
+ | `setup.files` | `object \| undefined` | Optional scaffold file contents |
396
+
397
+ Registration is idempotent per domain. Registering the same domain twice throws `RegistrationError` with `code: 'DOMAIN_TAKEN'`.
398
+
399
+ ### Programmatic migrate (reindex)
400
+
401
+ Trigger an immediate re-crawl after publishing new content:
402
+
403
+ ```typescript
404
+ import { migrate, MigrateError } from 'apptvty/setup';
405
+
406
+ try {
407
+ const result = await migrate({
408
+ siteId: process.env.APPTVTY_SITE_ID!,
409
+ apiKey: process.env.APPTVTY_API_KEY!,
410
+ apiUrl: 'https://api.apptvty.com', // optional — for staging/self-hosted
411
+ });
412
+ console.log(result.message); // "Reindex started. Your site content will be updated within a few minutes."
413
+ } catch (err) {
414
+ if (err instanceof MigrateError) {
415
+ console.error(err.code, err.message);
416
+ }
417
+ }
418
+ ```
419
+
420
+ The crawl runs asynchronously; the endpoint returns 202 immediately.
421
+
422
+ ---
423
+
424
+ ## Advanced: framework-agnostic usage
425
+
426
+ Use `ApptvtyClient` and `RequestLogger` directly for custom frameworks:
427
+
428
+ ```typescript
429
+ import { ApptvtyClient, RequestLogger, detectCrawler, createQueryHandler } from 'apptvty';
430
+
431
+ const config = { apiKey: 'ak_...', siteId: 'site_...' };
432
+ const client = new ApptvtyClient(config);
433
+ const logger = new RequestLogger(client, config);
434
+
435
+ // In your request handler:
436
+ const crawlerInfo = detectCrawler(req.headers['user-agent'] ?? '');
437
+ logger.enqueue({
438
+ site_id: config.siteId,
439
+ timestamp: new Date().toISOString(),
440
+ method: req.method,
441
+ path: req.url,
442
+ status_code: res.statusCode,
443
+ response_time_ms: elapsedMs,
444
+ ip_address: req.socket.remoteAddress ?? '',
445
+ user_agent: req.headers['user-agent'] ?? '',
446
+ referer: req.headers.referer ?? null,
447
+ is_ai_crawler: crawlerInfo.isAi,
448
+ crawler_type: crawlerInfo.name,
449
+ crawler_organization: crawlerInfo.organization,
450
+ confidence_score: crawlerInfo.confidence,
451
+ });
452
+
453
+ // Flush on shutdown
454
+ process.on('SIGTERM', () => logger.flush());
455
+ ```
456
+
457
+ ---
458
+
459
+ ## Request logging behavior
460
+
461
+ - Logs are **batched in memory** and flushed every `flushInterval` ms (default 5s) or when `batchSize` entries accumulate (default 50)
462
+ - Logging is **non-blocking** — failures never propagate to the request handler
463
+ - The logger automatically flushes on `SIGTERM`, `SIGINT`, and `beforeExit`
464
+ - The flush timer is **unreferenced** — it will not keep a process alive after all other work is done
465
+
466
+ **Paths skipped by middleware (never logged):**
467
+ - `/_next/` — Next.js build assets
468
+ - `/api/_*` — Internal Next.js API routes
469
+ - `/favicon.ico`
470
+ - Static file extensions: `.svg`, `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.ico`, `.woff`, `.woff2`, `.ttf`, `.css`, `.js.map`
471
+
472
+ ---
473
+
474
+ ## Analytics dashboard
475
+
476
+ Visit your `dashboardUrl` after registration to:
477
+ - View AI vs human traffic split over time
478
+ - See which crawlers are visiting and how often
479
+ - Browse agent queries and the answers served
480
+ - Track USDC earnings from sponsored ads
481
+ - Manage API keys and site settings
482
+
483
+ The link is valid for 30 days. Add an email address in the dashboard to establish a permanent login.
484
+
485
+ ---
486
+
487
+ ## Testing
488
+
489
+ ```bash
490
+ npm test # Unit + integration (hits real dev API)
491
+ npm run test:unit # Unit tests only (mocked API)
492
+ npm run test:integration # Integration tests against deployed dev API
493
+ ```
494
+
495
+ **Integration tests** use **https://api.apptvty.com** by default: they register a new test site, send real logs, and verify analytics and migrate. No env vars required. Needs network access.
496
+
497
+ ---
498
+
499
+ ## Publishing the SDK (npm)
500
+
501
+ The SDK is an **npm package**. “Deploy” means publishing to the registry so users can `npm install apptvty`.
502
+
503
+ **Prerequisites**
504
+
505
+ - npm account ([npmjs.com](https://www.npmjs.com/signup))
506
+ - Logged in: `npm login`
507
+
508
+ **Release steps**
509
+
510
+ 1. **Bump version** in `package.json` (or use `npm version`):
511
+ ```bash
512
+ npm version patch # 0.1.0 → 0.1.1
513
+ # or: npm version minor # 0.1.0 → 0.2.0
514
+ ```
515
+
516
+ 2. **Run tests** (optional but recommended):
517
+ ```bash
518
+ npm run test:unit
519
+ npm run test:integration # uses api.apptvty.com
520
+ ```
521
+
522
+ 3. **Publish**:
523
+ ```bash
524
+ npm publish
525
+ ```
526
+ `prepublishOnly` runs `npm run build && npm run typecheck` automatically before the package is uploaded.
527
+
528
+ For a **scoped package** (e.g. `@apptvty/sdk`), make sure `package.json` has `"name": "@apptvty/sdk"` and use:
529
+ ```bash
530
+ npm publish --access public
531
+ ```
532
+
533
+ ---
534
+
535
+ ## License
536
+
537
+ MIT
@@ -0,0 +1,87 @@
1
+ import {
2
+ ApptvtyClient,
3
+ RequestLogger,
4
+ createQueryHandler,
5
+ detectCrawler,
6
+ getClientIp
7
+ } from "./chunk-WATTAPBA.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-RGUS6IL6.mjs.map
@@ -0,0 +1 @@
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":[]}