mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
fix: 4 boot-time noise + correctness bugs surfaced by post-deploy smoke
All four were pre-existing; the audit smoke-test made them visible. Fixed
together because they share a "boot console-warn cleanup" theme.
1. streaks ensureSeeded race (DexieError2 ×2)
- Two boot-time liveQuery callers passed the `count > 0` check before
either had written, then the second's `.add()` hit a ConstraintError.
- Fix: cache the seed promise per module, run the existence check +
bulkAdd inside one Dexie RW transaction, and only insert MISSING
defs (preserves existing currentStreak/longestStreak counts).
2. encryptRecord('agents', …) "wrong table name?" warning
- The DEV-only check fired whenever a record carried none of the
registered encrypted fields, regardless of whether anything could
actually leak. `ensureDefaultAgent` writes a fresh agent row before
`systemPrompt` / `memory` exist — pure noise.
- Fix: drop the "no fields at all" branch. Keep the case-mismatch
branch (the branch that actually catches silent plaintext leaks).
3. Passkey signInWithPasskey "Cannot read properties of undefined
(reading 'allowCredentials')"
- Client destructured `{ options, challengeId }` from the server's
options response, but Better-Auth's `@better-auth/passkey` plugin
returns the raw PublicKeyCredentialRequestOptionsJSON (no
envelope) and tracks the challenge in a signed cookie. Both
`options` and `challengeId` came back undefined; SimpleWebAuthn
blew up the moment it tried to read the request shape. Verify body
`{ challengeId, credential }` was likewise wrong — Better-Auth
wants `{ response }`.
- Fix: align both register and authenticate flows with Better-Auth's
native shape on options + verify, and add `credentials: 'include'`
on every fetch so the challenge cookie actually round-trips.
Server's verify proxy now reads `parsed?.response?.id` for
credentialID rate-limiting.
4. /api/v1/me/onboarding/ → 404
- Hono's nested router (`app.route(prefix, sub)` + inner
`app.get('/')`) matches the prefix-without-slash form only. The
onboarding-status store sent the request with a trailing slash, so
every login produced a 404 + a console warn.
- Fix: client sends the path without trailing slash; mana-auth picks
up `hono/trailing-slash` middleware as defense-in-depth so a future
accidental trailing slash on any /me/* route 301-redirects instead
of 404-ing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
44f9155ed3
commit
0c30a16eb5
6 changed files with 106 additions and 55 deletions
|
|
@ -429,7 +429,16 @@ export function createAuthService(config: AuthServiceConfig): AuthServiceInterfa
|
|||
},
|
||||
|
||||
/**
|
||||
* Register a new passkey for the current user
|
||||
* Register a new passkey for the current user.
|
||||
*
|
||||
* The shape of the server's options response and the verify
|
||||
* request body match Better-Auth's `@better-auth/passkey` plugin
|
||||
* exactly: the options endpoint returns the raw
|
||||
* PublicKeyCredentialCreationOptionsJSON (no envelope), and the
|
||||
* verify endpoint accepts `{ response, name? }`. The challenge
|
||||
* is carried in a server-set signed cookie — that's why every
|
||||
* fetch in the flow MUST send `credentials: 'include'` so the
|
||||
* cookie survives the round-trip.
|
||||
*/
|
||||
async registerPasskey(friendlyName?: string): Promise<AuthResult> {
|
||||
try {
|
||||
|
|
@ -440,6 +449,7 @@ export function createAuthService(config: AuthServiceConfig): AuthServiceInterfa
|
|||
// Step 1: Get registration options from server
|
||||
const optionsRes = await fetch(`${baseUrl}${endpoints.passkeyRegisterOptions}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
|
|
@ -451,19 +461,22 @@ export function createAuthService(config: AuthServiceConfig): AuthServiceInterfa
|
|||
return { success: false, error: err.message || 'Failed to get registration options' };
|
||||
}
|
||||
|
||||
const { options, challengeId } = await optionsRes.json();
|
||||
const webauthnOptions = await optionsRes.json();
|
||||
|
||||
// Step 2: Create credential via browser WebAuthn API
|
||||
const credential = await startRegistration({ optionsJSON: options });
|
||||
const credential = await startRegistration({ optionsJSON: webauthnOptions });
|
||||
|
||||
// Step 3: Send credential to server for verification
|
||||
// Step 3: Send credential to server for verification.
|
||||
// `name` is the Better-Auth parameter name for the
|
||||
// passkey label; `response` is the credential payload.
|
||||
const verifyRes = await fetch(`${baseUrl}${endpoints.passkeyRegisterVerify}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
},
|
||||
body: JSON.stringify({ challengeId, credential, friendlyName }),
|
||||
body: JSON.stringify({ response: credential, name: friendlyName }),
|
||||
});
|
||||
|
||||
if (!verifyRes.ok) {
|
||||
|
|
@ -493,6 +506,13 @@ export function createAuthService(config: AuthServiceConfig): AuthServiceInterfa
|
|||
* where the browser surfaces passkeys directly inside the email autofill
|
||||
* dropdown instead of opening a modal. The host MUST verify
|
||||
* `PublicKeyCredential.isConditionalMediationAvailable()` first.
|
||||
*
|
||||
* Server / client shape matches Better-Auth's `@better-auth/passkey`
|
||||
* plugin exactly: options endpoint returns the raw
|
||||
* PublicKeyCredentialRequestOptionsJSON (no envelope), verify endpoint
|
||||
* accepts `{ response: credential }`. The challenge lives in a signed
|
||||
* cookie set by the server, so every fetch MUST send `credentials:
|
||||
* 'include'` for the cookie to round-trip.
|
||||
*/
|
||||
async signInWithPasskey(options: { conditional?: boolean } = {}): Promise<AuthResult> {
|
||||
try {
|
||||
|
|
@ -502,6 +522,7 @@ export function createAuthService(config: AuthServiceConfig): AuthServiceInterfa
|
|||
// Step 1: Get authentication options from server
|
||||
const optionsRes = await fetch(`${baseUrl}${endpoints.passkeyAuthOptions}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
|
|
@ -518,7 +539,7 @@ export function createAuthService(config: AuthServiceConfig): AuthServiceInterfa
|
|||
};
|
||||
}
|
||||
|
||||
const { options: webauthnOptions, challengeId } = await optionsRes.json();
|
||||
const webauthnOptions = await optionsRes.json();
|
||||
|
||||
// Step 2: Authenticate via browser WebAuthn API
|
||||
const credential = await startAuthentication({
|
||||
|
|
@ -526,11 +547,14 @@ export function createAuthService(config: AuthServiceConfig): AuthServiceInterfa
|
|||
useBrowserAutofill: options.conditional === true,
|
||||
});
|
||||
|
||||
// Step 3: Send credential to server for verification
|
||||
// Step 3: Send credential to server for verification.
|
||||
// Better-Auth expects `{ response: credential }` — the
|
||||
// challenge is read from the signed cookie, not the body.
|
||||
const verifyRes = await fetch(`${baseUrl}${endpoints.passkeyAuthVerify}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ challengeId, credential }),
|
||||
body: JSON.stringify({ response: credential }),
|
||||
});
|
||||
|
||||
if (!verifyRes.ok) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue