@forgecart/cli 2.202608300703.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 (76) hide show
  1. package/dist/src/commands/init.d.ts +21 -2
  2. package/dist/src/commands/init.js +18 -2
  3. package/dist/src/commands/init.js.map +1 -1
  4. package/package.json +1 -1
  5. package/templates/storefront/README.md +42 -4
  6. package/templates/storefront/next.config.js +29 -7
  7. package/templates/storefront/src/app/%5F%5Ffc/identify/route.ts +205 -0
  8. package/templates/storefront/src/app/%5F%5Ffc/track/route.ts +28 -25
  9. package/templates/storefront/src/app/__forge_beacon/route.ts +1 -1
  10. package/templates/storefront/src/app/cart/page.tsx +14 -2
  11. package/templates/storefront/src/app/checkout/page.tsx +14 -2
  12. package/templates/storefront/src/app/layout.tsx +85 -22
  13. package/templates/storefront/src/app/page.tsx +63 -20
  14. package/templates/storefront/src/app/pages/[slug]/not-found.tsx +23 -0
  15. package/templates/storefront/src/app/pages/[slug]/page.tsx +114 -0
  16. package/templates/storefront/src/app/ping/route.ts +1 -1
  17. package/templates/storefront/src/app/products/[slug]/not-found.tsx +6 -4
  18. package/templates/storefront/src/app/products/[slug]/page.tsx +204 -21
  19. package/templates/storefront/src/app/products/page.tsx +41 -6
  20. package/templates/storefront/src/app/register/page.tsx +54 -0
  21. package/templates/storefront/src/app/reset-password/page.tsx +60 -0
  22. package/templates/storefront/src/app/robots.ts +69 -0
  23. package/templates/storefront/src/app/sitemap.ts +106 -0
  24. package/templates/storefront/src/app/verify/page.tsx +155 -0
  25. package/templates/storefront/src/components/CartView.tsx +26 -7
  26. package/templates/storefront/src/components/ForgeTracker.tsx +108 -1
  27. package/templates/storefront/src/components/Header.tsx +30 -10
  28. package/templates/storefront/src/components/LanguageSwitcher.tsx +88 -0
  29. package/templates/storefront/src/components/LocaleLink.tsx +49 -0
  30. package/templates/storefront/src/components/ProductCard.tsx +10 -4
  31. package/templates/storefront/src/components/account/AccountMessage.tsx +59 -0
  32. package/templates/storefront/src/components/account/RegisterForm.tsx +283 -0
  33. package/templates/storefront/src/components/account/RequestPasswordResetForm.tsx +96 -0
  34. package/templates/storefront/src/components/account/ResetPasswordForm.tsx +169 -0
  35. package/templates/storefront/src/components/checkout/CheckoutGate.tsx +12 -4
  36. package/templates/storefront/src/lib/account/account-link.ts +76 -0
  37. package/templates/storefront/src/lib/account/register-state.ts +133 -0
  38. package/templates/storefront/src/lib/account/reset-password-state.ts +111 -0
  39. package/templates/storefront/src/lib/account/verify-state.ts +56 -0
  40. package/templates/storefront/src/lib/account-actions.ts +76 -0
  41. package/templates/storefront/src/lib/account-session.ts +47 -0
  42. package/templates/storefront/src/lib/asset-alt.ts +34 -0
  43. package/templates/storefront/src/lib/content/render-fields.tsx +256 -0
  44. package/templates/storefront/src/lib/content/resolve-page.ts +143 -0
  45. package/templates/storefront/src/lib/experiments.ts +1 -1
  46. package/templates/storefront/src/lib/forgecart.ts +300 -27
  47. package/templates/storefront/src/lib/format.ts +12 -14
  48. package/templates/storefront/src/lib/identify-forward.ts +152 -0
  49. package/templates/storefront/src/lib/locale/channel-locales-loader.ts +169 -0
  50. package/templates/storefront/src/lib/locale/channel-locales-map.ts +46 -0
  51. package/templates/storefront/src/lib/locale/channel-locales.ts +191 -0
  52. package/templates/storefront/src/lib/locale/grammar.ts +194 -0
  53. package/templates/storefront/src/lib/locale/localized-path.ts +55 -0
  54. package/templates/storefront/src/lib/locale/middleware-plan.ts +107 -0
  55. package/templates/storefront/src/lib/locale/request-binding.ts +80 -0
  56. package/templates/storefront/src/lib/locale/request-locale.ts +66 -0
  57. package/templates/storefront/src/lib/marketing-params.ts +213 -0
  58. package/templates/storefront/src/lib/money.ts +50 -0
  59. package/templates/storefront/src/lib/seo/alternates.ts +120 -0
  60. package/templates/storefront/src/lib/seo/json-ld.ts +266 -0
  61. package/templates/storefront/src/lib/seo/metadata.ts +323 -0
  62. package/templates/storefront/src/lib/seo/noindex.ts +218 -0
  63. package/templates/storefront/src/lib/seo/public-origin.ts +166 -0
  64. package/templates/storefront/src/lib/seo/redirect-plan.ts +86 -0
  65. package/templates/storefront/src/lib/seo/resolve-path.ts +126 -0
  66. package/templates/storefront/src/lib/seo/scaffolded-routes.ts +83 -0
  67. package/templates/storefront/src/lib/seo/sitemap-cache.ts +114 -0
  68. package/templates/storefront/src/lib/seo/sitemap-entries.ts +321 -0
  69. package/templates/storefront/src/lib/session-actions.ts +15 -8
  70. package/templates/storefront/src/lib/session-cookies.ts +98 -0
  71. package/templates/storefront/src/lib/shop-config.ts +9 -2
  72. package/templates/storefront/src/lib/shop-session.ts +20 -1
  73. package/templates/storefront/src/lib/track-forward.ts +43 -14
  74. package/templates/storefront/src/middleware.ts +150 -8
  75. package/templates/storefront/src/seo/redirects.ts +44 -0
  76. package/templates/storefront/src/server/runner.ts +1 -2
