@peaje/proxy 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vercel.cjs CHANGED
@@ -52,7 +52,35 @@ 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";
83
+ var PEAJE_HOST_HEADER = "x-peaje-forwarded-host";
56
84
 
57
85
  // ../shared/src/kit/proxy-runtime.ts
58
86
  var PEAJE_ORIGIN_HEADER = "x-peaje-origin";
@@ -62,6 +90,38 @@ function iguales(a, b) {
62
90
  for (let i = 0; i < a.length; i++) d |= a.charCodeAt(i) ^ b.charCodeAt(i);
63
91
  return d === 0;
64
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
+ }
65
125
  function coincidePatron(patron, pathname) {
66
126
  const p = patron.split("/").filter(Boolean);
67
127
  const parts = segmentosCanonicos(pathname);
@@ -102,43 +162,43 @@ function crearProxyRuntime(opciones) {
102
162
  const base = opciones.base.replace(/\/+$/, "");
103
163
  const ttl = opciones.ttlMs ?? 6e4;
104
164
  const fetchImpl = opciones.fetchImpl ?? fetch;
105
- let cache = {
106
- at: 0,
107
- rutas: opciones.respaldo?.rutas ?? [],
108
- prefijos: opciones.respaldo?.prefijos ?? [],
109
- patrones: opciones.respaldo?.patrones ?? []
110
- };
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: [] };
111
169
  let refrescando = null;
112
170
  async function refrescar() {
113
171
  try {
114
172
  const res = await fetchImpl(`${base}/kit/manifest.json`, { signal: AbortSignal.timeout(5e3) });
115
173
  if (!res.ok) throw new Error(String(res.status));
116
174
  const m = await res.json();
117
- cache = {
118
- at: Date.now(),
119
- rutas: Array.isArray(m.rutas) ? m.rutas : cache.rutas,
120
- prefijos: Array.isArray(m.prefijos) ? m.prefijos : cache.prefijos,
121
- patrones: Array.isArray(m.patrones) ? m.patrones : cache.patrones
122
- };
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 };
123
177
  } catch {
124
178
  cache = { ...cache, at: Date.now() };
125
179
  } finally {
126
180
  refrescando = null;
127
181
  }
128
182
  }
129
- async function listas() {
183
+ async function patrones() {
130
184
  const vencido = Date.now() - cache.at >= ttl;
131
185
  if (vencido) {
132
186
  refrescando ??= refrescar();
133
187
  if (cache.at === 0) await refrescando;
134
188
  }
135
- return cache;
189
+ return cache.patrones;
136
190
  }
137
191
  return {
192
+ /** Los prefijos de API efectivos, normalizados (para los avisos). */
193
+ prefijosApi,
138
194
  /** Rutas de API con precio que este sitio no puede cobrar por falta de secreto. */
139
195
  async sinSecreto() {
140
196
  if (opciones.secreto) return [];
141
- 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));
142
202
  },
143
203
  /** Fuerza la carga del manifiesto (útil al arrancar o en tests). */
144
204
  cargar: () => refrescando ??= refrescar(),
@@ -150,14 +210,18 @@ function crearProxyRuntime(opciones) {
150
210
  const secreto = opciones.secreto || null;
151
211
  const prueba = input.headers.get(PEAJE_ORIGIN_HEADER);
152
212
  if (secreto && prueba && iguales(prueba, secreto)) return null;
153
- const { rutas, prefijos, patrones } = await listas();
154
213
  const path = input.pathname;
155
214
  const canon = pathCanonico(path).toLowerCase();
156
215
  const esFetchDePeaje = input.headers.get(PEAJE_FETCH_HEADER) !== null;
157
216
  const fija = (rutas.includes(path) || rutas.some((r) => r.toLowerCase() === canon)) && !(canon === "/llms.txt" && esFetchDePeaje);
158
217
  const prefijo = prefijos.some((p) => path.startsWith(p) || canon.startsWith(p.toLowerCase()));
159
- const method = input.method.toUpperCase();
160
- 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
+ }
161
225
  if (!fija && !prefijo && !patron) return null;
162
226
  return { destino: `${base}${path}${input.search ?? ""}` };
163
227
  }
@@ -302,29 +366,36 @@ function baseDe({ slug, gateway }) {
302
366
  }
