@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 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 query, and carries `x-forwarded-host` and `x-forwarded-proto` so the 402 challenge names your domain (strict x402 clients abort when it does not). If the gateway is unreachable, the catalog paths are still forwarded from a built-in list; priced API routes resume when the manifest loads.
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
- let cache = {
79
- at: 0,
80
- rutas: opciones.respaldo?.rutas ?? [],
81
- prefijos: opciones.respaldo?.prefijos ?? [],
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
- cache = {
91
- at: Date.now(),
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 listas() {
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 listas()).patrones.filter((p) => patronReenviable(p.path));
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
- const method = input.method.toUpperCase();
133
- const patron = secreto !== null && patrones.some((p) => p.method.toUpperCase() === method && patronReenviable(p.path) && coincidePatron(p.path, path));
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 (!secreto && !avisado) {
372
+ if (!avisado) {
296
373
  avisado = true;
297
- void runtime.sinSecreto().then((faltan) => {
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
- salida.set("x-forwarded-host", primero("x-forwarded-host") ?? url.host);
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
- let cache = {
107
- at: 0,
108
- rutas: opciones.respaldo?.rutas ?? [],
109
- prefijos: opciones.respaldo?.prefijos ?? [],
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
- cache = {
119
- at: Date.now(),
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 listas() {
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 listas()).patrones.filter((p) => patronReenviable(p.path));
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
- const method = input.method.toUpperCase();
161
- const patron = secreto !== null && patrones.some((p) => p.method.toUpperCase() === method && patronReenviable(p.path) && coincidePatron(p.path, path));
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 (!secreto && !avisado) {
397
+ if (!avisado) {
321
398
  avisado = true;
322
- void runtime.sinSecreto().then((faltan) => {
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
- salida.set("x-forwarded-host", primero("x-forwarded-host") ?? url.host);
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" });
package/dist/express.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  crearPeaje
3
- } from "./chunk-6ADZXHC7.js";
3
+ } from "./chunk-QHUY5N5S.js";
4
4
 
5
5
  // src/express.ts
6
6
  import { Readable } from "stream";