@forgecart/cli 2.202608221935.0 → 2.202609190800.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 (90) hide show
  1. package/dist/src/cli.js +3 -21
  2. package/dist/src/cli.js.map +1 -1
  3. package/dist/src/commands/__test__/cli-harness.d.ts +21 -0
  4. package/dist/src/commands/__test__/cli-harness.js +29 -0
  5. package/dist/src/commands/__test__/cli-harness.js.map +1 -0
  6. package/dist/src/commands/init.d.ts +28 -2
  7. package/dist/src/commands/init.js +100 -15
  8. package/dist/src/commands/init.js.map +1 -1
  9. package/dist/src/commands/refresh.d.ts +40 -0
  10. package/dist/src/commands/refresh.js +147 -0
  11. package/dist/src/commands/refresh.js.map +1 -0
  12. package/dist/src/commands/template-manifest.d.ts +38 -0
  13. package/dist/src/commands/template-manifest.js +116 -0
  14. package/dist/src/commands/template-manifest.js.map +1 -0
  15. package/dist/src/version.d.ts +10 -0
  16. package/dist/src/version.js +25 -0
  17. package/dist/src/version.js.map +1 -0
  18. package/package.json +1 -1
  19. package/templates/storefront/README.md +42 -4
  20. package/templates/storefront/next.config.js +29 -7
  21. package/templates/storefront/src/app/%5F%5Ffc/identify/route.ts +205 -0
  22. package/templates/storefront/src/app/%5F%5Ffc/track/route.ts +28 -25
  23. package/templates/storefront/src/app/__forge_beacon/route.ts +1 -1
  24. package/templates/storefront/src/app/cart/page.tsx +14 -2
  25. package/templates/storefront/src/app/checkout/page.tsx +14 -2
  26. package/templates/storefront/src/app/layout.tsx +85 -22
  27. package/templates/storefront/src/app/page.tsx +63 -20
  28. package/templates/storefront/src/app/pages/[slug]/not-found.tsx +23 -0
  29. package/templates/storefront/src/app/pages/[slug]/page.tsx +114 -0
  30. package/templates/storefront/src/app/ping/route.ts +1 -1
  31. package/templates/storefront/src/app/products/[slug]/not-found.tsx +6 -4
  32. package/templates/storefront/src/app/products/[slug]/page.tsx +204 -21
  33. package/templates/storefront/src/app/products/page.tsx +41 -6
  34. package/templates/storefront/src/app/register/page.tsx +54 -0
  35. package/templates/storefront/src/app/reset-password/page.tsx +60 -0
  36. package/templates/storefront/src/app/robots.ts +69 -0
  37. package/templates/storefront/src/app/sitemap.ts +106 -0
  38. package/templates/storefront/src/app/verify/page.tsx +155 -0
  39. package/templates/storefront/src/components/CartView.tsx +26 -7
  40. package/templates/storefront/src/components/ForgeTracker.tsx +108 -1
  41. package/templates/storefront/src/components/Header.tsx +30 -10
  42. package/templates/storefront/src/components/LanguageSwitcher.tsx +88 -0
  43. package/templates/storefront/src/components/LocaleLink.tsx +49 -0
  44. package/templates/storefront/src/components/ProductCard.tsx +10 -4
  45. package/templates/storefront/src/components/account/AccountMessage.tsx +59 -0
  46. package/templates/storefront/src/components/account/RegisterForm.tsx +283 -0
  47. package/templates/storefront/src/components/account/RequestPasswordResetForm.tsx +96 -0
  48. package/templates/storefront/src/components/account/ResetPasswordForm.tsx +169 -0
  49. package/templates/storefront/src/components/checkout/CheckoutGate.tsx +12 -4
  50. package/templates/storefront/src/lib/account/account-link.ts +76 -0
  51. package/templates/storefront/src/lib/account/register-state.ts +133 -0
  52. package/templates/storefront/src/lib/account/reset-password-state.ts +111 -0
  53. package/templates/storefront/src/lib/account/verify-state.ts +56 -0
  54. package/templates/storefront/src/lib/account-actions.ts +76 -0
  55. package/templates/storefront/src/lib/account-session.ts +47 -0
  56. package/templates/storefront/src/lib/asset-alt.ts +34 -0
  57. package/templates/storefront/src/lib/content/render-fields.tsx +256 -0
  58. package/templates/storefront/src/lib/content/resolve-page.ts +143 -0
  59. package/templates/storefront/src/lib/experiments.ts +1 -1
  60. package/templates/storefront/src/lib/forgecart.ts +300 -27
  61. package/templates/storefront/src/lib/format.ts +12 -14
  62. package/templates/storefront/src/lib/identify-forward.ts +152 -0
  63. package/templates/storefront/src/lib/locale/channel-locales-loader.ts +169 -0
  64. package/templates/storefront/src/lib/locale/channel-locales-map.ts +46 -0
  65. package/templates/storefront/src/lib/locale/channel-locales.ts +191 -0
  66. package/templates/storefront/src/lib/locale/grammar.ts +194 -0
  67. package/templates/storefront/src/lib/locale/localized-path.ts +55 -0
  68. package/templates/storefront/src/lib/locale/middleware-plan.ts +107 -0
  69. package/templates/storefront/src/lib/locale/request-binding.ts +80 -0
  70. package/templates/storefront/src/lib/locale/request-locale.ts +66 -0
  71. package/templates/storefront/src/lib/marketing-params.ts +213 -0
  72. package/templates/storefront/src/lib/money.ts +50 -0
  73. package/templates/storefront/src/lib/seo/alternates.ts +120 -0
  74. package/templates/storefront/src/lib/seo/json-ld.ts +266 -0
  75. package/templates/storefront/src/lib/seo/metadata.ts +323 -0
  76. package/templates/storefront/src/lib/seo/noindex.ts +218 -0
  77. package/templates/storefront/src/lib/seo/public-origin.ts +166 -0
  78. package/templates/storefront/src/lib/seo/redirect-plan.ts +86 -0
  79. package/templates/storefront/src/lib/seo/resolve-path.ts +126 -0
  80. package/templates/storefront/src/lib/seo/scaffolded-routes.ts +83 -0
  81. package/templates/storefront/src/lib/seo/sitemap-cache.ts +114 -0
  82. package/templates/storefront/src/lib/seo/sitemap-entries.ts +321 -0
  83. package/templates/storefront/src/lib/session-actions.ts +15 -8
  84. package/templates/storefront/src/lib/session-cookies.ts +98 -0
  85. package/templates/storefront/src/lib/shop-config.ts +9 -2
  86. package/templates/storefront/src/lib/shop-session.ts +42 -5
  87. package/templates/storefront/src/lib/track-forward.ts +43 -14
  88. package/templates/storefront/src/middleware.ts +196 -16
  89. package/templates/storefront/src/seo/redirects.ts +44 -0
  90. package/templates/storefront/src/server/runner.ts +1 -2
