Filed by the os-dev seat while delivering #15447 (PR #17209), on the dispatching domain:cli seat's instruction to file this separately rather than widen that PR. ⛔ Filed unassigned and unlabelled; grading is triage's.
The asymmetry
POST /api/v1/auth/sys-oauth-application/register is an ObjectStack-owned mount in packages/plugins/plugin-auth/src/auth-plugin.ts: a session-required self-service wrapper over the vendor's /oauth2/create-client. It reads a body member spelled exactly name and maps it onto client_name, and it splits the Console's newline-separated redirect-URL textarea into the redirect_uris array the vendor schema requires.
No client SDK method builds that URL. The only caller is the Console's sys_oauth_application create action, which posts it directly. So a capability the platform implements over HTTP — registering an OAuth client with a plain name, and with the redirect URLs given as text rather than pre-split — is reachable from the Console and from nothing an @objectstack/client caller can write.
The SDK's own door, oauth.applications.register, posts to the vendor route instead, which honours neither spelling: it strips name and requires redirect_uris already split.
Driven, not inferred
From the measurement round on #15447 (comment 5559384773 — real betterAuth plus real oauthProvider over the real ObjectQL engine on a real TCP socket, driven through the real ObjectStackClient):
POST /api/v1/auth/sys-oauth-application/register with { name: 'WRAPPER-NAME-15447', redirectURLs: '...', type: 'web' } answered 200, the response carried client_name: "WRAPPER-NAME-15447", and the sys_oauth_application row's name column was set.
- The same spelling posted through
oauth.applications.register to /oauth2/create-client answered 201 with the value stripped — absent from the response, absent from oauth.applications.get, absent from oauth.applications.list, null in the DB column.
PR #17209 removes name from the SDK request type, because on the route that method posts to the member was never honoured and keeping it kept a silent trap alive. That removal makes this gap visible rather than creating it: the spelling the platform does honour is now honoured only somewhere an SDK caller cannot reach.
⚠️ This is a declared disposition, not an undeclared hole — read before grading
The route ledger already carries the row, and it already says this in as many words (packages/plugins/plugin-auth/src/auth-route-ledger.ts):
{ route: 'POST /api/v1/auth/sys-oauth-application/register', family: 'objectstack-mount', source: 'objectstack', disposition: 'server-only',
note: 'no SDK method builds this URL — the sys_oauth_application create action posts it directly; session-required self-service wrapper over better-auth /oauth2/create-client that splits the Console\'s newline-separated redirect-URL textarea into the redirect_uris array the vendor schema requires' }
So nothing here is drifting and no gate is blind: server-only is a real, machine-checked disposition and the note states the reason. What this card asks is therefore a capability question, not a defect report — should this mount be rebooked sdk and gain a method, or does it stay Console-only by design? Graded as a defect it would be over-graded; graded as an observation it would lose the driven asymmetry above.
Precedent for the shape and for the answer being "yes, sometimes": #10975 asked exactly this of another ObjectStack mount (POST /api/v1/auth/set-initial-password, ledgered as an sdk objectstack mount). admin/oauth2/toggle-disabled is a second ObjectStack oauth mount in the same family.
What a fix would have to decide
- Whether the SDK should express a door whose purpose is to serve a Console form — the textarea split is UI-shaped, and an SDK caller already holds an array.
- If yes, whether the method lives beside
oauth.applications.register (two registration methods on one namespace, which is its own trap shape) or is named for the wrapper it is.
- The ledger row moves from
server-only to sdk with a client: entry in the same change, and pnpm check:auth-mount-ledger / pnpm check:route-ledger-census are what hold that honest.
⛔ Not in scope for PR #17209, which is a pull-back of a request type and adds nothing.
Generated by Claude Code
Filed by the
os-devseat while delivering #15447 (PR #17209), on the dispatchingdomain:cliseat's instruction to file this separately rather than widen that PR. ⛔ Filed unassigned and unlabelled; grading is triage's.The asymmetry
POST /api/v1/auth/sys-oauth-application/registeris an ObjectStack-owned mount inpackages/plugins/plugin-auth/src/auth-plugin.ts: a session-required self-service wrapper over the vendor's/oauth2/create-client. It reads a body member spelled exactlynameand maps it ontoclient_name, and it splits the Console's newline-separated redirect-URL textarea into theredirect_urisarray the vendor schema requires.No client SDK method builds that URL. The only caller is the Console's
sys_oauth_applicationcreate action, which posts it directly. So a capability the platform implements over HTTP — registering an OAuth client with a plainname, and with the redirect URLs given as text rather than pre-split — is reachable from the Console and from nothing an@objectstack/clientcaller can write.The SDK's own door,
oauth.applications.register, posts to the vendor route instead, which honours neither spelling: it stripsnameand requiresredirect_urisalready split.Driven, not inferred
From the measurement round on #15447 (comment 5559384773 — real
betterAuthplus realoauthProviderover the real ObjectQL engine on a real TCP socket, driven through the realObjectStackClient):POST /api/v1/auth/sys-oauth-application/registerwith{ name: 'WRAPPER-NAME-15447', redirectURLs: '...', type: 'web' }answered 200, the response carriedclient_name: "WRAPPER-NAME-15447", and thesys_oauth_applicationrow'snamecolumn was set.oauth.applications.registerto/oauth2/create-clientanswered 201 with the value stripped — absent from the response, absent fromoauth.applications.get, absent fromoauth.applications.list,nullin the DB column.PR #17209 removes
namefrom the SDK request type, because on the route that method posts to the member was never honoured and keeping it kept a silent trap alive. That removal makes this gap visible rather than creating it: the spelling the platform does honour is now honoured only somewhere an SDK caller cannot reach.The route ledger already carries the row, and it already says this in as many words (
packages/plugins/plugin-auth/src/auth-route-ledger.ts):So nothing here is drifting and no gate is blind:
server-onlyis a real, machine-checked disposition and the note states the reason. What this card asks is therefore a capability question, not a defect report — should this mount be rebookedsdkand gain a method, or does it stay Console-only by design? Graded as a defect it would be over-graded; graded as an observation it would lose the driven asymmetry above.Precedent for the shape and for the answer being "yes, sometimes": #10975 asked exactly this of another ObjectStack mount (
POST /api/v1/auth/set-initial-password, ledgered as ansdkobjectstack mount).admin/oauth2/toggle-disabledis a second ObjectStack oauth mount in the same family.What a fix would have to decide
oauth.applications.register(two registration methods on one namespace, which is its own trap shape) or is named for the wrapper it is.server-onlytosdkwith aclient:entry in the same change, andpnpm check:auth-mount-ledger/pnpm check:route-ledger-censusare what hold that honest.⛔ Not in scope for PR #17209, which is a pull-back of a request type and adds nothing.
Generated by Claude Code