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.
@@ -108,6 +108,28 @@ var ApptvtyClient = class {
108
108
  this.warn("Failed to log impression (billing may be delayed):", err);
109
109
  }
110
110
  }
111
+ // ─── Programmatic Indexing ───────────────────────────────────────────────────
112
+ /**
113
+ * Manually push text or markdown directly into your site's RAG vector database.
114
+ * This instantly embeds the content for AI agents to query, bypassing the
115
+ * need for a scheduled HTML crawl. This is ideal for Client-Side Rendered (CSR)
116
+ * Apps or for injecting private institutional facts/databases.
117
+ *
118
+ * @example
119
+ * const result = await client.pushKnowledge({
120
+ * title: "Capitol Technology University Profile",
121
+ * content: "Here are all the detailed specs, majors, and acceptances...",
122
+ * url: "https://thecollegeradar.com/institution/499" // Optional reference
123
+ * });
124
+ */
125
+ async pushKnowledge(params) {
126
+ const payload = {
127
+ title: params.title,
128
+ content: params.content,
129
+ url: params.url
130
+ };
131
+ return this.post(`/v1/sites/${this.siteId}/knowledge`, payload);
132
+ }
111
133
  // ─── Analytics (for coding agents) ───────────────────────────────────────────
112
134
  // These allow agents to check activity, logs, and errors without a human.
113
135
  /** Get 30-day traffic overview (requests, AI %, crawlers, queries). */
@@ -355,8 +377,12 @@ var ApptvtyClient = class {
355
377
  let dashboardUrl = (typeof process !== "undefined" ? process.env?.DASHBOARD_URL : void 0) ?? "https://dashboard.apptvty.com/login";
356
378
  try {
357
379
  const json = JSON.parse(text);
380
+ if (json?.error?.code === "INSUFFICIENT_BALANCE") {
381
+ throw new ApptvtyApiError(402, path, text);
382
+ }
358
383
  dashboardUrl = json?.error?.details?.dashboard_url ?? dashboardUrl;
359
- } catch {
384
+ } catch (e) {
385
+ if (e instanceof ApptvtyApiError) throw e;
360
386
  }
361
387
  throw new ApptvtyTrialExpiredError(dashboardUrl);
362
388
  }
