@forgecart/cli 2.202608121449.0 → 2.202608200713.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.
Files changed (63) hide show
  1. package/dist/src/commands/init.d.ts +16 -0
  2. package/dist/src/commands/init.js +18 -7
  3. package/dist/src/commands/init.js.map +1 -1
  4. package/package.json +1 -1
  5. package/templates/storefront/README.md +145 -61
  6. package/templates/storefront/next.config.js +20 -0
  7. package/templates/storefront/package.json +10 -2
  8. package/templates/storefront/postcss.config.js +1 -2
  9. package/templates/storefront/src/app/%5F%5Ffc/track/route.ts +30 -12
  10. package/templates/storefront/src/app/__forge_beacon/route.ts +1 -2
  11. package/templates/storefront/src/app/api/%5F%5Fbackend/methods/route.ts +27 -0
  12. package/templates/storefront/src/app/cart/page.tsx +33 -6
  13. package/templates/storefront/src/app/checkout/page.tsx +40 -0
  14. package/templates/storefront/src/app/error.tsx +21 -0
  15. package/templates/storefront/src/app/global-error.tsx +23 -0
  16. package/templates/storefront/src/app/globals.css +105 -8
  17. package/templates/storefront/src/app/layout.tsx +45 -17
  18. package/templates/storefront/src/app/page.tsx +153 -43
  19. package/templates/storefront/src/app/ping/route.ts +1 -2
  20. package/templates/storefront/src/app/products/[slug]/not-found.tsx +3 -8
  21. package/templates/storefront/src/app/products/[slug]/page.tsx +69 -23
  22. package/templates/storefront/src/app/products/page.tsx +52 -9
  23. package/templates/storefront/src/components/CartView.tsx +244 -117
  24. package/templates/storefront/src/components/ForgeTracker.tsx +131 -26
  25. package/templates/storefront/src/components/Header.tsx +8 -10
  26. package/templates/storefront/src/components/ProductCard.tsx +25 -13
  27. package/templates/storefront/src/components/ProductPurchase.tsx +13 -18
  28. package/templates/storefront/src/components/checkout/AddressStep.tsx +288 -0
  29. package/templates/storefront/src/components/checkout/CheckoutFlow.tsx +543 -0
  30. package/templates/storefront/src/components/checkout/CheckoutGate.tsx +45 -0
  31. package/templates/storefront/src/components/checkout/PaymentElementForm.tsx +138 -0
  32. package/templates/storefront/src/components/checkout/PaymentFormEmbed.tsx +89 -0
  33. package/templates/storefront/src/components/checkout/RatesStep.tsx +113 -0
  34. package/templates/storefront/src/instrumentation.ts +34 -0
  35. package/templates/storefront/src/lib/action-result.ts +30 -0
  36. package/templates/storefront/src/lib/backend-actions.ts +20 -0
  37. package/templates/storefront/src/lib/backend-client.ts +47 -0
  38. package/templates/storefront/src/lib/cart-context.tsx +157 -22
  39. package/templates/storefront/src/lib/checkout-session.ts +185 -0
  40. package/templates/storefront/src/lib/error-messages.ts +24 -0
  41. package/templates/storefront/src/lib/experiments.ts +42 -44
  42. package/templates/storefront/src/lib/forgecart.ts +61 -78
  43. package/templates/storefront/src/lib/format.ts +91 -0
  44. package/templates/storefront/src/lib/session-actions.ts +54 -0
  45. package/templates/storefront/src/lib/shop-config.ts +44 -0
  46. package/templates/storefront/src/lib/shop-session.ts +114 -0
  47. package/templates/storefront/src/lib/track-forward.ts +14 -1
  48. package/templates/storefront/src/lib/uuid.ts +19 -0
  49. package/templates/storefront/src/server/app.module.ts +18 -0
  50. package/templates/storefront/src/server/backend-api.ts +26 -0
  51. package/templates/storefront/src/server/backend-method.decorator.ts +23 -0
  52. package/templates/storefront/src/server/bootstrap.ts +122 -0
  53. package/templates/storefront/src/server/customer-extras/customer-extras.module.ts +13 -0
  54. package/templates/storefront/src/server/customer-extras/service/customer-extras.service.ts +58 -0
  55. package/templates/storefront/src/server/customer-extras/type/customer-extras.types.ts +11 -0
  56. package/templates/storefront/src/server/forgecart/forgecart-client.factory.ts +69 -0
  57. package/templates/storefront/src/server/forgecart/forgecart.module.ts +9 -0
  58. package/templates/storefront/src/server/runner.ts +91 -0
  59. package/templates/storefront/src/server/types.ts +36 -0
  60. package/templates/storefront/tsconfig.json +2 -0
  61. package/templates/storefront/.env.example +0 -12
  62. package/templates/storefront/src/lib/cart-actions.ts +0 -139
  63. package/templates/storefront/tailwind.config.js +0 -8
