@rovela-ai/sdk 0.19.3 → 0.19.5

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.
Files changed (211) hide show
  1. package/dist/admin/server/admin-session.d.ts.map +1 -1
  2. package/dist/admin/server/admin-session.js +7 -0
  3. package/dist/admin/server/admin-session.js.map +1 -1
  4. package/dist/auth/server/customer-session.d.ts.map +1 -1
  5. package/dist/auth/server/customer-session.js +5 -0
  6. package/dist/auth/server/customer-session.js.map +1 -1
  7. package/dist/checkout/server/checkout-customer.d.ts.map +1 -1
  8. package/dist/checkout/server/checkout-customer.js +5 -0
  9. package/dist/checkout/server/checkout-customer.js.map +1 -1
  10. package/docs/changelog/README.md +53 -0
  11. package/docs/changelog/epoch-01-chrome-consent.md +63 -0
  12. package/docs/changelog/epoch-02-platform-commerce.md +108 -0
  13. package/docs/changelog/epoch-03-i18n-files.md +72 -0
  14. package/docs/changelog/epoch-04-emails.md +65 -0
  15. package/docs/changelog/epoch-05-admin-experience.md +98 -0
  16. package/docs/changelog/epoch-06-mobile-integrity.md +72 -0
  17. package/docs/design/admin-ledger-visual-universe.html +1961 -0
  18. package/docs/design/admin-sidebar-and-band.html +542 -0
  19. package/package.json +3 -1
  20. package/templates/store-template/.claude/skills/add-section/SKILL.md +438 -0
  21. package/templates/store-template/.claude/skills/build-feature/SKILL.md +201 -0
  22. package/templates/store-template/.claude/skills/customize-theme/SKILL.md +167 -0
  23. package/templates/store-template/.claude/skills/debug-errors/SKILL.md +68 -0
  24. package/templates/store-template/.claude/skills/frontend-design/SKILL.md +176 -0
  25. package/templates/store-template/.claude/skills/generate-images/SKILL.md +115 -0
  26. package/templates/store-template/.claude/skills/manage-products/SKILL.md +187 -0
  27. package/templates/store-template/.claude/skills/override-sdk-component/SKILL.md +154 -0
  28. package/templates/store-template/.env.example +50 -0
  29. package/templates/store-template/.mcp.json +8 -0
  30. package/templates/store-template/.rovela/blueprint.example.json +190 -0
  31. package/templates/store-template/CLAUDE.md +14 -0
  32. package/templates/store-template/app/LayoutContent.tsx +106 -0
  33. package/templates/store-template/app/[...path]/page.tsx +79 -0
  34. package/templates/store-template/app/account/addresses/page.tsx +45 -0
  35. package/templates/store-template/app/account/layout.tsx +17 -0
  36. package/templates/store-template/app/account/orders/[id]/invoice/page.tsx +277 -0
  37. package/templates/store-template/app/account/orders/[id]/page.tsx +725 -0
  38. package/templates/store-template/app/account/orders/page.tsx +153 -0
  39. package/templates/store-template/app/account/page.tsx +184 -0
  40. package/templates/store-template/app/account/profile/page.tsx +434 -0
  41. package/templates/store-template/app/admin/(dashboard)/account/page.tsx +16 -0
  42. package/templates/store-template/app/admin/(dashboard)/analytics/events/page.tsx +14 -0
  43. package/templates/store-template/app/admin/(dashboard)/analytics/layout.tsx +48 -0
  44. package/templates/store-template/app/admin/(dashboard)/analytics/page.tsx +15 -0
  45. package/templates/store-template/app/admin/(dashboard)/analytics/visitors/page.tsx +14 -0
  46. package/templates/store-template/app/admin/(dashboard)/categories/[id]/page.tsx +38 -0
  47. package/templates/store-template/app/admin/(dashboard)/categories/new/page.tsx +35 -0
  48. package/templates/store-template/app/admin/(dashboard)/categories/page.tsx +223 -0
  49. package/templates/store-template/app/admin/(dashboard)/customers/[id]/page.tsx +26 -0
  50. package/templates/store-template/app/admin/(dashboard)/customers/page.tsx +33 -0
  51. package/templates/store-template/app/admin/(dashboard)/discounts/page.tsx +16 -0
  52. package/templates/store-template/app/admin/(dashboard)/emails/page.tsx +16 -0
  53. package/templates/store-template/app/admin/(dashboard)/files/page.tsx +16 -0
  54. package/templates/store-template/app/admin/(dashboard)/layout.tsx +44 -0
  55. package/templates/store-template/app/admin/(dashboard)/loading.tsx +88 -0
  56. package/templates/store-template/app/admin/(dashboard)/locations/page.tsx +16 -0
  57. package/templates/store-template/app/admin/(dashboard)/orders/[id]/page.tsx +66 -0
  58. package/templates/store-template/app/admin/(dashboard)/orders/page.tsx +26 -0
  59. package/templates/store-template/app/admin/(dashboard)/page.tsx +146 -0
  60. package/templates/store-template/app/admin/(dashboard)/payments/page.tsx +15 -0
  61. package/templates/store-template/app/admin/(dashboard)/products/[id]/page.tsx +38 -0
  62. package/templates/store-template/app/admin/(dashboard)/products/new/page.tsx +35 -0
  63. package/templates/store-template/app/admin/(dashboard)/products/page.tsx +31 -0
  64. package/templates/store-template/app/admin/(dashboard)/settings/page.tsx +46 -0
  65. package/templates/store-template/app/admin/(dashboard)/shipping/page.tsx +15 -0
  66. package/templates/store-template/app/admin/(dashboard)/tax/page.tsx +15 -0
  67. package/templates/store-template/app/admin/(dashboard)/users/[id]/page.tsx +114 -0
  68. package/templates/store-template/app/admin/(dashboard)/users/page.tsx +16 -0
  69. package/templates/store-template/app/admin/accept-invite/page.tsx +36 -0
  70. package/templates/store-template/app/admin/forgot-password/page.tsx +13 -0
  71. package/templates/store-template/app/admin/layout.tsx +47 -0
  72. package/templates/store-template/app/admin/login/page.tsx +528 -0
  73. package/templates/store-template/app/admin/reset-password/page.tsx +36 -0
  74. package/templates/store-template/app/api/account/addresses/[id]/route.ts +10 -0
  75. package/templates/store-template/app/api/account/addresses/route.ts +10 -0
  76. package/templates/store-template/app/api/account/change-password/route.ts +107 -0
  77. package/templates/store-template/app/api/account/orders/[id]/refund/route.ts +11 -0
  78. package/templates/store-template/app/api/account/orders/[id]/return/route.ts +14 -0
  79. package/templates/store-template/app/api/account/orders/[id]/route.ts +42 -0
  80. package/templates/store-template/app/api/account/orders/route.ts +29 -0
  81. package/templates/store-template/app/api/account/profile/route.ts +165 -0
  82. package/templates/store-template/app/api/admin/accept-invite/route.ts +11 -0
  83. package/templates/store-template/app/api/admin/analytics/events/route.ts +9 -0
  84. package/templates/store-template/app/api/admin/analytics/route.ts +10 -0
  85. package/templates/store-template/app/api/admin/analytics/visitors/route.ts +9 -0
  86. package/templates/store-template/app/api/admin/branding/favicon/route.ts +9 -0
  87. package/templates/store-template/app/api/admin/branding/logo/route.ts +9 -0
  88. package/templates/store-template/app/api/admin/categories/[id]/route.ts +9 -0
  89. package/templates/store-template/app/api/admin/categories/route.ts +9 -0
  90. package/templates/store-template/app/api/admin/check/route.ts +10 -0
  91. package/templates/store-template/app/api/admin/customer-groups/[id]/route.ts +10 -0
  92. package/templates/store-template/app/api/admin/customer-groups/route.ts +10 -0
  93. package/templates/store-template/app/api/admin/customers/[id]/addresses/[addressId]/route.ts +15 -0
  94. package/templates/store-template/app/api/admin/customers/[id]/addresses/route.ts +15 -0
  95. package/templates/store-template/app/api/admin/customers/[id]/route.ts +11 -0
  96. package/templates/store-template/app/api/admin/customers/bulk/route.ts +8 -0
  97. package/templates/store-template/app/api/admin/customers/route.ts +9 -0
  98. package/templates/store-template/app/api/admin/discounts/[id]/route.ts +11 -0
  99. package/templates/store-template/app/api/admin/discounts/route.ts +10 -0
  100. package/templates/store-template/app/api/admin/emails/preview/route.ts +4 -0
  101. package/templates/store-template/app/api/admin/emails/route.ts +5 -0
  102. package/templates/store-template/app/api/admin/emails/test/route.ts +4 -0
  103. package/templates/store-template/app/api/admin/files/route.ts +8 -0
  104. package/templates/store-template/app/api/admin/forgot-password/route.ts +7 -0
  105. package/templates/store-template/app/api/admin/linked-customer/route.ts +11 -0
  106. package/templates/store-template/app/api/admin/locations/[id]/route.ts +10 -0
  107. package/templates/store-template/app/api/admin/locations/route.ts +10 -0
  108. package/templates/store-template/app/api/admin/me/password/route.ts +13 -0
  109. package/templates/store-template/app/api/admin/me/route.ts +11 -0
  110. package/templates/store-template/app/api/admin/orders/[id]/refund/route.ts +7 -0
  111. package/templates/store-template/app/api/admin/orders/[id]/return/approve/route.ts +7 -0
  112. package/templates/store-template/app/api/admin/orders/[id]/return/reject/route.ts +7 -0
  113. package/templates/store-template/app/api/admin/orders/[id]/route.ts +9 -0
  114. package/templates/store-template/app/api/admin/orders/[id]/ship-with-shippo/route.ts +6 -0
  115. package/templates/store-template/app/api/admin/orders/[id]/shippo-rates/route.ts +6 -0
  116. package/templates/store-template/app/api/admin/orders/bulk/route.ts +8 -0
  117. package/templates/store-template/app/api/admin/orders/route.ts +9 -0
  118. package/templates/store-template/app/api/admin/payment-methods/[id]/route.ts +10 -0
  119. package/templates/store-template/app/api/admin/payment-methods/route.ts +10 -0
  120. package/templates/store-template/app/api/admin/products/[id]/route.ts +9 -0
  121. package/templates/store-template/app/api/admin/products/bulk/route.ts +9 -0
  122. package/templates/store-template/app/api/admin/products/route.ts +9 -0
  123. package/templates/store-template/app/api/admin/products/stats/route.ts +10 -0
  124. package/templates/store-template/app/api/admin/reset-password/route.ts +10 -0
  125. package/templates/store-template/app/api/admin/settings/route.ts +11 -0
  126. package/templates/store-template/app/api/admin/setup/route.ts +10 -0
  127. package/templates/store-template/app/api/admin/setup-guide/manual-mark/route.ts +13 -0
  128. package/templates/store-template/app/api/admin/setup-guide/route.ts +10 -0
  129. package/templates/store-template/app/api/admin/shipping/carriers/[id]/route.ts +9 -0
  130. package/templates/store-template/app/api/admin/shipping/carriers/route.ts +9 -0
  131. package/templates/store-template/app/api/admin/shipping/rates/[id]/route.ts +9 -0
  132. package/templates/store-template/app/api/admin/shipping/rates/route.ts +9 -0
  133. package/templates/store-template/app/api/admin/shipping/shippo/api-key/route.ts +1 -0
  134. package/templates/store-template/app/api/admin/shipping/shippo/validate/route.ts +1 -0
  135. package/templates/store-template/app/api/admin/shipping/zones/[id]/route.ts +9 -0
  136. package/templates/store-template/app/api/admin/shipping/zones/route.ts +9 -0
  137. package/templates/store-template/app/api/admin/stats/route.ts +9 -0
  138. package/templates/store-template/app/api/admin/stripe-status/route.ts +1 -0
  139. package/templates/store-template/app/api/admin/tax-zones/[id]/route.ts +7 -0
  140. package/templates/store-template/app/api/admin/tax-zones/route.ts +7 -0
  141. package/templates/store-template/app/api/admin/users/[id]/route.ts +14 -0
  142. package/templates/store-template/app/api/admin/users/route.ts +12 -0
  143. package/templates/store-template/app/api/admin-auth/[...nextauth]/route.ts +17 -0
  144. package/templates/store-template/app/api/analytics/track/route.ts +12 -0
  145. package/templates/store-template/app/api/auth/[...nextauth]/route.ts +19 -0
  146. package/templates/store-template/app/api/auth/forgot-password/route.ts +9 -0
  147. package/templates/store-template/app/api/auth/register/route.ts +9 -0
  148. package/templates/store-template/app/api/auth/resend-verification/route.ts +9 -0
  149. package/templates/store-template/app/api/auth/reset-password/route.ts +10 -0
  150. package/templates/store-template/app/api/auth/verify-email/route.ts +10 -0
  151. package/templates/store-template/app/api/categories/route.ts +10 -0
  152. package/templates/store-template/app/api/checkout/countries/route.ts +7 -0
  153. package/templates/store-template/app/api/checkout/discount/route.ts +9 -0
  154. package/templates/store-template/app/api/checkout/manual/route.ts +10 -0
  155. package/templates/store-template/app/api/checkout/payment-methods/route.ts +8 -0
  156. package/templates/store-template/app/api/checkout/pickup-locations/route.ts +8 -0
  157. package/templates/store-template/app/api/checkout/route.ts +10 -0
  158. package/templates/store-template/app/api/checkout/shipping/route.ts +21 -0
  159. package/templates/store-template/app/api/checkout/status/route.ts +10 -0
  160. package/templates/store-template/app/api/contact/route.ts +164 -0
  161. package/templates/store-template/app/api/media/presign/route.ts +7 -0
  162. package/templates/store-template/app/api/media/route.ts +7 -0
  163. package/templates/store-template/app/api/products/[slug]/route.ts +10 -0
  164. package/templates/store-template/app/api/products/route.ts +10 -0
  165. package/templates/store-template/app/api/store/settings/route.ts +12 -0
  166. package/templates/store-template/app/api/webhooks/stripe/route.ts +63 -0
  167. package/templates/store-template/app/auth/forgot-password/page.tsx +38 -0
  168. package/templates/store-template/app/auth/layout.tsx +17 -0
  169. package/templates/store-template/app/auth/reset-password/page.tsx +81 -0
  170. package/templates/store-template/app/auth/signin/page.tsx +62 -0
  171. package/templates/store-template/app/auth/signup/page.tsx +38 -0
  172. package/templates/store-template/app/auth/verify-email/page.tsx +185 -0
  173. package/templates/store-template/app/cart/page.tsx +137 -0
  174. package/templates/store-template/app/checkout/page.tsx +88 -0
  175. package/templates/store-template/app/checkout/success/page.tsx +50 -0
  176. package/templates/store-template/app/contact/layout.tsx +24 -0
  177. package/templates/store-template/app/contact/page.tsx +321 -0
  178. package/templates/store-template/app/faq/page.tsx +153 -0
  179. package/templates/store-template/app/globals.css +99 -0
  180. package/templates/store-template/app/layout.tsx +269 -0
  181. package/templates/store-template/app/not-found.tsx +35 -0
  182. package/templates/store-template/app/page.tsx +23 -0
  183. package/templates/store-template/app/privacy/page.tsx +281 -0
  184. package/templates/store-template/app/products/[slug]/ProductDetailClient.tsx +179 -0
  185. package/templates/store-template/app/products/[slug]/page.tsx +149 -0
  186. package/templates/store-template/app/products/layout.tsx +26 -0
  187. package/templates/store-template/app/products/page.tsx +102 -0
  188. package/templates/store-template/app/robots.ts +25 -0
  189. package/templates/store-template/app/sitemap.ts +76 -0
  190. package/templates/store-template/app/terms/page.tsx +313 -0
  191. package/templates/store-template/components/PathSyncBridge.tsx +61 -0
  192. package/templates/store-template/components/Providers.tsx +37 -0
  193. package/templates/store-template/components/layout/Footer.tsx +150 -0
  194. package/templates/store-template/components/layout/Header.tsx +162 -0
  195. package/templates/store-template/components/sections/.gitkeep +7 -0
  196. package/templates/store-template/components/sections/CTA.tsx +51 -0
  197. package/templates/store-template/components/sections/Features.tsx +91 -0
  198. package/templates/store-template/components/sections/Hero.tsx +85 -0
  199. package/templates/store-template/eslint.config.mjs +18 -0
  200. package/templates/store-template/lib/store-config.ts +77 -0
  201. package/templates/store-template/lib/store-policy.ts +68 -0
  202. package/templates/store-template/lib/utils.ts +21 -0
  203. package/templates/store-template/next-env.d.ts +6 -0
  204. package/templates/store-template/next.config.js +105 -0
  205. package/templates/store-template/package.json +32 -0
  206. package/templates/store-template/postcss.config.js +6 -0
  207. package/templates/store-template/proxy.ts +150 -0
  208. package/templates/store-template/public/rovela-logo.png +0 -0
  209. package/templates/store-template/tailwind.config.ts +74 -0
  210. package/templates/store-template/tsconfig.json +41 -0
  211. package/templates/store-template/types/next-auth.d.ts +32 -0
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Single source of truth for the store's shipping / returns / contact facts,
3
+ * derived from real configured data (lib/store-config.ts + store_settings).
4
+ *
5
+ * Both the Organization JSON-LD (app/layout.tsx) and the visible + marked-up
6
+ * FAQ (app/faq/page.tsx) read from here, so the structured data ALWAYS mirrors
7
+ * the visible page content — the invariant search + AI engines require (a
8
+ * mismatch gets the listing suppressed).
9
+ */
10
+
11
+ import { findSettings } from '@rovela-ai/sdk/core/server'
12
+ import type { ReturnPolicyInput } from '@rovela-ai/sdk/core'
13
+ import { storeConfig } from '@/lib/store-config'
14
+
15
+ type Settings = Awaited<ReturnType<typeof findSettings>>
16
+
17
+ /** Parse `storeConfig.orders.returnPolicy` ('30-days' | 'no-returns' | '14-days' …) → days. */
18
+ export function returnWindowDays(): number {
19
+ const p = String(storeConfig.orders?.returnPolicy ?? '30-days')
20
+ if (/no[-\s]?return/i.test(p)) return 0
21
+ const m = p.match(/(\d+)/)
22
+ return m ? parseInt(m[1], 10) : 30
23
+ }
24
+
25
+ /** Conservative default — merchant states the real policy on /faq + legal. */
26
+ export const RETURN_FEES: 'free' | 'customer-paid' = 'customer-paid'
27
+
28
+ export function shipsToCountries(settings: Settings): string[] {
29
+ return settings?.shippingCountries && settings.shippingCountries.length > 0
30
+ ? settings.shippingCountries
31
+ : ['US']
32
+ }
33
+
34
+ export function freeShippingThreshold(settings: Settings): number {
35
+ return settings?.freeShippingThreshold && settings.freeShippingThreshold > 0
36
+ ? settings.freeShippingThreshold
37
+ : 0
38
+ }
39
+
40
+ /**
41
+ * Public support email — merchant's own only. Never the `support@store.com`
42
+ * placeholder or a Rovela address. Falls back to support@<live-domain>.
43
+ */
44
+ export function supportEmail(settings: Settings): string | null {
45
+ const fromSettings = settings?.storeEmail?.trim()
46
+ if (fromSettings) return fromSettings
47
+ const cfg = storeConfig.supportEmail
48
+ if (cfg && cfg !== 'support@store.com') return cfg
49
+ const host = (process.env.NEXT_PUBLIC_APP_URL || '')
50
+ .replace(/^https?:\/\//, '')
51
+ .replace(/\/.*$/, '')
52
+ .trim()
53
+ const ownDomain =
54
+ !!host &&
55
+ !host.endsWith('rovela.app') &&
56
+ !host.endsWith('vercel.app') &&
57
+ !host.startsWith('localhost')
58
+ return ownDomain ? `support@${host}` : null
59
+ }
60
+
61
+ /** Return-policy shape for `buildStoreJsonLdGraph`. Days `0` ⇒ "returns not permitted". */
62
+ export function returnPolicyInput(settings: Settings): ReturnPolicyInput {
63
+ return {
64
+ merchantReturnDays: returnWindowDays(),
65
+ returnFees: RETURN_FEES,
66
+ applicableCountry: shipsToCountries(settings)[0] || 'US',
67
+ }
68
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Utility Functions
3
+ *
4
+ * Re-exports utilities from @rovela-ai/sdk/core for convenience.
5
+ * Add any store-specific utilities below the re-exports.
6
+ */
7
+
8
+ // Re-export SDK utilities
9
+ export {
10
+ cn,
11
+ formatPrice,
12
+ parsePrice,
13
+ generateSlug,
14
+ formatDate,
15
+ formatRelativeTime,
16
+ truncate,
17
+ capitalize,
18
+ titleCase,
19
+ isValidEmail,
20
+ isValidPhone,
21
+ } from '@rovela-ai/sdk/core'
@@ -0,0 +1,6 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ import "./.next/types/routes.d.ts";
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,105 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ // Enable transpilation of the SDK package
4
+ transpilePackages: ['@rovela-ai/sdk'],
5
+
6
+ // Next.js 16 blocks dev-only requests (HMR WebSocket included) from any
7
+ // origin other than `localhost` by default. Sandbox previews are served
8
+ // cross-origin via Blaxel's proxy on hostnames like
9
+ // <hash>.<region>.preview.bl.run (e.g. abc...88.us-was-1.preview.bl.run)
10
+ // The matcher Next uses (server/app-render/csrf-protection.js → `isCsrfOriginAllowed`)
11
+ // is segment-by-segment: a single `*` matches ONE label only, so
12
+ // `*.preview.bl.run` matches `foo.preview.bl.run` but NOT
13
+ // `foo.us-was-1.preview.bl.run` (two leading labels). Use `**` for
14
+ // cross-label matching — survives Blaxel adding new regions later.
15
+ // See: https://nextjs.org/docs/app/api-reference/config/next-config-js/allowedDevOrigins
16
+ allowedDevOrigins: ['**.preview.bl.run', '**.bl.run'],
17
+
18
+ // Hide Next.js dev badge — merchants see the sandbox preview during edit
19
+ // sessions and the badge looks like a bug to non-technical users. No-op in
20
+ // production builds (badge only renders in `next dev`).
21
+ devIndicators: false,
22
+
23
+ // Skip TypeScript build-time check. The sandbox runs `next dev` continuously
24
+ // during merchant edits — type errors surface immediately in the preview, so
25
+ // by the time we redeploy, the code is already validated. Re-checking on
26
+ // Vercel's build container is wasted time. Belt-and-suspenders only since
27
+ // Turbopack itself doesn't run tsc.
28
+ typescript: { ignoreBuildErrors: true },
29
+
30
+ // THE WIN: Turbopack filesystem cache for production builds. Vercel
31
+ // preserves `.next/cache` between deploys; without this flag, Turbopack
32
+ // ignores it and rebuilds everything from scratch each time (~55s).
33
+ // With the flag, warm rebuilds reuse cached compiler artifacts and drop
34
+ // to ~10-15s.
35
+ //
36
+ // Empirically validated (2026-05-13): sandbox build time goes from 24s
37
+ // warm → 5s warm with this flag enabled. Translation for Vercel:
38
+ // first deploy of a new project stays ~55s (cold), every subsequent
39
+ // deploy drops to ~10-15s. Opt-in experimental flag in Next.js 16,
40
+ // expected to graduate to stable.
41
+ experimental: {
42
+ turbopackFileSystemCacheForBuild: true,
43
+ },
44
+
45
+ // Ship the blueprint with EVERY serverless function. The email theme
46
+ // derives brand colors/fonts by reading .rovela/blueprint.json at runtime
47
+ // through a bundler-invisible loader (the emails graph is client-reachable,
48
+ // so it cannot statically import fs) -- which also makes the read invisible
49
+ // to Vercel's file tracer. Without this, only routes that statically import
50
+ // the SDK's core/config get the file, and every other function (checkout,
51
+ // webhooks, auth, admin emails) silently falls back to default styling.
52
+ outputFileTracingIncludes: {
53
+ '/**': ['./.rovela/blueprint.json'],
54
+ },
55
+
56
+ // Image domains for Unsplash, CDNs, and R2 storage
57
+ images: {
58
+ remotePatterns: [
59
+ {
60
+ protocol: 'https',
61
+ hostname: 'images.unsplash.com',
62
+ },
63
+ {
64
+ protocol: 'https',
65
+ hostname: '*.unsplash.com',
66
+ },
67
+ {
68
+ protocol: 'https',
69
+ hostname: '*.supabase.co',
70
+ },
71
+ {
72
+ protocol: 'https',
73
+ hostname: '*.cloudflare.com',
74
+ },
75
+ // R2 storage domains
76
+ {
77
+ protocol: 'https',
78
+ hostname: '*.r2.dev',
79
+ },
80
+ {
81
+ protocol: 'https',
82
+ hostname: '*.r2.cloudflarestorage.com',
83
+ },
84
+ ],
85
+ },
86
+
87
+ // Serve the AI-assistant identity file (public/llms.txt, written during
88
+ // generation) as Markdown, and keep it out of the search index — it's an
89
+ // identity / anti-hallucination signal for ChatGPT / Perplexity / Claude,
90
+ // not a ranking page.
91
+ async headers() {
92
+ return [
93
+ {
94
+ source: '/llms.txt',
95
+ headers: [
96
+ { key: 'Content-Type', value: 'text/markdown; charset=utf-8' },
97
+ { key: 'X-Robots-Tag', value: 'noindex' },
98
+ ],
99
+ },
100
+ ]
101
+ },
102
+
103
+ }
104
+
105
+ module.exports = nextConfig
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "store",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint ."
10
+ },
11
+ "dependencies": {
12
+ "@rovela-ai/sdk": "0.19.4",
13
+ "@tailwindcss/forms": "0.5.11",
14
+ "@tailwindcss/typography": "0.5.19",
15
+ "autoprefixer": "10.4.24",
16
+ "lucide-react": "0.555.0",
17
+ "next": "16.1.6",
18
+ "next-auth": "4.24.13",
19
+ "postcss": "8.5.6",
20
+ "react": "19.2.4",
21
+ "react-dom": "19.2.4",
22
+ "tailwindcss": "3.4.19"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "24.10.13",
26
+ "@types/react": "19.2.14",
27
+ "@types/react-dom": "19.2.3",
28
+ "eslint": "9.39.2",
29
+ "eslint-config-next": "16.1.6",
30
+ "typescript": "5.9.3"
31
+ }
32
+ }
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
@@ -0,0 +1,150 @@
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ // Pure locale data (no React, edge-safe). SINGLE SOURCE: adding a language
3
+ // to the SDK automatically extends prefix parsing + geo detection here.
4
+ import { SUPPORTED_LOCALES, COUNTRY_TO_LOCALE } from '@rovela-ai/sdk/locales'
5
+
6
+ /**
7
+ * Canonical-host redirect (SEO / trust).
8
+ *
9
+ * A deployed store answers on several hosts that all serve identical content:
10
+ * - the Rovela deployment URL ( *.rovela.app )
11
+ * - Vercel's deployment aliases ( *.vercel.app, including per-deploy hashes )
12
+ * - the merchant's own custom domain ( e.g. mystore.com ), once verified
13
+ *
14
+ * With no single canonical host, search engines and AI shopping crawlers index
15
+ * EVERY host as a separate site — duplicate content, diluted ranking, and the
16
+ * platform's *.rovela.app / *.vercel.app URLs surfacing in Google instead of the
17
+ * merchant's brand. (This is exactly what happened to early stores.)
18
+ *
19
+ * Fix: derive the canonical host from NEXT_PUBLIC_APP_URL — the merchant's live
20
+ * URL, which the platform points at the custom domain once it verifies and then
21
+ * triggers a rebuild so this value is re-inlined. When that canonical host is a
22
+ * real custom domain and a request arrives on a DIFFERENT host, 308-redirect to
23
+ * the canonical host (preserving path, query, method, and body).
24
+ *
25
+ * Strictly gated: it only ever fires when the canonical host is a real custom
26
+ * domain. While a store still lives on *.rovela.app (no custom domain yet), or
27
+ * inside the sandbox / preview ( *.bl.run ) or on localhost, the canonical host
28
+ * is NOT a custom domain, so nothing redirects and there are no loops.
29
+ *
30
+ * NEXT_PUBLIC_APP_URL is inlined at build time, so this constant is resolved
31
+ * once per cold start with the value baked into the deployment.
32
+ */
33
+
34
+ function resolveCanonicalHost(): string | null {
35
+ const raw = process.env.NEXT_PUBLIC_APP_URL || ''
36
+ if (!raw) return null
37
+
38
+ let host: string
39
+ try {
40
+ host = new URL(raw).host.toLowerCase()
41
+ } catch {
42
+ return null
43
+ }
44
+
45
+ // Only a real custom domain is a valid redirect target. Platform + preview +
46
+ // local hosts are never canonical.
47
+ if (
48
+ !host ||
49
+ host.endsWith('.rovela.app') ||
50
+ host.endsWith('.vercel.app') ||
51
+ host.endsWith('.bl.run') ||
52
+ host.startsWith('localhost') ||
53
+ host.startsWith('127.0.0.1')
54
+ ) {
55
+ return null
56
+ }
57
+
58
+ return host
59
+ }
60
+
61
+ const CANONICAL_HOST = resolveCanonicalHost()
62
+
63
+ /**
64
+ * Locale routing (i18n core — SDK `core/i18n`).
65
+ *
66
+ * The proxy stays DB-FREE (edge runtime): it only PARSES the URL and
67
+ * forwards detection hints as request headers; the redirect DECISIONS
68
+ * (which need store settings) live in the root layout via the SDK's
69
+ * `resolveLocaleRedirect`. Contract:
70
+ *
71
+ * /fr/products → rewrite to /products + `x-rovela-locale: fr`
72
+ * any request → `x-rovela-geo-locale` (from x-vercel-ip-country, hint
73
+ * only) + `x-rovela-pathname` (ORIGINAL path, prefix
74
+ * included — the layout builds redirects from it)
75
+ *
76
+ * Locale prefixes are only parsed for page-like paths — /api, /admin,
77
+ * Next internals and file requests pass through untouched.
78
+ */
79
+ function parseLocalePrefix(pathname: string): { locale: string; rest: string } | null {
80
+ for (const l of SUPPORTED_LOCALES) {
81
+ if (pathname === `/${l}` || pathname.startsWith(`/${l}/`)) {
82
+ const rest = pathname.slice(l.length + 1) || '/'
83
+ // Never treat /fr/api or /fr/admin as localized surfaces.
84
+ if (rest.startsWith('/api') || rest.startsWith('/admin')) return null
85
+ return { locale: l, rest }
86
+ }
87
+ }
88
+ return null
89
+ }
90
+
91
+ function isLocaleEligible(pathname: string): boolean {
92
+ if (pathname.startsWith('/api') || pathname.startsWith('/admin')) return false
93
+ if (pathname.startsWith('/_next')) return false
94
+ // File requests (images, sitemap.xml, robots.txt, …) are never localized.
95
+ if (/\.[a-zA-Z0-9]+$/.test(pathname)) return false
96
+ return true
97
+ }
98
+
99
+ export function proxy(request: NextRequest) {
100
+ // ── 1. Canonical-host redirect (must run first: one hop lands the visitor
101
+ // on the canonical host WITH their original path, locale prefix
102
+ // included; the next request there does the locale work). ──
103
+ const requestHost = (request.headers.get('host') || '').toLowerCase().split(':')[0]
104
+ if (
105
+ CANONICAL_HOST &&
106
+ requestHost &&
107
+ requestHost !== CANONICAL_HOST &&
108
+ requestHost !== 'localhost' &&
109
+ requestHost !== '127.0.0.1'
110
+ ) {
111
+ const url = new URL(request.url)
112
+ url.protocol = 'https:'
113
+ url.host = CANONICAL_HOST
114
+ url.port = ''
115
+ // 308 (permanent + method/body preserving) so Google consolidates ranking
116
+ // signals onto the canonical host and POSTs to API routes are not broken.
117
+ return NextResponse.redirect(url, 308)
118
+ }
119
+
120
+ // ── 2. Locale parse + detection hints. ──
121
+ const url = new URL(request.url)
122
+ if (!isLocaleEligible(url.pathname)) return NextResponse.next()
123
+
124
+ const requestHeaders = new Headers(request.headers)
125
+ requestHeaders.set('x-rovela-pathname', url.pathname)
126
+
127
+ const country = (request.headers.get('x-vercel-ip-country') || '').toUpperCase()
128
+ // Raw IP country for the cookie-banner region rule (consentRegionExempt).
129
+ // Missing/unknown country fails CLOSED (banner shows) — see cookie-consent.
130
+ if (country) requestHeaders.set('x-rovela-country', country)
131
+ const geoLocale = COUNTRY_TO_LOCALE[country]
132
+ if (geoLocale) requestHeaders.set('x-rovela-geo-locale', geoLocale)
133
+
134
+ const parsed = parseLocalePrefix(url.pathname)
135
+ if (parsed) {
136
+ requestHeaders.set('x-rovela-locale', parsed.locale)
137
+ const rewritten = new URL(url)
138
+ rewritten.pathname = parsed.rest
139
+ return NextResponse.rewrite(rewritten, { request: { headers: requestHeaders } })
140
+ }
141
+
142
+ return NextResponse.next({ request: { headers: requestHeaders } })
143
+ }
144
+
145
+ export const config = {
146
+ // Run on everything EXCEPT Next internals, static assets, and ACME / domain
147
+ // verification paths (served by the platform, not the app). The redirect must
148
+ // still cover pages, API routes, sitemap.xml, robots.txt, and OG images.
149
+ matcher: ['/((?!_next/static|_next/image|_next/data|\\.well-known|favicon\\.ico).*)'],
150
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Tailwind CSS Configuration
3
+ *
4
+ * NOTE: This file is a placeholder. Claude Agent will regenerate it using:
5
+ * import { generateTailwindConfig } from '@rovela-ai/sdk/theme'
6
+ * const config = generateTailwindConfig(blueprint.theme)
7
+ *
8
+ * This default configuration provides basic styling for the initial build.
9
+ */
10
+
11
+ import type { Config } from 'tailwindcss'
12
+
13
+ const config: Config = {
14
+ content: [
15
+ './app/**/*.{js,ts,jsx,tsx,mdx}',
16
+ './components/**/*.{js,ts,jsx,tsx,mdx}',
17
+ './lib/**/*.{js,ts,jsx,tsx,mdx}',
18
+ // Include SDK components
19
+ './node_modules/@rovela-ai/sdk/**/*.{js,ts,jsx,tsx}',
20
+ ],
21
+ theme: {
22
+ extend: {
23
+ colors: {
24
+ // Semantic colors (CSS variables set in globals.css)
25
+ background: 'hsl(var(--background))',
26
+ foreground: 'hsl(var(--foreground))',
27
+ card: {
28
+ DEFAULT: 'hsl(var(--card))',
29
+ foreground: 'hsl(var(--card-foreground))',
30
+ },
31
+ popover: {
32
+ DEFAULT: 'hsl(var(--popover))',
33
+ foreground: 'hsl(var(--popover-foreground))',
34
+ },
35
+ primary: {
36
+ DEFAULT: 'hsl(var(--primary))',
37
+ foreground: 'hsl(var(--primary-foreground))',
38
+ },
39
+ secondary: {
40
+ DEFAULT: 'hsl(var(--secondary))',
41
+ foreground: 'hsl(var(--secondary-foreground))',
42
+ },
43
+ muted: {
44
+ DEFAULT: 'hsl(var(--muted))',
45
+ foreground: 'hsl(var(--muted-foreground))',
46
+ },
47
+ accent: {
48
+ DEFAULT: 'hsl(var(--accent))',
49
+ foreground: 'hsl(var(--accent-foreground))',
50
+ },
51
+ destructive: {
52
+ DEFAULT: 'hsl(var(--destructive))',
53
+ foreground: 'hsl(var(--destructive-foreground))',
54
+ },
55
+ border: 'hsl(var(--border))',
56
+ input: 'hsl(var(--input))',
57
+ ring: 'hsl(var(--ring))',
58
+ },
59
+ borderRadius: {
60
+ lg: 'var(--radius)',
61
+ md: 'calc(var(--radius) - 2px)',
62
+ sm: 'calc(var(--radius) - 4px)',
63
+ },
64
+ fontFamily: {
65
+ heading: ['var(--font-heading)', 'system-ui', 'sans-serif'],
66
+ body: ['var(--font-body)', 'system-ui', 'sans-serif'],
67
+ mono: ['var(--font-mono)', 'monospace'],
68
+ },
69
+ },
70
+ },
71
+ plugins: [],
72
+ }
73
+
74
+ export default config
@@ -0,0 +1,41 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
9
+ "allowJs": true,
10
+ "skipLibCheck": true,
11
+ "strict": true,
12
+ "noEmit": true,
13
+ "esModuleInterop": true,
14
+ "module": "esnext",
15
+ "moduleResolution": "bundler",
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "jsx": "react-jsx",
19
+ "incremental": true,
20
+ "plugins": [
21
+ {
22
+ "name": "next"
23
+ }
24
+ ],
25
+ "paths": {
26
+ "@/*": [
27
+ "./*"
28
+ ]
29
+ }
30
+ },
31
+ "include": [
32
+ "next-env.d.ts",
33
+ "**/*.ts",
34
+ "**/*.tsx",
35
+ ".next/types/**/*.ts",
36
+ ".next/dev/types/**/*.ts"
37
+ ],
38
+ "exclude": [
39
+ "node_modules"
40
+ ]
41
+ }
@@ -0,0 +1,32 @@
1
+ import 'next-auth'
2
+
3
+ declare module 'next-auth' {
4
+ interface Session {
5
+ user: {
6
+ id: string
7
+ email: string
8
+ name?: string | null
9
+ image?: string | null
10
+ emailVerified?: boolean
11
+ role?: 'owner' | 'admin'
12
+ }
13
+ }
14
+
15
+ interface User {
16
+ id: string
17
+ email: string
18
+ name?: string | null
19
+ emailVerified?: boolean
20
+ role?: 'owner' | 'admin'
21
+ }
22
+ }
23
+
24
+ declare module 'next-auth/jwt' {
25
+ interface JWT {
26
+ id: string
27
+ email: string
28
+ name?: string | null
29
+ emailVerified?: boolean
30
+ role?: 'owner' | 'admin'
31
+ }
32
+ }