@peaje/next 0.2.3 → 0.3.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
@@ -50,8 +50,20 @@ Set `PEAJE_ORIGIN_SECRET` in your hosting environment variables (copy it from th
50
50
 
51
51
  The rewrites above are fixed at build time, so they cannot know about an API route you add to your dashboard tomorrow. `peajeProxy` fetches `/{slug}/kit/manifest.json` from the gateway, keeps it for 60 seconds, and rewrites any request that matches a priced route (`/api/forecast`, `/api/history/:city`, `/api/batch/*`) to the gateway, which answers 402 or, once paid, forwards to your origin. Catch-all patterns (`/*`, `/:id` at the root) are not forwarded from your domain: they would send the whole site to the gateway.
52
52
 
53
+ What the manifest can and cannot change on your domain (since 0.3.0):
54
+
55
+ - The fixed paths (`/llms.txt`, `/openapi.json`, `/mcp`, `/.well-known/*`, `/developers`, `/docs`, ...) and the `/r/` and `/checkout_sessions/` prefixes come only from the list bundled in this package. Paths the manifest adds are ignored.
56
+ - Priced API routes from the manifest are forwarded only under `apiPrefixes`, which you set in code. The default is `['/api/', '/v1/']`. A route the dashboard prices elsewhere (say `GET /weather/:city`) keeps charging at the gateway URL, and the proxy logs one warning naming it; to charge it on your domain, pass `peajeProxy({ slug, apiPrefixes: ['/api/', '/v1/', '/weather/'] })`.
57
+ - Never forwarded, whatever the manifest or `apiPrefixes` say: the root `/` and reserved app paths (`/login`, `/logout`, `/signin`, `/signup`, `/register`, `/auth`, `/oauth`, `/session`, `/account`, `/admin`, `/dashboard`, `/api/auth` for NextAuth, `/_next`, `/_vercel`, `/static`, `/assets`, `/favicon.ico`, `/robots.txt`, `/sitemap.xml`), and everything under them.
58
+
59
+ So a gateway bug or a compromised gateway cannot take over a page of your site. Upgrading from 0.2.x: if your priced routes live outside `/api/` and `/v1/`, add their prefix to `apiPrefixes` before you deploy 0.3.0.
60
+
61
+ Cookies: the requests the proxy forwards to the gateway leave out the `cookie` header, so your visitors' session cookies never leave your domain. `authorization` is forwarded (it carries the MPP credential). If the handler of a priced route reads a cookie, it no longer gets one through Peaje; agents do not send your site's cookies anyway. The build-time rewrites from `withPeaje` cannot drop headers; with `peajeProxy` in place (and its matcher covering those paths) it handles them first.
62
+
53
63
  With your own middleware: `export default peajeProxy({ slug }, myMiddleware)`. Peaje runs first; when the request is not one of its routes, yours runs.
54
64
 
65
+ Options: `slug` (required), `gateway`, `secret` (default `process.env.PEAJE_ORIGIN_SECRET`) and `apiPrefixes` (default `['/api/', '/v1/']`).
66
+
55
67
  ## 2. Head
56
68
 
57
69
  ```tsx
@@ -62,7 +74,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
62
74
  return (
63
75
  <html lang="en">
64
76
  <head>
65
- <PeajeHead slug="cafe-andino" />
77
+ <PeajeHead slug="cafe-andino" originHost="cafeandino.com" />
66
78
  </head>
67
79
  <body>{children}</body>
68
80
  </html>
@@ -72,7 +84,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
72
84
 
73
85
  The offers in the JSON-LD come from your gateway (`/discovery/resources`) and are revalidated hourly. If the gateway is unreachable, the component renders the block without offers and never throws: a Peaje outage cannot break your build or your pages.
74
86
 
75
- Props: `slug` (required), `gateway`, `originHost` and `name`. Without `originHost` the component reads the request host through `next/headers`, which makes the layout dynamic; pass `originHost="cafeandino.com"` to keep static rendering.
87
+ Props: `slug` (required), `gateway`, `originHost` and `name`. Pass `originHost="cafeandino.com"`: without it the component reads the request host through `next/headers`, which makes every page under the layout dynamic. `npx @peaje/cli@1 init` writes it for you.
76
88
 
77
89
  ## 3. Footer
78
90
 
@@ -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
  function reglasNext(base) {
@@ -100,6 +127,38 @@ function iguales(a, b) {
100
127
  for (let i = 0; i < a.length; i++) d |= a.charCodeAt(i) ^ b.charCodeAt(i);
101
128
  return d === 0;
102
129
  }
130
+ var HEADERS_NO_REENVIADOS = ["cookie"];
131
+ function esReservado(path) {
132
+ const canon = pathCanonico(path).toLowerCase();
133
+ return PATHS_RESERVADOS.some((r) => r === "/" ? canon === "/" : canon === r || canon.startsWith(`${r}/`));
134
+ }
135
+ function prefijosApiValidos(prefijos = PREFIJOS_API_POR_DEFECTO) {
136
+ const out = [];
137
+ for (const crudo of prefijos) {
138
+ const segs = segmentosCanonicos(String(crudo ?? "")).map((s) => s.toLowerCase());
139
+ if (segs.length === 0) continue;
140
+ const path = `/${segs.join("/")}`;
141
+ if (esReservado(path)) continue;
142
+ if (!out.includes(`${path}/`)) out.push(`${path}/`);
143
+ }
144
+ return out;
145
+ }
146
+ function patronPermitido(patron, prefijos) {
147
+ if (!patronReenviable(patron)) return false;
148
+ const segs = patron.split("/").filter(Boolean);
149
+ const literal = (s) => s !== "*" && !s.startsWith(":");
150
+ const bajoPrefijo = prefijos.some((pre) => {
151
+ const ps = pre.split("/").filter(Boolean);
152
+ return segs.length > ps.length && ps.every((s, i) => literal(segs[i]) && segs[i].toLowerCase() === s);
153
+ });
154
+ if (!bajoPrefijo) return false;
155
+ const literales = [];
156
+ for (const s of segs) {
157
+ if (!literal(s)) break;
158
+ literales.push(s);
159
+ }
160
+ return !esReservado(`/${literales.join("/")}`);
161
+ }
103
162
  function coincidePatron(patron, pathname) {
104
163
  const p = patron.split("/").filter(Boolean);
105
164
  const parts = segmentosCanonicos(pathname);
@@ -140,43 +199,43 @@ function crearProxyRuntime(opciones) {
140
199
  const base = opciones.base.replace(/\/+$/, "");
141
200
  const ttl = opciones.ttlMs ?? 6e4;
142
201
  const fetchImpl = opciones.fetchImpl ?? fetch;
143
- let cache = {
144
- at: 0,
145
- rutas: opciones.respaldo?.rutas ?? [],
146
- prefijos: opciones.respaldo?.prefijos ?? [],
147
- patrones: opciones.respaldo?.patrones ?? []
148
- };
202
+ const rutas = [...opciones.fijas?.rutas ?? RUTAS_PROXY.map((r) => r.path)];
203
+ const prefijos = [...opciones.fijas?.prefijos ?? RUTAS_DINAMICAS.map((r) => r.prefijo)];
204
+ const prefijosApi = prefijosApiValidos(opciones.prefijosApi ?? PREFIJOS_API_POR_DEFECTO);
205
+ let cache = { at: 0, patrones: [] };
149
206
  let refrescando = null;
150
207
  async function refrescar() {
151
208
  try {
152
209
  const res = await fetchImpl(`${base}/kit/manifest.json`, { signal: AbortSignal.timeout(5e3) });
153
210
  if (!res.ok) throw new Error(String(res.status));
154
211
  const m = await res.json();
155
- cache = {
156
- at: Date.now(),
157
- rutas: Array.isArray(m.rutas) ? m.rutas : cache.rutas,
158
- prefijos: Array.isArray(m.prefijos) ? m.prefijos : cache.prefijos,
159
- patrones: Array.isArray(m.patrones) ? m.patrones : cache.patrones
160
- };
212
+ const patrones2 = Array.isArray(m.patrones) ? m.patrones.filter((p) => typeof p?.method === "string" && typeof p?.path === "string") : cache.patrones;
213
+ cache = { at: Date.now(), patrones: patrones2 };
161
214
  } catch {
162
215
  cache = { ...cache, at: Date.now() };
163
216
  } finally {
164
217
  refrescando = null;
165
218
  }
166
219
  }
167
- async function listas() {
220
+ async function patrones() {
168
221
  const vencido = Date.now() - cache.at >= ttl;
169
222
  if (vencido) {
170
223
  refrescando ??= refrescar();
171
224
  if (cache.at === 0) await refrescando;
172
225
  }
173
- return cache;
226
+ return cache.patrones;
174
227
  }
175
228
  return {
229
+ /** Los prefijos de API efectivos, normalizados (para los avisos). */
230
+ prefijosApi,
176
231
  /** Rutas de API con precio que este sitio no puede cobrar por falta de secreto. */
177
232
  async sinSecreto() {
178
233
  if (opciones.secreto) return [];
179
- return (await listas()).patrones.filter((p) => patronReenviable(p.path));
234
+ return (await patrones()).filter((p) => patronPermitido(p.path, prefijosApi));
235
+ },
236
+ /** Rutas de API con precio que no se reenvían por caer fuera de `prefijosApi` o en un path reservado. */
237
+ async fueraDePrefijos() {
238
+ return (await patrones()).filter((p) => patronReenviable(p.path) && !patronPermitido(p.path, prefijosApi));
180
239
  },
181
240
  /** Fuerza la carga del manifiesto (útil al arrancar o en tests). */
182
241
  cargar: () => refrescando ??= refrescar(),
@@ -188,14 +247,18 @@ function crearProxyRuntime(opciones) {
188
247
  const secreto = opciones.secreto || null;
189
248
  const prueba = input.headers.get(PEAJE_ORIGIN_HEADER);
190
249
  if (secreto && prueba && iguales(prueba, secreto)) return null;
191
- const { rutas, prefijos, patrones } = await listas();
192
250
  const path = input.pathname;
193
251
  const canon = pathCanonico(path).toLowerCase();
194
252
  const esFetchDePeaje = input.headers.get(PEAJE_FETCH_HEADER) !== null;
195
253
  const fija = (rutas.includes(path) || rutas.some((r) => r.toLowerCase() === canon)) && !(canon === "/llms.txt" && esFetchDePeaje);
196
254
  const prefijo = prefijos.some((p) => path.startsWith(p) || canon.startsWith(p.toLowerCase()));
197
- const method = input.method.toUpperCase();
198
- const patron = secreto !== null && patrones.some((p) => p.method.toUpperCase() === method && patronReenviable(p.path) && coincidePatron(p.path, path));
255
+ let patron = false;
256
+ if (!fija && !prefijo && secreto !== null && !esReservado(path) && prefijosApi.some((pre) => canon.startsWith(pre))) {
257
+ const method = input.method.toUpperCase();
258
+ patron = (await patrones()).some(
259
+ (p) => p.method.toUpperCase() === method && patronPermitido(p.path, prefijosApi) && coincidePatron(p.path, path)
260
+ );
261
+ }
199
262
  if (!fija && !prefijo && !patron) return null;
200
263
  return { destino: `${base}${path}${input.search ?? ""}` };
201
264
  }
@@ -363,12 +426,12 @@ function withPeaje(nextConfig, opciones) {
363
426
  var src_default = withPeaje;
364
427
 
365
428
  export {
366
- RUTAS_PROXY,
367
- RUTAS_DINAMICAS,
429
+ PREFIJOS_API_POR_DEFECTO,
368
430
  PEAJE_HOST_HEADER,
369
431
  jsonParaScript,
370
432
  jsonLdWebApi,
371
433
  LINKS_HEAD,
434
+ HEADERS_NO_REENVIADOS,
372
435
  crearProxyRuntime,
373
436
  GATEWAY_POR_DEFECTO,
374
437
  baseDe,
package/dist/head.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  baseDe,
4
4
  jsonLdWebApi,
5
5
  jsonParaScript
6
- } from "./chunk-H33X23BP.js";
6
+ } from "./chunk-6R2LY77J.js";
7
7
 
8
8
  // src/head.tsx
9
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  fusionarRewrites,
5
5
  src_default,
6
6
  withPeaje
7
- } from "./chunk-H33X23BP.js";
7
+ } from "./chunk-6R2LY77J.js";
8
8
  export {
9
9
  GATEWAY_POR_DEFECTO,
10
10
  baseDe,
package/dist/proxy.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
  }
@@ -304,35 +367,44 @@ function baseDe({ slug, gateway }) {
304
367
 
305
368
  // src/proxy.ts
306
369
  var runtimes = /* @__PURE__ */ new Map();
307
- function runtimeDe(base, secreto) {
308
- const clave = `${base}|${secreto ? "con" : "sin"}`;
370
+ function runtimeDe(base, secreto, prefijosApi) {
371
+ const clave = `${base}|${secreto ? "con" : "sin"}|${prefijosApi.join(",")}`;
309
372
  let r = runtimes.get(clave);
310
373
  if (!r) {
311
- r = crearProxyRuntime({
312
- base,
313
- secreto,
314
- respaldo: { rutas: RUTAS_PROXY.map((x) => x.path), prefijos: RUTAS_DINAMICAS.map((x) => x.prefijo), patrones: [] }
315
- });
374
+ r = crearProxyRuntime({ base, secreto, prefijosApi });
316
375
  runtimes.set(clave, r);
317
376
  }
318
377
  return r;
319
378
  }
320
379
  var avisado = false;
380
+ function avisar(runtime, conSecreto) {
381
+ if (!conSecreto) {
382
+ void runtime.sinSecreto().then((faltan) => {
383
+ if (faltan.length > 0) {
384
+ console.warn(
385
+ `[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.`
386
+ );
387
+ }
388
+ });
389
+ }
390
+ void runtime.fueraDePrefijos().then((fuera) => {
391
+ if (fuera.length > 0) {
392
+ console.warn(
393
+ `[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.`
394
+ );
395
+ }
396
+ });
397
+ }
321
398
  function peajeProxy(opciones, siguiente) {
322
399
  if (!opciones?.slug) throw new Error("peajeProxy: `slug` is required (the business slug from your Peaje dashboard).");
323
400
  const base = baseDe(opciones);
324
401
  const secreto = opciones.secret ?? process.env.PEAJE_ORIGIN_SECRET ?? null;
402
+ const prefijosApi = opciones.apiPrefixes ?? PREFIJOS_API_POR_DEFECTO;
325
403
  return async (req, ev) => {
326
- const runtime = runtimeDe(base, secreto);
327
- if (!secreto && !avisado) {
404
+ const runtime = runtimeDe(base, secreto, prefijosApi);
405
+ if (!avisado) {
328
406
  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
- });
407
+ avisar(runtime, Boolean(secreto));
336
408
  }
337
409
  const decision = await runtime.decidir({
338
410
  method: req.method,
@@ -342,6 +414,7 @@ function peajeProxy(opciones, siguiente) {
342
414
  });
343
415
  if (decision) {
344
416
  const headers = new Headers(req.headers);
417
+ for (const h of HEADERS_NO_REENVIADOS) headers.delete(h);
345
418
  headers.set("x-forwarded-host", req.nextUrl.host);
346
419
  headers.set(PEAJE_HOST_HEADER, req.nextUrl.host);
347
420
  headers.set("x-forwarded-proto", req.nextUrl.protocol.replace(":", ""));
package/dist/proxy.d.cts CHANGED
@@ -5,6 +5,13 @@ type Middleware = (req: NextRequest, ev: NextFetchEvent) => Response | undefined
5
5
  type OpcionesProxy = OpcionesPeaje & {
6
6
  /** Por defecto `process.env.PEAJE_ORIGIN_SECRET`. */
7
7
  secret?: string;
8
+ /**
9
+ * Prefijos bajo los que viven tus rutas de API con precio. Una ruta del
10
+ * dashboard fuera de ellos cobra solo en la URL del gateway, no en tu
11
+ * dominio. Por defecto `['/api/', '/v1/']`. La raíz y los paths reservados
12
+ * (`/login`, `/auth`, `/api/auth`, `/_next`...) nunca se aceptan.
13
+ */
14
+ apiPrefixes?: string[];
8
15
  };
9
16
  declare function peajeProxy(opciones: OpcionesProxy, siguiente?: Middleware): Middleware;
10
17
 
package/dist/proxy.d.ts CHANGED
@@ -5,6 +5,13 @@ type Middleware = (req: NextRequest, ev: NextFetchEvent) => Response | undefined
5
5
  type OpcionesProxy = OpcionesPeaje & {
6
6
  /** Por defecto `process.env.PEAJE_ORIGIN_SECRET`. */
7
7
  secret?: string;
8
+ /**
9
+ * Prefijos bajo los que viven tus rutas de API con precio. Una ruta del
10
+ * dashboard fuera de ellos cobra solo en la URL del gateway, no en tu
11
+ * dominio. Por defecto `['/api/', '/v1/']`. La raíz y los paths reservados
12
+ * (`/login`, `/auth`, `/api/auth`, `/_next`...) nunca se aceptan.
13
+ */
14
+ apiPrefixes?: string[];
8
15
  };
9
16
  declare function peajeProxy(opciones: OpcionesProxy, siguiente?: Middleware): Middleware;
10
17
 
package/dist/proxy.js CHANGED
@@ -1,43 +1,52 @@
1
1
  import {
2
+ HEADERS_NO_REENVIADOS,
2
3
  PEAJE_HOST_HEADER,
3
- RUTAS_DINAMICAS,
4
- RUTAS_PROXY,
4
+ PREFIJOS_API_POR_DEFECTO,
5
5
  baseDe,
6
6
  crearProxyRuntime
7
- } from "./chunk-H33X23BP.js";
7
+ } from "./chunk-6R2LY77J.js";
8
8
 
9
9
  // src/proxy.ts
10
10
  import { NextResponse } from "next/server";
11
11
  var runtimes = /* @__PURE__ */ new Map();
12
- function runtimeDe(base, secreto) {
13
- const clave = `${base}|${secreto ? "con" : "sin"}`;
12
+ function runtimeDe(base, secreto, prefijosApi) {
13
+ const clave = `${base}|${secreto ? "con" : "sin"}|${prefijosApi.join(",")}`;
14
14
  let r = runtimes.get(clave);
15
15
  if (!r) {
16
- r = crearProxyRuntime({
17
- base,
18
- secreto,
19
- respaldo: { rutas: RUTAS_PROXY.map((x) => x.path), prefijos: RUTAS_DINAMICAS.map((x) => x.prefijo), patrones: [] }
20
- });
16
+ r = crearProxyRuntime({ base, secreto, prefijosApi });
21
17
  runtimes.set(clave, r);
22
18
  }
23
19
  return r;
24
20
  }
25
21
  var avisado = false;
22
+ function avisar(runtime, conSecreto) {
23
+ if (!conSecreto) {
24
+ void runtime.sinSecreto().then((faltan) => {
25
+ if (faltan.length > 0) {
26
+ console.warn(
27
+ `[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.`
28
+ );
29
+ }
30
+ });
31
+ }
32
+ void runtime.fueraDePrefijos().then((fuera) => {
33
+ if (fuera.length > 0) {
34
+ console.warn(
35
+ `[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.`
36
+ );
37
+ }
38
+ });
39
+ }
26
40
  function peajeProxy(opciones, siguiente) {
27
41
  if (!opciones?.slug) throw new Error("peajeProxy: `slug` is required (the business slug from your Peaje dashboard).");
28
42
  const base = baseDe(opciones);
29
43
  const secreto = opciones.secret ?? process.env.PEAJE_ORIGIN_SECRET ?? null;
44
+ const prefijosApi = opciones.apiPrefixes ?? PREFIJOS_API_POR_DEFECTO;
30
45
  return async (req, ev) => {
31
- const runtime = runtimeDe(base, secreto);
32
- if (!secreto && !avisado) {
46
+ const runtime = runtimeDe(base, secreto, prefijosApi);
47
+ if (!avisado) {
33
48
  avisado = true;
34
- void runtime.sinSecreto().then((faltan) => {
35
- if (faltan.length > 0) {
36
- console.warn(
37
- `[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.`
38
- );
39
- }
40
- });
49
+ avisar(runtime, Boolean(secreto));
41
50
  }
42
51
  const decision = await runtime.decidir({
43
52
  method: req.method,
@@ -47,6 +56,7 @@ function peajeProxy(opciones, siguiente) {
47
56
  });
48
57
  if (decision) {
49
58
  const headers = new Headers(req.headers);
59
+ for (const h of HEADERS_NO_REENVIADOS) headers.delete(h);
50
60
  headers.set("x-forwarded-host", req.nextUrl.host);
51
61
  headers.set(PEAJE_HOST_HEADER, req.nextUrl.host);
52
62
  headers.set("x-forwarded-proto", req.nextUrl.protocol.replace(":", ""));
package/package.json CHANGED
@@ -1,12 +1,20 @@
1
1
  {
2
2
  "name": "@peaje/next",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Peaje for Next.js: runtime proxy, rewrites and the head block that make your site answer 402 (MPP, x402), MCP and OpenAPI on your own domain.",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/sandragcarrillo/peaje.git",
9
+ "directory": "packages/peaje-next"
10
+ },
11
+ "homepage": "https://github.com/sandragcarrillo/peaje/tree/main/packages/peaje-next#readme",
12
+ "bugs": "https://github.com/sandragcarrillo/peaje/issues",
6
13
  "type": "module",
7
14
  "sideEffects": false,
8
15
  "files": [
9
- "dist"
16
+ "dist",
17
+ "LICENSE"
10
18
  ],
11
19
  "main": "./dist/index.cjs",
12
20
  "module": "./dist/index.js",