@nxgt/janus-hono 0.5.2 → 0.7.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/README.md CHANGED
@@ -47,18 +47,22 @@ declarations import without extensions, so `nodenext` is not supported.
47
47
  | Export | What it is |
48
48
  | --- | --- |
49
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`, `secondFactor.confirm`, `secondFactor.recover`, or anything that answered `{ token, session, user }` — and answers the user. With a second factor configured, narrow `signIn`'s answer on `status` first |
50
+ | `sendSession(c, auth, signedIn, options?)` | Appends the session cookie to the response — after `signUp`, `signIn`, `secondFactor.confirm`, `secondFactor.recover`, or anything that answered `{ token, session, user }` — and answers the user. When the answer carries a `deviceToken` — the sign-in was given a `device` — it appends the device cookie too, or sets it again for another `maxAge`; `options.device` is its `DeviceCookieOptions`. With a second factor configured, narrow `signIn`'s answer on `status` first |
51
+ | `deviceOf(c, options?)` | The device token the request's device cookie holds, or `null` when there is none or it is empty: what a sign-in takes as `device` — `auth.signIn(input, { device: deviceOf(c) })`. `options` are the `DeviceCookieOptions` given to `sendSession` |
52
+ | `DeviceCookieOptions` | The device cookie: `{ name?, domain?, path?, sameSite?, secure?, maxAge? }` — `'janus-device'`, none, `'/'`, `'Lax'`, `true` and 400 days (`34560000` seconds) by default. Always `HttpOnly`. `sameSite` is Hono's capitalised `'Lax' \| 'Strict' \| 'None'` |
53
+ | `DEVICE_COOKIE` | `'janus-device'`, the device cookie's default name |
54
+ | `SendSessionOptions` | `{ device? }`, the options of `sendSession` — for a wrapper of your own |
51
55
  | `fresh(maxAge, { clock? })` | Middleware, after `session()`. The route runs only for a session that proved who it is less than `maxAge` ago — signed in, or confirmed since by `auth.stepUp.confirm`. An older one throws `STEP_UP_REQUIRED`, which `janusErrors()` answers 403; an anonymous request is 401 with no body. `clock` is the one given to `janus()`, in a spec |
52
56
  | `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 |
53
- | `janusErrors({ report?, fallback? })` | An `app.onError` handler: every `JanusError` answered with `statusOf(code)` and `bodyOf(error)` — and a `Retry-After` header when it carries `retryAfter`, a throttled sign-in; 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 — and cannot change the answer: one that throws or rejects is a warning, and the 503 is sent |
54
- | `statusOf(code)` | The status a code deserves — `@nxgt/janus`'s `statusOf`, typed as Hono's `ContentfulStatusCode`: `STORE_FAILED` 503, `CREDENTIALS_INVALID` and `CODE_INVALID` 401, `USER_INACTIVE` and `STEP_UP_REQUIRED` 403, `LOGIN_TAKEN`, `SECOND_FACTOR_NOT_ENROLLED` and `SECOND_FACTOR_ACTIVE` 409, … Exhaustive over `JanusErrorCode` |
55
- | `bodyOf(error)` | `{ code }`, plus `issues` for `USER_INVALID`, `minLength` for `PASSWORD_TOO_SHORT` and `attemptsLeft` for any `CODE_INVALID` that carries it — `secondFactor.confirm`'s, `secondFactor.recover`'s, `signInCode.confirm`'s and `stepUp.confirm`'s — and `retryAfter` for a `CREDENTIALS_INVALID` whose login is throttled: what the client can act on, and nothing else |
57
+ | `janusErrors({ report?, fallback? })` | An `app.onError` handler: every `JanusError` answered with `statusOf(code)` and `bodyOf(error)` — and a `Retry-After` header when it carries `retryAfter`, a throttled sign-in or a throttled e-mail request; 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 — and cannot change the answer: one that throws or rejects is a warning, and the 503 is sent |
58
+ | `statusOf(code)` | The status a code deserves — `@nxgt/janus`'s `statusOf`, typed as Hono's `ContentfulStatusCode`: `STORE_FAILED` 503, `CREDENTIALS_INVALID` and `CODE_INVALID` 401, `MAIL_THROTTLED` 429, `USER_INACTIVE` and `STEP_UP_REQUIRED` 403, `LOGIN_TAKEN`, `SECOND_FACTOR_NOT_ENROLLED` and `SECOND_FACTOR_ACTIVE` 409, … Exhaustive over `JanusErrorCode` |
59
+ | `bodyOf(error)` | `{ code }`, plus `issues` for `USER_INVALID`, `minLength` for `PASSWORD_TOO_SHORT` and `attemptsLeft` for any `CODE_INVALID` that carries it — `secondFactor.confirm`'s, `secondFactor.recover`'s, `signInCode.confirm`'s and `stepUp.confirm`'s — and `retryAfter` for a `CREDENTIALS_INVALID` whose login is throttled, and for `MAIL_THROTTLED` (429): what the client can act on, and nothing else |
56
60
  | `SessionOptions<Type>` | `{ type?, required? }`, the options of `session()` — for a wrapper of your own |
57
61
  | `SessionEnv<typeof auth, Type?, Required?>` | The `Env` `session()` sets, for `new Hono<SessionEnv<typeof auth>>()` |
58
62
  | `UserOfAuth<typeof auth>` | The users an instance knows, as a union narrowed by `user.type` |
