keystone-design-bootstrap 1.0.102 → 1.0.104

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/README.md +4 -4
  2. package/dist/blog-post-vWzW8yFb.d.ts +50 -0
  3. package/dist/contexts/index.d.ts +13 -0
  4. package/dist/contexts/index.js +173 -0
  5. package/dist/contexts/index.js.map +1 -0
  6. package/dist/design_system/components/DynamicFormFields.d.ts +15 -0
  7. package/dist/design_system/components/DynamicFormFields.js +5176 -0
  8. package/dist/design_system/components/DynamicFormFields.js.map +1 -0
  9. package/dist/design_system/elements/index.d.ts +383 -0
  10. package/dist/design_system/elements/index.js +4007 -0
  11. package/dist/design_system/elements/index.js.map +1 -0
  12. package/dist/design_system/logo/keystone-logo.d.ts +6 -0
  13. package/dist/design_system/logo/keystone-logo.js +145 -0
  14. package/dist/design_system/logo/keystone-logo.js.map +1 -0
  15. package/dist/design_system/sections/index.d.ts +233 -0
  16. package/dist/design_system/sections/index.js +20048 -0
  17. package/dist/design_system/sections/index.js.map +1 -0
  18. package/dist/form-C94A_PX_.d.ts +36 -0
  19. package/dist/index.d.ts +70 -0
  20. package/dist/index.js +20608 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/lib/component-registry.d.ts +13 -0
  23. package/dist/lib/component-registry.js +36 -0
  24. package/dist/lib/component-registry.js.map +1 -0
  25. package/dist/lib/hooks/index.d.ts +83 -0
  26. package/dist/lib/hooks/index.js +182 -0
  27. package/dist/lib/hooks/index.js.map +1 -0
  28. package/dist/lib/server-api.d.ts +67 -0
  29. package/dist/lib/server-api.js +195 -0
  30. package/dist/lib/server-api.js.map +1 -0
  31. package/dist/package-DeHKpQp7.d.ts +121 -0
  32. package/dist/photos-CmBdWiuZ.d.ts +27 -0
  33. package/dist/themes/index.d.ts +17 -0
  34. package/dist/themes/index.js +29 -0
  35. package/dist/themes/index.js.map +1 -0
  36. package/dist/tracking/index.d.ts +254 -0
  37. package/dist/tracking/index.js +587 -0
  38. package/dist/tracking/index.js.map +1 -0
  39. package/dist/types/index.d.ts +313 -0
  40. package/dist/types/index.js +1 -0
  41. package/dist/types/index.js.map +1 -0
  42. package/dist/utils/cx.d.ts +15 -0
  43. package/dist/utils/cx.js +18 -0
  44. package/dist/utils/cx.js.map +1 -0
  45. package/dist/utils/gradient-placeholder.d.ts +7 -0
  46. package/dist/utils/gradient-placeholder.js +59 -0
  47. package/dist/utils/gradient-placeholder.js.map +1 -0
  48. package/dist/utils/is-react-component.d.ts +21 -0
  49. package/dist/utils/is-react-component.js +20 -0
  50. package/dist/utils/is-react-component.js.map +1 -0
  51. package/dist/utils/markdown-toc.d.ts +14 -0
  52. package/dist/utils/markdown-toc.js +29 -0
  53. package/dist/utils/markdown-toc.js.map +1 -0
  54. package/dist/utils/phone-helpers.d.ts +24 -0
  55. package/dist/utils/phone-helpers.js +26 -0
  56. package/dist/utils/phone-helpers.js.map +1 -0
  57. package/dist/utils/photo-helpers.d.ts +38 -0
  58. package/dist/utils/photo-helpers.js +41 -0
  59. package/dist/utils/photo-helpers.js.map +1 -0
  60. package/dist/website-photos-Cl1YqAno.d.ts +21 -0
  61. package/package.json +1 -1
  62. package/src/lib/consumer-session.ts +3 -1
  63. package/src/next/routes/consumer-auth.ts +47 -39
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/markdown-toc.ts"],"sourcesContent":["/**\n * Utility functions for extracting table of contents from markdown\n */\n\nexport interface TableOfContentsItem {\n id: string;\n title: string;\n level: number;\n}\n\n/**\n * Generate a URL-friendly ID from a heading text\n */\nfunction slugify(text: string): string {\n return text\n .toLowerCase()\n .trim()\n .replace(/[^\\w\\s-]/g, '') // Remove special characters\n .replace(/[\\s_-]+/g, '-') // Replace spaces and underscores with hyphens\n .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens\n}\n\n/**\n * Extract headings from markdown content and generate table of contents\n */\nexport function extractTableOfContents(markdown: string): TableOfContentsItem[] {\n if (!markdown) return [];\n\n const toc: TableOfContentsItem[] = [];\n const lines = markdown.split('\\n');\n\n for (const line of lines) {\n // Match markdown headings (##, ###, etc.)\n const headingMatch = line.match(/^(#{1,6})\\s+(.+)$/);\n if (headingMatch) {\n const level = headingMatch[1].length;\n const title = headingMatch[2].trim();\n \n // Only include h2 and h3 headings in TOC\n if (level >= 2 && level <= 3) {\n const id = slugify(title);\n toc.push({\n id,\n title,\n level,\n });\n }\n }\n }\n\n return toc;\n}\n\n\n"],"mappings":";AAaA,SAAS,QAAQ,MAAsB;AACrC,SAAO,KACJ,YAAY,EACZ,KAAK,EACL,QAAQ,aAAa,EAAE,EACvB,QAAQ,YAAY,GAAG,EACvB,QAAQ,YAAY,EAAE;AAC3B;AAKO,SAAS,uBAAuB,UAAyC;AAC9E,MAAI,CAAC,SAAU,QAAO,CAAC;AAEvB,QAAM,MAA6B,CAAC;AACpC,QAAM,QAAQ,SAAS,MAAM,IAAI;AAEjC,aAAW,QAAQ,OAAO;AAExB,UAAM,eAAe,KAAK,MAAM,mBAAmB;AACnD,QAAI,cAAc;AAChB,YAAM,QAAQ,aAAa,CAAC,EAAE;AAC9B,YAAM,QAAQ,aAAa,CAAC,EAAE,KAAK;AAGnC,UAAI,SAAS,KAAK,SAAS,GAAG;AAC5B,cAAM,KAAK,QAAQ,KAAK;AACxB,YAAI,KAAK;AAAA,UACP;AAAA,UACA;AAAA,UACA;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * List of countries with their respective country code, flag, phone code, and phone mask.
3
+ */
4
+ declare const countries: ({
5
+ name: string;
6
+ code: string;
7
+ flag: string;
8
+ phoneCode: string;
9
+ phoneMask: string;
10
+ } | {
11
+ name: string;
12
+ code: string;
13
+ flag: string;
14
+ phoneCode: string;
15
+ phoneMask?: undefined;
16
+ })[];
17
+
18
+ type Country = (typeof countries)[0];
19
+ /** Get national-format mask from country by stripping the country code prefix (e.g. "+1 (###) ###-####" → "(###) ###-####"). */
20
+ declare function getNationalMask(country: Country | undefined): string;
21
+ /** Format a raw digit string into a mask pattern where '#' represents one digit. No trailing literals so backspace works naturally. */
22
+ declare function formatDigitsToMask(digits: string, mask: string): string;
23
+
24
+ export { formatDigitsToMask, getNationalMask };
@@ -0,0 +1,26 @@
1
+ // src/utils/phone-helpers.ts
2
+ function getNationalMask(country) {
3
+ if (!(country == null ? void 0 : country.phoneMask)) return "";
4
+ const code = country.phoneCode.startsWith("+") ? country.phoneCode : `+${country.phoneCode}`;
5
+ const escaped = code.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6
+ return country.phoneMask.replace(new RegExp(`^\\s*${escaped}[\\s-]*`), "").trim();
7
+ }
8
+ function formatDigitsToMask(digits, mask) {
9
+ if (digits.length === 0) return "";
10
+ let i = 0;
11
+ let out = "";
12
+ for (const c of mask) {
13
+ if (c === "#") {
14
+ if (i < digits.length) out += digits[i++];
15
+ else break;
16
+ } else if (i < digits.length) {
17
+ out += c;
18
+ }
19
+ }
20
+ return out;
21
+ }
22
+ export {
23
+ formatDigitsToMask,
24
+ getNationalMask
25
+ };
26
+ //# sourceMappingURL=phone-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/phone-helpers.ts"],"sourcesContent":["import type countries from './countries';\n\ntype Country = (typeof countries)[0];\n\n/** Get national-format mask from country by stripping the country code prefix (e.g. \"+1 (###) ###-####\" → \"(###) ###-####\"). */\nexport function getNationalMask(country: Country | undefined): string {\n if (!country?.phoneMask) return '';\n const code = country.phoneCode.startsWith('+') ? country.phoneCode : `+${country.phoneCode}`;\n const escaped = code.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n return country.phoneMask.replace(new RegExp(`^\\\\s*${escaped}[\\\\s-]*`), '').trim();\n}\n\n/** Format a raw digit string into a mask pattern where '#' represents one digit. No trailing literals so backspace works naturally. */\nexport function formatDigitsToMask(digits: string, mask: string): string {\n if (digits.length === 0) return '';\n let i = 0;\n let out = '';\n for (const c of mask) {\n if (c === '#') {\n if (i < digits.length) out += digits[i++];\n else break;\n } else if (i < digits.length) {\n out += c;\n }\n }\n return out;\n}\n"],"mappings":";AAKO,SAAS,gBAAgB,SAAsC;AACpE,MAAI,EAAC,mCAAS,WAAW,QAAO;AAChC,QAAM,OAAO,QAAQ,UAAU,WAAW,GAAG,IAAI,QAAQ,YAAY,IAAI,QAAQ,SAAS;AAC1F,QAAM,UAAU,KAAK,QAAQ,uBAAuB,MAAM;AAC1D,SAAO,QAAQ,UAAU,QAAQ,IAAI,OAAO,QAAQ,OAAO,SAAS,GAAG,EAAE,EAAE,KAAK;AAClF;AAGO,SAAS,mBAAmB,QAAgB,MAAsB;AACvE,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,MAAI,IAAI;AACR,MAAI,MAAM;AACV,aAAW,KAAK,MAAM;AACpB,QAAI,MAAM,KAAK;AACb,UAAI,IAAI,OAAO,OAAQ,QAAO,OAAO,GAAG;AAAA,UACnC;AAAA,IACP,WAAW,IAAI,OAAO,QAAQ;AAC5B,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1,38 @@
1
+ import { P as PhotoAttachment } from '../photos-CmBdWiuZ.js';
2
+ import { W as WebsitePhotos } from '../website-photos-Cl1YqAno.js';
3
+
4
+ /**
5
+ * Helper functions for extracting photo URLs from photo associations
6
+ */
7
+
8
+ /**
9
+ * True if the URL looks like a video by path extension. Used to avoid using video URLs in img src.
10
+ */
11
+ declare function isVideoUrl(url: string | null | undefined): boolean;
12
+ /**
13
+ * Get the best available photo URL from a photos array
14
+ * Priority: featured photo > first photo > fallback
15
+ */
16
+ declare function getPhotoUrl(photos?: PhotoAttachment[]): string | null;
17
+ /**
18
+ * Get avatar URL for team members or authors.
19
+ * Returns a URL only when there is a real photo in attachments; otherwise null.
20
+ * Callers should use PhotoWithFallback (gradient fallback) or Avatar with initials when null.
21
+ */
22
+ /** Optional fallbackId/name reserved for future use (e.g. deterministic fallback). */
23
+ declare function getAvatarUrl(photos?: PhotoAttachment[], _fallbackId?: number | string, _name?: string): string | null;
24
+ /**
25
+ * Get featured image URL for blog posts
26
+ */
27
+ declare function getFeaturedImageUrl(photos?: PhotoAttachment[]): string | null;
28
+ /**
29
+ * Get logo URL from website_photos API (which aggregates from account_photos)
30
+ *
31
+ * The website_photos API endpoint returns logos from account_photos with photo_type: 'logo',
32
+ * with industry fallback. This is the primary and only source for logos.
33
+ *
34
+ * Returns undefined if no logo is available (for PhotoWithFallback gradient fallback)
35
+ */
36
+ declare function getLogoUrl(websitePhotos?: WebsitePhotos | null): string | undefined;
37
+
38
+ export { PhotoAttachment, getAvatarUrl, getFeaturedImageUrl, getLogoUrl, getPhotoUrl, isVideoUrl };
@@ -0,0 +1,41 @@
1
+ // src/utils/photo-helpers.ts
2
+ var VIDEO_EXTENSIONS = [".mp4", ".mov", ".webm", ".m4v", ".avi", ".mkv", ".flv", ".wmv"];
3
+ function isVideoUrl(url) {
4
+ var _a;
5
+ if (!url || typeof url !== "string") return false;
6
+ const path = (_a = url.split("?")[0]) != null ? _a : "";
7
+ const ext = path.slice(path.lastIndexOf(".")).toLowerCase();
8
+ return VIDEO_EXTENSIONS.includes(ext);
9
+ }
10
+ function getPhotoUrl(photos) {
11
+ if (photos && photos.length > 0) {
12
+ const featuredPhoto = photos.find((pa) => pa.featured);
13
+ const photoToUse = featuredPhoto || photos[0];
14
+ const photo = photoToUse.photo;
15
+ if (photo) {
16
+ return photo.large_url || photo.medium_url || photo.thumbnail_url || photo.original_url || null;
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+ function getAvatarUrl(photos, _fallbackId, _name) {
22
+ return getPhotoUrl(photos);
23
+ }
24
+ function getFeaturedImageUrl(photos) {
25
+ return getPhotoUrl(photos);
26
+ }
27
+ function getLogoUrl(websitePhotos) {
28
+ var _a;
29
+ if ((_a = websitePhotos == null ? void 0 : websitePhotos.logo) == null ? void 0 : _a.url) {
30
+ return websitePhotos.logo.url;
31
+ }
32
+ return void 0;
33
+ }
34
+ export {
35
+ getAvatarUrl,
36
+ getFeaturedImageUrl,
37
+ getLogoUrl,
38
+ getPhotoUrl,
39
+ isVideoUrl
40
+ };
41
+ //# sourceMappingURL=photo-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/photo-helpers.ts"],"sourcesContent":["/**\n * Helper functions for extracting photo URLs from photo associations\n */\n\nimport type { PhotoAttachment } from '../types/api/photos';\nimport type { WebsitePhotos } from '../types/api/website-photos';\n\nexport type { PhotoAttachment } from '../types/api/photos';\n\n/** Video file extensions treated as video (match backend ExternalPhotoService.video_url?) */\nconst VIDEO_EXTENSIONS = ['.mp4', '.mov', '.webm', '.m4v', '.avi', '.mkv', '.flv', '.wmv'];\n\n/**\n * True if the URL looks like a video by path extension. Used to avoid using video URLs in img src.\n */\nexport function isVideoUrl(url: string | null | undefined): boolean {\n if (!url || typeof url !== 'string') return false;\n const path = url.split('?')[0] ?? '';\n const ext = path.slice(path.lastIndexOf('.')).toLowerCase();\n return VIDEO_EXTENSIONS.includes(ext);\n}\n\n/**\n * Get the best available photo URL from a photos array\n * Priority: featured photo > first photo > fallback\n */\nexport function getPhotoUrl(\n photos?: PhotoAttachment[]\n): string | null {\n // Priority 1: Featured photo from photos array\n if (photos && photos.length > 0) {\n const featuredPhoto = photos.find(pa => pa.featured);\n const photoToUse = featuredPhoto || photos[0];\n const photo = photoToUse.photo;\n \n if (photo) {\n return photo.large_url || photo.medium_url || photo.thumbnail_url || photo.original_url || null;\n }\n }\n \n // Fallback (gradient or null)\n return null;\n}\n\n/**\n * Get avatar URL for team members or authors.\n * Returns a URL only when there is a real photo in attachments; otherwise null.\n * Callers should use PhotoWithFallback (gradient fallback) or Avatar with initials when null.\n */\n/** Optional fallbackId/name reserved for future use (e.g. deterministic fallback). */\n/* eslint-disable @typescript-eslint/no-unused-vars -- _fallbackId, _name reserved for API */\nexport function getAvatarUrl(\n photos?: PhotoAttachment[],\n _fallbackId?: number | string,\n _name?: string\n): string | null {\n return getPhotoUrl(photos);\n}\n/* eslint-enable @typescript-eslint/no-unused-vars */\n\n/**\n * Get featured image URL for blog posts\n */\nexport function getFeaturedImageUrl(\n photos?: PhotoAttachment[]\n): string | null {\n return getPhotoUrl(photos);\n}\n\n/**\n * Get logo URL from website_photos API (which aggregates from account_photos)\n * \n * The website_photos API endpoint returns logos from account_photos with photo_type: 'logo',\n * with industry fallback. This is the primary and only source for logos.\n * \n * Returns undefined if no logo is available (for PhotoWithFallback gradient fallback)\n */\nexport function getLogoUrl(\n websitePhotos?: WebsitePhotos | null\n): string | undefined {\n if (websitePhotos?.logo?.url) {\n return websitePhotos.logo.url;\n }\n \n return undefined;\n}\n\n"],"mappings":";AAUA,IAAM,mBAAmB,CAAC,QAAQ,QAAQ,SAAS,QAAQ,QAAQ,QAAQ,QAAQ,MAAM;AAKlF,SAAS,WAAW,KAAyC;AAfpE;AAgBE,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,QAAO,SAAI,MAAM,GAAG,EAAE,CAAC,MAAhB,YAAqB;AAClC,QAAM,MAAM,KAAK,MAAM,KAAK,YAAY,GAAG,CAAC,EAAE,YAAY;AAC1D,SAAO,iBAAiB,SAAS,GAAG;AACtC;AAMO,SAAS,YACd,QACe;AAEf,MAAI,UAAU,OAAO,SAAS,GAAG;AAC/B,UAAM,gBAAgB,OAAO,KAAK,QAAM,GAAG,QAAQ;AACnD,UAAM,aAAa,iBAAiB,OAAO,CAAC;AAC5C,UAAM,QAAQ,WAAW;AAEzB,QAAI,OAAO;AACT,aAAO,MAAM,aAAa,MAAM,cAAc,MAAM,iBAAiB,MAAM,gBAAgB;AAAA,IAC7F;AAAA,EACF;AAGA,SAAO;AACT;AASO,SAAS,aACd,QACA,aACA,OACe;AACf,SAAO,YAAY,MAAM;AAC3B;AAMO,SAAS,oBACd,QACe;AACf,SAAO,YAAY,MAAM;AAC3B;AAUO,SAAS,WACd,eACoB;AA/EtB;AAgFE,OAAI,oDAAe,SAAf,mBAAqB,KAAK;AAC5B,WAAO,cAAc,KAAK;AAAA,EAC5B;AAEA,SAAO;AACT;","names":[]}
@@ -0,0 +1,21 @@
1
+ interface WebsitePhoto {
2
+ id: number;
3
+ url: string;
4
+ thumbnail_url?: string;
5
+ medium_url?: string;
6
+ alt: string;
7
+ source: 'account' | 'industry';
8
+ }
9
+ interface WebsitePhotos {
10
+ logo?: WebsitePhoto | null;
11
+ favicon?: WebsitePhoto | null;
12
+ hero?: WebsitePhoto | null;
13
+ contact?: WebsitePhoto | null;
14
+ about?: WebsitePhoto | null;
15
+ careers?: WebsitePhoto | null;
16
+ preview_image?: WebsitePhoto | null;
17
+ stock_photos?: WebsitePhoto[];
18
+ }
19
+ type WebsitePhotosResponse = WebsitePhotos;
20
+
21
+ export type { WebsitePhotos as W, WebsitePhoto as a, WebsitePhotosResponse as b };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keystone-design-bootstrap",
3
- "version": "1.0.102",
3
+ "version": "1.0.104",
4
4
  "description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -8,8 +8,10 @@ export const CONSUMER_TOKEN_COOKIE = 'ks_consumer_token';
8
8
  import type { Consumer, ConversationSummary, Message, ContactSummary } from '../types/api/consumer';
9
9
  export type { Consumer, ConversationSummary, Message, ContactSummary };
10
10
 
11
+ // AUTH_API_URL (when set) points consumer auth/session endpoints at a dedicated
12
+ // auth service (e.g. Heimdal); everything else keeps using API_URL.
11
13
  function getApiUrl(): string {
12
- return process.env.API_URL || 'http://localhost:3000/api/v1';
14
+ return process.env.AUTH_API_URL || process.env.API_URL || 'http://localhost:3000/api/v1';
13
15
  }
14
16
 
15
17
  async function consumerFetch<T>(path: string, token: string): Promise<T | null> {
@@ -11,9 +11,9 @@
11
11
  *
12
12
  * Env (server-side only):
13
13
  * - API_URL (default: http://localhost:3000/api/v1)
14
+ * - AUTH_API_URL (optional) — overrides API_URL for consumer auth/session endpoints.
15
+ * Set this when auth lives on a separate service (e.g. Heimdal) from the data API.
14
16
  * - API_KEY
15
- * - CONSUMER_AUTH_URL (optional; overrides passwordless routes base, e.g.
16
- * https://www.keystone.app/api/consumer)
17
17
  */
18
18
 
19
19
  import { CONSUMER_TOKEN_COOKIE } from '../../lib/consumer-session';
@@ -25,19 +25,13 @@ type NextResponseLike = { json: (body: unknown, init?: ResponseInit) => any };
25
25
  const COOKIE_MAX_AGE = 60 * 60 * 24 * 30; // 30 days
26
26
 
27
27
  function getApiUrl(): string {
28
- return process.env.API_URL || 'http://localhost:3000/api/v1';
28
+ return process.env.AUTH_API_URL || process.env.API_URL || 'http://localhost:3000/api/v1';
29
29
  }
30
30
 
31
31
  function getApiKey(): string {
32
32
  return process.env.API_KEY || '';
33
33
  }
34
34
 
35
- function getPasswordlessBaseUrl(): string {
36
- const override = (process.env.CONSUMER_AUTH_URL || '').trim();
37
- if (override) return override.replace(/\/+$/, '');
38
- return `${getApiUrl().replace(/\/+$/, '')}/consumer/auth`;
39
- }
40
-
41
35
  function apiHeaders(request?: Request): Record<string, string> {
42
36
  const key = getApiKey();
43
37
  return {
@@ -47,8 +41,22 @@ function apiHeaders(request?: Request): Record<string, string> {
47
41
  };
48
42
  }
49
43
 
50
- function responseData(json: Record<string, unknown>): Record<string, unknown> {
51
- return (json.data as Record<string, unknown>) || json;
44
+ function asString(value: unknown): string | undefined {
45
+ return typeof value === 'string' && value ? value : undefined;
46
+ }
47
+
48
+ // Rails returns errors at the top level ({ error, code }); FastAPI backends
49
+ // (Heimdal) nest them under `detail`, where `error` is either a string or a
50
+ // { code, message } object for validation failures. Read all shapes, but only
51
+ // ever surface strings.
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ function upstreamError(json: any): { error?: string; code?: string; retry_in_seconds?: number } {
54
+ const err = json.error ?? json.detail?.error;
55
+ return {
56
+ error: asString(err) || asString(err?.message),
57
+ code: asString(json.code) || asString(json.detail?.code) || asString(err?.code),
58
+ retry_in_seconds: json.retry_in_seconds ?? json.detail?.retry_in_seconds,
59
+ };
52
60
  }
53
61
 
54
62
  // POST /api/consumer/initiate
@@ -72,8 +80,8 @@ async function handleInitiate(request: Request, NR: NextResponseLike): Promise<R
72
80
  if (res.status === 404) return NR.json({ exists: false });
73
81
  if (!res.ok) return NR.json({ exists: null });
74
82
 
75
- const json = await res.json().catch(() => ({})) as Record<string, unknown>;
76
- const data = responseData(json);
83
+ const json = await res.json().catch(() => ({}));
84
+ const data = json.data as Record<string, unknown> | undefined;
77
85
  return NR.json({
78
86
  exists: true,
79
87
  firstName: data?.first_name ?? undefined,
@@ -102,8 +110,9 @@ async function handleLogin(request: Request, NR: NextResponseLike): Promise<Resp
102
110
 
103
111
  const json = await res.json().catch(() => ({}));
104
112
  if (!res.ok) {
113
+ const err = upstreamError(json);
105
114
  return NR.json(
106
- { error: json.error || 'Login failed. Please try again.', code: json.code },
115
+ { error: err.error || 'Login failed. Please try again.', code: err.code },
107
116
  { status: res.status }
108
117
  );
109
118
  }
@@ -146,7 +155,10 @@ async function handleSignup(request: Request, NR: NextResponseLike): Promise<Res
146
155
 
147
156
  const json = await res.json().catch(() => ({}));
148
157
  if (!res.ok) {
149
- return NR.json({ error: json.error || 'Signup failed. Please try again.' }, { status: res.status });
158
+ return NR.json(
159
+ { error: upstreamError(json).error || 'Signup failed. Please try again.' },
160
+ { status: res.status }
161
+ );
150
162
  }
151
163
 
152
164
  const token = json.data?.token;
@@ -170,27 +182,27 @@ async function handleSendCode(request: Request, NR: NextResponseLike): Promise<R
170
182
  const { phone } = body;
171
183
  if (!phone) return NR.json({ error: 'Phone is required.' }, { status: 422 });
172
184
 
173
- const res = await fetch(`${getPasswordlessBaseUrl()}/send_code`, {
185
+ const res = await fetch(`${getApiUrl()}/consumer/auth/send_code`, {
174
186
  method: 'POST',
175
187
  headers: apiHeaders(request),
176
188
  body: JSON.stringify({ phone }),
177
189
  });
178
190
 
179
- const json = await res.json().catch(() => ({})) as Record<string, unknown>;
180
- const data = responseData(json);
191
+ const json = await res.json().catch(() => ({}));
181
192
  if (!res.ok) {
193
+ const err = upstreamError(json);
182
194
  return NR.json(
183
195
  {
184
- error: (json as Record<string, unknown>).error || 'Failed to send verification code. Please try again.',
185
- code: (json as Record<string, unknown>).code,
186
- retry_in_seconds: (json as Record<string, unknown>).retry_in_seconds,
196
+ error: err.error || 'Failed to send verification code. Please try again.',
197
+ code: err.code,
198
+ retry_in_seconds: err.retry_in_seconds,
187
199
  },
188
200
  { status: res.status }
189
201
  );
190
202
  }
191
203
 
192
204
  return NR.json({
193
- resend_available_at: data?.resend_available_at ?? null,
205
+ resend_available_at: json.data?.resend_available_at ?? null,
194
206
  });
195
207
  }
196
208
 
@@ -201,29 +213,26 @@ async function handleVerifyCode(request: Request, NR: NextResponseLike): Promise
201
213
  const { phone, code } = body;
202
214
  if (!phone || !code) return NR.json({ error: 'Phone and code are required.' }, { status: 422 });
203
215
 
204
- const res = await fetch(`${getPasswordlessBaseUrl()}/verify_code`, {
216
+ const res = await fetch(`${getApiUrl()}/consumer/auth/verify_code`, {
205
217
  method: 'POST',
206
218
  headers: apiHeaders(request),
207
219
  body: JSON.stringify({ phone, code }),
208
220
  });
209
221
 
210
- const json = await res.json().catch(() => ({})) as Record<string, unknown>;
211
- const data = responseData(json);
222
+ const json = await res.json().catch(() => ({}));
212
223
  if (!res.ok) {
224
+ const err = upstreamError(json);
213
225
  return NR.json(
214
- {
215
- error: (json as Record<string, unknown>).error || 'Verification failed. Please try again.',
216
- code: (json as Record<string, unknown>).code,
217
- },
226
+ { error: err.error || 'Verification failed. Please try again.', code: err.code },
218
227
  { status: res.status }
219
228
  );
220
229
  }
221
230
 
222
231
  return NR.json({
223
- verification_token: data?.verification_token,
224
- first_name: data?.first_name ?? '',
225
- last_name: data?.last_name ?? '',
226
- email: data?.email ?? '',
232
+ verification_token: json.data?.verification_token,
233
+ first_name: json.data?.first_name ?? '',
234
+ last_name: json.data?.last_name ?? '',
235
+ email: json.data?.email ?? '',
227
236
  });
228
237
  }
229
238
 
@@ -239,7 +248,7 @@ async function handlePasswordlessAuth(request: Request, NR: NextResponseLike): P
239
248
  return NR.json({ error: 'First name, last name, and email are required.' }, { status: 422 });
240
249
  }
241
250
 
242
- const res = await fetch(`${getPasswordlessBaseUrl()}/passwordless_auth`, {
251
+ const res = await fetch(`${getApiUrl()}/consumer/auth/passwordless_auth`, {
243
252
  method: 'POST',
244
253
  headers: apiHeaders(request),
245
254
  body: JSON.stringify({
@@ -251,21 +260,20 @@ async function handlePasswordlessAuth(request: Request, NR: NextResponseLike): P
251
260
  }),
252
261
  });
253
262
 
254
- const json = await res.json().catch(() => ({})) as Record<string, unknown>;
255
- const data = responseData(json);
263
+ const json = await res.json().catch(() => ({}));
256
264
  if (!res.ok) {
257
265
  return NR.json(
258
- { error: (json as Record<string, unknown>).error || 'Authentication failed. Please try again.' },
266
+ { error: upstreamError(json).error || 'Authentication failed. Please try again.' },
259
267
  { status: res.status }
260
268
  );
261
269
  }
262
270
 
263
- const token = data?.token;
271
+ const token = json.data?.token;
264
272
  if (!token) return NR.json({ error: 'No token received.' }, { status: 500 });
265
273
 
266
274
  // Surface the server-side Lead event_id so the browser can fire a single deduped
267
275
  // Lead (fbq track with { eventID }) that Meta matches against the CAPI Lead event.
268
- const response = NR.json({ eventId: data?.event_id ?? undefined });
276
+ const response = NR.json({ eventId: json.data?.event_id ?? undefined });
269
277
  response.cookies.set(CONSUMER_TOKEN_COOKIE, token, {
270
278
  httpOnly: true,
271
279
  secure: process.env.NODE_ENV === 'production',