@@ -122,21 +122,21 @@ export function isObviousBot(userAgent: string): boolean {
122
122
  }
123
123
 
124
124
  /**
125
- * Forward one event as a raw HTTP GraphQL POST. Any failure — missing config,
126
- * network, HTTP status, GraphQL errors — resolves to `accepted:false`; the
127
- * forwarder is best-effort by design (analytics must never break the
128
- * storefront) and `accepted:false` is terminal (callers never retry).
125
+ * The header set every upstream call carries.
126
+ *
127
+ * ONE builder, shared with the marketing-identity forwarder
128
+ * (`identify-forward.ts`), because both speak about the SAME shopper: the
129
+ * session riding as `Authorization` decides which identity a row is written to,
130
+ * so a header set that drifted between them would split one visitor in two and
131
+ * nothing downstream could put them back together.
129
132
  */
130
- export async function forwardTrackEvent(
131
- input: TrackEventInput,
133
+ export function buildUpstreamHeaders(
134
+ channelToken: string,
132
135
  { sessionToken, userAgent, forwardedFor, secChUa, secChUaMobile, secChUaPlatform }: ForwardHeaders,
133
- ): Promise<UpstreamOutcome> {
134
- const upstream = getUpstreamConfig();
135
- if (!upstream) return { accepted: false, eventId: null, sessionToken: null };
136
-
136
+ ): Record<string, string> {
137
137
  const headers: Record<string, string> = {
138
138
  'content-type': 'application/json',
139
- 'forgecart-token': upstream.channelToken,
139
+ 'forgecart-token': channelToken,
140
140
  };
141
141
  if (sessionToken) headers['Authorization'] = `Bearer ${sessionToken}`;
142
142
  if (userAgent) headers['user-agent'] = userAgent;
@@ -144,18 +144,47 @@ export async function forwardTrackEvent(
144
144
  if (secChUa) headers['sec-ch-ua'] = secChUa;
145
145
  if (secChUaMobile) headers['sec-ch-ua-mobile'] = secChUaMobile;
146
146
  if (secChUaPlatform) headers['sec-ch-ua-platform'] = secChUaPlatform;
147
+ return headers;
148
+ }
149
+
150
+ /**
151
+ * The session the shop API minted for a cookie-less request, if it minted one.
152
+ *
153
+ * Surfaced in the response `extensions` exactly once. Shared with the identity
154
+ * forwarder for the same reason the headers are: both relays may ESTABLISH the
155
+ * shopper's `forgecart-session` cookie from this value, and two readings of
156
+ * "did the upstream mint a session" would eventually mean two cookies.
157
+ */
158
+ export function readMintedSessionToken(
159
+ extensions: Record<string, unknown> | undefined,
160
+ ): string | null {
161
+ const minted = extensions?.['forgecart-auth-token'];
162
+ return typeof minted === 'string' && minted.length > 0 ? minted : null;
163
+ }
164
+
165
+ /**
166
+ * Forward one event as a raw HTTP GraphQL POST. Any failure — missing config,
167
+ * network, HTTP status, GraphQL errors — resolves to `accepted:false`; the
168
+ * forwarder is best-effort by design (analytics must never break the
169
+ * storefront) and `accepted:false` is terminal (callers never retry).
170
+ */
171
+ export async function forwardTrackEvent(
172
+ input: TrackEventInput,
173
+ forward: ForwardHeaders,
174
+ ): Promise<UpstreamOutcome> {
175
+ const upstream = getUpstreamConfig();
176
+ if (!upstream) return { accepted: false, eventId: null, sessionToken: null };
147
177
 
148
178
  try {
149
179
  const response = await fetch(upstream.shopApiUrl, {
150
180
  method: 'POST',
151
- headers,
181
+ headers: buildUpstreamHeaders(upstream.channelToken, forward),
152
182
  body: JSON.stringify({ query: TRACK_EVENT_MUTATION, variables: { input } }),
153
183
  });
154
184
  if (!response.ok) return { accepted: false, eventId: null, sessionToken: null };
155
185
 
156
186
  const payload = (await response.json()) as GraphQLTrackResponse;
157
- const minted = payload.extensions?.['forgecart-auth-token'];
158
- const capturedToken = typeof minted === 'string' && minted.length > 0 ? minted : null;
187
+ const capturedToken = readMintedSessionToken(payload.extensions);
159
188
  const trackEvent = payload.data?.trackEvent;
160
189
  if (!trackEvent) return { accepted: false, eventId: null, sessionToken: capturedToken };
161
190
  return {
@@ -2,8 +2,18 @@ import { NextResponse } from 'next/server';
2
2
  import type { NextRequest } from 'next/server';
3
3
  import { uuidv7 } from 'uuidv7';
4
4
 
5
+ import {
6
+ hasServerAuthoredHeaders,
7
+ localeRequestHeaders,
8
+ planLocaleRewrite,
9
+ } from './lib/locale/middleware-plan';
10
+ import type { LocaleRewritePlan } from './lib/locale/middleware-plan';
11
+ import { planRedirect } from './lib/seo/redirect-plan';
12
+ import { REDIRECTS } from './seo/redirects';
13
+
5
14
  /**
6
- * Visitor-identity + forced-experiment-entry middleware (design doc S6 + F-B).
15
+ * Visitor-identity + forced-experiment-entry + locale-rewrite middleware
16
+ * (design doc S6 + F-B; locale stage = epic launch#54 W1-8 / S1).
7
17
  *
8
18
  * S6 — visitor identity. Mints the durable anonymous visitor key — the
9
19
  * `forgecart-visitor` cookie (UUIDv7, so ids sort by first-touch time) —
@@ -54,6 +64,56 @@ import { uuidv7 } from 'uuidv7';
54
64
  * propagation for the VISITOR id (it reads-else-mints within a
55
65
  * request-scoped cache); the forced map is different: it either rode in on
56
66
  * the request cookie or is deterministically rewritten here.
67
+ *
68
+ * S1 — locale rewrite. `/<xx>/<rest>` is rewritten to `/<rest>`, with the
69
+ * locale carried forward on the request as `x-forgecart-locale` and announced
70
+ * on the response as `Content-Language`. The route tree therefore stays FLAT:
71
+ * no `[locale]` segment, no root catch-all (#1041 law).
72
+ *
73
+ * The locale DECISION is not here — it is `planLocaleRewrite`, a pure module
74
+ * the template spec project pins. This file imports `next/server`, so nothing
75
+ * in it can be reached by that project; the locale stage therefore keeps no
76
+ * decision of its own and only maps the two plan cases onto Next responses.
77
+ * (The F-B parsing and merging below predate that split and are still
78
+ * unpinned — they are covered, like the rest of this file's runtime
79
+ * behaviour, only by the storefront-verify phases.)
80
+ *
81
+ * Three properties of this stage are load-bearing:
82
+ *
83
+ * - It obeys the same zero-fetch / zero-env contract as the rest of this
84
+ * file. A two-letter first segment is only a CANDIDATE here; whether the
85
+ * channel offers it is decided in the layout's request-time hole, the only
86
+ * place that can ask. Validating here would require a fetch, and a fetch in
87
+ * middleware is exactly what the prewarm boot cannot afford.
88
+ * - It runs BEFORE, and independently of, the cookie fast path below. A
89
+ * rewrite is not cookie work: an already-identified visitor carrying no
90
+ * forced entry does no cookie work at all, so a locale stage placed inside
91
+ * that branch would render the DEFAULT language for every prefixed URL a
92
+ * returning visitor opens.
93
+ * - It is the only place `Content-Language` can be set at all. No RSC API
94
+ * lets a layout or page write a response header, so this is the single
95
+ * emitter by necessity rather than by policy — and nothing downstream may
96
+ * add a second, which would produce two header lines.
97
+ *
98
+ * Two consequences are accepted deliberately. It labels the CANDIDATE,
99
+ * because validity is not knowable here: `/zz/x` carries
100
+ * `Content-Language: zz` on the 404 the layout then raises, and
101
+ * `/en/x` carries `Content-Language: en` on its 308. Both are inert — a
102
+ * crawler drops a 404 rather than indexing its language, and a 308 has no
103
+ * body to describe. And the UNPREFIXED default-language response carries no
104
+ * header at all, since this stage cannot know the channel's default without
105
+ * a fetch. The layout announces that one as `<html lang>`, which is the
106
+ * signal search engines actually consume for language; the header is the
107
+ * weaker of the two and is required here only for the prefixed case.
108
+ *
109
+ * Store-wide routes are protected from the locale stage by `planLocaleRewrite`,
110
+ * not by the matcher. The matcher below excludes only `_next/static`,
111
+ * `_next/image`, `favicon.ico`, `ping` and `__forge_beacon`, and only in their
112
+ * UNPREFIXED spelling — its negative lookahead is anchored at the start of the
113
+ * path, so `/de/ping` reaches this file even though `/ping` does not.
114
+ * `sitemap.xml`, `robots.txt`, `api` and `__fc` are not excluded at all and run
115
+ * through this middleware in every spelling. What keeps every one of them
116
+ * unrewritten is the planner's `pass`, whose list is the grammar's.
57
117
  */
58
118
 
59
119
  const VISITOR_COOKIE_NAME = 'forgecart-visitor';
@@ -165,7 +225,68 @@ function mergeForcedEntries(request: NextRequest): string | null {
165
225
  return JSON.stringify(Object.fromEntries(merged));
166
226
  }
167
227
 
228
+ /**
229
+ * The response the cookie steps decorate — one per plan case.
230
+ *
231
+ * A `pass` keeps the untouched `NextResponse.next()` whenever it can, and that
232
+ * is a deliberate budget decision rather than a leftover: passing
233
+ * `{ request: { headers } }` makes Next copy the ENTIRE inbound header block
234
+ * into `x-middleware-request-*` on the middleware→server hop, against Node's
235
+ * 16 KB default. This matcher covers `/api/*`, `_next/data`, `sitemap.xml` and
236
+ * every RSC prefetch, so paying that on all of them — to sanitize the few
237
+ * requests that actually arrive forged — would be a poor trade. The mutated
238
+ * path is taken only when there is something to say or something to strip.
239
+ *
240
+ * Headers are cloned AFTER any forced-entry cookie rewrite, so a rewritten
241
+ * request carries the merged cookie header too — the landing render in a
242
+ * prefixed locale must still see its forced variant. The query string survives
243
+ * because `nextUrl.clone()` carries it and only `pathname` is reassigned.
244
+ */
245
+ function buildBaseResponse(
246
+ request: NextRequest,
247
+ plan: LocaleRewritePlan,
248
+ requestHeadersMutated: boolean,
249
+ ): NextResponse {
250
+ if (plan.kind === 'pass') {
251
+ if (!requestHeadersMutated && !hasServerAuthoredHeaders(request.headers)) {
252
+ return NextResponse.next();
253
+ }
254
+ const headers = localeRequestHeaders(request.headers, plan, request.nextUrl.search);
255
+ return NextResponse.next({ request: { headers } });
256
+ }
257
+
258
+ const url = request.nextUrl.clone();
259
+ url.pathname = plan.routePath;
260
+ const headers = localeRequestHeaders(request.headers, plan, request.nextUrl.search);
261
+ const response = NextResponse.rewrite(url, { request: { headers } });
262
+ response.headers.set('Content-Language', plan.locale);
263
+ return response;
264
+ }
265
+
168
266
  export function middleware(request: NextRequest): NextResponse {
267
+ // W1-9 — rename redirects, BEFORE everything else. A retired URL is not a
268
+ // page of this site any more, so nothing downstream should spend work on it:
269
+ // not the locale rewrite (the destination gets its own pass), not the cookie
270
+ // stages (the browser follows the 308 and is identified on the hop that
271
+ // actually renders). The query string rides along, so a campaign link's
272
+ // `?fc-exp=` survives a rename.
273
+ //
274
+ // The table is a SOURCE file (`src/seo/redirects.ts`) rather than
275
+ // `next.config.js`'s `redirects()` because that one is read once at boot: an
276
+ // entry added there does nothing until the server restarts, which in a
277
+ // workspace pod means the merchant watches their rename not work.
278
+ const redirectTo = planRedirect(request.nextUrl.pathname, REDIRECTS);
279
+ if (redirectTo !== null) {
280
+ const destination = request.nextUrl.clone();
281
+ destination.pathname = redirectTo;
282
+ return NextResponse.redirect(destination, 308);
283
+ }
284
+
285
+ // Then the locale plan — outside the cookie fast path below, because it is
286
+ // not cookie work. An identified visitor with no forced entry does no cookie
287
+ // work at all, so a locale stage inside that branch would serve the default
288
+ // language for every prefixed URL a returning visitor opens.
289
+ const plan = planLocaleRewrite(request.nextUrl.pathname);
169
290
  const forcedEntryValue = mergeForcedEntries(request);
170
291
  const storedVisitorId = request.cookies.get(VISITOR_COOKIE_NAME)?.value;
171
292
  const mintVisitor = !storedVisitorId;
@@ -175,18 +296,41 @@ export function middleware(request: NextRequest): NextResponse {
175
296
  // `mintVisitor` implies a sync: a fresh id is written to BOTH homes.
176
297
  const syncMirror =
177
298
  mintVisitor || request.cookies.get(VISITOR_MIRROR_COOKIE_NAME)?.value !== storedVisitorId;
178
- if (!forcedEntryValue && !syncMirror) {
299
+ // The steady-state fast path: an identified visitor on an unprefixed URL with
300
+ // no campaign parameter needs no request-header override at all, and skipping
301
+ // it avoids Next copying the entire inbound header block into
302
+ // `x-middleware-request-*` on every such request.
303
+ //
304
+ // `hasServerAuthoredHeaders` MUST be part of this condition, not only of
305
+ // `buildBaseResponse`. A bare `NextResponse.next()` emits no
306
+ // `x-middleware-override-headers`, and Next deletes non-overridden inbound
307
+ // request headers ONLY under that header — so returning early here would let a
308
+ // forged `x-forgecart-locale` survive into the render on the exact path most
309
+ // real traffic takes (every returning browser: visitor cookie present and its
310
+ // mirror in agreement). A forged request instead falls through to
311
+ // `buildBaseResponse`, which strips the server-authored names and re-emits.
312
+ if (
313
+ plan.kind === 'pass' &&
314
+ !forcedEntryValue &&
315
+ !syncMirror &&
316
+ !hasServerAuthoredHeaders(request.headers)
317
+ ) {
179
318
  return NextResponse.next();
180
319
  }
181
320
 
182
- let response: NextResponse;
183
321
  if (forcedEntryValue) {
184
322
  // Rewrite this request's own cookie header so the landing render already
185
323
  // resolves the forced variant (Set-Cookie alone reaches only the NEXT
186
- // request), then persist for subsequent navigations. No redirect — the
187
- // campaign URL stays shareable.
324
+ // request), then persist for subsequent navigations. The forced-entry
325
+ // stage itself never redirects — the campaign URL stays shareable — and
326
+ // when the locale stage does canonicalize one, the query string rides
327
+ // along, so the entry survives the hop either way.
188
328
  request.cookies.set(FORCED_ENTRY_COOKIE_NAME, forcedEntryValue);
189
- response = NextResponse.next({ request: { headers: request.headers } });
329
+ }
330
+
331
+ const response = buildBaseResponse(request, plan, forcedEntryValue !== null);
332
+
333
+ if (forcedEntryValue) {
190
334
  response.cookies.set({
191
335
  name: FORCED_ENTRY_COOKIE_NAME,
192
336
  value: forcedEntryValue,
@@ -196,8 +340,6 @@ export function middleware(request: NextRequest): NextResponse {
196
340
  maxAge: FORCED_ENTRY_COOKIE_MAX_AGE_SECONDS,
197
341
  path: '/',
198
342
  });
199
- } else {
200
- response = NextResponse.next();
201
343
  }
202
344
 
203
345
  if (syncMirror) {
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Rename redirects — the one file you edit when a page moves.
3
+ *
4
+ * When a page's URL changes, the old address does not stop existing: it is in
5
+ * search results, in links other people wrote, and in browser history. Left to
6
+ * 404 it loses everything the old URL had earned. A 308 hands that across to
7
+ * the new address instead.
8
+ *
9
+ * Add an entry and save. The dev server picks it up immediately, so the
10
+ * redirect is live in the same session — which is exactly why this is a source
11
+ * file and not `next.config.js`'s `redirects()`: that one is read once at boot,
12
+ * so an entry added there does nothing until the server restarts.
13
+ *
14
+ * export const REDIRECTS: readonly RedirectRule[] = [
15
+ * { from: '/summer-sale', to: '/promotions/summer' },
16
+ * ];
17
+ *
18
+ * Three things to know, all enforced in `lib/seo/redirect-plan.ts`:
19
+ *
20
+ * - **Paths are written WITHOUT a language prefix.** `/summer-sale` covers
21
+ * `/de/summer-sale` too, and each redirects inside its own language. Writing
22
+ * `/de/summer-sale` here would cover only German — and the language you
23
+ * forgot would keep 404ing, silently.
24
+ * - **Matching is exact.** `/summer-sale` does not match `/summer-sale/tents`
25
+ * or `/summer-sale-2024`. Each moved page gets its own entry, because a
26
+ * prefix rule would drag URLs you never meant to move.
27
+ * - **Chains collapse and cycles are refused.** `/a → /b → /c` answers ONE
28
+ * 308 straight to `/c`, since every extra hop weakens the signal a crawler
29
+ * carries across. A cycle redirects nowhere at all rather than trapping a
30
+ * browser in a loop.
31
+ */
32
+
33
+ /** One moved page: an old address and the address that replaced it. */
34
+ export interface RedirectRule {
35
+ /** The retired path, unprefixed and rooted — e.g. `/summer-sale`. */
36
+ from: string;
37
+ /** Where it lives now, unprefixed and rooted — e.g. `/promotions/summer`. */
38
+ to: string;
39
+ }
40
+
41
+ /**
42
+ * The table. Empty in a fresh scaffold — nothing has been renamed yet.
43
+ */
44
+ export const REDIRECTS: readonly RedirectRule[] = [];
@@ -4,13 +4,12 @@ import { extractError, type ExtractedError } from '@forgecart/sdk';
4
4
  import { cookies } from 'next/headers';
5
5
 
6
6
  import { UNREACHABLE_ERROR, type ActionResult } from '../lib/action-result';
7
+ import { SESSION_COOKIE } from '../lib/session-cookies';
7
8
  import { getBackend } from './bootstrap';
8
9
  import { ForgeCartClientFactory } from './forgecart/forgecart-client.factory';
9
10
  import { BackendError } from './types';
10
11
  import type { BackendSession } from './types';
11
12
 
12
- const SESSION_COOKIE = 'forgecart-session';
13
-
14
13
  /** The gate has no env yet (image pre-warm / before `forgecart init`). */
15
14
  const NOT_CONFIGURED: ExtractedError = {
16
15
  code: 'BACKEND_NOT_CONFIGURED',