fix: OAuth client API returned client_id: undefined; unknown id 500'd
The ORM Model.toJSON() serializes only schema fields, so the mapped client_id/scopes/redirect_uris/... that OAuthClient.get() attaches to the wrapped Resource were stripped from GET /api/oauth/client[/:id] responses. client_id came back undefined; the theta-env bootstrap then POSTed /api/oauth/client/undefined/rotate and got a 500, aborting stack bring-up whenever proxy-secrets.js lacked a usable secret. - OAuthClient.get() now emits an explicit public toJSON (client_id, name, slug, scopes, redirect_uris, allowed_groups, token_lifetime, is_valid), deliberately omitting client_secret_hash so it can't leak over the API. - OAuthClient.get() null-guards Resource.get() (which returns null, not throws) and returns a clean 404 for an unknown/undefined id instead of crashing on r.kind. - Regression tests: list/get expose client_id + hide the secret hash, the list-then-rotate bootstrap path, and unknown-id -> 4xx not 500. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -44,13 +44,21 @@ class OAuthClient {
|
||||
return r;
|
||||
}
|
||||
static async get(client_id) {
|
||||
const notFound = () => {
|
||||
const e = new Error('OAuthClient not found');
|
||||
e.status = 404;
|
||||
return e;
|
||||
};
|
||||
let r;
|
||||
try {
|
||||
r = await Resource.get(client_id);
|
||||
} catch (_) {
|
||||
throw new Error('OAuthClient not found');
|
||||
throw notFound();
|
||||
}
|
||||
if (r.kind !== 'oauth') throw new Error('OAuthClient not found');
|
||||
// Resource.get() returns null (does not throw) for a missing id —
|
||||
// guard it so a bad/undefined client_id is a clean 404, not a
|
||||
// "Cannot read properties of null (reading 'kind')" 500.
|
||||
if (!r || r.kind !== 'oauth') throw notFound();
|
||||
// Map metadata to top-level properties to satisfy routes/oauth.js without rewriting it
|
||||
r.client_id = r.id;
|
||||
r.client_secret_hash = r.metadata.client_secret_hash;
|
||||
@@ -69,6 +77,29 @@ class OAuthClient {
|
||||
return raw_secret;
|
||||
};
|
||||
|
||||
// The ORM Model.toJSON() only serializes schema fields, so the mapped
|
||||
// properties above (client_id, scopes, redirect_uris, …) would be
|
||||
// stripped from any res.json() — that's why GET /api/oauth/client
|
||||
// returned client_id: undefined and the bootstrap's rotate blew up.
|
||||
// Emit the public shape explicitly. client_secret_hash is deliberately
|
||||
// omitted so it never leaks over the API.
|
||||
r.toJSON = function () {
|
||||
return {
|
||||
client_id: r.id,
|
||||
id: r.id,
|
||||
kind: r.kind,
|
||||
name: r.name,
|
||||
slug: r.slug,
|
||||
owner: r.owner,
|
||||
description: r.description,
|
||||
redirect_uris: r.redirect_uris,
|
||||
scopes: r.scopes,
|
||||
allowed_groups: r.allowed_groups,
|
||||
token_lifetime: r.token_lifetime,
|
||||
is_valid: r.is_valid,
|
||||
};
|
||||
};
|
||||
|
||||
// proxy update to handle metadata correctly
|
||||
const originalUpdate = r.update.bind(r);
|
||||
r.update = async (data) => {
|
||||
|
||||
Reference in New Issue
Block a user