@payez/next-mvp 4.13.1 → 4.15.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 (58) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/dist/api-handlers/auth/credential-login.js +46 -0
  3. package/dist/api-handlers/mfa/first-enrol.d.ts +5 -0
  4. package/dist/api-handlers/mfa/first-enrol.js +58 -0
  5. package/dist/api-handlers/mfa/totp-confirm.d.ts +1 -1
  6. package/dist/api-handlers/mfa/totp-confirm.js +10 -6
  7. package/dist/api-handlers/test/force-expire.d.ts +4 -5
  8. package/dist/api-handlers/test/force-expire.js +14 -63
  9. package/dist/auth/better-auth.d.ts +16 -0
  10. package/dist/auth/better-auth.js +34 -0
  11. package/dist/components/auth/CredentialSignInForm.d.ts +6 -1
  12. package/dist/components/auth/CredentialSignInForm.js +15 -1
  13. package/dist/components/mfa/FirstEnrolPanel.d.ts +21 -0
  14. package/dist/components/mfa/FirstEnrolPanel.js +115 -0
  15. package/dist/lib/credential-sign-in-client.d.ts +4 -0
  16. package/dist/lib/credential-sign-in-client.js +13 -1
  17. package/dist/lib/first-enrol.d.ts +66 -0
  18. package/dist/lib/first-enrol.js +188 -0
  19. package/dist/lib/idp-client-config.js +1 -1
  20. package/dist/lib/log-safe.d.ts +6 -0
  21. package/dist/lib/log-safe.js +9 -0
  22. package/dist/lib/payez-client-secret.js +2 -1
  23. package/dist/lib/token-pages-switch.d.ts +23 -0
  24. package/dist/lib/token-pages-switch.js +48 -0
  25. package/dist/pages/first-enroll/page.d.ts +7 -0
  26. package/dist/pages/first-enroll/page.js +15 -0
  27. package/dist/pages/test-env/JwtInspectPage.d.ts +16 -14
  28. package/dist/pages/test-env/JwtInspectPage.js +16 -14
  29. package/dist/pages/test-env/RefreshTokenPage.d.ts +7 -10
  30. package/dist/pages/test-env/RefreshTokenPage.js +7 -10
  31. package/dist/pages/test-env/guarded.js +21 -57
  32. package/dist/pages/test-env/index.d.ts +15 -24
  33. package/dist/pages/test-env/index.js +15 -26
  34. package/dist/routes/mfa/first-enrol-confirm.d.ts +10 -0
  35. package/dist/routes/mfa/first-enrol-confirm.js +14 -0
  36. package/dist/routes/mfa/first-enrol-enroll.d.ts +10 -0
  37. package/dist/routes/mfa/first-enrol-enroll.js +14 -0
  38. package/package.json +26 -1
  39. package/src/api-handlers/auth/credential-login.ts +49 -0
  40. package/src/api-handlers/mfa/first-enrol.ts +55 -0
  41. package/src/api-handlers/mfa/totp-confirm.ts +13 -5
  42. package/src/api-handlers/test/force-expire.ts +13 -70
  43. package/src/auth/better-auth.ts +44 -2
  44. package/src/components/auth/CredentialSignInForm.tsx +18 -1
  45. package/src/components/mfa/FirstEnrolPanel.tsx +189 -0
  46. package/src/lib/credential-sign-in-client.ts +13 -1
  47. package/src/lib/first-enrol.ts +180 -0
  48. package/src/lib/idp-client-config.ts +2 -2
  49. package/src/lib/log-safe.ts +9 -0
  50. package/src/lib/payez-client-secret.ts +2 -1
  51. package/src/lib/token-pages-switch.ts +44 -0
  52. package/src/pages/first-enroll/page.tsx +19 -0
  53. package/src/pages/test-env/JwtInspectPage.tsx +16 -14
  54. package/src/pages/test-env/RefreshTokenPage.tsx +7 -10
  55. package/src/pages/test-env/guarded.tsx +21 -55
  56. package/src/pages/test-env/index.ts +15 -26
  57. package/src/routes/mfa/first-enrol-confirm.ts +10 -0
  58. package/src/routes/mfa/first-enrol-enroll.ts +10 -0
