@peaje/proxy 0.1.2 → 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/LICENSE +21 -0
- package/README.md +16 -2
- package/dist/{chunk-IU5YMR4J.js → chunk-SWB3HJVY.js} +162 -34
- package/dist/express.cjs +156 -33
- package/dist/express.js +5 -2
- package/dist/hono.cjs +170 -35
- package/dist/hono.d.cts +3 -0
- package/dist/hono.d.ts +3 -0
- package/dist/hono.js +10 -2
- package/dist/index.cjs +161 -34
- package/dist/index.d.cts +23 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +1 -1
- package/dist/vercel.cjs +158 -32
- package/dist/vercel.js +8 -1
- package/package.json +5 -2
package/dist/vercel.cjs
CHANGED
|
@@ -52,9 +52,54 @@ var RUTAS_DINAMICAS = [
|
|
|
52
52
|
{ path: "/r/:slug", prefijo: "/r/", nota: "links con precio: el 402 en tu dominio" },
|
|
53
53
|
{ path: "/checkout_sessions/:id", prefijo: "/checkout_sessions/", nota: "leer una sesi\xF3n de checkout ACP" }
|
|
54
54
|
];
|
|
55
|
+
var PREFIJOS_API_POR_DEFECTO = ["/api/", "/v1/"];
|
|
56
|
+
var PATHS_RESERVADOS = [
|
|
57
|
+
"/",
|
|
58
|
+
"/login",
|
|
59
|
+
"/logout",
|
|
60
|
+
"/signin",
|
|
61
|
+
"/signout",
|
|
62
|
+
"/signup",
|
|
63
|
+
"/sign-in",
|
|
64
|
+
"/sign-out",
|
|
65
|
+
"/sign-up",
|
|
66
|
+
"/register",
|
|
67
|
+
"/auth",
|
|
68
|
+
"/oauth",
|
|
69
|
+
"/session",
|
|
70
|
+
"/account",
|
|
71
|
+
"/admin",
|
|
72
|
+
"/dashboard",
|
|
73
|
+
"/_next",
|
|
74
|
+
"/_vercel",
|
|
75
|
+
"/api/auth",
|
|
76
|
+
"/static",
|
|
77
|
+
"/assets",
|
|
78
|
+
"/favicon.ico",
|
|
79
|
+
"/robots.txt",
|
|
80
|
+
"/sitemap.xml"
|
|
81
|
+
];
|
|
55
82
|
var PEAJE_FETCH_HEADER = "x-peaje-fetch";
|
|
56
83
|
var PEAJE_HOST_HEADER = "x-peaje-forwarded-host";
|
|
57
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
|
+
|
|
58
103
|
// ../shared/src/kit/proxy-runtime.ts
|
|
59
104
|
var PEAJE_ORIGIN_HEADER = "x-peaje-origin";
|
|
60
105
|
function iguales(a, b) {
|
|
@@ -63,6 +108,38 @@ function iguales(a, b) {
|
|
|
63
108
|
for (let i = 0; i < a.length; i++) d |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
64
109
|
return d === 0;
|
|
65
110
|
}
|
|
111
|
+
var HEADERS_NO_REENVIADOS = ["cookie"];
|
|
112
|
+
function esReservado(path) {
|
|
113
|
+
const canon = pathCanonico(path).toLowerCase();
|
|
114
|
+
return PATHS_RESERVADOS.some((r) => r === "/" ? canon === "/" : canon === r || canon.startsWith(`${r}/`));
|
|
115
|
+
}
|
|
116
|
+
function prefijosApiValidos(prefijos = PREFIJOS_API_POR_DEFECTO) {
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const crudo of prefijos) {
|
|
119
|
+
const segs = segmentosCanonicos(String(crudo ?? "")).map((s) => s.toLowerCase());
|
|
120
|
+
if (segs.length === 0) continue;
|
|
121
|
+
const path = `/${segs.join("/")}`;
|
|
122
|
+
if (esReservado(path)) continue;
|
|
123
|
+
if (!out.includes(`${path}/`)) out.push(`${path}/`);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
function patronPermitido(patron, prefijos) {
|
|
128
|
+
if (!patronReenviable(patron)) return false;
|
|
129
|
+
const segs = patron.split("/").filter(Boolean);
|
|
130
|
+
const literal = (s) => s !== "*" && !s.startsWith(":");
|
|
131
|
+
const bajoPrefijo = prefijos.some((pre) => {
|
|
132
|
+
const ps = pre.split("/").filter(Boolean);
|
|
133
|
+
return segs.length > ps.length && ps.every((s, i) => literal(segs[i]) && segs[i].toLowerCase() === s);
|
|
134
|
+
});
|
|
135
|
+
if (!bajoPrefijo) return false;
|
|
136
|
+
const literales = [];
|
|
137
|
+
for (const s of segs) {
|
|
138
|
+
if (!literal(s)) break;
|
|
139
|
+
literales.push(s);
|
|
140
|
+
}
|
|
141
|
+
return !esReservado(`/${literales.join("/")}`);
|
|
142
|
+
}
|
|
66
143
|
function coincidePatron(patron, pathname) {
|
|
67
144
|
const p = patron.split("/").filter(Boolean);
|
|
68
145
|
const parts = segmentosCanonicos(pathname);
|
|
@@ -99,50 +176,77 @@ function patronReenviable(patron) {
|
|
|
99
176
|
if (segs.length === 0) return false;
|
|
100
177
|
return !(segs.length === 1 && (segs[0] === "*" || segs[0].startsWith(":")));
|
|
101
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
|
+
}
|
|
102
190
|
function crearProxyRuntime(opciones) {
|
|
103
191
|
const base = opciones.base.replace(/\/+$/, "");
|
|
104
192
|
const ttl = opciones.ttlMs ?? 6e4;
|
|
105
193
|
const fetchImpl = opciones.fetchImpl ?? fetch;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
patrones: opciones.respaldo?.patrones ?? []
|
|
111
|
-
};
|
|
194
|
+
const rutas = [...opciones.fijas?.rutas ?? RUTAS_PROXY.map((r) => r.path)];
|
|
195
|
+
const prefijos = [...opciones.fijas?.prefijos ?? RUTAS_DINAMICAS.map((r) => r.prefijo)];
|
|
196
|
+
const prefijosApi = prefijosApiValidos(opciones.prefijosApi ?? PREFIJOS_API_POR_DEFECTO);
|
|
197
|
+
let cache = { at: 0, patrones: [] };
|
|
112
198
|
let refrescando = null;
|
|
113
199
|
async function refrescar() {
|
|
114
200
|
try {
|
|
115
201
|
const res = await fetchImpl(`${base}/kit/manifest.json`, { signal: AbortSignal.timeout(5e3) });
|
|
116
202
|
if (!res.ok) throw new Error(String(res.status));
|
|
117
203
|
const m = await res.json();
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
rutas: Array.isArray(m.rutas) ? m.rutas : cache.rutas,
|
|
121
|
-
prefijos: Array.isArray(m.prefijos) ? m.prefijos : cache.prefijos,
|
|
122
|
-
patrones: Array.isArray(m.patrones) ? m.patrones : cache.patrones
|
|
123
|
-
};
|
|
204
|
+
const patrones2 = Array.isArray(m.patrones) ? m.patrones.filter((p) => typeof p?.method === "string" && typeof p?.path === "string") : cache.patrones;
|
|
205
|
+
cache = { at: Date.now(), patrones: patrones2 };
|
|
124
206
|
} catch {
|
|
125
207
|
cache = { ...cache, at: Date.now() };
|
|
126
208
|
} finally {
|
|
127
209
|
refrescando = null;
|
|
128
210
|
}
|
|
129
211
|
}
|
|
130
|
-
async function
|
|
212
|
+
async function patrones() {
|
|
131
213
|
const vencido = Date.now() - cache.at >= ttl;
|
|
132
214
|
if (vencido) {
|
|
133
215
|
refrescando ??= refrescar();
|
|
134
216
|
if (cache.at === 0) await refrescando;
|
|
135
217
|
}
|
|
136
|
-
return cache;
|
|
218
|
+
return cache.patrones;
|
|
137
219
|
}
|
|
138
220
|
return {
|
|
221
|
+
/** Los prefijos de API efectivos, normalizados (para los avisos). */
|
|
222
|
+
prefijosApi,
|
|
139
223
|
/** Rutas de API con precio que este sitio no puede cobrar por falta de secreto. */
|
|
140
224
|
async sinSecreto() {
|
|
141
225
|
if (opciones.secreto) return [];
|
|
142
|
-
return (await
|
|
226
|
+
return (await patrones()).filter((p) => patronPermitido(p.path, prefijosApi));
|
|
227
|
+
},
|
|
228
|
+
/** Rutas de API con precio que no se reenvían por caer fuera de `prefijosApi` o en un path reservado. */
|
|
229
|
+
async fueraDePrefijos() {
|
|
230
|
+
return (await patrones()).filter((p) => patronReenviable(p.path) && !patronPermitido(p.path, prefijosApi));
|
|
143
231
|
},
|
|
144
232
|
/** Fuerza la carga del manifiesto (útil al arrancar o en tests). */
|
|
145
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
|
+
},
|
|
146
250
|
/**
|
|
147
251
|
* Decide si la request va al gateway. `method` y `pathname` de la request,
|
|
148
252
|
* `search` con el `?` incluido, y los headers para cortar bucles.
|
|
@@ -151,14 +255,18 @@ function crearProxyRuntime(opciones) {
|
|
|
151
255
|
const secreto = opciones.secreto || null;
|
|
152
256
|
const prueba = input.headers.get(PEAJE_ORIGIN_HEADER);
|
|
153
257
|
if (secreto && prueba && iguales(prueba, secreto)) return null;
|
|
154
|
-
const { rutas, prefijos, patrones } = await listas();
|
|
155
258
|
const path = input.pathname;
|
|
156
259
|
const canon = pathCanonico(path).toLowerCase();
|
|
157
260
|
const esFetchDePeaje = input.headers.get(PEAJE_FETCH_HEADER) !== null;
|
|
158
261
|
const fija = (rutas.includes(path) || rutas.some((r) => r.toLowerCase() === canon)) && !(canon === "/llms.txt" && esFetchDePeaje);
|
|
159
262
|
const prefijo = prefijos.some((p) => path.startsWith(p) || canon.startsWith(p.toLowerCase()));
|
|
160
|
-
|
|
161
|
-
|
|
263
|
+
let patron = false;
|
|
264
|
+
if (!fija && !prefijo && secreto !== null && !esReservado(path) && prefijosApi.some((pre) => canon.startsWith(pre))) {
|
|
265
|
+
const method = input.method.toUpperCase();
|
|
266
|
+
patron = (await patrones()).some(
|
|
267
|
+
(p) => p.method.toUpperCase() === method && patronPermitido(p.path, prefijosApi) && coincidePatron(p.path, path)
|
|
268
|
+
);
|
|
269
|
+
}
|
|
162
270
|
if (!fija && !prefijo && !patron) return null;
|
|
163
271
|
return { destino: `${base}${path}${input.search ?? ""}` };
|
|
164
272
|
}
|
|
@@ -303,29 +411,37 @@ function baseDe({ slug, gateway }) {
|
|
|
303
411
|
}
|
|
304
412
|
var SALTO = ["host", "connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "te", "trailer", "content-length"];
|
|
305
413
|
var avisado = false;
|
|
414
|
+
function avisar(runtime, conSecreto) {
|
|
415
|
+
if (!conSecreto) {
|
|
416
|
+
void runtime.sinSecreto().then((faltan) => {
|
|
417
|
+
if (faltan.length > 0) {
|
|
418
|
+
console.warn(
|
|
419
|
+
`[peaje] ${faltan.length} priced API route(s) are not charged on this domain because PEAJE_ORIGIN_SECRET is not set: ${faltan.map((p) => `${p.method} ${p.path}`).join(", ")}. Copy it from the Kit page of your Peaje dashboard into your hosting environment variables.`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
void runtime.fueraDePrefijos().then((fuera) => {
|
|
425
|
+
if (fuera.length > 0) {
|
|
426
|
+
console.warn(
|
|
427
|
+
`[peaje] ${fuera.length} priced API route(s) are outside apiPrefixes (${runtime.prefijosApi.join(", ") || "none"}) or on a reserved path, so they are charged only at the gateway URL, not on this domain: ${fuera.map((p) => `${p.method} ${p.path}`).join(", ")}. A route under a prefix of its own (/weather/:city) is charged here once that prefix is in the apiPrefixes option; a single path at the root or a reserved path never is.`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
306
432
|
function crearPeaje(opciones) {
|
|
307
433
|
if (!opciones?.slug) throw new Error("@peaje/proxy: `slug` is required (the business slug from your Peaje dashboard).");
|
|
308
434
|
const base = baseDe(opciones);
|
|
309
435
|
const secreto = opciones.secret ?? env("PEAJE_ORIGIN_SECRET") ?? null;
|
|
310
436
|
const fetchImpl = opciones.fetch ?? ((...a) => fetch(...a));
|
|
311
|
-
const runtime = crearProxyRuntime({
|
|
312
|
-
|
|
313
|
-
secreto,
|
|
314
|
-
fetchImpl,
|
|
315
|
-
respaldo: { rutas: RUTAS_PROXY.map((x) => x.path), prefijos: RUTAS_DINAMICAS.map((x) => x.prefijo), patrones: [] }
|
|
316
|
-
});
|
|
437
|
+
const runtime = crearProxyRuntime({ base, secreto, fetchImpl, prefijosApi: opciones.apiPrefixes ?? PREFIJOS_API_POR_DEFECTO });
|
|
438
|
+
const reportar = opciones.reportBotVisits !== false;
|
|
317
439
|
return {
|
|
318
440
|
base,
|
|
319
441
|
async decidir({ method, url, headers }) {
|
|
320
|
-
if (!
|
|
442
|
+
if (!avisado) {
|
|
321
443
|
avisado = true;
|
|
322
|
-
|
|
323
|
-
if (faltan.length > 0) {
|
|
324
|
-
console.warn(
|
|
325
|
-
`[peaje] ${faltan.length} priced API route(s) are not charged on this domain because PEAJE_ORIGIN_SECRET is not set: ${faltan.map((p) => `${p.method} ${p.path}`).join(", ")}. Copy it from the Kit page of your Peaje dashboard into your hosting environment variables.`
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
});
|
|
444
|
+
avisar(runtime, Boolean(secreto));
|
|
329
445
|
}
|
|
330
446
|
const d = await runtime.decidir({ method, pathname: url.pathname, search: url.search, headers });
|
|
331
447
|
return d?.destino ?? null;
|
|
@@ -333,6 +449,7 @@ function crearPeaje(opciones) {
|
|
|
333
449
|
async reenviar(destino, { method, url, headers, body }) {
|
|
334
450
|
const salida = new Headers(headers);
|
|
335
451
|
for (const h of SALTO) salida.delete(h);
|
|
452
|
+
for (const h of HEADERS_NO_REENVIADOS) salida.delete(h);
|
|
336
453
|
const primero = (h) => headers.get(h)?.split(",")[0]?.trim() || null;
|
|
337
454
|
const host = primero("x-forwarded-host") ?? url.host;
|
|
338
455
|
salida.set("x-forwarded-host", host);
|
|
@@ -346,6 +463,9 @@ function crearPeaje(opciones) {
|
|
|
346
463
|
vuelta.delete("transfer-encoding");
|
|
347
464
|
vuelta.delete("connection");
|
|
348
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;
|
|
349
469
|
}
|
|
350
470
|
};
|
|
351
471
|
}
|
|
@@ -371,11 +491,17 @@ function peajeVercel(opciones, siguiente) {
|
|
|
371
491
|
const destino = await peaje.decidir(entrada);
|
|
372
492
|
if (destino) {
|
|
373
493
|
const headers = new Headers(request.headers);
|
|
494
|
+
for (const h of HEADERS_NO_REENVIADOS) headers.delete(h);
|
|
374
495
|
headers.set("x-forwarded-host", url.host);
|
|
375
496
|
headers.set(PEAJE_HOST_HEADER, url.host);
|
|
376
497
|
headers.set("x-forwarded-proto", url.protocol.replace(":", ""));
|
|
377
498
|
return respuestaMiddleware({ "x-middleware-rewrite": destino }, headers);
|
|
378
499
|
}
|
|
500
|
+
const lectura = peaje.reportarLectura(entrada);
|
|
501
|
+
if (lectura) {
|
|
502
|
+
if (context?.waitUntil) context.waitUntil(lectura);
|
|
503
|
+
else void lectura;
|
|
504
|
+
}
|
|
379
505
|
const propia = siguiente ? await siguiente(request, context) : void 0;
|
|
380
506
|
return propia ?? respuestaMiddleware({ "x-middleware-next": "1" });
|
|
381
507
|
};
|
package/dist/vercel.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
HEADERS_NO_REENVIADOS,
|
|
2
3
|
PEAJE_HOST_HEADER,
|
|
3
4
|
crearPeaje
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-SWB3HJVY.js";
|
|
5
6
|
|
|
6
7
|
// src/vercel.ts
|
|
7
8
|
function respuestaMiddleware(control, requestHeaders) {
|
|
@@ -24,11 +25,17 @@ function peajeVercel(opciones, siguiente) {
|
|
|
24
25
|
const destino = await peaje.decidir(entrada);
|
|
25
26
|
if (destino) {
|
|
26
27
|
const headers = new Headers(request.headers);
|
|
28
|
+
for (const h of HEADERS_NO_REENVIADOS) headers.delete(h);
|
|
27
29
|
headers.set("x-forwarded-host", url.host);
|
|
28
30
|
headers.set(PEAJE_HOST_HEADER, url.host);
|
|
29
31
|
headers.set("x-forwarded-proto", url.protocol.replace(":", ""));
|
|
30
32
|
return respuestaMiddleware({ "x-middleware-rewrite": destino }, headers);
|
|
31
33
|
}
|
|
34
|
+
const lectura = peaje.reportarLectura(entrada);
|
|
35
|
+
if (lectura) {
|
|
36
|
+
if (context?.waitUntil) context.waitUntil(lectura);
|
|
37
|
+
else void lectura;
|
|
38
|
+
}
|
|
32
39
|
const propia = siguiente ? await siguiente(request, context) : void 0;
|
|
33
40
|
return propia ?? respuestaMiddleware({ "x-middleware-next": "1" });
|
|
34
41
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peaje/proxy",
|
|
3
|
-
"version": "0.1
|
|
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": {
|
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
"url": "git+https://github.com/sandragcarrillo/peaje.git",
|
|
9
9
|
"directory": "packages/peaje-proxy"
|
|
10
10
|
},
|
|
11
|
+
"homepage": "https://github.com/sandragcarrillo/peaje/tree/main/packages/peaje-proxy#readme",
|
|
12
|
+
"bugs": "https://github.com/sandragcarrillo/peaje/issues",
|
|
11
13
|
"type": "module",
|
|
12
14
|
"sideEffects": false,
|
|
13
15
|
"files": [
|
|
14
|
-
"dist"
|
|
16
|
+
"dist",
|
|
17
|
+
"LICENSE"
|
|
15
18
|
],
|
|
16
19
|
"main": "./dist/index.cjs",
|
|
17
20
|
"module": "./dist/index.js",
|