keystone-design-bootstrap 1.0.101 → 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.101",
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;
@@ -47,6 +47,71 @@ function apiHeaders(request?: Request): Record<string, string> {
47
47
  };
48
48
  }
49
49
 
50
+ function responseData(json: Record<string, unknown>): Record<string, unknown> {
51
+ return (json.data as Record<string, unknown>) || json;
52
+ }
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
+
50
115
  // POST /api/consumer/initiate
51
116
  // Looks up an existing user by email or phone, creating a Contact if needed.
52
117
  // Returns { exists, firstName, hasPassword }.
@@ -68,8 +133,8 @@ async function handleInitiate(request: Request, NR: NextResponseLike): Promise<R
68
133
  if (res.status === 404) return NR.json({ exists: false });
69
134
  if (!res.ok) return NR.json({ exists: null });
70
135
 
71
- const json = await res.json().catch(() => ({}));
72
- const data = json.data as Record<string, unknown> | undefined;
136
+ const json = await res.json().catch(() => ({})) as Record<string, unknown>;
137
+ const data = responseData(json);
73
138
  return NR.json({
74
139
  exists: true,
75
140
  firstName: data?.first_name ?? undefined,
@@ -104,7 +169,8 @@ async function handleLogin(request: Request, NR: NextResponseLike): Promise<Resp
104
169
  );
105
170
  }
106
171
 
107
- const token = json.data?.token;
172
+ const data = responseData(json);
173
+ const token = extractAuthToken(data);
108
174
  if (!token) return NR.json({ error: 'No token received.' }, { status: 500 });
109
175
 
110
176
  const response = NR.json({});
@@ -140,15 +206,16 @@ async function handleSignup(request: Request, NR: NextResponseLike): Promise<Res
140
206
  }),
141
207
  });
142
208
 
143
- const json = await res.json().catch(() => ({}));
209
+ const json = await res.json().catch(() => ({})) as Record<string, unknown>;
144
210
  if (!res.ok) {
145
211
  return NR.json({ error: json.error || 'Signup failed. Please try again.' }, { status: res.status });
146
212
  }
147
213
 
148
- const token = json.data?.token;
214
+ const data = responseData(json);
215
+ const token = extractAuthToken(data);
149
216
  if (!token) return NR.json({ error: 'No token received.' }, { status: 500 });
150
217
 
151
- const response = NR.json({ claimed: json.data?.claimed ?? false });
218
+ const response = NR.json({ claimed: data?.claimed ?? false });
152
219
  response.cookies.set(CONSUMER_TOKEN_COOKIE, token, {
153
220
  httpOnly: true,
154
221
  secure: process.env.NODE_ENV === 'production',
@@ -172,20 +239,21 @@ async function handleSendCode(request: Request, NR: NextResponseLike): Promise<R
172
239
  body: JSON.stringify({ phone }),
173
240
  });
174
241
 
175
- const json = await res.json().catch(() => ({}));
242
+ const json = await res.json().catch(() => ({})) as Record<string, unknown>;
243
+ const data = responseData(json);
176
244
  if (!res.ok) {
177
245
  return NR.json(
178
246
  {
179
- error: json.error || 'Failed to send verification code. Please try again.',
180
- code: json.code,
181
- retry_in_seconds: json.retry_in_seconds,
247
+ error: (json as Record<string, unknown>).error || 'Failed to send verification code. Please try again.',
248
+ code: (json as Record<string, unknown>).code,
249
+ retry_in_seconds: (json as Record<string, unknown>).retry_in_seconds,
182
250
  },
183
251
  { status: res.status }
184
252
  );
185
253
  }
186
254
 
187
255
  return NR.json({
188
- resend_available_at: json.data?.resend_available_at ?? null,
256
+ resend_available_at: data?.resend_available_at ?? null,
189
257
  });
190
258
  }
191
259
 
@@ -202,19 +270,43 @@ async function handleVerifyCode(request: Request, NR: NextResponseLike): Promise
202
270
  body: JSON.stringify({ phone, code }),
203
271
  });
204
272
 
205
- const json = await res.json().catch(() => ({}));
273
+ const json = await res.json().catch(() => ({})) as Record<string, unknown>;
274
+ const data = responseData(json);
206
275
  if (!res.ok) {
207
276
  return NR.json(
208
- { error: json.error || 'Verification failed. Please try again.', code: json.code },
277
+ {
278
+ error: (json as Record<string, unknown>).error || 'Verification failed. Please try again.',
279
+ code: (json as Record<string, unknown>).code,
280
+ },
209
281
  { status: res.status }
210
282
  );
211
283
  }
212
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
+
213
305
  return NR.json({
214
- verification_token: json.data?.verification_token,
215
- first_name: json.data?.first_name ?? '',
216
- last_name: json.data?.last_name ?? '',
217
- email: json.data?.email ?? '',
306
+ verification_token: data?.verification_token,
307
+ first_name: data?.first_name ?? '',
308
+ last_name: data?.last_name ?? '',
309
+ email: data?.email ?? '',
218
310
  });
219
311
  }
220
312
 
@@ -242,17 +334,21 @@ async function handlePasswordlessAuth(request: Request, NR: NextResponseLike): P
242
334
  }),
243
335
  });
244
336
 
245
- const json = await res.json().catch(() => ({}));
337
+ const json = await res.json().catch(() => ({})) as Record<string, unknown>;
338
+ const data = responseData(json);
246
339
  if (!res.ok) {
247
- return NR.json({ error: json.error || 'Authentication failed. Please try again.' }, { status: res.status });
340
+ return NR.json(
341
+ { error: (json as Record<string, unknown>).error || 'Authentication failed. Please try again.' },
342
+ { status: res.status }
343
+ );
248
344
  }
249
345
 
250
- const token = json.data?.token;
346
+ const token = extractAuthToken(data);
251
347
  if (!token) return NR.json({ error: 'No token received.' }, { status: 500 });
252
348
 
253
349
  // Surface the server-side Lead event_id so the browser can fire a single deduped
254
350
  // Lead (fbq track with { eventID }) that Meta matches against the CAPI Lead event.
255
- const response = NR.json({ eventId: json.data?.event_id ?? undefined });
351
+ const response = NR.json({ eventId: data?.event_id ?? undefined });
256
352
  response.cookies.set(CONSUMER_TOKEN_COOKIE, token, {
257
353
  httpOnly: true,
258
354
  secure: process.env.NODE_ENV === 'production',