@payez/next-mvp 4.1.20 → 4.4.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.
- package/POST-INSTALL.md +408 -0
- package/README.md +7 -0
- package/dist/api-handlers/account/recovery/initiate.js +35 -2
- package/dist/api-handlers/account/recovery/send-code.d.ts +0 -33
- package/dist/api-handlers/account/recovery/send-code.js +35 -2
- package/dist/api-handlers/account/recovery/verify-code.d.ts +0 -33
- package/dist/api-handlers/account/recovery/verify-code.js +35 -2
- package/dist/api-handlers/account/reset-password.js +35 -2
- package/dist/api-handlers/account/validate-password.js +21 -3
- package/dist/api-handlers/admin/analytics.js +2 -1
- package/dist/api-handlers/admin/audit.js +2 -1
- package/dist/api-handlers/admin/sessions.js +2 -1
- package/dist/api-handlers/admin/stats.js +2 -1
- package/dist/api-handlers/admin/users.js +2 -1
- package/dist/api-handlers/admin/vibe-data.js +2 -1
- package/dist/api-handlers/auth/jwks.js +10 -0
- package/dist/api-handlers/auth/login.js +5 -2
- package/dist/api-handlers/auth/passkey-signin.js +16 -1
- package/dist/api-handlers/auth/refresh.d.ts +23 -3
- package/dist/api-handlers/auth/refresh.js +107 -16
- package/dist/api-handlers/auth/signout.js +3 -2
- package/dist/api-handlers/auth/verify-code.js +5 -2
- package/dist/api-handlers/session/viability.js +27 -1
- package/dist/api-handlers/test/force-expire.d.ts +29 -15
- package/dist/api-handlers/test/force-expire.js +109 -14
- package/dist/auth/better-auth.d.ts +263 -6
- package/dist/auth/better-auth.js +437 -59
- package/dist/auth/utils/idp-client.js +1 -1
- package/dist/client/AuthContext.js +49 -17
- package/dist/components/account/UserAvatarMenu.js +12 -0
- package/dist/components/auth/ModeAwareLoginPage.js +10 -8
- package/dist/components/auth/ModeAwareSignupPage.js +5 -2
- package/dist/components/auth/TraditionalAuthSection.js +5 -1
- package/dist/hooks/useAuthSettings.js +14 -2
- package/dist/hooks/usePublicAuthSettings.d.ts +2 -2
- package/dist/hooks/usePublicAuthSettings.js +28 -14
- package/dist/lib/client-key.d.ts +61 -0
- package/dist/lib/client-key.js +72 -0
- package/dist/lib/demo-mode.js +2 -1
- package/dist/lib/ensure-fresh-access-token.js +12 -1
- package/dist/lib/idp-client-config.d.ts +25 -2
- package/dist/lib/idp-client-config.js +381 -36
- package/dist/lib/idp-fetch.d.ts +2 -0
- package/dist/lib/idp-fetch.js +31 -1
- package/dist/lib/idp-url-guard.d.ts +29 -0
- package/dist/lib/idp-url-guard.js +38 -0
- package/dist/lib/redis.js +77 -5
- package/dist/lib/session-cookie-shape.d.ts +69 -0
- package/dist/lib/session-cookie-shape.js +126 -0
- package/dist/lib/session-store.d.ts +35 -2
- package/dist/lib/session-store.js +135 -4
- package/dist/lib/single-flight.d.ts +35 -0
- package/dist/lib/single-flight.js +30 -0
- package/dist/lib/token-lifecycle.js +12 -0
- package/dist/middleware/create-middleware.js +12 -0
- package/dist/models/SessionModel.d.ts +41 -0
- package/dist/models/SessionModel.js +9 -0
- package/dist/pages/admin-login/page.d.ts +1 -1
- package/dist/pages/admin-login/page.js +5 -41
- package/dist/pages/security/EnhancedSecurityPage.d.ts +35 -1
- package/dist/pages/security/EnhancedSecurityPage.js +42 -2
- package/dist/pages/security/index.d.ts +1 -0
- package/dist/pages/security/index.js +3 -0
- package/dist/pages/test-env/EmergencyLogoutPage.d.ts +17 -5
- package/dist/pages/test-env/EmergencyLogoutPage.js +17 -5
- package/dist/pages/test-env/JwtInspectPage.d.ts +30 -5
- package/dist/pages/test-env/JwtInspectPage.js +166 -7
- package/dist/pages/test-env/RefreshTokenPage.d.ts +22 -6
- package/dist/pages/test-env/RefreshTokenPage.js +22 -6
- package/dist/pages/test-env/TestEnvPage.d.ts +16 -4
- package/dist/pages/test-env/TestEnvPage.js +16 -4
- package/dist/pages/test-env/guarded.d.ts +4 -0
- package/dist/pages/test-env/guarded.js +86 -0
- package/dist/pages/test-env/index.d.ts +50 -7
- package/dist/pages/test-env/index.js +58 -12
- package/dist/routes/auth/logout.js +3 -2
- package/dist/routes/auth/refresh.js +27 -1
- package/dist/routes/auth/session.d.ts +12 -3
- package/dist/routes/auth/session.js +92 -9
- package/dist/routes/auth/settings.d.ts +34 -4
- package/dist/routes/auth/settings.js +87 -33
- package/dist/routes/auth/viability.d.ts +1 -1
- package/dist/routes/auth/viability.js +122 -11
- package/dist/server/auth-guard.js +22 -15
- package/dist/server/auth.d.ts +170 -4
- package/dist/server/auth.js +42 -75
- package/dist/server/decode-session.js +84 -10
- package/dist/types/auth.d.ts +18 -0
- package/package.json +969 -958
- package/scripts/check-internal-url-usage.sh +73 -73
- package/scripts/check-publish-health.mjs +120 -0
- package/scripts/dev-broker.ps1 +71 -6
- package/scripts/print-client-config.mjs +162 -0
- package/scripts/server-only-stub.cjs +4 -0
- package/src/api-handlers/account/recovery/initiate.ts +35 -2
- package/src/api-handlers/account/recovery/send-code.ts +36 -2
- package/src/api-handlers/account/recovery/verify-code.ts +36 -2
- package/src/api-handlers/account/reset-password.ts +35 -2
- package/src/api-handlers/account/validate-password.ts +22 -3
- package/src/api-handlers/admin/analytics.ts +2 -1
- package/src/api-handlers/admin/audit.ts +2 -1
- package/src/api-handlers/admin/sessions.ts +2 -1
- package/src/api-handlers/admin/stats.ts +2 -1
- package/src/api-handlers/admin/users.ts +2 -1
- package/src/api-handlers/admin/vibe-data.ts +2 -1
- package/src/api-handlers/auth/jwks.ts +10 -0
- package/src/api-handlers/auth/login.ts +5 -2
- package/src/api-handlers/auth/passkey-signin.ts +112 -98
- package/src/api-handlers/auth/refresh.ts +112 -15
- package/src/api-handlers/auth/signout.ts +4 -3
- package/src/api-handlers/auth/verify-code.ts +5 -2
- package/src/api-handlers/mfa/backup-code-verify.ts +175 -175
- package/src/api-handlers/mfa/backup-codes.ts +41 -41
- package/src/api-handlers/mfa/methods.ts +46 -46
- package/src/api-handlers/mfa/totp-confirm.ts +42 -42
- package/src/api-handlers/mfa/totp-enroll.ts +42 -42
- package/src/api-handlers/passkeys/list.ts +31 -31
- package/src/api-handlers/passkeys/manage.ts +54 -54
- package/src/api-handlers/passkeys/register-options.ts +42 -42
- package/src/api-handlers/passkeys/register-verify.ts +41 -41
- package/src/api-handlers/session/viability.ts +29 -1
- package/src/api-handlers/test/force-expire.ts +118 -13
- package/src/auth/better-auth.ts +486 -51
- package/src/auth/utils/idp-client.ts +1 -1
- package/src/client/AuthContext.tsx +55 -19
- package/src/components/account/UserAvatarMenu.tsx +11 -0
- package/src/components/auth/ModeAwareLoginPage.tsx +40 -49
- package/src/components/auth/ModeAwareSignupPage.tsx +5 -2
- package/src/components/auth/TraditionalAuthSection.tsx +5 -1
- package/src/hooks/useAuthSettings.ts +14 -2
- package/src/hooks/usePublicAuthSettings.ts +38 -16
- package/src/lib/client-key.ts +88 -0
- package/src/lib/demo-mode.ts +2 -1
- package/src/lib/ensure-fresh-access-token.ts +12 -1
- package/src/lib/idp-client-config.ts +399 -36
- package/src/lib/idp-fetch.ts +36 -1
- package/src/lib/idp-url-guard.ts +37 -0
- package/src/lib/redis.ts +84 -6
- package/src/lib/session-cookie-shape.ts +184 -0
- package/src/lib/session-store.ts +152 -6
- package/src/lib/single-flight.ts +64 -0
- package/src/lib/token-lifecycle.ts +12 -0
- package/src/middleware/create-middleware.ts +11 -0
- package/src/models/SessionModel.ts +49 -0
- package/src/pages/admin-login/page.tsx +17 -127
- package/src/pages/security/EnhancedSecurityPage.tsx +95 -9
- package/src/pages/security/index.ts +3 -0
- package/src/pages/test-env/EmergencyLogoutPage.tsx +17 -5
- package/src/pages/test-env/JwtInspectPage.tsx +235 -20
- package/src/pages/test-env/RefreshTokenPage.tsx +22 -6
- package/src/pages/test-env/TestEnvPage.tsx +16 -4
- package/src/pages/test-env/guarded.tsx +81 -0
- package/src/pages/test-env/index.ts +59 -7
- package/src/routes/auth/logout.ts +4 -3
- package/src/routes/auth/passkey-signin.ts +13 -13
- package/src/routes/auth/refresh.ts +30 -1
- package/src/routes/auth/session.ts +98 -9
- package/src/routes/auth/settings.ts +144 -42
- package/src/routes/auth/viability.ts +129 -11
- package/src/routes/mfa/backup-code-verify.ts +1 -1
- package/src/routes/mfa/backup-codes.ts +10 -10
- package/src/routes/mfa/methods.ts +22 -22
- package/src/routes/mfa/totp-confirm.ts +10 -10
- package/src/routes/mfa/totp-enroll.ts +10 -10
- package/src/routes/mfa/totp-verify.ts +14 -14
- package/src/routes/passkeys/list.ts +10 -10
- package/src/routes/passkeys/manage.ts +23 -23
- package/src/routes/passkeys/register-options.ts +10 -10
- package/src/routes/passkeys/register-verify.ts +10 -10
- package/src/server/auth-guard.ts +24 -17
- package/src/server/auth.ts +54 -74
- package/src/server/decode-session.ts +95 -11
- package/src/templates/instrumentation.ts +21 -2
- package/src/types/auth.ts +20 -0
- package/dist/lib/auth-secret.d.ts +0 -17
- package/dist/lib/auth-secret.js +0 -116
- package/src/lib/auth-secret.ts +0 -137
package/POST-INSTALL.md
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# Post-Install Debt
|
|
2
|
+
|
|
3
|
+
**A running tab of everything a consuming site must do BY HAND after `npm install @payez/next-mvp`.**
|
|
4
|
+
|
|
5
|
+
This file exists because "works out of the box" is the product claim and it is currently
|
|
6
|
+
false. Every row below is work the package pushed onto the consumer. The tab is the backlog:
|
|
7
|
+
the goal is to empty it, not to document it more thoroughly.
|
|
8
|
+
|
|
9
|
+
Started 2026-08-26 from the UMI Browser (`umi_browser_website`, client 16) integration, where
|
|
10
|
+
every single row was hit in one sitting. UMI is the honest measure of the out-of-box
|
|
11
|
+
experience because nobody had pre-wired it — unlike idealresume.online and idealvibe.online,
|
|
12
|
+
which were built alongside the package and quietly absorbed each of these as they appeared.
|
|
13
|
+
|
|
14
|
+
**Add a row the moment you hand-write something the package could have shipped.** A step you
|
|
15
|
+
only discover by hitting it is the same defect class this codebase keeps paying for: present,
|
|
16
|
+
plausible and inert.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Scoring
|
|
21
|
+
|
|
22
|
+
- **P1** — site is broken or insecure until done, and nothing tells you
|
|
23
|
+
- **P2** — site is degraded or wrong until done
|
|
24
|
+
- **P3** — friction; you get there, but slower than you should
|
|
25
|
+
|
|
26
|
+
**Fix class:**
|
|
27
|
+
- `auto` — the package could just do this, no consumer action at all
|
|
28
|
+
- `codemod` — a generator / `npx @payez/next-mvp init` could write it
|
|
29
|
+
- `doc` — genuinely a consumer choice; it only needs to be findable
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## The tab
|
|
34
|
+
|
|
35
|
+
| # | What you must hand-write | P | Fix class | Status |
|
|
36
|
+
|---|---|---|---|---|
|
|
37
|
+
| 1 | 16 CSS variables in `globals.css` | P1 | auto | open |
|
|
38
|
+
| 2 | `/api/auth/[...nextauth]` route file | P1 | codemod | open |
|
|
39
|
+
| 3 | `/api/auth/settings` route file | P1 | codemod | open |
|
|
40
|
+
| 4 | `/login` page mounting `ModeAwareLoginPage` | P1 | codemod | open |
|
|
41
|
+
| 5 | `instrumentation.ts` + the node split | P1 | codemod | open |
|
|
42
|
+
| 6 | `next.config.js` transpile + tracing keys | P1 | doc | open |
|
|
43
|
+
| 7 | `--primary` vs `--color-primary` collision | P2 | auto | open |
|
|
44
|
+
| 8 | `/test-env/*` mounts + `ENABLE_TEST_ROUTES` | P2 | doc | open |
|
|
45
|
+
| 9 | Local user record (route + lib + hook) | P2 | codemod | open |
|
|
46
|
+
| 10 | `server-auth.ts` wrapper | P3 | auto | open |
|
|
47
|
+
| 11 | `REDIS_URL` and the required env set | P3 | doc | open |
|
|
48
|
+
| 12 | `providers.tsx` provider nesting | P3 | codemod | open |
|
|
49
|
+
| 13 | `/api/internal/session-data` — page ships, endpoint does not | P1 | auto | open |
|
|
50
|
+
| 14 | **`routes/auth/nextauth` is a trap — do not mount it** | P1 | auto | open |
|
|
51
|
+
| 15 | `id_token` dropped by a module-scoped ALS | P1 | auto | **CLOSED 4.3.4** |
|
|
52
|
+
| 16 | Tenant-side: `VIBE_COLLECTION` is doing double duty | P2 | doc | open |
|
|
53
|
+
| 17 | Tenant-side: vibe-client 0.2.3 sends the wrong filter shape | P2 | — | open (not this package) |
|
|
54
|
+
|
|
55
|
+
**Nothing is closed yet. There is no `postinstall` script and no `init` command.**
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 1. Sixteen CSS variables, and `ThemeProvider` emits none of them — P1
|
|
60
|
+
|
|
61
|
+
The worst row on the tab, because it is invisible: the site renders, it just looks wrong, and
|
|
62
|
+
the fallbacks make it look *deliberately* wrong rather than unstyled.
|
|
63
|
+
|
|
64
|
+
Measured 2026-08-26 against installed `dist`:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
components/ and pages/ READ 16 CSS variables
|
|
68
|
+
theme/ThemeProvider WRITES 0
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The sixteen:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
--background --bg-card --bg-default --border --border-default --card
|
|
75
|
+
--color-error --color-primary --foreground --input --muted --muted-foreground
|
|
76
|
+
--primary --text-muted --text-primary --text-secondary
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`ThemeProvider` takes a theme object and emits **nothing to CSS**. So all three sites
|
|
80
|
+
independently hand-authored the same variable block in their own `globals.css` — three
|
|
81
|
+
copies, already drifting.
|
|
82
|
+
|
|
83
|
+
**Fix:** `ThemeProvider` maps the theme object onto these sixteen and sets them. That is the
|
|
84
|
+
entire job it appears to exist to do. Until then a site that installs the package and uses a
|
|
85
|
+
component gets silent fallbacks.
|
|
86
|
+
|
|
87
|
+
## 2–4. The package exports routes and pages; you mount every one — P1
|
|
88
|
+
|
|
89
|
+
`@payez/next-mvp` exports working handlers and pages. It cannot create files in your `app/`
|
|
90
|
+
directory, so **every one of them is a 404 until you write a re-export by hand** — and
|
|
91
|
+
nothing tells you which exist.
|
|
92
|
+
|
|
93
|
+
UMI had **zero of them mounted** and looked like a working site. The login page rendered a
|
|
94
|
+
demo gate against hardcoded strings; `/api/auth/*` did not exist at all, so the sign-in
|
|
95
|
+
screen's request for what the client offers 404'd and it drew nothing.
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
app/api/auth/[...nextauth]/route.ts export { GET, POST } from '@payez/next-mvp/routes/auth/nextauth';
|
|
99
|
+
app/api/auth/settings/route.ts export { GET } from '@payez/next-mvp/routes/auth/settings';
|
|
100
|
+
app/login/page.tsx ModeAwareLoginPage + your branding
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Fix:** `npx @payez/next-mvp init` writes these. Failing that, one page in the README that
|
|
104
|
+
lists every mountable export — there is currently no such list anywhere.
|
|
105
|
+
|
|
106
|
+
## 5. `instrumentation.ts`, and the part the template gets wrong — P1
|
|
107
|
+
|
|
108
|
+
A template ships at `src/templates/instrumentation.ts` — the only template in the package —
|
|
109
|
+
but you still copy it in by hand, and it is **not** the shape that works.
|
|
110
|
+
|
|
111
|
+
Two things it does not tell you:
|
|
112
|
+
|
|
113
|
+
- **`instrumentation.ts` is compiled for BOTH the node and edge runtimes.** `NEXT_RUNTIME` is
|
|
114
|
+
a *runtime* guard; imports resolve at *build* time, so node-only dependencies still get
|
|
115
|
+
traced into the edge bundle and the build fails. The working shape is a thin
|
|
116
|
+
`instrumentation.ts` that dynamic-imports `instrumentation-node.ts`.
|
|
117
|
+
- **`await import()` does not stay dynamic under `module: CommonJS`.** TypeScript emits
|
|
118
|
+
`Promise.resolve().then(() => require(...))`, which is statically traced exactly like a
|
|
119
|
+
top-level import. Cost us three failed fixes.
|
|
120
|
+
|
|
121
|
+
Also: the template's catch block used to log *"Will retry initialization on first request"*
|
|
122
|
+
and continue. It does not retry. A site whose IDP handshake failed would serve requests
|
|
123
|
+
with no auth config and say so nowhere. Now `process.exit(1)`.
|
|
124
|
+
|
|
125
|
+
**Fix:** ship the two-file split as the template, and put both facts above in it as comments.
|
|
126
|
+
|
|
127
|
+
## 6. `next.config.js` — two keys, and one that must never come back — P1
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
transpilePackages: ['@payez/next-mvp'],
|
|
131
|
+
outputFileTracingRoot: path.join(__dirname),
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Mentioned in two docs, in passing, not as a required step.
|
|
135
|
+
|
|
136
|
+
**Do not add a `webpack:` key to work around a bundler warning.** It disables Turbopack and
|
|
137
|
+
turns a cosmetic warning into a hard `UnhandledSchemeError`. A note now sits in UMI's
|
|
138
|
+
`next.config.js` saying so.
|
|
139
|
+
|
|
140
|
+
**Related, and the reason a day was lost:** `next dev` uses **webpack** by default in Next
|
|
141
|
+
15.5. Turbopack is opt-in via `--turbopack`. Assuming otherwise sent three bundler fixes at
|
|
142
|
+
the wrong bundler.
|
|
143
|
+
|
|
144
|
+
## 7. `--primary` and `--color-primary` are two names for one value — P2
|
|
145
|
+
|
|
146
|
+
Components read both. Nothing defines the relationship. `TraditionalAuthSection`'s submit
|
|
147
|
+
button hardcodes `var(--color-primary, #10b981)` — so a site that defines `--primary` and not
|
|
148
|
+
`--color-primary` gets a **green** button no matter what its brand colour is. UMI is
|
|
149
|
+
`#FF6B5B`; it defines both, as a workaround.
|
|
150
|
+
|
|
151
|
+
This is the CLAUDE.md rule the package itself breaks: **one value gets one name.** Pick
|
|
152
|
+
`--color-primary`, and drop the hardcoded emerald — a fallback that silently substitutes
|
|
153
|
+
another product's brand colour is worse than no fallback.
|
|
154
|
+
|
|
155
|
+
## 8. `/test-env/*` — four pages, a flag, and a trap — P2
|
|
156
|
+
|
|
157
|
+
Four diagnostics ship (`TestEnvPage`, `JwtInspectPage`, `RefreshTokenPage`,
|
|
158
|
+
`EmergencyLogoutPage`). Documented in **zero** docs. idealvibe mounts five routes; UMI mounted
|
|
159
|
+
none and every URL 404'd.
|
|
160
|
+
|
|
161
|
+
They are exported **only** as guarded server components — the raw client pages are not
|
|
162
|
+
exported. The guard reads `ENABLE_TEST_ROUTES`, exact-match `'true'`, fail-closed, and returns
|
|
163
|
+
a real 404 rather than a 403 so probing cannot confirm the route exists. All of that is
|
|
164
|
+
correct and should not be softened.
|
|
165
|
+
|
|
166
|
+
**The trap:** putting `'use client'` on the page file that re-exports the guard. Non-`NEXT_PUBLIC_`
|
|
167
|
+
variables do not exist in the browser, so the guard reads `undefined` and denies **forever** —
|
|
168
|
+
a gate that greps as present and protects nothing. Hit on 2026-08-26.
|
|
169
|
+
|
|
170
|
+
These pages show a **live session token with a copy button**. `ENABLE_TEST_ROUTES=true` belongs
|
|
171
|
+
in `.env.development` and nowhere else. Verified both ways on UMI: flag off, all four 404 while
|
|
172
|
+
`/login` stays 200.
|
|
173
|
+
|
|
174
|
+
**Fix:** document the four exports, the server-component requirement, and the flag — together,
|
|
175
|
+
in one place, with the `'use client'` trap called out.
|
|
176
|
+
|
|
177
|
+
## 9. The local user record — three files, written three times — P2
|
|
178
|
+
|
|
179
|
+
The IDP owns identity; the tenant app needs its own row so application data can join to a user.
|
|
180
|
+
The package ships **none** of it, so all three sites wrote the same three files:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
app/api/auth/record-login/route.ts upsert on sign-in
|
|
184
|
+
app/lib/vibe-user.ts find-or-create by idp_user_id
|
|
185
|
+
app/hooks/useLoginRecorder.ts fire once per session
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Two things every consumer must get right and has no way to know:
|
|
189
|
+
|
|
190
|
+
- **Identity is `idp_user_id`, never email.** Email changes and gets reused; matching on it
|
|
191
|
+
silently merges two people.
|
|
192
|
+
- **The effect needs a ref guard.** Without one, a re-render or Fast Refresh posts again —
|
|
193
|
+
harmless for the row, but it inflates `login_count`, which is then wrong in a way nobody
|
|
194
|
+
would think to question.
|
|
195
|
+
|
|
196
|
+
The app-specific parts (tiers, credits, milestones — idealresume's runs to 1,567 lines) are
|
|
197
|
+
genuinely the site's. The find-or-create core is not.
|
|
198
|
+
|
|
199
|
+
## 10. `server-auth.ts` — a wrapper over one export — P3
|
|
200
|
+
|
|
201
|
+
Every site writes the same ~60-line wrapper over `getFreshIdpToken` to assemble the refresh
|
|
202
|
+
config from env and normalise the result. Package-side, this is a default config reader.
|
|
203
|
+
|
|
204
|
+
## 11. Env vars — no single list, no startup validation of the set — P3
|
|
205
|
+
|
|
206
|
+
`IDP_URL`, `CLIENT_ID`, `REFRESH_ENDPOINT`, `REDIS_URL`, `ENABLE_TEST_ROUTES`, plus the broker
|
|
207
|
+
secret. `REDIS_URL` was missing from UMI and surfaced as an unrelated-looking failure.
|
|
208
|
+
|
|
209
|
+
**`CLIENT_ID` is the only spelling.** Until 4.2.5 the package accepted `CLIENT_ID`,
|
|
210
|
+
`NEXT_PUBLIC_CLIENT_ID` and `NEXT_CLIENT_ID` for one value; that cost a live defect. All six
|
|
211
|
+
read sites are collapsed and `idp-client-config.ts` carries the banner. Do not reintroduce a
|
|
212
|
+
second name here.
|
|
213
|
+
|
|
214
|
+
**Fix:** ship `.env.example` with every variable, what reads it, and whether it is required.
|
|
215
|
+
|
|
216
|
+
## 12. `providers.tsx` — nesting order matters and is unstated — P3
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
AuthProvider → LoginRecorderProvider → ThemeProvider
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`ModeAwareLoginPage` reads `useAuthConfig()`, so without `AuthProvider` the sign-in screen
|
|
223
|
+
renders **no controls at all** — no Google button, no email form — even while
|
|
224
|
+
`/api/auth/settings` correctly reports `enabledProviders: ["google"]`. It fails as a blank
|
|
225
|
+
card, which reads as a styling problem.
|
|
226
|
+
|
|
227
|
+
**Do not set `mode` in a site's auth config.** What sign-in offers comes from the IDP via
|
|
228
|
+
`/api/auth/settings` (4.3.0+). A site hardcoding its own auth mode is how a login screen and
|
|
229
|
+
its own IDP end up disagreeing.
|
|
230
|
+
|
|
231
|
+
## 13. The Session Inspector fetches an endpoint the package does not ship — P1
|
|
232
|
+
|
|
233
|
+
`JwtInspectPage` fetches `/api/internal/session-data`. **No such handler exists in the
|
|
234
|
+
package and none is exported.** Line 39 of the page says the quiet part — "reads them from
|
|
235
|
+
the *consuming app's* `/api/internal/session-data` route" — which is a defensible design
|
|
236
|
+
call (a session record's shape is the app's business) documented in the one place no
|
|
237
|
+
consumer will ever read: the source of a page they did not write.
|
|
238
|
+
|
|
239
|
+
**It fails invisibly**, which is the real defect. The page does:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
fetch('/api/internal/session-data').then(r => r.ok ? r.json() : null).catch(() => {})
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
A 404 and a session with no data render **identically**: every field `N/A`, and
|
|
246
|
+
`Error: N/A` underneath, which reads as "no error". The inspector looked like it was
|
|
247
|
+
reporting a broken session. It was reporting a missing route. Found only by watching the
|
|
248
|
+
network tab.
|
|
249
|
+
|
|
250
|
+
UMI now owns `app/api/internal/session-data/route.ts`. Two things any implementation must
|
|
251
|
+
get right:
|
|
252
|
+
|
|
253
|
+
- **The 16 field names are a contract** (`JwtInspectPage` reads `ss.userId`, `ss.roles`,
|
|
254
|
+
`ss.idpAccessToken`, …). Rename one and that row silently goes `N/A` forever.
|
|
255
|
+
- **It returns the live IDP access AND refresh token.** The package's own `better-auth.ts`
|
|
256
|
+
names this exact endpoint as the leak path it refuses to put `id_token` behind. UMI gates
|
|
257
|
+
it on `ENABLE_TEST_ROUTES` — same fail-closed exact-match, read at request time, 404 not
|
|
258
|
+
403. Verified both ways.
|
|
259
|
+
|
|
260
|
+
**Fix:** either ship the route (it is mechanical — `getSession` + `getSession` from
|
|
261
|
+
`lib/session-store`, both already exported), or make the page say `endpoint missing` instead
|
|
262
|
+
of swallowing the 404. Preferably both.
|
|
263
|
+
|
|
264
|
+
## 14. `routes/auth/nextauth` is a trap. Do not mount it. — P1
|
|
265
|
+
|
|
266
|
+
**This one cost the most and looked the most correct.** The package ships
|
|
267
|
+
`routes/auth/nextauth`, its own docblock calls it "Ready-to-Use", and its usage example is
|
|
268
|
+
the obvious one-liner:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
export { GET, POST } from '@payez/next-mvp/routes/auth/nextauth'; // WRONG
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
It signs users in. It never provisions them.
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
routes/auth/nextauth GET -> toNextJsHandler(auth) — signs in. Stops.
|
|
278
|
+
createAuthGetHandler GET -> ba.GET + IDP token exchange — signs in AND provisions.
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Only `createAuthGetHandler` runs `exchangeOAuthForIdpTokens` after the OAuth callback. With
|
|
282
|
+
the re-export, Better Auth creates a **perfectly convincing session** — cookie, user id,
|
|
283
|
+
email, Google avatar — and the Redis record holding the IDP access token, refresh token and
|
|
284
|
+
roles is **never written**.
|
|
285
|
+
|
|
286
|
+
The damage surfaces nowhere near the cause:
|
|
287
|
+
|
|
288
|
+
- `/test-env/jwt-inspect` → `NO_REDIS_SESSION`, no tokens, no roles
|
|
289
|
+
- anything calling `getFreshIdpToken` → `NO_SESSION` **for a user who is visibly signed in**
|
|
290
|
+
- `record-login` → 401, so the app never creates its local user row
|
|
291
|
+
|
|
292
|
+
Correct wiring, and what idealvibe.online and idealresume.online **both** hand-write:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
export const GET = createAuthGetHandler('/login'); // your login path
|
|
296
|
+
export async function POST(request: Request) {
|
|
297
|
+
const ba = await getBetterAuthHandler();
|
|
298
|
+
return ba!.POST(request);
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Neither working site uses the re-export. That is not boilerplate they failed to factor out —
|
|
303
|
+
it is the only wiring that works.
|
|
304
|
+
|
|
305
|
+
**This row is also a correction to row 2 of this tab.** Row 2's advice — prefer the package
|
|
306
|
+
re-export over copying another site's file — is right for `settings` and wrong here, and I
|
|
307
|
+
followed it into this exact bug on UMI. A codemod that generates the "obvious" re-export
|
|
308
|
+
would ship this defect to every new consumer.
|
|
309
|
+
|
|
310
|
+
**Fix:** `routes/auth/nextauth` should either export the exchanging handler or refuse to
|
|
311
|
+
exist. A "Ready-to-Use" export that yields a half-provisioned session is worse than no
|
|
312
|
+
export, because it looks like the sanctioned path.
|
|
313
|
+
|
|
314
|
+
## 15. `id_token` was dropped because the ALS was module-scoped — P1, CLOSED in 4.3.4
|
|
315
|
+
|
|
316
|
+
The most expensive one, and the package's own fault end to end.
|
|
317
|
+
|
|
318
|
+
`state` (the Better Auth instance) was moved to `globalThis` by 181814 **because this module
|
|
319
|
+
loads more than once per process** — instrumentation and the auth route are separate chunks
|
|
320
|
+
with separate registries. `idTokenScope`, the AsyncLocalStorage that carries the provider's
|
|
321
|
+
raw `id_token` from `getUserInfo` to the IDP exchange, was left in module scope.
|
|
322
|
+
|
|
323
|
+
So the two copies shared the instance and **not** the scope:
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
copy A builds the instance -> the getUserInfo closure captures A's idTokenScope
|
|
327
|
+
copy B serves the callback -> createAuthGetHandler runs .run({}) on B's
|
|
328
|
+
getUserInfo fires, reads A -> getStore() undefined, `if (scope)` skips SILENTLY
|
|
329
|
+
the exchange reads B -> empty, so id_token is omitted from the body
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Sign-in succeeds at every visible step — cookie, user id, email, avatar. The IDP then
|
|
333
|
+
answers `401 UNVERIFIED_ASSERTION`, and its own log names the reason:
|
|
334
|
+
`reason="id_token missing"` (idp.payez.net, correlationId 0HNO2755RESR1, 2026-08-26).
|
|
335
|
+
|
|
336
|
+
**The consequence is a security control that shipped as a no-op.** 472fb75c added the raw
|
|
337
|
+
`id_token` because `/api/ExternalAuth/oauth-callback` would mint an IDP access+refresh pair
|
|
338
|
+
for any *asserted* email, on a public LoadBalancer — the only verifiable field in that body
|
|
339
|
+
is the provider's signed token. Wherever this module double-loads, that fix never arrived.
|
|
340
|
+
|
|
341
|
+
Fixed in 4.3.4: `idTokenScope` now lives on `globalThis` under the same pattern as `state`,
|
|
342
|
+
and the silent `if (scope)` skip warns and names its two causes.
|
|
343
|
+
|
|
344
|
+
**The lesson generalises past this bug: shared instance + per-copy request scope is the worst
|
|
345
|
+
combination.** Everything looks wired, and the one field that proves identity is dropped. If
|
|
346
|
+
any other module-scoped singleton in this package participates in a request, audit it the
|
|
347
|
+
same way.
|
|
348
|
+
|
|
349
|
+
## 16. Tenant-side: one env var, two unrelated jobs — P2
|
|
350
|
+
|
|
351
|
+
Not this package's bug, but every consumer that has a marketing form will hit it.
|
|
352
|
+
|
|
353
|
+
`createVibeClient` resolves the collection as
|
|
354
|
+
`config.defaultCollection || process.env.VIBE_COLLECTION || 'vibe_app'`. The library default
|
|
355
|
+
is already correct. UMI set `VIBE_COLLECTION=payez_contact_list` for its **waitlist form**,
|
|
356
|
+
and that variable is global to the client — so it silently retargeted every collection call
|
|
357
|
+
in the app, including user identity. Sign-in queried the waitlist's collection for a `users`
|
|
358
|
+
table that does not live there.
|
|
359
|
+
|
|
360
|
+
Same rule as `CLIENT_ID`, from the other direction: there, one value had three names; here,
|
|
361
|
+
one name carries two values. Pin the collection at the call site.
|
|
362
|
+
|
|
363
|
+
## 17. Tenant-side: vibe-client 0.2.3 sends a filter shape the API rejects — P2
|
|
364
|
+
|
|
365
|
+
`list({ filter })` serialises through `convertFiltersToVibeFormat`, which returns an **array**
|
|
366
|
+
`[{ field, operator, value }]`. `PayEz.Vibe.Public.Api` binds `filter` to a single
|
|
367
|
+
`VibeFilterNode` **object** (`VibeQueryModels.cs:63`), so the request fails to deserialise
|
|
368
|
+
before the query builder ever runs:
|
|
369
|
+
|
|
370
|
+
```
|
|
371
|
+
400 VALIDATION_ERROR $.filter
|
|
372
|
+
"The JSON value could not be converted to ...QueryBuilder.VibeFilterNode"
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
The API is not being arbitrary — `VibeQueryBuilder.cs:355` documents this exact query as
|
|
376
|
+
supported (`{ "idp_user_id": { "eq": 22 } }`), and :94 accepts the flat
|
|
377
|
+
`{ field, operator, value }` form too. The array wrapper is the whole defect, and the API
|
|
378
|
+
change is deliberate.
|
|
379
|
+
|
|
380
|
+
**This affects every filtered query from 0.2.3, on all three sites.** UMI was only the first
|
|
381
|
+
to make one — measured in the IDP proxy logs, UMI's client was the sole source of `$.filter`
|
|
382
|
+
400s in 24h while idealresume queried cleanly, because idealresume's calls in that window did
|
|
383
|
+
not filter.
|
|
384
|
+
|
|
385
|
+
UMI hand-rolls the single lookup against the proxy with the correct node, reusing the
|
|
386
|
+
library's `resolveConfig()`. That is a workaround with a cost: it duplicates the HMAC recipe
|
|
387
|
+
(`timestamp|method|endpoint`, key base64-decoded) because `generateHmacSignature` is not
|
|
388
|
+
exported, so a signing change in the library breaks that call with a 401. **The real fix is a
|
|
389
|
+
vibe-client release that emits a `VibeFilterNode`**; its source is not in this workspace.
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## What the tab says about the package
|
|
394
|
+
|
|
395
|
+
Fourteen rows; **eleven are `auto` or `codemod`** — the package could do them, or generate
|
|
396
|
+
them, with no judgement call from the consumer. Only three are real consumer choices.
|
|
397
|
+
|
|
398
|
+
The three P1 route/page rows share one root cause: **the package can export a thing but cannot
|
|
399
|
+
mount it**, and nothing enumerates what is mountable. That is one fix, not three — a list, or
|
|
400
|
+
an `init` command that writes the re-exports.
|
|
401
|
+
|
|
402
|
+
Eight of the fourteen appear in **no doc at all**. `QUICK_START.md` is a stub with a single
|
|
403
|
+
heading. Row 14 is worse than undocumented — the wrong path is the one with the usage example.
|
|
404
|
+
|
|
405
|
+
The recurring shape, which is the same one this codebase keeps paying for: **things present,
|
|
406
|
+
plausible and inert.** A `ThemeProvider` that provides no theme. A template whose comment
|
|
407
|
+
promises a retry that never happens. A guard that greps as a gate and denies everything. An
|
|
408
|
+
exported page nothing mounts. None of them announce themselves; all of them look done.
|
package/README.md
CHANGED
|
@@ -6,6 +6,13 @@ Drop-in authentication for Next.js. One package, zero secret management in produ
|
|
|
6
6
|
npm install @payez/next-mvp next-auth
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
> **Installing is not finishing.** There is no `postinstall` step and no generator, so the
|
|
10
|
+
> package cannot mount its own routes or pages into your `app/` directory, and it does not
|
|
11
|
+
> emit the CSS variables its components read. **[POST-INSTALL.md](./POST-INSTALL.md) is the
|
|
12
|
+
> complete list of what you must still do by hand** — including three that leave the site
|
|
13
|
+
> broken with nothing to tell you why: the auth routes, the login page, and the sixteen theme
|
|
14
|
+
> variables. Read it before you conclude something is wrong with the package.
|
|
15
|
+
|
|
9
16
|
## How Secrets Work
|
|
10
17
|
|
|
11
18
|
You never manage signing keys or OAuth credentials directly. The package resolves them automatically at startup based on your environment.
|
|
@@ -2,20 +2,53 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.POST = POST;
|
|
4
4
|
const server_1 = require("next/server");
|
|
5
|
+
const idp_client_config_1 = require("../../../lib/idp-client-config");
|
|
6
|
+
const client_key_1 = require("../../../lib/client-key");
|
|
5
7
|
function getIdpUrl() {
|
|
6
8
|
const url = process.env.IDP_URL;
|
|
7
9
|
if (!url)
|
|
8
10
|
throw new Error('[IDP_URL] FATAL: IDP_URL environment variable is REQUIRED.');
|
|
9
11
|
return url;
|
|
10
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* RECOVERY CLIENT KEY — cards 268012 (S12) and 268410.
|
|
15
|
+
*
|
|
16
|
+
* These four proxies populated NONE of PayEzClientIdResolver's six slots, so
|
|
17
|
+
* RequirePayEzUnAuthorized 404'd the request BEFORE the action ran and no
|
|
18
|
+
* recovery token was ever minted. That is why S12 failed at step 1.
|
|
19
|
+
*
|
|
20
|
+
* NOT `ENV_CONFIG.CLIENT_ID`, which is what card 268012 originally prescribed.
|
|
21
|
+
* That variable is the ENVIRONMENT-SPECIFIC NUMERIC id, and the IDP accepts a
|
|
22
|
+
* numeric with NO existence check (RequirePayEzUnAuthorizedAttribute.cs:31-33)
|
|
23
|
+
* while it VALIDATES a slug (:41-47). Hardcoding the numeric here would have
|
|
24
|
+
* added four more sites to the 268410 family and made a stale id answer for
|
|
25
|
+
* the WRONG TENANT on a password-reset path, silently. Slug-preferring, with
|
|
26
|
+
* the numeric fallback logged, is the same resolver the other nine sites use.
|
|
27
|
+
*
|
|
28
|
+
* SERVER VALUE WINS: client_id is applied AFTER the caller's payload spread.
|
|
29
|
+
* These are unauthenticated recovery endpoints, so a browser-supplied
|
|
30
|
+
* client_id would be a tenant-targeting lever. The caller does not get to
|
|
31
|
+
* choose which tenant their password reset is addressed to.
|
|
32
|
+
*/
|
|
33
|
+
async function recoveryClientKey() {
|
|
34
|
+
try {
|
|
35
|
+
const config = await (0, idp_client_config_1.getIDPClientConfig)();
|
|
36
|
+
return (0, client_key_1.resolveClientKey)(config, 'account/recovery/initiate').key;
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
console.error('[account/recovery/initiate] could not resolve IDP client config for client_id:', e);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
11
43
|
async function POST(req) {
|
|
12
44
|
const IDP_URL = getIdpUrl();
|
|
13
45
|
try {
|
|
14
46
|
const { email } = await req.json();
|
|
47
|
+
const clientKey = await recoveryClientKey();
|
|
15
48
|
const upstream = await fetch(`${IDP_URL}/api/Account/recovery/initiate`, {
|
|
16
49
|
method: 'POST',
|
|
17
|
-
headers: { 'Content-Type': 'application/json' },
|
|
18
|
-
body: JSON.stringify({ email }),
|
|
50
|
+
headers: { 'Content-Type': 'application/json', ...(clientKey ? { 'X-Client-Id': clientKey } : {}) },
|
|
51
|
+
body: JSON.stringify({ email, ...(clientKey ? { client_id: clientKey } : {}) }),
|
|
19
52
|
});
|
|
20
53
|
const data = await upstream.json().catch(() => ({}));
|
|
21
54
|
return server_1.NextResponse.json(data, { status: upstream.status });
|
|
@@ -1,35 +1,2 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
/**
|
|
3
|
-
* Account recovery — send-code
|
|
4
|
-
*
|
|
5
|
-
* WHY THIS WAS REWRITTEN (2026-08-02, swept out of QAPert defect #7)
|
|
6
|
-
* This handler read the caller's bearer straight off the inbound request:
|
|
7
|
-
*
|
|
8
|
-
* const auth = req.headers.get('authorization') || '';
|
|
9
|
-
* const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
|
|
10
|
-
* ... headers: { 'Authorization': `Bearer ${token}` }
|
|
11
|
-
*
|
|
12
|
-
* Browsers send a session COOKIE and no Authorization header, so `token` was
|
|
13
|
-
* ALWAYS the empty string and this always called the IDP with a literal
|
|
14
|
-
* "Bearer " and nothing after it.
|
|
15
|
-
*
|
|
16
|
-
* THAT IS WORSE THAN THE profile.ts VERSION OF THIS BUG, in two ways:
|
|
17
|
-
*
|
|
18
|
-
* 1. profile.ts at least 401'd when the header was missing. This one has no
|
|
19
|
-
* check at all — `|| ''` swallows the absence — so it proceeds to call
|
|
20
|
-
* upstream with an empty credential and returns the IDP's rejection as an
|
|
21
|
-
* opaque failure. The user sees recovery "not working", not "not
|
|
22
|
-
* authenticated", and the log shows an upstream error rather than an auth
|
|
23
|
-
* one. The defect hides itself.
|
|
24
|
-
* 2. It is BOTH HALVES of account recovery — send-code and verify-code — so
|
|
25
|
-
* the entire recovery path is unreachable, not merely degraded. This is
|
|
26
|
-
* the flow a locked-out user reaches for, i.e. it fails exactly when it is
|
|
27
|
-
* the only thing left that could help them.
|
|
28
|
-
*
|
|
29
|
-
* Fixed the same way as profile.ts and masked-info.ts: idpFetchJSON resolves
|
|
30
|
-
* the caller's token from the server-side session, so the cookie works and the
|
|
31
|
-
* token is never handed to browser JavaScript.
|
|
32
|
-
*
|
|
33
|
-
* @package @payez/next-mvp
|
|
34
|
-
*/
|
|
35
2
|
export declare function POST(req: NextRequest): Promise<NextResponse<any>>;
|
|
@@ -4,6 +4,8 @@ exports.POST = POST;
|
|
|
4
4
|
const server_1 = require("next/server");
|
|
5
5
|
const idp_fetch_1 = require("../../../lib/idp-fetch");
|
|
6
6
|
const env_1 = require("../../../config/env");
|
|
7
|
+
const idp_client_config_1 = require("../../../lib/idp-client-config");
|
|
8
|
+
const client_key_1 = require("../../../lib/client-key");
|
|
7
9
|
/**
|
|
8
10
|
* Account recovery — send-code
|
|
9
11
|
*
|
|
@@ -37,6 +39,36 @@ const env_1 = require("../../../config/env");
|
|
|
37
39
|
*
|
|
38
40
|
* @package @payez/next-mvp
|
|
39
41
|
*/
|
|
42
|
+
/**
|
|
43
|
+
* RECOVERY CLIENT KEY — cards 268012 (S12) and 268410.
|
|
44
|
+
*
|
|
45
|
+
* These four proxies populated NONE of PayEzClientIdResolver's six slots, so
|
|
46
|
+
* RequirePayEzUnAuthorized 404'd the request BEFORE the action ran and no
|
|
47
|
+
* recovery token was ever minted. That is why S12 failed at step 1.
|
|
48
|
+
*
|
|
49
|
+
* NOT `ENV_CONFIG.CLIENT_ID`, which is what card 268012 originally prescribed.
|
|
50
|
+
* That variable is the ENVIRONMENT-SPECIFIC NUMERIC id, and the IDP accepts a
|
|
51
|
+
* numeric with NO existence check (RequirePayEzUnAuthorizedAttribute.cs:31-33)
|
|
52
|
+
* while it VALIDATES a slug (:41-47). Hardcoding the numeric here would have
|
|
53
|
+
* added four more sites to the 268410 family and made a stale id answer for
|
|
54
|
+
* the WRONG TENANT on a password-reset path, silently. Slug-preferring, with
|
|
55
|
+
* the numeric fallback logged, is the same resolver the other nine sites use.
|
|
56
|
+
*
|
|
57
|
+
* SERVER VALUE WINS: client_id is applied AFTER the caller's payload spread.
|
|
58
|
+
* These are unauthenticated recovery endpoints, so a browser-supplied
|
|
59
|
+
* client_id would be a tenant-targeting lever. The caller does not get to
|
|
60
|
+
* choose which tenant their password reset is addressed to.
|
|
61
|
+
*/
|
|
62
|
+
async function recoveryClientKey() {
|
|
63
|
+
try {
|
|
64
|
+
const config = await (0, idp_client_config_1.getIDPClientConfig)();
|
|
65
|
+
return (0, client_key_1.resolveClientKey)(config, 'account/recovery/send-code').key;
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
console.error('[account/recovery/send-code] could not resolve IDP client config for client_id:', e);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
40
72
|
async function POST(req) {
|
|
41
73
|
let payload = {};
|
|
42
74
|
try {
|
|
@@ -45,10 +77,11 @@ async function POST(req) {
|
|
|
45
77
|
catch {
|
|
46
78
|
return server_1.NextResponse.json({ success: false, error: 'invalid_request', message: 'A JSON body is required' }, { status: 400 });
|
|
47
79
|
}
|
|
80
|
+
const clientKey = await recoveryClientKey();
|
|
48
81
|
const result = await (0, idp_fetch_1.idpFetchJSON)(req, `${env_1.ENV_CONFIG.IDP_URL}/api/Account/recovery/send-code`, {
|
|
49
82
|
method: 'POST',
|
|
50
|
-
headers: { 'Content-Type': 'application/json' },
|
|
51
|
-
body: JSON.stringify(payload),
|
|
83
|
+
headers: { 'Content-Type': 'application/json', ...(clientKey ? { 'X-Client-Id': clientKey } : {}) },
|
|
84
|
+
body: JSON.stringify({ ...payload, ...(clientKey ? { client_id: clientKey } : {}) }),
|
|
52
85
|
});
|
|
53
86
|
if (!result.ok) {
|
|
54
87
|
return server_1.NextResponse.json({
|
|
@@ -1,35 +1,2 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
/**
|
|
3
|
-
* Account recovery — verify-code
|
|
4
|
-
*
|
|
5
|
-
* WHY THIS WAS REWRITTEN (2026-08-02, swept out of QAPert defect #7)
|
|
6
|
-
* This handler read the caller's bearer straight off the inbound request:
|
|
7
|
-
*
|
|
8
|
-
* const auth = req.headers.get('authorization') || '';
|
|
9
|
-
* const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
|
|
10
|
-
* ... headers: { 'Authorization': `Bearer ${token}` }
|
|
11
|
-
*
|
|
12
|
-
* Browsers send a session COOKIE and no Authorization header, so `token` was
|
|
13
|
-
* ALWAYS the empty string and this always called the IDP with a literal
|
|
14
|
-
* "Bearer " and nothing after it.
|
|
15
|
-
*
|
|
16
|
-
* THAT IS WORSE THAN THE profile.ts VERSION OF THIS BUG, in two ways:
|
|
17
|
-
*
|
|
18
|
-
* 1. profile.ts at least 401'd when the header was missing. This one has no
|
|
19
|
-
* check at all — `|| ''` swallows the absence — so it proceeds to call
|
|
20
|
-
* upstream with an empty credential and returns the IDP's rejection as an
|
|
21
|
-
* opaque failure. The user sees recovery "not working", not "not
|
|
22
|
-
* authenticated", and the log shows an upstream error rather than an auth
|
|
23
|
-
* one. The defect hides itself.
|
|
24
|
-
* 2. It is BOTH HALVES of account recovery — send-code and verify-code — so
|
|
25
|
-
* the entire recovery path is unreachable, not merely degraded. This is
|
|
26
|
-
* the flow a locked-out user reaches for, i.e. it fails exactly when it is
|
|
27
|
-
* the only thing left that could help them.
|
|
28
|
-
*
|
|
29
|
-
* Fixed the same way as profile.ts and masked-info.ts: idpFetchJSON resolves
|
|
30
|
-
* the caller's token from the server-side session, so the cookie works and the
|
|
31
|
-
* token is never handed to browser JavaScript.
|
|
32
|
-
*
|
|
33
|
-
* @package @payez/next-mvp
|
|
34
|
-
*/
|
|
35
2
|
export declare function POST(req: NextRequest): Promise<NextResponse<any>>;
|
|
@@ -4,6 +4,8 @@ exports.POST = POST;
|
|
|
4
4
|
const server_1 = require("next/server");
|
|
5
5
|
const idp_fetch_1 = require("../../../lib/idp-fetch");
|
|
6
6
|
const env_1 = require("../../../config/env");
|
|
7
|
+
const idp_client_config_1 = require("../../../lib/idp-client-config");
|
|
8
|
+
const client_key_1 = require("../../../lib/client-key");
|
|
7
9
|
/**
|
|
8
10
|
* Account recovery — verify-code
|
|
9
11
|
*
|
|
@@ -37,6 +39,36 @@ const env_1 = require("../../../config/env");
|
|
|
37
39
|
*
|
|
38
40
|
* @package @payez/next-mvp
|
|
39
41
|
*/
|
|
42
|
+
/**
|
|
43
|
+
* RECOVERY CLIENT KEY — cards 268012 (S12) and 268410.
|
|
44
|
+
*
|
|
45
|
+
* These four proxies populated NONE of PayEzClientIdResolver's six slots, so
|
|
46
|
+
* RequirePayEzUnAuthorized 404'd the request BEFORE the action ran and no
|
|
47
|
+
* recovery token was ever minted. That is why S12 failed at step 1.
|
|
48
|
+
*
|
|
49
|
+
* NOT `ENV_CONFIG.CLIENT_ID`, which is what card 268012 originally prescribed.
|
|
50
|
+
* That variable is the ENVIRONMENT-SPECIFIC NUMERIC id, and the IDP accepts a
|
|
51
|
+
* numeric with NO existence check (RequirePayEzUnAuthorizedAttribute.cs:31-33)
|
|
52
|
+
* while it VALIDATES a slug (:41-47). Hardcoding the numeric here would have
|
|
53
|
+
* added four more sites to the 268410 family and made a stale id answer for
|
|
54
|
+
* the WRONG TENANT on a password-reset path, silently. Slug-preferring, with
|
|
55
|
+
* the numeric fallback logged, is the same resolver the other nine sites use.
|
|
56
|
+
*
|
|
57
|
+
* SERVER VALUE WINS: client_id is applied AFTER the caller's payload spread.
|
|
58
|
+
* These are unauthenticated recovery endpoints, so a browser-supplied
|
|
59
|
+
* client_id would be a tenant-targeting lever. The caller does not get to
|
|
60
|
+
* choose which tenant their password reset is addressed to.
|
|
61
|
+
*/
|
|
62
|
+
async function recoveryClientKey() {
|
|
63
|
+
try {
|
|
64
|
+
const config = await (0, idp_client_config_1.getIDPClientConfig)();
|
|
65
|
+
return (0, client_key_1.resolveClientKey)(config, 'account/recovery/verify-code').key;
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
console.error('[account/recovery/verify-code] could not resolve IDP client config for client_id:', e);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
40
72
|
async function POST(req) {
|
|
41
73
|
let payload = {};
|
|
42
74
|
try {
|
|
@@ -45,10 +77,11 @@ async function POST(req) {
|
|
|
45
77
|
catch {
|
|
46
78
|
return server_1.NextResponse.json({ success: false, error: 'invalid_request', message: 'A JSON body is required' }, { status: 400 });
|
|
47
79
|
}
|
|
80
|
+
const clientKey = await recoveryClientKey();
|
|
48
81
|
const result = await (0, idp_fetch_1.idpFetchJSON)(req, `${env_1.ENV_CONFIG.IDP_URL}/api/Account/recovery/verify-code`, {
|
|
49
82
|
method: 'POST',
|
|
50
|
-
headers: { 'Content-Type': 'application/json' },
|
|
51
|
-
body: JSON.stringify(payload),
|
|
83
|
+
headers: { 'Content-Type': 'application/json', ...(clientKey ? { 'X-Client-Id': clientKey } : {}) },
|
|
84
|
+
body: JSON.stringify({ ...payload, ...(clientKey ? { client_id: clientKey } : {}) }),
|
|
52
85
|
});
|
|
53
86
|
if (!result.ok) {
|
|
54
87
|
return server_1.NextResponse.json({
|