@peaje/proxy 0.1.2 → 0.2.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/LICENSE +21 -0
- package/README.md +11 -2
- package/dist/{chunk-IU5YMR4J.js → chunk-QHUY5N5S.js} +104 -32
- package/dist/express.cjs +103 -32
- package/dist/express.js +1 -1
- package/dist/hono.cjs +103 -32
- package/dist/hono.js +1 -1
- package/dist/index.cjs +103 -32
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1 -1
- package/dist/vercel.cjs +104 -32
- package/dist/vercel.js +3 -1
- package/package.json +5 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sandra Carrillo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -90,9 +90,17 @@ export default { fetch: async (req: Request) => (await peaje(req)) ?? app(req) }
|
|
|
90
90
|
|
|
91
91
|
- The catalog paths: `/llms.txt`, `/openapi.json`, `/mcp`, `/.well-known/*` Peaje files, `/developers`, `/docs` and the rest of the kit list.
|
|
92
92
|
- `/r/*` priced links and `/checkout_sessions/*`.
|
|
93
|
-
- Your priced API routes from the dashboard (`GET /api/forecast`, `/api/history/:city`, `/api/batch/*`), matching the method. Catch-alls at the root (`/*`, `/:id`) are never forwarded from your domain: they would send the whole site to the gateway.
|
|
93
|
+
- Your priced API routes from the dashboard (`GET /api/forecast`, `/api/history/:city`, `/api/batch/*`), matching the method, but only under `apiPrefixes` (default `['/api/', '/v1/']`). Catch-alls at the root (`/*`, `/:id`) are never forwarded from your domain: they would send the whole site to the gateway.
|
|
94
94
|
|
|
95
|
-
The forwarded request keeps method, body and
|
|
95
|
+
The forwarded request keeps method, body, query and `authorization` (the MPP credential), and carries `x-forwarded-host` and `x-forwarded-proto` so the 402 challenge names your domain (strict x402 clients abort when it does not). It leaves out the `cookie` header: your visitors' session cookies never leave your domain, and a priced route's handler no longer receives cookies through Peaje. If the gateway is unreachable, the catalog paths are still forwarded; priced API routes resume when the manifest loads.
|
|
96
|
+
|
|
97
|
+
### What the manifest cannot do (since 0.2.0)
|
|
98
|
+
|
|
99
|
+
- Add fixed paths or prefixes. Those come only from the list bundled in this package; anything extra in the manifest is ignored.
|
|
100
|
+
- Forward an API route outside `apiPrefixes`. A route priced elsewhere (`GET /weather/:city`) keeps charging at the gateway URL and you get one warning in the logs naming it. To charge it on your domain: `{ slug, apiPrefixes: ['/api/', '/v1/', '/weather/'] }`.
|
|
101
|
+
- Touch the root `/` or reserved app paths, whatever `apiPrefixes` says: `/login`, `/logout`, `/signin`, `/signup`, `/register`, `/auth`, `/oauth`, `/session`, `/account`, `/admin`, `/dashboard`, `/api/auth`, `/_next`, `/_vercel`, `/static`, `/assets`, `/favicon.ico`, `/robots.txt`, `/sitemap.xml` and everything under them.
|
|
102
|
+
|
|
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.
|
|
96
104
|
|
|
97
105
|
## Options
|
|
98
106
|
|
|
@@ -101,6 +109,7 @@ The forwarded request keeps method, body and query, and carries `x-forwarded-hos
|
|
|
101
109
|
| `slug` | required, the business slug from your dashboard |
|
|
102
110
|
| `secret` | `process.env.PEAJE_ORIGIN_SECRET` |
|
|
103
111
|
| `gateway` | `process.env.PEAJE_GATEWAY_URL` or `https://api.usepeaje.com` |
|
|
112
|
+
| `apiPrefixes` | `['/api/', '/v1/']`: where your priced API routes live; the root and reserved paths are never accepted |
|
|
104
113
|
|
|
105
114
|
## License
|
|
106
115
|
|
|
@@ -25,6 +25,33 @@ var RUTAS_DINAMICAS = [
|
|
|
25
25
|
{ path: "/r/:slug", prefijo: "/r/", nota: "links con precio: el 402 en tu dominio" },
|
|
26
26
|
{ path: "/checkout_sessions/:id", prefijo: "/checkout_sessions/", nota: "leer una sesi\xF3n de checkout ACP" }
|
|
27
27
|
];
|
|
28
|
+
var PREFIJOS_API_POR_DEFECTO = ["/api/", "/v1/"];
|
|
29
|
+
var PATHS_RESERVADOS = [
|
|
30
|
+
"/",
|
|
31
|
+
"/login",
|
|
32
|
+
"/logout",
|
|
33
|
+
"/signin",
|
|
34
|
+
"/signout",
|
|
35
|
+
"/signup",
|
|
36
|
+
"/sign-in",
|
|
37
|
+
"/sign-out",
|
|
38
|
+
"/sign-up",
|
|
39
|
+
"/register",
|
|
40
|
+
"/auth",
|
|
41
|
+
"/oauth",
|
|
42
|
+
"/session",
|
|
43
|
+
"/account",
|
|
44
|
+
"/admin",
|
|
45
|
+
"/dashboard",
|
|
46
|
+
"/_next",
|
|
47
|
+
"/_vercel",
|
|
48
|
+
"/api/auth",
|
|
49
|
+
"/static",
|
|
50
|
+
"/assets",
|
|
51
|
+
"/favicon.ico",
|
|
52
|
+
"/robots.txt",
|
|
53
|
+
"/sitemap.xml"
|
|
54
|
+
];
|
|
28
55
|
var PEAJE_FETCH_HEADER = "x-peaje-fetch";
|
|
29
56
|
var PEAJE_HOST_HEADER = "x-peaje-forwarded-host";
|
|
30
57
|
|
|
@@ -36,6 +63,38 @@ function iguales(a, b) {
|
|
|
36
63
|
for (let i = 0; i < a.length; i++) d |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
37
64
|
return d === 0;
|
|
38
65
|
}
|
|
66
|
+
var HEADERS_NO_REENVIADOS = ["cookie"];
|
|
67
|
+
function esReservado(path) {
|
|
68
|
+
const canon = pathCanonico(path).toLowerCase();
|
|
69
|
+
return PATHS_RESERVADOS.some((r) => r === "/" ? canon === "/" : canon === r || canon.startsWith(`${r}/`));
|
|
70
|
+
}
|
|
71
|
+
function prefijosApiValidos(prefijos = PREFIJOS_API_POR_DEFECTO) {
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const crudo of prefijos) {
|
|
74
|
+
const segs = segmentosCanonicos(String(crudo ?? "")).map((s) => s.toLowerCase());
|
|
75
|
+
if (segs.length === 0) continue;
|
|
76
|
+
const path = `/${segs.join("/")}`;
|
|
77
|
+
if (esReservado(path)) continue;
|
|
78
|
+
if (!out.includes(`${path}/`)) out.push(`${path}/`);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
function patronPermitido(patron, prefijos) {
|
|
83
|
+
if (!patronReenviable(patron)) return false;
|
|
84
|
+
const segs = patron.split("/").filter(Boolean);
|
|
85
|
+
const literal = (s) => s !== "*" && !s.startsWith(":");
|
|
86
|
+
const bajoPrefijo = prefijos.some((pre) => {
|
|
87
|
+
const ps = pre.split("/").filter(Boolean);
|
|
88
|
+
return segs.length > ps.length && ps.every((s, i) => literal(segs[i]) && segs[i].toLowerCase() === s);
|
|
89
|
+
});
|
|
90
|
+
if (!bajoPrefijo) return false;
|
|
91
|
+
const literales = [];
|
|
92
|
+
for (const s of segs) {
|
|
93
|
+
if (!literal(s)) break;
|
|
94
|
+
literales.push(s);
|
|
95
|
+
}
|
|
96
|
+
return !esReservado(`/${literales.join("/")}`);
|
|
97
|
+
}
|
|
39
98
|
function coincidePatron(patron, pathname) {
|
|
40
99
|
const p = patron.split("/").filter(Boolean);
|
|
41
100
|
const parts = segmentosCanonicos(pathname);
|
|
@@ -76,43 +135,43 @@ function crearProxyRuntime(opciones) {
|
|
|
76
135
|
const base = opciones.base.replace(/\/+$/, "");
|
|
77
136
|
const ttl = opciones.ttlMs ?? 6e4;
|
|
78
137
|
const fetchImpl = opciones.fetchImpl ?? fetch;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
patrones: opciones.respaldo?.patrones ?? []
|
|
84
|
-
};
|
|
138
|
+
const rutas = [...opciones.fijas?.rutas ?? RUTAS_PROXY.map((r) => r.path)];
|
|
139
|
+
const prefijos = [...opciones.fijas?.prefijos ?? RUTAS_DINAMICAS.map((r) => r.prefijo)];
|
|
140
|
+
const prefijosApi = prefijosApiValidos(opciones.prefijosApi ?? PREFIJOS_API_POR_DEFECTO);
|
|
141
|
+
let cache = { at: 0, patrones: [] };
|
|
85
142
|
let refrescando = null;
|
|
86
143
|
async function refrescar() {
|
|
87
144
|
try {
|
|
88
145
|
const res = await fetchImpl(`${base}/kit/manifest.json`, { signal: AbortSignal.timeout(5e3) });
|
|
89
146
|
if (!res.ok) throw new Error(String(res.status));
|
|
90
147
|
const m = await res.json();
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
rutas: Array.isArray(m.rutas) ? m.rutas : cache.rutas,
|
|
94
|
-
prefijos: Array.isArray(m.prefijos) ? m.prefijos : cache.prefijos,
|
|
95
|
-
patrones: Array.isArray(m.patrones) ? m.patrones : cache.patrones
|
|
96
|
-
};
|
|
148
|
+
const patrones2 = Array.isArray(m.patrones) ? m.patrones.filter((p) => typeof p?.method === "string" && typeof p?.path === "string") : cache.patrones;
|
|
149
|
+
cache = { at: Date.now(), patrones: patrones2 };
|
|
97
150
|
} catch {
|
|
98
151
|
cache = { ...cache, at: Date.now() };
|
|
99
152
|
} finally {
|
|
100
153
|
refrescando = null;
|
|
101
154
|
}
|
|
102
155
|
}
|
|
103
|
-
async function
|
|
156
|
+
async function patrones() {
|
|
104
157
|
const vencido = Date.now() - cache.at >= ttl;
|
|
105
158
|
if (vencido) {
|
|
106
159
|
refrescando ??= refrescar();
|
|
107
160
|
if (cache.at === 0) await refrescando;
|
|
108
161
|
}
|
|
109
|
-
return cache;
|
|
162
|
+
return cache.patrones;
|
|
110
163
|
}
|
|
111
164
|
return {
|
|
165
|
+
/** Los prefijos de API efectivos, normalizados (para los avisos). */
|
|
166
|
+
prefijosApi,
|
|
112
167
|
/** Rutas de API con precio que este sitio no puede cobrar por falta de secreto. */
|
|
113
168
|
async sinSecreto() {
|
|
114
169
|
if (opciones.secreto) return [];
|
|
115
|
-
return (await
|
|
170
|
+
return (await patrones()).filter((p) => patronPermitido(p.path, prefijosApi));
|
|
171
|
+
},
|
|
172
|
+
/** Rutas de API con precio que no se reenvían por caer fuera de `prefijosApi` o en un path reservado. */
|
|
173
|
+
async fueraDePrefijos() {
|
|
174
|
+
return (await patrones()).filter((p) => patronReenviable(p.path) && !patronPermitido(p.path, prefijosApi));
|
|
116
175
|
},
|
|
117
176
|
/** Fuerza la carga del manifiesto (útil al arrancar o en tests). */
|
|
118
177
|
cargar: () => refrescando ??= refrescar(),
|
|
@@ -124,14 +183,18 @@ function crearProxyRuntime(opciones) {
|
|
|
124
183
|
const secreto = opciones.secreto || null;
|
|
125
184
|
const prueba = input.headers.get(PEAJE_ORIGIN_HEADER);
|
|
126
185
|
if (secreto && prueba && iguales(prueba, secreto)) return null;
|
|
127
|
-
const { rutas, prefijos, patrones } = await listas();
|
|
128
186
|
const path = input.pathname;
|
|
129
187
|
const canon = pathCanonico(path).toLowerCase();
|
|
130
188
|
const esFetchDePeaje = input.headers.get(PEAJE_FETCH_HEADER) !== null;
|
|
131
189
|
const fija = (rutas.includes(path) || rutas.some((r) => r.toLowerCase() === canon)) && !(canon === "/llms.txt" && esFetchDePeaje);
|
|
132
190
|
const prefijo = prefijos.some((p) => path.startsWith(p) || canon.startsWith(p.toLowerCase()));
|
|
133
|
-
|
|
134
|
-
|
|
191
|
+
let patron = false;
|
|
192
|
+
if (!fija && !prefijo && secreto !== null && !esReservado(path) && prefijosApi.some((pre) => canon.startsWith(pre))) {
|
|
193
|
+
const method = input.method.toUpperCase();
|
|
194
|
+
patron = (await patrones()).some(
|
|
195
|
+
(p) => p.method.toUpperCase() === method && patronPermitido(p.path, prefijosApi) && coincidePatron(p.path, path)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
135
198
|
if (!fija && !prefijo && !patron) return null;
|
|
136
199
|
return { destino: `${base}${path}${input.search ?? ""}` };
|
|
137
200
|
}
|
|
@@ -276,6 +339,24 @@ function baseDe({ slug, gateway }) {
|
|
|
276
339
|
}
|
|
277
340
|
var SALTO = ["host", "connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "te", "trailer", "content-length"];
|
|
278
341
|
var avisado = false;
|
|
342
|
+
function avisar(runtime, conSecreto) {
|
|
343
|
+
if (!conSecreto) {
|
|
344
|
+
void runtime.sinSecreto().then((faltan) => {
|
|
345
|
+
if (faltan.length > 0) {
|
|
346
|
+
console.warn(
|
|
347
|
+
`[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.`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
void runtime.fueraDePrefijos().then((fuera) => {
|
|
353
|
+
if (fuera.length > 0) {
|
|
354
|
+
console.warn(
|
|
355
|
+
`[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.`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
279
360
|
function _reiniciarAviso() {
|
|
280
361
|
avisado = false;
|
|
281
362
|
}
|
|
@@ -284,24 +365,13 @@ function crearPeaje(opciones) {
|
|
|
284
365
|
const base = baseDe(opciones);
|
|
285
366
|
const secreto = opciones.secret ?? env("PEAJE_ORIGIN_SECRET") ?? null;
|
|
286
367
|
const fetchImpl = opciones.fetch ?? ((...a) => fetch(...a));
|
|
287
|
-
const runtime = crearProxyRuntime({
|
|
288
|
-
base,
|
|
289
|
-
secreto,
|
|
290
|
-
fetchImpl,
|
|
291
|
-
respaldo: { rutas: RUTAS_PROXY.map((x) => x.path), prefijos: RUTAS_DINAMICAS.map((x) => x.prefijo), patrones: [] }
|
|
292
|
-
});
|
|
368
|
+
const runtime = crearProxyRuntime({ base, secreto, fetchImpl, prefijosApi: opciones.apiPrefixes ?? PREFIJOS_API_POR_DEFECTO });
|
|
293
369
|
return {
|
|
294
370
|
base,
|
|
295
371
|
async decidir({ method, url, headers }) {
|
|
296
|
-
if (!
|
|
372
|
+
if (!avisado) {
|
|
297
373
|
avisado = true;
|
|
298
|
-
|
|
299
|
-
if (faltan.length > 0) {
|
|
300
|
-
console.warn(
|
|
301
|
-
`[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.`
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
});
|
|
374
|
+
avisar(runtime, Boolean(secreto));
|
|
305
375
|
}
|
|
306
376
|
const d = await runtime.decidir({ method, pathname: url.pathname, search: url.search, headers });
|
|
307
377
|
return d?.destino ?? null;
|
|
@@ -309,6 +379,7 @@ function crearPeaje(opciones) {
|
|
|
309
379
|
async reenviar(destino, { method, url, headers, body }) {
|
|
310
380
|
const salida = new Headers(headers);
|
|
311
381
|
for (const h of SALTO) salida.delete(h);
|
|
382
|
+
for (const h of HEADERS_NO_REENVIADOS) salida.delete(h);
|
|
312
383
|
const primero = (h) => headers.get(h)?.split(",")[0]?.trim() || null;
|
|
313
384
|
const host = primero("x-forwarded-host") ?? url.host;
|
|
314
385
|
salida.set("x-forwarded-host", host);
|
|
@@ -340,6 +411,7 @@ var src_default = peajeFetch;
|
|
|
340
411
|
|
|
341
412
|
export {
|
|
342
413
|
PEAJE_HOST_HEADER,
|
|
414
|
+
HEADERS_NO_REENVIADOS,
|
|
343
415
|
GATEWAY_POR_DEFECTO,
|
|
344
416
|
baseDe,
|
|
345
417
|
_reiniciarAviso,
|
package/dist/express.cjs
CHANGED
|
@@ -53,6 +53,33 @@ var RUTAS_DINAMICAS = [
|
|
|
53
53
|
{ path: "/r/:slug", prefijo: "/r/", nota: "links con precio: el 402 en tu dominio" },
|
|
54
54
|
{ path: "/checkout_sessions/:id", prefijo: "/checkout_sessions/", nota: "leer una sesi\xF3n de checkout ACP" }
|
|
55
55
|
];
|
|
56
|
+
var PREFIJOS_API_POR_DEFECTO = ["/api/", "/v1/"];
|
|
57
|
+
var PATHS_RESERVADOS = [
|
|
58
|
+
"/",
|
|
59
|
+
"/login",
|
|
60
|
+
"/logout",
|
|
61
|
+
"/signin",
|
|
62
|
+
"/signout",
|
|
63
|
+
"/signup",
|
|
64
|
+
"/sign-in",
|
|
65
|
+
"/sign-out",
|
|
66
|
+
"/sign-up",
|
|
67
|
+
"/register",
|
|
68
|
+
"/auth",
|
|
69
|
+
"/oauth",
|
|
70
|
+
"/session",
|
|
71
|
+
"/account",
|
|
72
|
+
"/admin",
|
|
73
|
+
"/dashboard",
|
|
74
|
+
"/_next",
|
|
75
|
+
"/_vercel",
|
|
76
|
+
"/api/auth",
|
|
77
|
+
"/static",
|
|
78
|
+
"/assets",
|
|
79
|
+
"/favicon.ico",
|
|
80
|
+
"/robots.txt",
|
|
81
|
+
"/sitemap.xml"
|
|
82
|
+
];
|
|
56
83
|
var PEAJE_FETCH_HEADER = "x-peaje-fetch";
|
|
57
84
|
var PEAJE_HOST_HEADER = "x-peaje-forwarded-host";
|
|
58
85
|
|
|
@@ -64,6 +91,38 @@ function iguales(a, b) {
|
|
|
64
91
|
for (let i = 0; i < a.length; i++) d |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
65
92
|
return d === 0;
|
|
66
93
|
}
|
|
94
|
+
var HEADERS_NO_REENVIADOS = ["cookie"];
|
|
95
|
+
function esReservado(path) {
|
|
96
|
+
const canon = pathCanonico(path).toLowerCase();
|
|
97
|
+
return PATHS_RESERVADOS.some((r) => r === "/" ? canon === "/" : canon === r || canon.startsWith(`${r}/`));
|
|
98
|
+
}
|
|
99
|
+
function prefijosApiValidos(prefijos = PREFIJOS_API_POR_DEFECTO) {
|
|
100
|
+
const out = [];
|
|
101
|
+
for (const crudo of prefijos) {
|
|
102
|
+
const segs = segmentosCanonicos(String(crudo ?? "")).map((s) => s.toLowerCase());
|
|
103
|
+
if (segs.length === 0) continue;
|
|
104
|
+
const path = `/${segs.join("/")}`;
|
|
105
|
+
if (esReservado(path)) continue;
|
|
106
|
+
if (!out.includes(`${path}/`)) out.push(`${path}/`);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
function patronPermitido(patron, prefijos) {
|
|
111
|
+
if (!patronReenviable(patron)) return false;
|
|
112
|
+
const segs = patron.split("/").filter(Boolean);
|
|
113
|
+
const literal = (s) => s !== "*" && !s.startsWith(":");
|
|
114
|
+
const bajoPrefijo = prefijos.some((pre) => {
|
|
115
|
+
const ps = pre.split("/").filter(Boolean);
|
|
116
|
+
return segs.length > ps.length && ps.every((s, i) => literal(segs[i]) && segs[i].toLowerCase() === s);
|
|
117
|
+
});
|
|
118
|
+
if (!bajoPrefijo) return false;
|
|
119
|
+
const literales = [];
|
|
120
|
+
for (const s of segs) {
|
|
121
|
+
if (!literal(s)) break;
|
|
122
|
+
literales.push(s);
|
|
123
|
+
}
|
|
124
|
+
return !esReservado(`/${literales.join("/")}`);
|
|
125
|
+
}
|
|
67
126
|
function coincidePatron(patron, pathname) {
|
|
68
127
|
const p = patron.split("/").filter(Boolean);
|
|
69
128
|
const parts = segmentosCanonicos(pathname);
|
|
@@ -104,43 +163,43 @@ function crearProxyRuntime(opciones) {
|
|
|
104
163
|
const base = opciones.base.replace(/\/+$/, "");
|
|
105
164
|
const ttl = opciones.ttlMs ?? 6e4;
|
|
106
165
|
const fetchImpl = opciones.fetchImpl ?? fetch;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
patrones: opciones.respaldo?.patrones ?? []
|
|
112
|
-
};
|
|
166
|
+
const rutas = [...opciones.fijas?.rutas ?? RUTAS_PROXY.map((r) => r.path)];
|
|
167
|
+
const prefijos = [...opciones.fijas?.prefijos ?? RUTAS_DINAMICAS.map((r) => r.prefijo)];
|
|
168
|
+
const prefijosApi = prefijosApiValidos(opciones.prefijosApi ?? PREFIJOS_API_POR_DEFECTO);
|
|
169
|
+
let cache = { at: 0, patrones: [] };
|
|
113
170
|
let refrescando = null;
|
|
114
171
|
async function refrescar() {
|
|
115
172
|
try {
|
|
116
173
|
const res = await fetchImpl(`${base}/kit/manifest.json`, { signal: AbortSignal.timeout(5e3) });
|
|
117
174
|
if (!res.ok) throw new Error(String(res.status));
|
|
118
175
|
const m = await res.json();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
rutas: Array.isArray(m.rutas) ? m.rutas : cache.rutas,
|
|
122
|
-
prefijos: Array.isArray(m.prefijos) ? m.prefijos : cache.prefijos,
|
|
123
|
-
patrones: Array.isArray(m.patrones) ? m.patrones : cache.patrones
|
|
124
|
-
};
|
|
176
|
+
const patrones2 = Array.isArray(m.patrones) ? m.patrones.filter((p) => typeof p?.method === "string" && typeof p?.path === "string") : cache.patrones;
|
|
177
|
+
cache = { at: Date.now(), patrones: patrones2 };
|
|
125
178
|
} catch {
|
|
126
179
|
cache = { ...cache, at: Date.now() };
|
|
127
180
|
} finally {
|
|
128
181
|
refrescando = null;
|
|
129
182
|
}
|
|
130
183
|
}
|
|
131
|
-
async function
|
|
184
|
+
async function patrones() {
|
|
132
185
|
const vencido = Date.now() - cache.at >= ttl;
|
|
133
186
|
if (vencido) {
|
|
134
187
|
refrescando ??= refrescar();
|
|
135
188
|
if (cache.at === 0) await refrescando;
|
|
136
189
|
}
|
|
137
|
-
return cache;
|
|
190
|
+
return cache.patrones;
|
|
138
191
|
}
|
|
139
192
|
return {
|
|
193
|
+
/** Los prefijos de API efectivos, normalizados (para los avisos). */
|
|
194
|
+
prefijosApi,
|
|
140
195
|
/** Rutas de API con precio que este sitio no puede cobrar por falta de secreto. */
|
|
141
196
|
async sinSecreto() {
|
|
142
197
|
if (opciones.secreto) return [];
|
|
143
|
-
return (await
|
|
198
|
+
return (await patrones()).filter((p) => patronPermitido(p.path, prefijosApi));
|
|
199
|
+
},
|
|
200
|
+
/** Rutas de API con precio que no se reenvían por caer fuera de `prefijosApi` o en un path reservado. */
|
|
201
|
+
async fueraDePrefijos() {
|
|
202
|
+
return (await patrones()).filter((p) => patronReenviable(p.path) && !patronPermitido(p.path, prefijosApi));
|
|
144
203
|
},
|
|
145
204
|
/** Fuerza la carga del manifiesto (útil al arrancar o en tests). */
|
|
146
205
|
cargar: () => refrescando ??= refrescar(),
|
|
@@ -152,14 +211,18 @@ function crearProxyRuntime(opciones) {
|
|
|
152
211
|
const secreto = opciones.secreto || null;
|
|
153
212
|
const prueba = input.headers.get(PEAJE_ORIGIN_HEADER);
|
|
154
213
|
if (secreto && prueba && iguales(prueba, secreto)) return null;
|
|
155
|
-
const { rutas, prefijos, patrones } = await listas();
|
|
156
214
|
const path = input.pathname;
|
|
157
215
|
const canon = pathCanonico(path).toLowerCase();
|
|
158
216
|
const esFetchDePeaje = input.headers.get(PEAJE_FETCH_HEADER) !== null;
|
|
159
217
|
const fija = (rutas.includes(path) || rutas.some((r) => r.toLowerCase() === canon)) && !(canon === "/llms.txt" && esFetchDePeaje);
|
|
160
218
|
const prefijo = prefijos.some((p) => path.startsWith(p) || canon.startsWith(p.toLowerCase()));
|
|
161
|
-
|
|
162
|
-
|
|
219
|
+
let patron = false;
|
|
220
|
+
if (!fija && !prefijo && secreto !== null && !esReservado(path) && prefijosApi.some((pre) => canon.startsWith(pre))) {
|
|
221
|
+
const method = input.method.toUpperCase();
|
|
222
|
+
patron = (await patrones()).some(
|
|
223
|
+
(p) => p.method.toUpperCase() === method && patronPermitido(p.path, prefijosApi) && coincidePatron(p.path, path)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
163
226
|
if (!fija && !prefijo && !patron) return null;
|
|
164
227
|
return { destino: `${base}${path}${input.search ?? ""}` };
|
|
165
228
|
}
|
|
@@ -304,29 +367,36 @@ function baseDe({ slug, gateway }) {
|
|
|
304
367
|
}
|
|
305
368
|
var SALTO = ["host", "connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "te", "trailer", "content-length"];
|
|
306
369
|
var avisado = false;
|
|
370
|
+
function avisar(runtime, conSecreto) {
|
|
371
|
+
if (!conSecreto) {
|
|
372
|
+
void runtime.sinSecreto().then((faltan) => {
|
|
373
|
+
if (faltan.length > 0) {
|
|
374
|
+
console.warn(
|
|
375
|
+
`[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.`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
void runtime.fueraDePrefijos().then((fuera) => {
|
|
381
|
+
if (fuera.length > 0) {
|
|
382
|
+
console.warn(
|
|
383
|
+
`[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.`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
307
388
|
function crearPeaje(opciones) {
|
|
308
389
|
if (!opciones?.slug) throw new Error("@peaje/proxy: `slug` is required (the business slug from your Peaje dashboard).");
|
|
309
390
|
const base = baseDe(opciones);
|
|
310
391
|
const secreto = opciones.secret ?? env("PEAJE_ORIGIN_SECRET") ?? null;
|
|
311
392
|
const fetchImpl = opciones.fetch ?? ((...a) => fetch(...a));
|
|
312
|
-
const runtime = crearProxyRuntime({
|
|
313
|
-
base,
|
|
314
|
-
secreto,
|
|
315
|
-
fetchImpl,
|
|
316
|
-
respaldo: { rutas: RUTAS_PROXY.map((x) => x.path), prefijos: RUTAS_DINAMICAS.map((x) => x.prefijo), patrones: [] }
|
|
317
|
-
});
|
|
393
|
+
const runtime = crearProxyRuntime({ base, secreto, fetchImpl, prefijosApi: opciones.apiPrefixes ?? PREFIJOS_API_POR_DEFECTO });
|
|
318
394
|
return {
|
|
319
395
|
base,
|
|
320
396
|
async decidir({ method, url, headers }) {
|
|
321
|
-
if (!
|
|
397
|
+
if (!avisado) {
|
|
322
398
|
avisado = true;
|
|
323
|
-
|
|
324
|
-
if (faltan.length > 0) {
|
|
325
|
-
console.warn(
|
|
326
|
-
`[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.`
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
});
|
|
399
|
+
avisar(runtime, Boolean(secreto));
|
|
330
400
|
}
|
|
331
401
|
const d = await runtime.decidir({ method, pathname: url.pathname, search: url.search, headers });
|
|
332
402
|
return d?.destino ?? null;
|
|
@@ -334,6 +404,7 @@ function crearPeaje(opciones) {
|
|
|
334
404
|
async reenviar(destino, { method, url, headers, body }) {
|
|
335
405
|
const salida = new Headers(headers);
|
|
336
406
|
for (const h of SALTO) salida.delete(h);
|
|
407
|
+
for (const h of HEADERS_NO_REENVIADOS) salida.delete(h);
|
|
337
408
|
const primero = (h) => headers.get(h)?.split(",")[0]?.trim() || null;
|
|
338
409
|
const host = primero("x-forwarded-host") ?? url.host;
|
|
339
410
|
salida.set("x-forwarded-host", host);
|
package/dist/express.js
CHANGED
package/dist/hono.cjs
CHANGED
|
@@ -52,6 +52,33 @@ 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
|
|
|
@@ -63,6 +90,38 @@ function iguales(a, b) {
|
|
|
63
90
|
for (let i = 0; i < a.length; i++) d |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
64
91
|
return d === 0;
|
|
65
92
|
}
|
|
93
|
+
var HEADERS_NO_REENVIADOS = ["cookie"];
|
|
94
|
+
function esReservado(path) {
|
|
95
|
+
const canon = pathCanonico(path).toLowerCase();
|
|
96
|
+
return PATHS_RESERVADOS.some((r) => r === "/" ? canon === "/" : canon === r || canon.startsWith(`${r}/`));
|
|
97
|
+
}
|
|
98
|
+
function prefijosApiValidos(prefijos = PREFIJOS_API_POR_DEFECTO) {
|
|
99
|
+
const out = [];
|
|
100
|
+
for (const crudo of prefijos) {
|
|
101
|
+
const segs = segmentosCanonicos(String(crudo ?? "")).map((s) => s.toLowerCase());
|
|
102
|
+
if (segs.length === 0) continue;
|
|
103
|
+
const path = `/${segs.join("/")}`;
|
|
104
|
+
if (esReservado(path)) continue;
|
|
105
|
+
if (!out.includes(`${path}/`)) out.push(`${path}/`);
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
function patronPermitido(patron, prefijos) {
|
|
110
|
+
if (!patronReenviable(patron)) return false;
|
|
111
|
+
const segs = patron.split("/").filter(Boolean);
|
|
112
|
+
const literal = (s) => s !== "*" && !s.startsWith(":");
|
|
113
|
+
const bajoPrefijo = prefijos.some((pre) => {
|
|
114
|
+
const ps = pre.split("/").filter(Boolean);
|
|
115
|
+
return segs.length > ps.length && ps.every((s, i) => literal(segs[i]) && segs[i].toLowerCase() === s);
|
|
116
|
+
});
|
|
117
|
+
if (!bajoPrefijo) return false;
|
|
118
|
+
const literales = [];
|
|
119
|
+
for (const s of segs) {
|
|
120
|
+
if (!literal(s)) break;
|
|
121
|
+
literales.push(s);
|
|
122
|
+
}
|
|
123
|
+
return !esReservado(`/${literales.join("/")}`);
|
|
124
|
+
}
|
|
66
125
|
function coincidePatron(patron, pathname) {
|
|
67
126
|
const p = patron.split("/").filter(Boolean);
|
|
68
127
|
const parts = segmentosCanonicos(pathname);
|
|
@@ -103,43 +162,43 @@ function crearProxyRuntime(opciones) {
|
|
|
103
162
|
const base = opciones.base.replace(/\/+$/, "");
|
|
104
163
|
const ttl = opciones.ttlMs ?? 6e4;
|
|
105
164
|
const fetchImpl = opciones.fetchImpl ?? fetch;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
patrones: opciones.respaldo?.patrones ?? []
|
|
111
|
-
};
|
|
165
|
+
const rutas = [...opciones.fijas?.rutas ?? RUTAS_PROXY.map((r) => r.path)];
|
|
166
|
+
const prefijos = [...opciones.fijas?.prefijos ?? RUTAS_DINAMICAS.map((r) => r.prefijo)];
|
|
167
|
+
const prefijosApi = prefijosApiValidos(opciones.prefijosApi ?? PREFIJOS_API_POR_DEFECTO);
|
|
168
|
+
let cache = { at: 0, patrones: [] };
|
|
112
169
|
let refrescando = null;
|
|
113
170
|
async function refrescar() {
|
|
114
171
|
try {
|
|
115
172
|
const res = await fetchImpl(`${base}/kit/manifest.json`, { signal: AbortSignal.timeout(5e3) });
|
|
116
173
|
if (!res.ok) throw new Error(String(res.status));
|
|
117
174
|
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
|
-
};
|
|
175
|
+
const patrones2 = Array.isArray(m.patrones) ? m.patrones.filter((p) => typeof p?.method === "string" && typeof p?.path === "string") : cache.patrones;
|
|
176
|
+
cache = { at: Date.now(), patrones: patrones2 };
|
|
124
177
|
} catch {
|
|
125
178
|
cache = { ...cache, at: Date.now() };
|
|
126
179
|
} finally {
|
|
127
180
|
refrescando = null;
|
|
128
181
|
}
|
|
129
182
|
}
|
|
130
|
-
async function
|
|
183
|
+
async function patrones() {
|
|
131
184
|
const vencido = Date.now() - cache.at >= ttl;
|
|
132
185
|
if (vencido) {
|
|
133
186
|
refrescando ??= refrescar();
|
|
134
187
|
if (cache.at === 0) await refrescando;
|
|
135
188
|
}
|
|
136
|
-
return cache;
|
|
189
|
+
return cache.patrones;
|
|
137
190
|
}
|
|
138
191
|
return {
|
|
192
|
+
/** Los prefijos de API efectivos, normalizados (para los avisos). */
|
|
193
|
+
prefijosApi,
|
|
139
194
|
/** Rutas de API con precio que este sitio no puede cobrar por falta de secreto. */
|
|
140
195
|
async sinSecreto() {
|
|
141
196
|
if (opciones.secreto) return [];
|
|
142
|
-
return (await
|
|
197
|
+
return (await patrones()).filter((p) => patronPermitido(p.path, prefijosApi));
|
|
198
|
+
},
|
|
199
|
+
/** Rutas de API con precio que no se reenvían por caer fuera de `prefijosApi` o en un path reservado. */
|
|
200
|
+
async fueraDePrefijos() {
|
|
201
|
+
return (await patrones()).filter((p) => patronReenviable(p.path) && !patronPermitido(p.path, prefijosApi));
|
|
143
202
|
},
|
|
144
203
|
/** Fuerza la carga del manifiesto (útil al arrancar o en tests). */
|
|
145
204
|
cargar: () => refrescando ??= refrescar(),
|
|
@@ -151,14 +210,18 @@ function crearProxyRuntime(opciones) {
|
|
|
151
210
|
const secreto = opciones.secreto || null;
|
|
152
211
|
const prueba = input.headers.get(PEAJE_ORIGIN_HEADER);
|
|
153
212
|
if (secreto && prueba && iguales(prueba, secreto)) return null;
|
|
154
|
-
const { rutas, prefijos, patrones } = await listas();
|
|
155
213
|
const path = input.pathname;
|
|
156
214
|
const canon = pathCanonico(path).toLowerCase();
|
|
157
215
|
const esFetchDePeaje = input.headers.get(PEAJE_FETCH_HEADER) !== null;
|
|
158
216
|
const fija = (rutas.includes(path) || rutas.some((r) => r.toLowerCase() === canon)) && !(canon === "/llms.txt" && esFetchDePeaje);
|
|
159
217
|
const prefijo = prefijos.some((p) => path.startsWith(p) || canon.startsWith(p.toLowerCase()));
|
|
160
|
-
|
|
161
|
-
|
|
218
|
+
let patron = false;
|
|
219
|
+
if (!fija && !prefijo && secreto !== null && !esReservado(path) && prefijosApi.some((pre) => canon.startsWith(pre))) {
|
|
220
|
+
const method = input.method.toUpperCase();
|
|
221
|
+
patron = (await patrones()).some(
|
|
222
|
+
(p) => p.method.toUpperCase() === method && patronPermitido(p.path, prefijosApi) && coincidePatron(p.path, path)
|
|
223
|
+
);
|
|
224
|
+
}
|
|
162
225
|
if (!fija && !prefijo && !patron) return null;
|
|
163
226
|
return { destino: `${base}${path}${input.search ?? ""}` };
|
|
164
227
|
}
|
|
@@ -303,29 +366,36 @@ function baseDe({ slug, gateway }) {
|
|
|
303
366
|
}
|
|
304
367
|
var SALTO = ["host", "connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "te", "trailer", "content-length"];
|
|
305
368
|
var avisado = false;
|
|
369
|
+
function avisar(runtime, conSecreto) {
|
|
370
|
+
if (!conSecreto) {
|
|
371
|
+
void runtime.sinSecreto().then((faltan) => {
|
|
372
|
+
if (faltan.length > 0) {
|
|
373
|
+
console.warn(
|
|
374
|
+
`[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.`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
void runtime.fueraDePrefijos().then((fuera) => {
|
|
380
|
+
if (fuera.length > 0) {
|
|
381
|
+
console.warn(
|
|
382
|
+
`[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.`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
306
387
|
function crearPeaje(opciones) {
|
|
307
388
|
if (!opciones?.slug) throw new Error("@peaje/proxy: `slug` is required (the business slug from your Peaje dashboard).");
|
|
308
389
|
const base = baseDe(opciones);
|
|
309
390
|
const secreto = opciones.secret ?? env("PEAJE_ORIGIN_SECRET") ?? null;
|
|
310
391
|
const fetchImpl = opciones.fetch ?? ((...a) => fetch(...a));
|
|
311
|
-
const runtime = crearProxyRuntime({
|
|
312
|
-
base,
|
|
313
|
-
secreto,
|
|
314
|
-
fetchImpl,
|
|
315
|
-
respaldo: { rutas: RUTAS_PROXY.map((x) => x.path), prefijos: RUTAS_DINAMICAS.map((x) => x.prefijo), patrones: [] }
|
|
316
|
-
});
|
|
392
|
+
const runtime = crearProxyRuntime({ base, secreto, fetchImpl, prefijosApi: opciones.apiPrefixes ?? PREFIJOS_API_POR_DEFECTO });
|
|
317
393
|
return {
|
|
318
394
|
base,
|
|
319
395
|
async decidir({ method, url, headers }) {
|
|
320
|
-
if (!
|
|
396
|
+
if (!avisado) {
|
|
321
397
|
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
|
-
});
|
|
398
|
+
avisar(runtime, Boolean(secreto));
|
|
329
399
|
}
|
|
330
400
|
const d = await runtime.decidir({ method, pathname: url.pathname, search: url.search, headers });
|
|
331
401
|
return d?.destino ?? null;
|
|
@@ -333,6 +403,7 @@ function crearPeaje(opciones) {
|
|
|
333
403
|
async reenviar(destino, { method, url, headers, body }) {
|
|
334
404
|
const salida = new Headers(headers);
|
|
335
405
|
for (const h of SALTO) salida.delete(h);
|
|
406
|
+
for (const h of HEADERS_NO_REENVIADOS) salida.delete(h);
|
|
336
407
|
const primero = (h) => headers.get(h)?.split(",")[0]?.trim() || null;
|
|
337
408
|
const host = primero("x-forwarded-host") ?? url.host;
|
|
338
409
|
salida.set("x-forwarded-host", host);
|
package/dist/hono.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -56,6 +56,33 @@ var RUTAS_DINAMICAS = [
|
|
|
56
56
|
{ path: "/r/:slug", prefijo: "/r/", nota: "links con precio: el 402 en tu dominio" },
|
|
57
57
|
{ path: "/checkout_sessions/:id", prefijo: "/checkout_sessions/", nota: "leer una sesi\xF3n de checkout ACP" }
|
|
58
58
|
];
|
|
59
|
+
var PREFIJOS_API_POR_DEFECTO = ["/api/", "/v1/"];
|
|
60
|
+
var PATHS_RESERVADOS = [
|
|
61
|
+
"/",
|
|
62
|
+
"/login",
|
|
63
|
+
"/logout",
|
|
64
|
+
"/signin",
|
|
65
|
+
"/signout",
|
|
66
|
+
"/signup",
|
|
67
|
+
"/sign-in",
|
|
68
|
+
"/sign-out",
|
|
69
|
+
"/sign-up",
|
|
70
|
+
"/register",
|
|
71
|
+
"/auth",
|
|
72
|
+
"/oauth",
|
|
73
|
+
"/session",
|
|
74
|
+
"/account",
|
|
75
|
+
"/admin",
|
|
76
|
+
"/dashboard",
|
|
77
|
+
"/_next",
|
|
78
|
+
"/_vercel",
|
|
79
|
+
"/api/auth",
|
|
80
|
+
"/static",
|
|
81
|
+
"/assets",
|
|
82
|
+
"/favicon.ico",
|
|
83
|
+
"/robots.txt",
|
|
84
|
+
"/sitemap.xml"
|
|
85
|
+
];
|
|
59
86
|
var PEAJE_FETCH_HEADER = "x-peaje-fetch";
|
|
60
87
|
var PEAJE_HOST_HEADER = "x-peaje-forwarded-host";
|
|
61
88
|
|
|
@@ -67,6 +94,38 @@ function iguales(a, b) {
|
|
|
67
94
|
for (let i = 0; i < a.length; i++) d |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
68
95
|
return d === 0;
|
|
69
96
|
}
|
|
97
|
+
var HEADERS_NO_REENVIADOS = ["cookie"];
|
|
98
|
+
function esReservado(path) {
|
|
99
|
+
const canon = pathCanonico(path).toLowerCase();
|
|
100
|
+
return PATHS_RESERVADOS.some((r) => r === "/" ? canon === "/" : canon === r || canon.startsWith(`${r}/`));
|
|
101
|
+
}
|
|
102
|
+
function prefijosApiValidos(prefijos = PREFIJOS_API_POR_DEFECTO) {
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const crudo of prefijos) {
|
|
105
|
+
const segs = segmentosCanonicos(String(crudo ?? "")).map((s) => s.toLowerCase());
|
|
106
|
+
if (segs.length === 0) continue;
|
|
107
|
+
const path = `/${segs.join("/")}`;
|
|
108
|
+
if (esReservado(path)) continue;
|
|
109
|
+
if (!out.includes(`${path}/`)) out.push(`${path}/`);
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
function patronPermitido(patron, prefijos) {
|
|
114
|
+
if (!patronReenviable(patron)) return false;
|
|
115
|
+
const segs = patron.split("/").filter(Boolean);
|
|
116
|
+
const literal = (s) => s !== "*" && !s.startsWith(":");
|
|
117
|
+
const bajoPrefijo = prefijos.some((pre) => {
|
|
118
|
+
const ps = pre.split("/").filter(Boolean);
|
|
119
|
+
return segs.length > ps.length && ps.every((s, i) => literal(segs[i]) && segs[i].toLowerCase() === s);
|
|
120
|
+
});
|
|
121
|
+
if (!bajoPrefijo) return false;
|
|
122
|
+
const literales = [];
|
|
123
|
+
for (const s of segs) {
|
|
124
|
+
if (!literal(s)) break;
|
|
125
|
+
literales.push(s);
|
|
126
|
+
}
|
|
127
|
+
return !esReservado(`/${literales.join("/")}`);
|
|
128
|
+
}
|
|
70
129
|
function coincidePatron(patron, pathname) {
|
|
71
130
|
const p = patron.split("/").filter(Boolean);
|
|
72
131
|
const parts = segmentosCanonicos(pathname);
|
|
@@ -107,43 +166,43 @@ function crearProxyRuntime(opciones) {
|
|
|
107
166
|
const base = opciones.base.replace(/\/+$/, "");
|
|
108
167
|
const ttl = opciones.ttlMs ?? 6e4;
|
|
109
168
|
const fetchImpl = opciones.fetchImpl ?? fetch;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
patrones: opciones.respaldo?.patrones ?? []
|
|
115
|
-
};
|
|
169
|
+
const rutas = [...opciones.fijas?.rutas ?? RUTAS_PROXY.map((r) => r.path)];
|
|
170
|
+
const prefijos = [...opciones.fijas?.prefijos ?? RUTAS_DINAMICAS.map((r) => r.prefijo)];
|
|
171
|
+
const prefijosApi = prefijosApiValidos(opciones.prefijosApi ?? PREFIJOS_API_POR_DEFECTO);
|
|
172
|
+
let cache = { at: 0, patrones: [] };
|
|
116
173
|
let refrescando = null;
|
|
117
174
|
async function refrescar() {
|
|
118
175
|
try {
|
|
119
176
|
const res = await fetchImpl(`${base}/kit/manifest.json`, { signal: AbortSignal.timeout(5e3) });
|
|
120
177
|
if (!res.ok) throw new Error(String(res.status));
|
|
121
178
|
const m = await res.json();
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
rutas: Array.isArray(m.rutas) ? m.rutas : cache.rutas,
|
|
125
|
-
prefijos: Array.isArray(m.prefijos) ? m.prefijos : cache.prefijos,
|
|
126
|
-
patrones: Array.isArray(m.patrones) ? m.patrones : cache.patrones
|
|
127
|
-
};
|
|
179
|
+
const patrones2 = Array.isArray(m.patrones) ? m.patrones.filter((p) => typeof p?.method === "string" && typeof p?.path === "string") : cache.patrones;
|
|
180
|
+
cache = { at: Date.now(), patrones: patrones2 };
|
|
128
181
|
} catch {
|
|
129
182
|
cache = { ...cache, at: Date.now() };
|
|
130
183
|
} finally {
|
|
131
184
|
refrescando = null;
|
|
132
185
|
}
|
|
133
186
|
}
|
|
134
|
-
async function
|
|
187
|
+
async function patrones() {
|
|
135
188
|
const vencido = Date.now() - cache.at >= ttl;
|
|
136
189
|
if (vencido) {
|
|
137
190
|
refrescando ??= refrescar();
|
|
138
191
|
if (cache.at === 0) await refrescando;
|
|
139
192
|
}
|
|
140
|
-
return cache;
|
|
193
|
+
return cache.patrones;
|
|
141
194
|
}
|
|
142
195
|
return {
|
|
196
|
+
/** Los prefijos de API efectivos, normalizados (para los avisos). */
|
|
197
|
+
prefijosApi,
|
|
143
198
|
/** Rutas de API con precio que este sitio no puede cobrar por falta de secreto. */
|
|
144
199
|
async sinSecreto() {
|
|
145
200
|
if (opciones.secreto) return [];
|
|
146
|
-
return (await
|
|
201
|
+
return (await patrones()).filter((p) => patronPermitido(p.path, prefijosApi));
|
|
202
|
+
},
|
|
203
|
+
/** Rutas de API con precio que no se reenvían por caer fuera de `prefijosApi` o en un path reservado. */
|
|
204
|
+
async fueraDePrefijos() {
|
|
205
|
+
return (await patrones()).filter((p) => patronReenviable(p.path) && !patronPermitido(p.path, prefijosApi));
|
|
147
206
|
},
|
|
148
207
|
/** Fuerza la carga del manifiesto (útil al arrancar o en tests). */
|
|
149
208
|
cargar: () => refrescando ??= refrescar(),
|
|
@@ -155,14 +214,18 @@ function crearProxyRuntime(opciones) {
|
|
|
155
214
|
const secreto = opciones.secreto || null;
|
|
156
215
|
const prueba = input.headers.get(PEAJE_ORIGIN_HEADER);
|
|
157
216
|
if (secreto && prueba && iguales(prueba, secreto)) return null;
|
|
158
|
-
const { rutas, prefijos, patrones } = await listas();
|
|
159
217
|
const path = input.pathname;
|
|
160
218
|
const canon = pathCanonico(path).toLowerCase();
|
|
161
219
|
const esFetchDePeaje = input.headers.get(PEAJE_FETCH_HEADER) !== null;
|
|
162
220
|
const fija = (rutas.includes(path) || rutas.some((r) => r.toLowerCase() === canon)) && !(canon === "/llms.txt" && esFetchDePeaje);
|
|
163
221
|
const prefijo = prefijos.some((p) => path.startsWith(p) || canon.startsWith(p.toLowerCase()));
|
|
164
|
-
|
|
165
|
-
|
|
222
|
+
let patron = false;
|
|
223
|
+
if (!fija && !prefijo && secreto !== null && !esReservado(path) && prefijosApi.some((pre) => canon.startsWith(pre))) {
|
|
224
|
+
const method = input.method.toUpperCase();
|
|
225
|
+
patron = (await patrones()).some(
|
|
226
|
+
(p) => p.method.toUpperCase() === method && patronPermitido(p.path, prefijosApi) && coincidePatron(p.path, path)
|
|
227
|
+
);
|
|
228
|
+
}
|
|
166
229
|
if (!fija && !prefijo && !patron) return null;
|
|
167
230
|
return { destino: `${base}${path}${input.search ?? ""}` };
|
|
168
231
|
}
|
|
@@ -307,6 +370,24 @@ function baseDe({ slug, gateway }) {
|
|
|
307
370
|
}
|
|
308
371
|
var SALTO = ["host", "connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "te", "trailer", "content-length"];
|
|
309
372
|
var avisado = false;
|
|
373
|
+
function avisar(runtime, conSecreto) {
|
|
374
|
+
if (!conSecreto) {
|
|
375
|
+
void runtime.sinSecreto().then((faltan) => {
|
|
376
|
+
if (faltan.length > 0) {
|
|
377
|
+
console.warn(
|
|
378
|
+
`[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.`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
void runtime.fueraDePrefijos().then((fuera) => {
|
|
384
|
+
if (fuera.length > 0) {
|
|
385
|
+
console.warn(
|
|
386
|
+
`[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.`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
310
391
|
function _reiniciarAviso() {
|
|
311
392
|
avisado = false;
|
|
312
393
|
}
|
|
@@ -315,24 +396,13 @@ function crearPeaje(opciones) {
|
|
|
315
396
|
const base = baseDe(opciones);
|
|
316
397
|
const secreto = opciones.secret ?? env("PEAJE_ORIGIN_SECRET") ?? null;
|
|
317
398
|
const fetchImpl = opciones.fetch ?? ((...a) => fetch(...a));
|
|
318
|
-
const runtime = crearProxyRuntime({
|
|
319
|
-
base,
|
|
320
|
-
secreto,
|
|
321
|
-
fetchImpl,
|
|
322
|
-
respaldo: { rutas: RUTAS_PROXY.map((x) => x.path), prefijos: RUTAS_DINAMICAS.map((x) => x.prefijo), patrones: [] }
|
|
323
|
-
});
|
|
399
|
+
const runtime = crearProxyRuntime({ base, secreto, fetchImpl, prefijosApi: opciones.apiPrefixes ?? PREFIJOS_API_POR_DEFECTO });
|
|
324
400
|
return {
|
|
325
401
|
base,
|
|
326
402
|
async decidir({ method, url, headers }) {
|
|
327
|
-
if (!
|
|
403
|
+
if (!avisado) {
|
|
328
404
|
avisado = true;
|
|
329
|
-
|
|
330
|
-
if (faltan.length > 0) {
|
|
331
|
-
console.warn(
|
|
332
|
-
`[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.`
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
});
|
|
405
|
+
avisar(runtime, Boolean(secreto));
|
|
336
406
|
}
|
|
337
407
|
const d = await runtime.decidir({ method, pathname: url.pathname, search: url.search, headers });
|
|
338
408
|
return d?.destino ?? null;
|
|
@@ -340,6 +410,7 @@ function crearPeaje(opciones) {
|
|
|
340
410
|
async reenviar(destino, { method, url, headers, body }) {
|
|
341
411
|
const salida = new Headers(headers);
|
|
342
412
|
for (const h of SALTO) salida.delete(h);
|
|
413
|
+
for (const h of HEADERS_NO_REENVIADOS) salida.delete(h);
|
|
343
414
|
const primero = (h) => headers.get(h)?.split(",")[0]?.trim() || null;
|
|
344
415
|
const host = primero("x-forwarded-host") ?? url.host;
|
|
345
416
|
salida.set("x-forwarded-host", host);
|
package/dist/index.d.cts
CHANGED
|
@@ -10,6 +10,13 @@ type OpcionesPeaje = {
|
|
|
10
10
|
secret?: string;
|
|
11
11
|
/** URL pública del gateway. Por defecto `PEAJE_GATEWAY_URL` o la de producción. */
|
|
12
12
|
gateway?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Prefijos bajo los que viven tus rutas de API con precio. Una ruta del
|
|
15
|
+
* dashboard fuera de ellos cobra solo en la URL del gateway, no en tu
|
|
16
|
+
* dominio. Por defecto `['/api/', '/v1/']`. La raíz y los paths reservados
|
|
17
|
+
* (`/login`, `/auth`, `/api/auth`, `/_next`...) nunca se aceptan.
|
|
18
|
+
*/
|
|
19
|
+
apiPrefixes?: string[];
|
|
13
20
|
/** Para tests o runtimes sin fetch global. */
|
|
14
21
|
fetch?: typeof fetch;
|
|
15
22
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ type OpcionesPeaje = {
|
|
|
10
10
|
secret?: string;
|
|
11
11
|
/** URL pública del gateway. Por defecto `PEAJE_GATEWAY_URL` o la de producción. */
|
|
12
12
|
gateway?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Prefijos bajo los que viven tus rutas de API con precio. Una ruta del
|
|
15
|
+
* dashboard fuera de ellos cobra solo en la URL del gateway, no en tu
|
|
16
|
+
* dominio. Por defecto `['/api/', '/v1/']`. La raíz y los paths reservados
|
|
17
|
+
* (`/login`, `/auth`, `/api/auth`, `/_next`...) nunca se aceptan.
|
|
18
|
+
*/
|
|
19
|
+
apiPrefixes?: string[];
|
|
13
20
|
/** Para tests o runtimes sin fetch global. */
|
|
14
21
|
fetch?: typeof fetch;
|
|
15
22
|
};
|
package/dist/index.js
CHANGED
package/dist/vercel.cjs
CHANGED
|
@@ -52,6 +52,33 @@ 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
|
|
|
@@ -63,6 +90,38 @@ function iguales(a, b) {
|
|
|
63
90
|
for (let i = 0; i < a.length; i++) d |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
64
91
|
return d === 0;
|
|
65
92
|
}
|
|
93
|
+
var HEADERS_NO_REENVIADOS = ["cookie"];
|
|
94
|
+
function esReservado(path) {
|
|
95
|
+
const canon = pathCanonico(path).toLowerCase();
|
|
96
|
+
return PATHS_RESERVADOS.some((r) => r === "/" ? canon === "/" : canon === r || canon.startsWith(`${r}/`));
|
|
97
|
+
}
|
|
98
|
+
function prefijosApiValidos(prefijos = PREFIJOS_API_POR_DEFECTO) {
|
|
99
|
+
const out = [];
|
|
100
|
+
for (const crudo of prefijos) {
|
|
101
|
+
const segs = segmentosCanonicos(String(crudo ?? "")).map((s) => s.toLowerCase());
|
|
102
|
+
if (segs.length === 0) continue;
|
|
103
|
+
const path = `/${segs.join("/")}`;
|
|
104
|
+
if (esReservado(path)) continue;
|
|
105
|
+
if (!out.includes(`${path}/`)) out.push(`${path}/`);
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
function patronPermitido(patron, prefijos) {
|
|
110
|
+
if (!patronReenviable(patron)) return false;
|
|
111
|
+
const segs = patron.split("/").filter(Boolean);
|
|
112
|
+
const literal = (s) => s !== "*" && !s.startsWith(":");
|
|
113
|
+
const bajoPrefijo = prefijos.some((pre) => {
|
|
114
|
+
const ps = pre.split("/").filter(Boolean);
|
|
115
|
+
return segs.length > ps.length && ps.every((s, i) => literal(segs[i]) && segs[i].toLowerCase() === s);
|
|
116
|
+
});
|
|
117
|
+
if (!bajoPrefijo) return false;
|
|
118
|
+
const literales = [];
|
|
119
|
+
for (const s of segs) {
|
|
120
|
+
if (!literal(s)) break;
|
|
121
|
+
literales.push(s);
|
|
122
|
+
}
|
|
123
|
+
return !esReservado(`/${literales.join("/")}`);
|
|
124
|
+
}
|
|
66
125
|
function coincidePatron(patron, pathname) {
|
|
67
126
|
const p = patron.split("/").filter(Boolean);
|
|
68
127
|
const parts = segmentosCanonicos(pathname);
|
|
@@ -103,43 +162,43 @@ function crearProxyRuntime(opciones) {
|
|
|
103
162
|
const base = opciones.base.replace(/\/+$/, "");
|
|
104
163
|
const ttl = opciones.ttlMs ?? 6e4;
|
|
105
164
|
const fetchImpl = opciones.fetchImpl ?? fetch;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
patrones: opciones.respaldo?.patrones ?? []
|
|
111
|
-
};
|
|
165
|
+
const rutas = [...opciones.fijas?.rutas ?? RUTAS_PROXY.map((r) => r.path)];
|
|
166
|
+
const prefijos = [...opciones.fijas?.prefijos ?? RUTAS_DINAMICAS.map((r) => r.prefijo)];
|
|
167
|
+
const prefijosApi = prefijosApiValidos(opciones.prefijosApi ?? PREFIJOS_API_POR_DEFECTO);
|
|
168
|
+
let cache = { at: 0, patrones: [] };
|
|
112
169
|
let refrescando = null;
|
|
113
170
|
async function refrescar() {
|
|
114
171
|
try {
|
|
115
172
|
const res = await fetchImpl(`${base}/kit/manifest.json`, { signal: AbortSignal.timeout(5e3) });
|
|
116
173
|
if (!res.ok) throw new Error(String(res.status));
|
|
117
174
|
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
|
-
};
|
|
175
|
+
const patrones2 = Array.isArray(m.patrones) ? m.patrones.filter((p) => typeof p?.method === "string" && typeof p?.path === "string") : cache.patrones;
|
|
176
|
+
cache = { at: Date.now(), patrones: patrones2 };
|
|
124
177
|
} catch {
|
|
125
178
|
cache = { ...cache, at: Date.now() };
|
|
126
179
|
} finally {
|
|
127
180
|
refrescando = null;
|
|
128
181
|
}
|
|
129
182
|
}
|
|
130
|
-
async function
|
|
183
|
+
async function patrones() {
|
|
131
184
|
const vencido = Date.now() - cache.at >= ttl;
|
|
132
185
|
if (vencido) {
|
|
133
186
|
refrescando ??= refrescar();
|
|
134
187
|
if (cache.at === 0) await refrescando;
|
|
135
188
|
}
|
|
136
|
-
return cache;
|
|
189
|
+
return cache.patrones;
|
|
137
190
|
}
|
|
138
191
|
return {
|
|
192
|
+
/** Los prefijos de API efectivos, normalizados (para los avisos). */
|
|
193
|
+
prefijosApi,
|
|
139
194
|
/** Rutas de API con precio que este sitio no puede cobrar por falta de secreto. */
|
|
140
195
|
async sinSecreto() {
|
|
141
196
|
if (opciones.secreto) return [];
|
|
142
|
-
return (await
|
|
197
|
+
return (await patrones()).filter((p) => patronPermitido(p.path, prefijosApi));
|
|
198
|
+
},
|
|
199
|
+
/** Rutas de API con precio que no se reenvían por caer fuera de `prefijosApi` o en un path reservado. */
|
|
200
|
+
async fueraDePrefijos() {
|
|
201
|
+
return (await patrones()).filter((p) => patronReenviable(p.path) && !patronPermitido(p.path, prefijosApi));
|
|
143
202
|
},
|
|
144
203
|
/** Fuerza la carga del manifiesto (útil al arrancar o en tests). */
|
|
145
204
|
cargar: () => refrescando ??= refrescar(),
|
|
@@ -151,14 +210,18 @@ function crearProxyRuntime(opciones) {
|
|
|
151
210
|
const secreto = opciones.secreto || null;
|
|
152
211
|
const prueba = input.headers.get(PEAJE_ORIGIN_HEADER);
|
|
153
212
|
if (secreto && prueba && iguales(prueba, secreto)) return null;
|
|
154
|
-
const { rutas, prefijos, patrones } = await listas();
|
|
155
213
|
const path = input.pathname;
|
|
156
214
|
const canon = pathCanonico(path).toLowerCase();
|
|
157
215
|
const esFetchDePeaje = input.headers.get(PEAJE_FETCH_HEADER) !== null;
|
|
158
216
|
const fija = (rutas.includes(path) || rutas.some((r) => r.toLowerCase() === canon)) && !(canon === "/llms.txt" && esFetchDePeaje);
|
|
159
217
|
const prefijo = prefijos.some((p) => path.startsWith(p) || canon.startsWith(p.toLowerCase()));
|
|
160
|
-
|
|
161
|
-
|
|
218
|
+
let patron = false;
|
|
219
|
+
if (!fija && !prefijo && secreto !== null && !esReservado(path) && prefijosApi.some((pre) => canon.startsWith(pre))) {
|
|
220
|
+
const method = input.method.toUpperCase();
|
|
221
|
+
patron = (await patrones()).some(
|
|
222
|
+
(p) => p.method.toUpperCase() === method && patronPermitido(p.path, prefijosApi) && coincidePatron(p.path, path)
|
|
223
|
+
);
|
|
224
|
+
}
|
|
162
225
|
if (!fija && !prefijo && !patron) return null;
|
|
163
226
|
return { destino: `${base}${path}${input.search ?? ""}` };
|
|
164
227
|
}
|
|
@@ -303,29 +366,36 @@ function baseDe({ slug, gateway }) {
|
|
|
303
366
|
}
|
|
304
367
|
var SALTO = ["host", "connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "te", "trailer", "content-length"];
|
|
305
368
|
var avisado = false;
|
|
369
|
+
function avisar(runtime, conSecreto) {
|
|
370
|
+
if (!conSecreto) {
|
|
371
|
+
void runtime.sinSecreto().then((faltan) => {
|
|
372
|
+
if (faltan.length > 0) {
|
|
373
|
+
console.warn(
|
|
374
|
+
`[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.`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
void runtime.fueraDePrefijos().then((fuera) => {
|
|
380
|
+
if (fuera.length > 0) {
|
|
381
|
+
console.warn(
|
|
382
|
+
`[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.`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
306
387
|
function crearPeaje(opciones) {
|
|
307
388
|
if (!opciones?.slug) throw new Error("@peaje/proxy: `slug` is required (the business slug from your Peaje dashboard).");
|
|
308
389
|
const base = baseDe(opciones);
|
|
309
390
|
const secreto = opciones.secret ?? env("PEAJE_ORIGIN_SECRET") ?? null;
|
|
310
391
|
const fetchImpl = opciones.fetch ?? ((...a) => fetch(...a));
|
|
311
|
-
const runtime = crearProxyRuntime({
|
|
312
|
-
base,
|
|
313
|
-
secreto,
|
|
314
|
-
fetchImpl,
|
|
315
|
-
respaldo: { rutas: RUTAS_PROXY.map((x) => x.path), prefijos: RUTAS_DINAMICAS.map((x) => x.prefijo), patrones: [] }
|
|
316
|
-
});
|
|
392
|
+
const runtime = crearProxyRuntime({ base, secreto, fetchImpl, prefijosApi: opciones.apiPrefixes ?? PREFIJOS_API_POR_DEFECTO });
|
|
317
393
|
return {
|
|
318
394
|
base,
|
|
319
395
|
async decidir({ method, url, headers }) {
|
|
320
|
-
if (!
|
|
396
|
+
if (!avisado) {
|
|
321
397
|
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
|
-
});
|
|
398
|
+
avisar(runtime, Boolean(secreto));
|
|
329
399
|
}
|
|
330
400
|
const d = await runtime.decidir({ method, pathname: url.pathname, search: url.search, headers });
|
|
331
401
|
return d?.destino ?? null;
|
|
@@ -333,6 +403,7 @@ function crearPeaje(opciones) {
|
|
|
333
403
|
async reenviar(destino, { method, url, headers, body }) {
|
|
334
404
|
const salida = new Headers(headers);
|
|
335
405
|
for (const h of SALTO) salida.delete(h);
|
|
406
|
+
for (const h of HEADERS_NO_REENVIADOS) salida.delete(h);
|
|
336
407
|
const primero = (h) => headers.get(h)?.split(",")[0]?.trim() || null;
|
|
337
408
|
const host = primero("x-forwarded-host") ?? url.host;
|
|
338
409
|
salida.set("x-forwarded-host", host);
|
|
@@ -371,6 +442,7 @@ function peajeVercel(opciones, siguiente) {
|
|
|
371
442
|
const destino = await peaje.decidir(entrada);
|
|
372
443
|
if (destino) {
|
|
373
444
|
const headers = new Headers(request.headers);
|
|
445
|
+
for (const h of HEADERS_NO_REENVIADOS) headers.delete(h);
|
|
374
446
|
headers.set("x-forwarded-host", url.host);
|
|
375
447
|
headers.set(PEAJE_HOST_HEADER, url.host);
|
|
376
448
|
headers.set("x-forwarded-proto", url.protocol.replace(":", ""));
|
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-QHUY5N5S.js";
|
|
5
6
|
|
|
6
7
|
// src/vercel.ts
|
|
7
8
|
function respuestaMiddleware(control, requestHeaders) {
|
|
@@ -24,6 +25,7 @@ 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(":", ""));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peaje/proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|