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.
- package/README.md +4 -4
- package/dist/blog-post-vWzW8yFb.d.ts +50 -0
- package/dist/contexts/index.d.ts +13 -0
- package/dist/contexts/index.js +173 -0
- package/dist/contexts/index.js.map +1 -0
- package/dist/design_system/components/DynamicFormFields.d.ts +15 -0
- package/dist/design_system/components/DynamicFormFields.js +5176 -0
- package/dist/design_system/components/DynamicFormFields.js.map +1 -0
- package/dist/design_system/elements/index.d.ts +383 -0
- package/dist/design_system/elements/index.js +4007 -0
- package/dist/design_system/elements/index.js.map +1 -0
- package/dist/design_system/logo/keystone-logo.d.ts +6 -0
- package/dist/design_system/logo/keystone-logo.js +145 -0
- package/dist/design_system/logo/keystone-logo.js.map +1 -0
- package/dist/design_system/sections/index.d.ts +233 -0
- package/dist/design_system/sections/index.js +20048 -0
- package/dist/design_system/sections/index.js.map +1 -0
- package/dist/form-C94A_PX_.d.ts +36 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +20608 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/component-registry.d.ts +13 -0
- package/dist/lib/component-registry.js +36 -0
- package/dist/lib/component-registry.js.map +1 -0
- package/dist/lib/hooks/index.d.ts +83 -0
- package/dist/lib/hooks/index.js +182 -0
- package/dist/lib/hooks/index.js.map +1 -0
- package/dist/lib/server-api.d.ts +67 -0
- package/dist/lib/server-api.js +195 -0
- package/dist/lib/server-api.js.map +1 -0
- package/dist/package-DeHKpQp7.d.ts +121 -0
- package/dist/photos-CmBdWiuZ.d.ts +27 -0
- package/dist/themes/index.d.ts +17 -0
- package/dist/themes/index.js +29 -0
- package/dist/themes/index.js.map +1 -0
- package/dist/tracking/index.d.ts +254 -0
- package/dist/tracking/index.js +587 -0
- package/dist/tracking/index.js.map +1 -0
- package/dist/types/index.d.ts +313 -0
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/cx.d.ts +15 -0
- package/dist/utils/cx.js +18 -0
- package/dist/utils/cx.js.map +1 -0
- package/dist/utils/gradient-placeholder.d.ts +7 -0
- package/dist/utils/gradient-placeholder.js +59 -0
- package/dist/utils/gradient-placeholder.js.map +1 -0
- package/dist/utils/is-react-component.d.ts +21 -0
- package/dist/utils/is-react-component.js +20 -0
- package/dist/utils/is-react-component.js.map +1 -0
- package/dist/utils/markdown-toc.d.ts +14 -0
- package/dist/utils/markdown-toc.js +29 -0
- package/dist/utils/markdown-toc.js.map +1 -0
- package/dist/utils/phone-helpers.d.ts +24 -0
- package/dist/utils/phone-helpers.js +26 -0
- package/dist/utils/phone-helpers.js.map +1 -0
- package/dist/utils/photo-helpers.d.ts +38 -0
- package/dist/utils/photo-helpers.js +41 -0
- package/dist/utils/photo-helpers.js.map +1 -0
- package/dist/website-photos-Cl1YqAno.d.ts +21 -0
- package/package.json +1 -1
- package/src/lib/consumer-session.ts +3 -1
- 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
|
@@ -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
|
|
51
|
-
return
|
|
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(() => ({}))
|
|
76
|
-
const data =
|
|
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:
|
|
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(
|
|
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(`${
|
|
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(() => ({}))
|
|
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:
|
|
185
|
-
code:
|
|
186
|
-
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(`${
|
|
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(() => ({}))
|
|
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(`${
|
|
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(() => ({}))
|
|
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
|
|
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',
|