303
367
  var SALTO = ["host", "connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "te", "trailer", "content-length"];
304
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
+ }
305
387
  function crearPeaje(opciones) {
306
388
  if (!opciones?.slug) throw new Error("@peaje/proxy: `slug` is required (the business slug from your Peaje dashboard).");
307
389
  const base = baseDe(opciones);
308
390
  const secreto = opciones.secret ?? env("PEAJE_ORIGIN_SECRET") ?? null;
309
391
  const fetchImpl = opciones.fetch ?? ((...a) => fetch(...a));
310
- const runtime = crearProxyRuntime({
311
- base,
312
- secreto,
313
- fetchImpl,
314
- respaldo: { rutas: RUTAS_PROXY.map((x) => x.path), prefijos: RUTAS_DINAMICAS.map((x) => x.prefijo), patrones: [] }
315
- });
392
+ const runtime = crearProxyRuntime({ base, secreto, fetchImpl, prefijosApi: opciones.apiPrefixes ?? PREFIJOS_API_POR_DEFECTO });
316
393
  return {
317
394
  base,
318
395
  async decidir({ method, url, headers }) {
319
- if (!secreto && !avisado) {
396
+ if (!avisado) {
320
397
  avisado = true;
321
- void runtime.sinSecreto().then((faltan) => {
322
- if (faltan.length > 0) {
323
- console.warn(
324
- `[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.`
325
- );
326
- }
327
- });
398
+ avisar(runtime, Boolean(secreto));
328
399
  }
329
400
  const d = await runtime.decidir({ method, pathname: url.pathname, search: url.search, headers });
330
401
  return d?.destino ?? null;
@@ -332,8 +403,11 @@ function crearPeaje(opciones) {
332
403
  async reenviar(destino, { method, url, headers, body }) {
333
404
  const salida = new Headers(headers);
334
405
  for (const h of SALTO) salida.delete(h);
406
+ for (const h of HEADERS_NO_REENVIADOS) salida.delete(h);
335
407
  const primero = (h) => headers.get(h)?.split(",")[0]?.trim() || null;
336
- salida.set("x-forwarded-host", primero("x-forwarded-host") ?? url.host);
408
+ const host = primero("x-forwarded-host") ?? url.host;
409
+ salida.set("x-forwarded-host", host);
410
+ salida.set(PEAJE_HOST_HEADER, host);
337
411
  salida.set("x-forwarded-proto", primero("x-forwarded-proto") ?? url.protocol.replace(":", ""));
338
412
  const sinBody = method === "GET" || method === "HEAD";
339
413
  const res = await fetchImpl(destino, { method, headers: salida, body: sinBody ? null : body, redirect: "manual" });
@@ -368,7 +442,9 @@ function peajeVercel(opciones, siguiente) {
368
442
  const destino = await peaje.decidir(entrada);
369
443
  if (destino) {
370
444
  const headers = new Headers(request.headers);
445
+ for (const h of HEADERS_NO_REENVIADOS) headers.delete(h);
371
446
  headers.set("x-forwarded-host", url.host);
447
+ headers.set(PEAJE_HOST_HEADER, url.host);
372
448
  headers.set("x-forwarded-proto", url.protocol.replace(":", ""));
373
449
  return respuestaMiddleware({ "x-middleware-rewrite": destino }, headers);
374
450
  }
package/dist/vercel.d.cts CHANGED
@@ -1,23 +1,5 @@
1
1
  import { OpcionesPeaje } from './index.cjs';
2
2
 
3
- /**
4
- * `@peaje/proxy/vercel`: Routing Middleware de Vercel para proyectos que no
5
- * son Next (Vite, SPA estática, Remix, SvelteKit en Vercel). Va en
6
- * `middleware.ts` en la raíz, al lado de `package.json`:
7
- *
8
- * import peaje from '@peaje/proxy/vercel'
9
- * export default peaje({ slug: 'mi-negocio' })
10
- * export const config = { matcher: ['/((?!assets/|favicon.ico).*)'] }
11
- *
12
- * Responde con el mismo contrato de headers que `rewrite()` y `next()` de
13
- * `@vercel/functions` (y antes `@vercel/edge`, y `NextResponse` en Next):
14
- * `x-middleware-rewrite` con el destino, `x-middleware-next` para seguir, y
15
- * `x-middleware-override-headers` + `x-middleware-request-<nombre>` para
16
- * cambiar los headers de la request. Así no hace falta instalar nada más: la
17
- * request no pasa por la función, Vercel la reescribe al gateway (método,
18
- * body y query intactos). Con un middleware propio: `peaje({ slug }, tuyo)`.
19
- */
20
-
21
3
  type Contexto = {
22
4
  waitUntil(promesa: Promise<unknown>): void;
23
5
  };
package/dist/vercel.d.ts CHANGED
@@ -1,23 +1,5 @@
1
1
  import { OpcionesPeaje } from './index.js';
2
2
 
3
- /**
4
- * `@peaje/proxy/vercel`: Routing Middleware de Vercel para proyectos que no
5
- * son Next (Vite, SPA estática, Remix, SvelteKit en Vercel). Va en
6
- * `middleware.ts` en la raíz, al lado de `package.json`:
7
- *
8
- * import peaje from '@peaje/proxy/vercel'
9
- * export default peaje({ slug: 'mi-negocio' })
10
- * export const config = { matcher: ['/((?!assets/|favicon.ico).*)'] }
11
- *
12
- * Responde con el mismo contrato de headers que `rewrite()` y `next()` de
13
- * `@vercel/functions` (y antes `@vercel/edge`, y `NextResponse` en Next):
14
- * `x-middleware-rewrite` con el destino, `x-middleware-next` para seguir, y
15
- * `x-middleware-override-headers` + `x-middleware-request-<nombre>` para
16
- * cambiar los headers de la request. Así no hace falta instalar nada más: la
17
- * request no pasa por la función, Vercel la reescribe al gateway (método,
18
- * body y query intactos). Con un middleware propio: `peaje({ slug }, tuyo)`.
19
- */
20
-
21
3
  type Contexto = {
22
4
  waitUntil(promesa: Promise<unknown>): void;
23
5
  };
package/dist/vercel.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import {
2
+ HEADERS_NO_REENVIADOS,
3
+ PEAJE_HOST_HEADER,
2
4
  crearPeaje
3
- } from "./chunk-6ADZXHC7.js";
5
+ } from "./chunk-QHUY5N5S.js";
4
6
 
5
7
  // src/vercel.ts
6
8
  function respuestaMiddleware(control, requestHeaders) {
@@ -23,7 +25,9 @@ function peajeVercel(opciones, siguiente) {
23
25
  const destino = await peaje.decidir(entrada);
24
26
  if (destino) {
25
27
  const headers = new Headers(request.headers);
28
+ for (const h of HEADERS_NO_REENVIADOS) headers.delete(h);
26
29
  headers.set("x-forwarded-host", url.host);
30
+ headers.set(PEAJE_HOST_HEADER, url.host);
27
31
  headers.set("x-forwarded-proto", url.protocol.replace(":", ""));
28
32
  return respuestaMiddleware({ "x-middleware-rewrite": destino }, headers);
29
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peaje/proxy",
3
- "version": "0.1.1",
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",