apptvty 0.2.5 → 0.3.1

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/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -361,7 +371,7 @@ var ApptvtyClient = class {
361
371
  this.log("X402 challenge auto-paid from wallet, retrying request");
362
372
  return this.post(path, body);
363
373
  }
364
- let dashboardUrl = "https://dashboard.apptvty.com/login";
374
+ let dashboardUrl = (typeof process !== "undefined" ? process.env?.DASHBOARD_URL : void 0) ?? "https://dashboard.apptvty.com/login";
365
375
  try {
366
376
  const json = JSON.parse(text);
367
377
  dashboardUrl = json?.error?.details?.dashboard_url ?? dashboardUrl;
@@ -413,7 +423,7 @@ var RequestLogger = class {
413
423
  this.client = client;
414
424
  this.queue = [];
415
425
  this.timer = null;
416
- this.flushing = false;
426
+ this.activeFlushPromise = null;
417
427
  this.batchSize = config.batchSize ?? 50;
418
428
  this.debug = config.debug ?? false;
419
429
  const interval = config.flushInterval ?? 5e3;
@@ -438,21 +448,31 @@ var RequestLogger = class {
438
448
  /** Enqueue a single log entry. Non-blocking. */
439
449
  enqueue(entry) {
440
450
  this.queue.push(entry);
441
- if (this.queue.length >= this.batchSize) {
442
- void this.flush();
443
- }
451
+ void this.flush();
444
452
  }
445
- /** Flush the current queue to the API. */
453
+ /** Flush the current queue to the API. Returns the active Promise so Edge runtimes can wait. */
446
454
  async flush() {
447
- if (this.flushing || this.queue.length === 0) return;
448
- this.flushing = true;
449
- const batch = this.queue.splice(0, this.batchSize);
450
- try {
451
- await this.client.sendLogs(batch);
452
- } catch {
453
- } finally {
454
- this.flushing = false;
455
+ if (this.queue.length === 0) {
456
+ return this.activeFlushPromise || Promise.resolve();
457
+ }
458
+ if (this.activeFlushPromise) {
459
+ await this.activeFlushPromise;
460
+ if (this.queue.length > 0) return this.flush();
461
+ return Promise.resolve();
455
462
  }
463
+ const batch = this.queue.splice(0, this.batchSize);
464
+ this.activeFlushPromise = (async () => {
465
+ try {
466
+ await this.client.sendLogs(batch);
467
+ } catch {
468
+ } finally {
469
+ this.activeFlushPromise = null;
470
+ if (this.queue.length > 0) {
471
+ void this.flush();
472
+ }
473
+ }
474
+ })();
475
+ return this.activeFlushPromise;
456
476
  }
457
477
  /**
458
478
  * Synchronous-ish flush for process shutdown.
@@ -676,7 +696,7 @@ function createQueryHandler(client, config) {
676
696
  if (trimmedQuery.length > 500) {
677
697
  return errorResponse(400, "QUERY_TOO_LONG", "Query must be 500 characters or fewer");
678
698
  }
679
- const requestId = crypto.randomUUID();
699
+ const requestId = globalThis.crypto.randomUUID();
680
700
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
681
701
  const startMs = Date.now();
682
702
  const surfaceAds = req.surface_ads !== false;
@@ -704,16 +724,21 @@ function createQueryHandler(client, config) {
704
724
  return errorResponse(502, "UPSTREAM_ERROR", "Could not retrieve an answer at this time");
705
725
  }
706
726
  const responseTimeMs = Date.now() - startMs;
707
- const ads = backendResponse.sponsored ? Array.isArray(backendResponse.sponsored) ? backendResponse.sponsored : [backendResponse.sponsored] : [];
708
- for (const ad of ads) {
709
- void client.logImpression({
710
- impression_id: ad.impression_id,
711
- site_id: config.siteId,
712
- query: trimmedQuery,
713
- agent_ua: req.userAgent,
714
- agent_ip: req.ipAddress,
715
- timestamp
716
- });
727
+ const ads = backendResponse.related_resources ? Array.isArray(backendResponse.related_resources) ? backendResponse.related_resources : [backendResponse.related_resources] : [];
728
+ if (ads.length > 0) {
729
+ await Promise.all(
730
+ ads.map(
731
+ (ad) => client.logImpression({
732
+ impression_id: ad.impression_id,
733
+ site_id: config.siteId,
734
+ query: trimmedQuery,
735
+ agent_ua: req.userAgent,
736
+ agent_ip: req.ipAddress,
737
+ timestamp
738
+ }).catch(() => {
739
+ })
740
+ )
741
+ );
717
742
  }
718
743
  const agentResponse = {
719
744
  success: true,
@@ -722,7 +747,7 @@ function createQueryHandler(client, config) {
722
747
  answer: backendResponse.answer,
723
748
  sources: backendResponse.sources,
724
749
  confidence: backendResponse.confidence,
725
- ...backendResponse.sponsored && { sponsored: backendResponse.sponsored },
750
+ ...backendResponse.related_resources && { related_resources: backendResponse.related_resources },
726
751
  metadata: {
727
752
  request_id: requestId,
728
753
  response_time_ms: responseTimeMs,
@@ -740,7 +765,7 @@ function errorResponse(status, code, message) {
740
765
  error: {
741
766
  code,
742
767
  message,
743
- request_id: crypto.randomUUID(),
768
+ request_id: globalThis.crypto.randomUUID(),
744
769
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
745
770
  }
746
771
  };
@@ -758,61 +783,58 @@ function getOrigin(url) {
758
783
  // src/middleware/nextjs.ts
759
784
  var import_server = require("next/server");
760
785
 
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>`);
786
+ // src/markdown.ts
787
+ var cheerio = __toESM(require("cheerio"));
788
+ function convertHtmlToMarkdown(html) {
789
+ if (!html) return "";
790
+ const $ = cheerio.load(html);
791
+ $("script, style, nav, footer, header, aside, svg, .ad, .sponsor, noscript").remove();
792
+ const main = $('main, article, [role="main"], #content, .content').first();
793
+ const root = main.length ? main : $("body");
794
+ let markdown = "";
795
+ root.find("h1, h2, h3, h4, h5, h6, p, ul, ol").each((_, el) => {
796
+ const $el = $(el);
797
+ const tagName = el.tagName.toLowerCase();
798
+ if (tagName === "ul" || tagName === "ol") {
799
+ $el.find("li").each((_2, li) => {
800
+ const text2 = cleanText($(li).text());
801
+ if (text2) markdown += `- ${text2}
802
+ `;
803
+ });
804
+ markdown += "\n";
805
+ return;
806
+ }
807
+ const text = cleanText($el.text());
808
+ if (!text) return;
809
+ if (tagName === "h1") markdown += `# ${text}
810
+
811
+ `;
812
+ else if (tagName === "h2") markdown += `## ${text}
813
+
814
+ `;
815
+ else if (tagName === "h3") markdown += `### ${text}
816
+
817
+ `;
818
+ else if (tagName === "h4") markdown += `#### ${text}
819
+
820
+ `;
821
+ else if (tagName === "h5") markdown += `##### ${text}
822
+
823
+ `;
824
+ else if (tagName === "h6") markdown += `###### ${text}
825
+
826
+ `;
827
+ else if (tagName === "p") markdown += `${text}
828
+
829
+ `;
830
+ });
831
+ if (markdown.trim().length < 50) {
832
+ markdown = cleanText(root.text()) + "\n\n";
782
833
  }
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}`;
834
+ return markdown.trim();
796
835
  }
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;");
836
+ function cleanText(text) {
837
+ return text.trim().replace(/\s+/g, " ");
816
838
  }
817
839
 
818
840
  // src/middleware/nextjs.ts
@@ -833,16 +855,38 @@ function getInstance(config) {
833
855
  function withApptvty(config, next) {
834
856
  const { client, logger } = getInstance(config);
835
857
  const queryPath = config.queryPath ?? "/query";
836
- return async function apptvtyMiddleware(request) {
858
+ return async function apptvtyMiddleware(request, event) {
837
859
  const startMs = Date.now();
838
860
  const userAgent = request.headers.get("user-agent") ?? "";
839
861
  const crawlerInfo = detectCrawler(userAgent);
840
862
  const scraperService = detectScraperService(userAgent);
841
863
  const aiCrawlerParam = parseBoolParam(request.nextUrl.searchParams.get("ai_crawler"), false);
842
864
  const isCrawler = crawlerInfo.isAi || aiCrawlerParam || scraperService.isScraperService;
865
+ if (request.nextUrl.pathname === "/api/apptvty/verify") {
866
+ const challenge = request.nextUrl.searchParams.get("challenge");
867
+ if (challenge) {
868
+ const encoder = new TextEncoder();
869
+ const keyData = encoder.encode(config.apiKey);
870
+ const cryptoKey = await globalThis.crypto.subtle.importKey(
871
+ "raw",
872
+ keyData,
873
+ { name: "HMAC", hash: "SHA-256" },
874
+ false,
875
+ ["sign"]
876
+ );
877
+ const prefixedChallenge = `apptvty_verify_challenge:${challenge}`;
878
+ const signatureBuffer = await globalThis.crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(prefixedChallenge));
879
+ const signature = Array.from(new Uint8Array(signatureBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
880
+ return import_server.NextResponse.json({
881
+ site_id: config.siteId,
882
+ verified: true,
883
+ signature
884
+ });
885
+ }
886
+ }
843
887
  let response;
844
888
  try {
845
- response = next ? await next(request) : import_server.NextResponse.next();
889
+ response = next ? await next(request, event) : import_server.NextResponse.next();
846
890
  } catch (err) {
847
891
  throw err;
848
892
  }
@@ -868,24 +912,53 @@ function withApptvty(config, next) {
868
912
  confidence_score: crawlerInfo.confidence,
869
913
  scraper_service: scraperService.name
870
914
  };
871
- logger.enqueue(entry);
872
- if (isCrawler && response.ok && !pathname.startsWith(queryPath)) {
873
- const contentType = response.headers.get("content-type") ?? "";
874
- if (contentType.includes("text/html")) {
875
- try {
876
- const modified = await injectAdsIntoResponse(
877
- response,
878
- client,
879
- config,
880
- pathname,
881
- userAgent,
882
- getClientIp(headers),
883
- scraperService.isScraperService
884
- );
885
- if (modified) return modified;
886
- } catch (err) {
887
- if (config.debug) console.warn("[apptvty] Ad injection failed:", err);
915
+ const isInternalRequest = request.headers.get("x-apptvty-internal") === "true";
916
+ if (!isInternalRequest && !pathname.startsWith(queryPath)) {
917
+ logger.enqueue(entry);
918
+ if (event && typeof event.waitUntil === "function") {
919
+ event.waitUntil(logger.flush());
920
+ }
921
+ }
922
+ if (isCrawler && !isInternalRequest && !pathname.startsWith(queryPath)) {
923
+ try {
924
+ const proxyReq = new Request(request.url, {
925
+ headers: new Headers(request.headers)
926
+ });
927
+ proxyReq.headers.set("x-apptvty-internal", "true");
928
+ const res = await fetch(proxyReq);
929
+ const contentType = res.headers.get("content-type") ?? "";
930
+ if (contentType.includes("text/html")) {
931
+ const html = await res.text();
932
+ let markdown = convertHtmlToMarkdown(html);
933
+ const pageAds = await client.getAdsForPage({ site_id: config.siteId, page_path: pathname });
934
+ if (pageAds.ads && pageAds.ads.length > 0) {
935
+ const ad = pageAds.ads[0];
936
+ markdown += `
937
+
938
+ ---
939
+ > **Sponsored:** [${ad.text}](${ad.url}) - ${ad.advertiser}
940
+ `;
941
+ client.logImpression({
942
+ impression_id: ad.impression_id,
943
+ site_id: config.siteId,
944
+ page_path: pathname,
945
+ agent_ua: userAgent,
946
+ agent_ip: getClientIp(headers),
947
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
948
+ }).catch(() => {
949
+ });
950
+ }
951
+ return new import_server.NextResponse(markdown, {
952
+ status: res.status,
953
+ headers: {
954
+ "Content-Type": "text/markdown",
955
+ "X-Apptvty-AEO": "true"
956
+ }
957
+ });
888
958
  }
959
+ return res;
960
+ } catch (err) {
961
+ if (config.debug) console.warn("[apptvty] Markdown proxy failed:", err);
889
962
  }
890
963
  }
891
964
  return response;
@@ -917,33 +990,6 @@ function createNextjsQueryHandler(config) {
917
990
  });
918
991
  };
919
992
  }
920
- async function injectAdsIntoResponse(response, client, config, pathname, userAgent, ipAddress, isScraperService) {
921
- const html = await response.text();
922
- if (!html) return null;
923
- const pageAds = await client.getAdsForPage({ site_id: config.siteId, page_path: pathname });
924
- if (!pageAds.ads || pageAds.ads.length === 0) return null;
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
- });
940
- }
941
- return new import_server.NextResponse(modified, {
942
- status: response.status,
943
- statusText: response.statusText,
944
- headers: newHeaders
945
- });
946
- }
947
993
  function parseBoolParam(value, defaultValue) {
948
994
  if (value === null) return defaultValue;
949
995
  return value === "1" || value === "true" || value === "yes";
@@ -952,6 +998,63 @@ function shouldSkip(pathname) {
952
998
  return pathname.startsWith("/_next/") || pathname.startsWith("/api/_") || pathname === "/favicon.ico" || /\.(svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|css|js\.map)$/.test(pathname);
953
999
  }
954
1000
 
1001
+ // src/ad-injection.ts
1002
+ var AD_INJECTION_MARKER = "<!-- apptvty-sponsored -->";
1003
+ function injectIntoHtml(html, ads, isScraperService) {
1004
+ if (!html || ads.length === 0) return html;
1005
+ if (html.includes(AD_INJECTION_MARKER)) return html;
1006
+ let modified = html;
1007
+ const contentBlock = buildContentStreamBlock(ads);
1008
+ if (modified.includes("</article>")) {
1009
+ modified = modified.replace("</article>", `${contentBlock}
1010
+ </article>`);
1011
+ } else if (modified.includes("</main>")) {
1012
+ modified = modified.replace("</main>", `${contentBlock}
1013
+ </main>`);
1014
+ } else if (!isScraperService && modified.includes("</body>")) {
1015
+ modified = modified.replace("</body>", `${contentBlock}
1016
+ </body>`);
1017
+ }
1018
+ if (!isScraperService && modified.includes("</head>")) {
1019
+ const jsonLdBlock = buildJsonLdBlock(ads);
1020
+ modified = modified.replace("</head>", `${jsonLdBlock}
1021
+ </head>`);
1022
+ }
1023
+ return modified;
1024
+ }
1025
+ function buildSponsoredHeader(ads) {
1026
+ return JSON.stringify(
1027
+ ads.map((ad) => ({ text: ad.text, url: ad.url, advertiser: ad.advertiser }))
1028
+ );
1029
+ }
1030
+ function buildContentStreamBlock(ads) {
1031
+ const paragraphs = ads.map(
1032
+ (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>`
1033
+ ).join("\n");
1034
+ return `${AD_INJECTION_MARKER}
1035
+ ${paragraphs}`;
1036
+ }
1037
+ function buildJsonLdBlock(ads) {
1038
+ const entries = ads.map((ad) => ({
1039
+ "@context": "https://schema.org",
1040
+ "@type": "WPAdBlock",
1041
+ sponsor: {
1042
+ "@type": "Organization",
1043
+ name: ad.advertiser,
1044
+ url: ad.url
1045
+ },
1046
+ description: ad.text
1047
+ }));
1048
+ const ld = entries.length === 1 ? entries[0] : entries;
1049
+ return `<script type="application/ld+json">${JSON.stringify(ld)}</script>`;
1050
+ }
1051
+ function escapeHtml(s) {
1052
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1053
+ }
1054
+ function escapeAttr(s) {
1055
+ return s.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1056
+ }
1057
+
955
1058
  // src/middleware/express.ts
956
1059
  var instances2 = /* @__PURE__ */ new Map();
957
1060
  function getInstance2(config) {