Scope


Scope is the way Kilpi stores values scoped to a single request.

It is used to store e.g. the current onUnauthorized handler set in Kilpi.onUnauthorized, as well as to cache the current subject.

Provide an explicit scope

You can provide an explicit scope using either Kilpi.runInScope (to run a function) or Kilpi.scoped (to wrap a function) as follows.

Kilpi.onUnauthorized(...); // Will not work, logs a warning
// Using Kilpi.runInScope (runs the given function in a scope)
const result = await Kilpi.runInScope(async () => {
Kilpi.onUnauthorized(...); // This works
...
})
// Using Kilpi.scoped (returns a function wrapped in a scope)
export const POST = Kilpi.scoped(async (req) => {
Kilpi.onUnauthorized(...); // This works
...
})

If possible, you should provide scope for each request using a middleware or similar.

app.use(async (_, next) => {
return await Kilpi.runInScope(async () => {
return await next(); // Run handler in Kilpi scope
});
})

Kilpi.runInScope and Kilpi.scoped use AsyncLocalStorage under the hood.

Implicit scope

Plugins can automatically provide an implicit scope in certain contexts, without having to call Kilpi.runInScope or Kilpi.scoped explicitly.

For example, the React Server Components Plugin automatically provides a scope for React Server Components.

const Kilpi = createKilpi({
// ...
plugins: [ReactServerComponentPlugin()],
})
export async function Page() {
Kilpi.onUnauthorized(() => redirect("/")); // Automatically works
Kilpi.authorize("authed");
}

This works by using the Kilpi.hooks.onRequestScope hook to automatically create a scope for each request, when no explicit scope is available.


Not able to provide a scope?

If you are unable to provide a request-level scope for any reason, you can use the following workarounds.

Kilpi.authorize and Kilpi.unauthorized

You can’t set up request-level error handlers using `Kilpi To use Kilpi.authorize without a scope, you can…

  1. Use the default error handler provided to createKilpi to handle unauthorized requests, and avoid using request-level error handlers with Kilpi.onUnauthorized.
  2. Avoid APIs that throw and instead use manual APIs (such as Kilpi.isAuthorized and Kilpi.getAuthorization instead of Kilpi.authorize).

Background and motivations

The scope API was designed to

  1. Reduce boilerplate

    With scope, you optimally only have to call Kilpi.onUnauthorized once, and all subsequent authorizations (in nested queries, components, …) will automatically use the same handler.

  2. Enable caching

    The scope is used to cache e.g. the subject for the current request.

  3. Enable protected queries

    Without a “global” onUnauthorized handler, you would have to define what happens when a protected query authorization fails. The same query should throw a HTTPForbiddenError in one context and redirect to the login page in another.

The primary trade-off of this approach is the complexity of having to manually provide a scope or use a plugin adds complexity to installing the library.