59
63
  | `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 |
60
64
  | `byParam(name, find)` | A `load` for `permission()`: `find(c.req.param(name))`, or `null` — a 404 — when the route has no such parameter |
61
- | `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 |
65
+ | `bindJanus({ auth?, access? })` | The functions above with the instances bound: `session(options?)`, `sendSession(c, signedIn, options?)` and `signOut(c)` with `auth`; `permission(permission, type, load, options?)` with `access`; `provide()` always |
62
66
  | `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 |
63
67
  | `ObjectData<C, Type>`, `PermissionOptions`, `Instances` | The types of `load`'s answer, of `permission()`'s options and of `provide()`'s argument |
64
68
  | `FreshOptions` | `{ clock? }`, the options of `fresh()` |
@@ -123,6 +127,33 @@ app.onError(janusErrors()); // a wrong code: 401 { code: 'CODE_INVALID', attempt
123
127
  `SECOND_FACTOR_ACTIVE` 409. The enrolment routes and a bearer client's
124
128
  sign-in are in [the routes guide](docs/guide/routes.md#a-second-factor).
125
129
 
130
+ ## Devices
131
+
132
+ With `janus({ devices })`, a sign-in given the device token the browser
133
+ holds tells a new device from a known one. `deviceOf(c)` reads the device
134
+ cookie, and `sendSession` sets it from the answer's `deviceToken`:
135
+
136
+ ```ts
137
+ import { deviceOf, sendSession } from '@nxgt/janus-hono';
138
+ import { Hono } from 'hono';
139
+ import { auth } from './auth'; // janus({ …, devices: { keys } })
140
+
141
+ const app = new Hono().post('/sign-in', async (c) => {
142
+ const { email, password } = await c.req.json();
143
+ const signedIn = await auth.signIn({ email, password }, { device: deviceOf(c) });
144
+ const user = sendSession(c, auth, signedIn); // the session cookie, and janus-device for 400 days
145
+ if (signedIn.newDevice) await tellTheUser(user, c.req.header('user-agent')); // yours — @nxgt/janus-mail's newSignIn
146
+ return c.json({ id: user.id });
147
+ });
148
+ ```
149
+
150
+ The device cookie is `janus-device`, `HttpOnly`, `Secure`, `SameSite=Lax`,
151
+ `Path=/`, for 400 days; `{ device: { name, domain, path, sameSite, secure,
152
+ maxAge } }` changes it — give `deviceOf` the same options. With a second
153
+ factor, give `deviceOf(c)` again to `secondFactor.confirm` and `recover`:
154
+ the challenge carries no device. [The routes guide](docs/guide/routes.md#the-device-cookie)
155
+ has the second factor's routes and a bearer client.
156
+
126
157
  ## Step-up
127
158
 
128
159
  A sensitive route asks for a recent proof: `fresh(maxAge)` after
@@ -295,18 +326,31 @@ passes `{ subject: (c) => … }` to `permission()`; one without permissions uses
295
326
  - **`permission()` needs `session()` before it**, or `{ subject }`. Without
296
327
  either, it throws a `TypeError` at the first request — a wiring error, not
297
328
  an anonymous 401.
329
+ - **Give `deviceOf` and `sendSession` the same options.** A cookie written
330
+ under one name or path and read under another is never found: every
331
+ sign-in is then a new device. Keep one `DeviceCookieOptions` and pass it
332
+ to both.
333
+ - **The device cookie's `sameSite` is Hono's, capitalised.** `'Lax'`, not
334
+ the `'lax'` of `janus({ cookie })` — a compile error, where a cast would
335
+ hand Hono a value it does not know.
336
+ - **`signOut` leaves the device cookie.** The device stays known after a
337
+ sign-out, which is the point: a user who signs out and back in is not
338
+ told of a new device.
339
+ - **With a second factor, give the device again.** `auth.secondFactor.confirm(challenge,
340
+ code, { device: deviceOf(c) })`: the challenge carries none, and a
341
+ confirmation given no device sets no device cookie and reports nothing.
298
342
  - **A denial is 403, so it tells that the object exists.** Where that is a
299
343
  leak, load only what the user may see and let `load` answer `null` — a 404.
300
344
 
301
345
  ## Documentation
302
346
 
303
- - [Guides](docs/README.md) — wiring the middleware, the routes of a sign-in, of a second factor and of a code or a link sent by e-mail, guarded routes
347
+ - [Guides](docs/README.md) — wiring the middleware, the routes of a sign-in, of a second factor, of a code or a link sent by e-mail and of the device cookie, guarded routes
304
348
  - [Troubleshooting](docs/troubleshooting.md) — by the symptom or message you see
305
349
  - [Roadmap](docs/roadmap.md) — what is next, and what is not planned
306
350
 
307
351
  ## Type safety, counted
308
352
 
309
- Twenty-six plausible mistakes are refused by the compiler, each with a
353
+ Twenty-eight plausible mistakes are refused by the compiler, each with a
310
354
  `@ts-expect-error` case in `test/types/`:
311
355
 
312
356
  - six in `session.ts`: reading `c.var.user` where it may be `null` (twice,
@@ -323,9 +367,12 @@ Twenty-six plausible mistakes are refused by the compiler, each with a
323
367
  - nine in `bind.ts`: the same refusals through `bindJanus()` — a nullable
324
368
  user, an unknown user type, a missing `ctx`, a misspelled permission — a
325
369
  `permission`, `session`, `sendSession` or `signOut` it was not given the
326
- instance for, and an `access` that is no `permissions()` instance.
370
+ instance for, and an `access` that is no `permissions()` instance;
327
371
  - one in `fresh.ts`: a `maxAge` that is no duration — `'10 minutes'` for
328
- `'10m'`.
372
+ `'10m'`;
373
+ - two in `device.ts`: the session cookie's lowercase `sameSite` given to the
374
+ device cookie, whose `'Lax'` is Hono's, and the device cookie's options
375
+ given to `sendSession` at the top rather than under `device`.
329
376
 
330
377
  ## Licence
331
378
 
package/dist/bind.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { Session } from '@nxgt/janus';
2
2
  import type { CheckableOf, ModelConfig, ObjectTypeOf, Permissions } from '@nxgt/janus/permissions';
3
3
  import type { Context, MiddlewareHandler } from 'hono';
4
4
  import { type Awaitable, type ObjectData, type OptionsArgs } from './permission';
5
- import { type Auth, type SessionEnv, type SessionOptions, type UserOfAuth } from './session';
5
+ import { type Auth, type SendSessionOptions, type SessionEnv, type SessionOptions, type UserOfAuth } from './session';
6
6
  /** `session(auth, options?)`, with `auth` bound: the same three forms. */
7
7
  export interface BoundSession<A> {
8
8
  <const T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']>(options: SessionOptions<T> & {
@@ -22,12 +22,13 @@ export type BoundPermission<C extends ModelConfig> = <const T extends ObjectType
22
22
  /** The functions of this package that take `auth`, with `auth` bound. */
23
23
  export interface BoundAuth<A> {
24
24
  readonly session: BoundSession<A>;
25
- /** `sendSession(c, auth, signedIn)`: sets the cookie, answers the user. */
25
+ /** `sendSession(c, auth, signedIn, options?)`: sets the cookies, answers the user. */
26
26
  readonly sendSession: <U>(c: Context, signedIn: {
27
27
  readonly token: string;
28
28
  readonly session: Session;
29
29
  readonly user: U;
30
- }) => U;
30
+ readonly deviceToken?: string | null;
31
+ }, options?: SendSessionOptions) => U;
31
32
  /** `signOut(c, auth)`. */
32
33
  readonly signOut: (c: Context) => Promise<boolean>;
33
34
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bind.d.ts","sourceRoot":"","sources":["../src/bind.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EACX,WAAW,EACX,WAAW,EACX,YAAY,EACZ,WAAW,EACX,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACvD,OAAO,EACN,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,WAAW,EAEhB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACN,KAAK,IAAI,EACT,KAAK,UAAU,EACf,KAAK,cAAc,EAInB,KAAK,UAAU,EACf,MAAM,WAAW,CAAC;AAEnB,0EAA0E;AAC1E,MAAM,WAAW,YAAY,CAAC,CAAC;IAC9B,CAAC,KAAK,CAAC,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAC7D,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG;QAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;KAAE,GACtD,iBAAiB,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;IAC7C,CAAC,KAAK,CAAC,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAC7D,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG;QAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAA;KAAE,GACzD,iBAAiB,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACvC,CAAC,KAAK,CAAC,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAC7D,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,GACxB,iBAAiB,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;CACvC;AAED,oDAAoD;AACpD,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,WAAW,IAAI,CACpD,KAAK,CAAC,CAAC,SAAS,YAAY,CAAC,CAAC,CAAC,EAC/B,KAAK,CAAC,CAAC,SAAS,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,EACjC,CAAC,SAAS,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,EAE1B,UAAU,EAAE,CAAC,EACb,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,EACzC,GAAG,OAAO,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAC/B,iBAAiB,CAAC;IAEtB,SAAS,EAAE;QAAE,MAAM,EAAE,CAAC,CAAA;KAAE,CAAC;CACzB,CAAC,CAAC;AAEH,yEAAyE;AACzE,MAAM,WAAW,SAAS,CAAC,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAClC,2EAA2E;IAC3E,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,EACvB,CAAC,EAAE,OAAO,EACV,QAAQ,EAAE;QACT,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;QAC1B,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;KACjB,KACG,CAAC,CAAC;IACP,0BAA0B;IAC1B,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACnD;AAED,0FAA0F;AAC1F,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChD,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;KAAE,CAAC;CAC5C;AAED;;;GAGG;AACH,MAAM,MAAM,KAAK,CAAC,CAAC,SAAS,QAAQ,IAAI;IACvC,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,iBAAiB,CAAC;QAEzC,SAAS,EAAE;aAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC;SAAE,CAAC;KACrD,CAAC,CAAC;CACH,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GACnD,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,GACpB,OAAO,CAAC,GACV,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,GACnD;IAAE,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC,CAAC,CAAA;CAAE,GAC3C,OAAO,CAAC,CAAC;AAEb;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,KAAK,CAAC,CAAC,SAAS,QAAQ,EAAE,SAAS,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAsB1E"}
1
+ {"version":3,"file":"bind.d.ts","sourceRoot":"","sources":["../src/bind.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { Context } from 'hono';
2
+ /** The device cookie's name when none is given. */
3
+ export declare const DEVICE_COOKIE = "janus-device";
4
+ /**
5
+ * The long-lived cookie that holds the device token `@nxgt/janus` answers
6
+ * as `deviceToken`. Every default is the strict one, as for the session
7
+ * cookie; give `deviceOf` and `sendSession` the same options.
8
+ */
9
+ export interface DeviceCookieOptions {
10
+ /** `'janus-device'` when absent. */
11
+ readonly name?: string;
12
+ readonly domain?: string;
13
+ /** `'/'` when absent. */
14
+ readonly path?: string;
15
+ /** `'Lax'` when absent. */
16
+ readonly sameSite?: 'Lax' | 'Strict' | 'None';
17
+ /** `true` when absent. */
18
+ readonly secure?: boolean;
19
+ /** In seconds. Four hundred days when absent — the longest a browser keeps one. */
20
+ readonly maxAge?: number;
21
+ }
22
+ /**
23
+ * The device token the request's cookie holds, or `null` when it holds none:
24
+ * what a sign-in takes as `device`.
25
+ *
26
+ * ```ts
27
+ * const signedIn = await auth.signIn({ email, password }, { device: deviceOf(c) });
28
+ * ```
29
+ */
30
+ export declare function deviceOf(c: Context, options?: DeviceCookieOptions): string | null;
31
+ /** Sets the device cookie, or sets it again so it lasts another `maxAge`. Appends. */
32
+ export declare function sendDevice(c: Context, deviceToken: string, options?: DeviceCookieOptions): void;
33
+ //# sourceMappingURL=device.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../src/device.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAGpC,mDAAmD;AACnD,eAAO,MAAM,aAAa,iBAAiB,CAAC;AAK5C;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IACnC,oCAAoC;IACpC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,yBAAyB;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,2BAA2B;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC9C,0BAA0B;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CACvB,CAAC,EAAE,OAAO,EACV,OAAO,GAAE,mBAAwB,GAC/B,MAAM,GAAG,IAAI,CAGf;AAED,sFAAsF;AACtF,wBAAgB,UAAU,CACzB,CAAC,EAAE,OAAO,EACV,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,mBAAwB,GAC/B,IAAI,CASN"}
package/dist/errors.d.ts CHANGED
@@ -13,7 +13,8 @@ export declare function statusOf(code: JanusErrorCode): ContentfulStatusCode;
13
13
  /**
14
14
  * The body a refusal is answered with: its `code`, and only what the client
15
15
  * can act on — the fields that failed the schema, the policy's minimum, the
16
- * attempts a challenge has left, the seconds a throttled sign-in waits.
16
+ * attempts a challenge has left, the seconds a throttled sign-in or a
17
+ * throttled request for an e-mail waits.
17
18
  * Never `reason`, `login`, a hash prefix or a cause: those are for your logs.
18
19
  */
19
20
  export declare function bodyOf(error: JanusError): {
@@ -40,8 +41,9 @@ export interface JanusErrorsOptions {
40
41
  }
41
42
  /**
42
43
  * An `app.onError` handler: every `JanusError` answered with its status and
43
- * `bodyOf(error)` — and a throttled sign-in with a `Retry-After` header, its
44
- * `retryAfter` in seconds; anything else handed to `fallback` — Hono's own
44
+ * `bodyOf(error)` — and a throttled sign-in, or a request past its e-mails
45
+ * (`MAIL_THROTTLED`, 429), with a `Retry-After` header, its `retryAfter` in
46
+ * seconds; anything else handed to `fallback` — Hono's own
45
47
  * behaviour when absent.
46
48
  *
47
49
  * Works for either side of `@nxgt/janus`: `can()`'s `STORE_FAILED` is a 503
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,UAAU,EACV,KAAK,cAAc,EAEnB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAElD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAEnE;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,oBAAoB,CAEnE;AAED;;;;;GAKG;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;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B,CAmBA;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;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,YAAY,CAY1E"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,UAAU,EACV,KAAK,cAAc,EAEnB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAElD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAEnE;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,oBAAoB,CAEnE;AAED;;;;;;GAMG;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;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B,CAoBA;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;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,YAAY,CAY1E"}
@@ -1 +1 @@
1
- {"version":3,"file":"fresh.d.ts","sourceRoot":"","sources":["../src/fresh.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,KAAK,EACV,KAAK,QAAQ,EAEb,KAAK,OAAO,EAGZ,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAE9C,6CAA6C;AAC7C,MAAM,WAAW,YAAY;IAC5B,yFAAyF;IACzF,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,KAAK,CACpB,MAAM,EAAE,QAAQ,EAChB,OAAO,GAAE,YAAiB,GACxB,iBAAiB,CAAC;IAEpB,SAAS,EAAE;QAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,CAAC;CACvC,CAAC,CAoBD"}
1
+ {"version":3,"file":"fresh.d.ts","sourceRoot":"","sources":["../src/fresh.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,KAAK,EACV,KAAK,QAAQ,EAEb,KAAK,OAAO,EAGZ,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAE9C,6CAA6C;AAC7C,MAAM,WAAW,YAAY;IAC5B,yFAAyF;IACzF,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,KAAK,CACpB,MAAM,EAAE,QAAQ,EAChB,OAAO,GAAE,YAAiB,GACxB,iBAAiB,CAAC;IAEpB,SAAS,EAAE;QAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,CAAC;CACvC,CAAC,CAqBD"}
package/dist/index.d.ts CHANGED
@@ -7,8 +7,11 @@
7
7
  * - `fresh(maxAge, options?)` — middleware, behind `session(auth)`: the
8
8
  * route runs only for a session that proved who it is less than `maxAge`
9
9
  * ago, `STEP_UP_REQUIRED` otherwise;
10
- * - `sendSession(c, auth, signedIn)` and `signOut(c, auth)` — the cookie, set
11
- * and cleared; `sendSession` answers the user;
10
+ * - `sendSession(c, auth, signedIn, options?)` and `signOut(c, auth)` — the
11
+ * cookie, set and cleared; `sendSession` answers the user, and sets the
12
+ * device cookie when the sign-in answered a `deviceToken`;
13
+ * - `deviceOf(c, options?)` — the device token the request's cookie holds,
14
+ * or `null`: what a sign-in takes as `device`;
12
15
  * - `permission(access, permission, type, load)` — middleware: the route runs
13
16
  * only if the subject holds the permission on the object `load` answers,
14
17
  * and gets that object as `c.var.object`; `byParam(name, find)` is a `load`
@@ -25,9 +28,10 @@
25
28
  * It defines no error class: what it lets through is `@nxgt/janus`'s own.
26
29
  */
27
30
  export { type Bindable, type Bound, type BoundAuth, type BoundPermission, type BoundSession, bindJanus, } from './bind';
31
+ export { DEVICE_COOKIE, type DeviceCookieOptions, deviceOf } from './device';
28
32
  export { bodyOf, type JanusErrorsOptions, janusErrors, statusOf, } from './errors';
29
33
  export { type FreshOptions, fresh } from './fresh';
30
34
  export { byParam, type ObjectData, type PermissionOptions, permission, } from './permission';
31
35
  export { type Instances, provide } from './provide';
32
- export { type SessionEnv, type SessionOptions, sendSession, session, signOut, type UserOfAuth, } from './session';
36
+ export { type SendSessionOptions, type SessionEnv, type SessionOptions, sendSession, session, signOut, type UserOfAuth, } from './session';
33
37
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;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,EAAE,KAAK,YAAY,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACnD,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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;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,EAAE,aAAa,EAAE,KAAK,mBAAmB,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAC7E,OAAO,EACN,MAAM,EACN,KAAK,kBAAkB,EACvB,WAAW,EACX,QAAQ,GACR,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACnD,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,kBAAkB,EACvB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,WAAW,EACX,OAAO,EACP,OAAO,EACP,KAAK,UAAU,GACf,MAAM,WAAW,CAAC"}
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ function permission(access, permission, type, load, options = {}) {
16
16
  return c.body(null, 403);
17
17
  c.set("object", object);
18
18
  await next();
19
+ return;
19
20
  };
20
21
  }
21
22
  function byParam(name, find) {
@@ -50,7 +51,28 @@ function provide(instances) {
50
51
  }
51
52
 
52
53
  // src/session.ts
53
- import { getCookie } from "hono/cookie";
54
+ import { getCookie as getCookie2 } from "hono/cookie";
55
+
56
+ // src/device.ts
57
+ import { getCookie, setCookie } from "hono/cookie";
58
+ var DEVICE_COOKIE = "janus-device";
59
+ var MAX_AGE = 400 * 24 * 60 * 60;
60
+ function deviceOf(c, options = {}) {
61
+ const token = getCookie(c, options.name ?? DEVICE_COOKIE);
62
+ return token === undefined || token === "" ? null : token;
63
+ }
64
+ function sendDevice(c, deviceToken, options = {}) {
65
+ setCookie(c, options.name ?? DEVICE_COOKIE, deviceToken, {
66
+ path: options.path ?? "/",
67
+ ...options.domain === undefined ? {} : { domain: options.domain },
68
+ sameSite: options.sameSite ?? "Lax",
69
+ secure: options.secure ?? true,
70
+ httpOnly: true,
71
+ maxAge: options.maxAge ?? MAX_AGE
72
+ });
73
+ }
74
+
75
+ // src/session.ts
54
76
  function session(auth, options = {}) {
55
77
  return async (c, next) => {
56
78
  const current = await auth.authenticate(c.req.raw, options.type === undefined ? undefined : { type: options.type });
@@ -61,15 +83,19 @@ function session(auth, options = {}) {
61
83
  c.set("session", current?.session ?? null);
62
84
  await next();
63
85
  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))) {
86
+ if (current?.renewed === true && getCookie2(c, auth.cookie.name) === current.token && !c.res.headers.getSetCookie().some((value) => value.startsWith(name))) {
65
87
  sendSession(c, auth, current);
66
88
  }
89
+ return;
67
90
  };
68
91
  }
69
- function sendSession(c, auth, signedIn) {
92
+ function sendSession(c, auth, signedIn, options = {}) {
70
93
  c.header("Set-Cookie", auth.cookie.serialize(signedIn.token, signedIn.session), {
71
94
  append: true
72
95
  });
96
+ if (typeof signedIn.deviceToken === "string") {
97
+ sendDevice(c, signedIn.deviceToken, options.device);
98
+ }
73
99
  return signedIn.user;
74
100
  }
75
101
  async function signOut(c, auth) {
@@ -85,13 +111,13 @@ function bindJanus(instances) {
85
111
  provide: () => provide(instances)
86
112
  };
87
113
  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);
114
+ bound["session"] = (options = {}) => session(auth, options);
115
+ bound["sendSession"] = (c, signedIn, options) => sendSession(c, auth, signedIn, options);
116
+ bound["signOut"] = (c) => signOut(c, auth);
91
117
  }
92
118
  if (access !== undefined) {
93
119
  const guard = permission;
94
- bound.permission = (...rest) => guard(access, ...rest);
120
+ bound["permission"] = (...rest) => guard(access, ...rest);
95
121
  }
96
122
  return bound;
97
123
  }
@@ -112,6 +138,7 @@ function bodyOf(error) {
112
138
  case "CODE_INVALID":
113
139
  return error.attemptsLeft === undefined ? { code: error.code } : { code: error.code, attemptsLeft: error.attemptsLeft };
114
140
  case "CREDENTIALS_INVALID":
141
+ case "MAIL_THROTTLED":
115
142
  return error.retryAfter === undefined ? { code: error.code } : { code: error.code, retryAfter: error.retryAfter };
116
143
  default:
117
144
  return { code: error.code };
@@ -165,12 +192,15 @@ function fresh(maxAge, options = {}) {
165
192
  throw new StepUpRequiredError("fresh(): the session proved who it is longer ago than maxAge — confirm with a step-up", { userId: current.userId });
166
193
  }
167
194
  await next();
195
+ return;
168
196
  };
169
197
  }
170
198
  export {
199
+ DEVICE_COOKIE,
171
200
  bindJanus,
172
201
  bodyOf,
173
202
  byParam,
203
+ deviceOf,
174
204
  fresh,
175
205
  janusErrors,
176
206
  permission,
@@ -181,5 +211,5 @@ export {
181
211
  statusOf
182
212
  };
183
213
 
184
- //# debugId=B7AF7A43DA4D8D7A64756E2164756E21
214
+ //# debugId=3BB1888C6A6D634864756E2164756E21
185
215
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/permission.ts", "../src/provide.ts", "../src/session.ts", "../src/bind.ts", "../src/errors.ts", "../src/fresh.ts"],
3
+ "sources": ["../src/permission.ts", "../src/provide.ts", "../src/session.ts", "../src/device.ts", "../src/bind.ts", "../src/errors.ts", "../src/fresh.ts"],
4
4
  "sourcesContent": [
5
- "import type {\n\tCheckableOf,\n\tCtxOf,\n\tFieldsOf,\n\tModelConfig,\n\tObjectTypeOf,\n\tPermissions,\n\tSubjectRef,\n} from '@nxgt/janus/permissions';\nimport type { Context, MiddlewareHandler } from 'hono';\n\n/**\n * An object of type `T` as the application loads it: its id, **every field a\n * `fromField` of its type reads**, and whatever else it carries. The type is\n * the middleware's to add.\n */\nexport type ObjectData<C extends ModelConfig, T extends ObjectTypeOf<C>> = {\n\treadonly id: string;\n} & { readonly [F in FieldsOf<C, T>]: string | null };\n\nexport type Awaitable<V> = V | Promise<V>;\n\n/**\n * The options of `permission()`: `ctx` required exactly when a condition of\n * the permission is reachable, as for `can()`.\n */\nexport type PermissionOptions<\n\tC extends ModelConfig,\n\tT extends ObjectTypeOf<C>,\n\tP extends string,\n\tO,\n> = {\n\t/**\n\t * Who asks. `c.var.user` — set by `session()` — when absent; pass it when\n\t * your users are not `janus()`'s. `null` is anonymous.\n\t */\n\treadonly subject?: (c: Context) => Awaitable<SubjectRef<C> | null>;\n} & ([CtxOf<C, T, P>] extends [never]\n\t? { readonly ctx?: never }\n\t: {\n\t\t\t/** The condition's context, read from the request and the loaded object. */\n\t\t\treadonly ctx: (c: Context, object: O) => Awaitable<CtxOf<C, T, P>>;\n\t\t});\n\n/**\n * The options argument: required exactly when `ctx` is. Loose when `T` or `P`\n * is its whole constraint — what the compiler falls back to when the object\n * type or the permission was wrong — so the error names that argument, not a\n * missing one; the rule `can()` follows.\n */\nexport type OptionsArgs<\n\tC extends ModelConfig,\n\tT extends ObjectTypeOf<C>,\n\tP extends string,\n\tO,\n> = [ObjectTypeOf<C>] extends [T]\n\t? IsSingle<ObjectTypeOf<C>> extends true\n\t\t? PermissionArgs<C, T, P, O>\n\t\t: [options?: LooseOptions]\n\t: PermissionArgs<C, T, P, O>;\n\n/** `OptionsArgs` once `T` is known right: loose when `P` is its whole constraint. */\ntype PermissionArgs<\n\tC extends ModelConfig,\n\tT extends ObjectTypeOf<C>,\n\tP extends string,\n\tO,\n> = [CheckableOf<C, T>] extends [P]\n\t? IsSingle<CheckableOf<C, T>> extends true\n\t\t? StrictArgs<C, T, P, O>\n\t\t: [options?: LooseOptions]\n\t: StrictArgs<C, T, P, O>;\n\ntype StrictArgs<\n\tC extends ModelConfig,\n\tT extends ObjectTypeOf<C>,\n\tP extends string,\n\tO,\n> = [CtxOf<C, T, P>] extends [never]\n\t? [options?: PermissionOptions<C, T, P, O>]\n\t: [options: PermissionOptions<C, T, P, O>];\n\ntype LooseOptions = {\n\treadonly subject?: (c: Context) => unknown;\n\treadonly ctx?: (c: Context, object: never) => unknown;\n};\n\ntype UnionToIntersection<U> = (\n\tU extends unknown\n\t\t? (union: U) => void\n\t\t: never\n) extends (intersection: infer I) => void\n\t? I\n\t: never;\n\n/** Whether `U` is one type rather than a union of several. */\ntype IsSingle<U> = [U] extends [UnionToIntersection<U>] ? true : false;\n\ntype LooseCan = (\n\tsubject: SubjectRef<ModelConfig> | null,\n\tpermission: string,\n\tobject: { readonly type: string; readonly id: string },\n\toptions?: { readonly ctx: unknown },\n) => Promise<boolean>;\n\n/**\n * The middleware that lets a request through only if its subject holds\n * `permission` on an object of `type`: it loads the object once, with `load`,\n * checks it with `access.can`, and hands it to the route as `c.var.object`.\n *\n * | The request | Answered |\n * | --- | --- |\n * | anonymous | 401, no body — before the object is loaded |\n * | `load` answers `null` | 404, no body |\n * | a denial | 403, no body |\n * | allowed | the route runs, `c.var.object` set |\n *\n * One per route: a second one throws a `TypeError`, since both would claim\n * `c.var.object`. A parent object is checked through an arrow in the model.\n *\n * **A failure throws**, as everywhere: a store that cannot answer is\n * `STORE_FAILED`, never a 403 — `janusErrors()` answers it 503.\n */\nexport function permission<\n\tC extends ModelConfig,\n\tconst T extends ObjectTypeOf<C>,\n\tconst P extends CheckableOf<C, T>,\n\tO extends ObjectData<C, T>,\n>(\n\taccess: Pick<Permissions<C>, 'can'>,\n\tpermission: P,\n\ttype: T,\n\tload: (c: Context) => Awaitable<O | null>,\n\t...options: OptionsArgs<C, T, P, O>\n): MiddlewareHandler<{\n\t// biome-ignore lint/style/useNamingConvention: Hono's `Env` names the key, not us.\n\tVariables: { object: O };\n}>;\nexport function permission(\n\taccess: { readonly can: unknown },\n\tpermission: string,\n\ttype: string,\n\tload: (c: Context) => Awaitable<{ readonly id: string } | null>,\n\toptions: {\n\t\treadonly subject?: (\n\t\t\tc: Context,\n\t\t) => Awaitable<SubjectRef<ModelConfig> | null>;\n\t\treadonly ctx?: (c: Context, object: unknown) => Awaitable<unknown>;\n\t} = {},\n): MiddlewareHandler {\n\tconst can = access.can as LooseCan;\n\n\treturn async (c, next) => {\n\t\tif (c.get('object') !== undefined) {\n\t\t\tthrow new TypeError(\n\t\t\t\t'permission(): c.var.object is already set — one permission() per route; check a parent object through an arrow in the model',\n\t\t\t);\n\t\t}\n\t\tconst subject =\n\t\t\toptions.subject === undefined ? userOf(c) : await options.subject(c);\n\t\tif (subject === null || subject === undefined) return c.body(null, 401);\n\n\t\tconst object = await load(c);\n\t\tif (object === null) return c.body(null, 404);\n\n\t\tconst allowed = await can(\n\t\t\tsubject,\n\t\t\tpermission,\n\t\t\tview(object, type),\n\t\t\toptions.ctx === undefined\n\t\t\t\t? undefined\n\t\t\t\t: { ctx: await options.ctx(c, object) },\n\t\t);\n\t\tif (!allowed) return c.body(null, 403);\n\n\t\tc.set('object', object);\n\t\tawait next();\n\t};\n}\n\n/**\n * A `load` reading one path parameter: `find(id)`, or `null` — a 404 — when\n * the route has no such parameter.\n *\n * ```ts\n * permission(access, 'view', 'record', byParam('id', (id) => records.find(id)))\n * ```\n */\nexport function byParam<O>(\n\tname: string,\n\tfind: (id: string) => Awaitable<O | null>,\n): (c: Context) => Awaitable<O | null> {\n\treturn (c) => {\n\t\tconst id = c.req.param(name);\n\t\treturn id === undefined ? null : find(id);\n\t};\n}\n\n/**\n * The object as `can()` reads it: `type` added, every other field read from\n * the loaded object itself — so a getter, a class's `#private` state or an\n * ORM document's accessors answer as they do in the route. A spread would\n * copy own enumerable fields only.\n */\nfunction view(\n\tobject: object,\n\ttype: string,\n): { readonly type: string; readonly id: string } {\n\treturn new Proxy(object, {\n\t\tget: (target, key) => (key === 'type' ? type : Reflect.get(target, key)),\n\t\thas: (target, key) => key === 'type' || Reflect.has(target, key),\n\t}) as { readonly type: string; readonly id: string };\n}\n\n/** `c.var.user`, which `session()` sets — or a wiring error when nothing did. */\nfunction userOf(c: Context): SubjectRef<ModelConfig> | null {\n\tconst user: unknown = c.get('user');\n\tif (user === undefined) {\n\t\tthrow new TypeError(\n\t\t\t'permission(): c.var.user is not set — put session(auth) before it, or pass { subject }',\n\t\t);\n\t}\n\treturn user as SubjectRef<ModelConfig> | null;\n}\n",
5
+ "import type {\n\tCheckableOf,\n\tCtxOf,\n\tFieldsOf,\n\tModelConfig,\n\tObjectTypeOf,\n\tPermissions,\n\tSubjectRef,\n} from '@nxgt/janus/permissions';\nimport type { Context, MiddlewareHandler } from 'hono';\n\n/**\n * An object of type `T` as the application loads it: its id, **every field a\n * `fromField` of its type reads**, and whatever else it carries. The type is\n * the middleware's to add.\n */\nexport type ObjectData<C extends ModelConfig, T extends ObjectTypeOf<C>> = {\n\treadonly id: string;\n} & { readonly [F in FieldsOf<C, T>]: string | null };\n\nexport type Awaitable<V> = V | Promise<V>;\n\n/**\n * The options of `permission()`: `ctx` required exactly when a condition of\n * the permission is reachable, as for `can()`.\n */\nexport type PermissionOptions<\n\tC extends ModelConfig,\n\tT extends ObjectTypeOf<C>,\n\tP extends string,\n\tO,\n> = {\n\t/**\n\t * Who asks. `c.var.user` — set by `session()` — when absent; pass it when\n\t * your users are not `janus()`'s. `null` is anonymous.\n\t */\n\treadonly subject?: (c: Context) => Awaitable<SubjectRef<C> | null>;\n} & ([CtxOf<C, T, P>] extends [never]\n\t? { readonly ctx?: never }\n\t: {\n\t\t\t/** The condition's context, read from the request and the loaded object. */\n\t\t\treadonly ctx: (c: Context, object: O) => Awaitable<CtxOf<C, T, P>>;\n\t\t});\n\n/**\n * The options argument: required exactly when `ctx` is. Loose when `T` or `P`\n * is its whole constraint — what the compiler falls back to when the object\n * type or the permission was wrong — so the error names that argument, not a\n * missing one; the rule `can()` follows.\n */\nexport type OptionsArgs<\n\tC extends ModelConfig,\n\tT extends ObjectTypeOf<C>,\n\tP extends string,\n\tO,\n> = [ObjectTypeOf<C>] extends [T]\n\t? IsSingle<ObjectTypeOf<C>> extends true\n\t\t? PermissionArgs<C, T, P, O>\n\t\t: [options?: LooseOptions]\n\t: PermissionArgs<C, T, P, O>;\n\n/** `OptionsArgs` once `T` is known right: loose when `P` is its whole constraint. */\ntype PermissionArgs<\n\tC extends ModelConfig,\n\tT extends ObjectTypeOf<C>,\n\tP extends string,\n\tO,\n> = [CheckableOf<C, T>] extends [P]\n\t? IsSingle<CheckableOf<C, T>> extends true\n\t\t? StrictArgs<C, T, P, O>\n\t\t: [options?: LooseOptions]\n\t: StrictArgs<C, T, P, O>;\n\ntype StrictArgs<\n\tC extends ModelConfig,\n\tT extends ObjectTypeOf<C>,\n\tP extends string,\n\tO,\n> = [CtxOf<C, T, P>] extends [never]\n\t? [options?: PermissionOptions<C, T, P, O>]\n\t: [options: PermissionOptions<C, T, P, O>];\n\ntype LooseOptions = {\n\treadonly subject?: (c: Context) => unknown;\n\treadonly ctx?: (c: Context, object: never) => unknown;\n};\n\ntype UnionToIntersection<U> = (\n\tU extends unknown\n\t\t? (union: U) => void\n\t\t: never\n) extends (intersection: infer I) => void\n\t? I\n\t: never;\n\n/** Whether `U` is one type rather than a union of several. */\ntype IsSingle<U> = [U] extends [UnionToIntersection<U>] ? true : false;\n\ntype LooseCan = (\n\tsubject: SubjectRef<ModelConfig> | null,\n\tpermission: string,\n\tobject: { readonly type: string; readonly id: string },\n\toptions?: { readonly ctx: unknown },\n) => Promise<boolean>;\n\n/**\n * The middleware that lets a request through only if its subject holds\n * `permission` on an object of `type`: it loads the object once, with `load`,\n * checks it with `access.can`, and hands it to the route as `c.var.object`.\n *\n * | The request | Answered |\n * | --- | --- |\n * | anonymous | 401, no body — before the object is loaded |\n * | `load` answers `null` | 404, no body |\n * | a denial | 403, no body |\n * | allowed | the route runs, `c.var.object` set |\n *\n * One per route: a second one throws a `TypeError`, since both would claim\n * `c.var.object`. A parent object is checked through an arrow in the model.\n *\n * **A failure throws**, as everywhere: a store that cannot answer is\n * `STORE_FAILED`, never a 403 — `janusErrors()` answers it 503.\n */\nexport function permission<\n\tC extends ModelConfig,\n\tconst T extends ObjectTypeOf<C>,\n\tconst P extends CheckableOf<C, T>,\n\tO extends ObjectData<C, T>,\n>(\n\taccess: Pick<Permissions<C>, 'can'>,\n\tpermission: P,\n\ttype: T,\n\tload: (c: Context) => Awaitable<O | null>,\n\t...options: OptionsArgs<C, T, P, O>\n): MiddlewareHandler<{\n\t// biome-ignore lint/style/useNamingConvention: Hono's `Env` names the key, not us.\n\tVariables: { object: O };\n}>;\nexport function permission(\n\taccess: { readonly can: unknown },\n\tpermission: string,\n\ttype: string,\n\tload: (c: Context) => Awaitable<{ readonly id: string } | null>,\n\toptions: {\n\t\treadonly subject?: (\n\t\t\tc: Context,\n\t\t) => Awaitable<SubjectRef<ModelConfig> | null>;\n\t\treadonly ctx?: (c: Context, object: unknown) => Awaitable<unknown>;\n\t} = {},\n): MiddlewareHandler {\n\tconst can = access.can as LooseCan;\n\n\treturn async (c, next) => {\n\t\tif (c.get('object') !== undefined) {\n\t\t\tthrow new TypeError(\n\t\t\t\t'permission(): c.var.object is already set — one permission() per route; check a parent object through an arrow in the model',\n\t\t\t);\n\t\t}\n\t\tconst subject =\n\t\t\toptions.subject === undefined ? userOf(c) : await options.subject(c);\n\t\tif (subject === null || subject === undefined) return c.body(null, 401);\n\n\t\tconst object = await load(c);\n\t\tif (object === null) return c.body(null, 404);\n\n\t\tconst allowed = await can(\n\t\t\tsubject,\n\t\t\tpermission,\n\t\t\tview(object, type),\n\t\t\toptions.ctx === undefined\n\t\t\t\t? undefined\n\t\t\t\t: { ctx: await options.ctx(c, object) },\n\t\t);\n\t\tif (!allowed) return c.body(null, 403);\n\n\t\tc.set('object', object);\n\t\tawait next();\n\t\treturn undefined;\n\t};\n}\n\n/**\n * A `load` reading one path parameter: `find(id)`, or `null` — a 404 — when\n * the route has no such parameter.\n *\n * ```ts\n * permission(access, 'view', 'record', byParam('id', (id) => records.find(id)))\n * ```\n */\nexport function byParam<O>(\n\tname: string,\n\tfind: (id: string) => Awaitable<O | null>,\n): (c: Context) => Awaitable<O | null> {\n\treturn (c) => {\n\t\tconst id = c.req.param(name);\n\t\treturn id === undefined ? null : find(id);\n\t};\n}\n\n/**\n * The object as `can()` reads it: `type` added, every other field read from\n * the loaded object itself — so a getter, a class's `#private` state or an\n * ORM document's accessors answer as they do in the route. A spread would\n * copy own enumerable fields only.\n */\nfunction view(\n\tobject: object,\n\ttype: string,\n): { readonly type: string; readonly id: string } {\n\treturn new Proxy(object, {\n\t\tget: (target, key) => (key === 'type' ? type : Reflect.get(target, key)),\n\t\thas: (target, key) => key === 'type' || Reflect.has(target, key),\n\t}) as { readonly type: string; readonly id: string };\n}\n\n/** `c.var.user`, which `session()` sets — or a wiring error when nothing did. */\nfunction userOf(c: Context): SubjectRef<ModelConfig> | null {\n\tconst user: unknown = c.get('user');\n\tif (user === undefined) {\n\t\tthrow new TypeError(\n\t\t\t'permission(): c.var.user is not set — put session(auth) before it, or pass { subject }',\n\t\t);\n\t}\n\treturn user as SubjectRef<ModelConfig> | null;\n}\n",
6
6
  "import type { MiddlewareHandler } from 'hono';\n\n/** What `provide()` takes: the `janus()` instance, the `permissions()` one, or both. */\nexport interface Instances {\n\treadonly auth?: object;\n\treadonly access?: object;\n}\n\n/**\n * The middleware that puts the instances on the context — `c.var.auth`, and\n * `c.var.access` — so a route writes users and tuples through them rather\n * than through a module import: `c.var.access.grant(record, 'owners', user)`.\n *\n * Only what is passed is set, and typed.\n */\nexport function provide<const I extends Instances>(\n\tinstances: I,\n): MiddlewareHandler<{\n\t// biome-ignore lint/style/useNamingConvention: Hono's `Env` names the key, not us.\n\tVariables: { [K in keyof I & keyof Instances]: I[K] };\n}> {\n\treturn async (c, next) => {\n\t\tif (instances.auth !== undefined) c.set('auth', instances.auth);\n\t\tif (instances.access !== undefined) c.set('access', instances.access);\n\t\tawait next();\n\t};\n}\n",
