feat(client): carry declared auth end to end, from security schemes to client capabilities - #58
Merged
Merged
Conversation
…introspected security schemes The parameter switch in convertOperation only handled path/query/header, so any `in: cookie` parameter vanished silently on the way into the IR. Add a cookie case plus a default arm that reports an unrecognized location via spec.Warnings instead of dropping it. Also sort extractFromOpenAPI's spec.Security by Key in introspector.go, matching the sort spec_parser.go already carries: both range the same kind of map, so a client built from a live router had the same nondeterministic AuthConfig field order the parser was already fixed for.
…der too operationToEndpoint has its own path/query/header parameter switch, separate from spec_parser.go's convertOperation, with the same gap: no cookie case, unknown locations dropped silently. A client generated by introspecting a live router lost cookie parameters that a client built from a parsed OpenAPI file would keep -- a distinction no caller of the generator can see coming. Fix it the same way: add the cookie case and report unrecognized locations via spec.Warnings instead of dropping them.
…d all-digit regression tests
…n location AuthConfig used to carry a single BearerToken/APIKey pair and addAuth hardcoded X-API-Key regardless of what the spec declared. generateAuthConfig now emits one field per security scheme (named after the scheme key) and generateAuthApply emits a single apply(header, url) that routes each credential to its declared location: Authorization for bearer/basic/oauth2, the declared header/cookie/query name for apiKey. websocket.go's handshake path now goes through the same apply instead of its own bearer-only check, so REST and WebSocket auth can't drift apart again.
Case-insensitive matching would skip a distinct, legal Go field (ApiKey vs APIKEY compile fine side by side) and silently drop a declared credential, which is worse than the confusing-but-harmless pair it was avoiding. Reverted resolveAuthFields to exact-string matching and fixed the test fixture to use two keys that actually collide (api_key and api-key both derive to ApiKey). Also documents the websocket.go apply(header, nil) gap: apiKey-in-query schemes aren't applied to the WS handshake yet since there's no *url.URL to hand it from a string-typed url var; Task 5 owns wiring the real one through.
WebSocket applied auth with a literal nil *url.URL, so a query-located scheme never reached the handshake; WebTransport never called apply at all despite already carrying an AuthConfig. Both now route through the same AuthConfig.apply as REST and SSE, with the WebSocket dial also threading the client's cookie jar so session auth survives the upgrade. WithCookieJar and WithSessionJar are new, unconditional client options so a generated client can hold and replay a session cookie even when the login endpoint that sets it isn't itself in securitySchemes.
Five pre-existing bugs kept the generated package from building: dead imports in client.go, rest.go, and types.go; an undefined AuthConfig reference in client.go, websocket.go, and webtransport.go whenever a spec declares no security scheme; and a swapped return-value order on the WebTransport dial call, verified against the real signature in both v0.6.0 (what this generator's own go.mod pins) and v0.12.0 (what this repo pins). All five predate this branch. Fixed each by computing the affected import or field from the generated body's actual content rather than a hand-maintained flag, so none of it can drift out of sync again. A go/parser syntax check alone would have passed on every one of these, since none is a syntax error, so generator_test.go also gained an AST-based unused-import gate plus two targeted tests for the two defects that gate still can't see on its own: an undefined identifier, and a call whose arguments still parse in the wrong order.
…lared Carries client.SecurityScheme (Key, ParamName, Type, In, Scheme) into the TypeScript manifest: a normalized securitySchemes table emitted once, and each operation naming its schemes by key in a sorted security array. The runtime's OperationMeta.security field (transport.ts) has waited for a producer since it was declared; this is it. Both the table and the per-operation field are omitted entirely when empty, matching how entities/provides/invalidates already treat their own bundle weight. Per-operation scheme keys are deduped and sorted for determinism, since Endpoint.Security carries no ordering guarantee the way spec.Security does. Also strengthens the with-auth capability fixture, whose sessionAuth scheme had Key and ParamName set to the same string by an earlier task -- which could no longer prove this change reads ParamName rather than re-emitting Key -- and updates the one byte-pinned golden test whose expected output this change legitimately alters.
…lagged operationSecurityKeys' dedup branch (a scheme repeated across two OR-alternatives with different scopes) was reachable but untested -- the capabilitySpec fixture cited as covering it actually only exercised the union-and-sort path across two DIFFERENT scheme names. Also add a per-operation omission check (a secured and an unsecured op isolated by a new opBlock helper, rather than a whole-file substring check that either could satisfy) and an explicit assertion that the http scheme's 'scheme' field is emitted as a string, not just type-checked through the tsc gate.
…ange Adds the end-to-end regression test proving an apiKey-in-cookie scheme survives from an OpenAPI document all the way to a generated Go client that actually sends the cookie, and documents the new AuthConfig shape, WithCookieJar/WithSessionJar, the TypeScript securitySchemes table, and the credentials option for cross-origin sessions.
…d auth
Six issues only showed up once every task landed together. Multiple cookie
schemes collapsed to one under a jar, because net/http.Request.AddCookie reads
Header.Get("Cookie") (first value only) then Header.Set (replaces the whole
slice) - so apply() now merges into the existing Cookie value instead of
Add-ing a second one. WebSocket's handshake had the same problem one layer up:
gorilla copies the caller's header over the jar's cookies, so Connect now
seeds header from the jar before calling apply, and apply's merge appends to
that instead of wiping it out.
An unhandled scheme type or http scheme (digest, mutualTLS - both legal and
unvalidated on the way in from spec_parser.go/introspector.go) used to get
consumed by resolveAuthFields and then silently emit nothing; both switches in
auth.go now have a default, and generateAuthConfig warns with the scheme's key
and type/scheme. A non-auth in: cookie parameter had the same silent-drop
problem one layer further down - Endpoint.CookieParams already lands from both
IR builders, but nothing warned that the generator doesn't emit it, so
Generate now does.
Also closed two test gaps: the WebSocket routing assertion could pass even if
Connect stopped calling apply entirely (webtransport got a targeted version of
this after an earlier review; websocket didn't), and nothing asserted the
username:password separator survives into the generated Basic header.
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
Conventional Commits ValidationPR Title: valid |
Six defects, all of them shapes the generator emits happily and the Go compiler rejects. None of them were reachable from the fixtures the parse and unused-import gates already ran, which is why they survived. websocket.go, sse.go and webtransport.go each emitted an import with no user left once a streaming endpoint declared no payload schema, or once reconnection was off. Those imports are now conditional on the thing that uses them. Extending the unused-import matrix with a schema-less spec is what turned them into failures first, and it found a fifth the brief did not list: websocket.go's "time". webtransport.go spliced a rendered type name into a single-line doc comment. An inline object schema renders as a multi-line anonymous struct, so the struct body's newlines escaped the comment and the rest landed at top level as stray tokens. docTypeName falls back to "message" when a rendering has no single line, and the signature on the next line still carries the full type. The go.mod the generator writes did not require webtransport-go at all, so a generated client importing it had nothing to resolve. That require block is now rendered from getDependencies rather than listed a second time, which is what stops the two drifting again. getDependencies pinned v0.6.0 while the emitted code targeted a later API. It pins v0.12.0 now, matching what this repo uses. That rename is Dialer to Transport, and the same version bump turns Stream and SendStream into structs with pointer receivers, so those fields are pointers too. That last pair is the reason for the new gate. TestGoGeneratorGeneratedModuleBuilds writes a generated client into a temp module and runs a real go build, because an undefined symbol and an unresolvable module are both valid syntax with no unused imports and only a compiler finds them. It skips rather than fails when the environment cannot support the build: under -short, with no go on PATH, or when the module cache cannot resolve the dependencies offline. Compile errors are never skipped. Also lifts ConnectionState and the reconnect helpers out of websocket.go into streaming.go. sse.go used all four, but only websocket.go declared them, so an SSE-only spec generated a client naming identifiers nothing had defined.
Entity garbage collection, container identity across a refetch, a WebTransport adapter, hydration for Vue and Angular, and a streamed SSR payload. Each one was written down as deliberately-left rather than overlooked, so the README's gap list shrinks by the same six. A refetch used to hand back a new root even when no record moved. The records survive one on their own, because a deep-equal write is not a write, but the skeleton does not: a refetch builds a second one and the container memos are keyed by node identity. read() now takes the previous value and reuses a container whose children are all identical to it. The comparison is shallow and bottom-up, so by the time a container is checked its children have already settled their own identity and === per child is the whole answer. A read stays proportional to the skeleton rather than to the response. That changes what useQuery hands you, so the React test asserting the old contract now asserts its converse as well. collect() drops every record no cached query and no pending overlay can reach, and it hangs off the existing LRU reap, which is the only moment entities can have become unreachable. Reachability is recomputed rather than refcounted: a count would need maintaining at every write, eviction, frame and rollback, and the one place it went wrong would free a record something still points at. Tombstones are now bounded by when they stop mattering. The cache tells the store the frame reading of the oldest request still outstanding and every stamp at or below it goes, so the 256 cap is a backstop rather than the mechanism. A dispatch stays registered until its response has committed, not until it arrives. racedSince is the reader a tombstone exists for, and retiring earlier lets a response expire the stamp it is about to consult, undoing the delete it straddles by its own arrival. webTransportConnection is not the four-line literal the gap claimed. A WebSocket hands you onMessage as a callback; datagrams arrive as a ReadableStream, so it is a pull loop with a reader to release and a decode that can fail one packet at a time. You pass a session that is already open, so this package still opens nothing. hydrateBoundary carries the two things every framework needs around hydrate: walk a payload once per cache, and decide which refusals a page survives. It lives in core because three copies of that second rule are three security postures waiting to drift. React collapses onto it, Vue gets the same component, and Angular gets a provider instead. Its content model is why: a component wrapping ng-content does not own its children, so injectQuery has already run by the time the wrapper's constructor does, and the one guarantee a boundary owes is the one that shape cannot make. streamingDehydrator emits the difference since the last flush. Calling dehydrate per boundary works and also re-sends the whole cache each time, so a page with ten boundaries ships its first query's records ten times. A record goes again when its version moves; everything else goes once. Three gaps stay, and the list says why. Per-field frame stamps need the server to stamp fields. Denormalized entity cycles and negative zero both need an encoding that is not JSON.
Contributor
Conventional Commits ValidationPR Title: valid |
The pattern was scoped to extensions/, for the Tailwind builds. That left the client packages and an npm install at the repo root turning up in every `git status`, one careless `git add .` away from being committed. A bare directory pattern matches at every level, so one line covers all three. Nothing under node_modules was tracked, so the ignore takes effect immediately.
Contributor
Conventional Commits ValidationPR Title: valid |
…g work 4506389 adds ~709 lines to client-core/src (Angular SSR hydration, cache, store and stream changes), which pushed two budgets over: entity store 2.17 kB -> measured 2.32 kB query engine and REST transport 8.5 kB -> measured 8.83 kB Raised to 2.4 kB and 8.9 kB, and the README table now shows measured figures rather than stale ones. The growth is deliberate feature work, which is what these budgets are meant to let through; they exist to catch the growth nobody intended. Worth watching: the two application-facing budgets did not move but are close now. core, REST only is 8.88 kB against 9 kB and core with streams is 13.73 kB against 14 kB, so roughly 120 B and 270 B of headroom respectively.
routeToEndpoint runs whenever router.OpenAPISpec() returns nil, and it never set endpoint.Authorization, so an app with OpenAPI disabled generated a client with roles and permissions silently missing: CollectRoles and CollectPermissions came back empty and the Role and Permission unions disappeared with no warning. Read auth.roles and auth.permissions the same way auth.providers already is, and produce the exact same shape resolveEndpointAuthz does for the OpenAPI path.
…ilities TypeScript's canCall and missingCapabilities have existed since roles and permissions landed there. Go only had Can, HasRole and HasPermission, so a Go service calling another Go service had no way to ask "would this call be refused" -- exactly the cross-language drift the shared collectors in internal/client/auth.go exist to prevent. Adds OperationName, a per-operation requirement table, and CanCall / MissingCapabilities on *Client, gated on the spec having REST endpoints the same way the TypeScript generator gates its equivalent. CanCall mirrors writeOperationPredicates' semantics deliberately: scopes ALL-of within an alternative and ORed across alternatives, roles ANY-of, permissions ALL-of, and an operation absent from the table is callable. Operation identifiers route through the same capabilityIdent/ resolveCapabilityConsts collision-warning path the other three unions already use, rather than a second silent-skip. Covered by emission tests, an omitted-when-no-endpoints test, the existing go-build gate extended to this shape, and a runtime test that builds and runs the generated code against a real Principal to check the ANY-of/ALL-of semantics rather than only the source text.
…enerated comments Neither function exists. The real options are WithAnyRole and WithAllPermissions, and WithRequiredRoles is exactly the and/or-hiding name the design doc rejected, so the old comment text taught readers the wrong API twice over -- and it ships inside every generated client, both languages.
…ything
Declaring either option just records a static requirement on the
route's (or group's) metadata. Nothing is enforced until that
requirement reaches MiddlewareWithRequirement, and the old wording
("requires the authenticated subject to hold...") read as though the
option itself did the checking.
…ueStrings WithGroupAnyRole and WithGroupAllPermissions are shipped public options with no test anywhere; add the group-level mirrors of the existing route-level metadata tests, including composition with WithGroupAuth. sortedUniqueStrings was only ever exercised with []string literals through processAuthzRequirements' tests. Add direct cases for []any of strings, []any with non-strings filtered out, a wholly wrong type, and nil, and assert the empty case is nil rather than just length zero -- the caller's omit-the-key logic depends on that distinction.
…gate TestCapabilitiesNeededForRoleOrPermissionAloneCoversTheWidenedGate claimed to cover role-or-permission-alone but only ever set Roles, so a regression dropping CollectPermissions from the OR would still pass under this test's name. Add the permissions-only fixture alongside the existing role-only one.
"auth.scopes" meant two opposite things. As route metadata it is the scopes a route REQUIRES, written by WithRequiredAuth and WithGroupRequiredScopes and read by the OpenAPI and client generators. As a context key it was the scopes the subject HOLDS, read by RequireScopes and RequireAnyScope. Different maps, so nothing ever collided at runtime, but one literal covering both requirement and possession is a trap for whoever reads it next. Scopes now get the same split roles got: the context key becomes "auth.subject.scopes" and the route metadata key stays exactly as it is. Nothing in this repository published subject scopes into the context before now, so those two interceptors were reading a key only host applications could have set. MiddlewareWithRequirement publishes authCtx.Scopes alongside the roles it already published, above the IsEmpty return so an authenticate-only route still fills it. The legacy "auth.scopes" read survives as a deprecated fallback naming its replacement, and both interceptors go through one subjectScopes helper so the deprecation note lives in one place. Tests cover the new key, the fallback and the deny path for both interceptors, plus the producer side in the auth registry. The new key wins when both are set, which each interceptor now asserts.
EndpointCapabilities hands back sorted, deduplicated scope alternatives.
EndpointAuthorization handed back whatever the endpoint happened to
hold. Every generator reads both, so that asymmetry left each language
to normalise roles and permissions for itself, and only TypeScript did.
No production spec reached the gap. resolveEndpointAuthz and
routeToEndpoint both sort, deduplicate and drop empties before an
Endpoint is built. A hand-built one does not, and an Endpoint carrying
Roles: []string{""} produced two different tables: TypeScript re-sorted
and dropped the empty, Go rendered it verbatim. An empty role is a role
no principal can hold, so Go's CanCall answered false for that operation
forever while TypeScript's canCall answered true for the same principal.
Normalise in EndpointAuthorization instead, reusing sortedUniqueScopes,
which authz.go and introspector.go already apply to roles and
permissions. It returns a copy rather than the endpoint's own pointer so
no caller can reach past it for the raw slices. The TypeScript generator
keeps its sortedUniqueStrings call as an order-determinism belt, but the
comment above it no longer claims to be the guard against divergence.
The new test drives one deliberately unnormalised Endpoint through both
generators and compares the two emitted tables against each other rather
than against a literal. An expectation pinned to Go's output would have
passed before this fix as happily as after it.
The Playwright MCP server writes console logs, traces and downloads into .playwright-mcp/ beside whatever directory it started from, which here is the repository root, and screenshots land next to it as loose PNGs named for the page and viewport width. The PNG rule is anchored with a leading slash. Every image this repository ships lives under docs/, so an unanchored *.png would have ignored docs/public/banner.png and the app icons along with the screenshot.
Contributor
Conventional Commits ValidationPR Title: valid |
Contributor
Conventional Commits ValidationPR Title: valid |
Contributor
Conventional Commits ValidationPR Title: valid |
Contributor
Conventional Commits ValidationPR Title: valid |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This started as one wrong line in
spec_parser.goand grew into the whole path an auth requirement travels: what your service declares, what the router writes into the OpenAPI document, and what a generated client can do with it once it gets there. Authentication is the first half. Authorization is the second, and it is new here.The bug it started as
The generated Go client could not do cookie or session auth. It turns out it could not do much else either: it sent
X-API-Keyfor every apiKey scheme whatever the document declared, ignored query and cookie locations entirely, declared basic-auth fields that nothing read, and had no cookie jar, so a login-then-use flow was impossible.One line caused most of it.
spec_parser.gobuilt the client IR like this:Nameon the source type is the wire parameter name, thesession_idin{type: apiKey, in: cookie, name: session_id}. The parser overwrote it with the map key and dropped it, and the IR had oneNamefield for two different concepts, so there was nowhere to put it even if the parser had wanted to keep it. With nothing to emit, the generator hardcoded.The server side was fine the whole time.
APIKeyProvidertakesWithAPIKeyCookie(name)and emits{apiKey, cookie, <name>}correctly, and the OpenAPI generator puts it incomponents.securitySchemes. Your service was describing itself accurately and the client generator threw the answer away one step later.Authentication: one field per declared scheme
SecuritySchemenow carriesKeyandParamNameseparately. That rename is the point: it made the compiler enumerate every read site so each could be checked rather than mechanically renamed.The generated
AuthConfighas one typed field per declared scheme, named from the scheme key:A single
apply(header, url)puts each credential where the document says it goes, and REST, SSE, WebSocket and WebTransport all route through it. Before this,addAuthwas REST-only and the WebSocket path carried its own bearer-only copy, which is how the two drifted apart.Sessions are opt-in:
A jar makes the client stateful, so the doc comment says plainly that it must not be shared between users.
WithCookieJar(jar)takes your own if you want a per-user or persistent one.On the TypeScript side the manifest emits a
securitySchemestable plus per-operation scheme keys, so anAuthProvidercan dispatch onmeta.securitythe waytransport.tsalways said it would.RestTransportOptionsgainedcredentials, threaded through to the generated fetch client, for cross-origin cookie sessions. Unset by default, so nothing changes unless you ask for it.Authorization: declared once, carried the whole way
AuthContextnow holdsRolesandPermissionsnext to the scopes it already had, and a route's requirement is a value rather than a closure:That constraint is deliberate. A value can be written into an OpenAPI document and read back by a generated client, and a closure cannot, which is what keeps resource-scoped questions (can this user edit document 42) inside handlers where they have the document in hand.
Registry.SetAuthorizeris the seam. Forge ships plain set membership and stops there: no role hierarchies, no permission inheritance, no wildcard matching. That is policy, and warden replaces the authorizer wholesale to provide it.Routes and groups declare what they want:
The names carry the combining rule because a name that hides and-versus-or sends you to the source to find out. Declaring records a requirement on the route's metadata and nothing more; enforcement happens when that requirement reaches
MiddlewareWithRequirement. The docs used to read as though the option itself did the checking, and no longer do.From there the OpenAPI generator carries roles and permissions as
x-forge-authz, which is how the client generator learns about them without importing the auth extension.What the clients do with it
Both generated clients get a capability surface. In Go:
TypeScript gets the same vocabulary in its own idiom:
setPrincipal,can,hasRole,hasPermission,canCall,missingCapabilities, and arequiredCapabilitiestable you can read directly. It also hascapabilitiesKnown(), so a UI can tell "this principal may not" apart from "nobody has told us yet" instead of rendering a disabled button at a user who is merely still loading.None of this is a gate. Every predicate answers from a principal you handed the client, so it exists to hide a button the server would refuse anyway. The generated file says so in its own header, and the review that added it kept finding places where the wording implied more than that.
Two names that meant two opposite things
auth.roleswas route metadata for the roles a route requires, and also the context key for the roles the subject holds. Same forauth.scopes. Different maps, so nothing ever collided at runtime, but one literal covering both requirement and possession is a trap for whoever reads it next.The context keys are now
auth.subject.rolesandauth.subject.scopes. The route metadata keys are untouched, since the OpenAPI and client generators read them. Both interceptors still honour the old key as a deprecated fallback, so an application that sets it from its own middleware keeps working.Breaking change
AuthConfig.BearerTokenandAuthConfig.APIKeyare gone.Regenerate and rename at the call site. There is no compatibility flag, because the old shape could not express what the specs already declared.
The generated client compiles now, which it did not before
This was not planned. While verifying the auth work, we wrote generated output into a scratch module and ran a real build. It failed, and had been failing for a while:
client.go,rest.goandtypes.goemitted imports that go unused for some specs,client.goandwebsocket.goreferencedc.authwhen noAuthConfigwas declared at all, andwebtransport.godestructured the dial as(*Session, *http.Response, error)when every version ofwebtransport-goreturns those two the other way round.All of that is fixed here, and six more of the same shape once we went looking: dead imports as soon as a streaming endpoint declares no payload schema, a
timeinwebsocket.gonobody had noticed, a doc comment that a multi-line inline struct broke out of, ago.modthat never requiredwebtransport-goat all, and aDialerthe package had renamed toTransportseveral versions earlier.ConnectionStateand the reconnect helpers moved intostreaming.goon the way, becausesse.goused all four and onlywebsocket.godeclared them, so an SSE-only spec generated a client naming identifiers nothing had defined.Two gates stand behind that now. Every emitted file gets parsed and each import checked for a real user, which needs no module cache and runs everywhere. Then
TestGoGeneratorGeneratedModuleBuildswrites a generated client into a temporary module and runs the actual compiler over six configurations, because an undefined symbol and an unresolvable module are both perfectly valid syntax with no unused imports, and only a compiler finds them. It skips rather than fails when the environment cannot support a build, under-short, with nogoon PATH, or when the module cache cannot resolve offline. Compile errors are never skipped, so a green run offline still means everything checkable was checked.Six client-core gaps, closed along the way
Not auth, but on the branch: entity garbage collection, container identity across a refetch, a WebTransport adapter, hydration for Vue and Angular, and a streamed SSR payload. Each was written down in the README as deliberately left rather than overlooked, so that list is six shorter.
The refetch one is the behaviour change to know about. A refetch used to hand back a new root even when no record moved, because a second skeleton gets built and the container memos are keyed by node identity.
read()now takes the previous value and reuses a container whose children are all identical to it. That changes whatuseQuerygives you, and the React test asserting the old contract asserts its converse now.Verification
go build ./...andgo vetclean;go test ./internal/client/... ./internal/router/... ./extensions/auth/...passespackages/client-core: suite green, typecheck clean, all size budgets within limitweb-client/generated-package.mdxdocuments the generated auth surface and the breaking changeTwo tests are worth reading rather than skimming. The end-to-end one feeds a document identical to what
WithAPIKeyCookieproduces and asserts the generated client sends that cookie, with a negative assertion thatX-API-Keyis absent; both fail against the pre-branch code. The parity one drives a deliberately unnormalized endpoint through the Go and TypeScript generators and compares the two emitted tables against each other rather than against a literal, because a table pinned to Go's output would have passed before that fix as happily as after it.Worth knowing during review
The generator has two IR builders,
spec_parser.gofor documents on disk andintrospector.gofor a live router. Every defect fixed here existed in both, and the second one was easy to miss. Both now keep cookie parameters, warn on an unknown parameter location, sort schemes deterministically, and populate declared authorization, so a client generated from an introspected router behaves the same as one generated from a file.Warnings rather than silence is the theme. A scheme type the generator cannot emit, two scheme keys that derive to the same Go field, two capability strings that derive to the same constant, a parameter location it does not recognise, a cookie parameter it will not put on the wire: each of those used to vanish, and each now reports through the generated client's
Warnings.Known gaps
Both of the ones this section used to list are closed.
An apiKey scheme whose
invalue the generator does not recognise used to get anAuthConfigfield regardless, whileapplyencodes only header, cookie and query. You could set that credential and it went nowhere. OpenAPI 3.1 allows exactly those three locations, but neither IR builder validates what the document said, so a typo, an OpenAPI 2.0body, or a scheme with noinall arrive at the generator. They warn and claim no field now, like every other unhandled scheme in that file. A credential that vanishes is the defect this work exists to remove; one that looks configured and goes nowhere is worse, because nothing at the call site suggests you should check.The six unrelated compile bugs are fixed too, in the section above.
Downstream
xraph/authsome#51 is the first consumer, and it is blocked on this shipping. It stamps a session's roles onto the auth context, declares on its admin routes the permission its middleware already enforces, and declares the session cookie its provider has always accepted and never described. None of it compiles until
AuthContext.Rolesis in a release, so that branch sits red until this one merges.Worth reading alongside this PR if you want to see whether the declaration path actually holds up: it is the same journey from a real service's own auth model out to a generated client, and it turned up two things this side got wrong on the first pass.