@reblu/site-next 1.19.0 → 1.20.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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @reblu/site-next
2
2
 
3
+ ## 1.20.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 92e0f6f: `CtaButtons` and `EditableCta` gain plug-and-play presentation props. The default markup is unchanged except for one accessibility fix: external CTAs now carry the new-window hint (see `newWindowHint`).
8
+ - `icon` — leading icon on every CTA, wrapped in an `aria-hidden` span.
9
+ - `primaryVariant` / `secondaryVariant` — `@reblu/site-ui` Button variant for the first CTA and for the rest (default `'default'`).
10
+ - `primaryClassName` / `secondaryClassName` — extra class per role, merged after `buttonClassName`, to map CTAs to the SITE's own button styles.
11
+ - `newWindowHint` — CTAs that open in a new tab now carry a visually-hidden «(se abre en una nueva ventana)» hint (a11y fix; overridable text, pass `''` to opt out, for example on a non-Spanish site). Their accessible name now ends with the hint.
12
+
13
+ `EditableCta` forwards every `CtaButtons` presentation prop and accepts the new `header` slot. New exported type: `CtaButtonVariant`.
14
+
15
+ - b75f066: Deprecate `createShopCheckEmailRoute` (REB-834). The proxied endpoint now always answers `{ exists: false }`, so do not mount it on a new SITE; an existing mount keeps working until the next major removes it. The README's "Wiring a SITE" no longer lists it.
16
+
17
+ ### Patch Changes
18
+
19
+ - Updated dependencies [92e0f6f]
20
+ - Updated dependencies [b75f066]
21
+ - Updated dependencies [b75f066]
22
+ - @reblu/site-contracts@1.14.0
23
+ - @reblu/site-client@5.6.0
24
+
3
25
  ## 1.19.0
4
26
 
5
27
  ### Minor Changes
package/README.md CHANGED
@@ -80,6 +80,43 @@ import { client } from '@/lib/reblu'
80
80
  <EditableCta slot="hero" client={client} fallback={<StaticHeroCta />} />
81
81
  ```
82
82
 
83
+ Slots: `header`, `hero`, `blog-end`, `services`, `contact`, `sticky-mobile`,
84
+ `footer`. A persistent header CTA, for example:
85
+
86
+ ```tsx
87
+ import { CalendarIcon } from 'lucide-react'
88
+
89
+ <EditableCta
90
+ slot="header"
91
+ client={client}
92
+ size="sm"
93
+ icon={<CalendarIcon />}
94
+ primaryVariant="default"
95
+ secondaryVariant="outline"
96
+ primaryClassName="btn-cta" // optional: map the primary CTA to your own button style
97
+ fallback={<StaticHeaderCta />}
98
+ />
99
+ ```
100
+
101
+ `EditableCta` forwards every `CtaButtons` presentation prop:
102
+
103
+ | Prop | Default | Effect |
104
+ | -------------------- | ---------------------------------- | ------------------------------------------------------- |
105
+ | `className` | — | Wrapper class (layout of the button group) |
106
+ | `buttonClassName` | — | Class on every button |
107
+ | `size` | `'lg'` | `@reblu/site-ui` Button size |
108
+ | `icon` | — | Leading icon on every CTA, wrapped in `aria-hidden` |
109
+ | `primaryVariant` | `'default'` | `@reblu/site-ui` Button variant of the first CTA |
110
+ | `secondaryVariant` | `'default'` | Button variant of every other CTA |
111
+ | `primaryClassName` | — | Extra class on the first CTA, after `buttonClassName` |
112
+ | `secondaryClassName` | — | Extra class on every other CTA, after `buttonClassName` |
113
+ | `newWindowHint` | `'(se abre en una nueva ventana)'` | Visually-hidden text on CTAs that open in a new tab |
114
+
115
+ CTAs with an absolute `http(s)` href open in a new tab
116
+ (`target="_blank" rel="noopener noreferrer"`) and carry the `newWindowHint` in a
117
+ `sr-only` span, so screen readers announce the new window. The primary CTA is the
118
+ first one by `sortOrder`.
119
+
83
120
  `CtaButtons` (the `'use client'` renderer) and `StickyMobileCta` are also exported.
84
121
  `CtaButtons` is additionally available at the `@reblu/site-next/cta-buttons`
85
122
  subpath, which resolves directly to the emitted `'use client'` boundary.
@@ -325,7 +362,7 @@ its own Client Component.
325
362
  | `createShopCheckoutRoute(opts?)` | `./shop-route-handlers` | No |
326
363
  | `createShopValidateCartRoute()` | `./shop-route-handlers` | No |
327
364
  | `createShopApplyDiscountRoute()` | `./shop-route-handlers` | No |
328
- | `createShopCheckEmailRoute()` | `./shop-route-handlers` | No |
365
+ | `createShopCheckEmailRoute()` — **deprecated** (REB-834) | `./shop-route-handlers` | No |
329
366
  | `createShopOrderRoute()` | `./shop-route-handlers` | No |
330
367
  | `getShopOrderSummary(client, sessionId)` | `./shop-server` | No |
331
368
  | `useShopOrderStatus(sessionId, opts?)` | `./shop` | **Yes** |
@@ -344,7 +381,7 @@ its own Client Component.
344
381
  <CartProvider>{children}</CartProvider>
345
382
  ```
346
383
 
