keystone-design-bootstrap 1.0.102 → 1.0.103

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keystone-design-bootstrap",
3
- "version": "1.0.102",
3
+ "version": "1.0.103",
4
4
  "description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -204,7 +204,30 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
204
204
  body: JSON.stringify({ phone: fullPhone, code }),
205
205
  });
206
206
  const result = await res.json().catch(() => ({}));
207
- if (!res.ok || !result.verification_token) {
207
+ if (!res.ok) {
208
+ setError(result.error || 'That code is invalid or expired.');
209
+ captureEvent('portal_login_failed', { step: 'signin', reason: result.code || 'verification_failed' });
210
+ return;
211
+ }
212
+
213
+ // Compatibility path: verify_code may directly authenticate and set the
214
+ // session cookie via the route handler (no passwordless_auth needed).
215
+ if (result.authenticated) {
216
+ await setPixelUserData({ email: result.email || undefined, phone: fullPhone });
217
+ firePixelEvent('Lead', undefined, result?.eventId);
218
+ captureEvent('portal_login_step_advanced', {
219
+ from_step: 'signin',
220
+ to_step: 'authenticated',
221
+ reason: 'verification_direct_auth',
222
+ });
223
+ captureEvent('portal_login_completed', { flow: 'signin' });
224
+ setResendAvailableAt(null);
225
+ setSecondsUntilResend(0);
226
+ if (onSuccess) onSuccess(); else router.refresh();
227
+ return;
228
+ }
229
+
230
+ if (!result.verification_token) {
208
231
  setError(result.error || 'That code is invalid or expired.');
209
232
  captureEvent('portal_login_failed', { step: 'signin', reason: result.code || 'verification_failed' });
210
233
  return;
@@ -51,6 +51,67 @@ function responseData(json: Record<string, unknown>): Record<string, unknown> {
51
51
  return (json.data as Record<string, unknown>) || json;
52
52
  }
53
53
 
54
+ function readStringField(obj: Record<string, unknown>, key: string): string | null {
55
+ const value = obj[key];
56
+ return typeof value === 'string' && value.length > 0 ? value : null;
57
+ }
58
+
59
+ function readNestedStringField(
60
+ obj: Record<string, unknown>,
61
+ parentKey: string,
62
+ key: string
63
+ ): string | null {
64
+ const parent = obj[parentKey];
65
+ if (!parent || typeof parent !== 'object') return null;
66
+ const value = (parent as Record<string, unknown>)[key];
67
+ return typeof value === 'string' && value.length > 0 ? value : null;
68
+ }
69
+
70
+ function extractAuthToken(data: Record<string, unknown>): string | null {
71
+ const direct =
72
+ readStringField(data, 'token') ||
73
+ readStringField(data, 'auth_token') ||
74
+ readStringField(data, 'access_token') ||
75
+ readStringField(data, 'session_token') ||
76
+ readStringField(data, 'jwt') ||
77
+ readNestedStringField(data, 'consumer', 'token') ||
78
+ readNestedStringField(data, 'session', 'token');
79
+ if (direct) return direct;
80
+
81
+ // Compatibility fallback: some backends rename token fields without notice.
82
+ // Walk a few levels and accept any non-empty string on keys containing "token"/"jwt".
83
+ const queue: Array<{ value: unknown; depth: number }> = [{ value: data, depth: 0 }];
84
+ const visited = new Set<object>();
85
+ while (queue.length > 0) {
86
+ const next = queue.shift();
87
+ if (!next) break;
88
+ const { value, depth } = next;
89
+ if (!value || typeof value !== 'object') continue;
90
+ if (visited.has(value as object)) continue;
91
+ visited.add(value as object);
92
+
93
+ const entries = Object.entries(value as Record<string, unknown>);
94
+ for (const [key, candidate] of entries) {
95
+ // Never treat verification tokens as auth/session JWTs.
96
+ if (/verification[_-]?token/i.test(key)) {
97
+ if (depth < 4 && candidate && typeof candidate === 'object') {
98
+ queue.push({ value: candidate, depth: depth + 1 });
99
+ }
100
+ continue;
101
+ }
102
+ const isTokenLikeKey = /token|jwt/i.test(key);
103
+ if (isTokenLikeKey && typeof candidate === 'string' && candidate.length > 0) {
104
+ return candidate;
105
+ }
106
+ if (depth < 4 && candidate && typeof candidate === 'object') {
107
+ queue.push({ value: candidate, depth: depth + 1 });
108
+ }
109
+ }
110
+ }
111
+
112
+ return null;
113
+ }
114
+
54
115
  // POST /api/consumer/initiate
55
116
  // Looks up an existing user by email or phone, creating a Contact if needed.
56
117
  // Returns { exists, firstName, hasPassword }.
@@ -108,7 +169,8 @@ async function handleLogin(request: Request, NR: NextResponseLike): Promise<Resp
108
169
  );
109
170
  }