7
- "import type { Session, SharedApi } from '@nxgt/janus';\nimport type { Context, MiddlewareHandler } from 'hono';\nimport { getCookie } from 'hono/cookie';\n\n/** Anything `janus()` answered: the part of it this package calls. */\nexport type Auth<U extends { readonly type: string }> = Pick<\n\tSharedApi<U>,\n\t'authenticate' | 'signOut' | 'cookie'\n>;\n\n/** The users an `auth` instance knows, as a union narrowed by `user.type`. */\nexport type UserOfAuth<A> = A extends Auth<infer U> ? U : never;\n\n/**\n * What `session()` sets on the context: `c.var.user` and `c.var.session`,\n * both `null` for an anonymous request unless the route requires a user.\n *\n * `new Hono<SessionEnv<typeof auth>>()` types every route of an app that\n * wires `app.use(session(auth))` once.\n */\nexport type SessionEnv<\n\tA,\n\tT extends UserOfAuth<A>['type'] = UserOfAuth<A>['type'],\n\tRequired extends boolean = false,\n> = {\n\t// biome-ignore lint/style/useNamingConvention: Hono's `Env` names the key, not us.\n\tVariables: Required extends true\n\t\t? {\n\t\t\t\tuser: Extract<UserOfAuth<A>, { readonly type: T }>;\n\t\t\t\tsession: Session;\n\t\t\t}\n\t\t: {\n\t\t\t\tuser: Extract<UserOfAuth<A>, { readonly type: T }> | null;\n\t\t\t\tsession: Session | null;\n\t\t\t};\n};\n\nexport interface SessionOptions<T extends string> {\n\t/** Only a user of this type is authenticated here; any other is anonymous. */\n\treadonly type?: T;\n\t/**\n\t * `true`: an anonymous request is answered 401, with no body, and the\n\t * route never runs — so `c.var.user` is never `null` in it. Only a literal\n\t * `true` types it so: a computed `boolean` leaves `c.var.user` nullable.\n\t */\n\treadonly required?: boolean;\n}\n\n/**\n * The middleware that reads who a request belongs to — `auth.authenticate`\n * on the request — and sets `c.var.user` and `c.var.session`.\n *\n * **An outage is not anonymous**: a store that cannot answer rejects with\n * `STORE_FAILED`, and this middleware lets it through to `app.onError` —\n * `janusErrors()` answers it 503, never 401.\n *\n * A session renewed in passing is sent again as a cookie, after the route\n * ran — but only to a request that presented it as one: a client that sends\n * `Authorization: Bearer` is not handed a cookie it never asked for. And\n * never over a session cookie the route set itself — `sendSession` after a\n * sign-in, `signOut`'s clearing — which a browser would otherwise overwrite.\n */\nexport function session<\n\tA extends Auth<{ readonly type: string }>,\n\tconst T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type'],\n>(\n\tauth: A,\n\toptions: SessionOptions<T> & { readonly required: true },\n): MiddlewareHandler<SessionEnv<A, T, true>>;\nexport function session<\n\tA extends Auth<{ readonly type: string }>,\n\tconst T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type'],\n>(\n\tauth: A,\n\toptions?: SessionOptions<T> & { readonly required?: false },\n): MiddlewareHandler<SessionEnv<A, T>>;\nexport function session<\n\tA extends Auth<{ readonly type: string }>,\n\tconst T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type'],\n>(auth: A, options: SessionOptions<T>): MiddlewareHandler<SessionEnv<A, T>>;\nexport function session(\n\tauth: Auth<{ readonly type: string }>,\n\toptions: SessionOptions<string> = {},\n): MiddlewareHandler {\n\treturn async (c, next) => {\n\t\tconst current = await auth.authenticate(\n\t\t\tc.req.raw,\n\t\t\toptions.type === undefined ? undefined : { type: options.type },\n\t\t);\n\n\t\tif (current === null && options.required === true) {\n\t\t\treturn c.body(null, 401);\n\t\t}\n\t\tc.set('user', current?.user ?? null);\n\t\tc.set('session', current?.session ?? null);\n\n\t\tawait next();\n\n\t\t// The route's own cookie wins: a sign-in or a sign-out behind this\n\t\t// middleware must not be undone by the session it replaced.\n\t\tconst name = `${auth.cookie.name}=`;\n\t\tif (\n\t\t\tcurrent?.renewed === true &&\n\t\t\tgetCookie(c, auth.cookie.name) === current.token &&\n\t\t\t!c.res.headers.getSetCookie().some((value) => value.startsWith(name))\n\t\t) {\n\t\t\tsendSession(c, auth, current);\n\t\t}\n\t};\n}\n\n/**\n * Sends the session cookie — after `signUp`, `signIn`, or anything else that\n * answered a session token and its session — and answers the user. Appends: a\n * cookie set before stays.\n *\n * ```ts\n * const user = sendSession(c, auth, await auth.signIn({ email, password }));\n * return c.json({ id: user.id }); // the token is in the cookie, never in the body\n * ```\n */\nexport function sendSession<U>(\n\tc: Context,\n\tauth: Pick<Auth<{ readonly type: string }>, 'cookie'>,\n\tsignedIn: {\n\t\treadonly token: string;\n\t\treadonly session: Session;\n\t\treadonly user: U;\n\t},\n): U {\n\tc.header(\n\t\t'Set-Cookie',\n\t\tauth.cookie.serialize(signedIn.token, signedIn.session),\n\t\t{\n\t\t\tappend: true,\n\t\t},\n\t);\n\treturn signedIn.user;\n}\n\n/**\n * Revokes the session the request presents and clears the cookie — cleared\n * whatever the answer, so a browser holding a stale cookie drops it too.\n * `false` when the request presented no session, or an unknown one.\n */\nexport async function signOut(\n\tc: Context,\n\tauth: Pick<Auth<{ readonly type: string }>, 'signOut' | 'cookie'>,\n): Promise<boolean> {\n\tconst revoked = await auth.signOut(c.req.raw);\n\tc.header('Set-Cookie', auth.cookie.clear(), { append: true });\n\treturn revoked;\n}\n",
8
- "import type { Session } from '@nxgt/janus';\nimport type {\n\tCheckableOf,\n\tModelConfig,\n\tObjectTypeOf,\n\tPermissions,\n} from '@nxgt/janus/permissions';\nimport type { Context, MiddlewareHandler } from 'hono';\nimport {\n\ttype Awaitable,\n\ttype ObjectData,\n\ttype OptionsArgs,\n\tpermission,\n} from './permission';\nimport { provide } from './provide';\nimport {\n\ttype Auth,\n\ttype SessionEnv,\n\ttype SessionOptions,\n\tsendSession,\n\tsession,\n\tsignOut,\n\ttype UserOfAuth,\n} from './session';\n\n/** `session(auth, options?)`, with `auth` bound: the same three forms. */\nexport interface BoundSession<A> {\n\t<const T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']>(\n\t\toptions: SessionOptions<T> & { readonly required: true },\n\t): MiddlewareHandler<SessionEnv<A, T, true>>;\n\t<const T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']>(\n\t\toptions?: SessionOptions<T> & { readonly required?: false },\n\t): MiddlewareHandler<SessionEnv<A, T>>;\n\t<const T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']>(\n\t\toptions: SessionOptions<T>,\n\t): MiddlewareHandler<SessionEnv<A, T>>;\n}\n\n/** `permission(access, …)`, with `access` bound. */\nexport type BoundPermission<C extends ModelConfig> = <\n\tconst T extends ObjectTypeOf<C>,\n\tconst P extends CheckableOf<C, T>,\n\tO extends ObjectData<C, T>,\n>(\n\tpermission: P,\n\ttype: T,\n\tload: (c: Context) => Awaitable<O | null>,\n\t...options: OptionsArgs<C, T, P, O>\n) => MiddlewareHandler<{\n\t// biome-ignore lint/style/useNamingConvention: Hono's `Env` names the key, not us.\n\tVariables: { object: O };\n}>;\n\n/** The functions of this package that take `auth`, with `auth` bound. */\nexport interface BoundAuth<A> {\n\treadonly session: BoundSession<A>;\n\t/** `sendSession(c, auth, signedIn)`: sets the cookie, answers the user. */\n\treadonly sendSession: <U>(\n\t\tc: Context,\n\t\tsignedIn: {\n\t\t\treadonly token: string;\n\t\t\treadonly session: Session;\n\t\t\treadonly user: U;\n\t\t},\n\t) => U;\n\t/** `signOut(c, auth)`. */\n\treadonly signOut: (c: Context) => Promise<boolean>;\n}\n\n/** What `bindJanus()` takes: the `janus()` instance, the `permissions()` one, or both. */\nexport interface Bindable {\n\treadonly auth?: Auth<{ readonly type: string }>;\n\t/** What `permission()` takes: a `permissions()` instance, or its `can`. */\n\treadonly access?: { readonly can: unknown };\n}\n\n/**\n * What `bindJanus(instances)` answers: `provide()` always; `session`,\n * `sendSession` and `signOut` with `auth`; `permission` with `access`.\n */\nexport type Bound<I extends Bindable> = {\n\t/** `provide(instances)`: every instance bound, on the context. */\n\treadonly provide: () => MiddlewareHandler<{\n\t\t// biome-ignore lint/style/useNamingConvention: Hono's `Env` names the key, not us.\n\t\tVariables: { [K in keyof I & keyof Bindable]: I[K] };\n\t}>;\n} & (I['auth'] extends Auth<{ readonly type: string }>\n\t? BoundAuth<I['auth']>\n\t: unknown) &\n\t(I['access'] extends Pick<Permissions<infer C>, 'can'>\n\t\t? { readonly permission: BoundPermission<C> }\n\t\t: unknown);\n\n/**\n * This package's functions with the instances bound once, so no route\n * repeats them:\n *\n * ```ts\n * const j = bindJanus({ auth, access });\n * const app = new Hono()\n * .use(j.session(), j.provide())\n * .get('/records/:id', j.permission('view', 'record', recordOf), (c) => c.json(c.var.object));\n * ```\n *\n * The same functions, typed the same way: `j.session({ required: true })` is\n * `session(auth, { required: true })`. Only what is given is bound — a\n * `permission` without `access` is absent from the type.\n */\nexport function bindJanus<const I extends Bindable>(instances: I): Bound<I> {\n\tconst { auth, access } = instances;\n\tconst bound: Record<string, unknown> = {\n\t\tprovide: () => provide(instances),\n\t};\n\tif (auth !== undefined) {\n\t\tbound.session = (options: SessionOptions<string> = {}) =>\n\t\t\tsession(auth, options);\n\t\tbound.sendSession = (\n\t\t\tc: Context,\n\t\t\tsignedIn: Parameters<typeof sendSession>[2],\n\t\t) => sendSession(c, auth, signedIn);\n\t\tbound.signOut = (c: Context) => signOut(c, auth);\n\t}\n\tif (access !== undefined) {\n\t\tconst guard = permission as unknown as (\n\t\t\taccess: object,\n\t\t\t...rest: unknown[]\n\t\t) => MiddlewareHandler;\n\t\tbound.permission = (...rest: unknown[]) => guard(access, ...rest);\n\t}\n\treturn bound as Bound<I>;\n}\n",
9
- "import {\n\tJanusError,\n\ttype JanusErrorCode,\n\tstatusOf as janusStatusOf,\n} from '@nxgt/janus';\nimport type { Context, ErrorHandler } from 'hono';\nimport type { HTTPException } from 'hono/http-exception';\nimport type { ContentfulStatusCode } from 'hono/utils/http-status';\n\n/**\n * The status each code deserves: `@nxgt/janus`'s own table, typed as the\n * `ContentfulStatusCode` `c.json()` takes. Kept as an export of its own so a\n * route that answers by hand reads the same status `janusErrors()` does.\n *\n * `STORE_FAILED` is 503 and nothing else — **an outage is not a negative\n * answer**, and a 401 or a 404 would tell every user they do not exist.\n */\nexport function statusOf(code: JanusErrorCode): ContentfulStatusCode {\n\treturn janusStatusOf(code);\n}\n\n/**\n * The body a refusal is answered with: its `code`, and only what the client\n * can act on — the fields that failed the schema, the policy's minimum, the\n * attempts a challenge has left, the seconds a throttled sign-in waits.\n * Never `reason`, `login`, a hash prefix or a cause: those are for your logs.\n */\nexport function bodyOf(error: JanusError): {\n\treadonly code: JanusErrorCode;\n\treadonly issues?: JanusError['issues'];\n\treadonly minLength?: number;\n\treadonly attemptsLeft?: number;\n\treadonly retryAfter?: number;\n} {\n\tswitch (error.code) {\n\t\tcase 'USER_INVALID':\n\t\t\treturn { code: error.code, issues: error.issues ?? [] };\n\t\tcase 'PASSWORD_TOO_SHORT':\n\t\t\treturn error.minLength === undefined\n\t\t\t\t? { code: error.code }\n\t\t\t\t: { code: error.code, minLength: error.minLength };\n\t\tcase 'CODE_INVALID':\n\t\t\treturn error.attemptsLeft === undefined\n\t\t\t\t? { code: error.code }\n\t\t\t\t: { code: error.code, attemptsLeft: error.attemptsLeft };\n\t\tcase 'CREDENTIALS_INVALID':\n\t\t\treturn error.retryAfter === undefined\n\t\t\t\t? { code: error.code }\n\t\t\t\t: { code: error.code, retryAfter: error.retryAfter };\n\t\tdefault:\n\t\t\treturn { code: error.code };\n\t}\n}\n\n/**\n * Hono's own answer to an error it was not told about, copied from its\n * default handler: the response of an `HTTPException` — merged with the\n * headers the route set before throwing — and a logged 500 for anything else.\n */\nfunction honoDefault(error: Error | HTTPException, c: Context): Response {\n\tif ('getResponse' in error) {\n\t\tconst response = error.getResponse();\n\t\treturn c.newResponse(response.body, response);\n\t}\n\tconsole.error(error);\n\treturn c.text('Internal Server Error', 500);\n}\n\n/** What `janusErrors()` takes. */\nexport interface JanusErrorsOptions {\n\t/**\n\t * Called with every `JanusError` answered 5xx — `STORE_FAILED`,\n\t * `UNSUPPORTED`, `PERMISSION_DEPTH`: the ones the server must fix, not the\n\t * client — before it is answered. Its `slot`, `operation`, `reason` and\n\t * `cause` are for your logs; the body never carries them.\n\t *\n\t * **It cannot stop the answer**: one that throws, or rejects, is a\n\t * `process.emitWarning`, and the 503 is sent all the same.\n\t */\n\treadonly report?: (error: JanusError, c: Context) => unknown;\n\t/** Every error that is not a `JanusError`. Hono's own handling when absent. */\n\treadonly fallback?: ErrorHandler;\n}\n\n/**\n * An `app.onError` handler: every `JanusError` answered with its status and\n * `bodyOf(error)` — and a throttled sign-in with a `Retry-After` header, its\n * `retryAfter` in seconds; anything else handed to `fallback` — Hono's own\n * behaviour when absent.\n *\n * Works for either side of `@nxgt/janus`: `can()`'s `STORE_FAILED` is a 503\n * here too, never a 403.\n *\n * ```ts\n * app.onError(janusErrors({ report: (error) => logger.error(error) }));\n * ```\n */\nexport function janusErrors(options: JanusErrorsOptions = {}): ErrorHandler {\n\tconst { report, fallback = honoDefault } = options;\n\treturn (error, c) => {\n\t\tif (!(error instanceof JanusError)) return fallback(error, c);\n\t\tconst status = statusOf(error.code);\n\t\tif (status >= 500 && report !== undefined) reportSafely(report, error, c);\n\t\tconst headers =\n\t\t\terror.retryAfter === undefined\n\t\t\t\t? undefined\n\t\t\t\t: { 'retry-after': String(error.retryAfter) };\n\t\treturn c.json(bodyOf(error), status, headers);\n\t};\n}\n\n/** `report`, whose own failure is a warning — never a lost answer. */\nfunction reportSafely(\n\treport: NonNullable<JanusErrorsOptions['report']>,\n\terror: JanusError,\n\tc: Context,\n): void {\n\tconst warn = (failure: unknown) =>\n\t\tprocess.emitWarning(\n\t\t\t`janusErrors: report failed on ${error.code}: ${failure instanceof Error ? failure.name : typeof failure}`,\n\t\t);\n\ttry {\n\t\t// A thenable of any make, as a native promise: its rejection warns too.\n\t\tPromise.resolve(report(error, c)).then(undefined, warn);\n\t} catch (failure) {\n\t\twarn(failure);\n\t}\n}\n",
10
- "import {\n\ttype Clock,\n\ttype Duration,\n\tparseDuration,\n\ttype Session,\n\tStepUpRequiredError,\n\tsystemClock,\n} from '@nxgt/janus';\nimport type { MiddlewareHandler } from 'hono';\n\n/** What `fresh()` takes besides `maxAge`. */\nexport interface FreshOptions {\n\t/** The clock given to `janus()`, when it is not the system's: `fixedClock` in a spec. */\n\treadonly clock?: Clock;\n}\n\n/**\n * The middleware that lets a route run only for a session that proved who\n * it is less than `maxAge` ago — signed in, or confirmed by\n * `auth.stepUp.confirm`, since. Behind `session(auth)`, which sets\n * `c.var.session`:\n *\n * ```ts\n * app.delete('/account', session(auth, { required: true }), fresh('10m'), deleteAccount);\n * ```\n *\n * A session older than that throws `STEP_UP_REQUIRED`, which `janusErrors()`\n * answers 403 with `{ code: 'STEP_UP_REQUIRED' }`: the client asks for a\n * step-up, then sends the request again. An anonymous request is answered\n * 401, with no body, as `session(auth, { required: true })` answers it.\n *\n * A `maxAge` that is not a duration is a `TypeError` here, when the app is\n * wired — not on the first request.\n * No `session()` before it is a `TypeError` at the first request — a\n * wiring error, not an anonymous 401 — as for `permission()`.\n */\nexport function fresh(\n\tmaxAge: Duration,\n\toptions: FreshOptions = {},\n): MiddlewareHandler<{\n\t// biome-ignore lint/style/useNamingConvention: Hono's `Env` names the key, not us.\n\tVariables: { session: Session | null };\n}> {\n\tconst maxAgeMs = parseDuration(maxAge, 'fresh: maxAge');\n\treturn async (c, next) => {\n\t\tconst current = c.var.session;\n\t\tif (current === undefined) {\n\t\t\tthrow new TypeError(\n\t\t\t\t'fresh(): c.var.session is not set — put session(auth) before it',\n\t\t\t);\n\t\t}\n\t\tif (current === null) return c.body(null, 401);\n\t\t// `assertFresh`'s rule, with a message naming the call written here.\n\t\tconst now = (options.clock ?? systemClock).now().getTime();\n\t\tif (now - current.authenticatedAt.getTime() >= maxAgeMs) {\n\t\t\tthrow new StepUpRequiredError(\n\t\t\t\t'fresh(): the session proved who it is longer ago than maxAge — confirm with a step-up',\n\t\t\t\t{ userId: current.userId },\n\t\t\t);\n\t\t}\n\t\tawait next();\n\t};\n}\n"
7
+ "import type { Session, SharedApi } from '@nxgt/janus';\nimport type { Context, MiddlewareHandler } from 'hono';\nimport { getCookie } from 'hono/cookie';\nimport { type DeviceCookieOptions, sendDevice } from './device';\n\n/** Anything `janus()` answered: the part of it this package calls. */\nexport type Auth<U extends { readonly type: string }> = Pick<\n\tSharedApi<U>,\n\t'authenticate' | 'signOut' | 'cookie'\n>;\n\n/** The users an `auth` instance knows, as a union narrowed by `user.type`. */\nexport type UserOfAuth<A> = A extends Auth<infer U> ? U : never;\n\n/**\n * What `session()` sets on the context: `c.var.user` and `c.var.session`,\n * both `null` for an anonymous request unless the route requires a user.\n *\n * `new Hono<SessionEnv<typeof auth>>()` types every route of an app that\n * wires `app.use(session(auth))` once.\n */\nexport type SessionEnv<\n\tA,\n\tT extends UserOfAuth<A>['type'] = UserOfAuth<A>['type'],\n\tRequired extends boolean = false,\n> = {\n\t// biome-ignore lint/style/useNamingConvention: Hono's `Env` names the key, not us.\n\tVariables: Required extends true\n\t\t? {\n\t\t\t\tuser: Extract<UserOfAuth<A>, { readonly type: T }>;\n\t\t\t\tsession: Session;\n\t\t\t}\n\t\t: {\n\t\t\t\tuser: Extract<UserOfAuth<A>, { readonly type: T }> | null;\n\t\t\t\tsession: Session | null;\n\t\t\t};\n};\n\nexport interface SessionOptions<T extends string> {\n\t/** Only a user of this type is authenticated here; any other is anonymous. */\n\treadonly type?: T;\n\t/**\n\t * `true`: an anonymous request is answered 401, with no body, and the\n\t * route never runs — so `c.var.user` is never `null` in it. Only a literal\n\t * `true` types it so: a computed `boolean` leaves `c.var.user` nullable.\n\t */\n\treadonly required?: boolean;\n}\n\n/**\n * The middleware that reads who a request belongs to — `auth.authenticate`\n * on the request — and sets `c.var.user` and `c.var.session`.\n *\n * **An outage is not anonymous**: a store that cannot answer rejects with\n * `STORE_FAILED`, and this middleware lets it through to `app.onError` —\n * `janusErrors()` answers it 503, never 401.\n *\n * A session renewed in passing is sent again as a cookie, after the route\n * ran — but only to a request that presented it as one: a client that sends\n * `Authorization: Bearer` is not handed a cookie it never asked for. And\n * never over a session cookie the route set itself — `sendSession` after a\n * sign-in, `signOut`'s clearing — which a browser would otherwise overwrite.\n */\nexport function session<\n\tA extends Auth<{ readonly type: string }>,\n\tconst T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type'],\n>(\n\tauth: A,\n\toptions: SessionOptions<T> & { readonly required: true },\n): MiddlewareHandler<SessionEnv<A, T, true>>;\nexport function session<\n\tA extends Auth<{ readonly type: string }>,\n\tconst T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type'],\n>(\n\tauth: A,\n\toptions?: SessionOptions<T> & { readonly required?: false },\n): MiddlewareHandler<SessionEnv<A, T>>;\nexport function session<\n\tA extends Auth<{ readonly type: string }>,\n\tconst T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type'],\n>(auth: A, options: SessionOptions<T>): MiddlewareHandler<SessionEnv<A, T>>;\nexport function session(\n\tauth: Auth<{ readonly type: string }>,\n\toptions: SessionOptions<string> = {},\n): MiddlewareHandler {\n\treturn async (c, next) => {\n\t\tconst current = await auth.authenticate(\n\t\t\tc.req.raw,\n\t\t\toptions.type === undefined ? undefined : { type: options.type },\n\t\t);\n\n\t\tif (current === null && options.required === true) {\n\t\t\treturn c.body(null, 401);\n\t\t}\n\t\tc.set('user', current?.user ?? null);\n\t\tc.set('session', current?.session ?? null);\n\n\t\tawait next();\n\n\t\t// The route's own cookie wins: a sign-in or a sign-out behind this\n\t\t// middleware must not be undone by the session it replaced.\n\t\tconst name = `${auth.cookie.name}=`;\n\t\tif (\n\t\t\tcurrent?.renewed === true &&\n\t\t\tgetCookie(c, auth.cookie.name) === current.token &&\n\t\t\t!c.res.headers.getSetCookie().some((value) => value.startsWith(name))\n\t\t) {\n\t\t\tsendSession(c, auth, current);\n\t\t}\n\t\treturn undefined;\n\t};\n}\n\n/** What `sendSession` takes besides: the device cookie's options. */\nexport interface SendSessionOptions {\n\treadonly device?: DeviceCookieOptions;\n}\n\n/**\n * Sends the session cookie — after `signUp`, `signIn`, or anything else that\n * answered a session token and its session — and answers the user. Appends: a\n * cookie set before stays.\n *\n * When the answer carries a `deviceToken` — the sign-in was given a\n * `device` — the device cookie is set too, or set again so it lasts another\n * four hundred days.\n *\n * ```ts\n * const signedIn = await auth.signIn({ email, password }, { device: deviceOf(c) });\n * const user = sendSession(c, auth, signedIn);\n * return c.json({ id: user.id }); // the token is in the cookie, never in the body\n * ```\n */\nexport function sendSession<U>(\n\tc: Context,\n\tauth: Pick<Auth<{ readonly type: string }>, 'cookie'>,\n\tsignedIn: {\n\t\treadonly token: string;\n\t\treadonly session: Session;\n\t\treadonly user: U;\n\t\treadonly deviceToken?: string | null;\n\t},\n\toptions: SendSessionOptions = {},\n): U {\n\tc.header(\n\t\t'Set-Cookie',\n\t\tauth.cookie.serialize(signedIn.token, signedIn.session),\n\t\t{\n\t\t\tappend: true,\n\t\t},\n\t);\n\tif (typeof signedIn.deviceToken === 'string') {\n\t\tsendDevice(c, signedIn.deviceToken, options.device);\n\t}\n\treturn signedIn.user;\n}\n\n/**\n * Revokes the session the request presents and clears the cookie — cleared\n * whatever the answer, so a browser holding a stale cookie drops it too.\n * `false` when the request presented no session, or an unknown one.\n */\nexport async function signOut(\n\tc: Context,\n\tauth: Pick<Auth<{ readonly type: string }>, 'signOut' | 'cookie'>,\n): Promise<boolean> {\n\tconst revoked = await auth.signOut(c.req.raw);\n\tc.header('Set-Cookie', auth.cookie.clear(), { append: true });\n\treturn revoked;\n}\n",
8
+ "import type { Context } from 'hono';\nimport { getCookie, setCookie } from 'hono/cookie';\n\n/** The device cookie's name when none is given. */\nexport const DEVICE_COOKIE = 'janus-device';\n\n/** Four hundred days: the longest a browser keeps a cookie. */\nconst MAX_AGE = 400 * 24 * 60 * 60;\n\n/**\n * The long-lived cookie that holds the device token `@nxgt/janus` answers\n * as `deviceToken`. Every default is the strict one, as for the session\n * cookie; give `deviceOf` and `sendSession` the same options.\n */\nexport interface DeviceCookieOptions {\n\t/** `'janus-device'` when absent. */\n\treadonly name?: string;\n\treadonly domain?: string;\n\t/** `'/'` when absent. */\n\treadonly path?: string;\n\t/** `'Lax'` when absent. */\n\treadonly sameSite?: 'Lax' | 'Strict' | 'None';\n\t/** `true` when absent. */\n\treadonly secure?: boolean;\n\t/** In seconds. Four hundred days when absent — the longest a browser keeps one. */\n\treadonly maxAge?: number;\n}\n\n/**\n * The device token the request's cookie holds, or `null` when it holds none:\n * what a sign-in takes as `device`.\n *\n * ```ts\n * const signedIn = await auth.signIn({ email, password }, { device: deviceOf(c) });\n * ```\n */\nexport function deviceOf(\n\tc: Context,\n\toptions: DeviceCookieOptions = {},\n): string | null {\n\tconst token = getCookie(c, options.name ?? DEVICE_COOKIE);\n\treturn token === undefined || token === '' ? null : token;\n}\n\n/** Sets the device cookie, or sets it again so it lasts another `maxAge`. Appends. */\nexport function sendDevice(\n\tc: Context,\n\tdeviceToken: string,\n\toptions: DeviceCookieOptions = {},\n): void {\n\tsetCookie(c, options.name ?? DEVICE_COOKIE, deviceToken, {\n\t\tpath: options.path ?? '/',\n\t\t...(options.domain === undefined ? {} : { domain: options.domain }),\n\t\tsameSite: options.sameSite ?? 'Lax',\n\t\tsecure: options.secure ?? true,\n\t\thttpOnly: true,\n\t\tmaxAge: options.maxAge ?? MAX_AGE,\n\t});\n}\n",
9
+ "import type { Session } from '@nxgt/janus';\nimport type {\n\tCheckableOf,\n\tModelConfig,\n\tObjectTypeOf,\n\tPermissions,\n} from '@nxgt/janus/permissions';\nimport type { Context, MiddlewareHandler } from 'hono';\nimport {\n\ttype Awaitable,\n\ttype ObjectData,\n\ttype OptionsArgs,\n\tpermission,\n} from './permission';\nimport { provide } from './provide';\nimport {\n\ttype Auth,\n\ttype SendSessionOptions,\n\ttype SessionEnv,\n\ttype SessionOptions,\n\tsendSession,\n\tsession,\n\tsignOut,\n\ttype UserOfAuth,\n} from './session';\n\n/** `session(auth, options?)`, with `auth` bound: the same three forms. */\nexport interface BoundSession<A> {\n\t<const T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']>(\n\t\toptions: SessionOptions<T> & { readonly required: true },\n\t): MiddlewareHandler<SessionEnv<A, T, true>>;\n\t<const T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']>(\n\t\toptions?: SessionOptions<T> & { readonly required?: false },\n\t): MiddlewareHandler<SessionEnv<A, T>>;\n\t<const T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']>(\n\t\toptions: SessionOptions<T>,\n\t): MiddlewareHandler<SessionEnv<A, T>>;\n}\n\n/** `permission(access, …)`, with `access` bound. */\nexport type BoundPermission<C extends ModelConfig> = <\n\tconst T extends ObjectTypeOf<C>,\n\tconst P extends CheckableOf<C, T>,\n\tO extends ObjectData<C, T>,\n>(\n\tpermission: P,\n\ttype: T,\n\tload: (c: Context) => Awaitable<O | null>,\n\t...options: OptionsArgs<C, T, P, O>\n) => MiddlewareHandler<{\n\t// biome-ignore lint/style/useNamingConvention: Hono's `Env` names the key, not us.\n\tVariables: { object: O };\n}>;\n\n/** The functions of this package that take `auth`, with `auth` bound. */\nexport interface BoundAuth<A> {\n\treadonly session: BoundSession<A>;\n\t/** `sendSession(c, auth, signedIn, options?)`: sets the cookies, answers the user. */\n\treadonly sendSession: <U>(\n\t\tc: Context,\n\t\tsignedIn: {\n\t\t\treadonly token: string;\n\t\t\treadonly session: Session;\n\t\t\treadonly user: U;\n\t\t\treadonly deviceToken?: string | null;\n\t\t},\n\t\toptions?: SendSessionOptions,\n\t) => U;\n\t/** `signOut(c, auth)`. */\n\treadonly signOut: (c: Context) => Promise<boolean>;\n}\n\n/** What `bindJanus()` takes: the `janus()` instance, the `permissions()` one, or both. */\nexport interface Bindable {\n\treadonly auth?: Auth<{ readonly type: string }>;\n\t/** What `permission()` takes: a `permissions()` instance, or its `can`. */\n\treadonly access?: { readonly can: unknown };\n}\n\n/**\n * What `bindJanus(instances)` answers: `provide()` always; `session`,\n * `sendSession` and `signOut` with `auth`; `permission` with `access`.\n */\nexport type Bound<I extends Bindable> = {\n\t/** `provide(instances)`: every instance bound, on the context. */\n\treadonly provide: () => MiddlewareHandler<{\n\t\t// biome-ignore lint/style/useNamingConvention: Hono's `Env` names the key, not us.\n\t\tVariables: { [K in keyof I & keyof Bindable]: I[K] };\n\t}>;\n} & (I['auth'] extends Auth<{ readonly type: string }>\n\t? BoundAuth<I['auth']>\n\t: unknown) &\n\t(I['access'] extends Pick<Permissions<infer C>, 'can'>\n\t\t? { readonly permission: BoundPermission<C> }\n\t\t: unknown);\n\n/**\n * This package's functions with the instances bound once, so no route\n * repeats them:\n *\n * ```ts\n * const j = bindJanus({ auth, access });\n * const app = new Hono()\n * .use(j.session(), j.provide())\n * .get('/records/:id', j.permission('view', 'record', recordOf), (c) => c.json(c.var.object));\n * ```\n *\n * The same functions, typed the same way: `j.session({ required: true })` is\n * `session(auth, { required: true })`. Only what is given is bound — a\n * `permission` without `access` is absent from the type.\n */\nexport function bindJanus<const I extends Bindable>(instances: I): Bound<I> {\n\tconst { auth, access } = instances;\n\tconst bound: Record<string, unknown> = {\n\t\tprovide: () => provide(instances),\n\t};\n\tif (auth !== undefined) {\n\t\tbound['session'] = (options: SessionOptions<string> = {}) =>\n\t\t\tsession(auth, options);\n\t\tbound['sendSession'] = (\n\t\t\tc: Context,\n\t\t\tsignedIn: Parameters<typeof sendSession>[2],\n\t\t\toptions?: SendSessionOptions,\n\t\t) => sendSession(c, auth, signedIn, options);\n\t\tbound['signOut'] = (c: Context) => signOut(c, auth);\n\t}\n\tif (access !== undefined) {\n\t\tconst guard = permission as unknown as (\n\t\t\taccess: object,\n\t\t\t...rest: unknown[]\n\t\t) => MiddlewareHandler;\n\t\tbound['permission'] = (...rest: unknown[]) => guard(access, ...rest);\n\t}\n\treturn bound as Bound<I>;\n}\n",
10
+ "import {\n\tJanusError,\n\ttype JanusErrorCode,\n\tstatusOf as janusStatusOf,\n} from '@nxgt/janus';\nimport type { Context, ErrorHandler } from 'hono';\nimport type { HTTPException } from 'hono/http-exception';\nimport type { ContentfulStatusCode } from 'hono/utils/http-status';\n\n/**\n * The status each code deserves: `@nxgt/janus`'s own table, typed as the\n * `ContentfulStatusCode` `c.json()` takes. Kept as an export of its own so a\n * route that answers by hand reads the same status `janusErrors()` does.\n *\n * `STORE_FAILED` is 503 and nothing else — **an outage is not a negative\n * answer**, and a 401 or a 404 would tell every user they do not exist.\n */\nexport function statusOf(code: JanusErrorCode): ContentfulStatusCode {\n\treturn janusStatusOf(code);\n}\n\n/**\n * The body a refusal is answered with: its `code`, and only what the client\n * can act on — the fields that failed the schema, the policy's minimum, the\n * attempts a challenge has left, the seconds a throttled sign-in or a\n * throttled request for an e-mail waits.\n * Never `reason`, `login`, a hash prefix or a cause: those are for your logs.\n */\nexport function bodyOf(error: JanusError): {\n\treadonly code: JanusErrorCode;\n\treadonly issues?: JanusError['issues'];\n\treadonly minLength?: number;\n\treadonly attemptsLeft?: number;\n\treadonly retryAfter?: number;\n} {\n\tswitch (error.code) {\n\t\tcase 'USER_INVALID':\n\t\t\treturn { code: error.code, issues: error.issues ?? [] };\n\t\tcase 'PASSWORD_TOO_SHORT':\n\t\t\treturn error.minLength === undefined\n\t\t\t\t? { code: error.code }\n\t\t\t\t: { code: error.code, minLength: error.minLength };\n\t\tcase 'CODE_INVALID':\n\t\t\treturn error.attemptsLeft === undefined\n\t\t\t\t? { code: error.code }\n\t\t\t\t: { code: error.code, attemptsLeft: error.attemptsLeft };\n\t\tcase 'CREDENTIALS_INVALID':\n\t\tcase 'MAIL_THROTTLED':\n\t\t\treturn error.retryAfter === undefined\n\t\t\t\t? { code: error.code }\n\t\t\t\t: { code: error.code, retryAfter: error.retryAfter };\n\t\tdefault:\n\t\t\treturn { code: error.code };\n\t}\n}\n\n/**\n * Hono's own answer to an error it was not told about, copied from its\n * default handler: the response of an `HTTPException` — merged with the\n * headers the route set before throwing — and a logged 500 for anything else.\n */\nfunction honoDefault(error: Error | HTTPException, c: Context): Response {\n\tif ('getResponse' in error) {\n\t\tconst response = error.getResponse();\n\t\treturn c.newResponse(response.body, response);\n\t}\n\tconsole.error(error);\n\treturn c.text('Internal Server Error', 500);\n}\n\n/** What `janusErrors()` takes. */\nexport interface JanusErrorsOptions {\n\t/**\n\t * Called with every `JanusError` answered 5xx — `STORE_FAILED`,\n\t * `UNSUPPORTED`, `PERMISSION_DEPTH`: the ones the server must fix, not the\n\t * client — before it is answered. Its `slot`, `operation`, `reason` and\n\t * `cause` are for your logs; the body never carries them.\n\t *\n\t * **It cannot stop the answer**: one that throws, or rejects, is a\n\t * `process.emitWarning`, and the 503 is sent all the same.\n\t */\n\treadonly report?: (error: JanusError, c: Context) => unknown;\n\t/** Every error that is not a `JanusError`. Hono's own handling when absent. */\n\treadonly fallback?: ErrorHandler;\n}\n\n/**\n * An `app.onError` handler: every `JanusError` answered with its status and\n * `bodyOf(error)` — and a throttled sign-in, or a request past its e-mails\n * (`MAIL_THROTTLED`, 429), with a `Retry-After` header, its `retryAfter` in\n * seconds; anything else handed to `fallback` — Hono's own\n * behaviour when absent.\n *\n * Works for either side of `@nxgt/janus`: `can()`'s `STORE_FAILED` is a 503\n * here too, never a 403.\n *\n * ```ts\n * app.onError(janusErrors({ report: (error) => logger.error(error) }));\n * ```\n */\nexport function janusErrors(options: JanusErrorsOptions = {}): ErrorHandler {\n\tconst { report, fallback = honoDefault } = options;\n\treturn (error, c) => {\n\t\tif (!(error instanceof JanusError)) return fallback(error, c);\n\t\tconst status = statusOf(error.code);\n\t\tif (status >= 500 && report !== undefined) reportSafely(report, error, c);\n\t\tconst headers =\n\t\t\terror.retryAfter === undefined\n\t\t\t\t? undefined\n\t\t\t\t: { 'retry-after': String(error.retryAfter) };\n\t\treturn c.json(bodyOf(error), status, headers);\n\t};\n}\n\n/** `report`, whose own failure is a warning — never a lost answer. */\nfunction reportSafely(\n\treport: NonNullable<JanusErrorsOptions['report']>,\n\terror: JanusError,\n\tc: Context,\n): void {\n\tconst warn = (failure: unknown) =>\n\t\tprocess.emitWarning(\n\t\t\t`janusErrors: report failed on ${error.code}: ${failure instanceof Error ? failure.name : typeof failure}`,\n\t\t);\n\ttry {\n\t\t// A thenable of any make, as a native promise: its rejection warns too.\n\t\tPromise.resolve(report(error, c)).then(undefined, warn);\n\t} catch (failure) {\n\t\twarn(failure);\n\t}\n}\n",
11
+ "import {\n\ttype Clock,\n\ttype Duration,\n\tparseDuration,\n\ttype Session,\n\tStepUpRequiredError,\n\tsystemClock,\n} from '@nxgt/janus';\nimport type { MiddlewareHandler } from 'hono';\n\n/** What `fresh()` takes besides `maxAge`. */\nexport interface FreshOptions {\n\t/** The clock given to `janus()`, when it is not the system's: `fixedClock` in a spec. */\n\treadonly clock?: Clock;\n}\n\n/**\n * The middleware that lets a route run only for a session that proved who\n * it is less than `maxAge` ago — signed in, or confirmed by\n * `auth.stepUp.confirm`, since. Behind `session(auth)`, which sets\n * `c.var.session`:\n *\n * ```ts\n * app.delete('/account', session(auth, { required: true }), fresh('10m'), deleteAccount);\n * ```\n *\n * A session older than that throws `STEP_UP_REQUIRED`, which `janusErrors()`\n * answers 403 with `{ code: 'STEP_UP_REQUIRED' }`: the client asks for a\n * step-up, then sends the request again. An anonymous request is answered\n * 401, with no body, as `session(auth, { required: true })` answers it.\n *\n * A `maxAge` that is not a duration is a `TypeError` here, when the app is\n * wired — not on the first request.\n * No `session()` before it is a `TypeError` at the first request — a\n * wiring error, not an anonymous 401 — as for `permission()`.\n */\nexport function fresh(\n\tmaxAge: Duration,\n\toptions: FreshOptions = {},\n): MiddlewareHandler<{\n\t// biome-ignore lint/style/useNamingConvention: Hono's `Env` names the key, not us.\n\tVariables: { session: Session | null };\n}> {\n\tconst maxAgeMs = parseDuration(maxAge, 'fresh: maxAge');\n\treturn async (c, next) => {\n\t\tconst current = c.var.session;\n\t\tif (current === undefined) {\n\t\t\tthrow new TypeError(\n\t\t\t\t'fresh(): c.var.session is not set — put session(auth) before it',\n\t\t\t);\n\t\t}\n\t\tif (current === null) return c.body(null, 401);\n\t\t// `assertFresh`'s rule, with a message naming the call written here.\n\t\tconst now = (options.clock ?? systemClock).now().getTime();\n\t\tif (now - current.authenticatedAt.getTime() >= maxAgeMs) {\n\t\t\tthrow new StepUpRequiredError(\n\t\t\t\t'fresh(): the session proved who it is longer ago than maxAge — confirm with a step-up',\n\t\t\t\t{ userId: current.userId },\n\t\t\t);\n\t\t}\n\t\tawait next();\n\t\treturn undefined;\n\t};\n}\n"
11
12
  ],
