@payez/next-mvp 4.13.0 → 4.14.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 (74) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/api-handlers/account/profile.d.ts +2 -39
  3. package/dist/api-handlers/account/profile.js +5 -2
  4. package/dist/api-handlers/anon/preferences.d.ts +1 -14
  5. package/dist/api-handlers/anon/preferences.js +5 -5
  6. package/dist/api-handlers/auth/credential-login.js +46 -0
  7. package/dist/api-handlers/health.d.ts +5 -0
  8. package/dist/api-handlers/health.js +48 -0
  9. package/dist/api-handlers/mfa/first-enrol.d.ts +5 -0
  10. package/dist/api-handlers/mfa/first-enrol.js +58 -0
  11. package/dist/api-handlers/session/refresh-viability.d.ts +3 -15
  12. package/dist/api-handlers/session/refresh-viability.js +5 -2
  13. package/dist/api-handlers/session/viability.d.ts +3 -8
  14. package/dist/api-handlers/session/viability.js +5 -2
  15. package/dist/auth/better-auth-exposure.d.ts +11 -0
  16. package/dist/auth/better-auth-exposure.js +73 -0
  17. package/dist/auth/better-auth.js +13 -1
  18. package/dist/auth/route-config.js +9 -0
  19. package/dist/components/auth/CredentialSignInForm.d.ts +6 -1
  20. package/dist/components/auth/CredentialSignInForm.js +15 -1
  21. package/dist/components/mfa/FirstEnrolPanel.d.ts +21 -0
  22. package/dist/components/mfa/FirstEnrolPanel.js +115 -0
  23. package/dist/lib/credential-sign-in-client.d.ts +4 -0
  24. package/dist/lib/credential-sign-in-client.js +13 -1
  25. package/dist/lib/first-enrol.d.ts +66 -0
  26. package/dist/lib/first-enrol.js +188 -0
  27. package/dist/lib/no-store.d.ts +13 -0
  28. package/dist/lib/no-store.js +35 -0
  29. package/dist/pages/first-enroll/page.d.ts +7 -0
  30. package/dist/pages/first-enroll/page.js +15 -0
  31. package/dist/routes/auth/session.d.ts +2 -9
  32. package/dist/routes/auth/session.js +5 -2
  33. package/dist/routes/auth/viability.d.ts +3 -54
  34. package/dist/routes/auth/viability.js +5 -2
  35. package/dist/routes/health.d.ts +1 -0
  36. package/dist/routes/health.js +16 -0
  37. package/dist/routes/mfa/first-enrol-confirm.d.ts +10 -0
  38. package/dist/routes/mfa/first-enrol-confirm.js +14 -0
  39. package/dist/routes/mfa/first-enrol-enroll.d.ts +10 -0
  40. package/dist/routes/mfa/first-enrol-enroll.js +14 -0
  41. package/dist/routes/mfa/methods.d.ts +2 -2
  42. package/dist/routes/mfa/methods.js +3 -1
  43. package/dist/routes/passkeys/list.d.ts +2 -10
  44. package/dist/routes/passkeys/list.js +4 -2
  45. package/dist/routes/session/refresh-viability.d.ts +2 -1
  46. package/dist/routes/session/refresh-viability.js +4 -2
  47. package/dist/utils/api.d.ts +33 -6
  48. package/dist/utils/api.js +35 -2
  49. package/package.json +26 -1
  50. package/src/api-handlers/account/profile.ts +5 -1
  51. package/src/api-handlers/anon/preferences.ts +5 -4
  52. package/src/api-handlers/auth/credential-login.ts +49 -0
  53. package/src/api-handlers/health.ts +53 -0
  54. package/src/api-handlers/mfa/first-enrol.ts +55 -0
  55. package/src/api-handlers/session/refresh-viability.ts +5 -1
  56. package/src/api-handlers/session/viability.ts +6 -2
  57. package/src/auth/better-auth-exposure.ts +69 -0
  58. package/src/auth/better-auth.ts +15 -1
  59. package/src/auth/route-config.ts +9 -0
  60. package/src/components/auth/CredentialSignInForm.tsx +18 -1
  61. package/src/components/mfa/FirstEnrolPanel.tsx +189 -0
  62. package/src/lib/credential-sign-in-client.ts +13 -1
  63. package/src/lib/first-enrol.ts +180 -0
  64. package/src/lib/no-store.ts +30 -0
  65. package/src/pages/first-enroll/page.tsx +19 -0
  66. package/src/routes/auth/session.ts +5 -1
  67. package/src/routes/auth/viability.ts +6 -2
  68. package/src/routes/health.ts +14 -0
  69. package/src/routes/mfa/first-enrol-confirm.ts +10 -0
  70. package/src/routes/mfa/first-enrol-enroll.ts +10 -0
  71. package/src/routes/mfa/methods.ts +3 -1
  72. package/src/routes/passkeys/list.ts +5 -1
  73. package/src/routes/session/refresh-viability.ts +5 -1
  74. package/src/utils/api.ts +50 -8
