fix(invite-claim): send publicKey + JSON error responses

The /join and OIDC accept pages were calling /api/register/complete without
the public key extracted from the WebAuthn attestation. storeCredential writes
a credentials row with a NOT NULL public_key column, so the request crashed
with an unhandled exception and Hono replied "Internal Server Error" as plain
text — the client JSON.parse choked on it.

- Extract credential.response.getPublicKey() on the client and include it in
  the completion payload (both /join and /oidc/accept flows).
- Add app.onError to return JSON 500s on /api/* routes so any future crashes
  produce parseable error bodies instead of cascading into confusing "Unexpected
  token 'I'" errors in the browser.
This commit is contained in:
Jeff Emmett 2026-04-17 10:09:52 -04:00
parent 0a896f5740
commit 5bb46afe6d
1 changed files with 20 additions and 0 deletions

View File

@ -484,6 +484,18 @@ app.use('*', cors({
credentials: true,
}));
// Return JSON for unhandled exceptions on JSON API routes so client-side
// `await res.json()` doesn't choke on plain-text "Internal Server Error".
app.onError((err, c) => {
const path = c.req.path;
console.error(`EncryptID: ${c.req.method} ${path} failed:`, err);
const wantsJson = path.startsWith('/api/') || (c.req.header('accept') || '').includes('application/json');
if (wantsJson) {
return c.json({ error: 'Internal server error', detail: err.message }, 500);
}
return c.text('Internal Server Error', 500);
});
// /api/internal/* is for service-to-service calls over the Docker network only.
// Traefik adds X-Forwarded-For for any request arriving through the public edge
// (CF tunnel, direct HTTPS). Direct container-to-container fetches do not set it.
@ -6385,6 +6397,11 @@ function joinPage(token: string): string {
.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');
const clientDataJSON = btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON)))
.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');
const publicKeyBytes = credential.response.getPublicKey ? credential.response.getPublicKey() : null;
const publicKey = publicKeyBytes
? btoa(String.fromCharCode(...new Uint8Array(publicKeyBytes)))
.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '')
: '';
const completeRes = await fetch('/api/register/complete', {
method: 'POST',
@ -6396,6 +6413,7 @@ function joinPage(token: string): string {
deviceName: detectDeviceName(),
credential: {
credentialId,
publicKey,
attestationObject,
clientDataJSON,
transports: credential.response.getTransports ? credential.response.getTransports() : [],
@ -6728,6 +6746,7 @@ function oidcAcceptPage(token: string): string {
});
showStatus('Completing registration...');
const publicKeyBytes = credential.response.getPublicKey ? credential.response.getPublicKey() : null;
const completeRes = await fetch('/api/register/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@ -6738,6 +6757,7 @@ function oidcAcceptPage(token: string): string {
deviceName: detectDeviceName(),
credential: {
credentialId: toB64url(credential.rawId),
publicKey: publicKeyBytes ? toB64url(publicKeyBytes) : '',
attestationObject: toB64url(credential.response.attestationObject),
clientDataJSON: toB64url(credential.response.clientDataJSON),
transports: credential.response.getTransports ? credential.response.getTransports() : [],