12
- "mappings": ";AA0IO,SAAS,UAAU,CACzB,QACA,YACA,MACA,MACA,UAKI,CAAC,GACe;AAAA,EACpB,MAAM,MAAM,OAAO;AAAA,EAEnB,OAAO,OAAO,GAAG,SAAS;AAAA,IACzB,IAAI,EAAE,IAAI,QAAQ,MAAM,WAAW;AAAA,MAClC,MAAM,IAAI,UACT,6HACD;AAAA,IACD;AAAA,IACA,MAAM,UACL,QAAQ,YAAY,YAAY,OAAO,CAAC,IAAI,MAAM,QAAQ,QAAQ,CAAC;AAAA,IACpE,IAAI,YAAY,QAAQ,YAAY;AAAA,MAAW,OAAO,EAAE,KAAK,MAAM,GAAG;AAAA,IAEtE,MAAM,SAAS,MAAM,KAAK,CAAC;AAAA,IAC3B,IAAI,WAAW;AAAA,MAAM,OAAO,EAAE,KAAK,MAAM,GAAG;AAAA,IAE5C,MAAM,UAAU,MAAM,IACrB,SACA,YACA,KAAK,QAAQ,IAAI,GACjB,QAAQ,QAAQ,YACb,YACA,EAAE,KAAK,MAAM,QAAQ,IAAI,GAAG,MAAM,EAAE,CACxC;AAAA,IACA,IAAI,CAAC;AAAA,MAAS,OAAO,EAAE,KAAK,MAAM,GAAG;AAAA,IAErC,EAAE,IAAI,UAAU,MAAM;AAAA,IACtB,MAAM,KAAK;AAAA;AAAA;AAYN,SAAS,OAAU,CACzB,MACA,MACsC;AAAA,EACtC,OAAO,CAAC,MAAM;AAAA,IACb,MAAM,KAAK,EAAE,IAAI,MAAM,IAAI;AAAA,IAC3B,OAAO,OAAO,YAAY,OAAO,KAAK,EAAE;AAAA;AAAA;AAU1C,SAAS,IAAI,CACZ,QACA,MACiD;AAAA,EACjD,OAAO,IAAI,MAAM,QAAQ;AAAA,IACxB,KAAK,CAAC,QAAQ,QAAS,QAAQ,SAAS,OAAO,QAAQ,IAAI,QAAQ,GAAG;AAAA,IACtE,KAAK,CAAC,QAAQ,QAAQ,QAAQ,UAAU,QAAQ,IAAI,QAAQ,GAAG;AAAA,EAChE,CAAC;AAAA;AAIF,SAAS,MAAM,CAAC,GAA4C;AAAA,EAC3D,MAAM,OAAgB,EAAE,IAAI,MAAM;AAAA,EAClC,IAAI,SAAS,WAAW;AAAA,IACvB,MAAM,IAAI,UACT,wFACD;AAAA,EACD;AAAA,EACA,OAAO;AAAA;;;AC/MD,SAAS,OAAkC,CACjD,WAIE;AAAA,EACF,OAAO,OAAO,GAAG,SAAS;AAAA,IACzB,IAAI,UAAU,SAAS;AAAA,MAAW,EAAE,IAAI,QAAQ,UAAU,IAAI;AAAA,IAC9D,IAAI,UAAU,WAAW;AAAA,MAAW,EAAE,IAAI,UAAU,UAAU,MAAM;AAAA,IACpE,MAAM,KAAK;AAAA;AAAA;;;ACtBb;AA8EO,SAAS,OAAO,CACtB,MACA,UAAkC,CAAC,GACf;AAAA,EACpB,OAAO,OAAO,GAAG,SAAS;AAAA,IACzB,MAAM,UAAU,MAAM,KAAK,aAC1B,EAAE,IAAI,KACN,QAAQ,SAAS,YAAY,YAAY,EAAE,MAAM,QAAQ,KAAK,CAC/D;AAAA,IAEA,IAAI,YAAY,QAAQ,QAAQ,aAAa,MAAM;AAAA,MAClD,OAAO,EAAE,KAAK,MAAM,GAAG;AAAA,IACxB;AAAA,IACA,EAAE,IAAI,QAAQ,SAAS,QAAQ,IAAI;AAAA,IACnC,EAAE,IAAI,WAAW,SAAS,WAAW,IAAI;AAAA,IAEzC,MAAM,KAAK;AAAA,IAIX,MAAM,OAAO,GAAG,KAAK,OAAO;AAAA,IAC5B,IACC,SAAS,YAAY,QACrB,UAAU,GAAG,KAAK,OAAO,IAAI,MAAM,QAAQ,SAC3C,CAAC,EAAE,IAAI,QAAQ,aAAa,EAAE,KAAK,CAAC,UAAU,MAAM,WAAW,IAAI,CAAC,GACnE;AAAA,MACD,YAAY,GAAG,MAAM,OAAO;AAAA,IAC7B;AAAA;AAAA;AAcK,SAAS,WAAc,CAC7B,GACA,MACA,UAKI;AAAA,EACJ,EAAE,OACD,cACA,KAAK,OAAO,UAAU,SAAS,OAAO,SAAS,OAAO,GACtD;AAAA,IACC,QAAQ;AAAA,EACT,CACD;AAAA,EACA,OAAO,SAAS;AAAA;AAQjB,eAAsB,OAAO,CAC5B,GACA,MACmB;AAAA,EACnB,MAAM,UAAU,MAAM,KAAK,QAAQ,EAAE,IAAI,GAAG;AAAA,EAC5C,EAAE,OAAO,cAAc,KAAK,OAAO,MAAM,GAAG,EAAE,QAAQ,KAAK,CAAC;AAAA,EAC5D,OAAO;AAAA;;;AC3CD,SAAS,SAAmC,CAAC,WAAwB;AAAA,EAC3E,QAAQ,MAAM,WAAW;AAAA,EACzB,MAAM,QAAiC;AAAA,IACtC,SAAS,MAAM,QAAQ,SAAS;AAAA,EACjC;AAAA,EACA,IAAI,SAAS,WAAW;AAAA,IACvB,MAAM,UAAU,CAAC,UAAkC,CAAC,MACnD,QAAQ,MAAM,OAAO;AAAA,IACtB,MAAM,cAAc,CACnB,GACA,aACI,YAAY,GAAG,MAAM,QAAQ;AAAA,IAClC,MAAM,UAAU,CAAC,MAAe,QAAQ,GAAG,IAAI;AAAA,EAChD;AAAA,EACA,IAAI,WAAW,WAAW;AAAA,IACzB,MAAM,QAAQ;AAAA,IAId,MAAM,aAAa,IAAI,SAAoB,MAAM,QAAQ,GAAG,IAAI;AAAA,EACjE;AAAA,EACA,OAAO;AAAA;;ACjIR;AAAA;AAAA,cAGC;AAAA;AAcM,SAAS,QAAQ,CAAC,MAA4C;AAAA,EACpE,OAAO,cAAc,IAAI;AAAA;AASnB,SAAS,MAAM,CAAC,OAMrB;AAAA,EACD,QAAQ,MAAM;AAAA,SACR;AAAA,MACJ,OAAO,EAAE,MAAM,MAAM,MAAM,QAAQ,MAAM,UAAU,CAAC,EAAE;AAAA,SAClD;AAAA,MACJ,OAAO,MAAM,cAAc,YACxB,EAAE,MAAM,MAAM,KAAK,IACnB,EAAE,MAAM,MAAM,MAAM,WAAW,MAAM,UAAU;AAAA,SAC9C;AAAA,MACJ,OAAO,MAAM,iBAAiB,YAC3B,EAAE,MAAM,MAAM,KAAK,IACnB,EAAE,MAAM,MAAM,MAAM,cAAc,MAAM,aAAa;AAAA,SACpD;AAAA,MACJ,OAAO,MAAM,eAAe,YACzB,EAAE,MAAM,MAAM,KAAK,IACnB,EAAE,MAAM,MAAM,MAAM,YAAY,MAAM,WAAW;AAAA;AAAA,MAEpD,OAAO,EAAE,MAAM,MAAM,KAAK;AAAA;AAAA;AAS7B,SAAS,WAAW,CAAC,OAA8B,GAAsB;AAAA,EACxE,IAAI,iBAAiB,OAAO;AAAA,IAC3B,MAAM,WAAW,MAAM,YAAY;AAAA,IACnC,OAAO,EAAE,YAAY,SAAS,MAAM,QAAQ;AAAA,EAC7C;AAAA,EACA,QAAQ,MAAM,KAAK;AAAA,EACnB,OAAO,EAAE,KAAK,yBAAyB,GAAG;AAAA;AAgCpC,SAAS,WAAW,CAAC,UAA8B,CAAC,GAAiB;AAAA,EAC3E,QAAQ,QAAQ,WAAW,gBAAgB;AAAA,EAC3C,OAAO,CAAC,OAAO,MAAM;AAAA,IACpB,IAAI,EAAE,iBAAiB;AAAA,MAAa,OAAO,SAAS,OAAO,CAAC;AAAA,IAC5D,MAAM,SAAS,SAAS,MAAM,IAAI;AAAA,IAClC,IAAI,UAAU,OAAO,WAAW;AAAA,MAAW,aAAa,QAAQ,OAAO,CAAC;AAAA,IACxE,MAAM,UACL,MAAM,eAAe,YAClB,YACA,EAAE,eAAe,OAAO,MAAM,UAAU,EAAE;AAAA,IAC9C,OAAO,EAAE,KAAK,OAAO,KAAK,GAAG,QAAQ,OAAO;AAAA;AAAA;AAK9C,SAAS,YAAY,CACpB,QACA,OACA,GACO;AAAA,EACP,MAAM,OAAO,CAAC,YACb,QAAQ,YACP,iCAAiC,MAAM,SAAS,mBAAmB,QAAQ,QAAQ,OAAO,OAAO,SAClG;AAAA,EACD,IAAI;AAAA,IAEH,QAAQ,QAAQ,OAAO,OAAO,CAAC,CAAC,EAAE,KAAK,WAAW,IAAI;AAAA,IACrD,OAAO,SAAS;AAAA,IACjB,KAAK,OAAO;AAAA;AAAA;;AC7Hd;AAAA;AAAA;AAAA;AAAA;AAoCO,SAAS,KAAK,CACpB,QACA,UAAwB,CAAC,GAIvB;AAAA,EACF,MAAM,WAAW,cAAc,QAAQ,eAAe;AAAA,EACtD,OAAO,OAAO,GAAG,SAAS;AAAA,IACzB,MAAM,UAAU,EAAE,IAAI;AAAA,IACtB,IAAI,YAAY,WAAW;AAAA,MAC1B,MAAM,IAAI,UACT,iEACD;AAAA,IACD;AAAA,IACA,IAAI,YAAY;AAAA,MAAM,OAAO,EAAE,KAAK,MAAM,GAAG;AAAA,IAE7C,MAAM,OAAO,QAAQ,SAAS,aAAa,IAAI,EAAE,QAAQ;AAAA,IACzD,IAAI,MAAM,QAAQ,gBAAgB,QAAQ,KAAK,UAAU;AAAA,MACxD,MAAM,IAAI,oBACT,yFACA,EAAE,QAAQ,QAAQ,OAAO,CAC1B;AAAA,IACD;AAAA,IACA,MAAM,KAAK;AAAA;AAAA;",
13
- "debugId": "B7AF7A43DA4D8D7A64756E2164756E21",
13
+ "mappings": "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",
14
+ "debugId": "3BB1888C6A6D634864756E2164756E21",
14
15
  "names": []
