apptvty 0.2.4 → 0.3.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.
@@ -353,7 +353,7 @@ var ApptvtyClient = class {
353
353
  this.log("X402 challenge auto-paid from wallet, retrying request");
354
354
  return this.post(path, body);
355
355
  }
356
- let dashboardUrl = "https://dashboard.apptvty.com/login";
356
+ let dashboardUrl = (typeof process !== "undefined" ? process.env?.DASHBOARD_URL : void 0) ?? "https://dashboard.apptvty.com/login";
357
357
  try {
358
358
  const json = JSON.parse(text);
359
359
  dashboardUrl = json?.error?.details?.dashboard_url ?? dashboardUrl;
@@ -535,7 +535,7 @@ var RequestLogger = class {
535
535
  this.client = client;
536
536
  this.queue = [];
537
537
  this.timer = null;
538
- this.flushing = false;
538
+ this.activeFlushPromise = null;
539
539
  this.batchSize = config.batchSize ?? 50;
540
540
  this.debug = config.debug ?? false;
541
541
  const interval = config.flushInterval ?? 5e3;
@@ -560,21 +560,31 @@ var RequestLogger = class {
560
560
  /** Enqueue a single log entry. Non-blocking. */
561
561
  enqueue(entry) {
562
562
  this.queue.push(entry);
563
- if (this.queue.length >= this.batchSize) {
564
- void this.flush();
565
- }
563
+ void this.flush();
566
564
  }
567
- /** Flush the current queue to the API. */
565
+ /** Flush the current queue to the API. Returns the active Promise so Edge runtimes can wait. */
568
566
  async flush() {
569
- if (this.flushing || this.queue.length === 0) return;
570
- this.flushing = true;
571
- const batch = this.queue.splice(0, this.batchSize);
572
- try {
573
- await this.client.sendLogs(batch);
574
- } catch {
575
- } finally {
576
- this.flushing = false;
567
+ if (this.queue.length === 0) {
568
+ return this.activeFlushPromise || Promise.resolve();
577
569
  }
570
+ if (this.activeFlushPromise) {
571
+ await this.activeFlushPromise;
572
+ if (this.queue.length > 0) return this.flush();
573
+ return Promise.resolve();
574
+ }
575
+ const batch = this.queue.splice(0, this.batchSize);
576
+ this.activeFlushPromise = (async () => {
577
+ try {
578
+ await this.client.sendLogs(batch);
579
+ } catch {
580
+ } finally {
581
+ this.activeFlushPromise = null;
582
+ if (this.queue.length > 0) {
583
+ void this.flush();
584
+ }
585
+ }
586
+ })();
587
+ return this.activeFlushPromise;
578
588
  }
579
589
  /**
580
590
  * Synchronous-ish flush for process shutdown.
@@ -665,7 +675,7 @@ function createQueryHandler(client, config) {
665
675
  if (trimmedQuery.length > 500) {
666
676
  return errorResponse(400, "QUERY_TOO_LONG", "Query must be 500 characters or fewer");
667
677
  }
668
- const requestId = crypto.randomUUID();
678
+ const requestId = globalThis.crypto.randomUUID();
669
679
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
670
680
  const startMs = Date.now();
671
681
  const surfaceAds = req.surface_ads !== false;
@@ -693,16 +703,21 @@ function createQueryHandler(client, config) {
693
703
  return errorResponse(502, "UPSTREAM_ERROR", "Could not retrieve an answer at this time");
694
704
  }
695
705
  const responseTimeMs = Date.now() - startMs;
696
- const ads = backendResponse.sponsored ? Array.isArray(backendResponse.sponsored) ? backendResponse.sponsored : [backendResponse.sponsored] : [];
697
- for (const ad of ads) {
698
- void client.logImpression({
699
- impression_id: ad.impression_id,
700
- site_id: config.siteId,
701
- query: trimmedQuery,
702
- agent_ua: req.userAgent,
703
- agent_ip: req.ipAddress,
704
- timestamp
705
- });
706
+ const ads = backendResponse.related_resources ? Array.isArray(backendResponse.related_resources) ? backendResponse.related_resources : [backendResponse.related_resources] : [];
707
+ if (ads.length > 0) {
708
+ await Promise.all(
709
+ ads.map(
710
+ (ad) => client.logImpression({
711
+ impression_id: ad.impression_id,
712
+ site_id: config.siteId,
713
+ query: trimmedQuery,
714
+ agent_ua: req.userAgent,
715
+ agent_ip: req.ipAddress,
716
+ timestamp
717
+ }).catch(() => {
718
+ })
719
+ )
720
+ );
706
721
  }
707
722
  const agentResponse = {
708
723
  success: true,
@@ -711,7 +726,7 @@ function createQueryHandler(client, config) {
711
726
  answer: backendResponse.answer,
712
727
  sources: backendResponse.sources,
713
728
  confidence: backendResponse.confidence,
714
- ...backendResponse.sponsored && { sponsored: backendResponse.sponsored },
729
+ ...backendResponse.related_resources && { related_resources: backendResponse.related_resources },
715
730
  metadata: {
716
731
  request_id: requestId,
717
732
  response_time_ms: responseTimeMs,
@@ -729,7 +744,7 @@ function errorResponse(status, code, message) {
729
744
  error: {
730
745
  code,
731
746
  message,
732
- request_id: crypto.randomUUID(),
747
+ request_id: globalThis.crypto.randomUUID(),
733
748
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
734
749
  }
735
750
  };
@@ -748,20 +763,49 @@ function getOrigin(url) {
748
763
  function createDashboardHandler(client, config) {
749
764
  return async function handleDashboard(req) {
750
765
  const { path, method, authHeader } = req;
751
- if (config.dashboardSecret) {
752
- const url2 = new URL(path, "http://localhost");
753
- const secretParam = url2.searchParams.get("secret");
754
- const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
755
- const isAuthorized = secretParam === config.dashboardSecret || bearerToken === config.dashboardSecret;
756
- if (!isAuthorized) {
757
- return jsonResponse(401, {
758
- error: "Unauthorized",
759
- message: "Dashboard access requires a valid secret. Please set APPTVTY_DASHBOARD_SECRET."
760
- });
761
- }
762
- }
763
766
  const url = new URL(path, "http://localhost");
764
767
  const cleanPath = url.pathname;
768
+ if (cleanPath.includes("/api/apptvty/verify")) {
769
+ const challenge = url.searchParams.get("challenge");
770
+ if (!challenge) {
771
+ return jsonResponse(400, { error: "Challenge required" });
772
+ }
773
+ const encoder = new TextEncoder();
774
+ const keyData = encoder.encode(config.apiKey);
775
+ const cryptoKey = await globalThis.crypto.subtle.importKey(
776
+ "raw",
777
+ keyData,
778
+ { name: "HMAC", hash: "SHA-256" },
779
+ false,
780
+ ["sign"]
781
+ );
782
+ const prefixedChallenge = `apptvty_verify_challenge:${challenge}`;
783
+ const signatureBuffer = await globalThis.crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(prefixedChallenge));
784
+ const signature = Array.from(new Uint8Array(signatureBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
785
+ return jsonResponse(200, {
786
+ site_id: config.siteId,
787
+ verified: true,
788
+ signature
789
+ });
790
+ }
791
+ if (!config.dashboardSecret) {
792
+ return jsonResponse(401, {
793
+ error: "Unauthorized",
794
+ message: "Dashboard access is disabled because APPTVTY_DASHBOARD_SECRET is not configured."
795
+ });
796
+ }
797
+ const secretParam = url.searchParams.get("secret");
798
+ const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
799
+ const isAuthorized = secretParam === config.dashboardSecret || bearerToken === config.dashboardSecret;
800
+ if (!isAuthorized) {
801
+ return jsonResponse(401, {
802
+ error: "Unauthorized",
803
+ message: "Invalid dashboard secret. Please provide a valid secret to access."
804
+ });
805
+ }
806
+ if (config.debug) {
807
+ console.error(`[apptvty] DashboardHandler: path=${path}, cleanPath=${cleanPath}`);
808
+ }
765
809
  if (cleanPath.includes("/api/overview")) {
766
810
  const data = await client.getSiteStats();
767
811
  return jsonResponse(200, data);
@@ -1122,16 +1166,38 @@ function getInstance(config) {
1122
1166
  function withApptvty(config, next) {
1123
1167
  const { client, logger } = getInstance(config);
1124
1168
  const queryPath = config.queryPath ?? "/query";
1125
- return async function apptvtyMiddleware(request) {
1169
+ return async function apptvtyMiddleware(request, event) {
1126
1170
  const startMs = Date.now();
1127
1171
  const userAgent = request.headers.get("user-agent") ?? "";
1128
1172
  const crawlerInfo = detectCrawler(userAgent);
1129
1173
  const scraperService = detectScraperService(userAgent);
1130
1174
  const aiCrawlerParam = parseBoolParam(request.nextUrl.searchParams.get("ai_crawler"), false);
1131
1175
  const isCrawler = crawlerInfo.isAi || aiCrawlerParam || scraperService.isScraperService;
1176
+ if (request.nextUrl.pathname === "/api/apptvty/verify") {
1177
+ const challenge = request.nextUrl.searchParams.get("challenge");
1178
+ if (challenge) {
1179
+ const encoder = new TextEncoder();
1180
+ const keyData = encoder.encode(config.apiKey);
1181
+ const cryptoKey = await globalThis.crypto.subtle.importKey(
1182
+ "raw",
1183
+ keyData,
1184
+ { name: "HMAC", hash: "SHA-256" },
1185
+ false,
1186
+ ["sign"]
1187
+ );
1188
+ const prefixedChallenge = `apptvty_verify_challenge:${challenge}`;
1189
+ const signatureBuffer = await globalThis.crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(prefixedChallenge));
1190
+ const signature = Array.from(new Uint8Array(signatureBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
1191
+ return import_server.NextResponse.json({
1192
+ site_id: config.siteId,
1193
+ verified: true,
1194
+ signature
1195
+ });
1196
+ }
1197
+ }
1132
1198
  let response;
1133
1199
  try {
1134
- response = next ? await next(request) : import_server.NextResponse.next();
1200
+ response = next ? await next(request, event) : import_server.NextResponse.next();
1135
1201
  } catch (err) {
1136
1202
  throw err;
1137
1203
  }
@@ -1144,13 +1210,13 @@ function withApptvty(config, next) {
1144
1210
  const entry = {
1145
1211
  site_id: config.siteId,
1146
1212
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1147
- method: request.method,
1148
- path: pathname,
1149
- status_code: response.status,
1213
+ request_method: request.method,
1214
+ request_path: pathname,
1215
+ response_status: response.status,
1150
1216
  response_time_ms: responseTimeMs,
1151
1217
  ip_address: getClientIp(headers),
1152
1218
  user_agent: userAgent,
1153
- referer: request.headers.get("referer"),
1219
+ referrer: request.headers.get("referer"),
1154
1220
  is_ai_crawler: crawlerInfo.isAi,
1155
1221
  crawler_type: crawlerInfo.name,
1156
1222
  crawler_organization: crawlerInfo.organization,
@@ -1158,6 +1224,9 @@ function withApptvty(config, next) {
1158
1224
  scraper_service: scraperService.name
1159
1225
  };
1160
1226
  logger.enqueue(entry);
1227
+ if (event && typeof event.waitUntil === "function") {
1228
+ event.waitUntil(logger.flush());
1229
+ }
1161
1230
  if (isCrawler && response.ok && !pathname.startsWith(queryPath)) {
1162
1231
  const contentType = response.headers.get("content-type") ?? "";
1163
1232
  if (contentType.includes("text/html")) {