@reblu/site-next 1.1.0 → 1.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.
@@ -0,0 +1,83 @@
1
+ // src/route-handlers.ts
2
+ function jsonErrorResponse(status, error, message) {
3
+ return new Response(
4
+ JSON.stringify({ error, message, correlation_id: crypto.randomUUID() }),
5
+ { status, headers: { "Content-Type": "application/json; charset=utf-8" } }
6
+ );
7
+ }
8
+ function createProxyRoute(targetPath, opts = {}) {
9
+ const {
10
+ method = "POST",
11
+ forwardSetCookie = false,
12
+ requireJson = false,
13
+ invalidRequestMessage = "Solicitud no v\xE1lida",
14
+ timeoutMs = 8e3,
15
+ configurationErrorMessage = "Error de configuraci\xF3n del servidor.",
16
+ timeoutMessage = "La solicitud ha superado el tiempo de espera.",
17
+ proxyErrorMessage = "Error de conexi\xF3n con el servidor."
18
+ } = opts;
19
+ return async function handler(request) {
20
+ if (requireJson) {
21
+ const contentType = request.headers.get("content-type") ?? "";
22
+ if (!contentType.includes("application/json")) {
23
+ return jsonErrorResponse(400, "invalid_request", invalidRequestMessage);
24
+ }
25
+ }
26
+ const baseUrl = opts.baseUrl ?? process.env.REBLU_API_BASE_URL ?? "";
27
+ const apiKey = opts.apiKey ?? process.env.REBLU_API_KEY ?? "";
28
+ if (!baseUrl || !apiKey) {
29
+ console.error(
30
+ `[createProxyRoute] Missing configuration for ${targetPath}: ` + `${!baseUrl ? "baseUrl/REBLU_API_BASE_URL " : ""}${!apiKey ? "apiKey/REBLU_API_KEY" : ""}`.trim()
31
+ );
32
+ return jsonErrorResponse(500, "missing_configuration", configurationErrorMessage);
33
+ }
34
+ const forwardedHeaders = {};
35
+ const ua = request.headers.get("user-agent");
36
+ if (ua) forwardedHeaders["user-agent"] = ua;
37
+ const xff = request.headers.get("x-forwarded-for");
38
+ if (xff) forwardedHeaders["x-forwarded-for"] = xff;
39
+ const xri = request.headers.get("x-real-ip");
40
+ if (xri) forwardedHeaders["x-real-ip"] = xri;
41
+ const upperMethod = method.toUpperCase();
42
+ const body = upperMethod === "GET" || upperMethod === "HEAD" ? void 0 : await request.text();
43
+ const search = request.nextUrl?.search ?? "";
44
+ const controller = new AbortController();
45
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
46
+ try {
47
+ const res = await fetch(`${baseUrl}${targetPath}${search}`, {
48
+ method,
49
+ headers: {
50
+ "Content-Type": "application/json",
51
+ "X-Reblu-Api-Key": apiKey,
52
+ ...forwardedHeaders
53
+ },
54
+ ...body ? { body } : {},
55
+ cache: "no-store",
56
+ signal: controller.signal
57
+ });
58
+ const data = await res.text();
59
+ const responseHeaders = new Headers({
60
+ "Content-Type": res.headers.get("Content-Type") ?? "application/json"
61
+ });
62
+ if (forwardSetCookie) {
63
+ for (const cookie of res.headers.getSetCookie()) {
64
+ responseHeaders.append("set-cookie", cookie);
65
+ }
66
+ }
67
+ return new Response(data, { status: res.status, headers: responseHeaders });
68
+ } catch (err) {
69
+ if (err instanceof Error && err.name === "AbortError") {
70
+ console.error(`[createProxyRoute] Timed out forwarding to ${targetPath} after ${timeoutMs}ms`);
71
+ return jsonErrorResponse(502, "upstream_timeout", timeoutMessage);
72
+ }
73
+ console.error(`[createProxyRoute] Error forwarding to ${targetPath}:`, err);
74
+ return jsonErrorResponse(502, "proxy_error", proxyErrorMessage);
75
+ } finally {
76
+ clearTimeout(timer);
77
+ }
78
+ };
79
+ }
80
+ export {
81
+ createProxyRoute
82
+ };
83
+ //# sourceMappingURL=route-handlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/route-handlers.ts"],"sourcesContent":["/**\n * `@reblu/site-next/route-handlers` — server-only, dependency-free Route\n * Handler factory (REB-510 / design doc \"site-auth-package\").\n *\n * This module MUST stay dependency-free (no `@reblu/site-ui`, no React): it is\n * imported directly by `app/api/*` Route Handler files, and its own tsup entry\n * carries NO `'use client'` directive (mirrors the `cache` subpath's R4\n * precedent for the opposite direction — see `tsup.config.ts`).\n *\n * Scope: this factory covers the `/api/site/auth/*` proxy shape specifically\n * (fixed target path, JSON body passthrough, optional `Set-Cookie` forward) —\n * the same shape as every one of demo-site's 8 auth Route Handlers. It is\n * modeled on `apps/demo-site/src/lib/api/proxy.ts` (`proxyToBaseApi`) but is\n * NOT a drop-in replacement for every one of that helper's call sites: it\n * does not support a per-request DYNAMIC `targetPath` (e.g.\n * `api/shop/order/[sessionId]/route.ts`'s `` `/api/site/shop/order/${id}` ``)\n * or an arbitrary extra-`headers` passthrough — both are genuinely out of\n * scope for REB-510 (auth only). demo-site's non-auth proxy routes keep using\n * `proxyToBaseApi` directly; only the 8 auth routes are candidates to adopt\n * this factory.\n *\n * export const POST = createProxyRoute('/api/site/auth/login')\n *\n * Deliberate behavior change vs the original (R6): `proxy.ts` reads\n * `REBLU_API_BASE_URL`/`REBLU_API_KEY` at MODULE LOAD. This factory reads them\n * LAZILY, inside the returned handler, on every request — `opts.baseUrl` /\n * `opts.apiKey` still take priority when provided.\n *\n * Why this factory reads `process.env` at all, unlike `@reblu/site-client`\n * (whose own docblock states \"the SDK never reads `process.env` itself\"):\n * `site-client` is instantiated ONCE by the consuming SITE, which already\n * owns and injects its env at that single call site. `createProxyRoute` is\n * the opposite shape on purpose — a ZERO-CONFIG one-liner meant to be called\n * once per Route Handler FILE (8+ files in a typical SITE), so requiring\n * every file to re-thread `baseUrl`/`apiKey` would defeat the \"one line\"\n * value proposition documented in the README. The env fallback is this\n * factory's deliberate ergonomic default; `opts.baseUrl`/`opts.apiKey` remain\n * available for a SITE that wants explicit, non-`process.env` control.\n *\n * Trust note on header forwarding: `X-Forwarded-For`/`X-Real-IP` are copied\n * from the incoming request VERBATIM (same as the original `proxy.ts`). On\n * Vercel the platform itself sets/overwrites these ahead of the Route\n * Handler, so they're trustworthy there — but this is a distributable SDK a\n * SITE can deploy anywhere. On a host where nothing strips inbound spoofed\n * values, a client can forge the IP base-api rate-limits on. Deploy this\n * factory only behind a platform that normalizes/overwrites these headers,\n * or in front of infra that strips client-supplied ones.\n */\nimport type { NextRequest } from 'next/server'\n\nexport interface CreateProxyRouteOptions {\n /** Overrides `REBLU_API_BASE_URL`. Read lazily per-request when omitted. */\n baseUrl?: string\n /** Overrides `REBLU_API_KEY`. Read lazily per-request when omitted. */\n apiKey?: string\n /** @default 'POST' */\n method?: string\n /** Forward upstream `Set-Cookie` headers to the browser. @default false */\n forwardSetCookie?: boolean\n /**\n * Reject non-`application/json` requests with a Spanish 400 before ever\n * reaching the network — mirrors `magic-link-exchange/route.ts`'s guard.\n * @default false\n */\n requireJson?: boolean\n /**\n * Overrides the Spanish `message` field of the `requireJson` 400 body, for\n * a non-Spanish-default SITE. The `error` code (`invalid_request`) and\n * shape stay fixed — only the human-facing text is overridable.\n * @default 'Solicitud no válida'\n */\n invalidRequestMessage?: string\n /**\n * Hard timeout (ms) for the upstream fetch, enforced via `AbortController`\n * — mirrors `@reblu/site-client`'s own `timeoutMs` (default `8000`, same\n * value). Without this, a hung base-api response would hang the SITE's\n * Route Handler indefinitely instead of degrading.\n * @default 8000\n */\n timeoutMs?: number\n /**\n * Overrides the `message` field of the 500 body returned when neither\n * `opts.baseUrl`/`opts.apiKey` nor `REBLU_API_BASE_URL`/`REBLU_API_KEY`\n * resolve to a non-empty value.\n * @default 'Error de configuración del servidor.'\n */\n configurationErrorMessage?: string\n /**\n * Overrides the `message` field of the 502 body returned when the\n * upstream fetch times out.\n * @default 'La solicitud ha superado el tiempo de espera.'\n */\n timeoutMessage?: string\n /**\n * Overrides the `message` field of the 502 body returned when the\n * upstream fetch fails for any other reason (DNS, connection refused,\n * etc.).\n * @default 'Error de conexión con el servidor.'\n */\n proxyErrorMessage?: string\n}\n\nfunction jsonErrorResponse(status: number, error: string, message: string): Response {\n return new Response(\n JSON.stringify({ error, message, correlation_id: crypto.randomUUID() }),\n { status, headers: { 'Content-Type': 'application/json; charset=utf-8' } },\n )\n}\n\n/**\n * Creates a Next.js Route Handler that proxies `request` to\n * `${baseUrl}${targetPath}` at base-api, injecting the `X-Reblu-Api-Key`\n * server-side so it never reaches the browser.\n */\nexport function createProxyRoute(targetPath: string, opts: CreateProxyRouteOptions = {}) {\n const {\n method = 'POST',\n forwardSetCookie = false,\n requireJson = false,\n invalidRequestMessage = 'Solicitud no válida',\n timeoutMs = 8000,\n configurationErrorMessage = 'Error de configuración del servidor.',\n timeoutMessage = 'La solicitud ha superado el tiempo de espera.',\n proxyErrorMessage = 'Error de conexión con el servidor.',\n } = opts\n\n return async function handler(request: NextRequest): Promise<Response> {\n if (requireJson) {\n const contentType = request.headers.get('content-type') ?? ''\n if (!contentType.includes('application/json')) {\n return jsonErrorResponse(400, 'invalid_request', invalidRequestMessage)\n }\n }\n\n // Lazy, per-request env read (R6) — opts win when provided.\n const baseUrl = opts.baseUrl ?? process.env.REBLU_API_BASE_URL ?? ''\n const apiKey = opts.apiKey ?? process.env.REBLU_API_KEY ?? ''\n\n if (!baseUrl || !apiKey) {\n console.error(\n `[createProxyRoute] Missing configuration for ${targetPath}: ` +\n `${!baseUrl ? 'baseUrl/REBLU_API_BASE_URL ' : ''}${!apiKey ? 'apiKey/REBLU_API_KEY' : ''}`.trim(),\n )\n return jsonErrorResponse(500, 'missing_configuration', configurationErrorMessage)\n }\n\n const forwardedHeaders: Record<string, string> = {}\n const ua = request.headers.get('user-agent')\n if (ua) forwardedHeaders['user-agent'] = ua\n const xff = request.headers.get('x-forwarded-for')\n if (xff) forwardedHeaders['x-forwarded-for'] = xff\n const xri = request.headers.get('x-real-ip')\n if (xri) forwardedHeaders['x-real-ip'] = xri\n\n const upperMethod = method.toUpperCase()\n const body =\n upperMethod === 'GET' || upperMethod === 'HEAD' ? undefined : await request.text()\n // Forward the original query string (e.g. `?slug=x`) regardless of\n // method — `targetPath` is fixed at factory-call time, so without this a\n // GET route configured via `{ method: 'GET' }` would silently drop it.\n const search = request.nextUrl?.search ?? ''\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n\n try {\n const res = await fetch(`${baseUrl}${targetPath}${search}`, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n 'X-Reblu-Api-Key': apiKey,\n ...forwardedHeaders,\n },\n ...(body ? { body } : {}),\n cache: 'no-store',\n signal: controller.signal,\n })\n\n const data = await res.text()\n const responseHeaders = new Headers({\n 'Content-Type': res.headers.get('Content-Type') ?? 'application/json',\n })\n\n if (forwardSetCookie) {\n for (const cookie of res.headers.getSetCookie()) {\n responseHeaders.append('set-cookie', cookie)\n }\n }\n\n return new Response(data, { status: res.status, headers: responseHeaders })\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n console.error(`[createProxyRoute] Timed out forwarding to ${targetPath} after ${timeoutMs}ms`)\n return jsonErrorResponse(502, 'upstream_timeout', timeoutMessage)\n }\n console.error(`[createProxyRoute] Error forwarding to ${targetPath}:`, err)\n return jsonErrorResponse(502, 'proxy_error', proxyErrorMessage)\n } finally {\n clearTimeout(timer)\n }\n }\n}\n"],"mappings":";AAsGA,SAAS,kBAAkB,QAAgB,OAAe,SAA2B;AACnF,SAAO,IAAI;AAAA,IACT,KAAK,UAAU,EAAE,OAAO,SAAS,gBAAgB,OAAO,WAAW,EAAE,CAAC;AAAA,IACtE,EAAE,QAAQ,SAAS,EAAE,gBAAgB,kCAAkC,EAAE;AAAA,EAC3E;AACF;AAOO,SAAS,iBAAiB,YAAoB,OAAgC,CAAC,GAAG;AACvF,QAAM;AAAA,IACJ,SAAS;AAAA,IACT,mBAAmB;AAAA,IACnB,cAAc;AAAA,IACd,wBAAwB;AAAA,IACxB,YAAY;AAAA,IACZ,4BAA4B;AAAA,IAC5B,iBAAiB;AAAA,IACjB,oBAAoB;AAAA,EACtB,IAAI;AAEJ,SAAO,eAAe,QAAQ,SAAyC;AACrE,QAAI,aAAa;AACf,YAAM,cAAc,QAAQ,QAAQ,IAAI,cAAc,KAAK;AAC3D,UAAI,CAAC,YAAY,SAAS,kBAAkB,GAAG;AAC7C,eAAO,kBAAkB,KAAK,mBAAmB,qBAAqB;AAAA,MACxE;AAAA,IACF;AAGA,UAAM,UAAU,KAAK,WAAW,QAAQ,IAAI,sBAAsB;AAClE,UAAM,SAAS,KAAK,UAAU,QAAQ,IAAI,iBAAiB;AAE3D,QAAI,CAAC,WAAW,CAAC,QAAQ;AACvB,cAAQ;AAAA,QACN,gDAAgD,UAAU,OACxD,GAAG,CAAC,UAAU,gCAAgC,EAAE,GAAG,CAAC,SAAS,yBAAyB,EAAE,GAAG,KAAK;AAAA,MACpG;AACA,aAAO,kBAAkB,KAAK,yBAAyB,yBAAyB;AAAA,IAClF;AAEA,UAAM,mBAA2C,CAAC;AAClD,UAAM,KAAK,QAAQ,QAAQ,IAAI,YAAY;AAC3C,QAAI,GAAI,kBAAiB,YAAY,IAAI;AACzC,UAAM,MAAM,QAAQ,QAAQ,IAAI,iBAAiB;AACjD,QAAI,IAAK,kBAAiB,iBAAiB,IAAI;AAC/C,UAAM,MAAM,QAAQ,QAAQ,IAAI,WAAW;AAC3C,QAAI,IAAK,kBAAiB,WAAW,IAAI;AAEzC,UAAM,cAAc,OAAO,YAAY;AACvC,UAAM,OACJ,gBAAgB,SAAS,gBAAgB,SAAS,SAAY,MAAM,QAAQ,KAAK;AAInF,UAAM,SAAS,QAAQ,SAAS,UAAU;AAE1C,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,SAAS;AAE5D,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,IAAI;AAAA,QAC1D;AAAA,QACA,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,mBAAmB;AAAA,UACnB,GAAG;AAAA,QACL;AAAA,QACA,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,QACvB,OAAO;AAAA,QACP,QAAQ,WAAW;AAAA,MACrB,CAAC;AAED,YAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,YAAM,kBAAkB,IAAI,QAAQ;AAAA,QAClC,gBAAgB,IAAI,QAAQ,IAAI,cAAc,KAAK;AAAA,MACrD,CAAC;AAED,UAAI,kBAAkB;AACpB,mBAAW,UAAU,IAAI,QAAQ,aAAa,GAAG;AAC/C,0BAAgB,OAAO,cAAc,MAAM;AAAA,QAC7C;AAAA,MACF;AAEA,aAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,QAAQ,SAAS,gBAAgB,CAAC;AAAA,IAC5E,SAAS,KAAK;AACZ,UAAI,eAAe,SAAS,IAAI,SAAS,cAAc;AACrD,gBAAQ,MAAM,8CAA8C,UAAU,UAAU,SAAS,IAAI;AAC7F,eAAO,kBAAkB,KAAK,oBAAoB,cAAc;AAAA,MAClE;AACA,cAAQ,MAAM,0CAA0C,UAAU,KAAK,GAAG;AAC1E,aAAO,kBAAkB,KAAK,eAAe,iBAAiB;AAAA,IAChE,UAAE;AACA,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF;AACF;","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reblu/site-next",
3
- "version": "1.1.0",
4
- "description": "Next.js RSC glue for Reblu SITEs — ISR cache adapter + editable CTA server/client components.",
3
+ "version": "1.2.0",
4
+ "description": "Next.js RSC glue for Reblu SITEs — cache + CTA + auth server/client components.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Reblu — Adolfo Unturbe",
7
7
  "homepage": "https://reblu.app",