110
171
 
111
- const token = json.data?.token;
172
+ const data = responseData(json);
173
+ const token = extractAuthToken(data);
112
174
  if (!token) return NR.json({ error: 'No token received.' }, { status: 500 });
113
175
 
114
176
  const response = NR.json({});
@@ -144,15 +206,16 @@ async function handleSignup(request: Request, NR: NextResponseLike): Promise<Res
144
206
  }),
145
207
  });
146
208
 
147
- const json = await res.json().catch(() => ({}));
209
+ const json = await res.json().catch(() => ({})) as Record<string, unknown>;
148
210
  if (!res.ok) {
149
211
  return NR.json({ error: json.error || 'Signup failed. Please try again.' }, { status: res.status });
150
212
  }
151
213
 
152
- const token = json.data?.token;
214
+ const data = responseData(json);
215
+ const token = extractAuthToken(data);
153
216
  if (!token) return NR.json({ error: 'No token received.' }, { status: 500 });
154
217
 
155
- const response = NR.json({ claimed: json.data?.claimed ?? false });
218
+ const response = NR.json({ claimed: data?.claimed ?? false });
156
219
  response.cookies.set(CONSUMER_TOKEN_COOKIE, token, {
157
220
  httpOnly: true,
158
221
  secure: process.env.NODE_ENV === 'production',
@@ -219,6 +282,26 @@ async function handleVerifyCode(request: Request, NR: NextResponseLike): Promise
219
282
  );
220
283
  }
221
284
 
285
+ // Some surfaces return a direct session token on verify.
286
+ const directToken = extractAuthToken(data);
287
+ if (directToken) {
288
+ const response = NR.json({
289
+ authenticated: true,
290
+ first_name: data?.first_name ?? '',
291
+ last_name: data?.last_name ?? '',
292
+ email: data?.email ?? '',
293
+ eventId: data?.event_id ?? undefined,
294
+ });
295
+ response.cookies.set(CONSUMER_TOKEN_COOKIE, directToken, {
296
+ httpOnly: true,
297
+ secure: process.env.NODE_ENV === 'production',
298
+ sameSite: 'lax',
299
+ path: '/',
300
+ maxAge: COOKIE_MAX_AGE,
301
+ });
302
+ return response;
303
+ }
304
+
222
305
  return NR.json({
223
306
  verification_token: data?.verification_token,
224
307
  first_name: data?.first_name ?? '',
@@ -260,7 +343,7 @@ async function handlePasswordlessAuth(request: Request, NR: NextResponseLike): P
260
343
  );
261
344
  }
262
345
 
263
- const token = data?.token;
346
+ const token = extractAuthToken(data);
264
347
  if (!token) return NR.json({ error: 'No token received.' }, { status: 500 });
265
348
 
266
349
  // Surface the server-side Lead event_id so the browser can fire a single deduped