@@ -0,0 +1,256 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import { getAssetAlt } from '../asset-alt';
4
+ import type { PageEntry, PageFieldValue } from '../forgecart';
5
+
6
+ /**
7
+ * The generic renderer for an ACF page record (#1934, epic launch#50).
8
+ *
9
+ * A page definition is whatever the merchant built in the dashboard, so this
10
+ * file cannot know the fields it will be handed — it renders by ACF field TYPE,
11
+ * driven by the `__typename` the shop API discriminates every value with. The
12
+ * layout is deliberately plain: this is the scaffold a merchant edits, and a
13
+ * template that guessed at a design would be undone before it shipped.
14
+ *
15
+ * Order and labels come from the DEFINITIONS, not the values: the shop API
16
+ * returns field definitions in the order the editor arranged them, while the
17
+ * value list is storage order and means nothing to a reader. A definition with
18
+ * no value (a display-only field — a divider, a chart) simply has nothing to
19
+ * match and renders nothing, which is why no field-category test appears here.
20
+ *
21
+ * Repeater rows render one level deep, and the bound is a real one rather than
22
+ * an oversight: the shop operation unrolls the group tree three levels
23
+ * (fragments cannot recurse), so each level is a DIFFERENT generated type and a
24
+ * general recursion has no single shape to stand on. One level covers the
25
+ * "sections of a page" shape this route exists for; deeper trees are a
26
+ * follow-up with the operation change they need.
27
+ */
28
+
29
+ /** What this renderer needs of a field definition — nothing about its storage. */
30
+ export interface RenderableDefinition {
31
+ readonly fieldName: string;
32
+ readonly title?: string | null;
33
+ readonly hideTitle?: boolean | null;
34
+ readonly children?: readonly RenderableDefinition[] | null;
35
+ }
36
+
37
+ /** One repeater as an entry carries it: a named group of rows. */
38
+ type PageRepeater = PageEntry['groups'][number];
39
+
40
+ export function PageFields({
41
+ definitions,
42
+ entry,
43
+ }: {
44
+ definitions: readonly RenderableDefinition[];
45
+ entry: PageEntry;
46
+ }): ReactNode {
47
+ return (
48
+ <div className="space-y-8">
49
+ {definitions.map((definition) => (
50
+ <PageField
51
+ key={definition.fieldName}
52
+ definition={definition}
53
+ value={entry.fields.find((field) => field.name === definition.fieldName)}
54
+ repeater={entry.groups.find((group) => group.name === definition.fieldName)}
55
+ />
56
+ ))}
57
+ </div>
58
+ );
59
+ }
60
+
61
+ /**
62
+ * One definition's section — its label and whatever it holds.
63
+ *
64
+ * A field with neither a value nor rows renders NOTHING, not an empty section:
65
+ * an unpublished column reads as null (the shop API's live-column gating) and a
66
+ * merchant who left a field blank meant to leave it blank. Either way a bare
67
+ * heading over nothing is noise on a public page.
68
+ */
69
+ function PageField({
70
+ definition,
71
+ value,
72
+ repeater,
73
+ }: {
74
+ definition: RenderableDefinition;
75
+ value: PageFieldValue | undefined;
76
+ repeater: PageRepeater | undefined;
77
+ }): ReactNode {
78
+ const body = repeater ? renderRepeater(repeater, definition.children ?? []) : renderValue(value);
79
+ if (body === null) return null;
80
+
81
+ return (
82
+ // `data-fc-field` carries the field's identity into the DOM: it is what a
83
+ // merchant styles a single field by, and the stable hook the storefront
84
+ // e2e reads.
85
+ <section className="space-y-2" data-fc-field={definition.fieldName}>
86
+ {definition.hideTitle !== true && definition.title ? (
87
+ <h2 className="text-xl font-semibold tracking-tight">{definition.title}</h2>
88
+ ) : null}
89
+ {body}
90
+ </section>
91
+ );
92
+ }
93
+
94
+ /** A repeater's rows, each row rendering its own members through the same law. */
95
+ function renderRepeater(
96
+ repeater: PageRepeater,
97
+ members: readonly RenderableDefinition[],
98
+ ): ReactNode {
99
+ if (repeater.entries.length === 0) return null;
100
+ return (
101
+ <div className="space-y-6">
102
+ {repeater.entries.map((row) => (
103
+ <div key={row.id} className="space-y-2">
104
+ {members.map((member) => {
105
+ const body = renderValue(row.fields.find((field) => field.name === member.fieldName));
106
+ if (body === null) return null;
107
+ return (
108
+ <div key={member.fieldName} data-fc-field={member.fieldName}>
109
+ {member.hideTitle !== true && member.title ? (
110
+ <h3 className="text-sm font-semibold text-base-content/70">{member.title}</h3>
111
+ ) : null}
112
+ {body}
113
+ </div>
114
+ );
115
+ })}
116
+ </div>
117
+ ))}
118
+ </div>
119
+ );
120
+ }
121
+
122
+ /**
123
+ * One stored value as markup, or `null` when it holds nothing to show.
124
+ *
125
+ * An exhaustive `switch` over the discriminated union rather than a lookup
126
+ * table, and the `default` arm is the reason: it assigns the value to `never`,
127
+ * so adding an ACF field type without teaching this file about it is a COMPILE
128
+ * error in the merchant's scaffold instead of a section that silently renders
129
+ * blank on a live page.
130
+ */
131
+ function renderValue(value: PageFieldValue | undefined): ReactNode {
132
+ if (value === undefined) return null;
133
+
134
+ switch (value.__typename) {
135
+ case 'AcfStringValue':
136
+ return text(value.stringValue);
137
+ case 'AcfIntValue':
138
+ return absent(value.intValue) ? null : text(String(value.intValue));
139
+ case 'AcfFloatValue':
140
+ return absent(value.floatValue) ? null : text(String(value.floatValue));
141
+ case 'AcfBoolValue':
142
+ return absent(value.boolValue) ? null : text(value.boolValue ? 'Yes' : 'No');
143
+ case 'AcfDateValue':
144
+ // The API serializes DateTime as ISO 8601; rendered as `<time>` with the
145
+ // machine-readable value intact, and NOT locale-formatted here — a
146
+ // formatted date is a merchant presentation choice, and guessing one in
147
+ // the scaffold would be a decision nobody made.
148
+ return absent(value.dateValue) ? null : (
149
+ <time dateTime={value.dateValue} className="text-base-content/80">
150
+ {value.dateValue}
151
+ </time>
152
+ );
153
+ case 'AcfStringListValue':
154
+ return list(value.stringListValue);
155
+ case 'AcfJsonValue':
156
+ // Shown verbatim in a code block: JSON on a page is structured data the
157
+ // merchant's own components consume, and pretty-printing it here would be
158
+ // this file inventing a shape for someone else's payload.
159
+ return absent(value.jsonValue) ? null : (
160
+ <pre className="overflow-x-auto rounded-box bg-base-200 p-4 text-xs">{value.jsonValue}</pre>
161
+ );
162
+ case 'AcfRichTextValue':
163
+ // The server-rendered projection, never the raw delta: `html` is produced
164
+ // by the API's own sanitizing renderer, which is the one place that
165
+ // decision belongs.
166
+ //
167
+ // The guard is also what satisfies the seam: `dangerouslySetInnerHTML`
168
+ // takes `string | TrustedHTML`, so the projection is rendered only once
169
+ // it IS a string — never coerced past an absence with a `?? ''`, which
170
+ // would publish an empty rich-text section as if the merchant had
171
+ // written one.
172
+ return absent(value.html) ? null : (
173
+ <div
174
+ className="max-w-none text-base leading-relaxed text-base-content/80"
175
+ dangerouslySetInnerHTML={{ __html: value.html }}
176
+ />
177
+ );
178
+ case 'AcfAssetValue':
179
+ return value.asset ? <AssetImage asset={value.asset} /> : null;
180
+ case 'AcfAssetListValue':
181
+ return value.assets.length === 0 ? null : (
182
+ <div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
183
+ {value.assets.map((asset) => (
184
+ <AssetImage key={asset.id} asset={asset} />
185
+ ))}
186
+ </div>
187
+ );
188
+ case 'AcfRelationValue':
189
+ // The API resolves the related row's display summary; the id alone would
190
+ // be meaningless on a storefront, and a dangling reference resolves to
191
+ // no summary at all, which renders as nothing rather than as a raw uuid.
192
+ return value.entity ? text(value.entity.label) : null;
193
+ case 'AcfRelationListValue':
194
+ return list(value.entities.map((entity) => entity.label));
195
+ default:
196
+ return exhausted(value);
197
+ }
198
+ }
199
+
200
+ /** One asset as an image — decorative unless the merchant described it. */
201
+ function AssetImage({ asset }: { asset: { preview: string; altText?: string | null } }): ReactNode {
202
+ return (
203
+ // eslint-disable-next-line @next/next/no-img-element
204
+ <img
205
+ src={asset.preview}
206
+ alt={getAssetAlt(asset)}
207
+ className="w-full rounded-box border border-base-300 object-cover"
208
+ />
209
+ );
210
+ }
211
+
212
+ function text(content: string | null | undefined): ReactNode {
213
+ if (absent(content) || content.trim() === '') return null;
214
+ return <p className="text-base leading-relaxed text-base-content/80">{content}</p>;
215
+ }
216
+
217
+ function list(items: readonly string[] | null | undefined): ReactNode {
218
+ if (absent(items) || items.length === 0) return null;
219
+ return (
220
+ <ul className="list-inside list-disc space-y-1 text-base-content/80">
221
+ {items.map((item, index) => (
222
+ <li key={`${item}-${String(index)}`}>{item}</li>
223
+ ))}
224
+ </ul>
225
+ );
226
+ }
227
+
228
+ /**
229
+ * Is there nothing here?
230
+ *
231
+ * The generated SDK marks every nullable selection OPTIONAL as well as
232
+ * nullable (`stringValue?: Maybe<string>`), so "the merchant left this empty"
233
+ * reaches this file as `null` from the API answering today and as `undefined`
234
+ * from any pod whose SDK does not carry the selection at all. Both mean the
235
+ * same thing to a reader, and collapsing them in ONE predicate is what keeps a
236
+ * `String(undefined)`, an empty `<time>` or a bare `No` off a live page — each
237
+ * of which a `=== null` check alone would have rendered.
238
+ */
239
+ function absent<T>(value: T | null | undefined): value is null | undefined {
240
+ return value === null || value === undefined;
241
+ }
242
+
243
+ /**
244
+ * The compile-time exhaustiveness check for the switch above: the parameter is
245
+ * `never`, so a new ACF field type stops this file from building until it is
246
+ * handled.
247
+ *
248
+ * At RUNTIME it renders nothing rather than throwing, and the asymmetry is
249
+ * deliberate. The only way to reach it is a pod whose generated SDK is older
250
+ * than the API answering it, and in that state the honest behaviour is to show
251
+ * the fields this build understands — a throw would blank the merchant's whole
252
+ * live page over one unknown section.
253
+ */
254
+ function exhausted(_value: never): null {
255
+ return null;
256
+ }
@@ -0,0 +1,143 @@
1
+ import { isLocaleSegment } from '../locale/grammar';
2
+
3
+ /**
4
+ * What a storefront page URL resolves to (#1934, epic launch#50) — the WHOLE
5
+ * decision behind `/pages/<route>`, as a value.
6
+ *
7
+ * PURE BY CONTRACT, for the reason `lib/seo/resolve-path.ts` is: this is the
8
+ * law that decides between a rendered page and a real 404, and
9
+ * `tool/storefront-template-spec` imports it directly by relative path from
10
+ * OUTSIDE the scaffold, so nothing reachable from here may need the template's
11
+ * Next toolchain. The route file is left as dispatch — it turns `notFound` into
12
+ * Next's `notFound()` and `ok` into markup, and decides nothing itself.
13
+ *
14
+ * The inputs are declared STRUCTURALLY rather than imported from the SDK for
15
+ * the same reason (`ResolvableProduct` precedent): the generated query types
16
+ * satisfy these shapes structurally, so the scaffold's own `tsc` proves the
17
+ * assignment while this module stays importable with no `paths`.
18
+ */
19
+
20
+ /**
21
+ * The subset of a page definition this decision reads, declared AS THE SHOP
22
+ * API'S GENERATED TYPES CARRY IT.
23
+ *
24
+ * Every nullable selection reaches a scaffold OPTIONAL as well as nullable
25
+ * (`route?: Maybe<string>`), so a contract spelled `string | null` would refuse
26
+ * the very value the query produces — and, this module having no compiler of
27
+ * its own in this repository, the refusal would surface in the merchant's
28
+ * `next build` instead of here. Admitting the shape is therefore part of the
29
+ * contract, not a laxity: what each absence MEANS is decided in the table
30
+ * below, once, rather than guessed at by each caller.
31
+ */
32
+ export interface RoutedPageGroup {
33
+ /**
34
+ * `'page'` for a storefront page; anything else — a data definition, or a
35
+ * selection from a pod whose SDK does not carry the field — is not one.
36
+ */
37
+ readonly kind?: string | null;
38
+ /**
39
+ * The route the definition is mounted at. Absent means mounted NOWHERE: the
40
+ * column is nullable because most definitions are data rather than pages.
41
+ */
42
+ readonly route?: string | null;
43
+ }
44
+
45
+ /**
46
+ * Why a page URL is a 404. Four distinct causes, kept apart because they answer
47
+ * different questions about a deployment — "the merchant has not published
48
+ * this yet" and "this URL was never a page" look identical to a visitor and are
49
+ * nothing alike to whoever is debugging.
50
+ */
51
+ export type PageNotFoundReason =
52
+ /**
53
+ * The slug is a two-letter locale segment. No page may hold one (launch#54
54
+ * W1-7 — the server refuses to mint such a route rather than auto-suffixing
55
+ * it), so this URL can never carry content in any deployment. Judged first
56
+ * and without consulting the lookup, so the rule is stated where it is true
57
+ * rather than inferred from an empty result.
58
+ */
59
+ | 'reserved-locale-segment'
60
+ /**
61
+ * No page definition of this channel is mounted at this route: the lookup
62
+ * answered nothing, or what it answered carries no route of its own and is
63
+ * therefore mounted nowhere.
64
+ */
65
+ | 'unknown-route'
66
+ /** Something answered, but it is not a page definition. */
67
+ | 'not-a-page'
68
+ /**
69
+ * The page exists and has nothing a visitor may see: never given a record, or
70
+ * holding only records the publish fence hides — a DRAFT, or one SCHEDULED
71
+ * for a moment that has not arrived. The shop API already removed those rows
72
+ * (it fences every storefront read to published content), so this arm is the
73
+ * renderer HONOURING an empty result, never a second publish check.
74
+ */
75
+ | 'no-live-content';
76
+
77
+ /**
78
+ * The answer for one page URL.
79
+ *
80
+ * The `ok` arm carries BOTH halves of what the route renders, narrowed to the
81
+ * caller's own types. That is why it is generic rather than a bare verdict: a
82
+ * caller that had to re-check `group !== null` after a resolution that already
83
+ * proved it would be asserting what the law just decided.
84
+ */
85
+ export type PageResolution<TGroup, TEntry> =
86
+ | { kind: 'ok'; group: TGroup; entry: TEntry }
87
+ | { kind: 'notFound'; reason: PageNotFoundReason };
88
+
89
+ export interface ResolvePageInput<TGroup, TEntry> {
90
+ /** The slug exactly as it appeared in the URL, locale prefix already stripped. */
91
+ requestedRoute: string;
92
+ /** What `pageByRoute` answered, or null when it answered nothing. */
93
+ group: TGroup | null;
94
+ /**
95
+ * The page's visible records, in the order the CALLER asked for. A page is
96
+ * one URL, so it serves one record: the first of these. Which record that is
97
+ * is the caller's decision and belongs at the query (see `getPageEntries`),
98
+ * not here.
99
+ */
100
+ entries: readonly TEntry[];
101
+ }
102
+
103
+ /**
104
+ * Decide the honest answer for a page URL.
105
+ *
106
+ * TOTAL over its input and ordered so each arm may assume the ones above it did
107
+ * not fire. Two of the four refusals are the feature's own contract — an
108
+ * unpublished or not-yet-live page answers 404 — and the other two are what
109
+ * make the function total.
110
+ */
111
+ export function resolvePage<TGroup extends RoutedPageGroup, TEntry>({
112
+ requestedRoute,
113
+ group,
114
+ entries,
115
+ }: ResolvePageInput<TGroup, TEntry>): PageResolution<TGroup, TEntry> {
116
+ if (isLocaleSegment(requestedRoute)) {
117
+ return { kind: 'notFound', reason: 'reserved-locale-segment' };
118
+ }
119
+
120
+ if (group === null) return { kind: 'notFound', reason: 'unknown-route' };
121
+
122
+ // A boundary check, not a redundant one: the shop query returns the general
123
+ // field-definition type and nothing in that TYPE says "page", so this is the
124
+ // template asserting that what it is about to render as a page is one — the
125
+ // same posture `parseScaffoldedRoute` takes toward a value another process
126
+ // wrote.
127
+ if (group.kind !== 'page') return { kind: 'notFound', reason: 'not-a-page' };
128
+
129
+ // Mounted-ness, read back off the answer itself, in the same posture as the
130
+ // kind check above: `pageByRoute` matched this group BY its route, so one
131
+ // that carries none contradicts the question it answered. Stating that as a
132
+ // 404 is what keeps the route from rendering a page no URL owns — and it is
133
+ // why the absence is resolved HERE rather than by a caller that would have
134
+ // to invent a meaning for it.
135
+ if (group.route === null || group.route === undefined) {
136
+ return { kind: 'notFound', reason: 'unknown-route' };
137
+ }
138
+
139
+ const entry = entries[0];
140
+ if (entry === undefined) return { kind: 'notFound', reason: 'no-live-content' };
141
+
142
+ return { kind: 'ok', group, entry };
143
+ }
@@ -10,6 +10,7 @@ import { after } from 'next/server';
10
10
  import { cache } from 'react';
11
11
  import { uuidv7 } from 'uuidv7';
12
12
 
13
+ import { SESSION_COOKIE } from './session-cookies';
13
14
  import { forwardTrackEvent, getUpstreamConfig, isObviousBot } from './track-forward';
14
15
  import type { TrackEventInput, UpstreamConfig } from './track-forward';
15
16
 
@@ -61,7 +62,6 @@ import type { TrackEventInput, UpstreamConfig } from './track-forward';
61
62
  */
62
63
 
63
64
  const VISITOR_COOKIE = 'forgecart-visitor';
64
- const SESSION_COOKIE = 'forgecart-session';
65
65
  const FORCED_ENTRY_COOKIE = 'forgecart-exp-force';
66
66
 
67
67
  /** Delivery-config cache TTL — the design doc's 60s config-plane freshness. */