@@ -24,6 +24,7 @@ import {
24
24
  import { getIDPClientConfig } from '../../lib/idp-client-config';
25
25
  import { logExceptionWithCorrelation } from '../../lib/safe-error-response';
26
26
  import { tokenLogId } from '../../lib/token-log-id';
27
+ import { noStore } from '../../lib/no-store';
27
28
  import { getRevocationStatus, warnNonNumericUserId, resolveNumericIdpUserId, type RevocationStatus } from '../../vibe/sessions';
28
29
 
29
30
  /**
@@ -141,7 +142,7 @@ function resolveClientId(sessionClientId: string | undefined): string | null {
141
142
  * - needsRefresh: boolean - Whether a refresh is recommended
142
143
  * - expiresIn: number - Seconds until token expires
143
144
  */
144
- export async function GET(req: NextRequest) {
145
+ async function getHandler(req: NextRequest) {
145
146
  try {
146
147
  // PAY-2216: ask for the session's STATE, not a session-or-null. A null used to cover a store outage, a deleted
147
148
  // session and no cookie alike, so this route answered 'No session found' to all three and the middleware could
@@ -525,4 +526,7 @@ export async function GET(req: NextRequest) {
525
526
  correlationId
526
527
  }, { status: 500 });
527
528
  }
528
- }
529
+ }
530
+
531
+ /** PAY-2994: session state is per-user and some answers carry tokens; never cached (noStore). */
532
+ export const GET = noStore(getHandler);
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Ready-to-use /api/health route: reports Redis reachability (PAY-2804).
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * // app/api/health/route.ts
7
+ * export const runtime = 'nodejs';
8
+ * export { GET } from '@payez/next-mvp/routes/health';
9
+ * ```
10
+ */
11
+ import { createHealthHandler } from '../api-handlers/health';
12
+ import { getRedis } from '../lib/redis';
13
+
14
+ export const GET = createHealthHandler(() => getRedis());
@@ -0,0 +1,10 @@
1
+ /**
2
+ * AUTH-045 first-enrolment: confirm the first authenticator code, clear the enrolment cookie, return the recovery codes. Mints no session.
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * // app/api/auth/first-enrol/confirm/route.ts
7
+ * export { POST } from '@payez/next-mvp/routes/mfa/first-enrol-confirm';
8
+ * ```
9
+ */
10
+ export { POST_CONFIRM as POST } from '../../api-handlers/mfa/first-enrol';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * AUTH-045 first-enrolment: begin TOTP enrolment for a user the IdP told to enrol (uses ONLY the httpOnly enrolment cookie the credential door parked).
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * // app/api/auth/first-enrol/enroll/route.ts
7
+ * export { POST } from '@payez/next-mvp/routes/mfa/first-enrol-enroll';
8
+ * ```
9
+ */
10
+ export { POST_ENROLL as POST } from '../../api-handlers/mfa/first-enrol';
@@ -11,8 +11,10 @@
11
11
  */
12
12
  import { NextRequest } from 'next/server';
13
13
  import { GET as listMethods, DELETE as deleteMethod } from '../../api-handlers/mfa/methods';
14
+ import { noStore } from '../../lib/no-store';
14
15
 
15
- export const GET = listMethods;
16
+ /** PAY-2994: the user's MFA methods; never cached (noStore). */
17
+ export const GET = noStore(listMethods);
16
18
 
17
19
  type Ctx = { params: Promise<{ id: string }> };
18
20
 
@@ -7,4 +7,8 @@
7
7
  * export { GET } from '@payez/next-mvp/routes/passkeys/list';
8
8
  * ```
9
9
  */
10
- export { GET } from '../../api-handlers/passkeys/list';
10
+ import { GET as listPasskeys } from '../../api-handlers/passkeys/list';
11
+ import { noStore } from '../../lib/no-store';
12
+
13
+ /** PAY-2994: the signed-in user's passkeys; never cached (noStore). */
14
+ export const GET = noStore(listPasskeys);
@@ -14,4 +14,8 @@
14
14
  * @since auth-ready-v2
15
15
  */
16
16
 
17
- export { GET } from '../../api-handlers/session/refresh-viability';
17
+ import { GET as refreshViability } from '../../api-handlers/session/refresh-viability';
18
+ import { noStore } from '../../lib/no-store';
19
+
20
+ /** PAY-2994: refresh-token viability of this session; never cached (noStore). */
21
+ export const GET = noStore(refreshViability);
package/src/utils/api.ts CHANGED
@@ -133,6 +133,45 @@ export function resetFailureText(status: number, rules: string[] = [], policy =
133
133
  return "We couldn't reset your password just now. Please try again.";
134
134
  }
135
135
 
