feat: auth provider contract — centralized identity + permissions (core)#138
Merged
Conversation
Core-only phase of #128: optional auth providers on StackProvider (client) and stack() (server). Adds useIdentity()/useCan() hooks, the <CanAccess> element-gating component, a permission prop on ComposedRoute with loginPath redirect through the top-level router, and per-request memoized identity resolution exposed via getRequestIdentity() for lifecycle hooks. Fully non-breaking: without a provider all code paths behave exactly as before. Co-authored-by: Cursor <cursoragent@cursor.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
|
✅ Shadcn registry validated — no registry changes detected. |
- useCan no longer returns a result computed for a previous identity or check inputs: resolved state carries the identity/key it was computed for and anything else reads as pending, so <CanAccess>/RouteGuard never briefly expose the prior user's permissions after refetch() - can() now receives the original params object (ref) instead of a JSON round-tripped copy, preserving Dates and other non-JSON values - RouteGuard deny throws a generic "Unauthorized" and logs the resource/action detail as a dev-only warning Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 330836a. Configure here.
Mirror the client StackAuthBoundary: a rejecting or throwing getIdentity resolves getRequestIdentity to null (with a logged error) instead of propagating and failing the whole request. Co-authored-by: Cursor <cursoragent@cursor.com>
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.

Closes #128
Summary
@btst/stack/context): optionalauthprop onStackProvidertaking aStackAuthProvider(getIdentity/can/loginPath). NewuseIdentity()anduseCan()hooks and a<CanAccess resource action fallback loading>element-gating component. Without a provider, identity isnull, all checks pass, and<CanAccess>renders children — behavior is identical to today.ComposedRouteaccepts an optionalpermission={{ resource, action }}. With a provider configured, denied unauthenticated users are redirected tologinPathvia the top-level router'snavigate(from Top-level router adapter on StackProvider (framework presets for Link/navigate/refresh/Image) #127/feat: top-level router/api props on StackProvider with framework presets #134,window.location.assignfallback) while the route'sLoadingComponentrenders; denied authenticated users get anUnauthorizederror thrown into the route's existing ErrorBoundary (same semantics asuseRouteLifecycle). No plugin declares permissions yet — adoption happens per plugin in Phase 2: per-plugin adoption sweep of the new core primitives #136.@btst/stack/api):stack({ auth })registers a lazy, per-request-memoized identity resolver keyed on the request'sHeadersinstance (which better-call passes into every endpoint ctx). Lifecycle hooks read it with the newgetRequestIdentity(ctx.headers)— the provider'sgetIdentityruns at most once per request. The provider is also exposed on the pluginStackContext. Withoutauth, the handler is returned untouched.Scope notes
<CanAccess>on controls, identity-prop removal) are Phase 2: per-plugin adoption sweep of the new core primitives #136; legacy guard/prop deletion is Phase 3: remove legacy dual paths and release v3.0.0 #137.onBefore*PageRenderedcallbacks,currentUserId/x-user-id/loginHrefwiring all keep working unchanged alongside the provider.Test plan
pnpm build,pnpm typecheck,pnpm lint,pnpm -F @btst/stack knippass locallypnpm vitest runinpackages/stack— 274 tests pass, including 25 new ones:auth-provider.test.tsx: no-provider pass-through (incl.renderToStringparity), identity resolution,can()allow/deny/error,<CanAccess>allow/deny/no-provider/loading, gated-route redirect tologinPath, authenticated-deny → ErrorBoundarystack-auth.test.ts: memory-adapterstack()— per-request memoization (1 provider call for N reads), fresh resolution per request, ctx propagation,nullwithout provider / outside a requestdocs && pnpm buildpasses (new MDX page + AutoTypeTables)Made with Cursor