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
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/middleware/express.ts
|
|
21
21
|
var express_exports = {};
|
|
22
22
|
__export(express_exports, {
|
|
23
|
+
createExpressDashboardHandler: () => createExpressDashboardHandler,
|
|
23
24
|
createExpressMiddleware: () => createExpressMiddleware,
|
|
24
25
|
createExpressQueryHandler: () => createExpressQueryHandler
|
|
25
26
|
});
|
|
@@ -38,6 +39,13 @@ var ApptvtyClient = class {
|
|
|
38
39
|
this.siteId = config.siteId;
|
|
39
40
|
this.debug = config.debug ?? false;
|
|
40
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Set the X402 (LSAT) token for subsequent requests.
|
|
44
|
+
* This is called after the agent has successfully paid an X402 challenge.
|
|
45
|
+
*/
|
|
46
|
+
setX402Token(macaroon, preimage) {
|
|
47
|
+
this.x402Token = `LSAT ${macaroon}:${preimage}`;
|
|
48
|
+
}
|
|
41
49
|
/**
|
|
42
50
|
* Send a batch of request log entries to the Apptvty ingestion API.
|
|
43
51
|
* Called by the logger's auto-flush — not called directly by user code.
|
|
@@ -110,6 +118,20 @@ var ApptvtyClient = class {
|
|
|
110
118
|
async getSiteDailyStats(days = 30) {
|
|
111
119
|
return this.get(`/v1/sites/${this.siteId}/stats/daily`, { days: String(days) });
|
|
112
120
|
}
|
|
121
|
+
/** Get recent activity logs (last 48h). */
|
|
122
|
+
async getRecentActivity(siteId, limit = 50, offset = 0) {
|
|
123
|
+
return this.get(`/v1/sites/${siteId}/activity`, {
|
|
124
|
+
limit: String(limit),
|
|
125
|
+
offset: String(offset)
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/** Get recent agent queries. */
|
|
129
|
+
async getRecentQueries(siteId, limit = 20, offset = 0) {
|
|
130
|
+
return this.get(`/v1/sites/${siteId}/queries`, {
|
|
131
|
+
limit: String(limit),
|
|
132
|
+
offset: String(offset)
|
|
133
|
+
});
|
|
134
|
+
}
|
|
113
135
|
/** Get crawler breakdown by type (default 30 days). */
|
|
114
136
|
async getSiteCrawlers(days = 30) {
|
|
115
137
|
return this.get(`/v1/sites/${this.siteId}/crawlers`, { days: String(days) });
|
|
@@ -126,6 +148,106 @@ var ApptvtyClient = class {
|
|
|
126
148
|
async getSiteWallet() {
|
|
127
149
|
return this.get(`/v1/sites/${this.siteId}/wallet`);
|
|
128
150
|
}
|
|
151
|
+
// ─── Campaign management (for coding agents) ────────────────────────────────
|
|
152
|
+
//
|
|
153
|
+
// A coding agent that installed the SDK can create and manage ad campaigns
|
|
154
|
+
// programmatically — no human dashboard login required.
|
|
155
|
+
//
|
|
156
|
+
// Typical agentic advertiser flow:
|
|
157
|
+
// 1. Check wallet balance: const w = await client.getSiteWallet()
|
|
158
|
+
// 2. Fund wallet if needed: send USDC to w.wallet_address on Base chain
|
|
159
|
+
// 3. Create campaign: await client.createCampaign({ ... })
|
|
160
|
+
// 4. Monitor performance: await client.getCampaign(id)
|
|
161
|
+
// 5. Adjust or pause: await client.updateCampaign(id, { status: 'paused' })
|
|
162
|
+
/**
|
|
163
|
+
* Create an ad campaign. The campaign goes live immediately once the wallet
|
|
164
|
+
* has sufficient balance.
|
|
165
|
+
*
|
|
166
|
+
* If the wallet balance is too low, throws `ApptvtyInsufficientBalanceError`
|
|
167
|
+
* which includes deposit instructions so the agent can fund the wallet and retry.
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* // Site-based: ad copy derived from your site's crawled content
|
|
171
|
+
* const campaign = await client.createCampaign({
|
|
172
|
+
* name: 'My Kubernetes Blog',
|
|
173
|
+
* advertiser_site_id: 'site_abc123',
|
|
174
|
+
* keywords: ['kubernetes', 'devops', 'containers'],
|
|
175
|
+
* categories: ['technology'],
|
|
176
|
+
* bid_per_view_usdc: 0.001,
|
|
177
|
+
* daily_budget_usdc: 1.0,
|
|
178
|
+
* total_budget_usdc: 20.0,
|
|
179
|
+
* });
|
|
180
|
+
*
|
|
181
|
+
* // Static: manually written ad copy
|
|
182
|
+
* const campaign = await client.createCampaign({
|
|
183
|
+
* name: 'My Blog — Static',
|
|
184
|
+
* ad_text: 'Deep dives on Kubernetes, written by practitioners.',
|
|
185
|
+
* landing_url: 'https://myblog.com',
|
|
186
|
+
* keywords: ['kubernetes', 'devops'],
|
|
187
|
+
* categories: ['technology'],
|
|
188
|
+
* bid_per_view_usdc: 0.001,
|
|
189
|
+
* daily_budget_usdc: 1.0,
|
|
190
|
+
* total_budget_usdc: 10.0,
|
|
191
|
+
* });
|
|
192
|
+
*/
|
|
193
|
+
async createCampaign(params) {
|
|
194
|
+
try {
|
|
195
|
+
return await this.post("/v1/campaigns", params);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (err instanceof ApptvtyApiError && err.statusCode === 402) {
|
|
198
|
+
let details;
|
|
199
|
+
try {
|
|
200
|
+
const body = JSON.parse(err.body);
|
|
201
|
+
details = body?.error;
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
throw new ApptvtyInsufficientBalanceError(details);
|
|
205
|
+
}
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* List all campaigns for this account.
|
|
211
|
+
* Also returns the schema (valid categories, minimum bid) so the agent
|
|
212
|
+
* knows valid field values without guessing.
|
|
213
|
+
*/
|
|
214
|
+
async listCampaigns() {
|
|
215
|
+
return this.get("/v1/campaigns");
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Get a single campaign by ID, including current spend and impression count.
|
|
219
|
+
* `budget_remaining_usdc` tells the agent how much budget is left before
|
|
220
|
+
* the campaign auto-pauses.
|
|
221
|
+
*/
|
|
222
|
+
async getCampaign(campaignId) {
|
|
223
|
+
return this.get(`/v1/campaigns/${campaignId}`);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Partially update a campaign. Only fields present in `params` are changed.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* // Pause a campaign
|
|
230
|
+
* await client.updateCampaign(id, { status: 'paused' });
|
|
231
|
+
*
|
|
232
|
+
* // Increase bid and daily budget
|
|
233
|
+
* await client.updateCampaign(id, { bid_per_view_usdc: 0.002, daily_budget_usdc: 5.0 });
|
|
234
|
+
*/
|
|
235
|
+
async updateCampaign(campaignId, params) {
|
|
236
|
+
return this.patch(`/v1/campaigns/${campaignId}`, params);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Pause a campaign immediately. Equivalent to updateCampaign(id, { status: 'paused' }).
|
|
240
|
+
* Campaigns are never deleted — billing history is retained.
|
|
241
|
+
*/
|
|
242
|
+
async pauseCampaign(campaignId) {
|
|
243
|
+
await this.delete(`/v1/campaigns/${campaignId}`);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Resume a paused campaign.
|
|
247
|
+
*/
|
|
248
|
+
async resumeCampaign(campaignId) {
|
|
249
|
+
await this.patch(`/v1/campaigns/${campaignId}`, { status: "active" });
|
|
250
|
+
}
|
|
129
251
|
async get(path, params) {
|
|
130
252
|
const url = new URL(`${this.baseUrl}${path}`);
|
|
131
253
|
if (params) {
|
|
@@ -134,7 +256,7 @@ var ApptvtyClient = class {
|
|
|
134
256
|
const response = await fetch(url.toString(), {
|
|
135
257
|
method: "GET",
|
|
136
258
|
headers: {
|
|
137
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
259
|
+
Authorization: this.x402Token ?? `Bearer ${this.apiKey}`,
|
|
138
260
|
"User-Agent": "apptvty-sdk/0.1.0"
|
|
139
261
|
},
|
|
140
262
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -145,22 +267,91 @@ var ApptvtyClient = class {
|
|
|
145
267
|
}
|
|
146
268
|
return response.json();
|
|
147
269
|
}
|
|
270
|
+
async patch(path, body) {
|
|
271
|
+
const url = `${this.baseUrl}${path}`;
|
|
272
|
+
const response = await fetch(url, {
|
|
273
|
+
method: "PATCH",
|
|
274
|
+
headers: {
|
|
275
|
+
"Authorization": this.x402Token ?? `Bearer ${this.apiKey}`,
|
|
276
|
+
"Content-Type": "application/json",
|
|
277
|
+
"User-Agent": "apptvty-sdk/0.1.0"
|
|
278
|
+
},
|
|
279
|
+
body: JSON.stringify(body),
|
|
280
|
+
signal: AbortSignal.timeout(1e4)
|
|
281
|
+
});
|
|
282
|
+
if (!response.ok) {
|
|
283
|
+
const text = await response.text().catch(() => "");
|
|
284
|
+
throw new ApptvtyApiError(response.status, path, text);
|
|
285
|
+
}
|
|
286
|
+
return response.json();
|
|
287
|
+
}
|
|
288
|
+
async delete(path) {
|
|
289
|
+
const url = `${this.baseUrl}${path}`;
|
|
290
|
+
const response = await fetch(url, {
|
|
291
|
+
method: "DELETE",
|
|
292
|
+
headers: {
|
|
293
|
+
"Authorization": this.x402Token ?? `Bearer ${this.apiKey}`,
|
|
294
|
+
"User-Agent": "apptvty-sdk/0.1.0"
|
|
295
|
+
},
|
|
296
|
+
signal: AbortSignal.timeout(1e4)
|
|
297
|
+
});
|
|
298
|
+
if (!response.ok) {
|
|
299
|
+
const text = await response.text().catch(() => "");
|
|
300
|
+
throw new ApptvtyApiError(response.status, path, text);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Settle an X402 challenge from the site's own USDC wallet balance.
|
|
305
|
+
* Called automatically when `post()` receives a 402 with an X402 header.
|
|
306
|
+
* Returns the preimage on success, or throws if the wallet balance is too low.
|
|
307
|
+
*/
|
|
308
|
+
async payX402Challenge(macaroon) {
|
|
309
|
+
const url = `${this.baseUrl}/v1/x402/pay`;
|
|
310
|
+
const response = await fetch(url, {
|
|
311
|
+
method: "POST",
|
|
312
|
+
headers: {
|
|
313
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
314
|
+
"Content-Type": "application/json",
|
|
315
|
+
"User-Agent": "apptvty-sdk/0.1.0"
|
|
316
|
+
},
|
|
317
|
+
body: JSON.stringify({ macaroon }),
|
|
318
|
+
signal: AbortSignal.timeout(1e4)
|
|
319
|
+
});
|
|
320
|
+
const text = await response.text().catch(() => "");
|
|
321
|
+
if (!response.ok) {
|
|
322
|
+
let details;
|
|
323
|
+
try {
|
|
324
|
+
details = JSON.parse(text)?.error?.details;
|
|
325
|
+
} catch {
|
|
326
|
+
}
|
|
327
|
+
throw new ApptvtyInsufficientBalanceError(details);
|
|
328
|
+
}
|
|
329
|
+
const json = JSON.parse(text);
|
|
330
|
+
return json.preimage;
|
|
331
|
+
}
|
|
148
332
|
async post(path, body) {
|
|
149
333
|
const url = `${this.baseUrl}${path}`;
|
|
150
334
|
const response = await fetch(url, {
|
|
151
335
|
method: "POST",
|
|
152
336
|
headers: {
|
|
153
|
-
"Authorization": `Bearer ${this.apiKey}`,
|
|
337
|
+
"Authorization": this.x402Token ?? `Bearer ${this.apiKey}`,
|
|
154
338
|
"Content-Type": "application/json",
|
|
155
339
|
"User-Agent": "apptvty-sdk/0.1.0"
|
|
156
340
|
},
|
|
157
341
|
body: JSON.stringify(body),
|
|
158
|
-
// Node 18+ fetch: set a reasonable timeout via AbortSignal
|
|
159
342
|
signal: AbortSignal.timeout(1e4)
|
|
160
343
|
});
|
|
161
344
|
if (!response.ok) {
|
|
162
345
|
const text = await response.text().catch(() => "");
|
|
163
346
|
if (response.status === 402) {
|
|
347
|
+
const authHeader = response.headers.get("WWW-Authenticate");
|
|
348
|
+
if (authHeader?.includes("X402") || authHeader?.includes("LSAT")) {
|
|
349
|
+
const macaroon = authHeader.match(/macaroon="([^"]+)"/)?.[1] || "";
|
|
350
|
+
const preimage = await this.payX402Challenge(macaroon);
|
|
351
|
+
this.setX402Token(macaroon, preimage);
|
|
352
|
+
this.log("X402 challenge auto-paid from wallet, retrying request");
|
|
353
|
+
return this.post(path, body);
|
|
354
|
+
}
|
|
164
355
|
let dashboardUrl = "https://dashboard.apptvty.com/login";
|
|
165
356
|
try {
|
|
166
357
|
const json = JSON.parse(text);
|
|
@@ -196,6 +387,16 @@ var ApptvtyTrialExpiredError = class extends Error {
|
|
|
196
387
|
this.name = "ApptvtyTrialExpiredError";
|
|
197
388
|
}
|
|
198
389
|
};
|
|
390
|
+
var ApptvtyInsufficientBalanceError = class extends Error {
|
|
391
|
+
constructor(details) {
|
|
392
|
+
const shortfall = details?.shortfall_usdc ?? "?";
|
|
393
|
+
super(
|
|
394
|
+
`Wallet balance too low to create campaign. Send ${shortfall} USDC to ${details?.deposit?.address ?? "your wallet"} on Base and retry.`
|
|
395
|
+
);
|
|
396
|
+
this.details = details;
|
|
397
|
+
this.name = "ApptvtyInsufficientBalanceError";
|
|
398
|
+
}
|
|
399
|
+
};
|
|
199
400
|
|
|
200
401
|
// src/crawler.ts
|
|
201
402
|
var KNOWN_CRAWLERS = [
|
|
@@ -305,6 +506,27 @@ function extractBotName(userAgent) {
|
|
|
305
506
|
}
|
|
306
507
|
return "unknown_ai_bot";
|
|
307
508
|
}
|
|
509
|
+
var SCRAPER_SERVICES = [
|
|
510
|
+
// Jina AI Reader — r.jina.ai/URL — converts any page to clean Markdown
|
|
511
|
+
{ name: "JinaReader", patterns: [/JinaReader/i] },
|
|
512
|
+
// Cloudflare Browser Rendering /crawl endpoint (open beta, announced March 2026)
|
|
513
|
+
{ name: "Cloudflare-BrowserRendering", patterns: [/CloudflareBrowserRenderingCrawler/i] },
|
|
514
|
+
// FireCrawl — LLM-ready content extraction service
|
|
515
|
+
{ name: "FireCrawl", patterns: [/FireCrawlAgent/i, /firecrawl/i] },
|
|
516
|
+
// Apify web scraping platform
|
|
517
|
+
{ name: "Apify", patterns: [/ApifyBot/i] }
|
|
518
|
+
];
|
|
519
|
+
function detectScraperService(userAgent) {
|
|
520
|
+
if (!userAgent) return { isScraperService: false, name: null };
|
|
521
|
+
for (const service of SCRAPER_SERVICES) {
|
|
522
|
+
for (const pattern of service.patterns) {
|
|
523
|
+
if (pattern.test(userAgent)) {
|
|
524
|
+
return { isScraperService: true, name: service.name };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return { isScraperService: false, name: null };
|
|
529
|
+
}
|
|
308
530
|
|
|
309
531
|
// src/logger.ts
|
|
310
532
|
var RequestLogger = class {
|
|
@@ -521,6 +743,354 @@ function getOrigin(url) {
|
|
|
521
743
|
}
|
|
522
744
|
}
|
|
523
745
|
|
|
746
|
+
// src/dashboard-handler.ts
|
|
747
|
+
function createDashboardHandler(client, config) {
|
|
748
|
+
return async function handleDashboard(req) {
|
|
749
|
+
const { path, method } = req;
|
|
750
|
+
if (path.endsWith("/api/overview")) {
|
|
751
|
+
const data = await client.getSiteStats();
|
|
752
|
+
return jsonResponse(200, data);
|
|
753
|
+
}
|
|
754
|
+
if (path.endsWith("/api/activity")) {
|
|
755
|
+
const url = new URL(path, "http://localhost");
|
|
756
|
+
const limit = parseInt(url.searchParams.get("limit") || "50");
|
|
757
|
+
const offset = parseInt(url.searchParams.get("offset") || "0");
|
|
758
|
+
const data = await client.getRecentActivity(config.siteId, limit, offset);
|
|
759
|
+
return jsonResponse(200, data);
|
|
760
|
+
}
|
|
761
|
+
if (path.endsWith("/api/queries")) {
|
|
762
|
+
const url = new URL(path, "http://localhost");
|
|
763
|
+
const limit = parseInt(url.searchParams.get("limit") || "20");
|
|
764
|
+
const offset = parseInt(url.searchParams.get("offset") || "0");
|
|
765
|
+
const data = await client.getRecentQueries(config.siteId, limit, offset);
|
|
766
|
+
return jsonResponse(200, data);
|
|
767
|
+
}
|
|
768
|
+
if (path.endsWith("/api/stats")) {
|
|
769
|
+
const data = await client.getSiteDailyStats();
|
|
770
|
+
return jsonResponse(200, data);
|
|
771
|
+
}
|
|
772
|
+
const html = getDashboardHtml(config);
|
|
773
|
+
return {
|
|
774
|
+
status: 200,
|
|
775
|
+
body: html,
|
|
776
|
+
headers: { "Content-Type": "text/html" }
|
|
777
|
+
};
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function jsonResponse(status, data) {
|
|
781
|
+
return {
|
|
782
|
+
status,
|
|
783
|
+
body: JSON.stringify(data),
|
|
784
|
+
headers: { "Content-Type": "application/json" }
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
function getDashboardHtml(config) {
|
|
788
|
+
return `
|
|
789
|
+
<!DOCTYPE html>
|
|
790
|
+
<html lang="en" class="dark">
|
|
791
|
+
<head>
|
|
792
|
+
<meta charset="UTF-8">
|
|
793
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
794
|
+
<title>Apptvty Logs \u2014 ${config.siteId}</title>
|
|
795
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
796
|
+
<script src="https://unpkg.com/lucide@latest"></script>
|
|
797
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
798
|
+
<style>
|
|
799
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
|
800
|
+
body { font-family: 'Inter', sans-serif; background-color: #09090b; color: #fafafa; }
|
|
801
|
+
.glass { background: rgba(24, 24, 27, 0.8); backdrop-filter: blur(12px); border: 1px solid rgba(39, 39, 42, 1); }
|
|
802
|
+
.gradient-text { background: linear-gradient(to right, #60a5fa, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
803
|
+
</style>
|
|
804
|
+
</head>
|
|
805
|
+
<body class="p-6">
|
|
806
|
+
<div id="app" class="max-w-7xl mx-auto space-y-6">
|
|
807
|
+
<!-- Header -->
|
|
808
|
+
<header class="flex justify-between items-center mb-8">
|
|
809
|
+
<div>
|
|
810
|
+
<h1 class="text-3xl font-bold gradient-text">Activity Logs</h1>
|
|
811
|
+
<p class="text-zinc-400">Real-time agentic insights for ${config.siteId}</p>
|
|
812
|
+
</div>
|
|
813
|
+
<div class="flex items-center gap-3">
|
|
814
|
+
<span class="flex h-2 w-2 rounded-full bg-green-500 animate-pulse"></span>
|
|
815
|
+
<span class="text-sm font-medium text-zinc-300">Live Connection</span>
|
|
816
|
+
</div>
|
|
817
|
+
</header>
|
|
818
|
+
|
|
819
|
+
<!-- Stats Grid -->
|
|
820
|
+
<div class="grid grid-cols-1 md:grid-cols-4 gap-4" id="stats-grid">
|
|
821
|
+
<div class="glass p-5 rounded-2xl animate-pulse h-24"></div>
|
|
822
|
+
<div class="glass p-5 rounded-2xl animate-pulse h-24"></div>
|
|
823
|
+
<div class="glass p-5 rounded-2xl animate-pulse h-24"></div>
|
|
824
|
+
<div class="glass p-5 rounded-2xl animate-pulse h-24"></div>
|
|
825
|
+
</div>
|
|
826
|
+
|
|
827
|
+
<!-- Main Content -->
|
|
828
|
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
829
|
+
<!-- Traffic Chart -->
|
|
830
|
+
<div class="lg:col-span-2 glass p-6 rounded-2xl">
|
|
831
|
+
<h2 class="text-lg font-semibold mb-4">Traffic Overview</h2>
|
|
832
|
+
<div class="h-[300px]">
|
|
833
|
+
<canvas id="trafficChart"></canvas>
|
|
834
|
+
</div>
|
|
835
|
+
</div>
|
|
836
|
+
|
|
837
|
+
<!-- Recent Queries -->
|
|
838
|
+
<div class="glass p-6 rounded-2xl flex flex-col h-[400px]">
|
|
839
|
+
<div class="flex justify-between items-center mb-4">
|
|
840
|
+
<h2 class="text-lg font-semibold">Agent Queries</h2>
|
|
841
|
+
<button id="load-more-queries" class="text-xs text-blue-400 hover:text-blue-300 transition-colors">Load More</button>
|
|
842
|
+
</div>
|
|
843
|
+
<div id="queries-list" class="flex-1 overflow-y-auto space-y-3 custom-scrollbar">
|
|
844
|
+
<div class="text-zinc-500 text-sm italic">Loading queries...</div>
|
|
845
|
+
</div>
|
|
846
|
+
</div>
|
|
847
|
+
</div>
|
|
848
|
+
|
|
849
|
+
<!-- Real-time Activity Table -->
|
|
850
|
+
<div class="glass p-6 rounded-2xl overflow-hidden">
|
|
851
|
+
<div class="flex justify-between items-center mb-4">
|
|
852
|
+
<h2 class="text-lg font-semibold">Real-time Activity</h2>
|
|
853
|
+
<button id="load-more-activity" class="text-sm px-3 py-1 bg-zinc-800 hover:bg-zinc-700 rounded-lg text-zinc-300 transition-colors">Load More</button>
|
|
854
|
+
</div>
|
|
855
|
+
<div class="overflow-x-auto">
|
|
856
|
+
<table class="w-full text-left">
|
|
857
|
+
<thead>
|
|
858
|
+
<tr class="text-zinc-500 text-sm border-b border-zinc-800">
|
|
859
|
+
<th class="pb-3 pr-4">Timestamp</th>
|
|
860
|
+
<th class="pb-3 pr-4">Method</th>
|
|
861
|
+
<th class="pb-3 pr-4">Path</th>
|
|
862
|
+
<th class="pb-3 pr-4">Agent</th>
|
|
863
|
+
<th class="pb-3">Status</th>
|
|
864
|
+
</tr>
|
|
865
|
+
</thead>
|
|
866
|
+
<tbody id="activity-body" class="text-sm">
|
|
867
|
+
<tr><td colspan="5" class="pt-4 text-zinc-500 italic">Connecting to activity stream...</td></tr>
|
|
868
|
+
</tbody>
|
|
869
|
+
</table>
|
|
870
|
+
</div>
|
|
871
|
+
</div>
|
|
872
|
+
</div>
|
|
873
|
+
|
|
874
|
+
<script>
|
|
875
|
+
const API_BASE = window.location.pathname.replace(//$/, '');
|
|
876
|
+
let queryOffset = 0;
|
|
877
|
+
let activityOffset = 0;
|
|
878
|
+
const LIMIT_QUERIES = 20;
|
|
879
|
+
const LIMIT_ACTIVITY = 50;
|
|
880
|
+
|
|
881
|
+
async function fetchData() {
|
|
882
|
+
try {
|
|
883
|
+
// Initial load or refresh (offset 0 resets)
|
|
884
|
+
const [overview, stats] = await Promise.all([
|
|
885
|
+
fetch(\`\${API_BASE}/api/overview\`).then(r => r.json()),
|
|
886
|
+
fetch(\`\${API_BASE}/api/stats\`).then(r => r.json())
|
|
887
|
+
]);
|
|
888
|
+
|
|
889
|
+
updateStats(overview);
|
|
890
|
+
initChart(stats);
|
|
891
|
+
|
|
892
|
+
// Fetch first pages if empty
|
|
893
|
+
if (queryOffset === 0) fetchQueries(true);
|
|
894
|
+
if (activityOffset === 0) fetchActivity(true);
|
|
895
|
+
} catch (err) {
|
|
896
|
+
console.error('Failed to fetch dashboard data:', err);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
async function fetchQueries(replace = false) {
|
|
901
|
+
try {
|
|
902
|
+
const data = await fetch(\`\${API_BASE}/api/queries?limit=\${LIMIT_QUERIES}&offset=\${queryOffset}\`).then(r => r.json());
|
|
903
|
+
updateQueries(data, replace);
|
|
904
|
+
} catch (e) { console.error('Queries error:', e); }
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
async function fetchActivity(replace = false) {
|
|
908
|
+
try {
|
|
909
|
+
const data = await fetch(\`\${API_BASE}/api/activity?limit=\${LIMIT_ACTIVITY}&offset=\${activityOffset}\`).then(r => r.json());
|
|
910
|
+
updateActivity(data, replace);
|
|
911
|
+
} catch (e) { console.error('Activity error:', e); }
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function updateStats(data) {
|
|
915
|
+
const grid = document.getElementById('stats-grid');
|
|
916
|
+
grid.innerHTML = \`
|
|
917
|
+
<div class="glass p-5 rounded-2xl">
|
|
918
|
+
<p class="text-zinc-400 text-xs font-medium uppercase tracking-wider mb-1">Total Requests</p>
|
|
919
|
+
<p class="text-2xl font-bold">\${data.total_requests_30d.toLocaleString()}</p>
|
|
920
|
+
</div>
|
|
921
|
+
<div class="glass p-5 rounded-2xl border-l-4 border-blue-500">
|
|
922
|
+
<p class="text-zinc-400 text-xs font-medium uppercase tracking-wider mb-1">AI Requests</p>
|
|
923
|
+
<p class="text-2xl font-bold">\${data.ai_requests_30d.toLocaleString()}</p>
|
|
924
|
+
</div>
|
|
925
|
+
<div class="glass p-5 rounded-2xl">
|
|
926
|
+
<p class="text-zinc-400 text-xs font-medium uppercase tracking-wider mb-1">AI Percentage</p>
|
|
927
|
+
<p class="text-2xl font-bold text-blue-400">\${data.ai_percentage.toFixed(1)}%</p>
|
|
928
|
+
</div>
|
|
929
|
+
<div class="glass p-5 rounded-2xl">
|
|
930
|
+
<p class="text-zinc-400 text-xs font-medium uppercase tracking-wider mb-1">Health Status</p>
|
|
931
|
+
<p class="text-2xl font-bold text-green-400">Optimal</p>
|
|
932
|
+
</div>
|
|
933
|
+
\`;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function updateQueries(data, replace) {
|
|
937
|
+
const list = document.getElementById('queries-list');
|
|
938
|
+
const items = Array.isArray(data) ? data : (data.queries || []);
|
|
939
|
+
const rows = items.map(q => \`
|
|
940
|
+
<div class="p-3 bg-zinc-900/50 rounded-xl border border-zinc-800 hover:border-zinc-700 transition-colors">
|
|
941
|
+
<p class="text-sm font-medium text-zinc-200">\${q.question || q.query}</p>
|
|
942
|
+
<div class="mt-2 flex justify-between items-center text-[10px] text-zinc-500">
|
|
943
|
+
<span class="flex items-center gap-1">
|
|
944
|
+
<i data-lucide="clock" class="w-3 h-3"></i>
|
|
945
|
+
\${new Date(q.timestamp).toLocaleString()}
|
|
946
|
+
</span>
|
|
947
|
+
<span class="px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-400 border border-blue-500/20">
|
|
948
|
+
Agent Match
|
|
949
|
+
</span>
|
|
950
|
+
</div>
|
|
951
|
+
</div>
|
|
952
|
+
\`).join('');
|
|
953
|
+
|
|
954
|
+
if (replace) list.innerHTML = rows || '<div class="text-zinc-600 text-sm">No recent queries.</div>';
|
|
955
|
+
else list.insertAdjacentHTML('beforeend', rows);
|
|
956
|
+
lucide.createIcons();
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function updateActivity(data, replace) {
|
|
960
|
+
const body = document.getElementById('activity-body');
|
|
961
|
+
const items = Array.isArray(data) ? data : (data.activity || []);
|
|
962
|
+
const rows = items.map(r => \`
|
|
963
|
+
<tr class="border-b border-zinc-800/50 hover:bg-zinc-800/20 transition-colors">
|
|
964
|
+
<td class="py-3 pr-4 text-zinc-400 text-xs">\${new Date(r.timestamp || r.created_at).toLocaleTimeString()}</td>
|
|
965
|
+
<td class="py-3 pr-4 font-mono text-[10px] tracking-tight text-blue-400">\${r.method}</td>
|
|
966
|
+
<td class="py-3 pr-4 text-zinc-300 max-w-xs truncate">\${r.path}</td>
|
|
967
|
+
<td class="py-3 pr-4 text-zinc-500">\${r.crawler_type || 'Human'}</td>
|
|
968
|
+
<td class="py-3">
|
|
969
|
+
<span class="px-2 py-0.5 rounded-md \${r.status_code >= 400 ? 'bg-red-500/10 text-red-400' : 'bg-green-500/10 text-green-400'} text-[10px] font-medium border border-current/20">
|
|
970
|
+
\${r.status_code || 200}
|
|
971
|
+
</span>
|
|
972
|
+
</td>
|
|
973
|
+
</tr>
|
|
974
|
+
\`).join('');
|
|
975
|
+
|
|
976
|
+
if (replace) body.innerHTML = rows || '<tr><td colspan="5" class="py-4 text-center text-zinc-600">No activity.</td></tr>';
|
|
977
|
+
else body.insertAdjacentHTML('beforeend', rows);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function initChart(stats) {
|
|
981
|
+
const canvas = document.getElementById('trafficChart');
|
|
982
|
+
if (window.myChart) window.myChart.destroy();
|
|
983
|
+
const ctx = canvas.getContext('2d');
|
|
984
|
+
window.myChart = new Chart(ctx, {
|
|
985
|
+
type: 'line',
|
|
986
|
+
data: {
|
|
987
|
+
labels: stats.map(s => s.date.split('-').slice(1).join('/')),
|
|
988
|
+
datasets: [
|
|
989
|
+
{
|
|
990
|
+
label: 'AI Requests',
|
|
991
|
+
data: stats.map(s => s.ai_requests),
|
|
992
|
+
borderColor: '#3b82f6',
|
|
993
|
+
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
994
|
+
fill: true,
|
|
995
|
+
tension: 0.4
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
label: 'Total Requests',
|
|
999
|
+
data: stats.map(s => s.total_requests),
|
|
1000
|
+
borderColor: '#8b5cf6',
|
|
1001
|
+
backgroundColor: 'rgba(139, 92, 246, 0.1)',
|
|
1002
|
+
fill: true,
|
|
1003
|
+
tension: 0.4
|
|
1004
|
+
}
|
|
1005
|
+
]
|
|
1006
|
+
},
|
|
1007
|
+
options: {
|
|
1008
|
+
responsive: true,
|
|
1009
|
+
maintainAspectRatio: false,
|
|
1010
|
+
plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false } },
|
|
1011
|
+
scales: {
|
|
1012
|
+
y: { beginAtZero: true, grid: { color: 'rgba(39, 39, 42, 0.5)' }, ticks: { color: '#71717a' } },
|
|
1013
|
+
x: { grid: { display: false }, ticks: { color: '#71717a' } }
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
document.getElementById('load-more-queries').onclick = () => {
|
|
1020
|
+
queryOffset += LIMIT_QUERIES;
|
|
1021
|
+
fetchQueries(false);
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
document.getElementById('load-more-activity').onclick = () => {
|
|
1025
|
+
activityOffset += LIMIT_ACTIVITY;
|
|
1026
|
+
fetchActivity(false);
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
fetchData();
|
|
1030
|
+
setInterval(fetchData, 30000); // Polling for updates (stats only)
|
|
1031
|
+
</script>
|
|
1032
|
+
</body>
|
|
1033
|
+
</html>
|
|
1034
|
+
`;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// src/ad-injection.ts
|
|
1038
|
+
var AD_INJECTION_MARKER = "<!-- apptvty-sponsored -->";
|
|
1039
|
+
function injectIntoHtml(html, ads, isScraperService) {
|
|
1040
|
+
if (!html || ads.length === 0) return html;
|
|
1041
|
+
if (html.includes(AD_INJECTION_MARKER)) return html;
|
|
1042
|
+
let modified = html;
|
|
1043
|
+
const contentBlock = buildContentStreamBlock(ads);
|
|
1044
|
+
if (modified.includes("</article>")) {
|
|
1045
|
+
modified = modified.replace("</article>", `${contentBlock}
|
|
1046
|
+
</article>`);
|
|
1047
|
+
} else if (modified.includes("</main>")) {
|
|
1048
|
+
modified = modified.replace("</main>", `${contentBlock}
|
|
1049
|
+
</main>`);
|
|
1050
|
+
} else if (!isScraperService && modified.includes("</body>")) {
|
|
1051
|
+
modified = modified.replace("</body>", `${contentBlock}
|
|
1052
|
+
</body>`);
|
|
1053
|
+
}
|
|
1054
|
+
if (!isScraperService && modified.includes("</head>")) {
|
|
1055
|
+
const jsonLdBlock = buildJsonLdBlock(ads);
|
|
1056
|
+
modified = modified.replace("</head>", `${jsonLdBlock}
|
|
1057
|
+
</head>`);
|
|
1058
|
+
}
|
|
1059
|
+
return modified;
|
|
1060
|
+
}
|
|
1061
|
+
function buildSponsoredHeader(ads) {
|
|
1062
|
+
return JSON.stringify(
|
|
1063
|
+
ads.map((ad) => ({ text: ad.text, url: ad.url, advertiser: ad.advertiser }))
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
function buildContentStreamBlock(ads) {
|
|
1067
|
+
const paragraphs = ads.map(
|
|
1068
|
+
(ad) => `<p data-apptvty-sponsored="${escapeAttr(ad.impression_id)}"><strong>[Sponsored]</strong> <a href="${escapeAttr(ad.url)}" rel="sponsored noopener">${escapeHtml(ad.text)}</a> \u2014 <span>${escapeHtml(ad.advertiser)}</span></p>`
|
|
1069
|
+
).join("\n");
|
|
1070
|
+
return `${AD_INJECTION_MARKER}
|
|
1071
|
+
${paragraphs}`;
|
|
1072
|
+
}
|
|
1073
|
+
function buildJsonLdBlock(ads) {
|
|
1074
|
+
const entries = ads.map((ad) => ({
|
|
1075
|
+
"@context": "https://schema.org",
|
|
1076
|
+
"@type": "WPAdBlock",
|
|
1077
|
+
sponsor: {
|
|
1078
|
+
"@type": "Organization",
|
|
1079
|
+
name: ad.advertiser,
|
|
1080
|
+
url: ad.url
|
|
1081
|
+
},
|
|
1082
|
+
description: ad.text
|
|
1083
|
+
}));
|
|
1084
|
+
const ld = entries.length === 1 ? entries[0] : entries;
|
|
1085
|
+
return `<script type="application/ld+json">${JSON.stringify(ld)}</script>`;
|
|
1086
|
+
}
|
|
1087
|
+
function escapeHtml(s) {
|
|
1088
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1089
|
+
}
|
|
1090
|
+
function escapeAttr(s) {
|
|
1091
|
+
return s.replace(/"/g, """).replace(/'/g, "'");
|
|
1092
|
+
}
|
|
1093
|
+
|
|
524
1094
|
// src/middleware/express.ts
|
|
525
1095
|
var instances = /* @__PURE__ */ new Map();
|
|
526
1096
|
function getInstance(config) {
|
|
@@ -533,12 +1103,77 @@ function getInstance(config) {
|
|
|
533
1103
|
return instances.get(key);
|
|
534
1104
|
}
|
|
535
1105
|
function createExpressMiddleware(config) {
|
|
536
|
-
const { logger } = getInstance(config);
|
|
537
|
-
return function
|
|
1106
|
+
const { client, logger } = getInstance(config);
|
|
1107
|
+
return function apptvtyMiddleware(req, res, next) {
|
|
538
1108
|
const startMs = Date.now();
|
|
539
1109
|
const userAgent = req.headers["user-agent"] ?? "";
|
|
540
1110
|
const crawlerInfo = detectCrawler(userAgent);
|
|
1111
|
+
const scraperService = detectScraperService(userAgent);
|
|
541
1112
|
const path = req.url ?? "/";
|
|
1113
|
+
const isCrawler = crawlerInfo.isAi || scraperService.isScraperService;
|
|
1114
|
+
const ipAddress = getClientIp(req.headers);
|
|
1115
|
+
const adsPromise = isCrawler && !shouldSkip(path) ? client.getAdsForPage({ site_id: config.siteId, page_path: path }).catch(() => ({ ads: [] })) : Promise.resolve({ ads: [] });
|
|
1116
|
+
if (isCrawler && !shouldSkip(path)) {
|
|
1117
|
+
const chunks = [];
|
|
1118
|
+
const originalWrite = res.write.bind(res);
|
|
1119
|
+
const originalEnd = res.end.bind(res);
|
|
1120
|
+
res.write = function(chunk, encodingOrCallback, callback) {
|
|
1121
|
+
if (chunk != null) {
|
|
1122
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1123
|
+
}
|
|
1124
|
+
if (typeof encodingOrCallback === "function") encodingOrCallback();
|
|
1125
|
+
else if (typeof callback === "function") callback();
|
|
1126
|
+
return true;
|
|
1127
|
+
};
|
|
1128
|
+
res.end = function(chunk, encodingOrCallback, callback) {
|
|
1129
|
+
if (chunk != null) {
|
|
1130
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1131
|
+
}
|
|
1132
|
+
const contentType = res.getHeader("content-type") ?? "";
|
|
1133
|
+
const isHtml = contentType.includes("text/html");
|
|
1134
|
+
if (!isHtml || chunks.length === 0) {
|
|
1135
|
+
res.write = originalWrite;
|
|
1136
|
+
res.end = originalEnd;
|
|
1137
|
+
return originalEnd(Buffer.concat(chunks), encodingOrCallback, callback);
|
|
1138
|
+
}
|
|
1139
|
+
const html = Buffer.concat(chunks).toString("utf-8");
|
|
1140
|
+
if (html.includes(AD_INJECTION_MARKER)) {
|
|
1141
|
+
res.write = originalWrite;
|
|
1142
|
+
res.end = originalEnd;
|
|
1143
|
+
return originalEnd(html, encodingOrCallback, callback);
|
|
1144
|
+
}
|
|
1145
|
+
adsPromise.then((pageAds) => {
|
|
1146
|
+
res.write = originalWrite;
|
|
1147
|
+
res.end = originalEnd;
|
|
1148
|
+
if (!pageAds.ads || pageAds.ads.length === 0) {
|
|
1149
|
+
originalEnd(html, encodingOrCallback, callback);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
const modified = injectIntoHtml(html, pageAds.ads, scraperService.isScraperService);
|
|
1153
|
+
res.setHeader("X-Sponsored-Content", buildSponsoredHeader(pageAds.ads));
|
|
1154
|
+
const buf = Buffer.from(modified, "utf-8");
|
|
1155
|
+
res.setHeader("Content-Length", buf.length);
|
|
1156
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1157
|
+
for (const ad of pageAds.ads) {
|
|
1158
|
+
client.logImpression({
|
|
1159
|
+
impression_id: ad.impression_id,
|
|
1160
|
+
site_id: config.siteId,
|
|
1161
|
+
page_path: path,
|
|
1162
|
+
agent_ua: userAgent,
|
|
1163
|
+
agent_ip: ipAddress,
|
|
1164
|
+
timestamp
|
|
1165
|
+
}).catch(() => {
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
originalEnd(buf, encodingOrCallback, callback);
|
|
1169
|
+
}).catch(() => {
|
|
1170
|
+
res.write = originalWrite;
|
|
1171
|
+
res.end = originalEnd;
|
|
1172
|
+
originalEnd(html, encodingOrCallback, callback);
|
|
1173
|
+
});
|
|
1174
|
+
return res;
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
542
1177
|
res.on("finish", () => {
|
|
543
1178
|
if (shouldSkip(path)) return;
|
|
544
1179
|
const entry = {
|
|
@@ -548,13 +1183,14 @@ function createExpressMiddleware(config) {
|
|
|
548
1183
|
path,
|
|
549
1184
|
status_code: res.statusCode,
|
|
550
1185
|
response_time_ms: Date.now() - startMs,
|
|
551
|
-
ip_address:
|
|
1186
|
+
ip_address: ipAddress,
|
|
552
1187
|
user_agent: userAgent,
|
|
553
1188
|
referer: req.headers["referer"] ?? null,
|
|
554
1189
|
is_ai_crawler: crawlerInfo.isAi,
|
|
555
1190
|
crawler_type: crawlerInfo.name,
|
|
556
1191
|
crawler_organization: crawlerInfo.organization,
|
|
557
|
-
confidence_score: crawlerInfo.confidence
|
|
1192
|
+
confidence_score: crawlerInfo.confidence,
|
|
1193
|
+
scraper_service: scraperService.name
|
|
558
1194
|
};
|
|
559
1195
|
logger.enqueue(entry);
|
|
560
1196
|
});
|
|
@@ -587,6 +1223,24 @@ function createExpressQueryHandler(config) {
|
|
|
587
1223
|
res.end(JSON.stringify(result.body));
|
|
588
1224
|
};
|
|
589
1225
|
}
|
|
1226
|
+
function createExpressDashboardHandler(config) {
|
|
1227
|
+
const { client } = getInstance(config);
|
|
1228
|
+
const handleDashboard = createDashboardHandler(client, config);
|
|
1229
|
+
return async function dashboardHandler(req, res) {
|
|
1230
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
1231
|
+
const result = await handleDashboard({
|
|
1232
|
+
path: url.pathname + url.search,
|
|
1233
|
+
method: req.method ?? "GET",
|
|
1234
|
+
apiKey: config.apiKey,
|
|
1235
|
+
siteId: config.siteId
|
|
1236
|
+
});
|
|
1237
|
+
for (const [key, value] of Object.entries(result.headers)) {
|
|
1238
|
+
res.setHeader(key, value);
|
|
1239
|
+
}
|
|
1240
|
+
res.statusCode = result.status;
|
|
1241
|
+
res.end(result.body);
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
590
1244
|
function parseBoolParam(value, defaultValue) {
|
|
591
1245
|
if (value === null) return defaultValue;
|
|
592
1246
|
return value === "1" || value === "true" || value === "yes";
|
|
@@ -596,6 +1250,7 @@ function shouldSkip(path) {
|
|
|
596
1250
|
}
|
|
597
1251
|
// Annotate the CommonJS export names for ESM import in node:
|
|
598
1252
|
0 && (module.exports = {
|
|
1253
|
+
createExpressDashboardHandler,
|
|
599
1254
|
createExpressMiddleware,
|
|
600
1255
|
createExpressQueryHandler
|
|
601
1256
|
});
|