@@ -1166,13 +1192,18 @@ function createExpressMiddleware(config) {
1166
1192
  const crawlerInfo = detectCrawler(userAgent);
1167
1193
  const scraperService = detectScraperService(userAgent);
1168
1194
  const path = req.url ?? "/";
1169
- const isCrawler = crawlerInfo.isAi || scraperService.isScraperService;
1170
1195
  const ipAddress = getClientIp(req.headers);
1171
- const adsPromise = isCrawler && !shouldSkip(path) ? client.getAdsForPage({ site_id: config.siteId, page_path: path }).catch(() => ({ ads: [] })) : Promise.resolve({ ads: [] });
1172
- if (isCrawler && !shouldSkip(path)) {
1173
- const chunks = [];
1174
- const originalWrite = res.write.bind(res);
1175
- const originalEnd = res.end.bind(res);
1196
+ const urlObj = new URL(path, `http://${req.headers.host ?? "localhost"}`);
1197
+ const aiCrawlerParam = parseBoolParam(urlObj.searchParams.get("ai_crawler"), false);
1198
+ const attributionId = urlObj.searchParams.get("atid");
1199
+ const isAi = crawlerInfo.isAi || aiCrawlerParam;
1200
+ const isScraper = isAi || scraperService.isScraperService || crawlerInfo.name === "unknown_bot";
1201
+ const adsPromise = !isInternalRequest(req) && !shouldSkip(path) ? client.getAdsForPage({ site_id: config.siteId, page_path: path }).catch(() => ({ ads: [] })) : Promise.resolve({ ads: [] });
1202
+ const chunks = [];
1203
+ const originalWrite = res.write.bind(res);
1204
+ const originalEnd = res.end.bind(res);
1205
+ const shouldBuffer = !isInternalRequest(req) && !shouldSkip(path);
1206
+ if (shouldBuffer) {
1176
1207
  res.write = function(chunk, encodingOrCallback, callback) {
1177
1208
  if (chunk != null) {
1178
1209
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
@@ -1187,17 +1218,12 @@ function createExpressMiddleware(config) {
1187
1218
  }
1188
1219
  const contentType = res.getHeader("content-type") ?? "";
1189
1220
  const isHtml = contentType.includes("text/html");
1190
- if (!isHtml || chunks.length === 0) {
1221
+ if (!isHtml || chunks.length === 0 || res.statusCode !== 200) {
1191
1222
  res.write = originalWrite;
1192
1223
  res.end = originalEnd;
1193
1224
  return originalEnd(Buffer.concat(chunks), encodingOrCallback, callback);
1194
1225
  }
1195
1226
  const html = Buffer.concat(chunks).toString("utf-8");
1196
- if (html.includes(AD_INJECTION_MARKER)) {
1197
- res.write = originalWrite;
1198
- res.end = originalEnd;
1199
- return originalEnd(html, encodingOrCallback, callback);
1200
- }
1201
1227
  adsPromise.then((pageAds) => {
1202
1228
  res.write = originalWrite;
1203
1229
  res.end = originalEnd;
@@ -1205,14 +1231,33 @@ function createExpressMiddleware(config) {
1205
1231
  originalEnd(html, encodingOrCallback, callback);
1206
1232
  return;
1207
1233
  }
1208
- const modified = injectIntoHtml(html, pageAds.ads, scraperService.isScraperService);
1234
+ const ad = pageAds.ads[0];
1235
+ let modified = html;
1236
+ if (isAi || scraperService.isScraperService) {
1237
+ modified = injectIntoHtml(html, pageAds.ads, scraperService.isScraperService);
1238
+ } else {
1239
+ const jsonLd = `
1240
+ <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>
1241
+ `;
1242
+ const stealthDiv = `
1243
+ <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>
1244
+ `;
1245
+ if (html.includes("</head>")) {
1246
+ modified = html.replace("</head>", `${jsonLd}</head>`);
1247
+ }
1248
+ if (modified.includes("</body>")) {
1249
+ modified = modified.replace("</body>", `${stealthDiv}</body>`);
1250
+ } else {
1251
+ modified += stealthDiv;
1252
+ }
1253
+ }
1209
1254
  res.setHeader("X-Sponsored-Content", buildSponsoredHeader(pageAds.ads));
1210
1255
  const buf = Buffer.from(modified, "utf-8");
1211
1256
  res.setHeader("Content-Length", buf.length);
1212
1257
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1213
- for (const ad of pageAds.ads) {
1258
+ for (const adItem of pageAds.ads) {
1214
1259
  client.logImpression({
1215
- impression_id: ad.impression_id,
1260
+ impression_id: adItem.impression_id,
1216
1261
  site_id: config.siteId,
1217
1262
  page_path: path,
1218
1263
  agent_ua: userAgent,
@@ -1231,7 +1276,7 @@ function createExpressMiddleware(config) {
1231
1276
  };
1232
1277
  }
1233
1278
  res.on("finish", () => {
1234
- if (shouldSkip(path)) return;
1279
+ if (shouldSkip(path) || isInternalRequest(req)) return;
1235
1280
  const entry = {
1236
1281
  site_id: config.siteId,
1237
1282
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1242,17 +1287,21 @@ function createExpressMiddleware(config) {
1242
1287
  ip_address: ipAddress,
1243
1288
  user_agent: userAgent,
1244
1289
  referrer: req.headers["referer"] ?? null,
1245
- is_ai_crawler: crawlerInfo.isAi,
1290
+ is_ai_crawler: isAi,
1246
1291
  crawler_type: crawlerInfo.name,
1247
1292
  crawler_organization: crawlerInfo.organization,
1248
1293
  confidence_score: crawlerInfo.confidence,
1249
- scraper_service: scraperService.name
1294
+ scraper_service: scraperService.name,
1295
+ attribution_id: attributionId
1250
1296
  };
1251
1297
  logger.enqueue(entry);
1252
1298
  });
1253
1299
  next();
1254
1300
  };
1255
1301
  }
1302
+ function isInternalRequest(req) {
1303
+ return req.headers["x-apptvty-internal"] === "true";
1304
+ }
1256
1305
  function createExpressQueryHandler(config) {
1257
1306
  const { client } = getInstance(config);
1258
1307
  const handleQuery = createQueryHandler(client, config);