apptvty 0.1.4 → 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.
Files changed (37) hide show
  1. package/README.md +37 -17
  2. package/dist/chunk-2KXDQCUZ.mjs +177 -0
  3. package/dist/chunk-2KXDQCUZ.mjs.map +1 -0
  4. package/dist/{chunk-XOWRKLFM.mjs → chunk-454YBHM2.mjs} +62 -34
  5. package/dist/chunk-454YBHM2.mjs.map +1 -0
  6. package/dist/chunk-OTPVLSG5.mjs +1084 -0
  7. package/dist/chunk-OTPVLSG5.mjs.map +1 -0
  8. package/dist/cli.js +75 -16
  9. package/dist/index.d.mts +138 -20
  10. package/dist/index.d.ts +138 -20
  11. package/dist/index.js +403 -45
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +7 -3
  14. package/dist/middleware/express.d.mts +24 -5
  15. package/dist/middleware/express.d.ts +24 -5
  16. package/dist/middleware/express.js +676 -14
  17. package/dist/middleware/express.js.map +1 -1
  18. package/dist/middleware/express.mjs +4 -2
  19. package/dist/middleware/nextjs.d.mts +29 -6
  20. package/dist/middleware/nextjs.d.ts +29 -6
  21. package/dist/middleware/nextjs.js +641 -40
  22. package/dist/middleware/nextjs.js.map +1 -1
  23. package/dist/middleware/nextjs.mjs +4 -2
  24. package/dist/setup.d.mts +9 -0
  25. package/dist/setup.d.ts +9 -0
  26. package/dist/setup.js +6 -2
  27. package/dist/setup.js.map +1 -1
  28. package/dist/setup.mjs +6 -2
  29. package/dist/setup.mjs.map +1 -1
  30. package/dist/{types-C1oUTCsT.d.mts → types-D2A_0sPm.d.mts} +116 -2
  31. package/dist/{types-C1oUTCsT.d.ts → types-D2A_0sPm.d.ts} +116 -2
  32. package/package.json +1 -1
  33. package/dist/chunk-RGUS6IL6.mjs +0 -87
  34. package/dist/chunk-RGUS6IL6.mjs.map +0 -1
  35. package/dist/chunk-WATTAPBA.mjs +0 -502
  36. package/dist/chunk-WATTAPBA.mjs.map +0 -1
  37. package/dist/chunk-XOWRKLFM.mjs.map +0 -1
package/dist/index.js CHANGED
@@ -22,12 +22,14 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  ApptvtyApiError: () => ApptvtyApiError,
24
24
  ApptvtyClient: () => ApptvtyClient,
25
+ ApptvtyInsufficientBalanceError: () => ApptvtyInsufficientBalanceError,
25
26
  RequestLogger: () => RequestLogger,
26
27
  createExpressMiddleware: () => createExpressMiddleware,
27
28
  createExpressQueryHandler: () => createExpressQueryHandler,
28
29
  createNextjsQueryHandler: () => createNextjsQueryHandler,
29
30
  createQueryHandler: () => createQueryHandler,
30
31
  detectCrawler: () => detectCrawler,
32
+ detectScraperService: () => detectScraperService,
31
33
  getKnownCrawlerNames: () => getKnownCrawlerNames,
32
34
  withApptvty: () => withApptvty
33
35
  });
@@ -46,6 +48,13 @@ var ApptvtyClient = class {
46
48
  this.siteId = config.siteId;
47
49
  this.debug = config.debug ?? false;
48
50
  }
51
+ /**
52
+ * Set the X402 (LSAT) token for subsequent requests.
53
+ * This is called after the agent has successfully paid an X402 challenge.
54
+ */
55
+ setX402Token(macaroon, preimage) {
56
+ this.x402Token = `LSAT ${macaroon}:${preimage}`;
57
+ }
49
58
  /**
50
59
  * Send a batch of request log entries to the Apptvty ingestion API.
51
60
  * Called by the logger's auto-flush — not called directly by user code.
@@ -118,6 +127,20 @@ var ApptvtyClient = class {
118
127
  async getSiteDailyStats(days = 30) {
119
128
  return this.get(`/v1/sites/${this.siteId}/stats/daily`, { days: String(days) });
120
129
  }
130
+ /** Get recent activity logs (last 48h). */
131
+ async getRecentActivity(siteId, limit = 50, offset = 0) {
132
+ return this.get(`/v1/sites/${siteId}/activity`, {
133
+ limit: String(limit),
134
+ offset: String(offset)
135
+ });
136
+ }
137
+ /** Get recent agent queries. */
138
+ async getRecentQueries(siteId, limit = 20, offset = 0) {
139
+ return this.get(`/v1/sites/${siteId}/queries`, {
140
+ limit: String(limit),
141
+ offset: String(offset)
142
+ });
143
+ }
121
144
  /** Get crawler breakdown by type (default 30 days). */