@@ -60,6 +60,26 @@
60
60
  "types": "./dist/cache.d.cts",
61
61
  "default": "./dist/cache.cjs"
62
62
  }
63
+ },
64
+ "./auth": {
65
+ "import": {
66
+ "types": "./dist/auth.d.ts",
67
+ "default": "./dist/auth.js"
68
+ },
69
+ "require": {
70
+ "types": "./dist/auth.d.cts",
71
+ "default": "./dist/auth.cjs"
72
+ }
73
+ },
74
+ "./route-handlers": {
75
+ "import": {
76
+ "types": "./dist/route-handlers.d.ts",
77
+ "default": "./dist/route-handlers.js"
78
+ },
79
+ "require": {
80
+ "types": "./dist/route-handlers.d.cts",
81
+ "default": "./dist/route-handlers.cjs"
82
+ }
63
83
  }
64
84
  },
65
85
  "publishConfig": {
@@ -67,12 +87,13 @@
67
87
  },
68
88
  "peerDependencies": {
69
89
  "next": ">=16",
90
+ "next-themes": "^0.4.6",
70
91
  "react": ">=19",
71
92
  "@reblu/site-ui": "0.2.0"
72
93
  },
73
94
  "dependencies": {
74
- "@reblu/site-client": "^0.1.1",
75
- "@reblu/site-contracts": "^0.1.1"
95
+ "@reblu/site-client": "^0.1.2",
96
+ "@reblu/site-contracts": "^0.1.2"
76
97
  },
77
98
  "devDependencies": {
78
99
  "@testing-library/jest-dom": "^6.9.1",
@@ -84,6 +105,7 @@
84
105
  "@vitest/coverage-v8": "^4.0.15",
85
106
  "jsdom": "^27.3.0",
86
107
  "next": "16.2.4",
108
+ "next-themes": "^0.4.6",
87
109
  "react": "19.2.4",
88
110
  "react-dom": "19.2.4",
89
111
  "tsup": "^8.5.0",