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.
- package/README.md +1 -1
- package/dist/{chunk-QERJ6LJA.mjs → chunk-4WO7W6JR.mjs} +33 -8
- package/dist/chunk-4WO7W6JR.mjs.map +1 -0
- package/dist/{chunk-WBHRLAUR.mjs → chunk-A6GFTPJ5.mjs} +84 -40
- package/dist/chunk-A6GFTPJ5.mjs.map +1 -0
- package/dist/{chunk-JILPRROG.mjs → chunk-EFCEIG74.mjs} +6 -6
- package/dist/{chunk-JILPRROG.mjs.map → chunk-EFCEIG74.mjs.map} +1 -1
- package/dist/cli.js +27 -6
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +77 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -3
- package/dist/middleware/express.d.mts +1 -1
- package/dist/middleware/express.d.ts +1 -1
- package/dist/middleware/express.js +87 -43
- package/dist/middleware/express.js.map +1 -1
- package/dist/middleware/express.mjs +2 -2
- package/dist/middleware/nextjs.d.mts +3 -3
- package/dist/middleware/nextjs.d.ts +3 -3
- package/dist/middleware/nextjs.js +114 -45
- package/dist/middleware/nextjs.js.map +1 -1
- package/dist/middleware/nextjs.mjs +2 -2
- package/dist/setup.d.mts +12 -1
- package/dist/setup.d.ts +12 -1
- package/dist/setup.js +43 -0
- package/dist/setup.js.map +1 -1
- package/dist/setup.mjs +41 -0
- package/dist/setup.mjs.map +1 -1
- package/dist/{types-Zt2qHOrW.d.mts → types-DwAWwqqD.d.mts} +36 -15
- package/dist/{types-Zt2qHOrW.d.ts → types-DwAWwqqD.d.ts} +36 -15
- package/package.json +1 -1
- package/dist/chunk-QERJ6LJA.mjs.map +0 -1
- package/dist/chunk-WBHRLAUR.mjs.map +0 -1
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
570
|
-
|
|
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.
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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.
|
|
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
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
-
|
|
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")) {
|