122
145
  async getSiteCrawlers(days = 30) {
123
146
  return this.get(`/v1/sites/${this.siteId}/crawlers`, { days: String(days) });
@@ -134,6 +157,106 @@ var ApptvtyClient = class {
134
157
  async getSiteWallet() {
135
158
  return this.get(`/v1/sites/${this.siteId}/wallet`);
136
159
  }
160
+ // ─── Campaign management (for coding agents) ────────────────────────────────
161
+ //
162
+ // A coding agent that installed the SDK can create and manage ad campaigns
163
+ // programmatically — no human dashboard login required.
164
+ //
165
+ // Typical agentic advertiser flow:
166
+ // 1. Check wallet balance: const w = await client.getSiteWallet()
167
+ // 2. Fund wallet if needed: send USDC to w.wallet_address on Base chain
168
+ // 3. Create campaign: await client.createCampaign({ ... })
169
+ // 4. Monitor performance: await client.getCampaign(id)
170
+ // 5. Adjust or pause: await client.updateCampaign(id, { status: 'paused' })
171
+ /**
172
+ * Create an ad campaign. The campaign goes live immediately once the wallet
173
+ * has sufficient balance.
174
+ *
175
+ * If the wallet balance is too low, throws `ApptvtyInsufficientBalanceError`
176
+ * which includes deposit instructions so the agent can fund the wallet and retry.
177
+ *
178
+ * @example
179
+ * // Site-based: ad copy derived from your site's crawled content
180
+ * const campaign = await client.createCampaign({
181
+ * name: 'My Kubernetes Blog',
182
+ * advertiser_site_id: 'site_abc123',
183
+ * keywords: ['kubernetes', 'devops', 'containers'],
184
+ * categories: ['technology'],
185
+ * bid_per_view_usdc: 0.001,
186
+ * daily_budget_usdc: 1.0,
187
+ * total_budget_usdc: 20.0,
188
+ * });
189
+ *
190
+ * // Static: manually written ad copy
191
+ * const campaign = await client.createCampaign({
192
+ * name: 'My Blog — Static',
193
+ * ad_text: 'Deep dives on Kubernetes, written by practitioners.',
194
+ * landing_url: 'https://myblog.com',
195
+ * keywords: ['kubernetes', 'devops'],
196
+ * categories: ['technology'],
197
+ * bid_per_view_usdc: 0.001,
198
+ * daily_budget_usdc: 1.0,
199
+ * total_budget_usdc: 10.0,
200
+ * });
201
+ */
202
+ async createCampaign(params) {
203
+ try {
204
+ return await this.post("/v1/campaigns", params);
205
+ } catch (err) {
206
+ if (err instanceof ApptvtyApiError && err.statusCode === 402) {
207
+ let details;
208
+ try {
209
+ const body = JSON.parse(err.body);
210
+ details = body?.error;
211
+ } catch {
212
+ }
213
+ throw new ApptvtyInsufficientBalanceError(details);
214
+ }
215
+ throw err;
216
+ }
217
+ }
218
+ /**
219
+ * List all campaigns for this account.
220
+ * Also returns the schema (valid categories, minimum bid) so the agent
221
+ * knows valid field values without guessing.
222
+ */
223
+ async listCampaigns() {
224
+ return this.get("/v1/campaigns");
225
+ }
226
+ /**
227
+ * Get a single campaign by ID, including current spend and impression count.
228
+ * `budget_remaining_usdc` tells the agent how much budget is left before
229
+ * the campaign auto-pauses.
230
+ */
231
+ async getCampaign(campaignId) {
232
+ return this.get(`/v1/campaigns/${campaignId}`);
233
+ }
234
+ /**
235
+ * Partially update a campaign. Only fields present in `params` are changed.
236
+ *
237
+ * @example
238
+ * // Pause a campaign
239
+ * await client.updateCampaign(id, { status: 'paused' });
240
+ *
241
+ * // Increase bid and daily budget
242
+ * await client.updateCampaign(id, { bid_per_view_usdc: 0.002, daily_budget_usdc: 5.0 });
243
+ */
244
+ async updateCampaign(campaignId, params) {
245
+ return this.patch(`/v1/campaigns/${campaignId}`, params);
246
+ }
247
+ /**
248
+ * Pause a campaign immediately. Equivalent to updateCampaign(id, { status: 'paused' }).
249
+ * Campaigns are never deleted — billing history is retained.
250
+ */
251
+ async pauseCampaign(campaignId) {
252
+ await this.delete(`/v1/campaigns/${campaignId}`);
253
+ }
254
+ /**
255
+ * Resume a paused campaign.
256
+ */
257
+ async resumeCampaign(campaignId) {
258
+ await this.patch(`/v1/campaigns/${campaignId}`, { status: "active" });
259
+ }
137
260
  async get(path, params) {
138
261
  const url = new URL(`${this.baseUrl}${path}`);
139
262
  if (params) {
@@ -142,9 +265,27 @@ var ApptvtyClient = class {
142
265
  const response = await fetch(url.toString(), {
143
266
  method: "GET",
144
267
  headers: {
145
- Authorization: `Bearer ${this.apiKey}`,
268
+ Authorization: this.x402Token ?? `Bearer ${this.apiKey}`,
269
+ "User-Agent": "apptvty-sdk/0.1.0"
270
+ },
271
+ signal: AbortSignal.timeout(1e4)
272
+ });
273
+ if (!response.ok) {
274
+ const text = await response.text().catch(() => "");
275
+ throw new ApptvtyApiError(response.status, path, text);
276
+ }
277
+ return response.json();
278
+ }
279
+ async patch(path, body) {
280
+ const url = `${this.baseUrl}${path}`;
281
+ const response = await fetch(url, {
282
+ method: "PATCH",
283
+ headers: {
284
+ "Authorization": this.x402Token ?? `Bearer ${this.apiKey}`,
285
+ "Content-Type": "application/json",
146
286
  "User-Agent": "apptvty-sdk/0.1.0"
147
287
  },
288
+ body: JSON.stringify(body),
148
289
  signal: AbortSignal.timeout(1e4)
149
290
  });
150
291
  if (!response.ok) {
@@ -153,22 +294,73 @@ var ApptvtyClient = class {
153
294
  }
154
295
  return response.json();
155
296
  }
297
+ async delete(path) {
298
+ const url = `${this.baseUrl}${path}`;
299
+ const response = await fetch(url, {
300
+ method: "DELETE",
301
+ headers: {
302
+ "Authorization": this.x402Token ?? `Bearer ${this.apiKey}`,
303
+ "User-Agent": "apptvty-sdk/0.1.0"
304
+ },
305
+ signal: AbortSignal.timeout(1e4)
306
+ });
307
+ if (!response.ok) {
308
+ const text = await response.text().catch(() => "");
309
+ throw new ApptvtyApiError(response.status, path, text);
310
+ }
311
+ }
312
+ /**
313
+ * Settle an X402 challenge from the site's own USDC wallet balance.
314
+ * Called automatically when `post()` receives a 402 with an X402 header.
315
+ * Returns the preimage on success, or throws if the wallet balance is too low.
316
+ */
317
+ async payX402Challenge(macaroon) {
318
+ const url = `${this.baseUrl}/v1/x402/pay`;
319
+ const response = await fetch(url, {
320
+ method: "POST",
321
+ headers: {
322
+ Authorization: `Bearer ${this.apiKey}`,
323
+ "Content-Type": "application/json",
324
+ "User-Agent": "apptvty-sdk/0.1.0"
325
+ },
326
+ body: JSON.stringify({ macaroon }),
327
+ signal: AbortSignal.timeout(1e4)
328
+ });
329
+ const text = await response.text().catch(() => "");
330
+ if (!response.ok) {
331
+ let details;
332
+ try {
333
+ details = JSON.parse(text)?.error?.details;
334
+ } catch {
335
+ }
336
+ throw new ApptvtyInsufficientBalanceError(details);
337
+ }
338
+ const json = JSON.parse(text);
339
+ return json.preimage;
340
+ }
156
341
  async post(path, body) {
157
342
  const url = `${this.baseUrl}${path}`;
158
343
  const response = await fetch(url, {
159
344
  method: "POST",
160
345
  headers: {
161
- "Authorization": `Bearer ${this.apiKey}`,
346
+ "Authorization": this.x402Token ?? `Bearer ${this.apiKey}`,
162
347
  "Content-Type": "application/json",
163
348
  "User-Agent": "apptvty-sdk/0.1.0"
164
349
  },
165
350
  body: JSON.stringify(body),
166
- // Node 18+ fetch: set a reasonable timeout via AbortSignal
167
351
  signal: AbortSignal.timeout(1e4)
168
352
  });
169
353
  if (!response.ok) {
170
354
  const text = await response.text().catch(() => "");
171
355
  if (response.status === 402) {
356
+ const authHeader = response.headers.get("WWW-Authenticate");
357
+ if (authHeader?.includes("X402") || authHeader?.includes("LSAT")) {
358
+ const macaroon = authHeader.match(/macaroon="([^"]+)"/)?.[1] || "";
359
+ const preimage = await this.payX402Challenge(macaroon);
360
+ this.setX402Token(macaroon, preimage);
361
+ this.log("X402 challenge auto-paid from wallet, retrying request");
362
+ return this.post(path, body);
363
+ }
172
364
  let dashboardUrl = "https://dashboard.apptvty.com/login";
173
365
  try {
174
366
  const json = JSON.parse(text);
@@ -204,6 +396,16 @@ var ApptvtyTrialExpiredError = class extends Error {
204
396
  this.name = "ApptvtyTrialExpiredError";
205
397
  }
206
398
  };
399
+ var ApptvtyInsufficientBalanceError = class extends Error {
400
+ constructor(details) {
401
+ const shortfall = details?.shortfall_usdc ?? "?";
402
+ super(
403
+ `Wallet balance too low to create campaign. Send ${shortfall} USDC to ${details?.deposit?.address ?? "your wallet"} on Base and retry.`
404
+ );
405
+ this.details = details;
406
+ this.name = "ApptvtyInsufficientBalanceError";
407
+ }
408
+ };
207
409
 
208
410
  // src/logger.ts
209
411
  var RequestLogger = class {
@@ -218,13 +420,20 @@ var RequestLogger = class {
218
420
  this.timer = setInterval(() => {
219
421
  void this.flush();
220
422
  }, interval);
221
- if (this.timer.unref) this.timer.unref();
222
- const handleExit = () => {
223
- void this.flushSync();
224
- };
225
- process.once("SIGTERM", handleExit);
226
- process.once("SIGINT", handleExit);
227
- process.once("beforeExit", handleExit);
423
+ if (this.timer && typeof this.timer.unref === "function") {
424
+ this.timer.unref();
425
+ }
426
+ if (typeof process !== "undefined" && typeof process.once === "function") {
427
+ const handleExit = () => {
428
+ void this.flushSync();
429
+ };
430
+ try {
431
+ process.once("SIGTERM", handleExit);
432
+ process.once("SIGINT", handleExit);
433
+ process.once("beforeExit", handleExit);
434
+ } catch {
435
+ }
436
+ }
228
437
  }
229
438
  /** Enqueue a single log entry. Non-blocking. */
230
439
  enqueue(entry) {
@@ -386,6 +595,27 @@ function extractBotName(userAgent) {
386
595
  function getKnownCrawlerNames() {
387
596
  return KNOWN_CRAWLERS.map((c) => c.name);
388
597
  }
598
+ var SCRAPER_SERVICES = [
599
+ // Jina AI Reader — r.jina.ai/URL — converts any page to clean Markdown
600
+ { name: "JinaReader", patterns: [/JinaReader/i] },
601
+ // Cloudflare Browser Rendering /crawl endpoint (open beta, announced March 2026)
602
+ { name: "Cloudflare-BrowserRendering", patterns: [/CloudflareBrowserRenderingCrawler/i] },
603
+ // FireCrawl — LLM-ready content extraction service
604
+ { name: "FireCrawl", patterns: [/FireCrawlAgent/i, /firecrawl/i] },
605
+ // Apify web scraping platform
606
+ { name: "Apify", patterns: [/ApifyBot/i] }
607
+ ];
608
+ function detectScraperService(userAgent) {
609
+ if (!userAgent) return { isScraperService: false, name: null };
610
+ for (const service of SCRAPER_SERVICES) {
611
+ for (const pattern of service.patterns) {
612
+ if (pattern.test(userAgent)) {
613
+ return { isScraperService: true, name: service.name };
614
+ }
615
+ }
616
+ }
617
+ return { isScraperService: false, name: null };
618
+ }
389
619
 
390
620
  // src/query-handler.ts
391
621
  var RESPONSE_HEADERS = {
@@ -527,6 +757,65 @@ function getOrigin(url) {
527
757
 
528
758
  // src/middleware/nextjs.ts
529
759
  var import_server = require("next/server");
760
+
761
+ // src/ad-injection.ts
762
+ var AD_INJECTION_MARKER = "<!-- apptvty-sponsored -->";
763
+ function injectIntoHtml(html, ads, isScraperService) {
764
+ if (!html || ads.length === 0) return html;
765
+ if (html.includes(AD_INJECTION_MARKER)) return html;
766
+ let modified = html;
767
+ const contentBlock = buildContentStreamBlock(ads);
768
+ if (modified.includes("</article>")) {
769
+ modified = modified.replace("</article>", `${contentBlock}
770
+ </article>`);
771
+ } else if (modified.includes("</main>")) {
772
+ modified = modified.replace("</main>", `${contentBlock}
773
+ </main>`);
774
+ } else if (!isScraperService && modified.includes("</body>")) {
775
+ modified = modified.replace("</body>", `${contentBlock}
776
+ </body>`);
777
+ }
778
+ if (!isScraperService && modified.includes("</head>")) {
779
+ const jsonLdBlock = buildJsonLdBlock(ads);
780
+ modified = modified.replace("</head>", `${jsonLdBlock}
781
+ </head>`);
782
+ }
783
+ return modified;
784
+ }
785
+ function buildSponsoredHeader(ads) {
786
+ return JSON.stringify(
787
+ ads.map((ad) => ({ text: ad.text, url: ad.url, advertiser: ad.advertiser }))
788
+ );
789
+ }
790
+ function buildContentStreamBlock(ads) {
791
+ const paragraphs = ads.map(
792
+ (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>`
793
+ ).join("\n");
794
+ return `${AD_INJECTION_MARKER}
795
+ ${paragraphs}`;
796
+ }
797
+ function buildJsonLdBlock(ads) {
798
+ const entries = ads.map((ad) => ({
799
+ "@context": "https://schema.org",
800
+ "@type": "WPAdBlock",
801
+ sponsor: {
802
+ "@type": "Organization",
803
+ name: ad.advertiser,
804
+ url: ad.url
805
+ },
806
+ description: ad.text
807
+ }));
808
+ const ld = entries.length === 1 ? entries[0] : entries;
809
+ return `<script type="application/ld+json">${JSON.stringify(ld)}</script>`;
810
+ }
811
+ function escapeHtml(s) {
812
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
813
+ }
814
+ function escapeAttr(s) {
815
+ return s.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
816
+ }
817
+
818
+ // src/middleware/nextjs.ts
530
819
  function headersToRecord(h) {
531
820
  const entries = h.entries();
532
821
  return Object.fromEntries(Array.from(entries));
@@ -548,8 +837,9 @@ function withApptvty(config, next) {
548
837
  const startMs = Date.now();
549
838
  const userAgent = request.headers.get("user-agent") ?? "";
550
839
  const crawlerInfo = detectCrawler(userAgent);
840
+ const scraperService = detectScraperService(userAgent);
551
841
  const aiCrawlerParam = parseBoolParam(request.nextUrl.searchParams.get("ai_crawler"), false);
552
- const isCrawler = crawlerInfo.isAi || aiCrawlerParam;
842
+ const isCrawler = crawlerInfo.isAi || aiCrawlerParam || scraperService.isScraperService;
553
843
  let response;
554
844
  try {
555
845
  response = next ? await next(request) : import_server.NextResponse.next();
@@ -575,14 +865,23 @@ function withApptvty(config, next) {
575
865
  is_ai_crawler: crawlerInfo.isAi,
576
866
  crawler_type: crawlerInfo.name,
577
867
  crawler_organization: crawlerInfo.organization,
578
- confidence_score: crawlerInfo.confidence
868
+ confidence_score: crawlerInfo.confidence,
869
+ scraper_service: scraperService.name
579
870
  };
580
871
  logger.enqueue(entry);
581
872
  if (isCrawler && response.ok && !pathname.startsWith(queryPath)) {
582
873
  const contentType = response.headers.get("content-type") ?? "";
583
874
  if (contentType.includes("text/html")) {
584
875
  try {
585
- const modified = await injectAdsIntoHtml(response, client, config.siteId, pathname);
876
+ const modified = await injectAdsIntoResponse(
877
+ response,
878
+ client,
879
+ config,
880
+ pathname,
881
+ userAgent,
882
+ getClientIp(headers),
883
+ scraperService.isScraperService
884
+ );
586
885
  if (modified) return modified;
587
886
  } catch (err) {
588
887
  if (config.debug) console.warn("[apptvty] Ad injection failed:", err);
@@ -618,40 +917,31 @@ function createNextjsQueryHandler(config) {
618
917
  });
619
918
  };
620
919
  }
621
- var AD_INJECTION_MARKER = "<!-- apptvty-sponsored -->";
622
- function buildAdBlock(ads) {
623
- const items = ads.map(
624
- (ad) => `<li><a href="${escapeHtml(ad.url)}" rel="nofollow">${escapeHtml(ad.text)}</a> <small>\u2014 ${escapeHtml(ad.advertiser)}</small></li>`
625
- ).join("\n");
626
- return `
627
- <section aria-label="Sponsored" data-sponsored ${AD_INJECTION_MARKER}>
628
- <h3>Sponsored</h3>
629
- <ul>${items}</ul>
630
- </section>`;
631
- }
632
- function escapeHtml(s) {
633
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
634
- }
635
- async function injectAdsIntoHtml(response, client, siteId, pathname) {
920
+ async function injectAdsIntoResponse(response, client, config, pathname, userAgent, ipAddress, isScraperService) {
636
921
  const html = await response.text();
637
- if (!html || html.includes(AD_INJECTION_MARKER)) return null;
638
- const pageAds = await client.getAdsForPage({ site_id: siteId, page_path: pathname });
922
+ if (!html) return null;
923
+ const pageAds = await client.getAdsForPage({ site_id: config.siteId, page_path: pathname });
639
924
  if (!pageAds.ads || pageAds.ads.length === 0) return null;
640
- const adBlock = buildAdBlock(pageAds.ads);
641
- let modified;
642
- if (html.includes("</body>")) {
643
- modified = html.replace("</body>", `${adBlock}
644
- </body>`);
645
- } else if (html.includes("</html>")) {
646
- modified = html.replace("</html>", `${adBlock}
647
- </html>`);
648
- } else {
649
- modified = html + adBlock;
925
+ const modified = injectIntoHtml(html, pageAds.ads, isScraperService);
926
+ if (modified === html) return null;
927
+ const newHeaders = new Headers(response.headers);
928
+ newHeaders.set("X-Sponsored-Content", buildSponsoredHeader(pageAds.ads));
929
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
930
+ for (const ad of pageAds.ads) {
931
+ client.logImpression({
932
+ impression_id: ad.impression_id,
933
+ site_id: config.siteId,
934
+ page_path: pathname,
935
+ agent_ua: userAgent,
936
+ agent_ip: ipAddress,
937
+ timestamp
938
+ }).catch(() => {
939
+ });
650
940
  }
651
941
  return new import_server.NextResponse(modified, {
652
942
  status: response.status,
653
943
  statusText: response.statusText,
654
- headers: new Headers(response.headers)
944
+ headers: newHeaders
655
945
  });
656
946
  }
657
947
  function parseBoolParam(value, defaultValue) {
@@ -674,12 +964,77 @@ function getInstance2(config) {
674
964
  return instances2.get(key);
675
965
  }
676
966
  function createExpressMiddleware(config) {
677
- const { logger } = getInstance2(config);
678
- return function apptvtyLogger(req, res, next) {
967
+ const { client, logger } = getInstance2(config);
968
+ return function apptvtyMiddleware(req, res, next) {
679
969
  const startMs = Date.now();
680
970
  const userAgent = req.headers["user-agent"] ?? "";
681
971
  const crawlerInfo = detectCrawler(userAgent);
972
+ const scraperService = detectScraperService(userAgent);
682
973
  const path = req.url ?? "/";
974
+ const isCrawler = crawlerInfo.isAi || scraperService.isScraperService;
975
+ const ipAddress = getClientIp(req.headers);
976
+ const adsPromise = isCrawler && !shouldSkip2(path) ? client.getAdsForPage({ site_id: config.siteId, page_path: path }).catch(() => ({ ads: [] })) : Promise.resolve({ ads: [] });
977
+ if (isCrawler && !shouldSkip2(path)) {
978
+ const chunks = [];
979
+ const originalWrite = res.write.bind(res);
980
+ const originalEnd = res.end.bind(res);
981
+ res.write = function(chunk, encodingOrCallback, callback) {
982
+ if (chunk != null) {
983
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
984
+ }
985
+ if (typeof encodingOrCallback === "function") encodingOrCallback();
986
+ else if (typeof callback === "function") callback();
987
+ return true;
988
+ };
989
+ res.end = function(chunk, encodingOrCallback, callback) {
990
+ if (chunk != null) {
991
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
992
+ }
993
+ const contentType = res.getHeader("content-type") ?? "";
994
+ const isHtml = contentType.includes("text/html");
995
+ if (!isHtml || chunks.length === 0) {
996
+ res.write = originalWrite;
997
+ res.end = originalEnd;
998
+ return originalEnd(Buffer.concat(chunks), encodingOrCallback, callback);
999
+ }
1000
+ const html = Buffer.concat(chunks).toString("utf-8");
1001
+ if (html.includes(AD_INJECTION_MARKER)) {
1002
+ res.write = originalWrite;
1003
+ res.end = originalEnd;
1004
+ return originalEnd(html, encodingOrCallback, callback);
1005
+ }
1006
+ adsPromise.then((pageAds) => {
1007
+ res.write = originalWrite;
1008
+ res.end = originalEnd;
1009
+ if (!pageAds.ads || pageAds.ads.length === 0) {
1010
+ originalEnd(html, encodingOrCallback, callback);
1011
+ return;
1012
+ }
1013
+ const modified = injectIntoHtml(html, pageAds.ads, scraperService.isScraperService);
1014
+ res.setHeader("X-Sponsored-Content", buildSponsoredHeader(pageAds.ads));
1015
+ const buf = Buffer.from(modified, "utf-8");
1016
+ res.setHeader("Content-Length", buf.length);
1017
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1018
+ for (const ad of pageAds.ads) {
1019
+ client.logImpression({
1020
+ impression_id: ad.impression_id,
1021
+ site_id: config.siteId,
1022
+ page_path: path,
1023
+ agent_ua: userAgent,
1024
+ agent_ip: ipAddress,
1025
+ timestamp
1026
+ }).catch(() => {
1027
+ });
1028
+ }
1029
+ originalEnd(buf, encodingOrCallback, callback);
1030
+ }).catch(() => {
1031
+ res.write = originalWrite;
1032
+ res.end = originalEnd;
1033
+ originalEnd(html, encodingOrCallback, callback);
1034
+ });
1035
+ return res;
1036
+ };
1037
+ }
683
1038
  res.on("finish", () => {
684
1039
  if (shouldSkip2(path)) return;
685
1040
  const entry = {
@@ -689,13 +1044,14 @@ function createExpressMiddleware(config) {
689
1044
  path,
690
1045
  status_code: res.statusCode,
691
1046
  response_time_ms: Date.now() - startMs,
692
- ip_address: getClientIp(req.headers),
1047
+ ip_address: ipAddress,
693
1048
  user_agent: userAgent,
694
1049
  referer: req.headers["referer"] ?? null,
695
1050
  is_ai_crawler: crawlerInfo.isAi,
696
1051
  crawler_type: crawlerInfo.name,
697
1052
  crawler_organization: crawlerInfo.organization,
698
- confidence_score: crawlerInfo.confidence
1053
+ confidence_score: crawlerInfo.confidence,
1054
+ scraper_service: scraperService.name
699
1055
  };
700
1056
  logger.enqueue(entry);
701
1057
  });
@@ -739,12 +1095,14 @@ function shouldSkip2(path) {
739
1095
  0 && (module.exports = {
740
1096
  ApptvtyApiError,
741
1097
  ApptvtyClient,
1098
+ ApptvtyInsufficientBalanceError,
742
1099
  RequestLogger,
743
1100
  createExpressMiddleware,
744
1101
  createExpressQueryHandler,
745
1102
  createNextjsQueryHandler,
746
1103
  createQueryHandler,
747
1104
  detectCrawler,
1105
+ detectScraperService,
748
1106
  getKnownCrawlerNames,
749
1107
  withApptvty
750
1108
  });