@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 +21 -0
- package/README.md +14 -2
- package/dist/{chunk-H33X23BP.js → chunk-6R2LY77J.js} +83 -20
- package/dist/head.js +1 -1
- package/dist/index.js +1 -1
- package/dist/proxy.cjs +107 -34
- package/dist/proxy.d.cts +7 -0
- package/dist/proxy.d.ts +7 -0
- package/dist/proxy.js +29 -19
- package/package.json +10 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sandra Carrillo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -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`.
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
package/dist/index.js
CHANGED
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
patrones: opciones.respaldo?.patrones ?? []
|
|
111
|
-
};
|
|
165
|
+
const rutas = [...opciones.fijas?.rutas ?? RUTAS_PROXY.map((r) => r.path)];
|
|
166
|
+
const prefijos = [...opciones.fijas?.prefijos ?? RUTAS_DINAMICAS.map((r) => r.prefijo)];
|
|
167
|
+
const prefijosApi = prefijosApiValidos(opciones.prefijosApi ?? PREFIJOS_API_POR_DEFECTO);
|
|
168
|
+
let cache = { at: 0, patrones: [] };
|
|
112
169
|
let refrescando = null;
|
|
113
170
|
async function refrescar() {
|
|
114
171
|
try {
|
|
115
172
|
const res = await fetchImpl(`${base}/kit/manifest.json`, { signal: AbortSignal.timeout(5e3) });
|
|
116
173
|
if (!res.ok) throw new Error(String(res.status));
|
|
117
174
|
const m = await res.json();
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
rutas: Array.isArray(m.rutas) ? m.rutas : cache.rutas,
|
|
121
|
-
prefijos: Array.isArray(m.prefijos) ? m.prefijos : cache.prefijos,
|
|
122
|
-
patrones: Array.isArray(m.patrones) ? m.patrones : cache.patrones
|
|
123
|
-
};
|
|
175
|
+
const patrones2 = Array.isArray(m.patrones) ? m.patrones.filter((p) => typeof p?.method === "string" && typeof p?.path === "string") : cache.patrones;
|
|
176
|
+
cache = { at: Date.now(), patrones: patrones2 };
|
|
124
177
|
} catch {
|
|
125
178
|
cache = { ...cache, at: Date.now() };
|
|
126
179
|
} finally {
|
|
127
180
|
refrescando = null;
|
|
128
181
|
}
|
|
129
182
|
}
|
|
130
|
-
async function
|
|
183
|
+
async function patrones() {
|
|
131
184
|
const vencido = Date.now() - cache.at >= ttl;
|
|
132
185
|
if (vencido) {
|
|
133
186
|
refrescando ??= refrescar();
|
|
134
187
|
if (cache.at === 0) await refrescando;
|
|
135
188
|
}
|
|
136
|
-
return cache;
|
|
189
|
+
return cache.patrones;
|
|
137
190
|
}
|
|
138
191
|
return {
|
|
192
|
+
/** Los prefijos de API efectivos, normalizados (para los avisos). */
|
|
193
|
+
prefijosApi,
|
|
139
194
|
/** Rutas de API con precio que este sitio no puede cobrar por falta de secreto. */
|
|
140
195
|
async sinSecreto() {
|
|
141
196
|
if (opciones.secreto) return [];
|
|
142
|
-
return (await
|
|
197
|
+
return (await patrones()).filter((p) => patronPermitido(p.path, prefijosApi));
|
|
198
|
+
},
|
|
199
|
+
/** Rutas de API con precio que no se reenvían por caer fuera de `prefijosApi` o en un path reservado. */
|
|
200
|
+
async fueraDePrefijos() {
|
|
201
|
+
return (await patrones()).filter((p) => patronReenviable(p.path) && !patronPermitido(p.path, prefijosApi));
|
|
143
202
|
},
|
|
144
203
|
/** Fuerza la carga del manifiesto (útil al arrancar o en tests). */
|
|
145
204
|
cargar: () => refrescando ??= refrescar(),
|
|
@@ -151,14 +210,18 @@ function crearProxyRuntime(opciones) {
|
|
|
151
210
|
const secreto = opciones.secreto || null;
|
|
152
211
|
const prueba = input.headers.get(PEAJE_ORIGIN_HEADER);
|
|
153
212
|
if (secreto && prueba && iguales(prueba, secreto)) return null;
|
|
154
|
-
const { rutas, prefijos, patrones } = await listas();
|
|
155
213
|
const path = input.pathname;
|
|
156
214
|
const canon = pathCanonico(path).toLowerCase();
|
|
157
215
|
const esFetchDePeaje = input.headers.get(PEAJE_FETCH_HEADER) !== null;
|
|
158
216
|
const fija = (rutas.includes(path) || rutas.some((r) => r.toLowerCase() === canon)) && !(canon === "/llms.txt" && esFetchDePeaje);
|
|
159
217
|
const prefijo = prefijos.some((p) => path.startsWith(p) || canon.startsWith(p.toLowerCase()));
|
|
160
|
-
|
|
161
|
-
|
|
218
|
+
let patron = false;
|
|
219
|
+
if (!fija && !prefijo && secreto !== null && !esReservado(path) && prefijosApi.some((pre) => canon.startsWith(pre))) {
|
|
220
|
+
const method = input.method.toUpperCase();
|
|
221
|
+
patron = (await patrones()).some(
|
|
222
|
+
(p) => p.method.toUpperCase() === method && patronPermitido(p.path, prefijosApi) && coincidePatron(p.path, path)
|
|
223
|
+
);
|
|
224
|
+
}
|
|
162
225
|
if (!fija && !prefijo && !patron) return null;
|
|
163
226
|
return { destino: `${base}${path}${input.search ?? ""}` };
|
|
164
227
|
}
|
|
@@ -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 (!
|
|
404
|
+
const runtime = runtimeDe(base, secreto, prefijosApi);
|
|
405
|
+
if (!avisado) {
|
|
328
406
|
avisado = true;
|
|
329
|
-
|
|
330
|
-
if (faltan.length > 0) {
|
|
331
|
-
console.warn(
|
|
332
|
-
`[peaje] ${faltan.length} priced API route(s) are not charged on this domain because PEAJE_ORIGIN_SECRET is not set: ${faltan.map((p) => `${p.method} ${p.path}`).join(", ")}. Copy it from the Kit page of your Peaje dashboard into your hosting environment variables.`
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
});
|
|
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
|
-
|
|
4
|
-
RUTAS_PROXY,
|
|
4
|
+
PREFIJOS_API_POR_DEFECTO,
|
|
5
5
|
baseDe,
|
|
6
6
|
crearProxyRuntime
|
|
7
|
-
} from "./chunk-
|
|
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 (!
|
|
46
|
+
const runtime = runtimeDe(base, secreto, prefijosApi);
|
|
47
|
+
if (!avisado) {
|
|
33
48
|
avisado = true;
|
|
34
|
-
|
|
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.
|
|
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",
|