apptvty 0.3.1 → 0.4.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/dist/index.js CHANGED
@@ -127,6 +127,28 @@ var ApptvtyClient = class {
127
127
  this.warn("Failed to log impression (billing may be delayed):", err);
128
128
  }
129
129
  }
130
+ // ─── Programmatic Indexing ───────────────────────────────────────────────────
131
+ /**
132
+ * Manually push text or markdown directly into your site's RAG vector database.
133
+ * This instantly embeds the content for AI agents to query, bypassing the
134
+ * need for a scheduled HTML crawl. This is ideal for Client-Side Rendered (CSR)
135
+ * Apps or for injecting private institutional facts/databases.
136
+ *
137
+ * @example
138
+ * const result = await client.pushKnowledge({
139
+ * title: "Capitol Technology University Profile",
140
+ * content: "Here are all the detailed specs, majors, and acceptances...",
141
+ * url: "https://thecollegeradar.com/institution/499" // Optional reference
142
+ * });
143
+ */
144
+ async pushKnowledge(params) {
145
+ const payload = {
146
+ title: params.title,
147
+ content: params.content,
148
+ url: params.url
149
+ };
150
+ return this.post(`/v1/sites/${this.siteId}/knowledge`, payload);
151
+ }
130
152
  // ─── Analytics (for coding agents) ───────────────────────────────────────────
131
153
  // These allow agents to check activity, logs, and errors without a human.
132
154
  /** Get 30-day traffic overview (requests, AI %, crawlers, queries). */
@@ -374,8 +396,12 @@ var ApptvtyClient = class {
374
396
  let dashboardUrl = (typeof process !== "undefined" ? process.env?.DASHBOARD_URL : void 0) ?? "https://dashboard.apptvty.com/login";
375
397
  try {
376
398
  const json = JSON.parse(text);
399
+ if (json?.error?.code === "INSUFFICIENT_BALANCE") {
400
+ throw new ApptvtyApiError(402, path, text);
401
+ }
377
402
  dashboardUrl = json?.error?.details?.dashboard_url ?? dashboardUrl;
378
- } catch {
403
+ } catch (e) {
404
+ if (e instanceof ApptvtyApiError) throw e;
379
405
  }
380
406
  throw new ApptvtyTrialExpiredError(dashboardUrl);
381
407
  }
