@nxgt/janus-hono 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Steve Tsala
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # @nxgt/janus-hono
2
+
3
+ [`@nxgt/janus`](https://www.npmjs.com/package/@nxgt/janus) in a
4
+ [Hono](https://hono.dev) app: the signed-in user on every request, the session
5
+ cookie set and cleared, a route guarded by a permission, the instances on the
6
+ context to grant and revoke through, and every error answered with the status
7
+ it deserves — an outage as 503, never as 401 or 403.
8
+
9
+ ```ts
10
+ import { janusErrors, sendSession, session, signOut } from '@nxgt/janus-hono';
11
+ import { Hono } from 'hono';
12
+ import { auth } from './auth'; // what janus() answered
13
+
14
+ const app = new Hono()
15
+ .post('/sign-in', async (c) => {
16
+ const { email, password } = await c.req.json();
17
+ const user = sendSession(c, auth, await auth.signIn({ email, password }));
18
+ return c.json({ id: user.id }); // the token is in the cookie, not the body
19
+ })
20
+ .post('/sign-out', async (c) => {
21
+ await signOut(c, auth);
22
+ return c.body(null, 204);
23
+ })
24
+ .get('/me', session(auth, { required: true }), (c) =>
25
+ c.json({ email: c.var.user.email }), // never null here
26
+ );
27
+
28
+ app.onError(janusErrors()); // CREDENTIALS_INVALID → 401, STORE_FAILED → 503, …
29
+ ```
30
+
31
+ > **0.x.** A minor version may still change the surface; the changelog says how.
32
+
33
+ ## Install
34
+
35
+ ```sh
36
+ bun add @nxgt/janus-hono @nxgt/janus hono
37
+ ```
38
+
39
+ Every peer is required: `@nxgt/janus`, `hono` (4.13.4 or later) and
40
+ `typescript` (6). `@nxgt/janus` is a **peer**, never a dependency: this package
41
+ defines no error class, so the `JanusError` you catch is the one you import.
42
+ Like `@nxgt/janus`, it expects `"moduleResolution": "bundler"`: the
43
+ declarations import without extensions, so `nodenext` is not supported.
44
+
45
+ ## API
46
+
47
+ | Export | What it is |
48
+ | --- | --- |
49
+ | `session(auth, options?)` | Middleware. Reads who the request belongs to — `auth.authenticate(c.req.raw)` — and sets `c.var.user` and `c.var.session`, `null` for an anonymous request. `{ required: true }` answers an anonymous request 401 and types `c.var.user` as never `null`. `{ type: 'staff' }` treats a user of any other type as anonymous. Sends a renewed session's cookie again |
50
+ | `sendSession(c, auth, signedIn)` | Appends the session cookie to the response — after `signUp`, `signIn`, or anything that answered `{ token, session, user }` — and answers the user |
51
+ | `signOut(c, auth)` | Revokes the session the request presents and clears the cookie, whatever the answer. `false` when the request presented no session, or an unknown one |
52
+ | `janusErrors({ report?, fallback? })` | An `app.onError` handler: every `JanusError` answered with `statusOf(code)` and `bodyOf(error)`; anything else to `fallback`, or to Hono's own handling. `report(error, c)` sees every one answered 5xx first — `STORE_FAILED` and the like, for your logs |
53
+ | `statusOf(code)` | The status a code deserves: `STORE_FAILED` 503, `CREDENTIALS_INVALID` 401, `USER_INACTIVE` 403, `LOGIN_TAKEN` 409, … Exhaustive over `JanusErrorCode` |
54
+ | `bodyOf(error)` | `{ code }`, plus `issues` for `USER_INVALID` and `minLength` for `PASSWORD_TOO_SHORT` — what the client can act on, and nothing else |
55
+ | `SessionOptions<Type>` | `{ type?, required? }`, the options of `session()` — for a wrapper of your own |
56
+ | `SessionEnv<typeof auth, Type?, Required?>` | The `Env` `session()` sets, for `new Hono<SessionEnv<typeof auth>>()` |
57
+ | `UserOfAuth<typeof auth>` | The users an instance knows, as a union narrowed by `user.type` |
58
+ | `permission(access, permission, type, load, options?)` | Middleware. Loads the object with `load(c)`, checks `access.can(c.var.user, permission, object)` with `type` added, and sets `c.var.object` to what `load` answered. Anonymous: 401, before loading. `load` answers `null`: 404. A denial: 403. `{ ctx: (c, object) => … }` is required exactly when the permission reaches a condition; `{ subject: (c) => … }` replaces `c.var.user`. One per route |
59
+ | `byParam(name, find)` | A `load` for `permission()`: `find(c.req.param(name))`, or `null` — a 404 — when the route has no such parameter |
60
+ | `bindJanus({ auth?, access? })` | The functions above with the instances bound: `session(options?)`, `sendSession(c, signedIn)` and `signOut(c)` with `auth`; `permission(permission, type, load, options?)` with `access`; `provide()` always |
61
+ | `provide({ auth?, access? })` | Middleware. Sets `c.var.auth` and `c.var.access` to the instances given — only those — for a route that writes users or tuples |
62
+ | `ObjectData<C, Type>`, `PermissionOptions`, `Instances` | The types of `load`'s answer, of `permission()`'s options and of `provide()`'s argument |
63
+
64
+ The whole status table is in [`@nxgt/janus`'s errors guide](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/errors.md).
65
+
66
+ ## Permissions
67
+
68
+ `permission()` guards a route: the object is loaded once, checked, and handed
69
+ to the route. `provide()` puts the instances on the context, so the route that
70
+ creates a record grants its owner through `c.var.access`:
71
+
72
+ ```ts
73
+ import { permission, provide, session } from '@nxgt/janus-hono';
74
+ import { Hono } from 'hono';
75
+ import { access, recordOf } from './access'; // the guide's model, and a loader
76
+ import { auth } from './auth';
77
+ import { records } from './records'; // your own store
78
+
79
+ const app = new Hono()
80
+ .use(session(auth), provide({ auth, access }))
81
+ .get(
82
+ '/records/:id',
83
+ permission(access, 'view', 'record', recordOf),
84
+ (c) => c.json(c.var.object), // your record type, loaded once
85
+ )
86
+ .put(
87
+ '/records/:id',
88
+ permission(access, 'edit', 'record', recordOf, {
89
+ ctx: (c, record) => ({ locked: record.locked }), // `edit` reaches a when()
90
+ }),
91
+ async (c) => c.json(await records.update(c.var.object.id, await c.req.json())),
92
+ )
93
+ .post('/records', session(auth, { type: 'patient', required: true }), async (c) => {
94
+ const record = await records.create(await c.req.json());
95
+ await c.var.access.grant({ type: 'record', id: record.id }, 'owner', c.var.user);
96
+ return c.json(record, 201);
97
+ });
98
+ ```
99
+
100
+ The model — `owner`, a `doctor` read from the record, `edit` under a condition
101
+ — and `recordOf` are those of the [guarded routes guide](docs/guide/permissions.md). The
102
+ permission, the object type, the fields a `fromField` reads and the `ctx`
103
+ of a condition are typed from the model, as they are for `can()`.
104
+ `janusErrors()` answers the failures of both sides alike — `can()`'s
105
+ `STORE_FAILED` is a 503, never a 403.
106
+
107
+ ### Bound once
108
+
109
+ `bindJanus({ auth, access })` answers the same functions with the instances
110
+ bound, so no route repeats them — typed exactly as the unbound ones:
111
+
112
+ ```ts
113
+ import { bindJanus, byParam } from '@nxgt/janus-hono';
114
+
115
+ const j = bindJanus({ auth, access });
116
+
117
+ const app = new Hono()
118
+ .use(j.session(), j.provide())
119
+ .get('/records/:id', j.permission('view', 'record', byParam('id', (id) => records.find(id))), (c) =>
120
+ c.json(c.var.object),
121
+ )
122
+ .post('/sign-out', async (c) => {
123
+ await j.signOut(c);
124
+ return c.body(null, 204);
125
+ });
126
+ ```
127
+
128
+ Only what is given is bound: `bindJanus({ auth })` has no `permission`.
129
+
130
+ Each side is usable alone. An application that signs users in some other way
131
+ passes `{ subject: (c) => … }` to `permission()`; one without permissions uses
132
+ `session()` and `janusErrors()` only.
133
+
134
+ ## Traps
135
+
136
+ - **Without `app.onError(janusErrors())`, an outage is Hono's 500.** Not a
137
+ 401 — `session()` never turns a failure into an anonymous request — but not
138
+ the 503 that tells a client to retry either.
139
+ - **`app.use(session(auth))` on its own line does not type the routes after
140
+ it.** Hono types a chain, not statements. Chain the calls, or declare the app
141
+ as `new Hono<SessionEnv<typeof auth>>()`.
142
+ - **A renewed session is sent again only as a cookie, and only to a request
143
+ that presented one.** A client that sends `Authorization: Bearer` is renewed
144
+ too — its session token does not change — but it is never handed a cookie it did not
145
+ ask for. It reads `session.expiresAt` if it needs to.
146
+ - **`required` is typed only for a literal `true`.** A computed `boolean`
147
+ compiles, and leaves `c.var.user` nullable.
148
+ - **`required` answers 401 with no body and no `WWW-Authenticate`.** Wrap the
149
+ route yourself for another answer: `session(auth)`, then check
150
+ `c.var.user === null`.
151
+ - **The cookie is `Secure` by default.** A browser drops it over plain
152
+ `http://`, `localhost` aside in most browsers. Set `cookie: { secure: false }`
153
+ in `janus()` for a development server that is not on `localhost`, never in
154
+ production.
155
+ - **`janusErrors()` logs nothing by itself.** A `STORE_FAILED` is answered 503
156
+ without a line in your logs unless you pass `report` —
157
+ `janusErrors({ report: (error) => logger.error(error) })`.
158
+ - **`bodyOf` never carries `reason`, `login` or a cause.** `CREDENTIALS_INVALID`
159
+ says one thing for an unknown login, a missing password and a wrong one, so a
160
+ response cannot tell which users exist. Log the error before you answer it
161
+ if you need the reason: `janusErrors` is a function of `(error, c)` you can
162
+ wrap.
163
+ - **`load` does not know the route's path.** It takes a plain `Context`:
164
+ `permission()` is built before Hono attaches it to a route, so
165
+ `c.req.param('id')` is `string | undefined` there. Answer `null` for a
166
+ missing id — that is a 404 — or let `byParam('id', find)` do it.
167
+ - **One `permission()` per route.** Both would claim `c.var.object`, so a
168
+ second throws a `TypeError`. Check a parent object through an arrow in the
169
+ model.
170
+ - **`permission()` needs `session()` before it**, or `{ subject }`. Without
171
+ either, it throws a `TypeError` at the first request — a wiring error, not
172
+ an anonymous 401.
173
+ - **A denial is 403, so it tells that the object exists.** Where that is a
174
+ leak, load only what the user may see and let `load` answer `null` — a 404.
175
+
176
+ ## Documentation
177
+
178
+ - [Guides](docs/README.md) — wiring the middleware, the routes of a sign-in, guarded routes
179
+ - [Troubleshooting](docs/troubleshooting.md) — by the symptom or message you see
180
+ - [Roadmap](docs/roadmap.md) — what is next, and what is not planned
181
+
182
+ ## Type safety, counted
183
+
184
+ Twenty-five plausible mistakes are refused by the compiler, each with a
185
+ `@ts-expect-error` case in `test/types/`:
186
+
187
+ - six in `session.ts`: reading `c.var.user` where it may be `null` (twice,
188
+ directly and through `SessionEnv`), a field of another user type, a user type
189
+ the instance does not know (twice, in `session()` and in `SessionEnv`), and a
190
+ cookie sent without its session;
191
+ - ten in `permission.ts`: a permission the object type does not declare, an
192
+ object type the model does not (twice: in a model of one object type and of
193
+ several, each reported on the misspelled argument), an object loaded without
194
+ a field a `fromField` reads, a condition reached with no `ctx`, a `ctx` of the
195
+ wrong shape, a `ctx` where no condition is reachable, a field the loaded
196
+ object does not have, granting a relation read from a field, and an instance
197
+ `provide()` was not given;
198
+ - nine in `bind.ts`: the same refusals through `bindJanus()` — a nullable
199
+ user, an unknown user type, a missing `ctx`, a misspelled permission — a
200
+ `permission`, `session`, `sendSession` or `signOut` it was not given the
201
+ instance for, and an `access` that is no `permissions()` instance.
202
+
203
+ ## Licence
204
+
205
+ MIT
package/dist/bind.d.ts ADDED
@@ -0,0 +1,76 @@
1
+ import type { Session } from '@nxgt/janus';
2
+ import type { CheckableOf, ModelConfig, ObjectTypeOf, Permissions } from '@nxgt/janus/permissions';
3
+ import type { Context, MiddlewareHandler } from 'hono';
4
+ import { type Awaitable, type ObjectData, type OptionsArgs } from './permission';
5
+ import { type Auth, type SessionEnv, type SessionOptions, type UserOfAuth } from './session';
6
+ /** `session(auth, options?)`, with `auth` bound: the same three forms. */
7
+ export interface BoundSession<A> {
8
+ <const T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']>(options: SessionOptions<T> & {
9
+ readonly required: true;
10
+ }): MiddlewareHandler<SessionEnv<A, T, true>>;
11
+ <const T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']>(options?: SessionOptions<T> & {
12
+ readonly required?: false;
13
+ }): MiddlewareHandler<SessionEnv<A, T>>;
14
+ <const T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']>(options: SessionOptions<T>): MiddlewareHandler<SessionEnv<A, T>>;
15
+ }
16
+ /** `permission(access, …)`, with `access` bound. */
17
+ export type BoundPermission<C extends ModelConfig> = <const T extends ObjectTypeOf<C>, const P extends CheckableOf<C, T>, O extends ObjectData<C, T>>(permission: P, type: T, load: (c: Context) => Awaitable<O | null>, ...options: OptionsArgs<C, T, P, O>) => MiddlewareHandler<{
18
+ Variables: {
19
+ object: O;
20
+ };
21
+ }>;
22
+ /** The functions of this package that take `auth`, with `auth` bound. */
23
+ export interface BoundAuth<A> {
24
+ readonly session: BoundSession<A>;
25
+ /** `sendSession(c, auth, signedIn)`: sets the cookie, answers the user. */
26
+ readonly sendSession: <U>(c: Context, signedIn: {
27
+ readonly token: string;
28
+ readonly session: Session;
29
+ readonly user: U;
30
+ }) => U;
31
+ /** `signOut(c, auth)`. */
32
+ readonly signOut: (c: Context) => Promise<boolean>;
33
+ }
34
+ /** What `bindJanus()` takes: the `janus()` instance, the `permissions()` one, or both. */
35
+ export interface Bindable {
36
+ readonly auth?: Auth<{
37
+ readonly type: string;
38
+ }>;
39
+ /** What `permission()` takes: a `permissions()` instance, or its `can`. */
40
+ readonly access?: {
41
+ readonly can: unknown;
42
+ };
43
+ }
44
+ /**
45
+ * What `bindJanus(instances)` answers: `provide()` always; `session`,
46
+ * `sendSession` and `signOut` with `auth`; `permission` with `access`.
47
+ */
48
+ export type Bound<I extends Bindable> = {
49
+ /** `provide(instances)`: every instance bound, on the context. */
50
+ readonly provide: () => MiddlewareHandler<{
51
+ Variables: {
52
+ [K in keyof I & keyof Bindable]: I[K];
53
+ };
54
+ }>;
55
+ } & (I['auth'] extends Auth<{
56
+ readonly type: string;
57
+ }> ? BoundAuth<I['auth']> : unknown) & (I['access'] extends Pick<Permissions<infer C>, 'can'> ? {
58
+ readonly permission: BoundPermission<C>;
59
+ } : unknown);
60
+ /**
61
+ * This package's functions with the instances bound once, so no route
62
+ * repeats them:
63
+ *
64
+ * ```ts
65
+ * const j = bindJanus({ auth, access });
66
+ * const app = new Hono()
67
+ * .use(j.session(), j.provide())
68
+ * .get('/records/:id', j.permission('view', 'record', recordOf), (c) => c.json(c.var.object));
69
+ * ```
70
+ *
71
+ * The same functions, typed the same way: `j.session({ required: true })` is
72
+ * `session(auth, { required: true })`. Only what is given is bound — a
73
+ * `permission` without `access` is absent from the type.
74
+ */
75
+ export declare function bindJanus<const I extends Bindable>(instances: I): Bound<I>;
76
+ //# sourceMappingURL=bind.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind.d.ts","sourceRoot":"","sources":["../src/bind.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import { JanusError, type JanusErrorCode } from '@nxgt/janus';
2
+ import type { Context, ErrorHandler } from 'hono';
3
+ import type { ContentfulStatusCode } from 'hono/utils/http-status';
4
+ /**
5
+ * The status each code deserves. Exhaustive: a code added to `@nxgt/janus`
6
+ * stops this file compiling instead of answering `undefined`.
7
+ *
8
+ * `STORE_FAILED` is 503 and nothing else — **an outage is not a negative
9
+ * answer**, and a 401 or a 404 would tell every user they do not exist.
10
+ */
11
+ export declare function statusOf(code: JanusErrorCode): ContentfulStatusCode;
12
+ /**
13
+ * The body a refusal is answered with: its `code`, and only what the client
14
+ * can act on — the fields that failed the schema, the policy's minimum. Never
15
+ * `reason`, `login`, a hash prefix or a cause: those are for your logs.
16
+ */
17
+ export declare function bodyOf(error: JanusError): {
18
+ readonly code: JanusErrorCode;
19
+ readonly issues?: JanusError['issues'];
20
+ readonly minLength?: number;
21
+ };
22
+ /** What `janusErrors()` takes. */
23
+ export interface JanusErrorsOptions {
24
+ /**
25
+ * Called with every `JanusError` answered 5xx — `STORE_FAILED`,
26
+ * `UNSUPPORTED`, `PERMISSION_DEPTH`: the ones the server must fix, not the
27
+ * client — before it is answered. Its `slot`, `operation`, `reason` and
28
+ * `cause` are for your logs; the body never carries them.
29
+ *
30
+ * **It cannot stop the answer**: one that throws, or rejects, is a
31
+ * `process.emitWarning`, and the 503 is sent all the same.
32
+ */
33
+ readonly report?: (error: JanusError, c: Context) => unknown;
34
+ /** Every error that is not a `JanusError`. Hono's own handling when absent. */
35
+ readonly fallback?: ErrorHandler;
36
+ }
37
+ /**
38
+ * An `app.onError` handler: every `JanusError` answered with its status and
39
+ * `bodyOf(error)`; anything else handed to `fallback` — Hono's own behaviour
40
+ * when absent.
41
+ *
42
+ * Works for either side of `@nxgt/janus`: `can()`'s `STORE_FAILED` is a 503
43
+ * here too, never a 403.
44
+ *
45
+ * ```ts
46
+ * app.onError(janusErrors({ report: (error) => logger.error(error) }));
47
+ * ```
48
+ */
49
+ export declare function janusErrors(options?: JanusErrorsOptions): ErrorHandler;
50
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAElD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAEnE;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,oBAAoB,CA2BnE;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG;IAC1C,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;IACvC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B,CAWA;AAgBD,kCAAkC;AAClC,MAAM,WAAW,kBAAkB;IAClC;;;;;;;;OAQG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC;IAC7D,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC;CACjC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,YAAY,CAQ1E"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `@nxgt/janus-hono` — `@nxgt/janus` in a Hono app.
3
+ *
4
+ * - `session(auth, options?)` — middleware: `c.var.user` and `c.var.session`
5
+ * on every request, a 401 where a user is required, the renewed cookie sent
6
+ * again;
7
+ * - `sendSession(c, auth, signedIn)` and `signOut(c, auth)` — the cookie, set
8
+ * and cleared; `sendSession` answers the user;
9
+ * - `permission(access, permission, type, load)` — middleware: the route runs
10
+ * only if the subject holds the permission on the object `load` answers,
11
+ * and gets that object as `c.var.object`; `byParam(name, find)` is a `load`
12
+ * reading one path parameter;
13
+ * - `provide({ auth, access })` — the instances on the context, for a route
14
+ * that writes users or tuples;
15
+ * - `janusErrors({ report?, fallback? })` — `app.onError`: every `JanusError`
16
+ * as its status, `STORE_FAILED` as 503 and never as 401 or 403; `report`
17
+ * sees the 5xx ones first.
18
+ *
19
+ * - `bindJanus({ auth, access })` — the functions above with the instances
20
+ * bound once: `j.session()`, `j.permission('view', 'record', load)`, …
21
+ *
22
+ * It defines no error class: what it lets through is `@nxgt/janus`'s own.
23
+ */
24
+ export { type Bindable, type Bound, type BoundAuth, type BoundPermission, type BoundSession, bindJanus, } from './bind';
25
+ export { bodyOf, type JanusErrorsOptions, janusErrors, statusOf, } from './errors';
26
+ export { byParam, type ObjectData, type PermissionOptions, permission, } from './permission';
27
+ export { type Instances, provide } from './provide';
28
+ export { type SessionEnv, type SessionOptions, sendSession, session, signOut, type UserOfAuth, } from './session';
29
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACN,KAAK,QAAQ,EACb,KAAK,KAAK,EACV,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,SAAS,GACT,MAAM,QAAQ,CAAC;AAChB,OAAO,EACN,MAAM,EACN,KAAK,kBAAkB,EACvB,WAAW,EACX,QAAQ,GACR,MAAM,UAAU,CAAC;AAClB,OAAO,EACN,OAAO,EACP,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,UAAU,GACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,KAAK,SAAS,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EACN,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,WAAW,EACX,OAAO,EACP,OAAO,EACP,KAAK,UAAU,GACf,MAAM,WAAW,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,181 @@
1
+ // src/permission.ts
2
+ function permission(access, permission, type, load, options = {}) {
3
+ const can = access.can;
4
+ return async (c, next) => {
5
+ if (c.get("object") !== undefined) {
6
+ throw new TypeError("permission(): c.var.object is already set — one permission() per route; check a parent object through an arrow in the model");
7
+ }
8
+ const subject = options.subject === undefined ? userOf(c) : await options.subject(c);
9
+ if (subject === null || subject === undefined)
10
+ return c.body(null, 401);
11
+ const object = await load(c);
12
+ if (object === null)
13
+ return c.body(null, 404);
14
+ const allowed = await can(subject, permission, view(object, type), options.ctx === undefined ? undefined : { ctx: await options.ctx(c, object) });
15
+ if (!allowed)
16
+ return c.body(null, 403);
17
+ c.set("object", object);
18
+ await next();
19
+ };
20
+ }
21
+ function byParam(name, find) {
22
+ return (c) => {
23
+ const id = c.req.param(name);
24
+ return id === undefined ? null : find(id);
25
+ };
26
+ }
27
+ function view(object, type) {
28
+ return new Proxy(object, {
29
+ get: (target, key) => key === "type" ? type : Reflect.get(target, key),
30
+ has: (target, key) => key === "type" || Reflect.has(target, key)
31
+ });
32
+ }
33
+ function userOf(c) {
34
+ const user = c.get("user");
35
+ if (user === undefined) {
36
+ throw new TypeError("permission(): c.var.user is not set — put session(auth) before it, or pass { subject }");
37
+ }
38
+ return user;
39
+ }
40
+
41
+ // src/provide.ts
42
+ function provide(instances) {
43
+ return async (c, next) => {
44
+ if (instances.auth !== undefined)
45
+ c.set("auth", instances.auth);
46
+ if (instances.access !== undefined)
47
+ c.set("access", instances.access);
48
+ await next();
49
+ };
50
+ }
51
+
52
+ // src/session.ts
53
+ import { getCookie } from "hono/cookie";
54
+ function session(auth, options = {}) {
55
+ return async (c, next) => {
56
+ const current = await auth.authenticate(c.req.raw, options.type === undefined ? undefined : { type: options.type });
57
+ if (current === null && options.required === true) {
58
+ return c.body(null, 401);
59
+ }
60
+ c.set("user", current?.user ?? null);
61
+ c.set("session", current?.session ?? null);
62
+ await next();
63
+ const name = `${auth.cookie.name}=`;
64
+ if (current?.renewed === true && getCookie(c, auth.cookie.name) === current.token && !c.res.headers.getSetCookie().some((value) => value.startsWith(name))) {
65
+ sendSession(c, auth, current);
66
+ }
67
+ };
68
+ }
69
+ function sendSession(c, auth, signedIn) {
70
+ c.header("Set-Cookie", auth.cookie.serialize(signedIn.token, signedIn.session), {
71
+ append: true
72
+ });
73
+ return signedIn.user;
74
+ }
75
+ async function signOut(c, auth) {
76
+ const revoked = await auth.signOut(c.req.raw);
77
+ c.header("Set-Cookie", auth.cookie.clear(), { append: true });
78
+ return revoked;
79
+ }
80
+
81
+ // src/bind.ts
82
+ function bindJanus(instances) {
83
+ const { auth, access } = instances;
84
+ const bound = {
85
+ provide: () => provide(instances)
86
+ };
87
+ if (auth !== undefined) {
88
+ bound.session = (options = {}) => session(auth, options);
89
+ bound.sendSession = (c, signedIn) => sendSession(c, auth, signedIn);
90
+ bound.signOut = (c) => signOut(c, auth);
91
+ }
92
+ if (access !== undefined) {
93
+ const guard = permission;
94
+ bound.permission = (...rest) => guard(access, ...rest);
95
+ }
96
+ return bound;
97
+ }
98
+ // src/errors.ts
99
+ import { JanusError } from "@nxgt/janus";
100
+ function statusOf(code) {
101
+ switch (code) {
102
+ case "STORE_FAILED":
103
+ return 503;
104
+ case "NOT_FOUND":
105
+ return 404;
106
+ case "LOGIN_TAKEN":
107
+ case "VERSION_CONFLICT":
108
+ return 409;
109
+ case "USER_INVALID":
110
+ case "PASSWORD_TOO_SHORT":
111
+ case "HASH_UNSUPPORTED":
112
+ case "INVALID_CURSOR":
113
+ case "TOKEN_UNKNOWN":
114
+ case "TOKEN_SPENT":
115
+ case "TOKEN_EXPIRED":
116
+ case "TOKEN_STALE":
117
+ return 400;
118
+ case "CREDENTIALS_INVALID":
119
+ return 401;
120
+ case "USER_INACTIVE":
121
+ return 403;
122
+ case "UNSUPPORTED":
123
+ return 501;
124
+ case "PERMISSION_DEPTH":
125
+ return 500;
126
+ }
127
+ }
128
+ function bodyOf(error) {
129
+ switch (error.code) {
130
+ case "USER_INVALID":
131
+ return { code: error.code, issues: error.issues ?? [] };
132
+ case "PASSWORD_TOO_SHORT":
133
+ return error.minLength === undefined ? { code: error.code } : { code: error.code, minLength: error.minLength };
134
+ default:
135
+ return { code: error.code };
136
+ }
137
+ }
138
+ function honoDefault(error, c) {
139
+ if ("getResponse" in error) {
140
+ const response = error.getResponse();
141
+ return c.newResponse(response.body, response);
142
+ }
143
+ console.error(error);
144
+ return c.text("Internal Server Error", 500);
145
+ }
146
+ function janusErrors(options = {}) {
147
+ const { report, fallback = honoDefault } = options;
148
+ return (error, c) => {
149
+ if (!(error instanceof JanusError))
150
+ return fallback(error, c);
151
+ const status = statusOf(error.code);
152
+ if (status >= 500 && report !== undefined)
153
+ reportSafely(report, error, c);
154
+ return c.json(bodyOf(error), status);
155
+ };
156
+ }
157
+ function reportSafely(report, error, c) {
158
+ const warn = (failure) => process.emitWarning(`janusErrors: report failed on ${error.code}: ${failure instanceof Error ? failure.name : typeof failure}`);
159
+ try {
160
+ const reported = report(error, c);
161
+ if (reported instanceof Promise)
162
+ reported.then(undefined, warn);
163
+ } catch (failure) {
164
+ warn(failure);
165
+ }
166
+ }
167
+ export {
168
+ bindJanus,
169
+ bodyOf,
170
+ byParam,
171
+ janusErrors,
172
+ permission,
173
+ provide,
174
+ sendSession,
175
+ session,
176
+ signOut,
177
+ statusOf
178
+ };
179
+
180
+ //# debugId=93D6634A3905348664756E2164756E21
181
+ //# sourceMappingURL=index.js.map