@peaje/proxy 0.1.1 → 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-6ADZXHC7.js → chunk-QHUY5N5S.js} +109 -33
- package/dist/express.cjs +107 -33
- package/dist/express.js +1 -1
- package/dist/hono.cjs +107 -33
- package/dist/hono.js +1 -1
- package/dist/index.cjs +107 -33
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1 -1
- package/dist/vercel.cjs +109 -33
- package/dist/vercel.d.cts +0 -18
- package/dist/vercel.d.ts +0 -18
- package/dist/vercel.js +5 -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,7 +25,35 @@ 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";
|
|
56
|
+
var PEAJE_HOST_HEADER = "x-peaje-forwarded-host";
|
|
29
57
|
|
|
30
58
|
// ../shared/src/kit/proxy-runtime.ts
|
|
31
59
|
var PEAJE_ORIGIN_HEADER = "x-peaje-origin";
|
|
@@ -35,6 +63,38 @@ function iguales(a, b) {
|
|
|
35
63
|
for (let i = 0; i < a.length; i++) d |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
36
64
|
return d === 0;
|
|
37
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
|
+
}
|
|
38
98
|
function coincidePatron(patron, pathname) {
|
|
39
99
|
const p = patron.split("/").filter(Boolean);
|
|
40
100
|
const parts = segmentosCanonicos(pathname);
|
|
@@ -75,43 +135,43 @@ function crearProxyRuntime(opciones) {
|
|
|
75
135
|
const base = opciones.base.replace(/\/+$/, "");
|
|
76
136
|
const ttl = opciones.ttlMs ?? 6e4;
|
|
77
137
|
const fetchImpl = opciones.fetchImpl ?? fetch;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
patrones: opciones.respaldo?.patrones ?? []
|
|
83
|
-
};
|
|
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: [] };
|
|
84
142
|
let refrescando = null;
|
|
85
143
|
async function refrescar() {
|
|
86
144
|
try {
|
|
87
145
|
const res = await fetchImpl(`${base}/kit/manifest.json`, { signal: AbortSignal.timeout(5e3) });
|
|
88
146
|
if (!res.ok) throw new Error(String(res.status));
|
|
89
147
|
const m = await res.json();
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
rutas: Array.isArray(m.rutas) ? m.rutas : cache.rutas,
|
|
93
|
-
prefijos: Array.isArray(m.prefijos) ? m.prefijos : cache.prefijos,
|
|
94
|
-
patrones: Array.isArray(m.patrones) ? m.patrones : cache.patrones
|
|
95
|
-
};
|
|
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 };
|
|
96
150
|
} catch {
|
|
97
151
|
cache = { ...cache, at: Date.now() };
|
|
98
152
|
} finally {
|
|
99
153
|
refrescando = null;
|
|
100
154
|
}
|
|
101
155
|
}
|
|
102
|
-
async function
|
|
156
|
+
async function patrones() {
|
|
103
157
|
const vencido = Date.now() - cache.at >= ttl;
|
|
104
158
|
if (vencido) {
|
|
105
159
|
refrescando ??= refrescar();
|
|
106
160
|
if (cache.at === 0) await refrescando;
|
|
107
161
|
}
|
|
108
|
-
return cache;
|
|
162
|
+
return cache.patrones;
|
|
109
163
|
}
|
|
110
164
|
return {
|
|
165
|
+
/** Los prefijos de API efectivos, normalizados (para los avisos). */
|
|
166
|
+
prefijosApi,
|
|
111
167
|
/** Rutas de API con precio que este sitio no puede cobrar por falta de secreto. */
|
|
112
168
|
async sinSecreto() {
|
|
113
169
|
if (opciones.secreto) return [];
|
|
114
|
-
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));
|
|
115
175
|
},
|
|
116
176
|
/** Fuerza la carga del manifiesto (útil al arrancar o en tests). */
|
|
117
177
|
cargar: () => refrescando ??= refrescar(),
|
|
@@ -123,14 +183,18 @@ function crearProxyRuntime(opciones) {
|
|
|
123
183
|
const secreto = opciones.secreto || null;
|
|
124
184
|
const prueba = input.headers.get(PEAJE_ORIGIN_HEADER);
|
|
125
185
|
if (secreto && prueba && iguales(prueba, secreto)) return null;
|
|
126
|
-
const { rutas, prefijos, patrones } = await listas();
|
|
127
186
|
const path = input.pathname;
|
|
128
187
|
const canon = pathCanonico(path).toLowerCase();
|
|
129
188
|
const esFetchDePeaje = input.headers.get(PEAJE_FETCH_HEADER) !== null;
|
|
130
189
|
const fija = (rutas.includes(path) || rutas.some((r) => r.toLowerCase() === canon)) && !(canon === "/llms.txt" && esFetchDePeaje);
|
|
131
190
|
const prefijo = prefijos.some((p) => path.startsWith(p) || canon.startsWith(p.toLowerCase()));
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
}
|
|
134
198
|
if (!fija && !prefijo && !patron) return null;
|
|
135
199
|
return { destino: `${base}${path}${input.search ?? ""}` };
|
|
136
200
|
}
|
|
@@ -275,6 +339,24 @@ function baseDe({ slug, gateway }) {
|
|
|
275
339
|
}
|
|
276
340
|
var SALTO = ["host", "connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "te", "trailer", "content-length"];
|
|
277
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
|
+
}
|
|
278
360
|
function _reiniciarAviso() {
|
|
279
361
|
avisado = false;
|
|
280
362
|
}
|
|
@@ -283,24 +365,13 @@ function crearPeaje(opciones) {
|
|
|
283
365
|
const base = baseDe(opciones);
|
|
284
366
|
const secreto = opciones.secret ?? env("PEAJE_ORIGIN_SECRET") ?? null;
|
|
285
367
|
const fetchImpl = opciones.fetch ?? ((...a) => fetch(...a));
|
|
286
|
-
const runtime = crearProxyRuntime({
|
|
287
|
-
base,
|
|
288
|
-
secreto,
|
|
289
|
-
fetchImpl,
|
|
290
|
-
respaldo: { rutas: RUTAS_PROXY.map((x) => x.path), prefijos: RUTAS_DINAMICAS.map((x) => x.prefijo), patrones: [] }
|
|
291
|
-
});
|
|
368
|
+
const runtime = crearProxyRuntime({ base, secreto, fetchImpl, prefijosApi: opciones.apiPrefixes ?? PREFIJOS_API_POR_DEFECTO });
|
|
292
369
|
return {
|
|
293
370
|
base,
|
|
294
371
|
async decidir({ method, url, headers }) {
|
|
295
|
-
if (!
|
|
372
|
+
if (!avisado) {
|
|
296
373
|
avisado = true;
|
|
297
|
-
|
|
298
|
-
if (faltan.length > 0) {
|
|
299
|
-
console.warn(
|
|
300
|
-
`[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.`
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
});
|
|
374
|
+
avisar(runtime, Boolean(secreto));
|
|
304
375
|
}
|
|
305
376
|
const d = await runtime.decidir({ method, pathname: url.pathname, search: url.search, headers });
|
|
306
377
|
return d?.destino ?? null;
|
|
@@ -308,8 +379,11 @@ function crearPeaje(opciones) {
|
|
|
308
379
|
async reenviar(destino, { method, url, headers, body }) {
|
|
309
380
|
const salida = new Headers(headers);
|
|
310
381
|
for (const h of SALTO) salida.delete(h);
|
|
382
|
+
for (const h of HEADERS_NO_REENVIADOS) salida.delete(h);
|
|
311
383
|
const primero = (h) => headers.get(h)?.split(",")[0]?.trim() || null;
|
|
312
|
-
|
|
384
|
+
const host = primero("x-forwarded-host") ?? url.host;
|
|
385
|
+
salida.set("x-forwarded-host", host);
|
|
386
|
+
salida.set(PEAJE_HOST_HEADER, host);
|
|
313
387
|
salida.set("x-forwarded-proto", primero("x-forwarded-proto") ?? url.protocol.replace(":", ""));
|
|
314
388
|
const sinBody = method === "GET" || method === "HEAD";
|
|
315
389
|
const res = await fetchImpl(destino, { method, headers: salida, body: sinBody ? null : body, redirect: "manual" });
|
|
@@ -336,6 +410,8 @@ function peajeFetch(opciones) {
|
|
|
336
410
|
var src_default = peajeFetch;
|
|
337
411
|
|
|
338
412
|
export {
|
|
413
|
+
PEAJE_HOST_HEADER,
|
|
414
|
+
HEADERS_NO_REENVIADOS,
|
|
339
415
|
GATEWAY_POR_DEFECTO,
|
|
340
416
|
baseDe,
|
|
341
417
|
_reiniciarAviso,
|
package/dist/express.cjs
CHANGED
|
@@ -53,7 +53,35 @@ 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";
|
|
84
|
+
var PEAJE_HOST_HEADER = "x-peaje-forwarded-host";
|
|
57
85
|
|
|
58
86
|
// ../shared/src/kit/proxy-runtime.ts
|
|
59
87
|
var PEAJE_ORIGIN_HEADER = "x-peaje-origin";
|
|
@@ -63,6 +91,38 @@ function iguales(a, b) {
|
|
|
63
91
|
for (let i = 0; i < a.length; i++) d |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
64
92
|
return d === 0;
|
|
65
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
|
+
}
|
|
66
126
|
function coincidePatron(patron, pathname) {
|
|
67
127
|
const p = patron.split("/").filter(Boolean);
|
|
68
128
|
const parts = segmentosCanonicos(pathname);
|
|
@@ -103,43 +163,43 @@ function crearProxyRuntime(opciones) {
|
|
|
103
163
|
const base = opciones.base.replace(/\/+$/, "");
|
|
104
164
|
const ttl = opciones.ttlMs ?? 6e4;
|
|
105
165
|
const fetchImpl = opciones.fetchImpl ?? fetch;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
patrones: opciones.respaldo?.patrones ?? []
|
|
111
|
-
};
|
|
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: [] };
|
|
112
170
|
let refrescando = null;
|
|
113
171
|
async function refrescar() {
|
|
114
172
|
try {
|
|
115
173
|
const res = await fetchImpl(`${base}/kit/manifest.json`, { signal: AbortSignal.timeout(5e3) });
|
|
116
174
|
if (!res.ok) throw new Error(String(res.status));
|
|
117
175
|
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
|
-
};
|
|
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 };
|
|
124
178
|
} catch {
|
|
125
179
|
cache = { ...cache, at: Date.now() };
|
|
126
180
|
} finally {
|
|
127
181
|
refrescando = null;
|
|
128
182
|
}
|
|
129
183
|
}
|
|
130
|
-
async function
|
|
184
|
+
async function patrones() {
|
|
131
185
|
const vencido = Date.now() - cache.at >= ttl;
|
|
132
186
|
if (vencido) {
|
|
133
187
|
refrescando ??= refrescar();
|
|
134
188
|
if (cache.at === 0) await refrescando;
|
|
135
189
|
}
|
|
136
|
-
return cache;
|
|
190
|
+
return cache.patrones;
|
|
137
191
|
}
|
|
138
192
|
return {
|
|
193
|
+
/** Los prefijos de API efectivos, normalizados (para los avisos). */
|
|
194
|
+
prefijosApi,
|
|
139
195
|
/** Rutas de API con precio que este sitio no puede cobrar por falta de secreto. */
|
|
140
196
|
async sinSecreto() {
|
|
141
197
|
if (opciones.secreto) return [];
|
|
142
|
-
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));
|
|
143
203
|
},
|
|
144
204
|
/** Fuerza la carga del manifiesto (útil al arrancar o en tests). */
|
|
145
205
|
cargar: () => refrescando ??= refrescar(),
|
|
@@ -151,14 +211,18 @@ function crearProxyRuntime(opciones) {
|
|
|
151
211
|
const secreto = opciones.secreto || null;
|
|
152
212
|
const prueba = input.headers.get(PEAJE_ORIGIN_HEADER);
|
|
153
213
|
if (secreto && prueba && iguales(prueba, secreto)) return null;
|
|
154
|
-
const { rutas, prefijos, patrones } = await listas();
|
|
155
214
|
const path = input.pathname;
|
|
156
215
|
const canon = pathCanonico(path).toLowerCase();
|
|
157
216
|
const esFetchDePeaje = input.headers.get(PEAJE_FETCH_HEADER) !== null;
|
|
158
217
|
const fija = (rutas.includes(path) || rutas.some((r) => r.toLowerCase() === canon)) && !(canon === "/llms.txt" && esFetchDePeaje);
|
|
159
218
|
const prefijo = prefijos.some((p) => path.startsWith(p) || canon.startsWith(p.toLowerCase()));
|
|
160
|
-
|
|
161
|
-
|
|
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
|
+
}
|
|
162
226
|
if (!fija && !prefijo && !patron) return null;
|
|
163
227
|
return { destino: `${base}${path}${input.search ?? ""}` };
|
|
164
228
|
}
|
|
@@ -303,29 +367,36 @@ function baseDe({ slug, gateway }) {
|
|
|
303
367
|
}
|
|
304
368
|
var SALTO = ["host", "connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "te", "trailer", "content-length"];
|
|
305
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
|
+
}
|
|
306
388
|
function crearPeaje(opciones) {
|
|
307
389
|
if (!opciones?.slug) throw new Error("@peaje/proxy: `slug` is required (the business slug from your Peaje dashboard).");
|
|
308
390
|
const base = baseDe(opciones);
|
|
309
391
|
const secreto = opciones.secret ?? env("PEAJE_ORIGIN_SECRET") ?? null;
|
|
310
392
|
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
|
-
});
|
|
393
|
+
const runtime = crearProxyRuntime({ base, secreto, fetchImpl, prefijosApi: opciones.apiPrefixes ?? PREFIJOS_API_POR_DEFECTO });
|
|
317
394
|
return {
|
|
318
395
|
base,
|
|
319
396
|
async decidir({ method, url, headers }) {
|
|
320
|
-
if (!
|
|
397
|
+
if (!avisado) {
|
|
321
398
|
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
|
-
});
|
|
399
|
+
avisar(runtime, Boolean(secreto));
|
|
329
400
|
}
|
|
330
401
|
const d = await runtime.decidir({ method, pathname: url.pathname, search: url.search, headers });
|
|
331
402
|
return d?.destino ?? null;
|
|
@@ -333,8 +404,11 @@ function crearPeaje(opciones) {
|
|
|
333
404
|
async reenviar(destino, { method, url, headers, body }) {
|
|
334
405
|
const salida = new Headers(headers);
|
|
335
406
|
for (const h of SALTO) salida.delete(h);
|
|
407
|
+
for (const h of HEADERS_NO_REENVIADOS) salida.delete(h);
|
|
336
408
|
const primero = (h) => headers.get(h)?.split(",")[0]?.trim() || null;
|
|
337
|
-
|
|
409
|
+
const host = primero("x-forwarded-host") ?? url.host;
|
|
410
|
+
salida.set("x-forwarded-host", host);
|
|
411
|
+
salida.set(PEAJE_HOST_HEADER, host);
|
|
338
412
|
salida.set("x-forwarded-proto", primero("x-forwarded-proto") ?? url.protocol.replace(":", ""));
|
|
339
413
|
const sinBody = method === "GET" || method === "HEAD";
|
|
340
414
|
const res = await fetchImpl(destino, { method, headers: salida, body: sinBody ? null : body, redirect: "manual" });
|