@@ -861,7 +887,8 @@ function withApptvty(config, next) {
861
887
  const crawlerInfo = detectCrawler(userAgent);
862
888
  const scraperService = detectScraperService(userAgent);
863
889
  const aiCrawlerParam = parseBoolParam(request.nextUrl.searchParams.get("ai_crawler"), false);
864
- const isCrawler = crawlerInfo.isAi || aiCrawlerParam || scraperService.isScraperService;
890
+ const isAi = crawlerInfo.isAi || aiCrawlerParam;
891
+ const isScraper = isAi || scraperService.isScraperService || crawlerInfo.name === "unknown_bot";
865
892
  if (request.nextUrl.pathname === "/api/apptvty/verify") {
866
893
  const challenge = request.nextUrl.searchParams.get("challenge");
867
894
  if (challenge) {
@@ -906,59 +933,87 @@ function withApptvty(config, next) {
906
933
  ip_address: getClientIp(headers),
907
934
  user_agent: userAgent,
908
935
  referrer: request.headers.get("referer"),
909
- is_ai_crawler: crawlerInfo.isAi,
936
+ is_ai_crawler: isAi,
910
937
  crawler_type: crawlerInfo.name,
911
938
  crawler_organization: crawlerInfo.organization,
912
939
  confidence_score: crawlerInfo.confidence,
913
- scraper_service: scraperService.name
940
+ scraper_service: scraperService.name,
941
+ attribution_id: request.nextUrl.searchParams.get("atid")
914
942
  };
915
- const isInternalRequest = request.headers.get("x-apptvty-internal") === "true";
916
- if (!isInternalRequest && !pathname.startsWith(queryPath)) {
943
+ const isInternalRequest2 = request.headers.get("x-apptvty-internal") === "true";
944
+ if (!isInternalRequest2 && !pathname.startsWith(queryPath)) {
917
945
  logger.enqueue(entry);
918
946
  if (event && typeof event.waitUntil === "function") {
919
947
  event.waitUntil(logger.flush());
920
948
  }
921
949
  }
922
- if (isCrawler && !isInternalRequest && !pathname.startsWith(queryPath)) {
950
+ if (!isInternalRequest2 && !pathname.startsWith(queryPath) && response.status === 200) {
923
951
  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 += `
952
+ const pageAds = await client.getAdsForPage({ site_id: config.siteId, page_path: pathname });
953
+ if (pageAds.ads && pageAds.ads.length > 0) {
954
+ const ad = pageAds.ads[0];
955
+ client.logImpression({
956
+ impression_id: ad.impression_id,
957
+ site_id: config.siteId,
958
+ page_path: pathname,
959
+ agent_ua: userAgent,
960
+ agent_ip: getClientIp(headers),
961
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
962
+ }).catch(() => {
963
+ });
964
+ if (isAi || scraperService.isScraperService) {
965
+ const proxyReq = new Request(request.url, { headers: new Headers(request.headers) });
966
+ proxyReq.headers.set("x-apptvty-internal", "true");
967
+ const res = await fetch(proxyReq);
968
+ const contentType = res.headers.get("content-type") ?? "";
969
+ if (contentType.includes("text/html")) {
970
+ const html = await res.text();
971
+ let markdown = convertHtmlToMarkdown(html);
972
+ markdown += `
937
973
 
938
974
  ---
939
975
  > **Sponsored:** [${ad.text}](${ad.url}) - ${ad.advertiser}
940
976
  `;
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
- });
977
+ return new import_server.NextResponse(markdown, {
978
+ status: res.status,
979
+ headers: {
980
+ "Content-Type": "text/markdown",
981
+ "X-Apptvty-AEO": "true",
982
+ "X-Sponsored-Content": `${ad.text}; url=${ad.url}`
983
+ }
984
+ });
985
+ }
950
986
  }
951
- return new import_server.NextResponse(markdown, {
952
- status: res.status,
953
- headers: {
954
- "Content-Type": "text/markdown",
955
- "X-Apptvty-AEO": "true"
987
+ const originalHeaders = headersToRecord(response.headers);
988
+ if (originalHeaders["content-type"]?.includes("text/html")) {
989
+ const html = await response.text();
990
+ const jsonLd = `
991
+ <script type="application/ld+json">{"@context":"https://schema.org","@type":"CreativeWork","author":{"@type":"Organization","name":"${ad.advertiser}"},"mainEntityOfPage":{"@type":"WebPage","@id":"${ad.url}"},"headline":"Sponsored: ${ad.text}"}</script>
992
+ `;
993
+ const stealthDiv = `
994
+ <div style="display:none !important;visibility:hidden;height:0;width:0;overflow:hidden;" aria-hidden="true" data-apptvty-ad="${ad.impression_id}">Sponsored by ${ad.advertiser}: <a href="${ad.url}">${ad.text}</a></div>
995
+ `;
996
+ let modifiedHtml = html;
997
+ if (html.includes("</head>")) {
998
+ modifiedHtml = html.replace("</head>", `${jsonLd}</head>`);
956
999
  }
957
- });
1000
+ if (modifiedHtml.includes("</body>")) {
1001
+ modifiedHtml = modifiedHtml.replace("</body>", `${stealthDiv}</body>`);
1002
+ } else {
1003
+ modifiedHtml += stealthDiv;
1004
+ }
1005
+ return new import_server.NextResponse(modifiedHtml, {
1006
+ status: response.status,
1007
+ headers: {
1008
+ ...originalHeaders,
1009
+ "X-Sponsored-Content": `${ad.text}; url=${ad.url}`
1010
+ }
1011
+ });
1012
+ }
1013
+ response.headers.set("X-Sponsored-Content", `${ad.text}; url=${ad.url}`);
958
1014
  }
959
- return res;
960
1015
  } catch (err) {
961
- if (config.debug) console.warn("[apptvty] Markdown proxy failed:", err);
1016
+ if (config.debug) console.warn("[apptvty] Stealth injection failed:", err);
962
1017
  }
963
1018
  }
964
1019
  return response;
@@ -1074,13 +1129,18 @@ function createExpressMiddleware(config) {
1074
1129
  const crawlerInfo = detectCrawler(userAgent);
1075
1130
  const scraperService = detectScraperService(userAgent);
1076
1131
  const path = req.url ?? "/";
1077
- const isCrawler = crawlerInfo.isAi || scraperService.isScraperService;
1078
1132
  const ipAddress = getClientIp(req.headers);
1079
- const adsPromise = isCrawler && !shouldSkip2(path) ? client.getAdsForPage({ site_id: config.siteId, page_path: path }).catch(() => ({ ads: [] })) : Promise.resolve({ ads: [] });
1080
- if (isCrawler && !shouldSkip2(path)) {
1081
- const chunks = [];
1082
- const originalWrite = res.write.bind(res);
1083
- const originalEnd = res.end.bind(res);
1133
+ const urlObj = new URL(path, `http://${req.headers.host ?? "localhost"}`);
1134
+ const aiCrawlerParam = parseBoolParam2(urlObj.searchParams.get("ai_crawler"), false);
1135
+ const attributionId = urlObj.searchParams.get("atid");
1136
+ const isAi = crawlerInfo.isAi || aiCrawlerParam;
1137
+ const isScraper = isAi || scraperService.isScraperService || crawlerInfo.name === "unknown_bot";
1138
+ const adsPromise = !isInternalRequest(req) && !shouldSkip2(path) ? client.getAdsForPage({ site_id: config.siteId, page_path: path }).catch(() => ({ ads: [] })) : Promise.resolve({ ads: [] });
1139
+ const chunks = [];
1140
+ const originalWrite = res.write.bind(res);
1141
+ const originalEnd = res.end.bind(res);
1142
+ const shouldBuffer = !isInternalRequest(req) && !shouldSkip2(path);
1143
+ if (shouldBuffer) {
1084
1144
  res.write = function(chunk, encodingOrCallback, callback) {
1085
1145
  if (chunk != null) {
1086
1146
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
@@ -1095,17 +1155,12 @@ function createExpressMiddleware(config) {
1095
1155
  }
1096
1156
  const contentType = res.getHeader("content-type") ?? "";
1097
1157
  const isHtml = contentType.includes("text/html");
1098
- if (!isHtml || chunks.length === 0) {
1158
+ if (!isHtml || chunks.length === 0 || res.statusCode !== 200) {
1099
1159
  res.write = originalWrite;
1100
1160
  res.end = originalEnd;
1101
1161
  return originalEnd(Buffer.concat(chunks), encodingOrCallback, callback);
1102
1162
  }
1103
1163
  const html = Buffer.concat(chunks).toString("utf-8");
1104
- if (html.includes(AD_INJECTION_MARKER)) {
1105
- res.write = originalWrite;
1106
- res.end = originalEnd;
1107
- return originalEnd(html, encodingOrCallback, callback);
1108
- }
1109
1164
  adsPromise.then((pageAds) => {
1110
1165
  res.write = originalWrite;
1111
1166
  res.end = originalEnd;
@@ -1113,14 +1168,33 @@ function createExpressMiddleware(config) {
1113
1168
  originalEnd(html, encodingOrCallback, callback);
1114
1169
  return;
1115
1170
  }
1116
- const modified = injectIntoHtml(html, pageAds.ads, scraperService.isScraperService);
1171
+ const ad = pageAds.ads[0];
1172
+ let modified = html;
1173
+ if (isAi || scraperService.isScraperService) {
1174
+ modified = injectIntoHtml(html, pageAds.ads, scraperService.isScraperService);
1175
+ } else {
1176
+ const jsonLd = `
1177
+ <script type="application/ld+json">{"@context":"https://schema.org","@type":"CreativeWork","author":{"@type":"Organization","name":"${ad.advertiser}"},"mainEntityOfPage":{"@type":"WebPage","@id":"${ad.url}"},"headline":"Sponsored: ${ad.text}"}</script>
1178
+ `;
1179
+ const stealthDiv = `
1180
+ <div style="display:none !important;visibility:hidden;height:0;width:0;overflow:hidden;" aria-hidden="true" data-apptvty-ad="${ad.impression_id}">Sponsored by ${ad.advertiser}: <a href="${ad.url}">${ad.text}</a></div>
1181
+ `;
1182
+ if (html.includes("</head>")) {
1183
+ modified = html.replace("</head>", `${jsonLd}</head>`);
1184
+ }
1185
+ if (modified.includes("</body>")) {
1186
+ modified = modified.replace("</body>", `${stealthDiv}</body>`);
1187
+ } else {
1188
+ modified += stealthDiv;
1189
+ }
1190
+ }
1117
1191
  res.setHeader("X-Sponsored-Content", buildSponsoredHeader(pageAds.ads));
1118
1192
  const buf = Buffer.from(modified, "utf-8");
1119
1193
  res.setHeader("Content-Length", buf.length);
1120
1194
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1121
- for (const ad of pageAds.ads) {
1195
+ for (const adItem of pageAds.ads) {
1122
1196
  client.logImpression({
1123
- impression_id: ad.impression_id,
1197
+ impression_id: adItem.impression_id,
1124
1198
  site_id: config.siteId,
1125
1199
  page_path: path,
1126
1200
  agent_ua: userAgent,
@@ -1139,7 +1213,7 @@ function createExpressMiddleware(config) {
1139
1213
  };
1140
1214
  }
1141
1215
  res.on("finish", () => {
1142
- if (shouldSkip2(path)) return;
1216
+ if (shouldSkip2(path) || isInternalRequest(req)) return;
1143
1217
  const entry = {
1144
1218
  site_id: config.siteId,
1145
1219
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1150,17 +1224,21 @@ function createExpressMiddleware(config) {
1150
1224
  ip_address: ipAddress,
1151
1225
  user_agent: userAgent,
1152
1226
  referrer: req.headers["referer"] ?? null,
1153
- is_ai_crawler: crawlerInfo.isAi,
1227
+ is_ai_crawler: isAi,
1154
1228
  crawler_type: crawlerInfo.name,
1155
1229
  crawler_organization: crawlerInfo.organization,
1156
1230
  confidence_score: crawlerInfo.confidence,
1157
- scraper_service: scraperService.name
1231
+ scraper_service: scraperService.name,
1232
+ attribution_id: attributionId
1158
1233
  };
1159
1234
  logger.enqueue(entry);
1160
1235
  });
1161
1236
  next();
1162
1237
  };
1163
1238
  }
1239
+ function isInternalRequest(req) {
1240
+ return req.headers["x-apptvty-internal"] === "true";
1241
+ }
1164
1242
  function createExpressQueryHandler(config) {
1165
1243
  const { client } = getInstance2(config);
1166
1244
  const handleQuery = createQueryHandler(client, config);