Found while binding the oauth.* return types in #14312 (PR #15445). Request side, not return side — out of that card's scope, filed rather than fixed in passing.
The defect
ObjectStackClient.oauth.applications.register declares this request type (packages/client/src/index.ts):
register: async (req: {
client_name?: string;
name?: string; // not in the vendor body schema
redirect_uris: string[];
token_endpoint_auth_method?: 'none' | 'client_secret_basic' | 'client_secret_post';
grant_types?: string[];
response_types?: string[];
client_uri?: string;
logo_uri?: string;
scope?: string;
scopes?: string[]; // not in the vendor body schema
contacts?: string[];
tos_uri?: string;
policy_uri?: string;
metadata?: Record< string, unknown >; // not in the vendor body schema
})
The route is POST /api/v1/auth/oauth2/create-client, mounted verbatim from @better-auth/oauth-provider (auth-route-ledger.ts records it source: 'better-auth'). Its body schema is a Zod object with z.core.$strip semantics, and its members are: redirect_uris, scope, client_name, client_uri, logo_uri, contacts, tos_uri, policy_uri, software_id, software_version, software_statement, post_logout_redirect_uris, backchannel_logout_uri, backchannel_logout_session_required, token_endpoint_auth_method, application_type, jwks, jwks_uri, grant_types, response_types, dpop_bound_access_tokens.
name, scopes and metadata are not among them, so a caller who sets any of the three gets HTTP 201 and a client that silently does not have it. $strip drops unknown keys rather than refusing them, so there is no error to notice.
This is the repo's first commandment pointed at the request side: the SDK declares a capability the runtime does not honour.
Measured, not inferred
Driven against a real server (real betterAuth + real oauthProvider over the real createObjectQLAdapterFactory on a real ObjectQL engine and a real better-sqlite3 database, real session cookie, real ObjectStackClient). Registering with metadata: { tenant: 'acme', tier: 7 } returned 201 and a body carrying neither key:
{"client_id":"pWhEhQXRmdLVGVZkgiobMJfxEmsgVCoB","client_secret":"EPsjhqMCoPzoNPiQdMRLDPfcUIWDnFmp","client_secret_expires_at":0,"scope":"openid profile email","user_id":"t3IvR7eGC7IHhZnZUy4ftuLgeesecJ4a","client_id_issued_at":1788534576,"client_name":"Probe App","client_uri":"``https://app.example.com","logo_uri":"https://app.example.com/logo.png","contacts":["ops@example.com"],"tos_uri":"https://app.example.com/tos","policy_uri":"https://app.example.com/privacy","redirect_uris":["https://app.example.com/cb"],"token_endpoint_auth_method":"client_secret_basic","grant_types":["authorization_code","refresh_token"],"response_types":["code"],"application_type":"web","disabled":false,"dpop_bound_access_tokens":false}``
metadata was measured directly. name and scopes rest on the body-schema reading above, not on a drive — worth confirming before acting, since the correct disposition may differ per member (name and scopes look like near-misses for the real client_name and scope, which the same type also declares; metadata has no vendor counterpart at all and the serialiser only ever spreads metadata that some other door stored).
Not decided here
Whether each member is deleted, renamed onto its real counterpart, or routed to a door that can honour it is a contract question, and the near-miss pairs (name / client_name, scopes / scope) mean deleting them changes what existing callers compile against. Left to triage.
Version measured: @better-auth/oauth-provider@1.7.2. Nothing here changes the return-type work in #14312 or its two sibling cards, which are return-side only.
Found while binding the
oauth.*return types in #14312 (PR #15445). Request side, not return side — out of that card's scope, filed rather than fixed in passing.The defect
ObjectStackClient.oauth.applications.registerdeclares this request type (packages/client/src/index.ts):The route is
POST /api/v1/auth/oauth2/create-client, mounted verbatim from@better-auth/oauth-provider(auth-route-ledger.tsrecords itsource: 'better-auth'). Its body schema is a Zod object withz.core.$stripsemantics, and its members are:redirect_uris,scope,client_name,client_uri,logo_uri,contacts,tos_uri,policy_uri,software_id,software_version,software_statement,post_logout_redirect_uris,backchannel_logout_uri,backchannel_logout_session_required,token_endpoint_auth_method,application_type,jwks,jwks_uri,grant_types,response_types,dpop_bound_access_tokens.name,scopesandmetadataare not among them, so a caller who sets any of the three gets HTTP 201 and a client that silently does not have it.$stripdrops unknown keys rather than refusing them, so there is no error to notice.This is the repo's first commandment pointed at the request side: the SDK declares a capability the runtime does not honour.
Measured, not inferred
Driven against a real server (real
betterAuth+ realoauthProviderover the realcreateObjectQLAdapterFactoryon a real ObjectQL engine and a real better-sqlite3 database, real session cookie, realObjectStackClient). Registering withmetadata: { tenant: 'acme', tier: 7 }returned 201 and a body carrying neither key:{"client_id":"pWhEhQXRmdLVGVZkgiobMJfxEmsgVCoB","client_secret":"EPsjhqMCoPzoNPiQdMRLDPfcUIWDnFmp","client_secret_expires_at":0,"scope":"openid profile email","user_id":"t3IvR7eGC7IHhZnZUy4ftuLgeesecJ4a","client_id_issued_at":1788534576,"client_name":"Probe App","client_uri":"``https://app.example.com","logo_uri":"https://app.example.com/logo.png","contacts":["ops@example.com"],"tos_uri":"https://app.example.com/tos","policy_uri":"https://app.example.com/privacy","redirect_uris":["https://app.example.com/cb"],"token_endpoint_auth_method":"client_secret_basic","grant_types":["authorization_code","refresh_token"],"response_types":["code"],"application_type":"web","disabled":false,"dpop_bound_access_tokens":false}``metadatawas measured directly.nameandscopesrest on the body-schema reading above, not on a drive — worth confirming before acting, since the correct disposition may differ per member (nameandscopeslook like near-misses for the realclient_nameandscope, which the same type also declares;metadatahas no vendor counterpart at all and the serialiser only ever spreads metadata that some other door stored).Not decided here
Whether each member is deleted, renamed onto its real counterpart, or routed to a door that can honour it is a contract question, and the near-miss pairs (
name/client_name,scopes/scope) mean deleting them changes what existing callers compile against. Left to triage.Version measured:
@better-auth/oauth-provider@1.7.2. Nothing here changes the return-type work in #14312 or its two sibling cards, which are return-side only.