@peaje/proxy 0.2.0 → 0.2.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/README.md CHANGED
@@ -102,6 +102,10 @@ The forwarded request keeps method, body, query and `authorization` (the MPP cre
102
102
 
103
103
  Upgrading from 0.1.x: if your priced routes live outside `/api/` and `/v1/`, add their prefix to `apiPrefixes` before you deploy 0.2.0.
104
104
 
105
+ ## Bot visits (since 0.2.1)
106
+
107
+ When a known AI bot (GPTBot, ClaudeBot, OAI-SearchBot, PerplexityBot, Googlebot) reads a page your app serves itself, such as your home page or a blog post, the proxy tells the gateway in the background, so the read shows up in your agent visits. The gateway never sees those requests otherwise. It sends only the path, without the query string, and the bot's user agent: no IP, no cookies, and visits from people are never reported. The Vercel adapter runs it in `waitUntil`, Hono on Workers in `executionCtx.waitUntil`, and `peajeFetch(request, waitUntil)` takes one; elsewhere it runs alongside the response with a 3 s timeout. Turn it off with `{ slug, reportBotVisits: false }`.
108
+
105
109
  ## Options
106
110
 
107
111
  | Option | Default |
@@ -110,6 +114,7 @@ Upgrading from 0.1.x: if your priced routes live outside `/api/` and `/v1/`, add
110
114
  | `secret` | `process.env.PEAJE_ORIGIN_SECRET` |
111
115
  | `gateway` | `process.env.PEAJE_GATEWAY_URL` or `https://api.usepeaje.com` |
112
116
  | `apiPrefixes` | `['/api/', '/v1/']`: where your priced API routes live; the root and reserved paths are never accepted |
117
+ | `reportBotVisits` | `true`: tell the gateway when an AI bot reads a page of your app, for your agent visits |
113
118
 
114
119
  ## License
115
120
 
@@ -55,6 +55,24 @@ var PATHS_RESERVADOS = [
55
55
  var PEAJE_FETCH_HEADER = "x-peaje-fetch";
56
56
  var PEAJE_HOST_HEADER = "x-peaje-forwarded-host";
57
57
 
58
+ // ../shared/src/kit/bots.ts
59
+ var BOTS_DE_VISITA = [
60
+ { patron: /OAI-SearchBot/i, tipo: "openai-searchbot" },
61
+ { patron: /PerplexityBot/i, tipo: "perplexitybot" },
62
+ { patron: /Googlebot/i, tipo: "googlebot" },
63
+ { patron: /ClaudeBot|anthropic-ai/i, tipo: "claudebot" },
64
+ { patron: /GPTBot/i, tipo: "gptbot" }
65
+ ];
66
+ function tipoDeBot(ua) {
67
+ if (!ua) return null;
68
+ for (const b of BOTS_DE_VISITA) if (b.patron.test(ua)) return b.tipo;
69
+ return null;
70
+ }
71
+ var PEAJE_VERIFICADOR_HEADER = "x-peaje-verificador";
72
+ var NO_ES_PAGINA = /\.(png|jpe?g|gif|webp|avif|svg|ico|css|m?js|map|woff2?|ttf|otf|eot|mp4|webm|mp3|wav)$/i;
73
+ var MAX_PATH_LECTURA = 500;
74
+ var MAX_UA_LECTURA = 300;
75
+
58
76
  // ../shared/src/kit/proxy-runtime.ts
59
77
  var PEAJE_ORIGIN_HEADER = "x-peaje-origin";
60
78
  function iguales(a, b) {
@@ -131,6 +149,17 @@ function patronReenviable(patron) {
131
149
  if (segs.length === 0) return false;
132
150
  return !(segs.length === 1 && (segs[0] === "*" || segs[0].startsWith(":")));
133
151
  }
152
+ function lecturaDeBot(input) {
153
+ const method = input.method.toUpperCase();
154
+ if (method !== "GET" && method !== "HEAD") return null;
155
+ const h = input.headers;
156
+ if (h.has(PEAJE_ORIGIN_HEADER) || h.has(PEAJE_FETCH_HEADER) || h.has(PEAJE_VERIFICADOR_HEADER)) return null;
157
+ const ua = h.get("user-agent");
158
+ if (!ua || !tipoDeBot(ua)) return null;
159
+ const path = input.pathname;
160
+ if (!path.startsWith("/") || path.startsWith("/_next/") || path.startsWith("/_vercel/") || NO_ES_PAGINA.test(path)) return null;
161
+ return { path: path.slice(0, MAX_PATH_LECTURA), method, userAgent: ua.slice(0, MAX_UA_LECTURA) };
162
+ }
134
163
  function crearProxyRuntime(opciones) {
135
164
  const base = opciones.base.replace(/\/+$/, "");
136
165
  const ttl = opciones.ttlMs ?? 6e4;
@@ -175,6 +204,22 @@ function crearProxyRuntime(opciones) {
175
204
  },
176
205
  /** Fuerza la carga del manifiesto (útil al arrancar o en tests). */
177
206
  cargar: () => refrescando ??= refrescar(),
207
+ /**
208
+ * Si la request es un bot leyendo una página del sitio, le avisa al
209
+ * gateway y devuelve la promesa del aviso (para `waitUntil`); si no, null.
210
+ * El aviso nunca lanza ni demora la página: 3 s de tope y se descarta la
211
+ * respuesta.
212
+ */
213
+ reportarLectura(input) {
214
+ const lectura = lecturaDeBot(input);
215
+ if (!lectura) return null;
216
+ return fetchImpl(`${base}/kit/visita`, {
217
+ method: "POST",
218
+ headers: { "content-type": "application/json" },
219
+ body: JSON.stringify(lectura),
220
+ signal: AbortSignal.timeout(3e3)
221
+ }).then((res) => res.body?.cancel()).catch(() => void 0);
222
+ },
178
223
  /**
179
224
  * Decide si la request va al gateway. `method` y `pathname` de la request,
180
225
  * `search` con el `?` incluido, y los headers para cortar bucles.
@@ -366,6 +411,7 @@ function crearPeaje(opciones) {
366
411
  const secreto = opciones.secret ?? env("PEAJE_ORIGIN_SECRET") ?? null;
367
412
  const fetchImpl = opciones.fetch ?? ((...a) => fetch(...a));
368
413
  const runtime = crearProxyRuntime({ base, secreto, fetchImpl, prefijosApi: opciones.apiPrefixes ?? PREFIJOS_API_POR_DEFECTO });
414
+ const reportar = opciones.reportBotVisits !== false;
369
415
  return {
370
416
  base,
371
417
  async decidir({ method, url, headers }) {
@@ -393,16 +439,26 @@ function crearPeaje(opciones) {
393
439
  vuelta.delete("transfer-encoding");
394
440
  vuelta.delete("connection");
395
441
  return new Response(method === "HEAD" ? null : res.body, { status: res.status, statusText: res.statusText, headers: vuelta });
442
+ },
443
+ reportarLectura({ method, url, headers }) {
444
+ return reportar ? runtime.reportarLectura({ method, pathname: url.pathname, headers }) : null;
396
445
  }
397
446
  };
398
447
  }
399
448
  function peajeFetch(opciones) {
400
449
  const peaje = crearPeaje(opciones);
401
- return async (request) => {
450
+ return async (request, waitUntil) => {
402
451
  const url = new URL(request.url);
403
452
  const entrada = { method: request.method.toUpperCase(), url, headers: request.headers };
404
453
  const destino = await peaje.decidir(entrada);
405
- if (!destino) return null;
454
+ if (!destino) {
455
+ const lectura = peaje.reportarLectura(entrada);
456
+ if (lectura) {
457
+ if (waitUntil) waitUntil(lectura);
458
+ else void lectura;
459
+ }
460
+ return null;
461
+ }
406
462
  const body = entrada.method === "GET" || entrada.method === "HEAD" ? null : await request.arrayBuffer();
407
463
  return peaje.reenviar(destino, { ...entrada, body });
408
464
  };
package/dist/express.cjs CHANGED
@@ -83,6 +83,24 @@ var PATHS_RESERVADOS = [
83
83
  var PEAJE_FETCH_HEADER = "x-peaje-fetch";
84
84
  var PEAJE_HOST_HEADER = "x-peaje-forwarded-host";
85
85
 
86
+ // ../shared/src/kit/bots.ts
87
+ var BOTS_DE_VISITA = [
88
+ { patron: /OAI-SearchBot/i, tipo: "openai-searchbot" },
89
+ { patron: /PerplexityBot/i, tipo: "perplexitybot" },
90
+ { patron: /Googlebot/i, tipo: "googlebot" },
91
+ { patron: /ClaudeBot|anthropic-ai/i, tipo: "claudebot" },
92
+ { patron: /GPTBot/i, tipo: "gptbot" }
93
+ ];
94
+ function tipoDeBot(ua) {
95
+ if (!ua) return null;
96
+ for (const b of BOTS_DE_VISITA) if (b.patron.test(ua)) return b.tipo;
97
+ return null;
98
+ }
99
+ var PEAJE_VERIFICADOR_HEADER = "x-peaje-verificador";
100
+ var NO_ES_PAGINA = /\.(png|jpe?g|gif|webp|avif|svg|ico|css|m?js|map|woff2?|ttf|otf|eot|mp4|webm|mp3|wav)$/i;
101
+ var MAX_PATH_LECTURA = 500;
102
+ var MAX_UA_LECTURA = 300;
103
+
86
104
  // ../shared/src/kit/proxy-runtime.ts
87
105
  var PEAJE_ORIGIN_HEADER = "x-peaje-origin";
88
106
  function iguales(a, b) {
@@ -159,6 +177,17 @@ function patronReenviable(patron) {
159
177
  if (segs.length === 0) return false;
160
178
  return !(segs.length === 1 && (segs[0] === "*" || segs[0].startsWith(":")));
161
179
  }
180
+ function lecturaDeBot(input) {
181
+ const method = input.method.toUpperCase();
182
+ if (method !== "GET" && method !== "HEAD") return null;
183
+ const h = input.headers;
184
+ if (h.has(PEAJE_ORIGIN_HEADER) || h.has(PEAJE_FETCH_HEADER) || h.has(PEAJE_VERIFICADOR_HEADER)) return null;
185
+ const ua = h.get("user-agent");
186
+ if (!ua || !tipoDeBot(ua)) return null;
187
+ const path = input.pathname;
188
+ if (!path.startsWith("/") || path.startsWith("/_next/") || path.startsWith("/_vercel/") || NO_ES_PAGINA.test(path)) return null;
189
+ return { path: path.slice(0, MAX_PATH_LECTURA), method, userAgent: ua.slice(0, MAX_UA_LECTURA) };
190
+ }
162
191
  function crearProxyRuntime(opciones) {
163
192
  const base = opciones.base.replace(/\/+$/, "");
164
193
  const ttl = opciones.ttlMs ?? 6e4;
@@ -203,6 +232,22 @@ function crearProxyRuntime(opciones) {
203
232
  },
204
233
  /** Fuerza la carga del manifiesto (útil al arrancar o en tests). */
205
234
  cargar: () => refrescando ??= refrescar(),
235
+ /**
236
+ * Si la request es un bot leyendo una página del sitio, le avisa al
237
+ * gateway y devuelve la promesa del aviso (para `waitUntil`); si no, null.
238
+ * El aviso nunca lanza ni demora la página: 3 s de tope y se descarta la
239
+ * respuesta.
240
+ */
241
+ reportarLectura(input) {
242
+ const lectura = lecturaDeBot(input);
243
+ if (!lectura) return null;
244
+ return fetchImpl(`${base}/kit/visita`, {
245
+ method: "POST",
246
+ headers: { "content-type": "application/json" },
247
+ body: JSON.stringify(lectura),
248
+ signal: AbortSignal.timeout(3e3)
249
+ }).then((res) => res.body?.cancel()).catch(() => void 0);
250
+ },
206
251
  /**
207
252
  * Decide si la request va al gateway. `method` y `pathname` de la request,
208
253
  * `search` con el `?` incluido, y los headers para cortar bucles.
@@ -391,6 +436,7 @@ function crearPeaje(opciones) {
391
436
  const secreto = opciones.secret ?? env("PEAJE_ORIGIN_SECRET") ?? null;
392
437
  const fetchImpl = opciones.fetch ?? ((...a) => fetch(...a));
393
438
  const runtime = crearProxyRuntime({ base, secreto, fetchImpl, prefijosApi: opciones.apiPrefixes ?? PREFIJOS_API_POR_DEFECTO });
439
+ const reportar = opciones.reportBotVisits !== false;
394
440
  return {
395
441
  base,
396
442
  async decidir({ method, url, headers }) {
@@ -418,6 +464,9 @@ function crearPeaje(opciones) {
418
464
  vuelta.delete("transfer-encoding");
419
465
  vuelta.delete("connection");
420
466
  return new Response(method === "HEAD" ? null : res.body, { status: res.status, statusText: res.statusText, headers: vuelta });
467
+ },
468
+ reportarLectura({ method, url, headers }) {
469
+ return reportar ? runtime.reportarLectura({ method, pathname: url.pathname, headers }) : null;
421
470
  }
422
471
  };
423
472
  }
@@ -477,7 +526,10 @@ function peajeExpress(opciones) {
477
526
  const r = req;
478
527
  const entrada = { method: (r.method ?? "GET").toUpperCase(), url: urlDe(r), headers: headersDe(r) };
479
528
  peaje.decidir(entrada).then(async (destino) => {
480
- if (!destino) return next();
529
+ if (!destino) {
530
+ void peaje.reportarLectura(entrada);
531
+ return next();
532
+ }
481
533
  const body = entrada.method === "GET" || entrada.method === "HEAD" ? null : await bodyDe(r);
482
534
  await enviar(res, await peaje.reenviar(destino, { ...entrada, body }));
483
535
  }).catch((e) => {
package/dist/express.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  crearPeaje
3
- } from "./chunk-QHUY5N5S.js";
3
+ } from "./chunk-SWB3HJVY.js";
4
4
 
5
5
  // src/express.ts
6
6
  import { Readable } from "stream";
@@ -58,7 +58,10 @@ function peajeExpress(opciones) {
58
58
  const r = req;
59
59
  const entrada = { method: (r.method ?? "GET").toUpperCase(), url: urlDe(r), headers: headersDe(r) };
60
60
  peaje.decidir(entrada).then(async (destino) => {
61
- if (!destino) return next();
61
+ if (!destino) {
62
+ void peaje.reportarLectura(entrada);
63
+ return next();
64
+ }
62
65
  const body = entrada.method === "GET" || entrada.method === "HEAD" ? null : await bodyDe(r);
63
66
  await enviar(res, await peaje.reenviar(destino, { ...entrada, body }));
64
67
  }).catch((e) => {
package/dist/hono.cjs CHANGED
@@ -82,6 +82,24 @@ var PATHS_RESERVADOS = [
82
82
  var PEAJE_FETCH_HEADER = "x-peaje-fetch";
83
83
  var PEAJE_HOST_HEADER = "x-peaje-forwarded-host";
84
84
 
85
+ // ../shared/src/kit/bots.ts
86
+ var BOTS_DE_VISITA = [
87
+ { patron: /OAI-SearchBot/i, tipo: "openai-searchbot" },
88
+ { patron: /PerplexityBot/i, tipo: "perplexitybot" },
89
+ { patron: /Googlebot/i, tipo: "googlebot" },
90
+ { patron: /ClaudeBot|anthropic-ai/i, tipo: "claudebot" },
91
+ { patron: /GPTBot/i, tipo: "gptbot" }
92
+ ];
93
+ function tipoDeBot(ua) {
94
+ if (!ua) return null;
95
+ for (const b of BOTS_DE_VISITA) if (b.patron.test(ua)) return b.tipo;
96
+ return null;
97
+ }
98
+ var PEAJE_VERIFICADOR_HEADER = "x-peaje-verificador";
99
+ var NO_ES_PAGINA = /\.(png|jpe?g|gif|webp|avif|svg|ico|css|m?js|map|woff2?|ttf|otf|eot|mp4|webm|mp3|wav)$/i;
100
+ var MAX_PATH_LECTURA = 500;
101
+ var MAX_UA_LECTURA = 300;
102
+
85
103
  // ../shared/src/kit/proxy-runtime.ts
86
104
  var PEAJE_ORIGIN_HEADER = "x-peaje-origin";
87
105
  function iguales(a, b) {
@@ -158,6 +176,17 @@ function patronReenviable(patron) {
158
176
  if (segs.length === 0) return false;
159
177
  return !(segs.length === 1 && (segs[0] === "*" || segs[0].startsWith(":")));
160
178
  }
179
+ function lecturaDeBot(input) {
180
+ const method = input.method.toUpperCase();
181
+ if (method !== "GET" && method !== "HEAD") return null;
182
+ const h = input.headers;
183
+ if (h.has(PEAJE_ORIGIN_HEADER) || h.has(PEAJE_FETCH_HEADER) || h.has(PEAJE_VERIFICADOR_HEADER)) return null;
184
+ const ua = h.get("user-agent");
185
+ if (!ua || !tipoDeBot(ua)) return null;
186
+ const path = input.pathname;
187
+ if (!path.startsWith("/") || path.startsWith("/_next/") || path.startsWith("/_vercel/") || NO_ES_PAGINA.test(path)) return null;
188
+ return { path: path.slice(0, MAX_PATH_LECTURA), method, userAgent: ua.slice(0, MAX_UA_LECTURA) };
189
+ }
161
190
  function crearProxyRuntime(opciones) {
162
191
  const base = opciones.base.replace(/\/+$/, "");
163
192
  const ttl = opciones.ttlMs ?? 6e4;
@@ -202,6 +231,22 @@ function crearProxyRuntime(opciones) {
202
231
  },
203
232
  /** Fuerza la carga del manifiesto (útil al arrancar o en tests). */
204
233
  cargar: () => refrescando ??= refrescar(),
234
+ /**
235
+ * Si la request es un bot leyendo una página del sitio, le avisa al
236
+ * gateway y devuelve la promesa del aviso (para `waitUntil`); si no, null.
237
+ * El aviso nunca lanza ni demora la página: 3 s de tope y se descarta la
238
+ * respuesta.
239
+ */
240
+ reportarLectura(input) {
241
+ const lectura = lecturaDeBot(input);
242
+ if (!lectura) return null;
243
+ return fetchImpl(`${base}/kit/visita`, {
244
+ method: "POST",
245
+ headers: { "content-type": "application/json" },
246
+ body: JSON.stringify(lectura),
247
+ signal: AbortSignal.timeout(3e3)
248
+ }).then((res) => res.body?.cancel()).catch(() => void 0);
249
+ },
205
250
  /**
206
251
  * Decide si la request va al gateway. `method` y `pathname` de la request,
207
252
  * `search` con el `?` incluido, y los headers para cortar bucles.
@@ -390,6 +435,7 @@ function crearPeaje(opciones) {
390
435
  const secreto = opciones.secret ?? env("PEAJE_ORIGIN_SECRET") ?? null;
391
436
  const fetchImpl = opciones.fetch ?? ((...a) => fetch(...a));
392
437
  const runtime = crearProxyRuntime({ base, secreto, fetchImpl, prefijosApi: opciones.apiPrefixes ?? PREFIJOS_API_POR_DEFECTO });
438
+ const reportar = opciones.reportBotVisits !== false;
393
439
  return {
394
440
  base,
395
441
  async decidir({ method, url, headers }) {
@@ -417,16 +463,26 @@ function crearPeaje(opciones) {
417
463
  vuelta.delete("transfer-encoding");
418
464
  vuelta.delete("connection");
419
465
  return new Response(method === "HEAD" ? null : res.body, { status: res.status, statusText: res.statusText, headers: vuelta });
466
+ },
467
+ reportarLectura({ method, url, headers }) {
468
+ return reportar ? runtime.reportarLectura({ method, pathname: url.pathname, headers }) : null;
420
469
  }
421
470
  };
422
471
  }
423
472
  function peajeFetch(opciones) {
424
473
  const peaje = crearPeaje(opciones);
425
- return async (request) => {
474
+ return async (request, waitUntil) => {
426
475
  const url = new URL(request.url);
427
476
  const entrada = { method: request.method.toUpperCase(), url, headers: request.headers };
428
477
  const destino = await peaje.decidir(entrada);
429
- if (!destino) return null;
478
+ if (!destino) {
479
+ const lectura = peaje.reportarLectura(entrada);
480
+ if (lectura) {
481
+ if (waitUntil) waitUntil(lectura);
482
+ else void lectura;
483
+ }
484
+ return null;
485
+ }
430
486
  const body = entrada.method === "GET" || entrada.method === "HEAD" ? null : await request.arrayBuffer();
431
487
  return peaje.reenviar(destino, { ...entrada, body });
432
488
  };
@@ -440,11 +496,19 @@ function peajeHono(opciones) {
440
496
  ...opciones,
441
497
  secret: opciones.secret ?? c.env?.PEAJE_ORIGIN_SECRET
442
498
  });
443
- const res = await peaje(c.req.raw);
499
+ const res = await peaje(c.req.raw, esperaDe(c));
444
500
  if (res) return res;
445
501
  await next();
446
502
  };
447
503
  }
504
+ function esperaDe(c) {
505
+ try {
506
+ const ctx = c.executionCtx;
507
+ return ctx ? (promesa) => ctx.waitUntil(promesa) : void 0;
508
+ } catch {
509
+ return void 0;
510
+ }
511
+ }
448
512
  var hono_default = peajeHono;
449
513
  // Annotate the CommonJS export names for ESM import in node:
450
514
  0 && (module.exports = {
package/dist/hono.d.cts CHANGED
@@ -17,6 +17,9 @@ type ContextoMinimo = {
17
17
  raw: Request;
18
18
  };
19
19
  env?: unknown;
20
+ executionCtx?: {
21
+ waitUntil(promesa: Promise<unknown>): void;
22
+ };
20
23
  };
21
24
  type MiddlewareHono = (c: ContextoMinimo, next: () => Promise<void>) => Promise<Response | void>;
22
25
  declare function peajeHono(opciones: OpcionesPeaje): MiddlewareHono;
package/dist/hono.d.ts CHANGED
@@ -17,6 +17,9 @@ type ContextoMinimo = {
17
17
  raw: Request;
18
18
  };
19
19
  env?: unknown;
20
+ executionCtx?: {
21
+ waitUntil(promesa: Promise<unknown>): void;
22
+ };
20
23
  };
21
24
  type MiddlewareHono = (c: ContextoMinimo, next: () => Promise<void>) => Promise<Response | void>;
22
25
  declare function peajeHono(opciones: OpcionesPeaje): MiddlewareHono;
package/dist/hono.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  peajeFetch
3
- } from "./chunk-QHUY5N5S.js";
3
+ } from "./chunk-SWB3HJVY.js";
4
4
 
5
5
  // src/hono.ts
6
6
  function peajeHono(opciones) {
@@ -10,11 +10,19 @@ function peajeHono(opciones) {
10
10
  ...opciones,
11
11
  secret: opciones.secret ?? c.env?.PEAJE_ORIGIN_SECRET
12
12
  });
13
- const res = await peaje(c.req.raw);
13
+ const res = await peaje(c.req.raw, esperaDe(c));
14
14
  if (res) return res;
15
15
  await next();
16
16
  };
17
17
  }
18
+ function esperaDe(c) {
19
+ try {
20
+ const ctx = c.executionCtx;
21
+ return ctx ? (promesa) => ctx.waitUntil(promesa) : void 0;
22
+ } catch {
23
+ return void 0;
24
+ }
25
+ }
18
26
  var hono_default = peajeHono;
19
27
  export {
20
28
  hono_default as default,
package/dist/index.cjs CHANGED
@@ -86,6 +86,24 @@ var PATHS_RESERVADOS = [
86
86
  var PEAJE_FETCH_HEADER = "x-peaje-fetch";
87
87
  var PEAJE_HOST_HEADER = "x-peaje-forwarded-host";
88
88
 
89
+ // ../shared/src/kit/bots.ts
90
+ var BOTS_DE_VISITA = [
91
+ { patron: /OAI-SearchBot/i, tipo: "openai-searchbot" },
92
+ { patron: /PerplexityBot/i, tipo: "perplexitybot" },
93
+ { patron: /Googlebot/i, tipo: "googlebot" },
94
+ { patron: /ClaudeBot|anthropic-ai/i, tipo: "claudebot" },
95
+ { patron: /GPTBot/i, tipo: "gptbot" }
96
+ ];
97
+ function tipoDeBot(ua) {
98
+ if (!ua) return null;
99
+ for (const b of BOTS_DE_VISITA) if (b.patron.test(ua)) return b.tipo;
100
+ return null;
101
+ }
102
+ var PEAJE_VERIFICADOR_HEADER = "x-peaje-verificador";
103
+ var NO_ES_PAGINA = /\.(png|jpe?g|gif|webp|avif|svg|ico|css|m?js|map|woff2?|ttf|otf|eot|mp4|webm|mp3|wav)$/i;
104
+ var MAX_PATH_LECTURA = 500;
105
+ var MAX_UA_LECTURA = 300;
106
+
89
107
  // ../shared/src/kit/proxy-runtime.ts
90
108
  var PEAJE_ORIGIN_HEADER = "x-peaje-origin";
91
109
  function iguales(a, b) {
@@ -162,6 +180,17 @@ function patronReenviable(patron) {
162
180
  if (segs.length === 0) return false;
163
181
  return !(segs.length === 1 && (segs[0] === "*" || segs[0].startsWith(":")));
164
182
  }
183
+ function lecturaDeBot(input) {
184
+ const method = input.method.toUpperCase();
185
+ if (method !== "GET" && method !== "HEAD") return null;
186
+ const h = input.headers;
187
+ if (h.has(PEAJE_ORIGIN_HEADER) || h.has(PEAJE_FETCH_HEADER) || h.has(PEAJE_VERIFICADOR_HEADER)) return null;
188
+ const ua = h.get("user-agent");
189
+ if (!ua || !tipoDeBot(ua)) return null;
190
+ const path = input.pathname;
191
+ if (!path.startsWith("/") || path.startsWith("/_next/") || path.startsWith("/_vercel/") || NO_ES_PAGINA.test(path)) return null;
192
+ return { path: path.slice(0, MAX_PATH_LECTURA), method, userAgent: ua.slice(0, MAX_UA_LECTURA) };
193
+ }
165
194
  function crearProxyRuntime(opciones) {
166
195
  const base = opciones.base.replace(/\/+$/, "");
167
196
  const ttl = opciones.ttlMs ?? 6e4;
@@ -206,6 +235,22 @@ function crearProxyRuntime(opciones) {
206
235
  },
207
236
  /** Fuerza la carga del manifiesto (útil al arrancar o en tests). */
208
237
  cargar: () => refrescando ??= refrescar(),
238
+ /**
239
+ * Si la request es un bot leyendo una página del sitio, le avisa al
240
+ * gateway y devuelve la promesa del aviso (para `waitUntil`); si no, null.
241
+ * El aviso nunca lanza ni demora la página: 3 s de tope y se descarta la
242
+ * respuesta.
243
+ */
244
+ reportarLectura(input) {
245
+ const lectura = lecturaDeBot(input);
246
+ if (!lectura) return null;
247
+ return fetchImpl(`${base}/kit/visita`, {
248
+ method: "POST",
249
+ headers: { "content-type": "application/json" },
250
+ body: JSON.stringify(lectura),
251
+ signal: AbortSignal.timeout(3e3)
252
+ }).then((res) => res.body?.cancel()).catch(() => void 0);
253
+ },
209
254
  /**
210
255
  * Decide si la request va al gateway. `method` y `pathname` de la request,
211
256
  * `search` con el `?` incluido, y los headers para cortar bucles.
@@ -397,6 +442,7 @@ function crearPeaje(opciones) {
397
442
  const secreto = opciones.secret ?? env("PEAJE_ORIGIN_SECRET") ?? null;
398
443
  const fetchImpl = opciones.fetch ?? ((...a) => fetch(...a));
399
444
  const runtime = crearProxyRuntime({ base, secreto, fetchImpl, prefijosApi: opciones.apiPrefixes ?? PREFIJOS_API_POR_DEFECTO });
445
+ const reportar = opciones.reportBotVisits !== false;
400
446
  return {
401
447
  base,
402
448
  async decidir({ method, url, headers }) {
@@ -424,16 +470,26 @@ function crearPeaje(opciones) {
424
470
  vuelta.delete("transfer-encoding");
425
471
  vuelta.delete("connection");
426
472
  return new Response(method === "HEAD" ? null : res.body, { status: res.status, statusText: res.statusText, headers: vuelta });
473
+ },
474
+ reportarLectura({ method, url, headers }) {
475
+ return reportar ? runtime.reportarLectura({ method, pathname: url.pathname, headers }) : null;
427
476
  }
428
477
  };
429
478
  }
430
479
  function peajeFetch(opciones) {
431
480
  const peaje = crearPeaje(opciones);
432
- return async (request) => {
481
+ return async (request, waitUntil) => {
433
482
  const url = new URL(request.url);
434
483
  const entrada = { method: request.method.toUpperCase(), url, headers: request.headers };
435
484
  const destino = await peaje.decidir(entrada);
436
- if (!destino) return null;
485
+ if (!destino) {
486
+ const lectura = peaje.reportarLectura(entrada);
487
+ if (lectura) {
488
+ if (waitUntil) waitUntil(lectura);
489
+ else void lectura;
490
+ }
491
+ return null;
492
+ }
437
493
  const body = entrada.method === "GET" || entrada.method === "HEAD" ? null : await request.arrayBuffer();
438
494
  return peaje.reenviar(destino, { ...entrada, body });
439
495
  };
package/dist/index.d.cts CHANGED
@@ -17,6 +17,12 @@ type OpcionesPeaje = {
17
17
  * (`/login`, `/auth`, `/api/auth`, `/_next`...) nunca se aceptan.
18
18
  */
19
19
  apiPrefixes?: string[];
20
+ /**
21
+ * Avisarle al gateway cuando un bot de IA lee una página de la app, para el
22
+ * registro de visitas. Solo viajan la ruta (sin query) y el user-agent del
23
+ * bot. Por defecto `true`.
24
+ */
25
+ reportBotVisits?: boolean;
20
26
  /** Para tests o runtimes sin fetch global. */
21
27
  fetch?: typeof fetch;
22
28
  };
@@ -39,15 +45,23 @@ type Peaje = {
39
45
  reenviar(destino: string, entrada: EntradaDecision & {
40
46
  body: BodyInit | null;
41
47
  }): Promise<Response>;
48
+ /**
49
+ * Para una request que no fue al gateway: si es un bot de IA leyendo una
50
+ * página, se lo avisa al gateway y devuelve la promesa del aviso (para
51
+ * `waitUntil`). Null si no hay nada que avisar. Nunca lanza.
52
+ */
53
+ reportarLectura(entrada: EntradaDecision): Promise<void> | null;
42
54
  };
43
55
  declare function baseDe({ slug, gateway }: Pick<OpcionesPeaje, 'slug' | 'gateway'>): string;
44
56
  /** Solo para tests: vuelve a permitir el aviso de secreto faltante. */
45
57
  declare function _reiniciarAviso(): void;
46
58
  declare function crearPeaje(opciones: OpcionesPeaje): Peaje;
47
- type PeajeFetch = (request: Request) => Promise<Response | null>;
59
+ type PeajeFetch = (request: Request, waitUntil?: (promesa: Promise<unknown>) => void) => Promise<Response | null>;
48
60
  /**
49
61
  * `(request) => Response | null`. Null: la request no es de Peaje y la
50
- * atiende la app. El body se lee (en memoria) solo si va al gateway.
62
+ * atiende la app. El body se lee (en memoria) solo si va al gateway. Con
63
+ * `waitUntil` (Workers, Vercel), el aviso de lectura de un bot corre ahí;
64
+ * sin él, al costado.
51
65
  */
52
66
  declare function peajeFetch(opciones: OpcionesPeaje): PeajeFetch;
53
67
 
package/dist/index.d.ts CHANGED
@@ -17,6 +17,12 @@ type OpcionesPeaje = {
17
17
  * (`/login`, `/auth`, `/api/auth`, `/_next`...) nunca se aceptan.
18
18
  */
19
19
  apiPrefixes?: string[];
20
+ /**
21
+ * Avisarle al gateway cuando un bot de IA lee una página de la app, para el
22
+ * registro de visitas. Solo viajan la ruta (sin query) y el user-agent del
23
+ * bot. Por defecto `true`.
24
+ */
25
+ reportBotVisits?: boolean;
20
26
  /** Para tests o runtimes sin fetch global. */
21
27
  fetch?: typeof fetch;
22
28
  };
@@ -39,15 +45,23 @@ type Peaje = {
39
45
  reenviar(destino: string, entrada: EntradaDecision & {
40
46
  body: BodyInit | null;
41
47
  }): Promise<Response>;
48
+ /**
49
+ * Para una request que no fue al gateway: si es un bot de IA leyendo una
50
+ * página, se lo avisa al gateway y devuelve la promesa del aviso (para
51
+ * `waitUntil`). Null si no hay nada que avisar. Nunca lanza.
52
+ */
53
+ reportarLectura(entrada: EntradaDecision): Promise<void> | null;
42
54
  };
43
55
  declare function baseDe({ slug, gateway }: Pick<OpcionesPeaje, 'slug' | 'gateway'>): string;
44
56
  /** Solo para tests: vuelve a permitir el aviso de secreto faltante. */
45
57
  declare function _reiniciarAviso(): void;
46
58
  declare function crearPeaje(opciones: OpcionesPeaje): Peaje;
47
- type PeajeFetch = (request: Request) => Promise<Response | null>;
59
+ type PeajeFetch = (request: Request, waitUntil?: (promesa: Promise<unknown>) => void) => Promise<Response | null>;
48
60
  /**
49
61
  * `(request) => Response | null`. Null: la request no es de Peaje y la
50
- * atiende la app. El body se lee (en memoria) solo si va al gateway.
62
+ * atiende la app. El body se lee (en memoria) solo si va al gateway. Con
63
+ * `waitUntil` (Workers, Vercel), el aviso de lectura de un bot corre ahí;
64
+ * sin él, al costado.
51
65
  */
52
66
  declare function peajeFetch(opciones: OpcionesPeaje): PeajeFetch;
53
67
 
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  crearPeaje,
6
6
  peajeFetch,
7
7
  src_default
8
- } from "./chunk-QHUY5N5S.js";
8
+ } from "./chunk-SWB3HJVY.js";
9
9
  export {
10
10
  GATEWAY_POR_DEFECTO,
11
11
  _reiniciarAviso,
package/dist/vercel.cjs CHANGED
@@ -82,6 +82,24 @@ var PATHS_RESERVADOS = [
82
82
  var PEAJE_FETCH_HEADER = "x-peaje-fetch";
83
83
  var PEAJE_HOST_HEADER = "x-peaje-forwarded-host";
84
84
 
85
+ // ../shared/src/kit/bots.ts
86
+ var BOTS_DE_VISITA = [
87
+ { patron: /OAI-SearchBot/i, tipo: "openai-searchbot" },
88
+ { patron: /PerplexityBot/i, tipo: "perplexitybot" },
89
+ { patron: /Googlebot/i, tipo: "googlebot" },
90
+ { patron: /ClaudeBot|anthropic-ai/i, tipo: "claudebot" },
91
+ { patron: /GPTBot/i, tipo: "gptbot" }
92
+ ];
93
+ function tipoDeBot(ua) {
94
+ if (!ua) return null;
95
+ for (const b of BOTS_DE_VISITA) if (b.patron.test(ua)) return b.tipo;
96
+ return null;
97
+ }
98
+ var PEAJE_VERIFICADOR_HEADER = "x-peaje-verificador";
99
+ var NO_ES_PAGINA = /\.(png|jpe?g|gif|webp|avif|svg|ico|css|m?js|map|woff2?|ttf|otf|eot|mp4|webm|mp3|wav)$/i;
100
+ var MAX_PATH_LECTURA = 500;
101
+ var MAX_UA_LECTURA = 300;
102
+
85
103
  // ../shared/src/kit/proxy-runtime.ts
86
104
  var PEAJE_ORIGIN_HEADER = "x-peaje-origin";
87
105
  function iguales(a, b) {
@@ -158,6 +176,17 @@ function patronReenviable(patron) {
158
176
  if (segs.length === 0) return false;
159
177
  return !(segs.length === 1 && (segs[0] === "*" || segs[0].startsWith(":")));
160
178
  }
179
+ function lecturaDeBot(input) {
180
+ const method = input.method.toUpperCase();
181
+ if (method !== "GET" && method !== "HEAD") return null;
182
+ const h = input.headers;
183
+ if (h.has(PEAJE_ORIGIN_HEADER) || h.has(PEAJE_FETCH_HEADER) || h.has(PEAJE_VERIFICADOR_HEADER)) return null;
184
+ const ua = h.get("user-agent");
185
+ if (!ua || !tipoDeBot(ua)) return null;
186
+ const path = input.pathname;
187
+ if (!path.startsWith("/") || path.startsWith("/_next/") || path.startsWith("/_vercel/") || NO_ES_PAGINA.test(path)) return null;
188
+ return { path: path.slice(0, MAX_PATH_LECTURA), method, userAgent: ua.slice(0, MAX_UA_LECTURA) };
189
+ }
161
190
  function crearProxyRuntime(opciones) {
162
191
  const base = opciones.base.replace(/\/+$/, "");
163
192
  const ttl = opciones.ttlMs ?? 6e4;
@@ -202,6 +231,22 @@ function crearProxyRuntime(opciones) {
202
231
  },
203
232
  /** Fuerza la carga del manifiesto (útil al arrancar o en tests). */
204
233
  cargar: () => refrescando ??= refrescar(),
234
+ /**
235
+ * Si la request es un bot leyendo una página del sitio, le avisa al
236
+ * gateway y devuelve la promesa del aviso (para `waitUntil`); si no, null.
237
+ * El aviso nunca lanza ni demora la página: 3 s de tope y se descarta la
238
+ * respuesta.
239
+ */
240
+ reportarLectura(input) {
241
+ const lectura = lecturaDeBot(input);
242
+ if (!lectura) return null;
243
+ return fetchImpl(`${base}/kit/visita`, {
244
+ method: "POST",
245
+ headers: { "content-type": "application/json" },
246
+ body: JSON.stringify(lectura),
247
+ signal: AbortSignal.timeout(3e3)
248
+ }).then((res) => res.body?.cancel()).catch(() => void 0);
249
+ },
205
250
  /**
206
251
  * Decide si la request va al gateway. `method` y `pathname` de la request,
207
252
  * `search` con el `?` incluido, y los headers para cortar bucles.
@@ -390,6 +435,7 @@ function crearPeaje(opciones) {
390
435
  const secreto = opciones.secret ?? env("PEAJE_ORIGIN_SECRET") ?? null;
391
436
  const fetchImpl = opciones.fetch ?? ((...a) => fetch(...a));
392
437
  const runtime = crearProxyRuntime({ base, secreto, fetchImpl, prefijosApi: opciones.apiPrefixes ?? PREFIJOS_API_POR_DEFECTO });
438
+ const reportar = opciones.reportBotVisits !== false;
393
439
  return {
394
440
  base,
395
441
  async decidir({ method, url, headers }) {
@@ -417,6 +463,9 @@ function crearPeaje(opciones) {
417
463
  vuelta.delete("transfer-encoding");
418
464
  vuelta.delete("connection");
419
465
  return new Response(method === "HEAD" ? null : res.body, { status: res.status, statusText: res.statusText, headers: vuelta });
466
+ },
467
+ reportarLectura({ method, url, headers }) {
468
+ return reportar ? runtime.reportarLectura({ method, pathname: url.pathname, headers }) : null;
420
469
  }
421
470
  };
422
471
  }
@@ -448,6 +497,11 @@ function peajeVercel(opciones, siguiente) {
448
497
  headers.set("x-forwarded-proto", url.protocol.replace(":", ""));
449
498
  return respuestaMiddleware({ "x-middleware-rewrite": destino }, headers);
450
499
  }
500
+ const lectura = peaje.reportarLectura(entrada);
501
+ if (lectura) {
502
+ if (context?.waitUntil) context.waitUntil(lectura);
503
+ else void lectura;
504
+ }
451
505
  const propia = siguiente ? await siguiente(request, context) : void 0;
452
506
  return propia ?? respuestaMiddleware({ "x-middleware-next": "1" });
453
507
  };
package/dist/vercel.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  HEADERS_NO_REENVIADOS,
3
3
  PEAJE_HOST_HEADER,
4
4
  crearPeaje
5
- } from "./chunk-QHUY5N5S.js";
5
+ } from "./chunk-SWB3HJVY.js";
6
6
 
7
7
  // src/vercel.ts
8
8
  function respuestaMiddleware(control, requestHeaders) {
@@ -31,6 +31,11 @@ function peajeVercel(opciones, siguiente) {
31
31
  headers.set("x-forwarded-proto", url.protocol.replace(":", ""));
32
32
  return respuestaMiddleware({ "x-middleware-rewrite": destino }, headers);
33
33
  }
34
+ const lectura = peaje.reportarLectura(entrada);
35
+ if (lectura) {
36
+ if (context?.waitUntil) context.waitUntil(lectura);
37
+ else void lectura;
38
+ }
34
39
  const propia = siguiente ? await siguiente(request, context) : void 0;
35
40
  return propia ?? respuestaMiddleware({ "x-middleware-next": "1" });
36
41
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peaje/proxy",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Peaje for any server: answer HTTP 402 (MPP, x402) for your priced API routes on your own domain. Fetch handler plus Express, Hono and Vercel Routing Middleware adapters.",
5
5
  "license": "MIT",
6
6
  "repository": {