347
- 2. **Route Handlers** — mount the five factories, one file each (mirrors the
384
+ 2. **Route Handlers** — mount the four factories, one file each (mirrors the
348
385
  `./route-handlers` auth pattern — a one-line file per route):
349
386
 
350
387
  ```ts
@@ -358,22 +395,37 @@ its own Client Component.
358
395
  // app/api/shop/apply-discount/route.ts
359
396
  export const POST = createShopApplyDiscountRoute()
360
397
 
361
- // app/api/shop/check-email/route.ts
362
- export const POST = createShopCheckEmailRoute()
363
-
364
398
  // app/api/shop/order/[sessionId]/route.ts
365
399
  export const GET = createShopOrderRoute()
366
400
  ```
367
401
 
368
- All five read `REBLU_API_BASE_URL`/`REBLU_API_KEY` lazily from `process.env`
369
- (same as `./route-handlers`'s `createProxyRoute`, which they wrap) — the
370
- API key never reaches the browser.
402
+ All of them read `REBLU_API_BASE_URL`/`REBLU_API_KEY` lazily from
403
+ `process.env` (same as `./route-handlers`'s `createProxyRoute`, which they
404
+ wrap) — the API key never reaches the browser.
405
+
406
+ Do not mount `createShopCheckEmailRoute()` on a new SITE: it is
407
+ **deprecated** (REB-834) and removed in the next major. Its endpoint used to
408
+ say whether an email had an account — an enumeration oracle on the
409
+ tenant's customer list — and now always answers `{ exists: false }`. An
410
+ existing mount keeps working; remove it at your convenience. The checkout
411
+ always offers «Iniciar sesión o continuar como invitado», and the server
412
+ links a guest's order to an existing account after payment.
413
+
414
+ **Forward the visitor's IP.** base-api rate-limits some public routes
415
+ per client IP (check-email, and the auth routes such as login and
416
+ magic-link), reading the IP from `x-forwarded-for`. A SITE
417
+ that calls `/api/site/*` from its own server must forward the visitor's
418
+ `x-forwarded-for` (and `x-real-ip`), or every visitor shares the SITE
419
+ server's single IP window and one busy page throttles them all.
420
+ `createProxyRoute` — and therefore every `createShop*Route` factory —
421
+ already forwards both headers; only hand-written server-side calls need to
422
+ do it themselves.
371
423
 
372
424
  3. **Pages** — create `app/shop/cart/page.tsx`, `app/shop/checkout/page.tsx`
373
425
  and `app/shop/order-confirmation/page.tsx`. Read the cart with `useCart()`
374
426
  from `./cart`, render `@reblu/site-blocks`'s `CartLineList`/`CartSummary`/
375
427
  `CheckoutForm`/`OrderConfirmation`, and `fetch` the Route Handlers from
376
- step 2 for checkout/validate-cart/apply-discount/check-email/order (never
428
+ step 2 for checkout/validate-cart/apply-discount/order (never
377
429
  call base-api directly from a Client Component — that's what step 2's
378
430
  Route Handlers are for). See `apps/demo-site`'s `shop/{cart,checkout,
379
431
  order-confirmation}/page.tsx` in this monorepo for a complete, working
@@ -37,22 +37,49 @@ module.exports = __toCommonJS(cta_buttons_exports);
37
37
  var import_site_ui = require("@reblu/site-ui");
38
38
  var import_link = __toESM(require("next/link"), 1);
39
39
  var import_jsx_runtime = require("react/jsx-runtime");
40
+ var DEFAULT_NEW_WINDOW_HINT = "(se abre en una nueva ventana)";
40
41
  function isExternal(href) {
41
42
  return /^https?:\/\//i.test(href);
42
43
  }
43
- function CtaButtons({ ctas, className, buttonClassName, size = "lg" }) {
44
+ function CtaButtons({
45
+ ctas,
46
+ className,
47
+ buttonClassName,
48
+ size = "lg",
49
+ icon,
50
+ primaryVariant = "default",
51
+ secondaryVariant = "default",
52
+ primaryClassName,
53
+ secondaryClassName,
54
+ newWindowHint = DEFAULT_NEW_WINDOW_HINT
55
+ }) {
44
56
  if (ctas.length === 0) return null;
45
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, "data-slot": "cta-buttons", children: ctas.map((cta) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
46
- import_site_ui.Button,
47
- {
48
- asChild: true,
49
- size,
50
- className: buttonClassName,
51
- "data-cta-id": cta.analyticsId ?? void 0,
52
- children: isExternal(cta.href) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: cta.href, target: "_blank", rel: "noopener noreferrer", children: cta.label }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_link.default, { href: cta.href, children: cta.label })
53
- },
54
- cta.id
55
- )) });
57
+ const leadingIcon = icon == null ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", className: "inline-flex shrink-0", children: icon });
58
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, "data-slot": "cta-buttons", children: ctas.map((cta, index) => {
59
+ const isPrimary = index === 0;
60
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
61
+ import_site_ui.Button,
62
+ {
63
+ asChild: true,
64
+ size,
65
+ variant: isPrimary ? primaryVariant : secondaryVariant,
66
+ className: (0, import_site_ui.cn)(buttonClassName, isPrimary ? primaryClassName : secondaryClassName),
67
+ "data-cta-id": cta.analyticsId ?? void 0,
68
+ children: isExternal(cta.href) ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("a", { href: cta.href, target: "_blank", rel: "noopener noreferrer", children: [
69
+ leadingIcon,
70
+ cta.label,
71
+ newWindowHint ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
72
+ " ",
73
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "sr-only", children: newWindowHint })
74
+ ] }) : null
75
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_link.default, { href: cta.href, children: [
76
+ leadingIcon,
77
+ cta.label
78
+ ] })
79
+ },
80
+ cta.id
81
+ );
82
+ }) });
56
83
  }
57
84
  // Annotate the CommonJS export names for ESM import in node:
58
85
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cta-buttons.tsx"],"sourcesContent":["'use client'\n\nimport type { Cta } from '@reblu/site-contracts'\nimport { Button } from '@reblu/site-ui'\nimport Link from 'next/link'\n\ntype ButtonSize = 'sm' | 'default' | 'lg'\n\nexport interface CtaButtonsProps {\n ctas: Cta[]\n /** Wrapper class (layout of the button group). */\n className?: string\n /** Class applied to each button. */\n buttonClassName?: string\n size?: ButtonSize\n}\n\n/** External = absolute http(s) URL (e.g. a Klinikare booking link). */\nfunction isExternal(href: string): boolean {\n return /^https?:\\/\\//i.test(href)\n}\n\n/**\n * Presentational CTA renderer (REB-394, moved to `@reblu/site-next` in REB-418).\n * Pure — takes already-resolved CTAs and renders one styled button-link each.\n * Internal hrefs use `next/link` (client routing); external URLs open in a new,\n * safely-rel tab. `analyticsId` is surfaced as `data-cta-id` on the clickable\n * element for GA `cta_click` tracking. Renders nothing when there are no CTAs.\n *\n * Imports `Button` from `@reblu/site-ui` (public) — never `@reblu/ui` (private).\n */\nexport function CtaButtons({ ctas, className, buttonClassName, size = 'lg' }: CtaButtonsProps) {\n if (ctas.length === 0) return null\n\n return (\n <div className={className} data-slot='cta-buttons'>\n {ctas.map((cta) => (\n <Button\n key={cta.id}\n asChild\n size={size}\n className={buttonClassName}\n data-cta-id={cta.analyticsId ?? undefined}\n >\n {isExternal(cta.href) ? (\n <a href={cta.href} target='_blank' rel='noopener noreferrer'>\n {cta.label}\n </a>\n ) : (\n <Link href={cta.href}>{cta.label}</Link>\n )}\n </Button>\n ))}\n </div>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,qBAAuB;AACvB,kBAAiB;AAyCL;AA3BZ,SAAS,WAAW,MAAuB;AACzC,SAAO,gBAAgB,KAAK,IAAI;AAClC;AAWO,SAAS,WAAW,EAAE,MAAM,WAAW,iBAAiB,OAAO,KAAK,GAAoB;AAC7F,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,SACE,4CAAC,SAAI,WAAsB,aAAU,eAClC,eAAK,IAAI,CAAC,QACT;AAAA,IAAC;AAAA;AAAA,MAEC,SAAO;AAAA,MACP;AAAA,MACA,WAAW;AAAA,MACX,eAAa,IAAI,eAAe;AAAA,MAE/B,qBAAW,IAAI,IAAI,IAClB,4CAAC,OAAE,MAAM,IAAI,MAAM,QAAO,UAAS,KAAI,uBACpC,cAAI,OACP,IAEA,4CAAC,YAAAA,SAAA,EAAK,MAAM,IAAI,MAAO,cAAI,OAAM;AAAA;AAAA,IAX9B,IAAI;AAAA,EAaX,CACD,GACH;AAEJ;","names":["Link"]}
1
+ {"version":3,"sources":["../src/cta-buttons.tsx"],"sourcesContent":["'use client'\n\nimport type { Cta } from '@reblu/site-contracts'\nimport { Button, cn, type ButtonVariantProps } from '@reblu/site-ui'\nimport Link from 'next/link'\nimport type { ReactNode } from 'react'\n\ntype ButtonSize = 'sm' | 'default' | 'lg'\n\n/** A `@reblu/site-ui` Button variant (`default`, `outline`, `secondary`, …). */\nexport type CtaButtonVariant = NonNullable<ButtonVariantProps['variant']>\n\n/** Default screen-reader hint appended to CTAs that open in a new tab. */\nconst DEFAULT_NEW_WINDOW_HINT = '(se abre en una nueva ventana)'\n\nexport interface CtaButtonsProps {\n ctas: Cta[]\n /** Wrapper class (layout of the button group). */\n className?: string\n /** Class applied to each button. */\n buttonClassName?: string\n size?: ButtonSize\n /**\n * Leading icon rendered before every CTA label. Decorative: it is wrapped in\n * an `aria-hidden` span, so the accessible name stays the label.\n */\n icon?: ReactNode\n /** Button variant of the primary CTA (the first one, by `sortOrder`). Default `default`. */\n primaryVariant?: CtaButtonVariant\n /** Button variant of every secondary CTA (all but the first). Default `default`. */\n secondaryVariant?: CtaButtonVariant\n /**\n * Extra class for the primary CTA, merged after `buttonClassName`. Lets a SITE\n * map the primary CTA to its own button style (e.g. a brand `cta` variant).\n */\n primaryClassName?: string\n /** Extra class for every secondary CTA, merged after `buttonClassName`. */\n secondaryClassName?: string\n /**\n * Visually-hidden text appended to CTAs that open in a new tab, so screen\n * readers announce the context change. Default «(se abre en una nueva ventana)».\n */\n newWindowHint?: string\n}\n\n/** External = absolute http(s) URL (e.g. a third-party booking link). */\nfunction isExternal(href: string): boolean {\n return /^https?:\\/\\//i.test(href)\n}\n\n/**\n * Presentational CTA renderer (REB-394, moved to `@reblu/site-next` in REB-418).\n * Pure — takes already-resolved CTAs and renders one styled button-link each.\n * Internal hrefs use `next/link` (client routing); external URLs open in a new,\n * safely-rel tab and carry a visually-hidden new-window hint. `analyticsId` is\n * surfaced as `data-cta-id` on the clickable element for GA `cta_click`\n * tracking. Renders nothing when there are no CTAs.\n *\n * The first CTA is the primary one; the rest are secondary. Each role takes its\n * own Button variant and extra class, so a SITE maps the CTAs to its own button\n * styles without forking this markup.\n *\n * Imports `Button` from `@reblu/site-ui` (public) — never `@reblu/ui` (private).\n */\nexport function CtaButtons({\n ctas,\n className,\n buttonClassName,\n size = 'lg',\n icon,\n primaryVariant = 'default',\n secondaryVariant = 'default',\n primaryClassName,\n secondaryClassName,\n newWindowHint = DEFAULT_NEW_WINDOW_HINT,\n}: CtaButtonsProps) {\n if (ctas.length === 0) return null\n\n const leadingIcon =\n icon == null ? null : (\n <span aria-hidden='true' className='inline-flex shrink-0'>\n {icon}\n </span>\n )\n\n return (\n <div className={className} data-slot='cta-buttons'>\n {ctas.map((cta, index) => {\n const isPrimary = index === 0\n return (\n <Button\n key={cta.id}\n asChild\n size={size}\n variant={isPrimary ? primaryVariant : secondaryVariant}\n className={cn(buttonClassName, isPrimary ? primaryClassName : secondaryClassName)}\n data-cta-id={cta.analyticsId ?? undefined}\n >\n {isExternal(cta.href) ? (\n <a href={cta.href} target='_blank' rel='noopener noreferrer'>\n {leadingIcon}\n {cta.label}\n {newWindowHint ? <> <span className='sr-only'>{newWindowHint}</span></> : null}\n </a>\n ) : (\n <Link href={cta.href}>\n {leadingIcon}\n {cta.label}\n </Link>\n )}\n </Button>\n )\n })}\n </div>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,qBAAoD;AACpD,kBAAiB;AA4EX;AAnEN,IAAM,0BAA0B;AAiChC,SAAS,WAAW,MAAuB;AACzC,SAAO,gBAAgB,KAAK,IAAI;AAClC;AAgBO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB;AAAA,EACA;AAAA,EACA,gBAAgB;AAClB,GAAoB;AAClB,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,QAAM,cACJ,QAAQ,OAAO,OACb,4CAAC,UAAK,eAAY,QAAO,WAAU,wBAChC,gBACH;AAGJ,SACE,4CAAC,SAAI,WAAsB,aAAU,eAClC,eAAK,IAAI,CAAC,KAAK,UAAU;AACxB,UAAM,YAAY,UAAU;AAC5B,WACE;AAAA,MAAC;AAAA;AAAA,QAEC,SAAO;AAAA,QACP;AAAA,QACA,SAAS,YAAY,iBAAiB;AAAA,QACtC,eAAW,mBAAG,iBAAiB,YAAY,mBAAmB,kBAAkB;AAAA,QAChF,eAAa,IAAI,eAAe;AAAA,QAE/B,qBAAW,IAAI,IAAI,IAClB,6CAAC,OAAE,MAAM,IAAI,MAAM,QAAO,UAAS,KAAI,uBACpC;AAAA;AAAA,UACA,IAAI;AAAA,UACJ,gBAAgB,4EAAE;AAAA;AAAA,YAAC,4CAAC,UAAK,WAAU,WAAW,yBAAc;AAAA,aAAO,IAAM;AAAA,WAC5E,IAEA,6CAAC,YAAAA,SAAA,EAAK,MAAM,IAAI,MACb;AAAA;AAAA,UACA,IAAI;AAAA,WACP;AAAA;AAAA,MAjBG,IAAI;AAAA,IAmBX;AAAA,EAEJ,CAAC,GACH;AAEJ;","names":["Link"]}
@@ -1,7 +1,11 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { Cta } from '@reblu/site-contracts';
3
+ import { ButtonVariantProps } from '@reblu/site-ui';
4
+ import { ReactNode } from 'react';
3
5
 
4
6
  type ButtonSize = 'sm' | 'default' | 'lg';
7
+ /** A `@reblu/site-ui` Button variant (`default`, `outline`, `secondary`, …). */
8
+ type CtaButtonVariant = NonNullable<ButtonVariantProps['variant']>;
5
9
  interface CtaButtonsProps {
6
10
  ctas: Cta[];
7
11
  /** Wrapper class (layout of the button group). */
@@ -9,16 +13,42 @@ interface CtaButtonsProps {
9
13
  /** Class applied to each button. */
10
14
  buttonClassName?: string;
11
15
  size?: ButtonSize;
16
+ /**
17
+ * Leading icon rendered before every CTA label. Decorative: it is wrapped in
18
+ * an `aria-hidden` span, so the accessible name stays the label.
19
+ */
20
+ icon?: ReactNode;
21
+ /** Button variant of the primary CTA (the first one, by `sortOrder`). Default `default`. */
22
+ primaryVariant?: CtaButtonVariant;
23
+ /** Button variant of every secondary CTA (all but the first). Default `default`. */
24
+ secondaryVariant?: CtaButtonVariant;
25
+ /**
26
+ * Extra class for the primary CTA, merged after `buttonClassName`. Lets a SITE
27
+ * map the primary CTA to its own button style (e.g. a brand `cta` variant).
28
+ */
29
+ primaryClassName?: string;
30
+ /** Extra class for every secondary CTA, merged after `buttonClassName`. */
31
+ secondaryClassName?: string;
32
+ /**
33
+ * Visually-hidden text appended to CTAs that open in a new tab, so screen
34
+ * readers announce the context change. Default «(se abre en una nueva ventana)».
35
+ */
36
+ newWindowHint?: string;
12
37
  }
13
38
  /**
14
39
  * Presentational CTA renderer (REB-394, moved to `@reblu/site-next` in REB-418).
15
40
  * Pure — takes already-resolved CTAs and renders one styled button-link each.
16
41
  * Internal hrefs use `next/link` (client routing); external URLs open in a new,
17
- * safely-rel tab. `analyticsId` is surfaced as `data-cta-id` on the clickable
18
- * element for GA `cta_click` tracking. Renders nothing when there are no CTAs.
42
+ * safely-rel tab and carry a visually-hidden new-window hint. `analyticsId` is
43
+ * surfaced as `data-cta-id` on the clickable element for GA `cta_click`
44
+ * tracking. Renders nothing when there are no CTAs.
45
+ *
46
+ * The first CTA is the primary one; the rest are secondary. Each role takes its
47
+ * own Button variant and extra class, so a SITE maps the CTAs to its own button
48
+ * styles without forking this markup.
19
49
  *
20
50
  * Imports `Button` from `@reblu/site-ui` (public) — never `@reblu/ui` (private).
21
51
  */
22
- declare function CtaButtons({ ctas, className, buttonClassName, size }: CtaButtonsProps): react_jsx_runtime.JSX.Element | null;
52
+ declare function CtaButtons({ ctas, className, buttonClassName, size, icon, primaryVariant, secondaryVariant, primaryClassName, secondaryClassName, newWindowHint, }: CtaButtonsProps): react_jsx_runtime.JSX.Element | null;
23
53
 
24
- export { CtaButtons, type CtaButtonsProps };
54
+ export { type CtaButtonVariant, CtaButtons, type CtaButtonsProps };
@@ -1,7 +1,11 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { Cta } from '@reblu/site-contracts';
3
+ import { ButtonVariantProps } from '@reblu/site-ui';
4
+ import { ReactNode } from 'react';
3
5
 
4
6
  type ButtonSize = 'sm' | 'default' | 'lg';
7
+ /** A `@reblu/site-ui` Button variant (`default`, `outline`, `secondary`, …). */
8
+ type CtaButtonVariant = NonNullable<ButtonVariantProps['variant']>;
5
9
  interface CtaButtonsProps {
6
10
  ctas: Cta[];
7
11
  /** Wrapper class (layout of the button group). */
@@ -9,16 +13,42 @@ interface CtaButtonsProps {
9
13
  /** Class applied to each button. */
10
14
  buttonClassName?: string;
11
15
  size?: ButtonSize;
16
+ /**
17
+ * Leading icon rendered before every CTA label. Decorative: it is wrapped in
18
+ * an `aria-hidden` span, so the accessible name stays the label.
19
+ */
20
+ icon?: ReactNode;
21
+ /** Button variant of the primary CTA (the first one, by `sortOrder`). Default `default`. */
22
+ primaryVariant?: CtaButtonVariant;
23
+ /** Button variant of every secondary CTA (all but the first). Default `default`. */
24
+ secondaryVariant?: CtaButtonVariant;
25
+ /**
26
+ * Extra class for the primary CTA, merged after `buttonClassName`. Lets a SITE
27
+ * map the primary CTA to its own button style (e.g. a brand `cta` variant).
28
+ */
29
+ primaryClassName?: string;
30
+ /** Extra class for every secondary CTA, merged after `buttonClassName`. */
31
+ secondaryClassName?: string;
32
+ /**
33
+ * Visually-hidden text appended to CTAs that open in a new tab, so screen
34
+ * readers announce the context change. Default «(se abre en una nueva ventana)».
35
+ */
36
+ newWindowHint?: string;
12
37
  }
13
38
  /**
14
39
  * Presentational CTA renderer (REB-394, moved to `@reblu/site-next` in REB-418).
15
40
  * Pure — takes already-resolved CTAs and renders one styled button-link each.
16
41
  * Internal hrefs use `next/link` (client routing); external URLs open in a new,
17
- * safely-rel tab. `analyticsId` is surfaced as `data-cta-id` on the clickable
18
- * element for GA `cta_click` tracking. Renders nothing when there are no CTAs.
42
+ * safely-rel tab and carry a visually-hidden new-window hint. `analyticsId` is
43
+ * surfaced as `data-cta-id` on the clickable element for GA `cta_click`
44
+ * tracking. Renders nothing when there are no CTAs.
45
+ *
46
+ * The first CTA is the primary one; the rest are secondary. Each role takes its
47
+ * own Button variant and extra class, so a SITE maps the CTAs to its own button
48
+ * styles without forking this markup.
19
49
  *
20
50
  * Imports `Button` from `@reblu/site-ui` (public) — never `@reblu/ui` (private).
21
51
  */
22
- declare function CtaButtons({ ctas, className, buttonClassName, size }: CtaButtonsProps): react_jsx_runtime.JSX.Element | null;
52
+ declare function CtaButtons({ ctas, className, buttonClassName, size, icon, primaryVariant, secondaryVariant, primaryClassName, secondaryClassName, newWindowHint, }: CtaButtonsProps): react_jsx_runtime.JSX.Element | null;
23
53
 
24
- export { CtaButtons, type CtaButtonsProps };
54
+ export { type CtaButtonVariant, CtaButtons, type CtaButtonsProps };
@@ -1,25 +1,52 @@
1
1
  "use client";
2
2
 
3
3
  // src/cta-buttons.tsx
4
- import { Button } from "@reblu/site-ui";
4
+ import { Button, cn } from "@reblu/site-ui";
5
5
  import Link from "next/link";
6
- import { jsx } from "react/jsx-runtime";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+ var DEFAULT_NEW_WINDOW_HINT = "(se abre en una nueva ventana)";
7
8
  function isExternal(href) {
8
9
  return /^https?:\/\//i.test(href);
9
10
  }
10
- function CtaButtons({ ctas, className, buttonClassName, size = "lg" }) {
11
+ function CtaButtons({
12
+ ctas,
13
+ className,
14
+ buttonClassName,
15
+ size = "lg",
16
+ icon,
17
+ primaryVariant = "default",
18
+ secondaryVariant = "default",
19
+ primaryClassName,
20
+ secondaryClassName,
21
+ newWindowHint = DEFAULT_NEW_WINDOW_HINT
22
+ }) {
11
23
  if (ctas.length === 0) return null;
12
- return /* @__PURE__ */ jsx("div", { className, "data-slot": "cta-buttons", children: ctas.map((cta) => /* @__PURE__ */ jsx(
13
- Button,
14
- {
15
- asChild: true,
16
- size,
17
- className: buttonClassName,
18
- "data-cta-id": cta.analyticsId ?? void 0,
19
- children: isExternal(cta.href) ? /* @__PURE__ */ jsx("a", { href: cta.href, target: "_blank", rel: "noopener noreferrer", children: cta.label }) : /* @__PURE__ */ jsx(Link, { href: cta.href, children: cta.label })
20
- },
21
- cta.id
22
- )) });
24
+ const leadingIcon = icon == null ? null : /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "inline-flex shrink-0", children: icon });
25
+ return /* @__PURE__ */ jsx("div", { className, "data-slot": "cta-buttons", children: ctas.map((cta, index) => {
26
+ const isPrimary = index === 0;
27
+ return /* @__PURE__ */ jsx(
28
+ Button,
29
+ {
30
+ asChild: true,
31
+ size,
32
+ variant: isPrimary ? primaryVariant : secondaryVariant,
33
+ className: cn(buttonClassName, isPrimary ? primaryClassName : secondaryClassName),
34
+ "data-cta-id": cta.analyticsId ?? void 0,
35
+ children: isExternal(cta.href) ? /* @__PURE__ */ jsxs("a", { href: cta.href, target: "_blank", rel: "noopener noreferrer", children: [
36
+ leadingIcon,
37
+ cta.label,
38
+ newWindowHint ? /* @__PURE__ */ jsxs(Fragment, { children: [
39
+ " ",
40
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: newWindowHint })
41
+ ] }) : null
42
+ ] }) : /* @__PURE__ */ jsxs(Link, { href: cta.href, children: [
43
+ leadingIcon,
44
+ cta.label
45
+ ] })
46
+ },
47
+ cta.id
48
+ );
49
+ }) });
23
50
  }
24
51
  export {
25
52
  CtaButtons
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cta-buttons.tsx"],"sourcesContent":["'use client'\n\nimport type { Cta } from '@reblu/site-contracts'\nimport { Button } from '@reblu/site-ui'\nimport Link from 'next/link'\n\ntype ButtonSize = 'sm' | 'default' | 'lg'\n\nexport interface CtaButtonsProps {\n ctas: Cta[]\n /** Wrapper class (layout of the button group). */\n className?: string\n /** Class applied to each button. */\n buttonClassName?: string\n size?: ButtonSize\n}\n\n/** External = absolute http(s) URL (e.g. a Klinikare booking link). */\nfunction isExternal(href: string): boolean {\n return /^https?:\\/\\//i.test(href)\n}\n\n/**\n * Presentational CTA renderer (REB-394, moved to `@reblu/site-next` in REB-418).\n * Pure — takes already-resolved CTAs and renders one styled button-link each.\n * Internal hrefs use `next/link` (client routing); external URLs open in a new,\n * safely-rel tab. `analyticsId` is surfaced as `data-cta-id` on the clickable\n * element for GA `cta_click` tracking. Renders nothing when there are no CTAs.\n *\n * Imports `Button` from `@reblu/site-ui` (public) — never `@reblu/ui` (private).\n */\nexport function CtaButtons({ ctas, className, buttonClassName, size = 'lg' }: CtaButtonsProps) {\n if (ctas.length === 0) return null\n\n return (\n <div className={className} data-slot='cta-buttons'>\n {ctas.map((cta) => (\n <Button\n key={cta.id}\n asChild\n size={size}\n className={buttonClassName}\n data-cta-id={cta.analyticsId ?? undefined}\n >\n {isExternal(cta.href) ? (\n <a href={cta.href} target='_blank' rel='noopener noreferrer'>\n {cta.label}\n </a>\n ) : (\n <Link href={cta.href}>{cta.label}</Link>\n )}\n </Button>\n ))}\n </div>\n )\n}\n"],"mappings":";;;AAGA,SAAS,cAAc;AACvB,OAAO,UAAU;AAyCL;AA3BZ,SAAS,WAAW,MAAuB;AACzC,SAAO,gBAAgB,KAAK,IAAI;AAClC;AAWO,SAAS,WAAW,EAAE,MAAM,WAAW,iBAAiB,OAAO,KAAK,GAAoB;AAC7F,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,SACE,oBAAC,SAAI,WAAsB,aAAU,eAClC,eAAK,IAAI,CAAC,QACT;AAAA,IAAC;AAAA;AAAA,MAEC,SAAO;AAAA,MACP;AAAA,MACA,WAAW;AAAA,MACX,eAAa,IAAI,eAAe;AAAA,MAE/B,qBAAW,IAAI,IAAI,IAClB,oBAAC,OAAE,MAAM,IAAI,MAAM,QAAO,UAAS,KAAI,uBACpC,cAAI,OACP,IAEA,oBAAC,QAAK,MAAM,IAAI,MAAO,cAAI,OAAM;AAAA;AAAA,IAX9B,IAAI;AAAA,EAaX,CACD,GACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../src/cta-buttons.tsx"],"sourcesContent":["'use client'\n\nimport type { Cta } from '@reblu/site-contracts'\nimport { Button, cn, type ButtonVariantProps } from '@reblu/site-ui'\nimport Link from 'next/link'\nimport type { ReactNode } from 'react'\n\ntype ButtonSize = 'sm' | 'default' | 'lg'\n\n/** A `@reblu/site-ui` Button variant (`default`, `outline`, `secondary`, …). */\nexport type CtaButtonVariant = NonNullable<ButtonVariantProps['variant']>\n\n/** Default screen-reader hint appended to CTAs that open in a new tab. */\nconst DEFAULT_NEW_WINDOW_HINT = '(se abre en una nueva ventana)'\n\nexport interface CtaButtonsProps {\n ctas: Cta[]\n /** Wrapper class (layout of the button group). */\n className?: string\n /** Class applied to each button. */\n buttonClassName?: string\n size?: ButtonSize\n /**\n * Leading icon rendered before every CTA label. Decorative: it is wrapped in\n * an `aria-hidden` span, so the accessible name stays the label.\n */\n icon?: ReactNode\n /** Button variant of the primary CTA (the first one, by `sortOrder`). Default `default`. */\n primaryVariant?: CtaButtonVariant\n /** Button variant of every secondary CTA (all but the first). Default `default`. */\n secondaryVariant?: CtaButtonVariant\n /**\n * Extra class for the primary CTA, merged after `buttonClassName`. Lets a SITE\n * map the primary CTA to its own button style (e.g. a brand `cta` variant).\n */\n primaryClassName?: string\n /** Extra class for every secondary CTA, merged after `buttonClassName`. */\n secondaryClassName?: string\n /**\n * Visually-hidden text appended to CTAs that open in a new tab, so screen\n * readers announce the context change. Default «(se abre en una nueva ventana)».\n */\n newWindowHint?: string\n}\n\n/** External = absolute http(s) URL (e.g. a third-party booking link). */\nfunction isExternal(href: string): boolean {\n return /^https?:\\/\\//i.test(href)\n}\n\n/**\n * Presentational CTA renderer (REB-394, moved to `@reblu/site-next` in REB-418).\n * Pure — takes already-resolved CTAs and renders one styled button-link each.\n * Internal hrefs use `next/link` (client routing); external URLs open in a new,\n * safely-rel tab and carry a visually-hidden new-window hint. `analyticsId` is\n * surfaced as `data-cta-id` on the clickable element for GA `cta_click`\n * tracking. Renders nothing when there are no CTAs.\n *\n * The first CTA is the primary one; the rest are secondary. Each role takes its\n * own Button variant and extra class, so a SITE maps the CTAs to its own button\n * styles without forking this markup.\n *\n * Imports `Button` from `@reblu/site-ui` (public) — never `@reblu/ui` (private).\n */\nexport function CtaButtons({\n ctas,\n className,\n buttonClassName,\n size = 'lg',\n icon,\n primaryVariant = 'default',\n secondaryVariant = 'default',\n primaryClassName,\n secondaryClassName,\n newWindowHint = DEFAULT_NEW_WINDOW_HINT,\n}: CtaButtonsProps) {\n if (ctas.length === 0) return null\n\n const leadingIcon =\n icon == null ? null : (\n <span aria-hidden='true' className='inline-flex shrink-0'>\n {icon}\n </span>\n )\n\n return (\n <div className={className} data-slot='cta-buttons'>\n {ctas.map((cta, index) => {\n const isPrimary = index === 0\n return (\n <Button\n key={cta.id}\n asChild\n size={size}\n variant={isPrimary ? primaryVariant : secondaryVariant}\n className={cn(buttonClassName, isPrimary ? primaryClassName : secondaryClassName)}\n data-cta-id={cta.analyticsId ?? undefined}\n >\n {isExternal(cta.href) ? (\n <a href={cta.href} target='_blank' rel='noopener noreferrer'>\n {leadingIcon}\n {cta.label}\n {newWindowHint ? <> <span className='sr-only'>{newWindowHint}</span></> : null}\n </a>\n ) : (\n <Link href={cta.href}>\n {leadingIcon}\n {cta.label}\n </Link>\n )}\n </Button>\n )\n })}\n </div>\n )\n}\n"],"mappings":";;;AAGA,SAAS,QAAQ,UAAmC;AACpD,OAAO,UAAU;AA4EX,SAsB2B,UAtB3B,KAsB2B,YAtB3B;AAnEN,IAAM,0BAA0B;AAiChC,SAAS,WAAW,MAAuB;AACzC,SAAO,gBAAgB,KAAK,IAAI;AAClC;AAgBO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB;AAAA,EACA;AAAA,EACA,gBAAgB;AAClB,GAAoB;AAClB,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,QAAM,cACJ,QAAQ,OAAO,OACb,oBAAC,UAAK,eAAY,QAAO,WAAU,wBAChC,gBACH;AAGJ,SACE,oBAAC,SAAI,WAAsB,aAAU,eAClC,eAAK,IAAI,CAAC,KAAK,UAAU;AACxB,UAAM,YAAY,UAAU;AAC5B,WACE;AAAA,MAAC;AAAA;AAAA,QAEC,SAAO;AAAA,QACP;AAAA,QACA,SAAS,YAAY,iBAAiB;AAAA,QACtC,WAAW,GAAG,iBAAiB,YAAY,mBAAmB,kBAAkB;AAAA,QAChF,eAAa,IAAI,eAAe;AAAA,QAE/B,qBAAW,IAAI,IAAI,IAClB,qBAAC,OAAE,MAAM,IAAI,MAAM,QAAO,UAAS,KAAI,uBACpC;AAAA;AAAA,UACA,IAAI;AAAA,UACJ,gBAAgB,iCAAE;AAAA;AAAA,YAAC,oBAAC,UAAK,WAAU,WAAW,yBAAc;AAAA,aAAO,IAAM;AAAA,WAC5E,IAEA,qBAAC,QAAK,MAAM,IAAI,MACb;AAAA;AAAA,UACA,IAAI;AAAA,WACP;AAAA;AAAA,MAjBG,IAAI;AAAA,IAmBX;AAAA,EAEJ,CAAC,GACH;AAEJ;","names":[]}
package/dist/index.cjs CHANGED
@@ -61,22 +61,49 @@ var NextCacheAdapter = class {
61
61
  var import_site_ui = require("@reblu/site-ui");
62
62
  var import_link = __toESM(require("next/link"), 1);
63
63
  var import_jsx_runtime = require("react/jsx-runtime");
64
+ var DEFAULT_NEW_WINDOW_HINT = "(se abre en una nueva ventana)";
64
65
  function isExternal(href) {
65
66
  return /^https?:\/\//i.test(href);
66
67
  }
67
- function CtaButtons({ ctas, className, buttonClassName, size = "lg" }) {
68
+ function CtaButtons({
69
+ ctas,
70
+ className,
71
+ buttonClassName,
72
+ size = "lg",
73
+ icon,
74
+ primaryVariant = "default",
75
+ secondaryVariant = "default",
76
+ primaryClassName,
77
+ secondaryClassName,
78
+ newWindowHint = DEFAULT_NEW_WINDOW_HINT
79
+ }) {
68
80
  if (ctas.length === 0) return null;
69
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, "data-slot": "cta-buttons", children: ctas.map((cta) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
70
- import_site_ui.Button,
71
- {
72
- asChild: true,
73
- size,
74
- className: buttonClassName,
75
- "data-cta-id": cta.analyticsId ?? void 0,
76
- children: isExternal(cta.href) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: cta.href, target: "_blank", rel: "noopener noreferrer", children: cta.label }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_link.default, { href: cta.href, children: cta.label })
77
- },
78
- cta.id
79
- )) });
81
+ const leadingIcon = icon == null ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", className: "inline-flex shrink-0", children: icon });
82
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, "data-slot": "cta-buttons", children: ctas.map((cta, index) => {
83
+ const isPrimary = index === 0;
84
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
85
+ import_site_ui.Button,
86
+ {
87
+ asChild: true,
88
+ size,
89
+ variant: isPrimary ? primaryVariant : secondaryVariant,
90
+ className: (0, import_site_ui.cn)(buttonClassName, isPrimary ? primaryClassName : secondaryClassName),
91
+ "data-cta-id": cta.analyticsId ?? void 0,
92
+ children: isExternal(cta.href) ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("a", { href: cta.href, target: "_blank", rel: "noopener noreferrer", children: [
93
+ leadingIcon,
94
+ cta.label,
95
+ newWindowHint ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
96
+ " ",
97
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "sr-only", children: newWindowHint })
98
+ ] }) : null
99
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_link.default, { href: cta.href, children: [
100
+ leadingIcon,
101
+ cta.label
102
+ ] })
103
+ },
104
+ cta.id
105
+ );
106
+ }) });
80
107
  }
81
108
 
82
109
  // src/sticky-mobile-cta.tsx
@@ -96,17 +123,10 @@ function StickyMobileCta({ ctas }) {
96
123
 
97
124
  // src/editable-cta.tsx
98
125
  var import_jsx_runtime3 = require("react/jsx-runtime");
99
- async function EditableCta({
100
- slot,
101
- client,
102
- className,
103
- buttonClassName,
104
- size,
105
- fallback = null
106
- }) {
126
+ async function EditableCta({ slot, client, fallback = null, ...buttonProps }) {
107
127
  const { data: ctas } = await client.ctas.list(slot).catch(() => ({ data: [] }));
108
128
  if (ctas.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: fallback });
109
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CtaButtons, { ctas, className, buttonClassName, size });
129
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CtaButtons, { ctas, ...buttonProps });
110
130
  }
111
131
  // Annotate the CommonJS export names for ESM import in node:
112
132
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/next-cache-adapter.ts","../src/cta-buttons.tsx","../src/sticky-mobile-cta.tsx","../src/editable-cta.tsx"],"sourcesContent":["/**\n * `@reblu/site-next` — Next.js RSC glue for Reblu SITEs (REB-418 / design doc §5).\n *\n * Two things live here, both framework-coupled to Next so the lower SDK layers\n * (`@reblu/site-client`, `@reblu/site-contracts`, `@reblu/site-ui`) stay\n * framework-agnostic:\n *\n * 1. {@link NextCacheAdapter} — implements `@reblu/site-client`'s\n * `RebluCacheAdapter`, translating cache policy into Next ISR `fetch` options.\n * 2. The editable-CTA glue (`EditableCta` server component + its `CtaButtons`\n * client boundary + `StickyMobileCta`), consuming the injected SDK client.\n *\n * `EditableCta` re-exports the `'use client'` `CtaButtons` from its own emitted\n * chunk, so the RSC boundary survives the build (see the R4 smoke test).\n */\nexport { NextCacheAdapter } from './next-cache-adapter'\nexport { CtaButtons, type CtaButtonsProps } from './cta-buttons'\nexport { StickyMobileCta, type StickyMobileCtaProps } from './sticky-mobile-cta'\nexport { EditableCta, type EditableCtaProps, type CtaClient, type CtaSlot } from './editable-cta'\n","import type { RebluCacheAdapter, RebluCachePolicy } from '@reblu/site-client'\n\n/**\n * Next.js ISR cache adapter (REB-418 / design doc §5.2).\n *\n * Implements the framework-agnostic {@link RebluCacheAdapter} interface exported\n * by `@reblu/site-client`, translating a {@link RebluCachePolicy} into the\n * `fetch` options Next understands. This is the piece that restores the exact\n * ISR behavior demo-site hard-codes today — now injected instead of baked in:\n *\n * ```ts\n * createRebluClient({ baseUrl, apiKey, cache: new NextCacheAdapter(60) })\n * ```\n *\n * - `revalidate` > 0 → `{ next: { revalidate } }` (time-based ISR)\n * - `revalidate` `false` / `0` → `{ cache: 'no-store' }` (dynamic / preview)\n * - omitted `revalidate` → the constructor `defaultRevalidate`\n */\nexport class NextCacheAdapter implements RebluCacheAdapter {\n /**\n * @param defaultRevalidate ISR window (seconds) applied when a request omits\n * its own `revalidate`. Defaults to `60` for parity with demo-site's current\n * ISR default (`client.ts:45`). Pass `false`/`0` to default to no-store.\n */\n constructor(private readonly defaultRevalidate: number | false = 60) {}\n\n resolveRequestInit(policy: RebluCachePolicy): RequestInit {\n const revalidate = policy.revalidate ?? this.defaultRevalidate\n if (revalidate === false || revalidate === 0) {\n return { cache: 'no-store' }\n }\n return { next: { revalidate } } as RequestInit\n }\n}\n","'use client'\n\nimport type { Cta } from '@reblu/site-contracts'\nimport { Button } from '@reblu/site-ui'\nimport Link from 'next/link'\n\ntype ButtonSize = 'sm' | 'default' | 'lg'\n\nexport interface CtaButtonsProps {\n ctas: Cta[]\n /** Wrapper class (layout of the button group). */\n className?: string\n /** Class applied to each button. */\n buttonClassName?: string\n size?: ButtonSize\n}\n\n/** External = absolute http(s) URL (e.g. a Klinikare booking link). */\nfunction isExternal(href: string): boolean {\n return /^https?:\\/\\//i.test(href)\n}\n\n/**\n * Presentational CTA renderer (REB-394, moved to `@reblu/site-next` in REB-418).\n * Pure — takes already-resolved CTAs and renders one styled button-link each.\n * Internal hrefs use `next/link` (client routing); external URLs open in a new,\n * safely-rel tab. `analyticsId` is surfaced as `data-cta-id` on the clickable\n * element for GA `cta_click` tracking. Renders nothing when there are no CTAs.\n *\n * Imports `Button` from `@reblu/site-ui` (public) — never `@reblu/ui` (private).\n */\nexport function CtaButtons({ ctas, className, buttonClassName, size = 'lg' }: CtaButtonsProps) {\n if (ctas.length === 0) return null\n\n return (\n <div className={className} data-slot='cta-buttons'>\n {ctas.map((cta) => (\n <Button\n key={cta.id}\n asChild\n size={size}\n className={buttonClassName}\n data-cta-id={cta.analyticsId ?? undefined}\n >\n {isExternal(cta.href) ? (\n <a href={cta.href} target='_blank' rel='noopener noreferrer'>\n {cta.label}\n </a>\n ) : (\n <Link href={cta.href}>{cta.label}</Link>\n )}\n </Button>\n ))}\n </div>\n )\n}\n","import type { Cta } from '@reblu/site-contracts'\n\nimport { CtaButtons } from './cta-buttons'\n\nexport interface StickyMobileCtaProps {\n ctas: Cta[]\n}\n\n/**\n * StickyMobileCta (REB-394, moved to `@reblu/site-next` in REB-418) — a\n * mobile-only sticky bottom bar surfacing the `sticky-mobile` slot CTAs. Hidden\n * on `md+` (desktop has its own CTAs). Renders nothing when no CTA is configured,\n * so it never adds empty chrome.\n */\nexport function StickyMobileCta({ ctas }: StickyMobileCtaProps) {\n if (ctas.length === 0) return null\n\n return (\n <div className='fixed inset-x-0 bottom-0 z-50 md:hidden border-t border-border bg-background/95 px-4 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/80'>\n <CtaButtons\n ctas={ctas}\n className='flex justify-center gap-3'\n buttonClassName='w-full rounded-lg font-semibold'\n size='lg'\n />\n </div>\n )\n}\n","import type { RebluClient } from '@reblu/site-client'\nimport type { Cta } from '@reblu/site-contracts'\nimport type { ReactNode } from 'react'\n\nimport { CtaButtons } from './cta-buttons'\n\ntype ButtonSize = 'sm' | 'default' | 'lg'\n\n/** The `Cta.slot` union — the valid placement keys, sourced from the contract. */\nexport type CtaSlot = Cta['slot']\n\n/**\n * The slice of the SDK client `EditableCta` needs: just the `ctas` namespace.\n * Typing it structurally lets a SITE pass either the full `RebluClient` or a\n * narrower singleton, without coupling this component to the whole surface.\n */\nexport type CtaClient = Pick<RebluClient, 'ctas'>\n\nexport interface EditableCtaProps {\n /** CtaSlot value: hero | blog-end | services | contact | sticky-mobile | footer. */\n slot: CtaSlot\n /** The Reblu SDK client instance the SITE owns (env-configured upstream). */\n client: CtaClient\n className?: string\n buttonClassName?: string\n size?: ButtonSize\n /**\n * Rendered when the slot has no configured CTA (or the upstream fetch fails).\n * Lets each placement keep its existing static CTA as a graceful fallback.\n */\n fallback?: ReactNode\n}\n\n/**\n * EditableCta (REB-394, moved to `@reblu/site-next` in REB-418) — drop-anywhere\n * async Server Component that renders the CTAs configured for a slot from nexus,\n * with no redeploy. Fetches active CTAs via the injected SDK client and degrades\n * gracefully: on empty/failure it renders `fallback` (a static CTA) instead of\n * breaking the page.\n *\n * The client is injected (prop or per-SITE singleton) so this package never\n * reads `process.env` and stays framework-glue only — the SITE owns the env and\n * wires the ISR-aware {@link NextCacheAdapter}.\n */\nexport async function EditableCta({\n slot,\n client,\n className,\n buttonClassName,\n size,\n fallback = null,\n}: EditableCtaProps) {\n const { data: ctas } = await client.ctas\n .list(slot)\n .catch(() => ({ data: [] as Cta[] }))\n\n if (ctas.length === 0) return <>{fallback}</>\n\n return (\n <CtaButtons ctas={ctas} className={className} buttonClassName={buttonClassName} size={size} />\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACkBO,IAAM,mBAAN,MAAoD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMzD,YAA6B,oBAAoC,IAAI;AAAxC;AAAA,EAAyC;AAAA,EAAzC;AAAA,EAE7B,mBAAmB,QAAuC;AACxD,UAAM,aAAa,OAAO,cAAc,KAAK;AAC7C,QAAI,eAAe,SAAS,eAAe,GAAG;AAC5C,aAAO,EAAE,OAAO,WAAW;AAAA,IAC7B;AACA,WAAO,EAAE,MAAM,EAAE,WAAW,EAAE;AAAA,EAChC;AACF;;;AC9BA,qBAAuB;AACvB,kBAAiB;AAyCL;AA3BZ,SAAS,WAAW,MAAuB;AACzC,SAAO,gBAAgB,KAAK,IAAI;AAClC;AAWO,SAAS,WAAW,EAAE,MAAM,WAAW,iBAAiB,OAAO,KAAK,GAAoB;AAC7F,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,SACE,4CAAC,SAAI,WAAsB,aAAU,eAClC,eAAK,IAAI,CAAC,QACT;AAAA,IAAC;AAAA;AAAA,MAEC,SAAO;AAAA,MACP;AAAA,MACA,WAAW;AAAA,MACX,eAAa,IAAI,eAAe;AAAA,MAE/B,qBAAW,IAAI,IAAI,IAClB,4CAAC,OAAE,MAAM,IAAI,MAAM,QAAO,UAAS,KAAI,uBACpC,cAAI,OACP,IAEA,4CAAC,YAAAA,SAAA,EAAK,MAAM,IAAI,MAAO,cAAI,OAAM;AAAA;AAAA,IAX9B,IAAI;AAAA,EAaX,CACD,GACH;AAEJ;;;ACpCM,IAAAC,sBAAA;AALC,SAAS,gBAAgB,EAAE,KAAK,GAAyB;AAC9D,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,SACE,6CAAC,SAAI,WAAU,uJACb;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAU;AAAA,MACV,iBAAgB;AAAA,MAChB,MAAK;AAAA;AAAA,EACP,GACF;AAEJ;;;AC6BgC,IAAAC,sBAAA;AAZhC,eAAsB,YAAY;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AACb,GAAqB;AACnB,QAAM,EAAE,MAAM,KAAK,IAAI,MAAM,OAAO,KACjC,KAAK,IAAI,EACT,MAAM,OAAO,EAAE,MAAM,CAAC,EAAW,EAAE;AAEtC,MAAI,KAAK,WAAW,EAAG,QAAO,6EAAG,oBAAS;AAE1C,SACE,6CAAC,cAAW,MAAY,WAAsB,iBAAkC,MAAY;AAEhG;","names":["Link","import_jsx_runtime","import_jsx_runtime"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/next-cache-adapter.ts","../src/cta-buttons.tsx","../src/sticky-mobile-cta.tsx","../src/editable-cta.tsx"],"sourcesContent":["/**\n * `@reblu/site-next` — Next.js RSC glue for Reblu SITEs (REB-418 / design doc §5).\n *\n * Two things live here, both framework-coupled to Next so the lower SDK layers\n * (`@reblu/site-client`, `@reblu/site-contracts`, `@reblu/site-ui`) stay\n * framework-agnostic:\n *\n * 1. {@link NextCacheAdapter} — implements `@reblu/site-client`'s\n * `RebluCacheAdapter`, translating cache policy into Next ISR `fetch` options.\n * 2. The editable-CTA glue (`EditableCta` server component + its `CtaButtons`\n * client boundary + `StickyMobileCta`), consuming the injected SDK client.\n *\n * `EditableCta` re-exports the `'use client'` `CtaButtons` from its own emitted\n * chunk, so the RSC boundary survives the build (see the R4 smoke test).\n */\nexport { NextCacheAdapter } from './next-cache-adapter'\nexport { CtaButtons, type CtaButtonsProps, type CtaButtonVariant } from './cta-buttons'\nexport { StickyMobileCta, type StickyMobileCtaProps } from './sticky-mobile-cta'\nexport { EditableCta, type EditableCtaProps, type CtaClient, type CtaSlot } from './editable-cta'\n","import type { RebluCacheAdapter, RebluCachePolicy } from '@reblu/site-client'\n\n/**\n * Next.js ISR cache adapter (REB-418 / design doc §5.2).\n *\n * Implements the framework-agnostic {@link RebluCacheAdapter} interface exported\n * by `@reblu/site-client`, translating a {@link RebluCachePolicy} into the\n * `fetch` options Next understands. This is the piece that restores the exact\n * ISR behavior demo-site hard-codes today — now injected instead of baked in:\n *\n * ```ts\n * createRebluClient({ baseUrl, apiKey, cache: new NextCacheAdapter(60) })\n * ```\n *\n * - `revalidate` > 0 → `{ next: { revalidate } }` (time-based ISR)\n * - `revalidate` `false` / `0` → `{ cache: 'no-store' }` (dynamic / preview)\n * - omitted `revalidate` → the constructor `defaultRevalidate`\n */\nexport class NextCacheAdapter implements RebluCacheAdapter {\n /**\n * @param defaultRevalidate ISR window (seconds) applied when a request omits\n * its own `revalidate`. Defaults to `60` for parity with demo-site's current\n * ISR default (`client.ts:45`). Pass `false`/`0` to default to no-store.\n */\n constructor(private readonly defaultRevalidate: number | false = 60) {}\n\n resolveRequestInit(policy: RebluCachePolicy): RequestInit {\n const revalidate = policy.revalidate ?? this.defaultRevalidate\n if (revalidate === false || revalidate === 0) {\n return { cache: 'no-store' }\n }\n return { next: { revalidate } } as RequestInit\n }\n}\n","'use client'\n\nimport type { Cta } from '@reblu/site-contracts'\nimport { Button, cn, type ButtonVariantProps } from '@reblu/site-ui'\nimport Link from 'next/link'\nimport type { ReactNode } from 'react'\n\ntype ButtonSize = 'sm' | 'default' | 'lg'\n\n/** A `@reblu/site-ui` Button variant (`default`, `outline`, `secondary`, …). */\nexport type CtaButtonVariant = NonNullable<ButtonVariantProps['variant']>\n\n/** Default screen-reader hint appended to CTAs that open in a new tab. */\nconst DEFAULT_NEW_WINDOW_HINT = '(se abre en una nueva ventana)'\n\nexport interface CtaButtonsProps {\n ctas: Cta[]\n /** Wrapper class (layout of the button group). */\n className?: string\n /** Class applied to each button. */\n buttonClassName?: string\n size?: ButtonSize\n /**\n * Leading icon rendered before every CTA label. Decorative: it is wrapped in\n * an `aria-hidden` span, so the accessible name stays the label.\n */\n icon?: ReactNode\n /** Button variant of the primary CTA (the first one, by `sortOrder`). Default `default`. */\n primaryVariant?: CtaButtonVariant\n /** Button variant of every secondary CTA (all but the first). Default `default`. */\n secondaryVariant?: CtaButtonVariant\n /**\n * Extra class for the primary CTA, merged after `buttonClassName`. Lets a SITE\n * map the primary CTA to its own button style (e.g. a brand `cta` variant).\n */\n primaryClassName?: string\n /** Extra class for every secondary CTA, merged after `buttonClassName`. */\n secondaryClassName?: string\n /**\n * Visually-hidden text appended to CTAs that open in a new tab, so screen\n * readers announce the context change. Default «(se abre en una nueva ventana)».\n */\n newWindowHint?: string\n}\n\n/** External = absolute http(s) URL (e.g. a third-party booking link). */\nfunction isExternal(href: string): boolean {\n return /^https?:\\/\\//i.test(href)\n}\n\n/**\n * Presentational CTA renderer (REB-394, moved to `@reblu/site-next` in REB-418).\n * Pure — takes already-resolved CTAs and renders one styled button-link each.\n * Internal hrefs use `next/link` (client routing); external URLs open in a new,\n * safely-rel tab and carry a visually-hidden new-window hint. `analyticsId` is\n * surfaced as `data-cta-id` on the clickable element for GA `cta_click`\n * tracking. Renders nothing when there are no CTAs.\n *\n * The first CTA is the primary one; the rest are secondary. Each role takes its\n * own Button variant and extra class, so a SITE maps the CTAs to its own button\n * styles without forking this markup.\n *\n * Imports `Button` from `@reblu/site-ui` (public) — never `@reblu/ui` (private).\n */\nexport function CtaButtons({\n ctas,\n className,\n buttonClassName,\n size = 'lg',\n icon,\n primaryVariant = 'default',\n secondaryVariant = 'default',\n primaryClassName,\n secondaryClassName,\n newWindowHint = DEFAULT_NEW_WINDOW_HINT,\n}: CtaButtonsProps) {\n if (ctas.length === 0) return null\n\n const leadingIcon =\n icon == null ? null : (\n <span aria-hidden='true' className='inline-flex shrink-0'>\n {icon}\n </span>\n )\n\n return (\n <div className={className} data-slot='cta-buttons'>\n {ctas.map((cta, index) => {\n const isPrimary = index === 0\n return (\n <Button\n key={cta.id}\n asChild\n size={size}\n variant={isPrimary ? primaryVariant : secondaryVariant}\n className={cn(buttonClassName, isPrimary ? primaryClassName : secondaryClassName)}\n data-cta-id={cta.analyticsId ?? undefined}\n >\n {isExternal(cta.href) ? (\n <a href={cta.href} target='_blank' rel='noopener noreferrer'>\n {leadingIcon}\n {cta.label}\n {newWindowHint ? <> <span className='sr-only'>{newWindowHint}</span></> : null}\n </a>\n ) : (\n <Link href={cta.href}>\n {leadingIcon}\n {cta.label}\n </Link>\n )}\n </Button>\n )\n })}\n </div>\n )\n}\n","import type { Cta } from '@reblu/site-contracts'\n\nimport { CtaButtons } from './cta-buttons'\n\nexport interface StickyMobileCtaProps {\n ctas: Cta[]\n}\n\n/**\n * StickyMobileCta (REB-394, moved to `@reblu/site-next` in REB-418) — a\n * mobile-only sticky bottom bar surfacing the `sticky-mobile` slot CTAs. Hidden\n * on `md+` (desktop has its own CTAs). Renders nothing when no CTA is configured,\n * so it never adds empty chrome.\n */\nexport function StickyMobileCta({ ctas }: StickyMobileCtaProps) {\n if (ctas.length === 0) return null\n\n return (\n <div className='fixed inset-x-0 bottom-0 z-50 md:hidden border-t border-border bg-background/95 px-4 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/80'>\n <CtaButtons\n ctas={ctas}\n className='flex justify-center gap-3'\n buttonClassName='w-full rounded-lg font-semibold'\n size='lg'\n />\n </div>\n )\n}\n","import type { RebluClient } from '@reblu/site-client'\nimport type { Cta } from '@reblu/site-contracts'\nimport type { ReactNode } from 'react'\n\nimport { CtaButtons, type CtaButtonsProps } from './cta-buttons'\n\n/** The `Cta.slot` union — the valid placement keys, sourced from the contract. */\nexport type CtaSlot = Cta['slot']\n\n/**\n * The slice of the SDK client `EditableCta` needs: just the `ctas` namespace.\n * Typing it structurally lets a SITE pass either the full `RebluClient` or a\n * narrower singleton, without coupling this component to the whole surface.\n */\nexport type CtaClient = Pick<RebluClient, 'ctas'>\n\n/** Presentation props `EditableCta` forwards unchanged to {@link CtaButtons}. */\ntype ForwardedButtonProps = Omit<CtaButtonsProps, 'ctas'>\n\nexport interface EditableCtaProps extends ForwardedButtonProps {\n /** CtaSlot value: header | hero | blog-end | services | contact | sticky-mobile | footer. */\n slot: CtaSlot\n /** The Reblu SDK client instance the SITE owns (env-configured upstream). */\n client: CtaClient\n /**\n * Rendered when the slot has no configured CTA (or the upstream fetch fails).\n * Lets each placement keep its existing static CTA as a graceful fallback.\n */\n fallback?: ReactNode\n}\n\n/**\n * EditableCta (REB-394, moved to `@reblu/site-next` in REB-418) — drop-anywhere\n * async Server Component that renders the CTAs configured for a slot from nexus,\n * with no redeploy. Fetches active CTAs via the injected SDK client and degrades\n * gracefully: on empty/failure it renders `fallback` (a static CTA) instead of\n * breaking the page.\n *\n * The client is injected (prop or per-SITE singleton) so this package never\n * reads `process.env` and stays framework-glue only — the SITE owns the env and\n * wires the ISR-aware {@link NextCacheAdapter}.\n */\nexport async function EditableCta({ slot, client, fallback = null, ...buttonProps }: EditableCtaProps) {\n const { data: ctas } = await client.ctas\n .list(slot)\n .catch(() => ({ data: [] as Cta[] }))\n\n if (ctas.length === 0) return <>{fallback}</>\n\n return <CtaButtons ctas={ctas} {...buttonProps} />\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACkBO,IAAM,mBAAN,MAAoD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMzD,YAA6B,oBAAoC,IAAI;AAAxC;AAAA,EAAyC;AAAA,EAAzC;AAAA,EAE7B,mBAAmB,QAAuC;AACxD,UAAM,aAAa,OAAO,cAAc,KAAK;AAC7C,QAAI,eAAe,SAAS,eAAe,GAAG;AAC5C,aAAO,EAAE,OAAO,WAAW;AAAA,IAC7B;AACA,WAAO,EAAE,MAAM,EAAE,WAAW,EAAE;AAAA,EAChC;AACF;;;AC9BA,qBAAoD;AACpD,kBAAiB;AA4EX;AAnEN,IAAM,0BAA0B;AAiChC,SAAS,WAAW,MAAuB;AACzC,SAAO,gBAAgB,KAAK,IAAI;AAClC;AAgBO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB;AAAA,EACA;AAAA,EACA,gBAAgB;AAClB,GAAoB;AAClB,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,QAAM,cACJ,QAAQ,OAAO,OACb,4CAAC,UAAK,eAAY,QAAO,WAAU,wBAChC,gBACH;AAGJ,SACE,4CAAC,SAAI,WAAsB,aAAU,eAClC,eAAK,IAAI,CAAC,KAAK,UAAU;AACxB,UAAM,YAAY,UAAU;AAC5B,WACE;AAAA,MAAC;AAAA;AAAA,QAEC,SAAO;AAAA,QACP;AAAA,QACA,SAAS,YAAY,iBAAiB;AAAA,QACtC,eAAW,mBAAG,iBAAiB,YAAY,mBAAmB,kBAAkB;AAAA,QAChF,eAAa,IAAI,eAAe;AAAA,QAE/B,qBAAW,IAAI,IAAI,IAClB,6CAAC,OAAE,MAAM,IAAI,MAAM,QAAO,UAAS,KAAI,uBACpC;AAAA;AAAA,UACA,IAAI;AAAA,UACJ,gBAAgB,4EAAE;AAAA;AAAA,YAAC,4CAAC,UAAK,WAAU,WAAW,yBAAc;AAAA,aAAO,IAAM;AAAA,WAC5E,IAEA,6CAAC,YAAAA,SAAA,EAAK,MAAM,IAAI,MACb;AAAA;AAAA,UACA,IAAI;AAAA,WACP;AAAA;AAAA,MAjBG,IAAI;AAAA,IAmBX;AAAA,EAEJ,CAAC,GACH;AAEJ;;;AChGM,IAAAC,sBAAA;AALC,SAAS,gBAAgB,EAAE,KAAK,GAAyB;AAC9D,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,SACE,6CAAC,SAAI,WAAU,uJACb;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAU;AAAA,MACV,iBAAgB;AAAA,MAChB,MAAK;AAAA;AAAA,EACP,GACF;AAEJ;;;ACoBgC,IAAAC,sBAAA;AALhC,eAAsB,YAAY,EAAE,MAAM,QAAQ,WAAW,MAAM,GAAG,YAAY,GAAqB;AACrG,QAAM,EAAE,MAAM,KAAK,IAAI,MAAM,OAAO,KACjC,KAAK,IAAI,EACT,MAAM,OAAO,EAAE,MAAM,CAAC,EAAW,EAAE;AAEtC,MAAI,KAAK,WAAW,EAAG,QAAO,6EAAG,oBAAS;AAE1C,SAAO,6CAAC,cAAW,MAAa,GAAG,aAAa;AAClD;","names":["Link","import_jsx_runtime","import_jsx_runtime"]}
package/dist/index.d.cts CHANGED
@@ -1,9 +1,11 @@
1
1
  export { NextCacheAdapter } from './cache.cjs';
2
- export { CtaButtons, CtaButtonsProps } from './cta-buttons.cjs';
2
+ import { CtaButtonsProps } from './cta-buttons.cjs';
3
+ export { CtaButtonVariant, CtaButtons } from './cta-buttons.cjs';
3
4
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
5
  import { Cta } from '@reblu/site-contracts';
5
6
  import { RebluClient } from '@reblu/site-client';
6
7
  import { ReactNode } from 'react';
8
+ import '@reblu/site-ui';
7
9
 
8
10
  interface StickyMobileCtaProps {
9
11
  ctas: Cta[];
@@ -16,7 +18,6 @@ interface StickyMobileCtaProps {
16
18
  */
17
19
  declare function StickyMobileCta({ ctas }: StickyMobileCtaProps): react_jsx_runtime.JSX.Element | null;
18
20
 
19
- type ButtonSize = 'sm' | 'default' | 'lg';
20
21
  /** The `Cta.slot` union — the valid placement keys, sourced from the contract. */
21
22
  type CtaSlot = Cta['slot'];
22
23
  /**
@@ -25,14 +26,13 @@ type CtaSlot = Cta['slot'];
25
26
  * narrower singleton, without coupling this component to the whole surface.
26
27
  */
27
28
  type CtaClient = Pick<RebluClient, 'ctas'>;
28
- interface EditableCtaProps {
29
- /** CtaSlot value: hero | blog-end | services | contact | sticky-mobile | footer. */
29
+ /** Presentation props `EditableCta` forwards unchanged to {@link CtaButtons}. */
30
+ type ForwardedButtonProps = Omit<CtaButtonsProps, 'ctas'>;
31
+ interface EditableCtaProps extends ForwardedButtonProps {
32
+ /** CtaSlot value: header | hero | blog-end | services | contact | sticky-mobile | footer. */
30
33
  slot: CtaSlot;
31
34
  /** The Reblu SDK client instance the SITE owns (env-configured upstream). */
32
35
  client: CtaClient;
33
- className?: string;
34
- buttonClassName?: string;
35
- size?: ButtonSize;
36
36
  /**
37
37
  * Rendered when the slot has no configured CTA (or the upstream fetch fails).
38
38
  * Lets each placement keep its existing static CTA as a graceful fallback.
@@ -50,6 +50,6 @@ interface EditableCtaProps {
50
50
  * reads `process.env` and stays framework-glue only — the SITE owns the env and
51
51
  * wires the ISR-aware {@link NextCacheAdapter}.
52
52
  */
53
- declare function EditableCta({ slot, client, className, buttonClassName, size, fallback, }: EditableCtaProps): Promise<react_jsx_runtime.JSX.Element>;
53
+ declare function EditableCta({ slot, client, fallback, ...buttonProps }: EditableCtaProps): Promise<react_jsx_runtime.JSX.Element>;
54
54
 
55
- export { type CtaClient, type CtaSlot, EditableCta, type EditableCtaProps, StickyMobileCta, type StickyMobileCtaProps };
55
+ export { CtaButtonsProps, type CtaClient, type CtaSlot, EditableCta, type EditableCtaProps, StickyMobileCta, type StickyMobileCtaProps };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  export { NextCacheAdapter } from './cache.js';
2
- export { CtaButtons, CtaButtonsProps } from './cta-buttons.js';
2
+ import { CtaButtonsProps } from './cta-buttons.js';
3
+ export { CtaButtonVariant, CtaButtons } from './cta-buttons.js';
3
4
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
5
  import { Cta } from '@reblu/site-contracts';
5
6
  import { RebluClient } from '@reblu/site-client';
6
7
  import { ReactNode } from 'react';
8
+ import '@reblu/site-ui';
7
9
 
8
10
  interface StickyMobileCtaProps {
9
11
  ctas: Cta[];
@@ -16,7 +18,6 @@ interface StickyMobileCtaProps {
16
18
  */
17
19
  declare function StickyMobileCta({ ctas }: StickyMobileCtaProps): react_jsx_runtime.JSX.Element | null;
18
20
 
19
- type ButtonSize = 'sm' | 'default' | 'lg';
20
21
  /** The `Cta.slot` union — the valid placement keys, sourced from the contract. */
21
22
  type CtaSlot = Cta['slot'];
22
23
  /**
@@ -25,14 +26,13 @@ type CtaSlot = Cta['slot'];
25
26
  * narrower singleton, without coupling this component to the whole surface.
26
27
  */
27
28
  type CtaClient = Pick<RebluClient, 'ctas'>;
28
- interface EditableCtaProps {
29
- /** CtaSlot value: hero | blog-end | services | contact | sticky-mobile | footer. */
29
+ /** Presentation props `EditableCta` forwards unchanged to {@link CtaButtons}. */
30
+ type ForwardedButtonProps = Omit<CtaButtonsProps, 'ctas'>;
31
+ interface EditableCtaProps extends ForwardedButtonProps {
32
+ /** CtaSlot value: header | hero | blog-end | services | contact | sticky-mobile | footer. */
30
33
  slot: CtaSlot;
31
34
  /** The Reblu SDK client instance the SITE owns (env-configured upstream). */
32
35
  client: CtaClient;
33
- className?: string;
34
- buttonClassName?: string;
35
- size?: ButtonSize;
36
36
  /**
37
37
  * Rendered when the slot has no configured CTA (or the upstream fetch fails).
38
38
  * Lets each placement keep its existing static CTA as a graceful fallback.
@@ -50,6 +50,6 @@ interface EditableCtaProps {
50
50
  * reads `process.env` and stays framework-glue only — the SITE owns the env and
51
51
  * wires the ISR-aware {@link NextCacheAdapter}.
52
52
  */
53
- declare function EditableCta({ slot, client, className, buttonClassName, size, fallback, }: EditableCtaProps): Promise<react_jsx_runtime.JSX.Element>;
53
+ declare function EditableCta({ slot, client, fallback, ...buttonProps }: EditableCtaProps): Promise<react_jsx_runtime.JSX.Element>;
54
54
 
55
- export { type CtaClient, type CtaSlot, EditableCta, type EditableCtaProps, StickyMobileCta, type StickyMobileCtaProps };
55
+ export { CtaButtonsProps, type CtaClient, type CtaSlot, EditableCta, type EditableCtaProps, StickyMobileCta, type StickyMobileCtaProps };
package/dist/index.js CHANGED
@@ -19,25 +19,52 @@ var NextCacheAdapter = class {
19
19
  };
20
20
 
21
21
  // src/cta-buttons.tsx
22
- import { Button } from "@reblu/site-ui";
22
+ import { Button, cn } from "@reblu/site-ui";
23
23
  import Link from "next/link";
24
- import { jsx } from "react/jsx-runtime";
24
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
25
+ var DEFAULT_NEW_WINDOW_HINT = "(se abre en una nueva ventana)";
25
26
  function isExternal(href) {
26
27
  return /^https?:\/\//i.test(href);
27
28
  }
28
- function CtaButtons({ ctas, className, buttonClassName, size = "lg" }) {
29
+ function CtaButtons({
30
+ ctas,
31
+ className,
32
+ buttonClassName,
33
+ size = "lg",
34
+ icon,
35
+ primaryVariant = "default",
36
+ secondaryVariant = "default",
37
+ primaryClassName,
38
+ secondaryClassName,
39
+ newWindowHint = DEFAULT_NEW_WINDOW_HINT
40
+ }) {
29
41
  if (ctas.length === 0) return null;
30
- return /* @__PURE__ */ jsx("div", { className, "data-slot": "cta-buttons", children: ctas.map((cta) => /* @__PURE__ */ jsx(
31
- Button,
32
- {
33
- asChild: true,
34
- size,
35
- className: buttonClassName,
36
- "data-cta-id": cta.analyticsId ?? void 0,
37
- children: isExternal(cta.href) ? /* @__PURE__ */ jsx("a", { href: cta.href, target: "_blank", rel: "noopener noreferrer", children: cta.label }) : /* @__PURE__ */ jsx(Link, { href: cta.href, children: cta.label })
38
- },
39
- cta.id
40
- )) });
42
+ const leadingIcon = icon == null ? null : /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "inline-flex shrink-0", children: icon });
43
+ return /* @__PURE__ */ jsx("div", { className, "data-slot": "cta-buttons", children: ctas.map((cta, index) => {
44
+ const isPrimary = index === 0;
45
+ return /* @__PURE__ */ jsx(
46
+ Button,
47
+ {
48
+ asChild: true,
49
+ size,
50
+ variant: isPrimary ? primaryVariant : secondaryVariant,
51
+ className: cn(buttonClassName, isPrimary ? primaryClassName : secondaryClassName),
52
+ "data-cta-id": cta.analyticsId ?? void 0,
53
+ children: isExternal(cta.href) ? /* @__PURE__ */ jsxs("a", { href: cta.href, target: "_blank", rel: "noopener noreferrer", children: [
54
+ leadingIcon,
55
+ cta.label,
56
+ newWindowHint ? /* @__PURE__ */ jsxs(Fragment, { children: [
57
+ " ",
58
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: newWindowHint })
59
+ ] }) : null
60
+ ] }) : /* @__PURE__ */ jsxs(Link, { href: cta.href, children: [
61
+ leadingIcon,
62
+ cta.label
63
+ ] })
64
+ },
65
+ cta.id
66
+ );
67
+ }) });
41
68
  }
42
69
 
43
70
  // src/sticky-mobile-cta.tsx
@@ -56,18 +83,11 @@ function StickyMobileCta({ ctas }) {
56
83
  }
57
84
 
58
85
  // src/editable-cta.tsx
59
- import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
60
- async function EditableCta({
61
- slot,
62
- client,
63
- className,
64
- buttonClassName,
65
- size,
66
- fallback = null
67
- }) {
86
+ import { Fragment as Fragment2, jsx as jsx3 } from "react/jsx-runtime";
87
+ async function EditableCta({ slot, client, fallback = null, ...buttonProps }) {
68
88
  const { data: ctas } = await client.ctas.list(slot).catch(() => ({ data: [] }));
69
- if (ctas.length === 0) return /* @__PURE__ */ jsx3(Fragment, { children: fallback });
70
- return /* @__PURE__ */ jsx3(CtaButtons, { ctas, className, buttonClassName, size });
89
+ if (ctas.length === 0) return /* @__PURE__ */ jsx3(Fragment2, { children: fallback });
90
+ return /* @__PURE__ */ jsx3(CtaButtons, { ctas, ...buttonProps });
71
91
  }
72
92
  export {
73
93
  CtaButtons,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/next-cache-adapter.ts","../src/cta-buttons.tsx","../src/sticky-mobile-cta.tsx","../src/editable-cta.tsx"],"sourcesContent":["import type { RebluCacheAdapter, RebluCachePolicy } from '@reblu/site-client'\n\n/**\n * Next.js ISR cache adapter (REB-418 / design doc §5.2).\n *\n * Implements the framework-agnostic {@link RebluCacheAdapter} interface exported\n * by `@reblu/site-client`, translating a {@link RebluCachePolicy} into the\n * `fetch` options Next understands. This is the piece that restores the exact\n * ISR behavior demo-site hard-codes today — now injected instead of baked in:\n *\n * ```ts\n * createRebluClient({ baseUrl, apiKey, cache: new NextCacheAdapter(60) })\n * ```\n *\n * - `revalidate` > 0 → `{ next: { revalidate } }` (time-based ISR)\n * - `revalidate` `false` / `0` → `{ cache: 'no-store' }` (dynamic / preview)\n * - omitted `revalidate` → the constructor `defaultRevalidate`\n */\nexport class NextCacheAdapter implements RebluCacheAdapter {\n /**\n * @param defaultRevalidate ISR window (seconds) applied when a request omits\n * its own `revalidate`. Defaults to `60` for parity with demo-site's current\n * ISR default (`client.ts:45`). Pass `false`/`0` to default to no-store.\n */\n constructor(private readonly defaultRevalidate: number | false = 60) {}\n\n resolveRequestInit(policy: RebluCachePolicy): RequestInit {\n const revalidate = policy.revalidate ?? this.defaultRevalidate\n if (revalidate === false || revalidate === 0) {\n return { cache: 'no-store' }\n }\n return { next: { revalidate } } as RequestInit\n }\n}\n","'use client'\n\nimport type { Cta } from '@reblu/site-contracts'\nimport { Button } from '@reblu/site-ui'\nimport Link from 'next/link'\n\ntype ButtonSize = 'sm' | 'default' | 'lg'\n\nexport interface CtaButtonsProps {\n ctas: Cta[]\n /** Wrapper class (layout of the button group). */\n className?: string\n /** Class applied to each button. */\n buttonClassName?: string\n size?: ButtonSize\n}\n\n/** External = absolute http(s) URL (e.g. a Klinikare booking link). */\nfunction isExternal(href: string): boolean {\n return /^https?:\\/\\//i.test(href)\n}\n\n/**\n * Presentational CTA renderer (REB-394, moved to `@reblu/site-next` in REB-418).\n * Pure — takes already-resolved CTAs and renders one styled button-link each.\n * Internal hrefs use `next/link` (client routing); external URLs open in a new,\n * safely-rel tab. `analyticsId` is surfaced as `data-cta-id` on the clickable\n * element for GA `cta_click` tracking. Renders nothing when there are no CTAs.\n *\n * Imports `Button` from `@reblu/site-ui` (public) — never `@reblu/ui` (private).\n */\nexport function CtaButtons({ ctas, className, buttonClassName, size = 'lg' }: CtaButtonsProps) {\n if (ctas.length === 0) return null\n\n return (\n <div className={className} data-slot='cta-buttons'>\n {ctas.map((cta) => (\n <Button\n key={cta.id}\n asChild\n size={size}\n className={buttonClassName}\n data-cta-id={cta.analyticsId ?? undefined}\n >\n {isExternal(cta.href) ? (\n <a href={cta.href} target='_blank' rel='noopener noreferrer'>\n {cta.label}\n </a>\n ) : (\n <Link href={cta.href}>{cta.label}</Link>\n )}\n </Button>\n ))}\n </div>\n )\n}\n","import type { Cta } from '@reblu/site-contracts'\n\nimport { CtaButtons } from './cta-buttons'\n\nexport interface StickyMobileCtaProps {\n ctas: Cta[]\n}\n\n/**\n * StickyMobileCta (REB-394, moved to `@reblu/site-next` in REB-418) — a\n * mobile-only sticky bottom bar surfacing the `sticky-mobile` slot CTAs. Hidden\n * on `md+` (desktop has its own CTAs). Renders nothing when no CTA is configured,\n * so it never adds empty chrome.\n */\nexport function StickyMobileCta({ ctas }: StickyMobileCtaProps) {\n if (ctas.length === 0) return null\n\n return (\n <div className='fixed inset-x-0 bottom-0 z-50 md:hidden border-t border-border bg-background/95 px-4 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/80'>\n <CtaButtons\n ctas={ctas}\n className='flex justify-center gap-3'\n buttonClassName='w-full rounded-lg font-semibold'\n size='lg'\n />\n </div>\n )\n}\n","import type { RebluClient } from '@reblu/site-client'\nimport type { Cta } from '@reblu/site-contracts'\nimport type { ReactNode } from 'react'\n\nimport { CtaButtons } from './cta-buttons'\n\ntype ButtonSize = 'sm' | 'default' | 'lg'\n\n/** The `Cta.slot` union — the valid placement keys, sourced from the contract. */\nexport type CtaSlot = Cta['slot']\n\n/**\n * The slice of the SDK client `EditableCta` needs: just the `ctas` namespace.\n * Typing it structurally lets a SITE pass either the full `RebluClient` or a\n * narrower singleton, without coupling this component to the whole surface.\n */\nexport type CtaClient = Pick<RebluClient, 'ctas'>\n\nexport interface EditableCtaProps {\n /** CtaSlot value: hero | blog-end | services | contact | sticky-mobile | footer. */\n slot: CtaSlot\n /** The Reblu SDK client instance the SITE owns (env-configured upstream). */\n client: CtaClient\n className?: string\n buttonClassName?: string\n size?: ButtonSize\n /**\n * Rendered when the slot has no configured CTA (or the upstream fetch fails).\n * Lets each placement keep its existing static CTA as a graceful fallback.\n */\n fallback?: ReactNode\n}\n\n/**\n * EditableCta (REB-394, moved to `@reblu/site-next` in REB-418) — drop-anywhere\n * async Server Component that renders the CTAs configured for a slot from nexus,\n * with no redeploy. Fetches active CTAs via the injected SDK client and degrades\n * gracefully: on empty/failure it renders `fallback` (a static CTA) instead of\n * breaking the page.\n *\n * The client is injected (prop or per-SITE singleton) so this package never\n * reads `process.env` and stays framework-glue only — the SITE owns the env and\n * wires the ISR-aware {@link NextCacheAdapter}.\n */\nexport async function EditableCta({\n slot,\n client,\n className,\n buttonClassName,\n size,\n fallback = null,\n}: EditableCtaProps) {\n const { data: ctas } = await client.ctas\n .list(slot)\n .catch(() => ({ data: [] as Cta[] }))\n\n if (ctas.length === 0) return <>{fallback}</>\n\n return (\n <CtaButtons ctas={ctas} className={className} buttonClassName={buttonClassName} size={size} />\n )\n}\n"],"mappings":";AAkBO,IAAM,mBAAN,MAAoD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMzD,YAA6B,oBAAoC,IAAI;AAAxC;AAAA,EAAyC;AAAA,EAAzC;AAAA,EAE7B,mBAAmB,QAAuC;AACxD,UAAM,aAAa,OAAO,cAAc,KAAK;AAC7C,QAAI,eAAe,SAAS,eAAe,GAAG;AAC5C,aAAO,EAAE,OAAO,WAAW;AAAA,IAC7B;AACA,WAAO,EAAE,MAAM,EAAE,WAAW,EAAE;AAAA,EAChC;AACF;;;AC9BA,SAAS,cAAc;AACvB,OAAO,UAAU;AAyCL;AA3BZ,SAAS,WAAW,MAAuB;AACzC,SAAO,gBAAgB,KAAK,IAAI;AAClC;AAWO,SAAS,WAAW,EAAE,MAAM,WAAW,iBAAiB,OAAO,KAAK,GAAoB;AAC7F,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,SACE,oBAAC,SAAI,WAAsB,aAAU,eAClC,eAAK,IAAI,CAAC,QACT;AAAA,IAAC;AAAA;AAAA,MAEC,SAAO;AAAA,MACP;AAAA,MACA,WAAW;AAAA,MACX,eAAa,IAAI,eAAe;AAAA,MAE/B,qBAAW,IAAI,IAAI,IAClB,oBAAC,OAAE,MAAM,IAAI,MAAM,QAAO,UAAS,KAAI,uBACpC,cAAI,OACP,IAEA,oBAAC,QAAK,MAAM,IAAI,MAAO,cAAI,OAAM;AAAA;AAAA,IAX9B,IAAI;AAAA,EAaX,CACD,GACH;AAEJ;;;ACpCM,gBAAAA,YAAA;AALC,SAAS,gBAAgB,EAAE,KAAK,GAAyB;AAC9D,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,SACE,gBAAAA,KAAC,SAAI,WAAU,uJACb,0BAAAA;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAU;AAAA,MACV,iBAAgB;AAAA,MAChB,MAAK;AAAA;AAAA,EACP,GACF;AAEJ;;;AC6BgC,0BAAAC,YAAA;AAZhC,eAAsB,YAAY;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AACb,GAAqB;AACnB,QAAM,EAAE,MAAM,KAAK,IAAI,MAAM,OAAO,KACjC,KAAK,IAAI,EACT,MAAM,OAAO,EAAE,MAAM,CAAC,EAAW,EAAE;AAEtC,MAAI,KAAK,WAAW,EAAG,QAAO,gBAAAA,KAAA,YAAG,oBAAS;AAE1C,SACE,gBAAAA,KAAC,cAAW,MAAY,WAAsB,iBAAkC,MAAY;AAEhG;","names":["jsx","jsx"]}
1
+ {"version":3,"sources":["../src/next-cache-adapter.ts","../src/cta-buttons.tsx","../src/sticky-mobile-cta.tsx","../src/editable-cta.tsx"],"sourcesContent":["import type { RebluCacheAdapter, RebluCachePolicy } from '@reblu/site-client'\n\n/**\n * Next.js ISR cache adapter (REB-418 / design doc §5.2).\n *\n * Implements the framework-agnostic {@link RebluCacheAdapter} interface exported\n * by `@reblu/site-client`, translating a {@link RebluCachePolicy} into the\n * `fetch` options Next understands. This is the piece that restores the exact\n * ISR behavior demo-site hard-codes today — now injected instead of baked in:\n *\n * ```ts\n * createRebluClient({ baseUrl, apiKey, cache: new NextCacheAdapter(60) })\n * ```\n *\n * - `revalidate` > 0 → `{ next: { revalidate } }` (time-based ISR)\n * - `revalidate` `false` / `0` → `{ cache: 'no-store' }` (dynamic / preview)\n * - omitted `revalidate` → the constructor `defaultRevalidate`\n */\nexport class NextCacheAdapter implements RebluCacheAdapter {\n /**\n * @param defaultRevalidate ISR window (seconds) applied when a request omits\n * its own `revalidate`. Defaults to `60` for parity with demo-site's current\n * ISR default (`client.ts:45`). Pass `false`/`0` to default to no-store.\n */\n constructor(private readonly defaultRevalidate: number | false = 60) {}\n\n resolveRequestInit(policy: RebluCachePolicy): RequestInit {\n const revalidate = policy.revalidate ?? this.defaultRevalidate\n if (revalidate === false || revalidate === 0) {\n return { cache: 'no-store' }\n }\n return { next: { revalidate } } as RequestInit\n }\n}\n","'use client'\n\nimport type { Cta } from '@reblu/site-contracts'\nimport { Button, cn, type ButtonVariantProps } from '@reblu/site-ui'\nimport Link from 'next/link'\nimport type { ReactNode } from 'react'\n\ntype ButtonSize = 'sm' | 'default' | 'lg'\n\n/** A `@reblu/site-ui` Button variant (`default`, `outline`, `secondary`, …). */\nexport type CtaButtonVariant = NonNullable<ButtonVariantProps['variant']>\n\n/** Default screen-reader hint appended to CTAs that open in a new tab. */\nconst DEFAULT_NEW_WINDOW_HINT = '(se abre en una nueva ventana)'\n\nexport interface CtaButtonsProps {\n ctas: Cta[]\n /** Wrapper class (layout of the button group). */\n className?: string\n /** Class applied to each button. */\n buttonClassName?: string\n size?: ButtonSize\n /**\n * Leading icon rendered before every CTA label. Decorative: it is wrapped in\n * an `aria-hidden` span, so the accessible name stays the label.\n */\n icon?: ReactNode\n /** Button variant of the primary CTA (the first one, by `sortOrder`). Default `default`. */\n primaryVariant?: CtaButtonVariant\n /** Button variant of every secondary CTA (all but the first). Default `default`. */\n secondaryVariant?: CtaButtonVariant\n /**\n * Extra class for the primary CTA, merged after `buttonClassName`. Lets a SITE\n * map the primary CTA to its own button style (e.g. a brand `cta` variant).\n */\n primaryClassName?: string\n /** Extra class for every secondary CTA, merged after `buttonClassName`. */\n secondaryClassName?: string\n /**\n * Visually-hidden text appended to CTAs that open in a new tab, so screen\n * readers announce the context change. Default «(se abre en una nueva ventana)».\n */\n newWindowHint?: string\n}\n\n/** External = absolute http(s) URL (e.g. a third-party booking link). */\nfunction isExternal(href: string): boolean {\n return /^https?:\\/\\//i.test(href)\n}\n\n/**\n * Presentational CTA renderer (REB-394, moved to `@reblu/site-next` in REB-418).\n * Pure — takes already-resolved CTAs and renders one styled button-link each.\n * Internal hrefs use `next/link` (client routing); external URLs open in a new,\n * safely-rel tab and carry a visually-hidden new-window hint. `analyticsId` is\n * surfaced as `data-cta-id` on the clickable element for GA `cta_click`\n * tracking. Renders nothing when there are no CTAs.\n *\n * The first CTA is the primary one; the rest are secondary. Each role takes its\n * own Button variant and extra class, so a SITE maps the CTAs to its own button\n * styles without forking this markup.\n *\n * Imports `Button` from `@reblu/site-ui` (public) — never `@reblu/ui` (private).\n */\nexport function CtaButtons({\n ctas,\n className,\n buttonClassName,\n size = 'lg',\n icon,\n primaryVariant = 'default',\n secondaryVariant = 'default',\n primaryClassName,\n secondaryClassName,\n newWindowHint = DEFAULT_NEW_WINDOW_HINT,\n}: CtaButtonsProps) {\n if (ctas.length === 0) return null\n\n const leadingIcon =\n icon == null ? null : (\n <span aria-hidden='true' className='inline-flex shrink-0'>\n {icon}\n </span>\n )\n\n return (\n <div className={className} data-slot='cta-buttons'>\n {ctas.map((cta, index) => {\n const isPrimary = index === 0\n return (\n <Button\n key={cta.id}\n asChild\n size={size}\n variant={isPrimary ? primaryVariant : secondaryVariant}\n className={cn(buttonClassName, isPrimary ? primaryClassName : secondaryClassName)}\n data-cta-id={cta.analyticsId ?? undefined}\n >\n {isExternal(cta.href) ? (\n <a href={cta.href} target='_blank' rel='noopener noreferrer'>\n {leadingIcon}\n {cta.label}\n {newWindowHint ? <> <span className='sr-only'>{newWindowHint}</span></> : null}\n </a>\n ) : (\n <Link href={cta.href}>\n {leadingIcon}\n {cta.label}\n </Link>\n )}\n </Button>\n )\n })}\n </div>\n )\n}\n","import type { Cta } from '@reblu/site-contracts'\n\nimport { CtaButtons } from './cta-buttons'\n\nexport interface StickyMobileCtaProps {\n ctas: Cta[]\n}\n\n/**\n * StickyMobileCta (REB-394, moved to `@reblu/site-next` in REB-418) — a\n * mobile-only sticky bottom bar surfacing the `sticky-mobile` slot CTAs. Hidden\n * on `md+` (desktop has its own CTAs). Renders nothing when no CTA is configured,\n * so it never adds empty chrome.\n */\nexport function StickyMobileCta({ ctas }: StickyMobileCtaProps) {\n if (ctas.length === 0) return null\n\n return (\n <div className='fixed inset-x-0 bottom-0 z-50 md:hidden border-t border-border bg-background/95 px-4 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/80'>\n <CtaButtons\n ctas={ctas}\n className='flex justify-center gap-3'\n buttonClassName='w-full rounded-lg font-semibold'\n size='lg'\n />\n </div>\n )\n}\n","import type { RebluClient } from '@reblu/site-client'\nimport type { Cta } from '@reblu/site-contracts'\nimport type { ReactNode } from 'react'\n\nimport { CtaButtons, type CtaButtonsProps } from './cta-buttons'\n\n/** The `Cta.slot` union — the valid placement keys, sourced from the contract. */\nexport type CtaSlot = Cta['slot']\n\n/**\n * The slice of the SDK client `EditableCta` needs: just the `ctas` namespace.\n * Typing it structurally lets a SITE pass either the full `RebluClient` or a\n * narrower singleton, without coupling this component to the whole surface.\n */\nexport type CtaClient = Pick<RebluClient, 'ctas'>\n\n/** Presentation props `EditableCta` forwards unchanged to {@link CtaButtons}. */\ntype ForwardedButtonProps = Omit<CtaButtonsProps, 'ctas'>\n\nexport interface EditableCtaProps extends ForwardedButtonProps {\n /** CtaSlot value: header | hero | blog-end | services | contact | sticky-mobile | footer. */\n slot: CtaSlot\n /** The Reblu SDK client instance the SITE owns (env-configured upstream). */\n client: CtaClient\n /**\n * Rendered when the slot has no configured CTA (or the upstream fetch fails).\n * Lets each placement keep its existing static CTA as a graceful fallback.\n */\n fallback?: ReactNode\n}\n\n/**\n * EditableCta (REB-394, moved to `@reblu/site-next` in REB-418) — drop-anywhere\n * async Server Component that renders the CTAs configured for a slot from nexus,\n * with no redeploy. Fetches active CTAs via the injected SDK client and degrades\n * gracefully: on empty/failure it renders `fallback` (a static CTA) instead of\n * breaking the page.\n *\n * The client is injected (prop or per-SITE singleton) so this package never\n * reads `process.env` and stays framework-glue only — the SITE owns the env and\n * wires the ISR-aware {@link NextCacheAdapter}.\n */\nexport async function EditableCta({ slot, client, fallback = null, ...buttonProps }: EditableCtaProps) {\n const { data: ctas } = await client.ctas\n .list(slot)\n .catch(() => ({ data: [] as Cta[] }))\n\n if (ctas.length === 0) return <>{fallback}</>\n\n return <CtaButtons ctas={ctas} {...buttonProps} />\n}\n"],"mappings":";AAkBO,IAAM,mBAAN,MAAoD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMzD,YAA6B,oBAAoC,IAAI;AAAxC;AAAA,EAAyC;AAAA,EAAzC;AAAA,EAE7B,mBAAmB,QAAuC;AACxD,UAAM,aAAa,OAAO,cAAc,KAAK;AAC7C,QAAI,eAAe,SAAS,eAAe,GAAG;AAC5C,aAAO,EAAE,OAAO,WAAW;AAAA,IAC7B;AACA,WAAO,EAAE,MAAM,EAAE,WAAW,EAAE;AAAA,EAChC;AACF;;;AC9BA,SAAS,QAAQ,UAAmC;AACpD,OAAO,UAAU;AA4EX,SAsB2B,UAtB3B,KAsB2B,YAtB3B;AAnEN,IAAM,0BAA0B;AAiChC,SAAS,WAAW,MAAuB;AACzC,SAAO,gBAAgB,KAAK,IAAI;AAClC;AAgBO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB;AAAA,EACA;AAAA,EACA,gBAAgB;AAClB,GAAoB;AAClB,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,QAAM,cACJ,QAAQ,OAAO,OACb,oBAAC,UAAK,eAAY,QAAO,WAAU,wBAChC,gBACH;AAGJ,SACE,oBAAC,SAAI,WAAsB,aAAU,eAClC,eAAK,IAAI,CAAC,KAAK,UAAU;AACxB,UAAM,YAAY,UAAU;AAC5B,WACE;AAAA,MAAC;AAAA;AAAA,QAEC,SAAO;AAAA,QACP;AAAA,QACA,SAAS,YAAY,iBAAiB;AAAA,QACtC,WAAW,GAAG,iBAAiB,YAAY,mBAAmB,kBAAkB;AAAA,QAChF,eAAa,IAAI,eAAe;AAAA,QAE/B,qBAAW,IAAI,IAAI,IAClB,qBAAC,OAAE,MAAM,IAAI,MAAM,QAAO,UAAS,KAAI,uBACpC;AAAA;AAAA,UACA,IAAI;AAAA,UACJ,gBAAgB,iCAAE;AAAA;AAAA,YAAC,oBAAC,UAAK,WAAU,WAAW,yBAAc;AAAA,aAAO,IAAM;AAAA,WAC5E,IAEA,qBAAC,QAAK,MAAM,IAAI,MACb;AAAA;AAAA,UACA,IAAI;AAAA,WACP;AAAA;AAAA,MAjBG,IAAI;AAAA,IAmBX;AAAA,EAEJ,CAAC,GACH;AAEJ;;;AChGM,gBAAAA,YAAA;AALC,SAAS,gBAAgB,EAAE,KAAK,GAAyB;AAC9D,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,SACE,gBAAAA,KAAC,SAAI,WAAU,uJACb,0BAAAA;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAU;AAAA,MACV,iBAAgB;AAAA,MAChB,MAAK;AAAA;AAAA,EACP,GACF;AAEJ;;;ACoBgC,qBAAAC,WAAA,OAAAC,YAAA;AALhC,eAAsB,YAAY,EAAE,MAAM,QAAQ,WAAW,MAAM,GAAG,YAAY,GAAqB;AACrG,QAAM,EAAE,MAAM,KAAK,IAAI,MAAM,OAAO,KACjC,KAAK,IAAI,EACT,MAAM,OAAO,EAAE,MAAM,CAAC,EAAW,EAAE;AAEtC,MAAI,KAAK,WAAW,EAAG,QAAO,gBAAAA,KAAAD,WAAA,EAAG,oBAAS;AAE1C,SAAO,gBAAAC,KAAC,cAAW,MAAa,GAAG,aAAa;AAClD;","names":["jsx","Fragment","jsx"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/shop/route-handlers.ts","../src/route-handlers.ts","../src/shop/checkout-redirect.ts"],"sourcesContent":["/**\n * `@reblu/site-next/shop-route-handlers` (REB-829) — Route Handler factories\n * for the cart/checkout write flow (`/api/site/shop/{checkout, validate-cart,\n * apply-discount, check-email, order/[sessionId]}`). Every demo-site route\n * these replace (`app/api/shop/*`) either hand-rolled the SAME proxy\n * boilerplate `./route-handlers`'s `createProxyRoute` already generalizes\n * (checkout/validate-cart/apply-discount/check-email), or called\n * `proxyToBaseApi` directly for the one dynamic-path route (`order/\n * [sessionId]`) that `createProxyRoute` deliberately does not cover (its own\n * docblock: \"a per-request DYNAMIC targetPath... is genuinely out of scope\").\n * This module is the dynamic-path extension `createProxyRoute` named as a\n * gap, kept as its own file (not folded into `./server`, REB-809's filters/\n * collection glue) because it is a distinct concern — a page that only reads\n * shop data never needs these factories, and a cart/checkout integration\n * never needs the filters glue.\n *\n * Server-only, dependency-free (only `createProxyRoute`, the local\n * `./checkout-redirect` helper, and the `next/server` type) — same isolation\n * direction as `./route-handlers`/`./shop/server.ts`: this entry must NEVER\n * carry `'use client'` (see `tsup.config.ts`).\n *\n * Mirrors `createProxyRoute`'s own API-key/server-only handling verbatim —\n * `REBLU_API_KEY` is read lazily, server-side, and injected as\n * `X-Reblu-Api-Key` on the upstream fetch only; it never reaches the\n * Response these handlers return to the browser.\n *\n * // app/api/shop/checkout/route.ts\n * export const POST = createShopCheckoutRoute()\n *\n * // app/api/shop/order/[sessionId]/route.ts\n * export const GET = createShopOrderRoute()\n */\nimport type { NextRequest } from 'next/server'\nimport { createProxyRoute, type CreateProxyRouteOptions } from '../route-handlers'\nimport { DEFAULT_CHECKOUT_REDIRECT_HOSTS, isSafeCheckoutRedirectUrl } from './checkout-redirect'\n\n/** Options for a fixed-path write route. `method` is always `'POST'` here. */\nexport type ShopWriteRouteOptions = Omit<CreateProxyRouteOptions, 'method'>\n\n/**\n * `requireJson: true` by default for every write route below: the browser\n * clients these proxy for (the cart/checkout UI) always set\n * `Content-Type: application/json`, and rejecting anything else with a 400\n * BEFORE the upstream fetch is strictly safer than forwarding an\n * unparseable body to base-api. A SITE with a non-JSON caller can still pass\n * `{ requireJson: false }`.\n */\nconst DEFAULT_WRITE_OPTS: ShopWriteRouteOptions = { requireJson: true }\n\n/** Options for `createShopCheckoutRoute` — standard success/cancel URLs (REB-837) + the redirect-origin re-check (REB-836). */\nexport interface ShopCheckoutRouteOptions extends ShopWriteRouteOptions {\n /**\n * Path (relative to the resolved SITE origin — see `siteOrigin` below,\n * never a caller-supplied value, so this can never redirect off-SITE)\n * used to build a default `successUrl` when the posted body doesn't\n * already include one. The literal `{CHECKOUT_SESSION_ID}` placeholder is\n * preserved verbatim — Stripe substitutes it at redirect time, same as\n * `cartPath`'s `{cart}` shorthand in the issue brief. Normalized (REB-837\n * review fix): must resolve to a single SITE-relative path starting with\n * `/` — an absolute URL or a protocol-relative `//` is rejected (logged,\n * falls back to the default) rather than silently building an off-SITE\n * redirect.\n * @default `${cartPath}?pago=ok&session_id={CHECKOUT_SESSION_ID}`\n */\n successPath?: string\n /**\n * Path used to build a default `cancelUrl`, same normalization and origin\n * rule as `successPath`.\n * @default `${cartPath}?pago=cancelado`\n */\n cancelPath?: string\n /** Base path the `successPath`/`cancelPath` defaults are built from. @default '/cart' */\n cartPath?: string\n /**\n * Explicit SITE origin to build `successUrl`/`cancelUrl` from, overriding\n * `request.nextUrl.origin` (REB-837 review fix). Needed behind a reverse\n * proxy or when the Route Handler's own request origin doesn't match the\n * SITE's real public origin. Falls back to `NEXT_PUBLIC_SITE_URL` when\n * omitted, then to `request.nextUrl.origin`.\n */\n siteOrigin?: string\n /** Allowed hosts for base-api's returned Checkout URL. @default ['checkout.stripe.com'] */\n allowedRedirectHosts?: string[]\n /**\n * Overrides the `message` field of the 502 body returned when base-api's\n * Checkout URL fails the redirect-origin re-check.\n * @default 'Error de conexión con el proveedor de pago.'\n */\n unsafeRedirectMessage?: string\n}\n\n/**\n * Normalizes a SITE-relative path for `successPath`/`cancelPath`/`cartPath`\n * (REB-837 review fix): must start with exactly one `/`, never an absolute\n * URL (`scheme://…`) or a protocol-relative `//…` — either would let a\n * misconfigured caller build a redirect off the SITE's own origin despite\n * the origin itself being trustworthy (request-derived or explicit\n * `siteOrigin`). A path missing its leading slash is PREFIXED with one; an\n * absolute/protocol-relative path is REJECTED outright (logged once) and\n * falls back to `fallback` rather than silently building a dangerous URL.\n */\nfunction normalizeSitePath(path: string, fallback: string): string {\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(path) || path.startsWith('//')) {\n console.error(\n `[createShopCheckoutRoute] Rejected an absolute/protocol-relative path \"${path}\" — using the default instead.`,\n )\n return fallback\n }\n return path.startsWith('/') ? path : `/${path}`\n}\n\nfunction unsafeRedirectResponse(message: string): Response {\n return new Response(\n JSON.stringify({ error: 'unsafe_redirect', message, correlation_id: crypto.randomUUID() }),\n { status: 502, headers: { 'Content-Type': 'application/json; charset=utf-8' } },\n )\n}\n\n/** Extracts `data.url` from a JSON response body, or `undefined` if absent/malformed. */\nfunction extractCheckoutUrl(rawBody: string): string | undefined {\n try {\n const parsed = JSON.parse(rawBody) as { data?: { url?: unknown } }\n return typeof parsed?.data?.url === 'string' ? parsed.data.url : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * Transport headers that describe the UPSTREAM byte stream, not `rawBody`\n * (re-serialized text `fetch` already decoded) — forwarding them verbatim\n * would tell the browser to gunzip/measure plain JSON and break checkout.\n * Review follow-up (native review on REB-836): this is an explicit blocklist,\n * not reliance on `createProxyRoute`'s current header set, so a future change\n * to that shared factory (e.g. the parallel REB-837 work) can't silently\n * reintroduce the leak here.\n */\nconst UNSAFE_PASSTHROUGH_HEADERS = new Set(['content-encoding', 'content-length', 'transfer-encoding'])\n\n/** Copies `headers` minus {@link UNSAFE_PASSTHROUGH_HEADERS} — safe to pair with a re-serialized body. */\nfunction safeResponseHeaders(headers: Headers): Headers {\n const safe = new Headers()\n for (const [key, value] of headers.entries()) {\n if (!UNSAFE_PASSTHROUGH_HEADERS.has(key.toLowerCase())) safe.set(key, value)\n }\n return safe\n}\n\n/**\n * `POST /api/site/shop/checkout` — starts a hosted Stripe Checkout session.\n *\n * REB-837: when the posted body doesn't already carry `successUrl`/\n * `cancelUrl` (an explicit value on the body always wins — this never\n * overrides a caller that builds its own), injects them built from\n * `successPath`/`cancelPath` and the resolved SITE origin (`siteOrigin` >\n * `NEXT_PUBLIC_SITE_URL` > `request.nextUrl.origin` — see `siteOrigin`'s\n * docblock). If no origin resolves, the body is forwarded unchanged —\n * base-api's own required-field validation is the backstop, not a second\n * origin check here.\n *\n * REB-836 (defence in depth): every SITE hands the response's `url` straight\n * to `window.location.assign`, so on a successful upstream response this\n * RE-CHECKS that `url` is `https:` on an allowed host (default\n * `checkout.stripe.com`) before ever returning it to the browser — a\n * compromised/misconfigured base-api or a tampered response in transit must\n * never redirect a buyer off Stripe's domain. Mirrors `@reblu/site-client`'s\n * own `shop.checkout()` guard (`./checkout-redirect.ts`'s docblock explains\n * why this is a deliberate duplicate, not a shared import). Non-2xx upstream\n * responses (validation errors, 404s, etc.) pass through unchanged — there is\n * no redirect URL to check.\n */\nexport function createShopCheckoutRoute(opts: ShopCheckoutRouteOptions = {}) {\n const {\n successPath,\n cancelPath,\n cartPath = '/cart',\n siteOrigin,\n allowedRedirectHosts = DEFAULT_CHECKOUT_REDIRECT_HOSTS,\n unsafeRedirectMessage = 'Error de conexión con el proveedor de pago.',\n ...proxyOpts\n } = opts\n\n const resolvedCartPath = normalizeSitePath(cartPath, '/cart')\n const defaultSuccessPath = `${resolvedCartPath}?pago=ok&session_id={CHECKOUT_SESSION_ID}`\n const defaultCancelPath = `${resolvedCartPath}?pago=cancelado`\n const resolvedSuccessPath =\n successPath !== undefined ? normalizeSitePath(successPath, defaultSuccessPath) : defaultSuccessPath\n const resolvedCancelPath =\n cancelPath !== undefined ? normalizeSitePath(cancelPath, defaultCancelPath) : defaultCancelPath\n\n const proxyHandler = createProxyRoute('/api/site/shop/checkout', {\n ...DEFAULT_WRITE_OPTS,\n ...proxyOpts,\n transformBody: (rawBody, request) => {\n // Lazy, per-request (R6): `siteOrigin` wins (explicit, proxy-safe),\n // then `NEXT_PUBLIC_SITE_URL` (env fallback for a SITE behind a\n // reverse proxy where `request.nextUrl.origin` doesn't match its real\n // public origin), then the request's own origin.\n const origin = siteOrigin ?? process.env.NEXT_PUBLIC_SITE_URL ?? request.nextUrl?.origin\n if (!origin) return rawBody\n\n let parsed: Record<string, unknown>\n try {\n parsed = JSON.parse(rawBody) as Record<string, unknown>\n } catch {\n return rawBody\n }\n\n if (typeof parsed.successUrl !== 'string') {\n parsed.successUrl = `${origin}${resolvedSuccessPath}`\n }\n if (typeof parsed.cancelUrl !== 'string') {\n parsed.cancelUrl = `${origin}${resolvedCancelPath}`\n }\n return JSON.stringify(parsed)\n },\n })\n\n return async function handler(request: NextRequest): Promise<Response> {\n const response = await proxyHandler(request)\n if (!response.ok) return response\n\n const rawBody = await response.text()\n const url = extractCheckoutUrl(rawBody)\n if (!url || !isSafeCheckoutRedirectUrl(url, allowedRedirectHosts)) {\n console.error('[createShopCheckoutRoute] Blocked an unsafe checkout redirect URL')\n return unsafeRedirectResponse(unsafeRedirectMessage)\n }\n\n return new Response(rawBody, { status: response.status, headers: safeResponseHeaders(response.headers) })\n }\n}\n\n/** `POST /api/site/shop/validate-cart` — live stock/price re-check before checkout. */\nexport function createShopValidateCartRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/validate-cart', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/** `POST /api/site/shop/apply-discount` — validates a discount code against the cart. */\nexport function createShopApplyDiscountRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/apply-discount', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/** `POST /api/site/shop/check-email` — whether a guest's email already has an account. */\nexport function createShopCheckEmailRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/check-email', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/** Options for the dynamic order-lookup route. `method` is always `'GET'`. */\nexport type ShopOrderRouteOptions = Omit<CreateProxyRouteOptions, 'method'>\n\n/**\n * `GET /api/site/shop/order/[sessionId]` — the one route in this file with a\n * per-request dynamic target path, so it can't be a one-line\n * `createProxyRoute(...)` call like the others: it reads Next's route\n * `params`, builds `/api/site/shop/order/{sessionId}` per request, and\n * delegates everything else (env, timeout, forwarding, error shaping) to\n * `createProxyRoute`.\n */\nexport function createShopOrderRoute(opts: ShopOrderRouteOptions = {}) {\n return async function handler(\n request: NextRequest,\n context: { params: Promise<{ sessionId: string }> },\n ): Promise<Response> {\n const { sessionId } = await context.params\n const targetPath = `/api/site/shop/order/${encodeURIComponent(sessionId)}`\n return createProxyRoute(targetPath, { ...opts, method: 'GET' })(request)\n }\n}\n","/**\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 * Transform the raw request body before it is forwarded upstream (REB-837\n * — `createShopCheckoutRoute`'s successPath/cancelPath default-injection\n * is the first caller). Receives the original `request` so a transform\n * can derive values from it (e.g. `request.nextUrl.origin`). Only called\n * for a non-GET/HEAD request with a non-empty body. A thrown error is\n * logged and the ORIGINAL body is forwarded unchanged — a transform bug\n * must never turn into a hard failure for every request through this\n * route.\n */\n transformBody?: (rawBody: string, request: NextRequest) => 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 let body =\n upperMethod === 'GET' || upperMethod === 'HEAD' ? undefined : await request.text()\n if (body && opts.transformBody) {\n try {\n body = opts.transformBody(body, request)\n } catch (err) {\n console.error(`[createProxyRoute] transformBody threw for ${targetPath}:`, err)\n }\n }\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","/**\n * Stripe Checkout redirect-origin guard (REB-836, defence in depth) — the\n * server-side half. `createShopCheckoutRoute` re-checks base-api's Checkout\n * URL before handing it to the browser, which otherwise immediately\n * `window.location.assign`s it.\n *\n * This is a DELIBERATE DUPLICATE of `@reblu/site-client`'s\n * `isSafeCheckoutRedirectUrl` (`packages/site-client/src/checkout-redirect.ts`),\n * not a shared import: the `./shop-route-handlers` entry is dependency-free by\n * design (see `../route-handlers.ts`'s docblock — \"must stay dependency-free\n * (no `@reblu/site-ui`)... so importing it from an `app/api/*` route never\n * drags in client components\"), and pulling in `@reblu/site-client` here\n * would break that isolation. Update both together if Stripe's Checkout host\n * ever changes.\n */\n\n/** The only Checkout redirect host trusted by default. */\nexport const DEFAULT_CHECKOUT_REDIRECT_HOSTS: readonly string[] = ['checkout.stripe.com']\n\n/**\n * True only when `url` parses as an absolute URL, uses `https:`, and its\n * hostname is in `allowedHosts` (default: {@link DEFAULT_CHECKOUT_REDIRECT_HOSTS}).\n * Never throws — an unparsable `url` is simply unsafe.\n */\nexport function isSafeCheckoutRedirectUrl(\n url: string,\n allowedHosts: readonly string[] = DEFAULT_CHECKOUT_REDIRECT_HOSTS,\n): boolean {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n return false\n }\n return parsed.protocol === 'https:' && allowedHosts.includes(parsed.hostname)\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/shop/route-handlers.ts","../src/route-handlers.ts","../src/shop/checkout-redirect.ts"],"sourcesContent":["/**\n * `@reblu/site-next/shop-route-handlers` (REB-829) — Route Handler factories\n * for the cart/checkout write flow (`/api/site/shop/{checkout, validate-cart,\n * apply-discount, check-email, order/[sessionId]}`). Every demo-site route\n * these replace (`app/api/shop/*`) either hand-rolled the SAME proxy\n * boilerplate `./route-handlers`'s `createProxyRoute` already generalizes\n * (checkout/validate-cart/apply-discount/check-email), or called\n * `proxyToBaseApi` directly for the one dynamic-path route (`order/\n * [sessionId]`) that `createProxyRoute` deliberately does not cover (its own\n * docblock: \"a per-request DYNAMIC targetPath... is genuinely out of scope\").\n * This module is the dynamic-path extension `createProxyRoute` named as a\n * gap, kept as its own file (not folded into `./server`, REB-809's filters/\n * collection glue) because it is a distinct concern — a page that only reads\n * shop data never needs these factories, and a cart/checkout integration\n * never needs the filters glue.\n *\n * Server-only, dependency-free (only `createProxyRoute`, the local\n * `./checkout-redirect` helper, and the `next/server` type) — same isolation\n * direction as `./route-handlers`/`./shop/server.ts`: this entry must NEVER\n * carry `'use client'` (see `tsup.config.ts`).\n *\n * Mirrors `createProxyRoute`'s own API-key/server-only handling verbatim —\n * `REBLU_API_KEY` is read lazily, server-side, and injected as\n * `X-Reblu-Api-Key` on the upstream fetch only; it never reaches the\n * Response these handlers return to the browser.\n *\n * // app/api/shop/checkout/route.ts\n * export const POST = createShopCheckoutRoute()\n *\n * // app/api/shop/order/[sessionId]/route.ts\n * export const GET = createShopOrderRoute()\n */\nimport type { NextRequest } from 'next/server'\nimport { createProxyRoute, type CreateProxyRouteOptions } from '../route-handlers'\nimport { DEFAULT_CHECKOUT_REDIRECT_HOSTS, isSafeCheckoutRedirectUrl } from './checkout-redirect'\n\n/** Options for a fixed-path write route. `method` is always `'POST'` here. */\nexport type ShopWriteRouteOptions = Omit<CreateProxyRouteOptions, 'method'>\n\n/**\n * `requireJson: true` by default for every write route below: the browser\n * clients these proxy for (the cart/checkout UI) always set\n * `Content-Type: application/json`, and rejecting anything else with a 400\n * BEFORE the upstream fetch is strictly safer than forwarding an\n * unparseable body to base-api. A SITE with a non-JSON caller can still pass\n * `{ requireJson: false }`.\n */\nconst DEFAULT_WRITE_OPTS: ShopWriteRouteOptions = { requireJson: true }\n\n/** Options for `createShopCheckoutRoute` — standard success/cancel URLs (REB-837) + the redirect-origin re-check (REB-836). */\nexport interface ShopCheckoutRouteOptions extends ShopWriteRouteOptions {\n /**\n * Path (relative to the resolved SITE origin — see `siteOrigin` below,\n * never a caller-supplied value, so this can never redirect off-SITE)\n * used to build a default `successUrl` when the posted body doesn't\n * already include one. The literal `{CHECKOUT_SESSION_ID}` placeholder is\n * preserved verbatim — Stripe substitutes it at redirect time, same as\n * `cartPath`'s `{cart}` shorthand in the issue brief. Normalized (REB-837\n * review fix): must resolve to a single SITE-relative path starting with\n * `/` — an absolute URL or a protocol-relative `//` is rejected (logged,\n * falls back to the default) rather than silently building an off-SITE\n * redirect.\n * @default `${cartPath}?pago=ok&session_id={CHECKOUT_SESSION_ID}`\n */\n successPath?: string\n /**\n * Path used to build a default `cancelUrl`, same normalization and origin\n * rule as `successPath`.\n * @default `${cartPath}?pago=cancelado`\n */\n cancelPath?: string\n /** Base path the `successPath`/`cancelPath` defaults are built from. @default '/cart' */\n cartPath?: string\n /**\n * Explicit SITE origin to build `successUrl`/`cancelUrl` from, overriding\n * `request.nextUrl.origin` (REB-837 review fix). Needed behind a reverse\n * proxy or when the Route Handler's own request origin doesn't match the\n * SITE's real public origin. Falls back to `NEXT_PUBLIC_SITE_URL` when\n * omitted, then to `request.nextUrl.origin`.\n */\n siteOrigin?: string\n /** Allowed hosts for base-api's returned Checkout URL. @default ['checkout.stripe.com'] */\n allowedRedirectHosts?: string[]\n /**\n * Overrides the `message` field of the 502 body returned when base-api's\n * Checkout URL fails the redirect-origin re-check.\n * @default 'Error de conexión con el proveedor de pago.'\n */\n unsafeRedirectMessage?: string\n}\n\n/**\n * Normalizes a SITE-relative path for `successPath`/`cancelPath`/`cartPath`\n * (REB-837 review fix): must start with exactly one `/`, never an absolute\n * URL (`scheme://…`) or a protocol-relative `//…` — either would let a\n * misconfigured caller build a redirect off the SITE's own origin despite\n * the origin itself being trustworthy (request-derived or explicit\n * `siteOrigin`). A path missing its leading slash is PREFIXED with one; an\n * absolute/protocol-relative path is REJECTED outright (logged once) and\n * falls back to `fallback` rather than silently building a dangerous URL.\n */\nfunction normalizeSitePath(path: string, fallback: string): string {\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(path) || path.startsWith('//')) {\n console.error(\n `[createShopCheckoutRoute] Rejected an absolute/protocol-relative path \"${path}\" — using the default instead.`,\n )\n return fallback\n }\n return path.startsWith('/') ? path : `/${path}`\n}\n\nfunction unsafeRedirectResponse(message: string): Response {\n return new Response(\n JSON.stringify({ error: 'unsafe_redirect', message, correlation_id: crypto.randomUUID() }),\n { status: 502, headers: { 'Content-Type': 'application/json; charset=utf-8' } },\n )\n}\n\n/** Extracts `data.url` from a JSON response body, or `undefined` if absent/malformed. */\nfunction extractCheckoutUrl(rawBody: string): string | undefined {\n try {\n const parsed = JSON.parse(rawBody) as { data?: { url?: unknown } }\n return typeof parsed?.data?.url === 'string' ? parsed.data.url : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * Transport headers that describe the UPSTREAM byte stream, not `rawBody`\n * (re-serialized text `fetch` already decoded) — forwarding them verbatim\n * would tell the browser to gunzip/measure plain JSON and break checkout.\n * Review follow-up (native review on REB-836): this is an explicit blocklist,\n * not reliance on `createProxyRoute`'s current header set, so a future change\n * to that shared factory (e.g. the parallel REB-837 work) can't silently\n * reintroduce the leak here.\n */\nconst UNSAFE_PASSTHROUGH_HEADERS = new Set(['content-encoding', 'content-length', 'transfer-encoding'])\n\n/** Copies `headers` minus {@link UNSAFE_PASSTHROUGH_HEADERS} — safe to pair with a re-serialized body. */\nfunction safeResponseHeaders(headers: Headers): Headers {\n const safe = new Headers()\n for (const [key, value] of headers.entries()) {\n if (!UNSAFE_PASSTHROUGH_HEADERS.has(key.toLowerCase())) safe.set(key, value)\n }\n return safe\n}\n\n/**\n * `POST /api/site/shop/checkout` — starts a hosted Stripe Checkout session.\n *\n * REB-837: when the posted body doesn't already carry `successUrl`/\n * `cancelUrl` (an explicit value on the body always wins — this never\n * overrides a caller that builds its own), injects them built from\n * `successPath`/`cancelPath` and the resolved SITE origin (`siteOrigin` >\n * `NEXT_PUBLIC_SITE_URL` > `request.nextUrl.origin` — see `siteOrigin`'s\n * docblock). If no origin resolves, the body is forwarded unchanged —\n * base-api's own required-field validation is the backstop, not a second\n * origin check here.\n *\n * REB-836 (defence in depth): every SITE hands the response's `url` straight\n * to `window.location.assign`, so on a successful upstream response this\n * RE-CHECKS that `url` is `https:` on an allowed host (default\n * `checkout.stripe.com`) before ever returning it to the browser — a\n * compromised/misconfigured base-api or a tampered response in transit must\n * never redirect a buyer off Stripe's domain. Mirrors `@reblu/site-client`'s\n * own `shop.checkout()` guard (`./checkout-redirect.ts`'s docblock explains\n * why this is a deliberate duplicate, not a shared import). Non-2xx upstream\n * responses (validation errors, 404s, etc.) pass through unchanged — there is\n * no redirect URL to check.\n */\nexport function createShopCheckoutRoute(opts: ShopCheckoutRouteOptions = {}) {\n const {\n successPath,\n cancelPath,\n cartPath = '/cart',\n siteOrigin,\n allowedRedirectHosts = DEFAULT_CHECKOUT_REDIRECT_HOSTS,\n unsafeRedirectMessage = 'Error de conexión con el proveedor de pago.',\n ...proxyOpts\n } = opts\n\n const resolvedCartPath = normalizeSitePath(cartPath, '/cart')\n const defaultSuccessPath = `${resolvedCartPath}?pago=ok&session_id={CHECKOUT_SESSION_ID}`\n const defaultCancelPath = `${resolvedCartPath}?pago=cancelado`\n const resolvedSuccessPath =\n successPath !== undefined ? normalizeSitePath(successPath, defaultSuccessPath) : defaultSuccessPath\n const resolvedCancelPath =\n cancelPath !== undefined ? normalizeSitePath(cancelPath, defaultCancelPath) : defaultCancelPath\n\n const proxyHandler = createProxyRoute('/api/site/shop/checkout', {\n ...DEFAULT_WRITE_OPTS,\n ...proxyOpts,\n transformBody: (rawBody, request) => {\n // Lazy, per-request (R6): `siteOrigin` wins (explicit, proxy-safe),\n // then `NEXT_PUBLIC_SITE_URL` (env fallback for a SITE behind a\n // reverse proxy where `request.nextUrl.origin` doesn't match its real\n // public origin), then the request's own origin.\n const origin = siteOrigin ?? process.env.NEXT_PUBLIC_SITE_URL ?? request.nextUrl?.origin\n if (!origin) return rawBody\n\n let parsed: Record<string, unknown>\n try {\n parsed = JSON.parse(rawBody) as Record<string, unknown>\n } catch {\n return rawBody\n }\n\n if (typeof parsed.successUrl !== 'string') {\n parsed.successUrl = `${origin}${resolvedSuccessPath}`\n }\n if (typeof parsed.cancelUrl !== 'string') {\n parsed.cancelUrl = `${origin}${resolvedCancelPath}`\n }\n return JSON.stringify(parsed)\n },\n })\n\n return async function handler(request: NextRequest): Promise<Response> {\n const response = await proxyHandler(request)\n if (!response.ok) return response\n\n const rawBody = await response.text()\n const url = extractCheckoutUrl(rawBody)\n if (!url || !isSafeCheckoutRedirectUrl(url, allowedRedirectHosts)) {\n console.error('[createShopCheckoutRoute] Blocked an unsafe checkout redirect URL')\n return unsafeRedirectResponse(unsafeRedirectMessage)\n }\n\n return new Response(rawBody, { status: response.status, headers: safeResponseHeaders(response.headers) })\n }\n}\n\n/** `POST /api/site/shop/validate-cart` — live stock/price re-check before checkout. */\nexport function createShopValidateCartRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/validate-cart', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/** `POST /api/site/shop/apply-discount` — validates a discount code against the cart. */\nexport function createShopApplyDiscountRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/apply-discount', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/**\n * `POST /api/site/shop/check-email`.\n *\n * @deprecated REB-834 — base-api no longer reveals whether an email has an\n * account (it was an enumeration oracle on the tenant's customer list): the\n * proxied answer is always `{ exists: false }`. Do not mount this route on a\n * new SITE; an existing mount keeps working until the next major removes it.\n * The checkout always offers «Iniciar sesión o continuar como invitado» and the\n * server links the order to an existing account after payment.\n */\nexport function createShopCheckEmailRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/check-email', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/** Options for the dynamic order-lookup route. `method` is always `'GET'`. */\nexport type ShopOrderRouteOptions = Omit<CreateProxyRouteOptions, 'method'>\n\n/**\n * `GET /api/site/shop/order/[sessionId]` — the one route in this file with a\n * per-request dynamic target path, so it can't be a one-line\n * `createProxyRoute(...)` call like the others: it reads Next's route\n * `params`, builds `/api/site/shop/order/{sessionId}` per request, and\n * delegates everything else (env, timeout, forwarding, error shaping) to\n * `createProxyRoute`.\n */\nexport function createShopOrderRoute(opts: ShopOrderRouteOptions = {}) {\n return async function handler(\n request: NextRequest,\n context: { params: Promise<{ sessionId: string }> },\n ): Promise<Response> {\n const { sessionId } = await context.params\n const targetPath = `/api/site/shop/order/${encodeURIComponent(sessionId)}`\n return createProxyRoute(targetPath, { ...opts, method: 'GET' })(request)\n }\n}\n","/**\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 * Transform the raw request body before it is forwarded upstream (REB-837\n * — `createShopCheckoutRoute`'s successPath/cancelPath default-injection\n * is the first caller). Receives the original `request` so a transform\n * can derive values from it (e.g. `request.nextUrl.origin`). Only called\n * for a non-GET/HEAD request with a non-empty body. A thrown error is\n * logged and the ORIGINAL body is forwarded unchanged — a transform bug\n * must never turn into a hard failure for every request through this\n * route.\n */\n transformBody?: (rawBody: string, request: NextRequest) => 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 let body =\n upperMethod === 'GET' || upperMethod === 'HEAD' ? undefined : await request.text()\n if (body && opts.transformBody) {\n try {\n body = opts.transformBody(body, request)\n } catch (err) {\n console.error(`[createProxyRoute] transformBody threw for ${targetPath}:`, err)\n }\n }\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","/**\n * Stripe Checkout redirect-origin guard (REB-836, defence in depth) — the\n * server-side half. `createShopCheckoutRoute` re-checks base-api's Checkout\n * URL before handing it to the browser, which otherwise immediately\n * `window.location.assign`s it.\n *\n * This is a DELIBERATE DUPLICATE of `@reblu/site-client`'s\n * `isSafeCheckoutRedirectUrl` (`packages/site-client/src/checkout-redirect.ts`),\n * not a shared import: the `./shop-route-handlers` entry is dependency-free by\n * design (see `../route-handlers.ts`'s docblock — \"must stay dependency-free\n * (no `@reblu/site-ui`)... so importing it from an `app/api/*` route never\n * drags in client components\"), and pulling in `@reblu/site-client` here\n * would break that isolation. Update both together if Stripe's Checkout host\n * ever changes.\n */\n\n/** The only Checkout redirect host trusted by default. */\nexport const DEFAULT_CHECKOUT_REDIRECT_HOSTS: readonly string[] = ['checkout.stripe.com']\n\n/**\n * True only when `url` parses as an absolute URL, uses `https:`, and its\n * hostname is in `allowedHosts` (default: {@link DEFAULT_CHECKOUT_REDIRECT_HOSTS}).\n * Never throws — an unparsable `url` is simply unsafe.\n */\nexport function isSafeCheckoutRedirectUrl(\n url: string,\n allowedHosts: readonly string[] = DEFAULT_CHECKOUT_REDIRECT_HOSTS,\n): boolean {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n return false\n }\n return parsed.protocol === 'https:' && allowedHosts.includes(parsed.hostname)\n}\n"],"mappings":"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","names":[]}
@@ -105,7 +105,16 @@ declare function createShopCheckoutRoute(opts?: ShopCheckoutRouteOptions): (requ
105
105
  declare function createShopValidateCartRoute(opts?: ShopWriteRouteOptions): (request: NextRequest) => Promise<Response>;
106
106
  /** `POST /api/site/shop/apply-discount` — validates a discount code against the cart. */
107
107
  declare function createShopApplyDiscountRoute(opts?: ShopWriteRouteOptions): (request: NextRequest) => Promise<Response>;
108
- /** `POST /api/site/shop/check-email` — whether a guest's email already has an account. */
108
+ /**
109
+ * `POST /api/site/shop/check-email`.
110
+ *
111
+ * @deprecated REB-834 — base-api no longer reveals whether an email has an
112
+ * account (it was an enumeration oracle on the tenant's customer list): the
113
+ * proxied answer is always `{ exists: false }`. Do not mount this route on a
114
+ * new SITE; an existing mount keeps working until the next major removes it.
115
+ * The checkout always offers «Iniciar sesión o continuar como invitado» and the
116
+ * server links the order to an existing account after payment.
117
+ */
109
118
  declare function createShopCheckEmailRoute(opts?: ShopWriteRouteOptions): (request: NextRequest) => Promise<Response>;
110
119
  /** Options for the dynamic order-lookup route. `method` is always `'GET'`. */
111
120
  type ShopOrderRouteOptions = Omit<CreateProxyRouteOptions, 'method'>;
@@ -105,7 +105,16 @@ declare function createShopCheckoutRoute(opts?: ShopCheckoutRouteOptions): (requ
105
105
  declare function createShopValidateCartRoute(opts?: ShopWriteRouteOptions): (request: NextRequest) => Promise<Response>;
106
106
  /** `POST /api/site/shop/apply-discount` — validates a discount code against the cart. */
107
107
  declare function createShopApplyDiscountRoute(opts?: ShopWriteRouteOptions): (request: NextRequest) => Promise<Response>;
108
- /** `POST /api/site/shop/check-email` — whether a guest's email already has an account. */
108
+ /**
109
+ * `POST /api/site/shop/check-email`.
110
+ *
111
+ * @deprecated REB-834 — base-api no longer reveals whether an email has an
112
+ * account (it was an enumeration oracle on the tenant's customer list): the
113
+ * proxied answer is always `{ exists: false }`. Do not mount this route on a
114
+ * new SITE; an existing mount keeps working until the next major removes it.
115
+ * The checkout always offers «Iniciar sesión o continuar como invitado» and the
116
+ * server links the order to an existing account after payment.
117
+ */
109
118
  declare function createShopCheckEmailRoute(opts?: ShopWriteRouteOptions): (request: NextRequest) => Promise<Response>;
110
119
  /** Options for the dynamic order-lookup route. `method` is always `'GET'`. */
111
120
  type ShopOrderRouteOptions = Omit<CreateProxyRouteOptions, 'method'>;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/route-handlers.ts","../src/shop/checkout-redirect.ts","../src/shop/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 * Transform the raw request body before it is forwarded upstream (REB-837\n * — `createShopCheckoutRoute`'s successPath/cancelPath default-injection\n * is the first caller). Receives the original `request` so a transform\n * can derive values from it (e.g. `request.nextUrl.origin`). Only called\n * for a non-GET/HEAD request with a non-empty body. A thrown error is\n * logged and the ORIGINAL body is forwarded unchanged — a transform bug\n * must never turn into a hard failure for every request through this\n * route.\n */\n transformBody?: (rawBody: string, request: NextRequest) => 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 let body =\n upperMethod === 'GET' || upperMethod === 'HEAD' ? undefined : await request.text()\n if (body && opts.transformBody) {\n try {\n body = opts.transformBody(body, request)\n } catch (err) {\n console.error(`[createProxyRoute] transformBody threw for ${targetPath}:`, err)\n }\n }\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","/**\n * Stripe Checkout redirect-origin guard (REB-836, defence in depth) — the\n * server-side half. `createShopCheckoutRoute` re-checks base-api's Checkout\n * URL before handing it to the browser, which otherwise immediately\n * `window.location.assign`s it.\n *\n * This is a DELIBERATE DUPLICATE of `@reblu/site-client`'s\n * `isSafeCheckoutRedirectUrl` (`packages/site-client/src/checkout-redirect.ts`),\n * not a shared import: the `./shop-route-handlers` entry is dependency-free by\n * design (see `../route-handlers.ts`'s docblock — \"must stay dependency-free\n * (no `@reblu/site-ui`)... so importing it from an `app/api/*` route never\n * drags in client components\"), and pulling in `@reblu/site-client` here\n * would break that isolation. Update both together if Stripe's Checkout host\n * ever changes.\n */\n\n/** The only Checkout redirect host trusted by default. */\nexport const DEFAULT_CHECKOUT_REDIRECT_HOSTS: readonly string[] = ['checkout.stripe.com']\n\n/**\n * True only when `url` parses as an absolute URL, uses `https:`, and its\n * hostname is in `allowedHosts` (default: {@link DEFAULT_CHECKOUT_REDIRECT_HOSTS}).\n * Never throws — an unparsable `url` is simply unsafe.\n */\nexport function isSafeCheckoutRedirectUrl(\n url: string,\n allowedHosts: readonly string[] = DEFAULT_CHECKOUT_REDIRECT_HOSTS,\n): boolean {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n return false\n }\n return parsed.protocol === 'https:' && allowedHosts.includes(parsed.hostname)\n}\n","/**\n * `@reblu/site-next/shop-route-handlers` (REB-829) — Route Handler factories\n * for the cart/checkout write flow (`/api/site/shop/{checkout, validate-cart,\n * apply-discount, check-email, order/[sessionId]}`). Every demo-site route\n * these replace (`app/api/shop/*`) either hand-rolled the SAME proxy\n * boilerplate `./route-handlers`'s `createProxyRoute` already generalizes\n * (checkout/validate-cart/apply-discount/check-email), or called\n * `proxyToBaseApi` directly for the one dynamic-path route (`order/\n * [sessionId]`) that `createProxyRoute` deliberately does not cover (its own\n * docblock: \"a per-request DYNAMIC targetPath... is genuinely out of scope\").\n * This module is the dynamic-path extension `createProxyRoute` named as a\n * gap, kept as its own file (not folded into `./server`, REB-809's filters/\n * collection glue) because it is a distinct concern — a page that only reads\n * shop data never needs these factories, and a cart/checkout integration\n * never needs the filters glue.\n *\n * Server-only, dependency-free (only `createProxyRoute`, the local\n * `./checkout-redirect` helper, and the `next/server` type) — same isolation\n * direction as `./route-handlers`/`./shop/server.ts`: this entry must NEVER\n * carry `'use client'` (see `tsup.config.ts`).\n *\n * Mirrors `createProxyRoute`'s own API-key/server-only handling verbatim —\n * `REBLU_API_KEY` is read lazily, server-side, and injected as\n * `X-Reblu-Api-Key` on the upstream fetch only; it never reaches the\n * Response these handlers return to the browser.\n *\n * // app/api/shop/checkout/route.ts\n * export const POST = createShopCheckoutRoute()\n *\n * // app/api/shop/order/[sessionId]/route.ts\n * export const GET = createShopOrderRoute()\n */\nimport type { NextRequest } from 'next/server'\nimport { createProxyRoute, type CreateProxyRouteOptions } from '../route-handlers'\nimport { DEFAULT_CHECKOUT_REDIRECT_HOSTS, isSafeCheckoutRedirectUrl } from './checkout-redirect'\n\n/** Options for a fixed-path write route. `method` is always `'POST'` here. */\nexport type ShopWriteRouteOptions = Omit<CreateProxyRouteOptions, 'method'>\n\n/**\n * `requireJson: true` by default for every write route below: the browser\n * clients these proxy for (the cart/checkout UI) always set\n * `Content-Type: application/json`, and rejecting anything else with a 400\n * BEFORE the upstream fetch is strictly safer than forwarding an\n * unparseable body to base-api. A SITE with a non-JSON caller can still pass\n * `{ requireJson: false }`.\n */\nconst DEFAULT_WRITE_OPTS: ShopWriteRouteOptions = { requireJson: true }\n\n/** Options for `createShopCheckoutRoute` — standard success/cancel URLs (REB-837) + the redirect-origin re-check (REB-836). */\nexport interface ShopCheckoutRouteOptions extends ShopWriteRouteOptions {\n /**\n * Path (relative to the resolved SITE origin — see `siteOrigin` below,\n * never a caller-supplied value, so this can never redirect off-SITE)\n * used to build a default `successUrl` when the posted body doesn't\n * already include one. The literal `{CHECKOUT_SESSION_ID}` placeholder is\n * preserved verbatim — Stripe substitutes it at redirect time, same as\n * `cartPath`'s `{cart}` shorthand in the issue brief. Normalized (REB-837\n * review fix): must resolve to a single SITE-relative path starting with\n * `/` — an absolute URL or a protocol-relative `//` is rejected (logged,\n * falls back to the default) rather than silently building an off-SITE\n * redirect.\n * @default `${cartPath}?pago=ok&session_id={CHECKOUT_SESSION_ID}`\n */\n successPath?: string\n /**\n * Path used to build a default `cancelUrl`, same normalization and origin\n * rule as `successPath`.\n * @default `${cartPath}?pago=cancelado`\n */\n cancelPath?: string\n /** Base path the `successPath`/`cancelPath` defaults are built from. @default '/cart' */\n cartPath?: string\n /**\n * Explicit SITE origin to build `successUrl`/`cancelUrl` from, overriding\n * `request.nextUrl.origin` (REB-837 review fix). Needed behind a reverse\n * proxy or when the Route Handler's own request origin doesn't match the\n * SITE's real public origin. Falls back to `NEXT_PUBLIC_SITE_URL` when\n * omitted, then to `request.nextUrl.origin`.\n */\n siteOrigin?: string\n /** Allowed hosts for base-api's returned Checkout URL. @default ['checkout.stripe.com'] */\n allowedRedirectHosts?: string[]\n /**\n * Overrides the `message` field of the 502 body returned when base-api's\n * Checkout URL fails the redirect-origin re-check.\n * @default 'Error de conexión con el proveedor de pago.'\n */\n unsafeRedirectMessage?: string\n}\n\n/**\n * Normalizes a SITE-relative path for `successPath`/`cancelPath`/`cartPath`\n * (REB-837 review fix): must start with exactly one `/`, never an absolute\n * URL (`scheme://…`) or a protocol-relative `//…` — either would let a\n * misconfigured caller build a redirect off the SITE's own origin despite\n * the origin itself being trustworthy (request-derived or explicit\n * `siteOrigin`). A path missing its leading slash is PREFIXED with one; an\n * absolute/protocol-relative path is REJECTED outright (logged once) and\n * falls back to `fallback` rather than silently building a dangerous URL.\n */\nfunction normalizeSitePath(path: string, fallback: string): string {\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(path) || path.startsWith('//')) {\n console.error(\n `[createShopCheckoutRoute] Rejected an absolute/protocol-relative path \"${path}\" — using the default instead.`,\n )\n return fallback\n }\n return path.startsWith('/') ? path : `/${path}`\n}\n\nfunction unsafeRedirectResponse(message: string): Response {\n return new Response(\n JSON.stringify({ error: 'unsafe_redirect', message, correlation_id: crypto.randomUUID() }),\n { status: 502, headers: { 'Content-Type': 'application/json; charset=utf-8' } },\n )\n}\n\n/** Extracts `data.url` from a JSON response body, or `undefined` if absent/malformed. */\nfunction extractCheckoutUrl(rawBody: string): string | undefined {\n try {\n const parsed = JSON.parse(rawBody) as { data?: { url?: unknown } }\n return typeof parsed?.data?.url === 'string' ? parsed.data.url : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * Transport headers that describe the UPSTREAM byte stream, not `rawBody`\n * (re-serialized text `fetch` already decoded) — forwarding them verbatim\n * would tell the browser to gunzip/measure plain JSON and break checkout.\n * Review follow-up (native review on REB-836): this is an explicit blocklist,\n * not reliance on `createProxyRoute`'s current header set, so a future change\n * to that shared factory (e.g. the parallel REB-837 work) can't silently\n * reintroduce the leak here.\n */\nconst UNSAFE_PASSTHROUGH_HEADERS = new Set(['content-encoding', 'content-length', 'transfer-encoding'])\n\n/** Copies `headers` minus {@link UNSAFE_PASSTHROUGH_HEADERS} — safe to pair with a re-serialized body. */\nfunction safeResponseHeaders(headers: Headers): Headers {\n const safe = new Headers()\n for (const [key, value] of headers.entries()) {\n if (!UNSAFE_PASSTHROUGH_HEADERS.has(key.toLowerCase())) safe.set(key, value)\n }\n return safe\n}\n\n/**\n * `POST /api/site/shop/checkout` — starts a hosted Stripe Checkout session.\n *\n * REB-837: when the posted body doesn't already carry `successUrl`/\n * `cancelUrl` (an explicit value on the body always wins — this never\n * overrides a caller that builds its own), injects them built from\n * `successPath`/`cancelPath` and the resolved SITE origin (`siteOrigin` >\n * `NEXT_PUBLIC_SITE_URL` > `request.nextUrl.origin` — see `siteOrigin`'s\n * docblock). If no origin resolves, the body is forwarded unchanged —\n * base-api's own required-field validation is the backstop, not a second\n * origin check here.\n *\n * REB-836 (defence in depth): every SITE hands the response's `url` straight\n * to `window.location.assign`, so on a successful upstream response this\n * RE-CHECKS that `url` is `https:` on an allowed host (default\n * `checkout.stripe.com`) before ever returning it to the browser — a\n * compromised/misconfigured base-api or a tampered response in transit must\n * never redirect a buyer off Stripe's domain. Mirrors `@reblu/site-client`'s\n * own `shop.checkout()` guard (`./checkout-redirect.ts`'s docblock explains\n * why this is a deliberate duplicate, not a shared import). Non-2xx upstream\n * responses (validation errors, 404s, etc.) pass through unchanged — there is\n * no redirect URL to check.\n */\nexport function createShopCheckoutRoute(opts: ShopCheckoutRouteOptions = {}) {\n const {\n successPath,\n cancelPath,\n cartPath = '/cart',\n siteOrigin,\n allowedRedirectHosts = DEFAULT_CHECKOUT_REDIRECT_HOSTS,\n unsafeRedirectMessage = 'Error de conexión con el proveedor de pago.',\n ...proxyOpts\n } = opts\n\n const resolvedCartPath = normalizeSitePath(cartPath, '/cart')\n const defaultSuccessPath = `${resolvedCartPath}?pago=ok&session_id={CHECKOUT_SESSION_ID}`\n const defaultCancelPath = `${resolvedCartPath}?pago=cancelado`\n const resolvedSuccessPath =\n successPath !== undefined ? normalizeSitePath(successPath, defaultSuccessPath) : defaultSuccessPath\n const resolvedCancelPath =\n cancelPath !== undefined ? normalizeSitePath(cancelPath, defaultCancelPath) : defaultCancelPath\n\n const proxyHandler = createProxyRoute('/api/site/shop/checkout', {\n ...DEFAULT_WRITE_OPTS,\n ...proxyOpts,\n transformBody: (rawBody, request) => {\n // Lazy, per-request (R6): `siteOrigin` wins (explicit, proxy-safe),\n // then `NEXT_PUBLIC_SITE_URL` (env fallback for a SITE behind a\n // reverse proxy where `request.nextUrl.origin` doesn't match its real\n // public origin), then the request's own origin.\n const origin = siteOrigin ?? process.env.NEXT_PUBLIC_SITE_URL ?? request.nextUrl?.origin\n if (!origin) return rawBody\n\n let parsed: Record<string, unknown>\n try {\n parsed = JSON.parse(rawBody) as Record<string, unknown>\n } catch {\n return rawBody\n }\n\n if (typeof parsed.successUrl !== 'string') {\n parsed.successUrl = `${origin}${resolvedSuccessPath}`\n }\n if (typeof parsed.cancelUrl !== 'string') {\n parsed.cancelUrl = `${origin}${resolvedCancelPath}`\n }\n return JSON.stringify(parsed)\n },\n })\n\n return async function handler(request: NextRequest): Promise<Response> {\n const response = await proxyHandler(request)\n if (!response.ok) return response\n\n const rawBody = await response.text()\n const url = extractCheckoutUrl(rawBody)\n if (!url || !isSafeCheckoutRedirectUrl(url, allowedRedirectHosts)) {\n console.error('[createShopCheckoutRoute] Blocked an unsafe checkout redirect URL')\n return unsafeRedirectResponse(unsafeRedirectMessage)\n }\n\n return new Response(rawBody, { status: response.status, headers: safeResponseHeaders(response.headers) })\n }\n}\n\n/** `POST /api/site/shop/validate-cart` — live stock/price re-check before checkout. */\nexport function createShopValidateCartRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/validate-cart', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/** `POST /api/site/shop/apply-discount` — validates a discount code against the cart. */\nexport function createShopApplyDiscountRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/apply-discount', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/** `POST /api/site/shop/check-email` — whether a guest's email already has an account. */\nexport function createShopCheckEmailRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/check-email', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/** Options for the dynamic order-lookup route. `method` is always `'GET'`. */\nexport type ShopOrderRouteOptions = Omit<CreateProxyRouteOptions, 'method'>\n\n/**\n * `GET /api/site/shop/order/[sessionId]` — the one route in this file with a\n * per-request dynamic target path, so it can't be a one-line\n * `createProxyRoute(...)` call like the others: it reads Next's route\n * `params`, builds `/api/site/shop/order/{sessionId}` per request, and\n * delegates everything else (env, timeout, forwarding, error shaping) to\n * `createProxyRoute`.\n */\nexport function createShopOrderRoute(opts: ShopOrderRouteOptions = {}) {\n return async function handler(\n request: NextRequest,\n context: { params: Promise<{ sessionId: string }> },\n ): Promise<Response> {\n const { sessionId } = await context.params\n const targetPath = `/api/site/shop/order/${encodeURIComponent(sessionId)}`\n return createProxyRoute(targetPath, { ...opts, method: 'GET' })(request)\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/route-handlers.ts","../src/shop/checkout-redirect.ts","../src/shop/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 * Transform the raw request body before it is forwarded upstream (REB-837\n * — `createShopCheckoutRoute`'s successPath/cancelPath default-injection\n * is the first caller). Receives the original `request` so a transform\n * can derive values from it (e.g. `request.nextUrl.origin`). Only called\n * for a non-GET/HEAD request with a non-empty body. A thrown error is\n * logged and the ORIGINAL body is forwarded unchanged — a transform bug\n * must never turn into a hard failure for every request through this\n * route.\n */\n transformBody?: (rawBody: string, request: NextRequest) => 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 let body =\n upperMethod === 'GET' || upperMethod === 'HEAD' ? undefined : await request.text()\n if (body && opts.transformBody) {\n try {\n body = opts.transformBody(body, request)\n } catch (err) {\n console.error(`[createProxyRoute] transformBody threw for ${targetPath}:`, err)\n }\n }\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","/**\n * Stripe Checkout redirect-origin guard (REB-836, defence in depth) — the\n * server-side half. `createShopCheckoutRoute` re-checks base-api's Checkout\n * URL before handing it to the browser, which otherwise immediately\n * `window.location.assign`s it.\n *\n * This is a DELIBERATE DUPLICATE of `@reblu/site-client`'s\n * `isSafeCheckoutRedirectUrl` (`packages/site-client/src/checkout-redirect.ts`),\n * not a shared import: the `./shop-route-handlers` entry is dependency-free by\n * design (see `../route-handlers.ts`'s docblock — \"must stay dependency-free\n * (no `@reblu/site-ui`)... so importing it from an `app/api/*` route never\n * drags in client components\"), and pulling in `@reblu/site-client` here\n * would break that isolation. Update both together if Stripe's Checkout host\n * ever changes.\n */\n\n/** The only Checkout redirect host trusted by default. */\nexport const DEFAULT_CHECKOUT_REDIRECT_HOSTS: readonly string[] = ['checkout.stripe.com']\n\n/**\n * True only when `url` parses as an absolute URL, uses `https:`, and its\n * hostname is in `allowedHosts` (default: {@link DEFAULT_CHECKOUT_REDIRECT_HOSTS}).\n * Never throws — an unparsable `url` is simply unsafe.\n */\nexport function isSafeCheckoutRedirectUrl(\n url: string,\n allowedHosts: readonly string[] = DEFAULT_CHECKOUT_REDIRECT_HOSTS,\n): boolean {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n return false\n }\n return parsed.protocol === 'https:' && allowedHosts.includes(parsed.hostname)\n}\n","/**\n * `@reblu/site-next/shop-route-handlers` (REB-829) — Route Handler factories\n * for the cart/checkout write flow (`/api/site/shop/{checkout, validate-cart,\n * apply-discount, check-email, order/[sessionId]}`). Every demo-site route\n * these replace (`app/api/shop/*`) either hand-rolled the SAME proxy\n * boilerplate `./route-handlers`'s `createProxyRoute` already generalizes\n * (checkout/validate-cart/apply-discount/check-email), or called\n * `proxyToBaseApi` directly for the one dynamic-path route (`order/\n * [sessionId]`) that `createProxyRoute` deliberately does not cover (its own\n * docblock: \"a per-request DYNAMIC targetPath... is genuinely out of scope\").\n * This module is the dynamic-path extension `createProxyRoute` named as a\n * gap, kept as its own file (not folded into `./server`, REB-809's filters/\n * collection glue) because it is a distinct concern — a page that only reads\n * shop data never needs these factories, and a cart/checkout integration\n * never needs the filters glue.\n *\n * Server-only, dependency-free (only `createProxyRoute`, the local\n * `./checkout-redirect` helper, and the `next/server` type) — same isolation\n * direction as `./route-handlers`/`./shop/server.ts`: this entry must NEVER\n * carry `'use client'` (see `tsup.config.ts`).\n *\n * Mirrors `createProxyRoute`'s own API-key/server-only handling verbatim —\n * `REBLU_API_KEY` is read lazily, server-side, and injected as\n * `X-Reblu-Api-Key` on the upstream fetch only; it never reaches the\n * Response these handlers return to the browser.\n *\n * // app/api/shop/checkout/route.ts\n * export const POST = createShopCheckoutRoute()\n *\n * // app/api/shop/order/[sessionId]/route.ts\n * export const GET = createShopOrderRoute()\n */\nimport type { NextRequest } from 'next/server'\nimport { createProxyRoute, type CreateProxyRouteOptions } from '../route-handlers'\nimport { DEFAULT_CHECKOUT_REDIRECT_HOSTS, isSafeCheckoutRedirectUrl } from './checkout-redirect'\n\n/** Options for a fixed-path write route. `method` is always `'POST'` here. */\nexport type ShopWriteRouteOptions = Omit<CreateProxyRouteOptions, 'method'>\n\n/**\n * `requireJson: true` by default for every write route below: the browser\n * clients these proxy for (the cart/checkout UI) always set\n * `Content-Type: application/json`, and rejecting anything else with a 400\n * BEFORE the upstream fetch is strictly safer than forwarding an\n * unparseable body to base-api. A SITE with a non-JSON caller can still pass\n * `{ requireJson: false }`.\n */\nconst DEFAULT_WRITE_OPTS: ShopWriteRouteOptions = { requireJson: true }\n\n/** Options for `createShopCheckoutRoute` — standard success/cancel URLs (REB-837) + the redirect-origin re-check (REB-836). */\nexport interface ShopCheckoutRouteOptions extends ShopWriteRouteOptions {\n /**\n * Path (relative to the resolved SITE origin — see `siteOrigin` below,\n * never a caller-supplied value, so this can never redirect off-SITE)\n * used to build a default `successUrl` when the posted body doesn't\n * already include one. The literal `{CHECKOUT_SESSION_ID}` placeholder is\n * preserved verbatim — Stripe substitutes it at redirect time, same as\n * `cartPath`'s `{cart}` shorthand in the issue brief. Normalized (REB-837\n * review fix): must resolve to a single SITE-relative path starting with\n * `/` — an absolute URL or a protocol-relative `//` is rejected (logged,\n * falls back to the default) rather than silently building an off-SITE\n * redirect.\n * @default `${cartPath}?pago=ok&session_id={CHECKOUT_SESSION_ID}`\n */\n successPath?: string\n /**\n * Path used to build a default `cancelUrl`, same normalization and origin\n * rule as `successPath`.\n * @default `${cartPath}?pago=cancelado`\n */\n cancelPath?: string\n /** Base path the `successPath`/`cancelPath` defaults are built from. @default '/cart' */\n cartPath?: string\n /**\n * Explicit SITE origin to build `successUrl`/`cancelUrl` from, overriding\n * `request.nextUrl.origin` (REB-837 review fix). Needed behind a reverse\n * proxy or when the Route Handler's own request origin doesn't match the\n * SITE's real public origin. Falls back to `NEXT_PUBLIC_SITE_URL` when\n * omitted, then to `request.nextUrl.origin`.\n */\n siteOrigin?: string\n /** Allowed hosts for base-api's returned Checkout URL. @default ['checkout.stripe.com'] */\n allowedRedirectHosts?: string[]\n /**\n * Overrides the `message` field of the 502 body returned when base-api's\n * Checkout URL fails the redirect-origin re-check.\n * @default 'Error de conexión con el proveedor de pago.'\n */\n unsafeRedirectMessage?: string\n}\n\n/**\n * Normalizes a SITE-relative path for `successPath`/`cancelPath`/`cartPath`\n * (REB-837 review fix): must start with exactly one `/`, never an absolute\n * URL (`scheme://…`) or a protocol-relative `//…` — either would let a\n * misconfigured caller build a redirect off the SITE's own origin despite\n * the origin itself being trustworthy (request-derived or explicit\n * `siteOrigin`). A path missing its leading slash is PREFIXED with one; an\n * absolute/protocol-relative path is REJECTED outright (logged once) and\n * falls back to `fallback` rather than silently building a dangerous URL.\n */\nfunction normalizeSitePath(path: string, fallback: string): string {\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(path) || path.startsWith('//')) {\n console.error(\n `[createShopCheckoutRoute] Rejected an absolute/protocol-relative path \"${path}\" — using the default instead.`,\n )\n return fallback\n }\n return path.startsWith('/') ? path : `/${path}`\n}\n\nfunction unsafeRedirectResponse(message: string): Response {\n return new Response(\n JSON.stringify({ error: 'unsafe_redirect', message, correlation_id: crypto.randomUUID() }),\n { status: 502, headers: { 'Content-Type': 'application/json; charset=utf-8' } },\n )\n}\n\n/** Extracts `data.url` from a JSON response body, or `undefined` if absent/malformed. */\nfunction extractCheckoutUrl(rawBody: string): string | undefined {\n try {\n const parsed = JSON.parse(rawBody) as { data?: { url?: unknown } }\n return typeof parsed?.data?.url === 'string' ? parsed.data.url : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * Transport headers that describe the UPSTREAM byte stream, not `rawBody`\n * (re-serialized text `fetch` already decoded) — forwarding them verbatim\n * would tell the browser to gunzip/measure plain JSON and break checkout.\n * Review follow-up (native review on REB-836): this is an explicit blocklist,\n * not reliance on `createProxyRoute`'s current header set, so a future change\n * to that shared factory (e.g. the parallel REB-837 work) can't silently\n * reintroduce the leak here.\n */\nconst UNSAFE_PASSTHROUGH_HEADERS = new Set(['content-encoding', 'content-length', 'transfer-encoding'])\n\n/** Copies `headers` minus {@link UNSAFE_PASSTHROUGH_HEADERS} — safe to pair with a re-serialized body. */\nfunction safeResponseHeaders(headers: Headers): Headers {\n const safe = new Headers()\n for (const [key, value] of headers.entries()) {\n if (!UNSAFE_PASSTHROUGH_HEADERS.has(key.toLowerCase())) safe.set(key, value)\n }\n return safe\n}\n\n/**\n * `POST /api/site/shop/checkout` — starts a hosted Stripe Checkout session.\n *\n * REB-837: when the posted body doesn't already carry `successUrl`/\n * `cancelUrl` (an explicit value on the body always wins — this never\n * overrides a caller that builds its own), injects them built from\n * `successPath`/`cancelPath` and the resolved SITE origin (`siteOrigin` >\n * `NEXT_PUBLIC_SITE_URL` > `request.nextUrl.origin` — see `siteOrigin`'s\n * docblock). If no origin resolves, the body is forwarded unchanged —\n * base-api's own required-field validation is the backstop, not a second\n * origin check here.\n *\n * REB-836 (defence in depth): every SITE hands the response's `url` straight\n * to `window.location.assign`, so on a successful upstream response this\n * RE-CHECKS that `url` is `https:` on an allowed host (default\n * `checkout.stripe.com`) before ever returning it to the browser — a\n * compromised/misconfigured base-api or a tampered response in transit must\n * never redirect a buyer off Stripe's domain. Mirrors `@reblu/site-client`'s\n * own `shop.checkout()` guard (`./checkout-redirect.ts`'s docblock explains\n * why this is a deliberate duplicate, not a shared import). Non-2xx upstream\n * responses (validation errors, 404s, etc.) pass through unchanged — there is\n * no redirect URL to check.\n */\nexport function createShopCheckoutRoute(opts: ShopCheckoutRouteOptions = {}) {\n const {\n successPath,\n cancelPath,\n cartPath = '/cart',\n siteOrigin,\n allowedRedirectHosts = DEFAULT_CHECKOUT_REDIRECT_HOSTS,\n unsafeRedirectMessage = 'Error de conexión con el proveedor de pago.',\n ...proxyOpts\n } = opts\n\n const resolvedCartPath = normalizeSitePath(cartPath, '/cart')\n const defaultSuccessPath = `${resolvedCartPath}?pago=ok&session_id={CHECKOUT_SESSION_ID}`\n const defaultCancelPath = `${resolvedCartPath}?pago=cancelado`\n const resolvedSuccessPath =\n successPath !== undefined ? normalizeSitePath(successPath, defaultSuccessPath) : defaultSuccessPath\n const resolvedCancelPath =\n cancelPath !== undefined ? normalizeSitePath(cancelPath, defaultCancelPath) : defaultCancelPath\n\n const proxyHandler = createProxyRoute('/api/site/shop/checkout', {\n ...DEFAULT_WRITE_OPTS,\n ...proxyOpts,\n transformBody: (rawBody, request) => {\n // Lazy, per-request (R6): `siteOrigin` wins (explicit, proxy-safe),\n // then `NEXT_PUBLIC_SITE_URL` (env fallback for a SITE behind a\n // reverse proxy where `request.nextUrl.origin` doesn't match its real\n // public origin), then the request's own origin.\n const origin = siteOrigin ?? process.env.NEXT_PUBLIC_SITE_URL ?? request.nextUrl?.origin\n if (!origin) return rawBody\n\n let parsed: Record<string, unknown>\n try {\n parsed = JSON.parse(rawBody) as Record<string, unknown>\n } catch {\n return rawBody\n }\n\n if (typeof parsed.successUrl !== 'string') {\n parsed.successUrl = `${origin}${resolvedSuccessPath}`\n }\n if (typeof parsed.cancelUrl !== 'string') {\n parsed.cancelUrl = `${origin}${resolvedCancelPath}`\n }\n return JSON.stringify(parsed)\n },\n })\n\n return async function handler(request: NextRequest): Promise<Response> {\n const response = await proxyHandler(request)\n if (!response.ok) return response\n\n const rawBody = await response.text()\n const url = extractCheckoutUrl(rawBody)\n if (!url || !isSafeCheckoutRedirectUrl(url, allowedRedirectHosts)) {\n console.error('[createShopCheckoutRoute] Blocked an unsafe checkout redirect URL')\n return unsafeRedirectResponse(unsafeRedirectMessage)\n }\n\n return new Response(rawBody, { status: response.status, headers: safeResponseHeaders(response.headers) })\n }\n}\n\n/** `POST /api/site/shop/validate-cart` — live stock/price re-check before checkout. */\nexport function createShopValidateCartRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/validate-cart', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/** `POST /api/site/shop/apply-discount` — validates a discount code against the cart. */\nexport function createShopApplyDiscountRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/apply-discount', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/**\n * `POST /api/site/shop/check-email`.\n *\n * @deprecated REB-834 — base-api no longer reveals whether an email has an\n * account (it was an enumeration oracle on the tenant's customer list): the\n * proxied answer is always `{ exists: false }`. Do not mount this route on a\n * new SITE; an existing mount keeps working until the next major removes it.\n * The checkout always offers «Iniciar sesión o continuar como invitado» and the\n * server links the order to an existing account after payment.\n */\nexport function createShopCheckEmailRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/check-email', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/** Options for the dynamic order-lookup route. `method` is always `'GET'`. */\nexport type ShopOrderRouteOptions = Omit<CreateProxyRouteOptions, 'method'>\n\n/**\n * `GET /api/site/shop/order/[sessionId]` — the one route in this file with a\n * per-request dynamic target path, so it can't be a one-line\n * `createProxyRoute(...)` call like the others: it reads Next's route\n * `params`, builds `/api/site/shop/order/{sessionId}` per request, and\n * delegates everything else (env, timeout, forwarding, error shaping) to\n * `createProxyRoute`.\n */\nexport function createShopOrderRoute(opts: ShopOrderRouteOptions = {}) {\n return async function handler(\n request: NextRequest,\n context: { params: Promise<{ sessionId: string }> },\n ): Promise<Response> {\n const { sessionId } = await context.params\n const targetPath = `/api/site/shop/order/${encodeURIComponent(sessionId)}`\n return createProxyRoute(targetPath, { ...opts, method: 'GET' })(request)\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reblu/site-next",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
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",
@@ -250,8 +250,8 @@
250
250
  "dependencies": {
251
251
  "@radix-ui/react-dialog": "^1.1.15",
252
252
  "zustand": "^5.0.9",
253
- "@reblu/site-client": "^5.4.0",
254
- "@reblu/site-contracts": "^1.12.0",
253
+ "@reblu/site-client": "^5.6.0",
254
+ "@reblu/site-contracts": "^1.14.0",
255
255
  "@reblu/site-ui": "^0.7.0"
256
256
  },
257
257
  "devDependencies": {