15
16
  }
@@ -1 +1 @@
1
- {"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"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"}
package/dist/session.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Session, SharedApi } from '@nxgt/janus';
2
2
  import type { Context, MiddlewareHandler } from 'hono';
3
+ import { type DeviceCookieOptions } from './device';
3
4
  /** Anything `janus()` answered: the part of it this package calls. */
4
5
  export type Auth<U extends {
5
6
  readonly type: string;
@@ -63,13 +64,22 @@ export declare function session<A extends Auth<{
63
64
  export declare function session<A extends Auth<{
64
65
  readonly type: string;
65
66
  }>, const T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']>(auth: A, options: SessionOptions<T>): MiddlewareHandler<SessionEnv<A, T>>;
67
+ /** What `sendSession` takes besides: the device cookie's options. */
68
+ export interface SendSessionOptions {
69
+ readonly device?: DeviceCookieOptions;
70
+ }
66
71
  /**
67
72
  * Sends the session cookie — after `signUp`, `signIn`, or anything else that
68
73
  * answered a session token and its session — and answers the user. Appends: a
69
74
  * cookie set before stays.
70
75
  *
76
+ * When the answer carries a `deviceToken` — the sign-in was given a
77
+ * `device` — the device cookie is set too, or set again so it lasts another
78
+ * four hundred days.
79
+ *
71
80
  * ```ts
72
- * const user = sendSession(c, auth, await auth.signIn({ email, password }));
81
+ * const signedIn = await auth.signIn({ email, password }, { device: deviceOf(c) });
82
+ * const user = sendSession(c, auth, signedIn);
73
83
  * return c.json({ id: user.id }); // the token is in the cookie, never in the body
74
84
  * ```
75
85
  */
@@ -79,7 +89,8 @@ export declare function sendSession<U>(c: Context, auth: Pick<Auth<{
79
89
  readonly token: string;
80
90
  readonly session: Session;
81
91
  readonly user: U;
82
- }): U;
92
+ readonly deviceToken?: string | null;
93
+ }, options?: SendSessionOptions): U;
83
94
  /**
84
95
  * Revokes the session the request presents and clears the cookie — cleared
85
96
  * whatever the answer, so a browser holding a stale cookie drops it too.
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAEvD,OAAO,EAAE,KAAK,mBAAmB,EAAc,MAAM,UAAU,CAAC;AAEhE,sEAAsE;AACtE,MAAM,MAAM,IAAI,CAAC,CAAC,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,IAAI,IAAI,CAC3D,SAAS,CAAC,CAAC,CAAC,EACZ,cAAc,GAAG,SAAS,GAAG,QAAQ,CACrC,CAAC;AAEF,8EAA8E;AAC9E,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAEhE;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,CACrB,CAAC,EACD,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EACvD,QAAQ,SAAS,OAAO,GAAG,KAAK,IAC7B;IAEH,SAAS,EAAE,QAAQ,SAAS,IAAI,GAC7B;QACA,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE;YAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;SAAE,CAAC,CAAC;QACnD,OAAO,EAAE,OAAO,CAAC;KACjB,GACA;QACA,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE;YAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;SAAE,CAAC,GAAG,IAAI,CAAC;QAC1D,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;KACxB,CAAC;CACJ,CAAC;AAEF,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,MAAM;IAC/C,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CACtB,CAAC,SAAS,IAAI,CAAC;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EACzC,KAAK,CAAC,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAE7D,IAAI,EAAE,CAAC,EACP,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG;IAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,GACtD,iBAAiB,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AAC7C,wBAAgB,OAAO,CACtB,CAAC,SAAS,IAAI,CAAC;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EACzC,KAAK,CAAC,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAE7D,IAAI,EAAE,CAAC,EACP,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG;IAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAA;CAAE,GACzD,iBAAiB,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACvC,wBAAgB,OAAO,CACtB,CAAC,SAAS,IAAI,CAAC;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EACzC,KAAK,CAAC,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAC5D,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAiC5E,qEAAqE;AACrE,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,MAAM,CAAC,EAAE,mBAAmB,CAAC;CACtC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC5B,CAAC,EAAE,OAAO,EACV,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAAE,QAAQ,CAAC,EACrD,QAAQ,EAAE;IACT,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC,EACD,OAAO,GAAE,kBAAuB,GAC9B,CAAC,CAYH;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAC5B,CAAC,EAAE,OAAO,EACV,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC,GAC/D,OAAO,CAAC,OAAO,CAAC,CAIlB"}
package/docs/README.md CHANGED
@@ -5,7 +5,7 @@ give the detail.
5
5
 
6
6
  | Page | Read it when |
7
7
  | --- | --- |
8
- | [The routes of an application](guide/routes.md) | You are writing sign-up, sign-in, a second factor's code, a sign-in by e-mailed code or link, sign-out and the e-mail flows as Hono routes, with one user type or several |
8
+ | [The routes of an application](guide/routes.md) | You are writing sign-up, sign-in, a second factor's code, a sign-in by e-mailed code or link, the device cookie of a sign-in from a new device, sign-out and the e-mail flows as Hono routes, with one user type or several |
9
9
  | [Guarded routes and writing tuples](guide/permissions.md) | You are guarding a route with a permission, passing a condition's context, or granting and revoking from a route |
10
10
  | [Troubleshooting](troubleshooting.md) | A route answers something you did not expect, or the compiler refuses one |
11
11
  | [Roadmap](roadmap.md) | You want to know what is coming, what shipped, and what is deliberately not planned |
@@ -177,6 +177,19 @@ retryAfter }` with a `Retry-After` header, the seconds until the next
177
177
  window; the route needs no code of its own. `janus({ signIn: { throttle } })`
178
178
  changes the limit ([passwords](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/passwords.md#password-guessing-is-throttled)).
179
179
 
180
+ **The requests that send an e-mail are throttled too.** Past five requests
181
+ for one address (or one user, for `verifyEmail.send` and an e-mailed
182
+ `stepUp.request`) in a 10-minute window, `@nxgt/janus` throws
183
+ `MAIL_THROTTLED` and sends nothing; `janusErrors()` answers it
184
+ `429 { code: 'MAIL_THROTTLED', retryAfter }` with a `Retry-After` header.
185
+ **A refused request spends and rotates nothing**: the last link or code sent
186
+ still works, so tell the visitor to use the last e-mail they received, or to
187
+ wait `retryAfter` seconds. `janus({ mail: { throttle } })` changes the limit;
188
+ keep its `window` no longer than the shortest `tokens.*` lifetime you use.
189
+ `@nxgt/janus` never sees IP addresses, so add a per-IP ceiling in front of
190
+ these routes with `@nxgt/redis` rate limits
191
+ ([example](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/email-flows.md#requests-that-send-e-mail-are-throttled)).
192
+
180
193
  What the throttle does not see is one password tried against many logins:
181
194
  limit the route **per client address** as well, with the limiter you already
182
195
  run, and a `changePassword` route per user — it compares the current
@@ -210,6 +223,7 @@ The refusals need no `try`: `janusErrors()` answers them.
210
223
  | `PASSWORD_TOO_SHORT` | 400 `{ code, minLength }` |
211
224
  | `LOGIN_TAKEN` | 409 `{ code }` |
212
225
  | `CREDENTIALS_INVALID` | 401 `{ code }` — the same for an unknown login and a wrong password; `{ code, retryAfter }` and a `Retry-After` header once the login is throttled |
226
+ | `MAIL_THROTTLED` | 429 `{ code, retryAfter }` and a `Retry-After` header — a request that sends an e-mail, past its attempts in the window |
213
227
  | `USER_INACTIVE` | 403 `{ code }` — only told to somebody who gave the right password |
214
228
 
215
229
  A bearer client — a mobile app — keeps its session token itself and sends it
@@ -401,8 +415,11 @@ cookie, would tell anyone which addresses have an account. The decoy is 32
401
415
  random bytes, the shape of a real challenge; confirming it is
402
416
  `TOKEN_UNKNOWN`. `maxAge: 600` is the default ten minutes of
403
417
  `tokens.signInCode`, written out so both cookies match. Not awaiting the
404
- mailer keeps the answer's time from telling either. Rate-limit this route
405
- per address and per client: every call sends an e-mail.
418
+ mailer keeps the answer's time from telling either. `@nxgt/janus` throttles
419
+ each address — five requests per 10 minutes, then `MAIL_THROTTLED`, which
420
+ `janusErrors()` answers 429 with `Retry-After` (the last code sent still
421
+ works); rate-limit the route per client yourself, since one client can ask for
422
+ many addresses.
406
423
 
407
424
  The code route still tells a decoy apart: a wrong code against it is 400
408
425
  `TOKEN_UNKNOWN`, against a real challenge 401 `CODE_INVALID` with
@@ -523,9 +540,9 @@ app.post('/sign-in/link', csrf({ origin: ORIGIN }), async (c) => {
523
540
  - **The request route answers `202` whoever asked.** `magicLink.request`
524
541
  answers `null` for an address nobody holds and for an inactive user. With
525
542
  a code, the route needs a decoy challenge. Nothing of a link reaches the
526
- visitor, so here there is no decoy to forge. Rate-limit the route per
527
- address and per client: every call sends an e-mail and cancels the link
528
- sent before it.
543
+ visitor, so here there is no decoy to forge. Every call sends an e-mail
544
+ and cancels the link sent before it: `@nxgt/janus` throttles each address
545
+ (`MAIL_THROTTLED`, 429 with `Retry-After`), and per client stays yours.
529
546
  - **The page echoes only a token of the token's shape.** Anything else
530
547
  written into the HTML is a cross-site scripting hole. Use a button, not a
531
548
  script that submits the form by itself: a scanner that runs scripts would
@@ -576,6 +593,103 @@ this e-mail with `mail.magicLink(issued)`, building its button with
576
593
  For the lifetime and every refusal, see [`@nxgt/janus`'s sign-in link
577
594
  guide](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/magic-link.md).
578
595
 
596
+ ## The device cookie
597
+
598
+ With `janus({ devices })`, every sign-in can tell the user it came from a
599
+ device they had not signed in from. The browser keeps a **device token** in
600
+ a long-lived cookie of its own; `deviceOf(c)` reads it for the sign-in's
601
+ `device`, and `sendSession` sets it from the answer's `deviceToken` — the
602
+ first time, and again at every sign-in, so it lasts another 400 days:
603
+
604
+ ```ts
605
+ import { deviceOf, sendSession } from '@nxgt/janus-hono';
606
+
607
+ app.post('/sign-up', async (c) => {
608
+ const { email, name, password } = await c.req.json();
609
+ const signedUp = await auth.signUp({ email, name, password }, { device: deviceOf(c) });
610
+ return c.json({ id: sendSession(c, auth, signedUp).id }, 201); // the device is known from the start
611
+ });
612
+
613
+ app.post('/sign-in', async (c) => {
614
+ const { email, password } = await c.req.json();
615
+ const signedIn = await auth.signIn({ email, password }, { device: deviceOf(c) });
616
+ const user = sendSession(c, auth, signedIn);
617
+ return c.json({ id: user.id, newDevice: signedIn.newDevice });
618
+ });
619
+ ```
620
+
621
+ `deviceOf(c)` answers `null` for a browser with no device cookie, or an
622
+ empty one: the sign-in then mints its first token. A token that proves
623
+ nothing — another user's, a forged one, one whose key was removed — is a
624
+ new device, never an error. A sign-in given no `device` answers no
625
+ `deviceToken`, and `sendSession` sets no device cookie.
626
+
627
+ | Option | Type | Default | Effect |
628
+ | --- | --- | --- | --- |
629
+ | `name` | `string` | `'janus-device'` (`DEVICE_COOKIE`) | The cookie's name |
630
+ | `domain` | `string` | none | `Domain=` |
631
+ | `path` | `string` | `'/'` | `Path=`: keep it covering every sign-in route |
632
+ | `sameSite` | `'Lax' \| 'Strict' \| 'None'` | `'Lax'` | `SameSite=` — Hono's capitalised values, unlike `janus({ cookie })`'s `'lax'` |
633
+ | `secure` | `boolean` | `true` | `Secure` |
634
+ | `maxAge` | seconds | `34560000`, 400 days | `Max-Age=` — the longest a browser keeps a cookie; above it, Hono's `setCookie` throws |
635
+
636
+ The cookie is always `HttpOnly`. Give `deviceOf` and `sendSession` the same
637
+ options — one object, passed to both:
638
+
639
+ ```ts
640
+ import { type DeviceCookieOptions, bindJanus, deviceOf } from '@nxgt/janus-hono';
641
+
642
+ const device: DeviceCookieOptions = { name: 'device', sameSite: 'Strict' };
643
+ const j = bindJanus({ auth });
644
+
645
+ app.post('/sign-in', async (c) => {
646
+ const signedIn = await auth.signIn(await c.req.json(), { device: deviceOf(c, device) });
647
+ j.sendSession(c, signedIn, { device });
648
+ return c.body(null, 204);
649
+ });
650
+ ```
651
+
652
+ **With a second factor, give the device again.** The challenge `signIn`
653
+ answers carries no device, and sets no device cookie: `confirm` and
654
+ `recover` decide, from what they are given. The device cookie's path is
655
+ `/`, so the code routes read it too:
656
+
657
+ ```ts
658
+ app.post('/sign-in/code', async (c) => {
659
+ const { code } = await c.req.json();
660
+ const challenge = getCookie(c, CHALLENGE);
661
+ if (challenge === undefined) return c.json({ code: 'TOKEN_UNKNOWN' }, 400);
662
+ const signedIn = await auth.secondFactor.confirm(challenge, code, { device: deviceOf(c) });
663
+ const user = sendSession(c, auth, signedIn); // and the device cookie
664
+ deleteCookie(c, CHALLENGE, scope);
665
+ return c.json({ id: user.id });
666
+ });
667
+ ```
668
+
669
+ The same holds for `/sign-in/recovery`, `signInCode.confirm` and
670
+ `magicLink.confirm`: pass `{ device: deviceOf(c) }` to each.
671
+
672
+ **A sign-out leaves the device cookie**: `signOut` clears the session
673
+ cookie only, so the device stays known. A browser two people take turns
674
+ signing in on holds one user's token at a time, so each of them is told of
675
+ a new device at every turn.
676
+
677
+ A bearer client — a mobile app — has no cookie jar to share: answer it the
678
+ `deviceToken` in the body, and take it back as `device`:
679
+
680
+ ```ts
681
+ app.post('/api/sign-in', async (c) => {
682
+ const { email, password, deviceToken } = await c.req.json();
683
+ const signedIn = await auth.signIn({ email, password }, { device: deviceToken ?? null });
684
+ return c.json({ token: signedIn.token, deviceToken: signedIn.deviceToken });
685
+ });
686
+ ```
687
+
688
+ The notice itself — which device, when — is
689
+ [`@nxgt/janus-mail`](https://www.npmjs.com/package/@nxgt/janus-mail)'s
690
+ `newSignIn`, sent when `signedIn.newDevice` is `true`, and everything about
691
+ the token is in [`@nxgt/janus`'s devices guide](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/devices.md).
692
+
579
693
  ## A step-up
580
694
 
581
695
  A route a stolen session should not run alone — deleting the account,
@@ -715,6 +829,7 @@ as staff replaces the patient's cookie.
715
829
  - [`@nxgt/janus` — sessions](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/sessions.md) — lifespans, renewal, the cookie's attributes
716
830
  - [`@nxgt/janus` — errors](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/errors.md) — every code
717
831
  - [`@nxgt/janus` — the second factor](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/second-factor.md) — keys, states, attempts
832
+ - [`@nxgt/janus` — devices](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/devices.md) — the device token, the event, rotating the keys
718
833
  - [`@nxgt/janus` — sign-in codes](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/sign-in-code.md) — requesting, the challenge, attempts, passwordless types
719
834
  - [Guarded routes and writing tuples](permissions.md) — `permission()`, `provide()`
720
835
  - [Troubleshooting](../troubleshooting.md)
package/docs/roadmap.md CHANGED
@@ -36,6 +36,15 @@ Nothing yet.
36
36
 
37
37
  Newest first; the [CHANGELOG](../CHANGELOG.md) holds every release.
38
38
 
39
+ - **A throttled e-mail request's `retryAfter`, v0.7.0** — `janusErrors()`
40
+ and `bodyOf()` answer `@nxgt/janus`'s `MAIL_THROTTLED` with 429, `retryAfter`
41
+ in the body and a `Retry-After` header. Needs `@nxgt/janus` 0.18.0.
42
+ - **The device cookie, v0.6.0** — `deviceOf(c)` reads the device token a
43
+ browser holds, for a sign-in's `device`, and `sendSession` sets it — or
44
+ sets it again for another 400 days — whenever `@nxgt/janus`'s answer
45
+ carries a `deviceToken`: `janus-device` (`DEVICE_COOKIE`), `HttpOnly`,
46
+ `Secure`, `SameSite=Lax`, `Path=/`, changed by `DeviceCookieOptions`.
47
+ `signOut` leaves it. Needs `@nxgt/janus` 0.17.0.
39
48
  - **The routes of a sign-in link, v0.5.1** — the routes guide's
40
49
  [A link sent by e-mail](guide/routes.md#a-link-sent-by-e-mail): a page
41
50
  that spends nothing, a `POST` behind `csrf()` that confirms, and the
@@ -19,6 +19,8 @@ for what causes each.
19
19
  - [`Expected 5 arguments, but got 4`, on `permission()`](#expected-5-arguments-but-got-4-on-permission)
20
20
  - [`Type '{ id: string; }' is not assignable to type 'Awaitable<ObjectData<…> | null>'`](#type--id-string--is-not-assignable-to-type-awaitableobjectdata--null)
21
21
  - [`Property 'access' does not exist on type 'Readonly<ContextVariableMap & …>'`](#property-access-does-not-exist-on-type-readonlycontextvariablemap--)
22
+ - [`Type '"lax"' is not assignable to type '"Lax" | "Strict" | "None" | undefined'. Did you mean '"Lax"'?`](#type-lax-is-not-assignable-to-type-lax--strict--none--undefined-did-you-mean-lax), on `deviceOf` or `sendSession`
23
+ - [`Object literal may only specify known properties, and 'name' does not exist in type 'SendSessionOptions'.`](#object-literal-may-only-specify-known-properties-and-name-does-not-exist-in-type-sendsessionoptions)
22
24
  - [`Argument of type 'SignInResult<…>' is not assignable to parameter of type …`, on `sendSession`](#argument-of-type-signinresult-is-not-assignable-to-parameter-of-type--readonly-token-string-readonly-session-session-readonly-user--)
23
25
 
24
26
  **Runtime**
@@ -35,10 +37,15 @@ for what causes each.
35
37
  - [`400 {"code":"HASH_UNSUPPORTED"}` on sign-in](#400-codehash_unsupported-on-sign-in)
36
38
  - [`401 {"code":"CODE_INVALID","attemptsLeft":<n>}` on the code form](#401-codecode_invalidattemptsleftn-on-the-code-form) — a second factor's, or the code sent by e-mail
37
39
  - [`401 {"code":"CREDENTIALS_INVALID","retryAfter":<n>}` with the right password](#401-codecredentials_invalidretryaftern-with-the-right-password)
40
+ - [`429 {"code":"MAIL_THROTTLED","retryAfter":<n>}` on a request for an e-mail](#429-codemail_throttledretryaftern-on-a-request-for-an-e-mail)
38
41
  - [`403 Forbidden` on `POST /sign-in/link`](#403-forbidden-on-post-sign-inlink) — Hono's `csrf()`, not `janusErrors()`
39
42
  - [`409 {"code":"SECOND_FACTOR_ACTIVE"}` or `409 {"code":"SECOND_FACTOR_NOT_ENROLLED"}`](#409-codesecond_factor_active-or-409-codesecond_factor_not_enrolled)
40
43
  - [The browser never sends the cookie back](#the-browser-never-sends-the-cookie-back)
41
44
  - [A bearer client holds an expiry earlier than the session's](#a-bearer-client-holds-an-expiry-earlier-than-the-sessions)
45
+ - [Every sign-in reports a new device](#every-sign-in-reports-a-new-device)
46
+ - [No device cookie is set](#no-device-cookie-is-set)
47
+ - [`Cookies Max-Age SHOULD NOT be greater than 400 days (34560000 seconds) in duration.`](#cookies-max-age-should-not-be-greater-than-400-days-34560000-seconds-in-duration)
48
+ - [`TypeError: signIn: a device was given, but janus() has no devices — pass devices: { keys }`](#typeerror-signin-a-device-was-given-but-janus-has-no-devices--pass-devices--keys-)
42
49
 
43
50
  ## Types
44
51
 
@@ -153,6 +160,22 @@ that instance: it sets, and types, only what it is given.
153
160
  const app = new Hono().use(session(auth), provide({ auth, access }));
154
161
  ```
155
162
 
163
+ ### `Type '"lax"' is not assignable to type '"Lax" | "Strict" | "None" | undefined'. Did you mean '"Lax"'?`
164
+
165
+ **When:** `tsc`, on `deviceOf(c, { sameSite: 'lax' })` or
166
+ `sendSession(c, auth, signedIn, { device: { sameSite: 'lax' } })` — the
167
+ session cookie's option copied over.
168
+ **Why:** the device cookie is set by Hono's `setCookie`, whose `sameSite` is
169
+ capitalised; `janus({ cookie })`'s is lowercase.
170
+ **Fix:** `{ sameSite: 'Lax' }` — or leave it out: `'Lax'` is the default.
171
+
172
+ ### `Object literal may only specify known properties, and 'name' does not exist in type 'SendSessionOptions'.`
173
+
174
+ **When:** `tsc`, on `sendSession(c, auth, signedIn, { name: 'device' })`.
175
+ **Why:** `sendSession`'s options hold the device cookie's under `device`.
176
+ **Fix:** `sendSession(c, auth, signedIn, { device: { name: 'device' } })`,
177
+ and the same object to `deviceOf(c, { name: 'device' })`.
178
+
156
179
  ### `Argument of type 'SignInResult<…>' is not assignable to parameter of type '{ readonly token: string; readonly session: Session; readonly user: … }'`
157
180
 
158
181
  Also: `Type 'SecondFactorRequired' is missing the following properties …: token, session, user`.
@@ -474,6 +497,38 @@ To change the limit, `janus({ signIn: { throttle: { attempts, window } } })`;
474
497
  in a test, advance the clock past `retryAfter`, or wire
475
498
  `signIn: { throttle: false }`.
476
499
 
500
+ ### `429 {"code":"MAIL_THROTTLED","retryAfter":<n>}` on a request for an e-mail
501
+
502
+ With a `Retry-After: <n>` header.
503
+
504
+ **When:** a route that asks for an e-mail — a sign-in link or code, a
505
+ password reset, an address check, an e-mailed step-up — after five requests
506
+ for the same address (the same user, for the last two) in the current
507
+ 10-minute window.
508
+
509
+ **Why:** `@nxgt/janus` throttles the requests that hand out something to
510
+ e-mail, on by default, each flow on its own; an unknown address is counted
511
+ and refused alike, so nothing is revealed. `retryAfter` is the seconds until
512
+ the window ends. **A refused request spends and rotates nothing**: nothing was
513
+ sent or issued, and the last link or code sent, by anyone's request, still
514
+ works until it expires.
515
+
516
+ **Fix:** tell the visitor to use the last e-mail they received, or to wait:
517
+
518
+ ```ts
519
+ const response = await fetch('/sign-in/email', { method: 'POST', body });
520
+ if (response.status === 429) {
521
+ const { retryAfter } = await response.json();
522
+ showWait(`Check your inbox: the last e-mail we sent still works. You can ask for a new one in ${Math.ceil(retryAfter / 60)} min.`);
523
+ }
524
+ ```
525
+
526
+ To change the limit, `janus({ mail: { throttle: { attempts, window } } })`
527
+ (keep `window` no longer than the shortest `tokens.*` lifetime you use, or the
528
+ last code can expire first); `@nxgt/janus` never sees IP addresses, so add a
529
+ per-IP ceiling with `@nxgt/redis` rate limits. In a test, advance the clock past `retryAfter`, or wire
530
+ `mail: { throttle: false }`.
531
+
477
532
  ### `403 Forbidden` on `POST /sign-in/link`
478
533
 
479
534
  The body is the text `Forbidden`, not a `{ code }`. It is not a `JanusError`:
@@ -592,3 +647,63 @@ app.get('/session', session(auth, { required: true }), (c) =>
592
647
  c.json({ expiresAt: c.var.session.expiresAt }),
593
648
  );
594
649
  ```
650
+
651
+ ### Every sign-in reports a new device
652
+
653
+ **When:** `signedIn.newDevice` is `true` — and a notice sent — at every
654
+ sign-in, from a browser the user always uses.
655
+ **Why:** the token `deviceOf` reads does not prove this user signed in
656
+ there. With this package, most often:
657
+
658
+ - `deviceOf` and `sendSession` were given different options — another
659
+ `name`, or a `path` the sign-in route is outside of — so the cookie written
660
+ is never the one read;
661
+ - the device cookie is `Secure`, and the page is served over plain `http://`
662
+ on a host other than `localhost`: see
663
+ [The browser never sends the cookie back](#the-browser-never-sends-the-cookie-back);
664
+ - a confirmation route passes `{ device: null }` rather than
665
+ `{ device: deviceOf(c) }`.
666
+
667
+ `@nxgt/janus`'s [troubleshooting](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/troubleshooting.md#every-sign-in-reports-a-new-device)
668
+ has the other causes: a device key removed, two people sharing a browser.
669
+ **Fix:** one `DeviceCookieOptions`, passed to both:
670
+
671
+ ```ts
672
+ const device = { name: 'device' } as const;
673
+ const signedIn = await auth.signIn(input, { device: deviceOf(c, device) });
674
+ sendSession(c, auth, signedIn, { device });
675
+ ```
676
+
677
+ ### No device cookie is set
678
+
679
+ **When:** `sendSession` appends the session cookie and no `janus-device`.
680
+ **Why:** the answer carries no `deviceToken`: the sign-in was given no
681
+ `device` — `{ device: deviceOf(c) }` left out, or a second factor's
682
+ `confirm` called without it, since the challenge carries none. The session
683
+ renewal of `session()` never sets it either.
684
+ **Fix:** give every call that opens a session `{ device: deviceOf(c) }`.
685
+
686
+ ### `Cookies Max-Age SHOULD NOT be greater than 400 days (34560000 seconds) in duration.`
687
+
688
+ **When:** `sendSession` answers a sign-in that carries a `deviceToken`, with
689
+ `device: { maxAge }` above 34560000 — most often milliseconds given where
690
+ seconds belong. It throws after the sign-in completed: the session exists,
691
+ the response is not sent.
692
+ **Why:** Hono's `setCookie` refuses a `Max-Age` beyond 400 days, the longest
693
+ a browser keeps a cookie.
694
+ **Fix:** give seconds, 400 days at most — or leave `maxAge` out, which is
695
+ 400 days.
696
+
697
+ ```ts
698
+ sendSession(c, auth, signedIn, { device: { maxAge: 60 * 60 * 24 * 180 } }); // 180 days
699
+ ```
700
+
701
+ ### `TypeError: signIn: a device was given, but janus() has no devices — pass devices: { keys }`
702
+
703
+ **When:** a route passes `{ device: deviceOf(c) }` — even when it answers
704
+ `null` — to a `janus()` wired without `devices`. It reaches
705
+ `janusErrors()`'s `fallback`, or Hono's 500: a `TypeError` is a wiring
706
+ mistake, not a refusal.
707
+ **Fix:** give that `janus()` `devices: { keys }` — every instance that signs
708
+ users in needs the same — or stop passing `device`. See
709
+ [`@nxgt/janus`'s devices guide](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/devices.md#wiring).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/janus-hono",
3
- "version": "0.5.2",
3
+ "version": "0.7.0",
4
4
  "description": "Hono middleware for @nxgt/janus: the signed-in user on every request, the session cookie, routes guarded by a permission, and every error as its status",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,13 +49,13 @@
49
49
  ]
50
50
  },
51
51
  "devDependencies": {
52
- "@nxgt/janus": "^0.16.0",
52
+ "@nxgt/janus": "^0.18.0",
53
53
  "@types/bun": "^1.4.2",
54
54
  "hono": "^4.13.9",
55
55
  "zod": "^4.6.5"
56
56
  },
57
57
  "peerDependencies": {
58
- "@nxgt/janus": "^0.16.0",
58
+ "@nxgt/janus": "^0.18.0",
59
59
  "hono": "^4.13.4",
60
60
  "typescript": "^6.0.3"
61
61
  }