@entitle/hono
v0.2.0
Published
Hono HTTP adapter for the Entitle policy engine
Downloads
51
Readme
@entitle/hono
Hono adapter for Entitle, a strongly-typed feature entitlements and policy engine for TypeScript.
It builds a Hono sub-application that serves the engine's operations over HTTP.
These routes can change who is entitled to what.
/assign,/revokeand/overridegrant any policy, or any limit, to any subject.authorizeis a required option on every Entitle adapter and there is no way to mount one without it:0.0.1served these routes with no authentication, which was an anonymous privilege-escalation API rather than a missing feature, and0.1.0made the hook required in order to close it.exposedefaults to'read-only', so the four mutating routes are not registered at all until you passexpose: 'all'.Read Security in
@entitle/core's README before you deploy this. It is the same text asnode_modules/@entitle/core/README.md, which this package installs alongside itself.
Install
pnpm add @entitle/hono @entitle/core honoUsage
createHonoApp(engine, options) returns a Hono app to mount with
parentApp.route('/prefix', app).
import { Hono } from 'hono'
import { createHonoApp } from '@entitle/hono'
const app = new Hono()
app.route(
'/entitle',
createHonoApp(engine, {
// Omit `expose` for a read-only mount: check / explain / resolve only.
// `'all'` additionally mounts assign / revoke / override / override/remove.
expose: 'all',
authorize: async ({ access, request }) => {
const session = await getSession(request.req.header('cookie'))
if (!session) return { ok: false, code: 'UNAUTHENTICATED' }
if (access === 'write' && !session.user.isAdmin) {
return { ok: false, code: 'FORBIDDEN', message: 'admin required' }
}
return { ok: true, actorId: session.user.id }
},
}),
)
export default appOptions
| Option | Required | Default | Meaning |
| ----------- | -------- | ------------- | ----------------------------------------------------- |
| authorize | yes | -- | Decides whether one request may perform one operation |
| expose | no | 'read-only' | Which routes exist: the three reads, or all seven |
authorize
Called once per request, before the operation runs, with:
| Field | Type | Meaning |
| ----------- | ----------------------------------- | ------------------------------------------------ |
| operation | AdminOperationName | Canonical operation name, e.g. 'assign_policy' |
| access | 'read' \| 'write' | Whether the operation observes or changes state |
| input | unknown | The parsed request body |
| request | Hono Context | The raw request is at request.req.raw |
Return { ok: true, actorId } to allow -- actorId is what the write is
attributed to. Return { ok: false, code: 'UNAUTHENTICATED' } for a 401 or
{ ok: false, code: 'FORBIDDEN' } for a 403; a bare { ok: false } is a 403. A
hook that throws or rejects denies the request: it fails closed, and the
handler is never reached.
A body this adapter cannot parse as JSON is reported before the hook runs, which matches how Express and Fastify behave -- both parse upstream of any route handler.
expose
Defaults to 'read-only', which mounts check, explain and resolve. The
four mutating routes are not registered at all until expose: 'all', so until
then they 404 rather than 403 -- they genuinely do not exist on that mount. This
is the option most likely to surprise you on an upgrade, because a missing
authorize is a compile error while a missing expose is a silent 404.
Routes
All four Entitle adapters serve the same POST routes at the same paths:
| Route | Access | Description |
| ----------------------- | ------- | ------------------------------------ |
| POST /check | read | Check a single feature for a subject |
| POST /explain | read | Full evaluation trace for a feature |
| POST /resolve | read | Resolve all features for a subject |
| POST /assign | write | Assign a policy to a subject |
| POST /revoke | write | Revoke a policy from a subject |
| POST /override | write | Set a per-subject feature override |
| POST /override/remove | write | Remove a per-subject override |
Every body names the subject by subjectId and subjectKind, removals
included.
Responses
Every success is the same body on all four adapters, with data always
present and null for the four operations that return nothing:
{
"ok": true,
"data": null
}All four adapters agree on this. Before 0.1.1 @entitle/express omitted the
data key and @entitle/fastify sent data: undefined, which its serializer
drops, so a client written against this adapter broke on a data === null check
when moved to either of those. The contract, the idempotent removals, and the three failures the envelope
does not cover are What a success response contains in
@entitle/core's README.
Errors
Every failure is the same body on all four adapters:
{
"ok": false,
"error": {
"code": "STORE_ERROR",
"message": "Failed to assign policy",
"correlationId": "0b9c1f8e-6f2a-4b51-9a2e-2d3f8f0f7c11"
}
}code decides the status; message is a constant chosen by the code path;
correlationId is present exactly when something was withheld, and matches the
logger.error line that carries the cause in full. A driver's error fields, a
store's SQL or file path, a feature's schema and an internal exception never
reach a response. The full contract is What an error response may contain
in @entitle/core's README.
Serving the API open, on purpose
For a sidecar bound to loopback, a mesh that authorizes at the edge, or a test harness:
import { createHonoApp, UNSAFE_allowUnauthenticated } from '@entitle/hono'
const entitle = createHonoApp(engine, {
expose: 'all',
authorize: UNSAFE_allowUnauthenticated,
})It warns once per process, and the name is deliberately greppable. The deployment
assumptions that have to hold first are under UNSAFE_allowUnauthenticated
in @entitle/core's README.
Testing
@entitle/testing exports createAuthorizeStub, which is the hook plus a record
of what the adapter asked it -- so a test can assert that a refused request never
reached the operation.
Links
@entitle/core-- the engine, and the guidance the sections above point at: what the routes are and which of them write,expose, serving a subject its own grants,explainas information disclosure, actor attribution,UNSAFE_allowUnauthenticatedand the preconditions it needs, and what an error response may contain. Also atnode_modules/@entitle/core/README.md.- Reporting a vulnerability -- privately, to https://github.com/fponticelli,
and not in a public issue.
0.0.1is deprecated and unsupported: it served these routes with no authentication.
License
MIT
