@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 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,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
- let cache = {
80
- at: 0,
81
- rutas: opciones.respaldo?.rutas ?? [],
82
- prefijos: opciones.respaldo?.prefijos ?? [],
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
- cache = {
92
- at: Date.now(),
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 listas() {
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 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));
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
- const method = input.method.toUpperCase();
134
- 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
+ }
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 (!secreto && !avisado) {
372
+ if (!avisado) {
297
373
  avisado = true;
298
- void runtime.sinSecreto().then((faltan) => {
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
- let cache = {
108
- at: 0,
109
- rutas: opciones.respaldo?.rutas ?? [],
110
- prefijos: opciones.respaldo?.prefijos ?? [],
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
- cache = {
120
- at: Date.now(),
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 listas() {
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 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));
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
- const method = input.method.toUpperCase();
162
- 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
+ }
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 (!secreto && !avisado) {
397
+ if (!avisado) {
322
398
  avisado = true;
323
- void runtime.sinSecreto().then((faltan) => {
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
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  crearPeaje
3
- } from "./chunk-IU5YMR4J.js";
3
+ } from "./chunk-QHUY5N5S.js";
4
4
 
5
5
  // src/express.ts
6
6
  import { Readable } from "stream";
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
- let cache = {
107
- at: 0,
108
- rutas: opciones.respaldo?.rutas ?? [],
109
- prefijos: opciones.respaldo?.prefijos ?? [],
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
- 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
- };
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 listas() {
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 listas()).patrones.filter((p) => patronReenviable(p.path));
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
- const method = input.method.toUpperCase();
161
- const patron = secreto !== null && patrones.some((p) => p.method.toUpperCase() === method && patronReenviable(p.path) && coincidePatron(p.path, path));
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 (!secreto && !avisado) {
396
+ if (!avisado) {
321
397
  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
- });
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
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  peajeFetch
3
- } from "./chunk-IU5YMR4J.js";
3
+ } from "./chunk-QHUY5N5S.js";
4
4
 
5
5
  // src/hono.ts
6
6
  function peajeHono(opciones) {
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
- let cache = {
111
- at: 0,
112
- rutas: opciones.respaldo?.rutas ?? [],
113
- prefijos: opciones.respaldo?.prefijos ?? [],
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
- cache = {
123
- at: Date.now(),
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 listas() {
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 listas()).patrones.filter((p) => patronReenviable(p.path));
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
- const method = input.method.toUpperCase();
165
- const patron = secreto !== null && patrones.some((p) => p.method.toUpperCase() === method && patronReenviable(p.path) && coincidePatron(p.path, path));
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 (!secreto && !avisado) {
403
+ if (!avisado) {
328
404
  avisado = true;
329
- void runtime.sinSecreto().then((faltan) => {
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
@@ -5,7 +5,7 @@ import {
5
5
  crearPeaje,
6
6
  peajeFetch,
7
7
  src_default
8
- } from "./chunk-IU5YMR4J.js";
8
+ } from "./chunk-QHUY5N5S.js";
9
9
  export {
10
10
  GATEWAY_POR_DEFECTO,
11
11
  _reiniciarAviso,
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
- let cache = {
107
- at: 0,
108
- rutas: opciones.respaldo?.rutas ?? [],
109
- prefijos: opciones.respaldo?.prefijos ?? [],
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
- 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
- };
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 listas() {
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 listas()).patrones.filter((p) => patronReenviable(p.path));
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
- const method = input.method.toUpperCase();
161
- const patron = secreto !== null && patrones.some((p) => p.method.toUpperCase() === method && patronReenviable(p.path) && coincidePatron(p.path, path));
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 (!secreto && !avisado) {
396
+ if (!avisado) {
321
397
  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
- });
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-IU5YMR4J.js";
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.1.2",
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",