@@ -22,8 +22,24 @@ export declare const DEPS_READY_MARKER_PATH = ".forgecart/deps-ready.json";
22
22
  export interface InitOptions {
23
23
  /** Channel token (required). */
24
24
  token?: string;
25
+ /**
26
+ * Channel admin secret for the storefront's server-side admin operations.
27
+ * Optional at the CLI boundary (a plain storefront needs only the shop
28
+ * token); the pod-provisioning path always passes it. Written to `.env`
29
+ * only — never `.forgecart/config.json`, which is committable metadata.
30
+ */
31
+ adminSecret?: string;
25
32
  /** Shop API base URL. */
26
33
  apiUrl: string;
34
+ /**
35
+ * Shop API URL for the SHOPPER'S BROWSER (the client session socket), when
36
+ * it differs from `apiUrl`. Workspace pods have a split horizon: the pod's
37
+ * server side reaches the shop over an internal address (docker bridge /
38
+ * cluster network) that a shopper's browser can never use — and an HTTPS
39
+ * page must open its websocket against WSS. Absent = `apiUrl` serves both
40
+ * sides (deployed storefronts, loopback dev).
41
+ */
42
+ browserApiUrl?: string;
27
43
  /** Whether to also scaffold a NestJS application (planned for a later phase). */
28
44
  application?: boolean;
29
45
  /**
@@ -123,13 +123,22 @@ function buildConfig(channelToken, options) {
123
123
  storefrontPath: '.',
124
124
  };
125
125
  }
126
- /** Build the `.env` contents for the storefront SDK client. */
127
- function buildEnv(channelToken, apiUrl) {
128
- return [
126
+ /** Build the `.env` contents for the storefront SDK clients. */
127
+ function buildEnv(channelToken, options) {
128
+ const lines = [
129
129
  `FORGECART_CHANNEL_TOKEN=${channelToken}`,
130
- `FORGECART_SHOP_API_URL=${apiUrl}`,
131
- '',
132
- ].join('\n');
130
+ `FORGECART_SHOP_API_URL=${options.apiUrl}`,
131
+ ];
132
+ const browserApiUrl = options.browserApiUrl?.trim();
133
+ if (browserApiUrl) {
134
+ lines.push(`FORGECART_SHOP_API_BROWSER_URL=${browserApiUrl}`);
135
+ }
136
+ const secret = options.adminSecret?.trim();
137
+ if (secret) {
138
+ lines.push(`FORGECART_ADMIN_SECRET=${secret}`);
139
+ }
140
+ lines.push('');
141
+ return lines.join('\n');
133
142
  }
134
143
  /**
135
144
  * Write the per-channel config the storefront reads at runtime: `.forgecart/
@@ -142,7 +151,7 @@ async function writeChannelConfig(targetDir, channelToken, options) {
142
151
  const configDir = join(targetDir, '.forgecart');
143
152
  await mkdir(configDir, { recursive: true });
144
153
  await writeFile(join(configDir, 'config.json'), `${JSON.stringify(buildConfig(channelToken, options), null, 2)}\n`, 'utf8');
145
- await writeFile(join(targetDir, '.env'), buildEnv(channelToken, options.apiUrl), 'utf8');
154
+ await writeFile(join(targetDir, '.env'), buildEnv(channelToken, options), 'utf8');
146
155
  }
147
156
  /** Print the post-scaffold next steps. */
148
157
  function printNextSteps(targetDir, application, depsInstalled) {
@@ -223,7 +232,9 @@ export function createInitCommand() {
223
232
  .description('Scaffold a ForgeCart storefront in the target directory')
224
233
  .argument('[name]', 'Target directory name (defaults to the current directory)')
225
234
  .option('-t, --token <token>', 'ForgeCart channel token (required)')
235
+ .option('--admin-secret <secret>', 'Channel admin secret for server-side admin operations (written to .env only)')
226
236
  .option('--api-url <url>', 'Shop API base URL written to the storefront environment', DEFAULT_SHOP_API_URL)
237
+ .option('--browser-api-url <url>', 'Shop API URL for the shopper browser when it differs from --api-url (split-horizon pods; must be HTTPS so the session socket can use WSS)')
227
238
  .option('-a, --application', 'Also scaffold a NestJS application (TODO: next phase)')
228
239
  .option('--config-only', 'Write only per-channel config into an existing pre-warmed storefront (skip the template copy)')
229
240
  .action(async (name, options) => {
@@ -1 +1 @@
1
- {"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/commands/init.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/commands/init.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgecart/cli",
3
- "version": "2.202608121449.0",
3
+ "version": "2.202608200713.0",
4
4
  "type": "module",
5
5
  "description": "CLI for scaffolding and operating ForgeCart channel storefronts",
6
6
  "bin": {
@@ -1,87 +1,171 @@
1
1
  # Storefront
2
2
 
3
- A minimal [Next.js 15](https://nextjs.org) (App Router) storefront for a
4
- ForgeCart channel. It talks to the ForgeCart shop GraphQL API through
5
- `@forgecart/sdk` and renders product data in Server Components.
3
+ A [Next.js](https://nextjs.org) (App Router) storefront for a ForgeCart
4
+ channel: product browsing, a server-backed cart, and a complete multi-step
5
+ checkout all speaking the ForgeCart shop GraphQL API through the generated
6
+ `@forgecart/sdk` client, styled with the daisyUI design system.
6
7
 
7
8
  This directory is the scaffold copied verbatim into your project by
8
9
  `forgecart init`. Nothing here is compiled by the ForgeCart monorepo; it ships
9
10
  as-is in the published `@forgecart/cli` package.
10
11
 
11
- ## How the channel token + API URL wire in
12
+ ## Configuration
12
13
 
13
- The storefront reads two environment variables (server-side only):
14
+ The storefront reads three environment variables (server-side only):
14
15
 
15
16
  | Variable | Purpose |
16
- | ------------------------- | ------------------------------------------------------------- |
17
- | `FORGECART_CHANNEL_TOKEN` | Identifies the channel; sent as the `forgecart-token` header. |
18
- | `FORGECART_SHOP_API_URL` | Base URL of the channel's shop GraphQL API (`.../shop-api`). |
19
-
20
- `forgecart init` writes both to `.env` for you. To configure manually,
21
- copy `.env.example` to `.env` and fill them in.
22
-
23
- These values are consumed in [`src/lib/forgecart.ts`](src/lib/forgecart.ts),
24
- which constructs and memoizes a singleton `ForgeCartShopClient`:
17
+ | ------------------------- | -------------------------------------------------------------- |
18
+ | `FORGECART_CHANNEL_TOKEN` | Identifies the channel; sent as the `forgecart-token` header. |
19
+ | `FORGECART_SHOP_API_URL` | Base URL of the channel's shop GraphQL API (`.../shop-api`). |
20
+ | `FORGECART_ADMIN_SECRET` | Channel admin secret for server-side admin operations. |
21
+
22
+ `forgecart init` writes all of them to `.env` it is the sole author of that
23
+ file, and `.env` is the only env file this project ever has (no `.env.example`,
24
+ no `.env.local`). None of the values ever reaches the
25
+ browser: every module that constructs an SDK client starts with
26
+ `import 'server-only'`, so a client component importing one is a **build
27
+ error**, and the workspace-pod image build additionally asserts no SDK code
28
+ lands in the client chunks.
29
+
30
+ ## The backend gate (`sdk.backend`)
31
+
32
+ The storefront embeds a real NestJS backend under `src/server/` — booted as
33
+ an application context (no HTTP server; Next.js owns HTTP) in
34
+ `instrumentation.ts`, with normal `@Module`/`@Injectable` structure. A
35
+ service method decorated with `@BackendMethod()` (and declared in
36
+ `backend-api.ts`) becomes callable from any client component through the
37
+ typed proxy:
25
38
 
26
39
  ```ts
27
- new ForgeCartShopClient({
28
- endpoint: process.env.FORGECART_SHOP_API_URL,
29
- channelToken: process.env.FORGECART_CHANNEL_TOKEN,
30
- });
31
- ```
32
-
33
- Because the client is only imported from Server Components, the channel token
34
- never reaches the browser.
40
+ import { backend } from '@/lib/backend-client';
35
41
 
36
- ## Running locally
37
-
38
- ```bash
39
- npm install
40
- npm run dev # next dev --turbopack -> http://localhost:3000
42
+ const result = await backend.assignCustomerHash({});
43
+ if (!result.ok) renderError(result.error);
41
44
  ```
42
45
 
43
- A ForgeCart channel workspace serves this app as its live site by running
44
- `npm run dev` (development) or `npm run build && npm run start` (production; see
45
- the `Procfile`).
46
+ One Server Action is the transport no routes, no fetch calls, no
47
+ websockets. Methods receive a per-invocation session `{ admin, shop,
48
+ hasSession }`: `admin` is the channel-admin SDK gate (from
49
+ `FORGECART_ADMIN_SECRET`) and each method owns what that authority may be
50
+ used for; inputs are untrusted. The decorator is the allowlist, boot fails
51
+ loudly if the decorator scan and `backend-api.ts` drift, and in dev
52
+ `/api/__backend/methods` lists the live registry. The worked example
53
+ (`src/server/customer-extras/`) mints a random referral hash for the
54
+ signed-in customer and stores it as an ACF custom-field entry.
55
+
56
+ ## Design system
57
+
58
+ Styling is Tailwind v4 + daisyUI 5, and every component uses **semantic**
59
+ classes only (`bg-base-100`, `btn-primary`, `badge-secondary`, `steps`,
60
+ `alert`, …). The single source of brand truth is the `@plugin "daisyui/theme"`
61
+ block in [`src/app/globals.css`](src/app/globals.css) — restyling the whole
62
+ storefront means editing those tokens and nothing else.
63
+
64
+ ## Data flow
65
+
66
+ **Transport.** The generated SDK carries every operation over graphql-ws — a
67
+ per-request client is a per-request websocket connection. All SDK calls run on
68
+ the server; nothing SDK-shaped ships to the browser.
69
+
70
+ - [`src/lib/forgecart.ts`](src/lib/forgecart.ts) — the channel-scoped
71
+ singleton client for anonymous reads (products, selling plans) via typed
72
+ helpers (`getProducts`, `getProductBySlug`, `getFeaturedProducts`,
73
+ `getChannelSellingPlanGroups`, …), plus the SDK type re-exports the
74
+ components render. Pure display helpers (`formatPrice`,
75
+ `getStartingPrice`, plan labels) live in
76
+ [`src/lib/format.ts`](src/lib/format.ts), which client components import
77
+ freely — it carries no SDK runtime.
78
+ - [`src/lib/shop-action.ts`](src/lib/shop-action.ts) — the Server-Action
79
+ runner: builds a per-request client around the shopper's
80
+ `forgecart-session` cookie, runs one operation, persists any newly minted
81
+ session token, translates failure into the typed envelope, and
82
+ `dispose()`s the client so no action leaks its socket.
83
+ - [`src/lib/action-result.ts`](src/lib/action-result.ts) — `ActionResult<T>`:
84
+ Server Actions **never throw** across the RSC boundary (Next production
85
+ redacts thrown action errors). Failures arrive in-band as the SDK's
86
+ `ExtractedError` (`code`, server-localized `message`, `variables`), which
87
+ the UI renders inline.
88
+
89
+ **Cart.** [`src/lib/cart-actions.ts`](src/lib/cart-actions.ts) drives the
90
+ shop **order** API — the real cart, never browser storage. The interactive
91
+ state lives in [`src/lib/cart-context.tsx`](src/lib/cart-context.tsx): seeded
92
+ with the server-fetched order, updated from each action's returned order, and
93
+ carrying the typed `error` channel the cart UI renders. Coupons
94
+ (`applyCoupon`/`removeCoupon`) recalculate discounts and totals server-side;
95
+ setting a line's quantity to `0` removes it.
96
+
97
+ **Checkout.** [`src/lib/checkout-actions.ts`](src/lib/checkout-actions.ts) +
98
+ [`src/components/checkout/`](src/components/checkout/) implement the
99
+ reference flow on daisyUI `steps`:
100
+
101
+ 1. **Contact** — guest email + name (`setCustomerForOrder`).
102
+ 2. **Address** — type-ahead via the shop `addressAutocomplete` API (optional
103
+ capability; manual entry always works), country select populated from
104
+ `availableCountries` (never a hardcoded list), and the **required phone
105
+ number** the API enforces.
106
+ 3. **Shipping** — `refreshShippingRateGroups` quotes carriers explicitly (the
107
+ read query never does), rates render grouped per shipment with per-group
108
+ carrier failures, and the customer picks — the ArrangingPayment
109
+ transition requires a selection in every group.
110
+ 4. **Payment** — eligible providers → `createPaymentSession` →
111
+ `getSessionTemplate` renders the provider's hosted form in
112
+ [`PaymentFormEmbed`](src/components/checkout/PaymentFormEmbed.tsx), which
113
+ listens for the origin-pinned `iframe-ready` / `payment-success` /
114
+ `payment-error` handshake. No provider-specific code exists in the
115
+ template; coupon edits freeze once a session exists (the amount is
116
+ locked at session creation).
117
+ 5. **Confirmation** — the order code and summary.
118
+
119
+ Errors render where they belong: field-level for coded field errors
120
+ (`SHIPPING_ADDRESS_PHONE_REQUIRED`, `SHIPPING_ADDRESS_COUNTRY_*`,
121
+ `COUPON_CODE_*`), a step-level `alert` otherwise, with
122
+ [`src/app/error.tsx`](src/app/error.tsx) and `global-error.tsx` as render
123
+ backstops.
124
+
125
+ ## Sessions
126
+
127
+ The anonymous session is minted by the API on the first cart mutation and
128
+ persisted into the httpOnly `forgecart-session` cookie by the action runner.
129
+ The `/__fc/track` analytics relay may **establish** that identity for pure
130
+ browsers but never replaces an existing cookie, and the tracker defers its
131
+ first flush so a fast add-to-cart wins the first-touch race.
46
132
 
47
133
  ## Project layout
48
134
 
49
135
  ```
50
136
  src/
51
137
  lib/
52
- forgecart.ts # shop client singleton + typed product helpers
53
- cart-context.tsx # local (localStorage) cart state for the MVP
138
+ forgecart.ts # server-only shop client + typed reads + SDK types
139
+ format.ts # pure display helpers (client-safe)
140
+ shop-action.ts # per-request action runner (envelope + dispose)
141
+ action-result.ts # ActionResult<T> + fallback error
142
+ cart-actions.ts # cart Server Actions (incl. coupons)
143
+ checkout-actions.ts # checkout Server Actions
144
+ cart-context.tsx # client cart state + typed error channel
54
145
  components/
55
- Header.tsx # top nav: Home / Products / Cart
56
- ProductCard.tsx # product tile
57
- AddToCartButton.tsx
146
+ Header.tsx # top nav with live cart count
147
+ ProductCard.tsx # product tile
148
+ ProductPurchase.tsx # variant picker + purchase options + add-to-cart
149
+ CartView.tsx # cart lines, coupons, totals, checkout CTA
150
+ checkout/
151
+ CheckoutFlow.tsx # the stepped flow orchestrator
152
+ AddressStep.tsx # autocomplete + countries + phone
153
+ RatesStep.tsx # grouped radio-card rate selection
154
+ PaymentFormEmbed.tsx # origin-checked hosted-form host
58
155
  app/
59
- layout.tsx # html shell + header/footer + CartProvider
60
- page.tsx # home: greeting + featured products
61
- products/
62
- page.tsx # product grid
63
- [slug]/page.tsx # product detail
64
- cart/page.tsx # local cart
156
+ layout.tsx # html shell + header/footer + CartProvider
157
+ error.tsx # route render backstop
158
+ global-error.tsx # root render backstop
159
+ page.tsx # home: featured products
160
+ products/ # grid + [slug] detail
161
+ cart/page.tsx # cart
162
+ checkout/page.tsx # the stepped checkout
65
163
  ```
66
164
 
67
- ## Data access
165
+ ## Development
68
166
 
69
- `src/lib/forgecart.ts` exposes typed helpers over the shop client:
70
-
71
- - `getProducts({ take, skip })` -> `{ items, totalItems }`
72
- - `getFeaturedProducts(count)` -> `StorefrontProduct[]`
73
- - `getProductBySlug(slug)` / `getProductById(id)` -> `StorefrontProduct | null`
74
- - `getStartingPrice(product)` and `formatPrice(minorUnits, currency)`
75
-
76
- The generated typed product operations (`shopClient.product.shopProducts` /
77
- `.shopProduct`) only select `id` + `optionGroups`, so these helpers use the
78
- SDK's supported raw-query escape hatch (`shopClient.query<T>(document, vars)`)
79
- to also fetch `name`, `slug`, `description`, `featuredAsset`, and `variants`
80
- (price in minor units). Every selected field exists on the shop schema's
81
- `Product` type — extend the documents in `forgecart.ts` to fetch more.
82
-
83
- ## Cart
84
-
85
- The starter cart (`src/lib/cart-context.tsx` + `app/cart/page.tsx`) keeps state
86
- in the browser via `localStorage`. To take real orders, wire it to the
87
- ForgeCart `cart` / `checkout` shop operations exposed by the SDK.
167
+ ```bash
168
+ npm install
169
+ npm run dev # Turbopack dev server (what workspace pods run)
170
+ npm run build # production build (what deploys run; see Procfile)
171
+ ```
@@ -4,6 +4,18 @@ const nextConfig = {
4
4
  // storefront with `next start` without a full node_modules tree.
5
5
  output: 'standalone',
6
6
  reactStrictMode: true,
7
+ // The embedded NestJS backend (src/server/) must be required at runtime
8
+ // from node_modules, not bundled: Nest's core carries optional
9
+ // peer-dependency requires (microservices, platform adapters) that the
10
+ // bundler would otherwise chase into "module not found" errors.
11
+ // NOTE: externalizing Nest keeps it out of the NORMAL server bundle, but
12
+ // `src/instrumentation.ts` compiles under its own rules and statically
13
+ // resolves Nest's lazy `require('class-transformer')` /
14
+ // `require('class-validator')` (class-serializer + ValidationPipe). Those
15
+ // optional peers are therefore REAL dependencies in package.json — remove
16
+ // them and `next build` dies with Module-not-found on the Epinio staging
17
+ // path (CI run 31764049991, both attempts) while dev mode keeps working.
18
+ serverExternalPackages: ['@nestjs/core', '@nestjs/common'],
7
19
  // The visual-editor preview runs THIS dev server inside a workspace pod,
8
20
  // embedded cross-origin in the dashboard's editor iframe. Next 16 ENFORCES
9
21
  // `allowedDevOrigins`: it 403s any cross-site request to an internal dev
@@ -16,6 +28,7 @@ const nextConfig = {
16
28
  // prod preview host `<code>.preview.forgecart.dev` — it must be listed
17
29
  // explicitly. Dev-only — `next start` (deployed storefronts) ignores it.
18
30
  allowedDevOrigins: [
31
+ '*.127.0.0.1.nip.io', // LOCAL preview ingress (k3s Traefik :8081) — the host every local pod serves under; without it Next dev 403s the store's own /_next chunks locally (2026-08-13; the wm real-next-app fixture pins the same contract)
19
32
  '*.vm.forgecart.com', // `pnpm vm` Cloudflare-tunnel dashboards
20
33
  '*.dev.forgecart.dev', // dev dashboard + dev preview subdomains
21
34
  '*.forgecart.dev', // single-label deployed preview hosts
@@ -24,6 +37,13 @@ const nextConfig = {
24
37
  'dashboard.test.forgecart.com', // test dashboard origin that embeds the editor
25
38
  'dashboard.forgecart.com', // prod dashboard origin that embeds the editor
26
39
  ],
40
+ // PROGRESSIVE PRE-RENDERING (Next 16: Cache Components). Every route builds
41
+ // a STATIC shell and streams its dynamic holes (product data, experiment
42
+ // branches, the shopper's cart) per request — uncached IO must sit inside a
43
+ // Suspense boundary, which is exactly how the pages are structured. `next
44
+ // dev` semantics are unchanged; the built/published storefront gets the
45
+ // prerendered shells.
46
+ cacheComponents: true,
27
47
  experimental: {
28
48
  // Persist Turbopack's dev compile artifacts to `.next` so a pod can serve
29
49
  // a PRE-WARMED `.next` baked at build time instead of paying a cold first
@@ -10,18 +10,26 @@
10
10
  "dependencies": {
11
11
  "@forgecart/designer-runtime": "^1.202607161748.0",
12
12
  "@forgecart/sdk": "^1.2.6",
13
+ "@nestjs/common": "^11.1.14",
14
+ "@nestjs/core": "^11.1.14",
15
+ "class-transformer": "^0.5.1",
16
+ "class-validator": "^0.14.2",
13
17
  "next": "^16.1.0",
14
18
  "react": "^19.0.0",
15
19
  "react-dom": "^19.0.0",
20
+ "reflect-metadata": "^0.2.2",
21
+ "rxjs": "^7.8.2",
22
+ "server-only": "^0.0.1",
16
23
  "uuidv7": "^1.1.0"
17
24
  },
18
25
  "devDependencies": {
26
+ "@tailwindcss/postcss": "^4.2.1",
19
27
  "@types/node": "^22.10.0",
20
28
  "@types/react": "^19.0.0",
21
29
  "@types/react-dom": "^19.0.0",
22
- "autoprefixer": "^10.4.20",
30
+ "daisyui": "^5.5.19",
23
31
  "postcss": "^8.4.49",
24
- "tailwindcss": "^3.4.17",
32
+ "tailwindcss": "^4.2.1",
25
33
  "typescript": "^5.7.0"
26
34
  }
27
35
  }
@@ -1,6 +1,5 @@
1
1
  module.exports = {
2
2
  plugins: {
3
- tailwindcss: {},
4
- autoprefixer: {},
3
+ '@tailwindcss/postcss': {},
5
4
  },
6
5
  };
@@ -26,13 +26,23 @@ import {
26
26
  * one contract. This route owns everything batch-shaped: parsing, per-item
27
27
  * validation, the bot gate, session threading, and cookie persistence.
28
28
  *
29
- * Session capture mirrors `cart-actions.ts#persistSession`: the shop API
30
- * surfaces a freshly minted session in the response `extensions`
31
- * (`forgecart-auth-token`) exactly once; the first touched item captures it,
32
- * the remaining items of the batch ride it as Bearer (one identity per batch),
33
- * and the relay response persists it into the httpOnly `forgecart-session`
34
- * cookie with the same attributes the cart actions use the first tracked
35
- * interaction establishes the shopper identity the cart will later reuse.
29
+ * Session capture: the shop API surfaces a freshly minted session in the
30
+ * response `extensions` (`forgecart-auth-token`) exactly once; the first
31
+ * touched item captures it and the remaining items of the batch ride it as
32
+ * Bearer (one identity per batch). The relay may ESTABLISH the shopper
33
+ * identity persisting the mint into the httpOnly `forgecart-session` cookie
34
+ * (same attributes as the cart actions') when the request arrived cookie-less
35
+ * but it never REPLACES an existing cookie. The asymmetry is deliberate:
36
+ * an incoming cookie that the upstream re-mints against is either a live
37
+ * session owned by the cart path (clobbering it would vanish a just-created
38
+ * cart — the first-touch double-mint race) or a stale one, whose replacement
39
+ * is the cart path's job (the SDK adopts re-mints and the cart actions
40
+ * persist them; the relay replacing it here would race those writers).
41
+ *
42
+ * Residual window: two writers that BOTH start cookie-less inside the same
43
+ * in-flight window remain last-writer-wins. With the tracker's deferred first
44
+ * flush (`ForgeTracker`'s establishment grace) that requires a click-to-
45
+ * response race under ~100ms — accepted.
36
46
  *
37
47
  * Contract: per-item isolation (one upstream failure never kills the batch),
38
48
  * `accepted:false` is TERMINAL for that item (the tracker never retries), and
@@ -44,7 +54,6 @@ import {
44
54
  * escape is Next's documented way to serve a literal-underscore URL segment.
45
55
  * A folder literally named `__fc` would silently 404.
46
56
  */
47
- export const dynamic = 'force-dynamic';
48
57
 
49
58
  const SESSION_COOKIE = 'forgecart-session';
50
59
  /** Mirrors `cart-actions.ts#persistSession` — the cookie is one shared identity. */
@@ -136,6 +145,12 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
136
145
 
137
146
  const incomingSession = request.cookies.get(SESSION_COOKIE)?.value ?? null;
138
147
  const forwardedFor = request.headers.get('x-forwarded-for');
148
+ // #1014: mirror the browser's low-entropy Client Hints trio upstream —
149
+ // Chromium sends them on every request; the shop API's device
150
+ // identification prefers them over the frozen UA.
151
+ const secChUa = request.headers.get('sec-ch-ua') ?? undefined;
152
+ const secChUaMobile = request.headers.get('sec-ch-ua-mobile') ?? undefined;
153
+ const secChUaPlatform = request.headers.get('sec-ch-ua-platform') ?? undefined;
139
154
 
140
155
  let sessionToken = incomingSession;
141
156
  const results: TrackItemResult[] = [];
@@ -148,15 +163,18 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
148
163
  results.push({ accepted: false, eventId: null });
149
164
  continue;
150
165
  }
151
- const outcome = await forwardTrackEvent(input, { sessionToken, userAgent, forwardedFor });
166
+ const outcome = await forwardTrackEvent(input, { sessionToken, userAgent, forwardedFor, secChUa, secChUaMobile, secChUaPlatform });
152
167
  results.push({ accepted: outcome.accepted, eventId: outcome.eventId });
153
168
  if (outcome.sessionToken) sessionToken = outcome.sessionToken;
154
169
  }
155
170
 
156
171
  const response = NextResponse.json({ results });
157
- // Persist a newly minted (or upstream-refreshed) session exactly like
158
- // `cart-actions.ts#persistSession` same cookie, same attributes.
159
- if (sessionToken && sessionToken !== incomingSession) {
172
+ // Establish-only persist: write the cookie ONLY when the request arrived
173
+ // without one. A mint against an EXISTING cookie is never persisted here —
174
+ // replacing a live session would vanish the cart it holds (first-touch
175
+ // double-mint race), and replacing a stale one belongs to the cart path,
176
+ // whose SDK capture + persist owns re-mints (see the module docstring).
177
+ if (!incomingSession && sessionToken) {
160
178
  response.cookies.set(SESSION_COOKIE, sessionToken, {
161
179
  httpOnly: true,
162
180
  sameSite: 'lax',
@@ -10,10 +10,9 @@
10
10
  *
11
11
  * Gated entirely on `NODE_ENV === 'development'`: a deployed `next start` storefront
12
12
  * answers 404 here and forwards nothing — the whole beacon path ships only in the
13
- * in-pod dev-server. `force-dynamic` keeps it out of the static prerender so it
13
+ * in-pod dev-server. Request access keeps it out of the static prerender (cacheComponents) so it
14
14
  * behaves identically under `next dev` and a dev build.
15
15
  */
16
- export const dynamic = 'force-dynamic';
17
16
 
18
17
  const BEACON_PORT = process.env.FORGE_BEACON_PORT ?? '3002';
19
18
 
@@ -0,0 +1,27 @@
1
+ import { NextResponse } from 'next/server';
2
+
3
+ import { getBackend } from '../../../../server/bootstrap';
4
+
5
+ /**
6
+ * DEV-ONLY registry listing — the agent's verification surface for backend
7
+ * features. After authoring a method, the agent probes this route and
8
+ * confirms the new name is listed; a boot failure (the api-map drift check)
9
+ * surfaces here as a 500 carrying the actionable message.
10
+ *
11
+ * Gated on `NODE_ENV === 'development'`: the literal check is inlined by
12
+ * the bundler, so a production build answers 404 and dead-code-eliminates
13
+ * the body. Method NAMES are the only thing exposed — never inputs,
14
+ * outputs, or configuration.
15
+ */
16
+ export async function GET(): Promise<NextResponse> {
17
+ if (process.env.NODE_ENV !== 'development') {
18
+ return NextResponse.json({ error: 'not found' }, { status: 404 });
19
+ }
20
+ try {
21
+ const { registry } = await getBackend();
22
+ return NextResponse.json({ methods: [...registry.keys()].sort() });
23
+ } catch (error) {
24
+ const reason = error instanceof Error ? error.message : String(error);
25
+ return NextResponse.json({ error: reason }, { status: 500 });
26
+ }
27
+ }
@@ -1,13 +1,40 @@
1
+ import { connection } from 'next/server';
2
+ import { Suspense } from 'react';
3
+
1
4
  import { CartView } from '../../components/CartView';
2
5
  import { getChannelSellingPlanGroups } from '../../lib/forgecart';
3
6
 
4
- export const dynamic = 'force-dynamic';
7
+ /**
8
+ * Cart route. The shopper's cart itself lives on their OWN websocket
9
+ * (`CartView` hydrates it client-side); the server's only read here is the
10
+ * PUBLIC channel subscription plans off the anonymous channel singleton —
11
+ * streamed as a hole so the shell stays static (cacheComponents).
12
+ */
13
+ export default function CartPage() {
14
+ return (
15
+ <Suspense fallback={<CartRouteSkeleton />}>
16
+ <CartWithPlans />
17
+ </Suspense>
18
+ );
19
+ }
5
20
 
6
- export default async function CartPage() {
7
- // Channel-wide subscription plans are fetched server-side (the shop client is
8
- // server-only) and handed to the client cart view, which renders the
9
- // "Subscribe to your whole order" box only when there are any.
21
+ async function CartWithPlans() {
22
+ await connection();
23
+ // Channel-wide subscription plans are public channel data the one
24
+ // server-fetched input; the "Subscribe to your whole order" box renders
25
+ // only when there are any.
10
26
  const channelGroups = await getChannelSellingPlanGroups();
11
-
12
27
  return <CartView channelGroups={channelGroups} />;
13
28
  }
29
+
30
+ function CartRouteSkeleton() {
31
+ return (
32
+ <div className="space-y-8" aria-busy="true" aria-label="Loading your cart">
33
+ <h1 className="text-3xl font-bold tracking-tight">Your cart</h1>
34
+ <div className="grid items-start gap-8 lg:grid-cols-[minmax(0,1fr)_360px]">
35
+ <div className="skeleton h-64 w-full"></div>
36
+ <div className="skeleton h-80 w-full"></div>
37
+ </div>
38
+ </div>
39
+ );
40
+ }
@@ -0,0 +1,40 @@
1
+ import { connection } from 'next/server';
2
+ import { Suspense } from 'react';
3
+
4
+ import { CheckoutGate } from '../../components/checkout/CheckoutGate';
5
+ import { getAvailableCountries } from '../../lib/forgecart';
6
+
7
+ /**
8
+ * Checkout entry. The server's only job here is the PUBLIC prefetch — the
9
+ * channel's shipping countries off the anonymous channel singleton, streamed
10
+ * as a hole (cacheComponents). The session work (the order itself, every
11
+ * checkout mutation) lives on the shopper's websocket: `CheckoutGate`
12
+ * hydrates the live cart client-side and mounts the flow, or shows the
13
+ * honest empty state.
14
+ */
15
+ export default function CheckoutPage() {
16
+ return (
17
+ <div className="mx-auto max-w-2xl space-y-6">
18
+ <h1 className="text-2xl font-bold tracking-tight">Checkout</h1>
19
+ <Suspense fallback={<CheckoutRouteSkeleton />}>
20
+ <CheckoutWithCountries />
21
+ </Suspense>
22
+ </div>
23
+ );
24
+ }
25
+
26
+ async function CheckoutWithCountries() {
27
+ await connection();
28
+ const countries = await getAvailableCountries();
29
+ return <CheckoutGate countries={countries} />;
30
+ }
31
+
32
+ function CheckoutRouteSkeleton() {
33
+ return (
34
+ <div className="space-y-4" aria-busy="true" aria-label="Loading checkout">
35
+ <div className="skeleton h-10 w-full"></div>
36
+ <div className="skeleton h-40 w-full"></div>
37
+ <div className="skeleton h-10 w-40"></div>
38
+ </div>
39
+ );
40
+ }