@@ -35,23 +35,25 @@ function decodeJwtPayload(token: string): any {
35
35
  * IN PRODUCTION. ⚠️
36
36
  *
37
37
  * Session Inspector Page. Debug page for inspecting session data from Redis:
38
- * shows user info, roles, 2FA status, and TOKENS. It offers
39
- * COPY-TO-CLIPBOARD buttons for the live SESSION TOKEN and ACCESS TOKEN, and
40
- * reads them from the consuming app's `/api/internal/session-data` route.
38
+ * shows user info, roles, 2FA status, and the DECODED CLAIMS of the IDP access
39
+ * token, which it reads from the consuming app's `/api/internal/session-data`
40
+ * route. Credentials themselves are shown as tokenLogId + length ONLY: the
41
+ * Copy-to-clipboard and Reveal controls this docstring used to describe were
42
+ * removed by PAY-1470 (BAPert 59022, 59040), and test-env-no-token-material
43
+ * holds the page to that. (PAY-3220: this sentence was stale until now.)
41
44
  *
42
45
  * ────────────────────────── WHY THIS WARNING EXISTS (QAPert 35000 / 248991) ──────────────────────────
43
- * THIS PAGE HAS NO ENV GATE OF ITS OWN. Unlike the force-expire handler
44
- * (default-denied behind ENABLE_TEST_ROUTES) or the idp-token route (behind
45
- * ENABLE_TOKEN_INSPECTOR), a mounted page is reachable exactly as far as the
46
- * consuming app's own middleware allows. MOUNTING IT IS THE GATE. There is no
47
- * second line of defence behind it.
46
+ * Like the force-expire handler (no flag since PAY-3216) and unlike the
47
+ * idp-token route (behind ENABLE_TOKEN_INSPECTOR), a mounted page is reachable
48
+ * exactly as far as the consuming app's own middleware allows. MOUNTING IT IS
49
+ * THE GATE. The one switch on it is PAYEZ_TOKEN_PAGES_OFF (PAY-3220), unset by
50
+ * default and set only on a production host on Jon's word.
48
51
  *
49
- * AND THE CREDENTIAL IT HANDS OVER IS THE PRECONDITION FOR A REAL ATTACK ON
50
- * THIS SAME PACKAGE. The 246007 finding reduces to "a caller who KNOWS A
51
- * SESSION TOKEN can act on a session it does not own". The purpose of this
52
- * page is to put a live session token on the clipboard of whoever loads it. A
53
- * screen-share, a shoulder-surfed screenshot or a support call is enough.
54
- * Treat the rendered value as a live credential, because that is what it is.
52
+ * WHAT IT STILL HANDS OVER IS SENSITIVE. The decoded claims say who the
53
+ * signed-in person is, which roles they hold and how they authenticated, and
54
+ * the 246007 finding reduces to "a caller who KNOWS A SESSION TOKEN can act on
55
+ * a session it does not own": a page that once showed that token is the reason
56
+ * this one is treated as a production hazard.
55
57
  *
56
58
  * NOTE FOR MAINTAINERS: this warning must stay on THIS docstring, the one
57
59
  * attached to the exported declaration. An identical block placed higher in
@@ -14,25 +14,22 @@ import { redactCredentials } from './redact-credentials';
14
14
  * `/api/test/force-expire`. That endpoint MUTATES SESSION STATE.
15
15
  *
16
16
  * ────────────────────────── WHY THIS WARNING EXISTS (QAPert 35000) ──────────────────────────
17
- * The force-expire HANDLER was fixed (244877: default-deny behind
18
- * ENABLE_TEST_ROUTES) and its docstring corrected to say so — but that fix
19
- * covered the ROUTE AND NOT THE PAGE THAT DRIVES IT. This page is a separate
17
+ * The force-expire HANDLER was fixed (244877) and its docstring corrected to
18
+ * say so — but that fix covered the ROUTE AND NOT THE PAGE THAT DRIVES IT. This page is a separate
20
19
  * public export carrying its own mount instructions, so a consumer can follow
21
20
  * these lines and never once read the handler's warning.
22
21
  *
23
- * THIS PAGE HAS NO ENV GATE OF ITS OWN. Mounting it is the gate. The two are
24
- * independent: mounting this page while ENABLE_TEST_ROUTES is unset gives you
25
- * a button that 404s — confusing, but safe. The unsafe configuration is
26
- * setting that flag anywhere this page is also reachable.
22
+ * THIS PAGE HAS NO ENV GATE OF ITS OWN, and since PAY-3216 neither has the
23
+ * handler it drives: mounting is the gate. Wherever this page and the
24
+ * force-expire route are both mounted, the button works.
27
25
  *
28
26
  * ────────────────────────── IF YOU MOUNT IT, MOUNT IT LIKE THIS ──────────────────────────
29
27
  * ```typescript
30
28
  * // app/test-env/refresh-token/page.tsx — TEST/DEV ENVIRONMENTS ONLY
31
29
  * export { RefreshTokenPage as default } from '@payez/next-mvp/pages/test-env';
32
30
  * ```
33
- * Requires ENABLE_TEST_ROUTES=true IN THAT ENVIRONMENT ONLY for the button to
34
- * do anything. Never set it in production. If you do not need to exercise the
35
- * refresh flow, DO NOT MOUNT THIS AT ALL.
31
+ * The button needs /api/test/force-expire mounted too; there is no flag. If
32
+ * you do not need to exercise the refresh flow, DO NOT MOUNT THIS AT ALL.
36
33
  */
37
34
  export function RefreshTokenPage() {
38
35
  const { data: session } = authClient.useSession();
@@ -1,81 +1,47 @@
1
1
  /**
2
- * SERVER-COMPONENT GUARDS for the test-env debug pages (card 249259, BAPert
3
- * ruling 35321). These are what `@payez/next-mvp/pages/test-env` exports. The
4
- * raw client components are NOT exported.
2
+ * The BARREL that `@payez/next-mvp/pages/test-env` exports: one server-component wrapper per test-env page.
5
3
  *
6
- * ── WHAT THIS IS, AND WHAT IT IS EMPHATICALLY NOT ───────────────────────────
7
- * MOUNTING IS STILL THE PRIMARY GATE. This wrapper is a backstop for a mount
8
- * that should not have happened. It is NOT a licence to mount these freely:
9
- * setting ENABLE_TEST_ROUTES=true in an environment where these pages are
10
- * mounted gives you exactly the exposure this package spent 248991 documenting
11
- * - a live session token and a copy button. If you are not actively debugging,
12
- * DO NOT MOUNT THESE AT ALL. The safest configuration remains their absence.
4
+ * PAY-3216 (Jon's order, 15 Nov): the ENABLE_TEST_ROUTES check and its notFound() are REMOVED. Each wrapper renders
5
+ * its page unconditionally, as the pages rendered before 1d40a8c. Ruling 35321 is withdrawn.
13
6
  *
14
- * ── WHY A SERVER COMPONENT, AND WHY THE PAGES THEMSELVES CANNOT DO THIS ─────
15
- * 248991 item 2 asked for a per-page env gate and it was correctly REFUSED:
16
- * the pages are `'use client'`, and non-`NEXT_PUBLIC_` variables are not
17
- * available in the browser (Next.js docs, environment-variables: "Non-
18
- * NEXT_PUBLIC_ environment variables are only available in the Node.js
19
- * environment"). A check written inside those files would read `undefined` on
20
- * the client and deny ALWAYS - killing the tool in dev too - while reading the
21
- * REAL value during SSR, producing a hydration mismatch. It would look like a
22
- * gate, satisfy a grep for one, and protect nothing.
7
+ * PAY-3220 (ruled Win 105653 s1): the ONE production switch, PAYEZ_TOKEN_PAGES_OFF (../../lib/token-pages-switch), sits
8
+ * here for the two token-showing pages ONLY. Unset, they render exactly as before. Set, they answer notFound(): a real
9
+ * 404, indistinguishable from a route that was never registered (a 403 would confirm the page exists).
10
+ * TestEnvPage (the index) and EmergencyLogoutPage are NOT covered, on purpose: Jon named the two token-showing pages.
23
11
  *
24
- * A SERVER component can read the variable for real. That is the whole reason
25
- * this file exists as a separate layer instead of a few lines in each page.
26
- *
27
- * ── FAIL CLOSED ─────────────────────────────────────────────────────────────
28
- * The test is `=== 'true'`, not truthiness. Unset, empty, "false", "0", "TRUE"
29
- * and every typo all deny. The UNSAFE state requires a deliberate, exact act;
30
- * the SAFE state is what you get by doing nothing, including by misconfiguring
31
- * it. A gate whose default is "open" is not a gate.
32
- *
33
- * `notFound()` is deliberate and is not a 403: a real 404 is indistinguishable
34
- * from a route that was never registered, whereas a 403 CONFIRMS the page
35
- * exists to anyone who probes for it. That is the same disclosure axis card
36
- * 245920 was raised about, so shipping the confirming variant here would
37
- * reintroduce next door the thing being fixed there.
38
- *
39
- * notFound() throws (NEXT_HTTP_ERROR_FALLBACK;404) and terminates rendering of
40
- * the segment, so nothing below it runs and the client component is never sent
41
- * to the browser. It does not need `return` - it is typed `never`.
12
+ * PER REQUEST: unstable_noStore() makes the segment dynamic, so a consumer's page is never prerendered with the build
13
+ * host's value. It is synchronous (the wrappers stay plain components, so PAY-3216's renderToString rows still hold) and
14
+ * a no-op outside a request. LIMIT: in Next 16 with cacheComponents (Dynamic I/O) it is a no-op too; the production
15
+ * roll's served check (both pages read back as 404 by request) is the row that would show it.
42
16
  */
43
17
  import { notFound } from 'next/navigation';
44
-
18
+ import { unstable_noStore as noStore } from 'next/cache';
19
+ import { tokenPagesOff } from '../../lib/token-pages-switch';
45
20
  import { TestEnvPage as TestEnvPageUnguarded } from './TestEnvPage';
46
21
  import { JwtInspectPage as JwtInspectPageUnguarded } from './JwtInspectPage';
47
22
  import { RefreshTokenPage as RefreshTokenPageUnguarded } from './RefreshTokenPage';
48
23
  import { EmergencyLogoutPage as EmergencyLogoutPageUnguarded } from './EmergencyLogoutPage';
49
24
 
50
- /**
51
- * The single gate. Exact-match on purpose - see FAIL CLOSED above.
52
- *
53
- * Read at REQUEST time inside each page rather than once at module scope: a
54
- * module-scope constant would be captured at build time, so a consumer who
55
- * changed the variable would keep whatever value was baked into the bundle and
56
- * would have no way to tell. Reading per render costs an object property
57
- * lookup and removes an entire class of "I turned it off and it stayed on".
58
- */
59
- function testRoutesEnabled(): boolean {
60
- return process.env.ENABLE_TEST_ROUTES === 'true';
61
- }
62
-
63
25
  export function TestEnvPage() {
64
- if (!testRoutesEnabled()) notFound();
65
26
  return <TestEnvPageUnguarded />;
66
27
  }
67
28
 
29
+ /** PAY-3220: per request, then the switch. notFound() throws, so nothing below it renders and no client code is sent. */
30
+ function refuseWhenTokenPagesOff(): void {
31
+ noStore();
32
+ if (tokenPagesOff()) notFound();
33
+ }
34
+
68
35
  export function JwtInspectPage() {
69
- if (!testRoutesEnabled()) notFound();
36
+ refuseWhenTokenPagesOff();
70
37
  return <JwtInspectPageUnguarded />;
71
38
  }
72
39
 
73
40
  export function RefreshTokenPage() {
74
- if (!testRoutesEnabled()) notFound();
41
+ refuseWhenTokenPagesOff();
75
42
  return <RefreshTokenPageUnguarded />;
76
43
  }
77
44
 
78
45
  export function EmergencyLogoutPage() {
79
- if (!testRoutesEnabled()) notFound();
80
46
  return <EmergencyLogoutPageUnguarded />;
81
47
  }
@@ -7,8 +7,8 @@
7
7
  * and the block below is a four-line copy-paste that mounts every debug page
8
8
  * this package has. It previously carried no warning at all.
9
9
  *
10
- * NONE of these pages has an env gate of its own. Unlike the force-expire
11
- * handler (ENABLE_TEST_ROUTES) or the idp-token route
10
+ * NONE of these pages has an env gate of its own. Like the force-expire
11
+ * handler (no flag since PAY-3216) and unlike the idp-token route
12
12
  * (ENABLE_TOKEN_INSPECTOR), a mounted page is reachable exactly as far as your
13
13
  * middleware allows. MOUNTING IS THE GATE — so gate the WHOLE /test-env
14
14
  * segment, per environment, not page by page.
@@ -22,28 +22,19 @@
22
22
  * RefreshTokenPage Drives the destructive /api/test/force-expire.
23
23
  * EmergencyLogoutPage Mildest. Self-inflicted, recoverable.
24
24
  *
25
- * ── THESE PAGES ARE NOW ENV-GATED, FAIL-CLOSED (249259, BAPert ruling 35321) ──
26
- * SUPERSEDES the previous note here, which said a gate was impossible and that
27
- * mounting was the only control. That was correct about the PAGES and wrong as
28
- * a conclusion: it CANNOT be done inside these `'use client'` files, but it CAN
29
- * be done in a server component wrapping them. That wrapper now ships, and it
30
- * is what the four names below resolve to — see ./guarded.tsx.
25
+ * ── NO ENV GATE (PAY-3216, Jon's order, 15 Nov) ─────────────────────────────
26
+ * The ENABLE_TEST_ROUTES gate that 1d40a8c (4.2.0, ruling 35321) put on these
27
+ * four pages is REMOVED: they render with no flag set, as before that commit.
28
+ * The ruling is withdrawn. MOUNTING IS THE GATE: a mounted page is reachable
29
+ * exactly as far as your middleware allows. Which of them a production site
30
+ * mounts is the site owner's choice.
31
31
  *
32
- * ENABLE_TEST_ROUTES === 'true' -> the page renders
33
- * anything else, including unset -> notFound()
34
- *
35
- * It is the DEFAULT, not an opt-in sibling: BAPert's ruling was explicit that
36
- * "an opt-in safety gate is one nobody opts into". The raw ungated components
37
- * are NOT exported. Same import path, same component names, same signatures —
38
- * consumers add one env var to their dev environments and change no code.
39
- *
40
- * ── MOUNTING IS STILL THE PRIMARY GATE ──────────────────────────────────────
41
- * Do not read the env var as making mounting safe. The wrapper is a backstop
42
- * for a mount that should not have happened. With ENABLE_TEST_ROUTES=true in an
43
- * environment where these are mounted you have exactly the exposure 248991
44
- * documented: a live session token, one click from the clipboard. If you are
45
- * not actively debugging, DO NOT MOUNT THESE AT ALL — the safest configuration
46
- * is still their absence, and the env var does not change that.
32
+ * ── ONE PRODUCTION SWITCH (PAY-3220) ────────────────────────────────────────
33
+ * PAYEZ_TOKEN_PAGES_OFF, UNSET by default (every page works as above). Set on
34
+ * a production host, on Jon's word only, it makes JwtInspectPage,
35
+ * RefreshTokenPage and /api/test/force-expire answer 404. TestEnvPage and
36
+ * EmergencyLogoutPage are not covered. Read server-side per request; see
37
+ * lib/token-pages-switch for the values it accepts.
47
38
  *
48
39
  * Each page's own docstring carries the detail. If you are not actively
49
40
  * debugging, DO NOT MOUNT THESE AT ALL — the safest configuration is their
@@ -65,9 +56,7 @@
65
56
  * ```
66
57
  */
67
58
 
68
- // GUARDED wrappers only. Exporting the raw components from './TestEnvPage'
69
- // etc. would reintroduce an ungated path to the same pages under a different
70
- // import, which is the "opt-in gate nobody opts into" the ruling rejected.
59
+ // Through the barrel (./guarded), as on main: it is PAY-3220's seam. Since PAY-3216 it renders each page with no flag.
71
60
  export {
72
61
  TestEnvPage,
73
62
  TestEnvPage as default,
@@ -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';