136
+ /**
137
+ * SMS-first sign-up (door 3b): when the phone code is right but the email is still unconfirmed, the server answers 200 success with
138
+ * `next_step = EMAIL_CONFIRMATION_REQUIRED` and NO reset token. `verifyOnboardingSmsCode` surfaces that as `nextStep`;
139
+ * `interpretSmsVerification` turns a result into ONE outcome, testing `nextStep` BEFORE `success`, so a consumer that uses it can
140
+ * never reach "verified" without a reset token or with an unfinished step. The sentences are the package's own, never the server's text.
141
+ */
142
+ export const SMS_NEXT_STEP_EMAIL_CONFIRMATION_REQUIRED = 'EMAIL_CONFIRMATION_REQUIRED';
143
+ export const SMS_EMAIL_CONFIRMATION_TEXT = 'Your phone is verified. Confirm your email to finish signing up.';
144
+ export const SMS_MORE_STEPS_TEXT = 'One more step is needed to finish signing up.';
145
+
146
+ export interface SmsCodeVerification {
147
+ success: boolean;
148
+ resetToken: string;
149
+ email: string;
150
+ clientKey: string;
151
+ /** Present when the server says more steps remain (an UNRECOGNISED value is kept as a non-empty string, never dropped). */
152
+ nextStep?: string;
153
+ }
154
+
155
+ export type SmsVerificationOutcome =
156
+ | { kind: 'verified'; resetToken: string; email: string; clientKey: string }
157
+ | { kind: 'email_confirmation_required'; message: string }
158
+ | { kind: 'more_steps'; message: string }
159
+ | { kind: 'not_verified' };
160
+
161
+ export function interpretSmsVerification(result: SmsCodeVerification): SmsVerificationOutcome {
162
+ // success:false WINS over any step (BAPert 88508): a failed answer must never say "Your phone is verified".
163
+ if (!result.success) return { kind: 'not_verified' };
164
+ // For every answer that is not a failure, nextStep comes BEFORE the token test: a step that is not finished is never "verified".
165
+ if (result.nextStep) {
166
+ return result.nextStep === SMS_NEXT_STEP_EMAIL_CONFIRMATION_REQUIRED
167
+ ? { kind: 'email_confirmation_required', message: SMS_EMAIL_CONFIRMATION_TEXT }
168
+ : { kind: 'more_steps', message: SMS_MORE_STEPS_TEXT };
169
+ }
170
+ // success with an EMPTY reset token is the shape that is dangerous (set-password would be called with ''): not verified.
171
+ if (!result.resetToken) return { kind: 'more_steps', message: SMS_MORE_STEPS_TEXT };
172
+ return { kind: 'verified', resetToken: result.resetToken, email: result.email, clientKey: result.clientKey };
173
+ }
174
+
136
175
  export class AccountApi {
137
176
  /**
138
177
  * Get masked user info (email, phone, authenticator status)
@@ -338,17 +377,12 @@ export class AccountApi {
338
377
  };
339
378
  }
340
379
 
341
- async verifyOnboardingSmsCode(onboardToken: string, code: string, clientKey?: string): Promise<{
342
- success: boolean;
343
- resetToken: string;
344
- email: string;
345
- clientKey: string;
346
- }> {
380
+ async verifyOnboardingSmsCode(onboardToken: string, code: string, clientKey?: string): Promise<SmsCodeVerification> {
347
381
  const qs = clientKey ? `?clientId=${encodeURIComponent(clientKey)}` : '';
348
382
  const res = await fetch(`/api/onboarding/verify-sms-code${qs}`, {
349
383
  method: 'POST',
350
384
  headers: { 'Content-Type': 'application/json' },
351
- body: JSON.stringify({
385
+ body: JSON.stringify({
352
386
  onboard_token: onboardToken,
353
387
  code: code
354
388
  })
@@ -360,11 +394,19 @@ export class AccountApi {
360
394
  }
361
395
  // Extract from nested data structure
362
396
  const data = result.data?.data || result.data || {};
397
+ // next_step (door 3b): read from the nested shapes AND a flat body. Absent / null / '' = no step; a non-string value is an
398
+ // UNRECOGNISED step and is kept as a non-empty string (never dropped, never mistaken for the known one). That includes false and 0:
399
+ // a server that means "no step" must send nothing or null (or '').
400
+ const rawStep = data.next_step ?? result.next_step;
401
+ const nextStep = rawStep === undefined || rawStep === null || rawStep === ''
402
+ ? undefined
403
+ : typeof rawStep === 'string' ? rawStep : 'UNRECOGNISED_NEXT_STEP';
363
404
  return {
364
405
  success: data.success !== false,
365
406
  resetToken: data.reset_token || '',
366
407
  email: data.email || '',
367
- clientKey: data.client_key || ''
408
+ clientKey: data.client_key || '',
409
+ ...(nextStep !== undefined ? { nextStep } : {})
368
410
  };
369
411
  }
370
412