@alxia/janus 0.2.0 → 0.3.1

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
@@ -88,7 +88,10 @@ and a body holding its `code` and only what a client can act on — the
88
88
  issues, a minimum length, attempts left, seconds to wait (with
89
89
  `Retry-After`). Never a login, a reason or a cause. `report(error, ctx)` is
90
90
  called for the 5xx. Anything else goes on, thrown, to the middlewares before
91
- it. Declared after `session()`, it would not see the session's
91
+ it. On an app with `alxia({ errors: 'problem' })`, it answers an RFC 9457
92
+ problem instead, the `code` and the rest of that body its extensions; a
93
+ required `session()`'s 401 and `permission()`'s 401, 404 and 403 follow
94
+ the app's format too. Declared after `session()`, it would not see the session's
92
95
  `STORE_FAILED`: it goes first, and after the observers (`logger()`,
93
96
  `secureHeaders()`), so that they see its reply. An observer settles
94
97
  `next()` without swallowing the error, so `janusErrors()` before one, or
@@ -194,6 +197,7 @@ on every app: annotate what it reads, or leave it unannotated.
194
197
  | `PermissionOptions`, `OptionsArgs` | its options: `subject`, `ctx`; and the rest of its arguments, the options required exactly when the permission has a condition; both take what `subject` and `ctx` read beyond `BaseContext` as their last two, defaulted, parameters |
195
198
  | `bodyOf`, `statusOf` | a refusal's body and status |
196
199
  | `UnauthenticatedBody`, `JanusErrorBody`, `PermissionRefusedBody` | their types |
200
+ | `UnauthenticatedProblem`, `JanusErrorProblem`, `PermissionRefusedProblem` | the same answers under `@alxia/core`'s `alxia({ errors: 'problem' })`: RFC 9457 problems, `janusErrors()`'s with its `code` and body as extensions |
197
201
  | `Auth`, `UserOfAuth`, `ObjectData`, `Awaitable` | the part of `janus()` this package calls, the users it knows, an object as the application loads it, a value or its promise |
198
202
 
199
203
  ## Documentation
@@ -201,3 +205,4 @@ on every app: annotate what it reads, or leave it unannotated.
201
205
  - [Guide](https://github.com/softistx/alxia/tree/develop/packages/janus/docs): a page per area — sessions, signing in and out with `ctx.auth` and the device cookie, janus's errors and their statuses, and the permission guard, reading what an earlier middleware added included.
202
206
  - [Troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/janus/docs/troubleshooting.md): an error message, or a request anonymous, refused or a 404 when it should not be, and what to do about it.
203
207
  - [Roadmap](https://github.com/softistx/alxia/blob/develop/packages/janus/docs/roadmap.md): what is coming, and what is not planned.
208
+ - [Recipes](https://github.com/softistx/alxia/blob/develop/docs/recipes/README.md): [Authenticate requests](https://github.com/softistx/alxia/blob/develop/docs/recipes/authentication.md), [Answer errors consistently](https://github.com/softistx/alxia/blob/develop/docs/recipes/errors.md).
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type BaseContext, type Middleware, type MiddlewareMark, type Next, type Reply } from '@alxia/core';
1
+ import { type BaseContext, type Middleware, type Next, type Problem, type Reply } from '@alxia/core';
2
2
  import { JanusError, type JanusErrorCode, type JanusErrorStatus, statusOf } from '@nxgt/janus';
3
3
  /**
4
4
  * The body a refusal is answered with: its `code`, and only what the client
@@ -22,15 +22,22 @@ export interface JanusErrorsOptions {
22
22
  */
23
23
  readonly report?: (error: JanusError, ctx: BaseContext) => unknown;
24
24
  }
25
+ /**
26
+ * A `JanusError` answered under `alxia({ errors: 'problem' })`: an RFC
27
+ * 9457 problem, its `code` and the members of `JanusErrorBody` its
28
+ * extensions.
29
+ */
30
+ export type JanusErrorProblem = Problem<JanusErrorStatus, JanusErrorBody>;
25
31
  /** What `janusErrors()` makes: a middleware that answers what janus throws behind it. */
26
- export type JanusErrors = Middleware<object, Promise<Next | Reply<JanusErrorStatus, JanusErrorBody>>> & MiddlewareMark;
32
+ export type JanusErrors = Middleware<object, Promise<Next | Reply<JanusErrorStatus, JanusErrorBody | JanusErrorProblem>>>;
27
33
  /**
28
34
  * Janus's errors answered, as a middleware: every `JanusError` thrown
29
35
  * behind it — by `session()`, `permission()`, a route — a sign-in
30
36
  * refused, a login taken, a store down — is answered with janus's status
31
- * and `bodyOf(error)`, typed on the routes declared after it. A throttled
37
+ * and `bodyOf(error)` — under `alxia({ errors: 'problem' })`, a problem
38
+ * whose extensions are that body — typed on the routes declared after it. A throttled
32
39
  * sign-in carries `Retry-After`. Anything else goes on, thrown, to the
33
- * middlewares before it, the app's `onError` or its 500.
40
+ * middlewares before it, or to the route boundary's 500.
34
41
  *
35
42
  * Give it to `use` before `session()`, so that a store down while the
36
43
  * session is read is answered too:
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,WAAW,EAEhB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,IAAI,EACT,KAAK,KAAK,EACV,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,UAAU,EACV,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,QAAQ,EACR,MAAM,aAAa,CAAC;AAErB;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC9B,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;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,cAAc,CAmBxD;AAED,MAAM,WAAW,kBAAkB;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC;CACnE;AAED,yFAAyF;AACzF,MAAM,MAAM,WAAW,GAAG,UAAU,CACnC,MAAM,EACN,OAAO,CAAC,IAAI,GAAG,KAAK,CAAC,gBAAgB,EAAE,cAAc,CAAC,CAAC,CACvD,GACA,cAAc,CAAC;AAEhB;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,WAAW,CASzE;AA0BD,OAAO,EAAE,QAAQ,EAAE,CAAC"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,WAAW,EAGhB,KAAK,UAAU,EAEf,KAAK,IAAI,EACT,KAAK,OAAO,EAGZ,KAAK,KAAK,EACV,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,UAAU,EACV,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,QAAQ,EACR,MAAM,aAAa,CAAC;AAErB;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC9B,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;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,cAAc,CAmBxD;AAED,MAAM,WAAW,kBAAkB;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC;CACnE;AAED;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,EAAE,cAAc,CAAC,CAAC;AAE1E,yFAAyF;AACzF,MAAM,MAAM,WAAW,GAAG,UAAU,CACnC,MAAM,EACN,OAAO,CAAC,IAAI,GAAG,KAAK,CAAC,gBAAgB,EAAE,cAAc,GAAG,iBAAiB,CAAC,CAAC,CAC3E,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,WAAW,CASzE;AA2BD,OAAO,EAAE,QAAQ,EAAE,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { DEVICE_COOKIE, type DeviceCookieOptions, deviceOf, sendDevice, } from './device';
2
- export { bodyOf, type JanusErrorBody, type JanusErrors, type JanusErrorsOptions, janusErrors, statusOf, } from './errors';
3
- export { byParam, type PermissionRefusedBody, permission, } from './permission';
2
+ export { bodyOf, type JanusErrorBody, type JanusErrorProblem, type JanusErrors, type JanusErrorsOptions, janusErrors, statusOf, } from './errors';
3
+ export { byParam, type PermissionRefusedBody, type PermissionRefusedProblem, permission, } from './permission';
4
4
  export type { Awaitable, ObjectData, OptionsArgs, PermissionOptions, } from './permission-options';
5
5
  export { type SendSessionOptions, sendSession, signOut } from './send';
6
- export { type SessionMiddleware, type SessionOptions, session, type UnauthenticatedBody, } from './session';
6
+ export { type SessionMiddleware, type SessionOptions, session, type UnauthenticatedBody, type UnauthenticatedProblem, } from './session';
7
7
  export type { Auth, RequestAuth, SessionOpened, UserOfAuth } from './types';
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,aAAa,EACb,KAAK,mBAAmB,EACxB,QAAQ,EACR,UAAU,GACV,MAAM,UAAU,CAAC;AAClB,OAAO,EACN,MAAM,EACN,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,WAAW,EACX,QAAQ,GACR,MAAM,UAAU,CAAC;AAClB,OAAO,EACN,OAAO,EACP,KAAK,qBAAqB,EAC1B,UAAU,GACV,MAAM,cAAc,CAAC;AACtB,YAAY,EACX,SAAS,EACT,UAAU,EACV,WAAW,EACX,iBAAiB,GACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,KAAK,kBAAkB,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACvE,OAAO,EACN,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,OAAO,EACP,KAAK,mBAAmB,GACxB,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,aAAa,EACb,KAAK,mBAAmB,EACxB,QAAQ,EACR,UAAU,GACV,MAAM,UAAU,CAAC;AAClB,OAAO,EACN,MAAM,EACN,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,WAAW,EACX,QAAQ,GACR,MAAM,UAAU,CAAC;AAClB,OAAO,EACN,OAAO,EACP,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC7B,UAAU,GACV,MAAM,cAAc,CAAC;AACtB,YAAY,EACX,SAAS,EACT,UAAU,EACV,WAAW,EACX,iBAAiB,GACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,KAAK,kBAAkB,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACvE,OAAO,EACN,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,OAAO,EACP,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,GAC3B,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -18,7 +18,11 @@ function sendDevice(ctx, token, options = {}) {
18
18
  }
19
19
  // src/errors.ts
20
20
  import {
21
- defineMiddleware
21
+ defineMiddleware,
22
+ errorFormat,
23
+ markFactory,
24
+ problem,
25
+ problemOf
22
26
  } from "@alxia/core";
23
27
  import {
24
28
  JanusError,
@@ -39,7 +43,7 @@ function bodyOf(error) {
39
43
  }
40
44
  }
41
45
  function janusErrors(options = {}) {
42
- return defineMiddleware(async (ctx, next) => {
46
+ return defineMiddleware(async function janusErrors(ctx, next) {
43
47
  try {
44
48
  return await next();
45
49
  } catch (error) {
@@ -55,30 +59,51 @@ function answer(error, ctx, options) {
55
59
  Promise.resolve().then(() => options.report?.(error, ctx)).catch((failure) => process.emitWarning(`janusErrors(): report failed: ${String(failure)}`));
56
60
  }
57
61
  const body = bodyOf(error);
58
- return ctx.reply(status, body, error.retryAfter === undefined ? {} : { headers: { "retry-after": String(error.retryAfter) } });
62
+ const init = error.retryAfter === undefined ? {} : { headers: { "retry-after": String(error.retryAfter) } };
63
+ if (errorFormat(ctx) === "problem") {
64
+ return problem(problemOf(ctx, { status, ...body }), init);
65
+ }
66
+ return ctx.reply(status, body, init);
59
67
  }
68
+ markFactory(janusErrors);
60
69
  // src/permission.ts
61
70
  import {
62
- defineMiddleware as defineMiddleware2
71
+ defineMiddleware as defineMiddleware2,
72
+ markFactory as markFactory2
73
+ } from "@alxia/core";
74
+
75
+ // src/refused.ts
76
+ import {
77
+ errorFormat as errorFormat2,
78
+ problem as problem2,
79
+ problemOf as problemOf2
63
80
  } from "@alxia/core";
81
+ function refused(ctx, status, body, detail) {
82
+ if (errorFormat2(ctx) === "problem") {
83
+ return problem2(problemOf2(ctx, { status, detail }));
84
+ }
85
+ return ctx.reply(status, body);
86
+ }
87
+
88
+ // src/permission.ts
64
89
  function permission(access, permission, type, load, ...options) {
65
90
  const loadOf = load;
66
91
  const { subject, ctx: ctxOf } = options[0] ?? {};
67
92
  const can = access.can;
68
- const refuse = (error) => {
69
- const body = { error };
70
- return body;
71
- };
72
- return defineMiddleware2()(async (ctx, next) => {
93
+ const refuse = (ctx, status, error, detail) => refused(ctx, status, { error }, detail);
94
+ return defineMiddleware2()(async function permissionGuard(ctx, next) {
73
95
  const who = subject === undefined ? userOf(ctx) : await subject(ctx);
74
- if (who === null)
75
- return ctx.reply(401, refuse("unauthenticated"));
96
+ if (who === null) {
97
+ return refuse(ctx, 401, "unauthenticated", "The request has no session");
98
+ }
76
99
  const object = await loadOf(ctx);
77
- if (object === null)
78
- return ctx.reply(404, refuse("not_found"));
100
+ if (object === null) {
101
+ return refuse(ctx, 404, "not_found", `No ${type} is found here`);
102
+ }
79
103
  const allowed = await can(who, permission, view(object, type), ctxOf === undefined ? undefined : { ctx: await ctxOf(ctx, object) });
80
- if (!allowed)
81
- return ctx.reply(403, refuse("forbidden"));
104
+ if (!allowed) {
105
+ return refuse(ctx, 403, "forbidden", `The ${permission} permission on this ${type} is not granted`);
106
+ }
82
107
  return next({ object });
83
108
  });
84
109
  }
@@ -101,6 +126,7 @@ function userOf(ctx) {
101
126
  }
102
127
  return user;
103
128
  }
129
+ markFactory2(permission);
104
130
  // src/send.ts
105
131
  function sendSession(ctx, auth, signedIn, options = {}) {
106
132
  ctx.set.headers.append("set-cookie", auth.cookie.serialize(signedIn.token, signedIn.session));
@@ -117,6 +143,7 @@ async function signOut(ctx, auth) {
117
143
  // src/session.ts
118
144
  import {
119
145
  defineMiddleware as defineMiddleware3,
146
+ markFactory as markFactory3,
120
147
  settle,
121
148
  withHeaders
122
149
  } from "@alxia/core";
@@ -151,11 +178,11 @@ function authenticateOnce(auth, request, type) {
151
178
  // src/session.ts
152
179
  function session(auth, options = {}) {
153
180
  const unauthenticated = { error: "unauthenticated" };
154
- return defineMiddleware3(async (ctx, next) => {
155
- const { request, reply } = ctx;
181
+ return defineMiddleware3(async function session(ctx, next) {
182
+ const { request } = ctx;
156
183
  const found = await authenticateOnce(auth, request, options.type);
157
184
  if (found === null && options.required === true) {
158
- return reply(401, unauthenticated);
185
+ return refused(ctx, 401, unauthenticated, "The request has no session");
159
186
  }
160
187
  const bound = {
161
188
  device: deviceOf(ctx, options.device),
@@ -178,6 +205,7 @@ function renewed(auth, request, found, response) {
178
205
  }
179
206
  return withHeaders(response, (headers) => headers.append("set-cookie", auth.cookie.serialize(found.token, found.session)));
180
207
  }
208
+ markFactory3(session);
181
209
  export {
182
210
  DEVICE_COOKIE,
183
211
  bodyOf,
@@ -192,5 +220,5 @@ export {
192
220
  statusOf
193
221
  };
194
222
 
195
- //# debugId=B189B0B99B3C49B664756E2164756E21
223
+ //# debugId=E839E5A61B58A76164756E2164756E21
196
224
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/device.ts", "../src/errors.ts", "../src/permission.ts", "../src/send.ts", "../src/session.ts", "../src/lookup.ts"],
3
+ "sources": ["../src/device.ts", "../src/errors.ts", "../src/permission.ts", "../src/refused.ts", "../src/send.ts", "../src/session.ts", "../src/lookup.ts"],
4
4
  "sourcesContent": [
5
5
  "import type { ResponseSettings } from '@alxia/core';\n\n/** `@nxgt/janus-hono`'s name for it, so a device remembered by one is by the other. */\nexport const DEVICE_COOKIE = 'janus-device';\n\nconst MAX_AGE = 400 * 24 * 60 * 60;\n\nexport interface DeviceCookieOptions {\n\treadonly name?: string;\n\treadonly domain?: string;\n\treadonly path?: string;\n\treadonly sameSite?: 'lax' | 'strict' | 'none';\n\treadonly secure?: boolean;\n\t/** Seconds. Four hundred days, the most a browser keeps, by default. */\n\treadonly maxAge?: number;\n}\n\n/** The device token the request carries, for `signIn(…, { device })`: `null` when none. */\nexport function deviceOf(\n\tctx: { readonly request: Request },\n\toptions: Pick<DeviceCookieOptions, 'name'> = {},\n): string | null {\n\tconst cookies = new Bun.CookieMap(ctx.request.headers.get('cookie') ?? '');\n\tconst value = cookies.get(options.name ?? DEVICE_COOKIE);\n\treturn value === null || value === '' ? null : value;\n}\n\n/** Sets the device cookie, or sets it again so it lasts another `maxAge`. */\nexport function sendDevice(\n\tctx: { readonly set: ResponseSettings },\n\ttoken: string,\n\toptions: DeviceCookieOptions = {},\n): void {\n\tctx.set.cookies.set(options.name ?? DEVICE_COOKIE, token, {\n\t\thttpOnly: true,\n\t\tpath: options.path ?? '/',\n\t\tsameSite: options.sameSite ?? 'lax',\n\t\tsecure: options.secure ?? true,\n\t\tmaxAge: options.maxAge ?? MAX_AGE,\n\t\t...(options.domain === undefined ? {} : { domain: options.domain }),\n\t});\n}\n",
6
- "import {\n\ttype BaseContext,\n\tdefineMiddleware,\n\ttype Middleware,\n\ttype MiddlewareMark,\n\ttype Next,\n\ttype Reply,\n} from '@alxia/core';\nimport {\n\tJanusError,\n\ttype JanusErrorCode,\n\ttype JanusErrorStatus,\n\tstatusOf,\n} from '@nxgt/janus';\n\n/**\n * The body a refusal is answered with: its `code`, and only what the client\n * can act on. Never `reason`, `login`, a hash prefix or a cause: those are\n * for your logs. `@nxgt/janus-hono`'s `bodyOf`, kept twice on purpose.\n */\nexport interface JanusErrorBody {\n\treadonly code: JanusErrorCode;\n\treadonly issues?: JanusError['issues'];\n\treadonly minLength?: number;\n\treadonly attemptsLeft?: number;\n\treadonly retryAfter?: number;\n}\n\nexport function bodyOf(error: JanusError): JanusErrorBody {\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\nexport interface JanusErrorsOptions {\n\t/**\n\t * Called with every `JanusError` answered 5xx — `STORE_FAILED` and the\n\t * like, the server's to fix — as it is answered: started, not awaited,\n\t * so a slow report never holds the response. It cannot change the\n\t * answer: one that throws or rejects is a warning.\n\t */\n\treadonly report?: (error: JanusError, ctx: BaseContext) => unknown;\n}\n\n/** What `janusErrors()` makes: a middleware that answers what janus throws behind it. */\nexport type JanusErrors = Middleware<\n\tobject,\n\tPromise<Next | Reply<JanusErrorStatus, JanusErrorBody>>\n> &\n\tMiddlewareMark;\n\n/**\n * Janus's errors answered, as a middleware: every `JanusError` thrown\n * behind it — by `session()`, `permission()`, a route — a sign-in\n * refused, a login taken, a store down — is answered with janus's status\n * and `bodyOf(error)`, typed on the routes declared after it. A throttled\n * sign-in carries `Retry-After`. Anything else goes on, thrown, to the\n * middlewares before it, the app's `onError` or its 500.\n *\n * Give it to `use` before `session()`, so that a store down while the\n * session is read is answered too:\n *\n * ```ts\n * app.use(janusErrors(), session(accounts)).post('/sign-in', ...);\n * ```\n *\n * **`STORE_FAILED` is a 503**, never a 401 or a 404: an outage is not an\n * answer.\n */\nexport function janusErrors(options: JanusErrorsOptions = {}): JanusErrors {\n\treturn defineMiddleware(async (ctx, next) => {\n\t\ttry {\n\t\t\treturn await next();\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof JanusError)) throw error;\n\t\t\treturn answer(error, ctx, options);\n\t\t}\n\t});\n}\n\n/** A `JanusError`, answered: its status, its body, reported when a 5xx. */\nfunction answer(\n\terror: JanusError,\n\tctx: BaseContext,\n\toptions: JanusErrorsOptions,\n): Reply<JanusErrorStatus, JanusErrorBody> {\n\tconst status: JanusErrorStatus = statusOf(error.code);\n\tif (status >= 500 && options.report !== undefined) {\n\t\tPromise.resolve()\n\t\t\t.then(() => options.report?.(error, ctx))\n\t\t\t.catch((failure: unknown) =>\n\t\t\t\tprocess.emitWarning(`janusErrors(): report failed: ${String(failure)}`),\n\t\t\t);\n\t}\n\tconst body = bodyOf(error);\n\treturn ctx.reply(\n\t\tstatus,\n\t\tbody,\n\t\terror.retryAfter === undefined\n\t\t\t? {}\n\t\t\t: { headers: { 'retry-after': String(error.retryAfter) } },\n\t);\n}\n\nexport { statusOf };\n",
7
- "import {\n\ttype BaseContext,\n\tdefineMiddleware,\n\ttype RequiresOf,\n} from '@alxia/core';\nimport type {\n\tCheckableOf,\n\tModelConfig,\n\tObjectTypeOf,\n\tPermissions,\n\tSubjectRef,\n} from '@nxgt/janus/permissions';\nimport type { Awaitable, ObjectData, OptionsArgs } from './permission-options';\n\n/** The body of each refusal. */\nexport interface PermissionRefusedBody {\n\treadonly error: 'unauthenticated' | 'not_found' | 'forbidden';\n}\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 * A guard, as a middleware: the routes declared after it run only if the\n * subject holds `permission` on an object of `type`. It loads the object\n * once, checks it with `access.can`, and hands it to them as `object`.\n *\n * | the request | answered |\n * | --- | --- |\n * | anonymous | 401 — before the object is loaded |\n * | `load` answers `null` | 404 |\n * | a denial | 403 |\n * | allowed | the route runs, `object` set |\n *\n * Each refusal is typed on those routes. **A failure throws**: a store that\n * cannot answer is `STORE_FAILED`, never a 403 — `janusErrors()` answers\n * it 503. Scope it with `group`, so it guards only its routes — given to\n * the app's `use`, it would refuse a request no route matches too:\n *\n * ```ts\n * app.use(janusErrors(), session(accounts)).group('/records/:id', (records) =>\n * records.use(permission(access, 'view', 'record', byParam('id', findRecord)))\n * .get('/', ({ object, reply }) => reply(200, object)));\n * ```\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\tLoadCtx extends object = BaseContext,\n\tSubjectCtx extends object = BaseContext,\n\tCheckCtx extends object = BaseContext,\n>(\n\taccess: Pick<Permissions<C>, 'can'>,\n\tpermission: P,\n\ttype: T,\n\tload: (ctx: BaseContext & LoadCtx) => Awaitable<O | null>,\n\t...options: OptionsArgs<C, T, P, O, SubjectCtx, CheckCtx>\n) {\n\t// `app.use` has checked that the app gives what `load`, `subject` and `ctx` read.\n\tconst loadOf = load as (ctx: BaseContext) => Awaitable<O | null>;\n\tconst { subject, ctx: ctxOf } = (options[0] ?? {}) as {\n\t\treadonly subject?: (\n\t\t\tctx: BaseContext,\n\t\t) => Awaitable<SubjectRef<ModelConfig> | null>;\n\t\treadonly ctx?: (ctx: BaseContext, object: unknown) => Awaitable<unknown>;\n\t};\n\tconst can = access.can as LooseCan;\n\tconst refuse = (error: PermissionRefusedBody['error']) => {\n\t\tconst body: PermissionRefusedBody = { error };\n\t\treturn body;\n\t};\n\treturn defineMiddleware<\n\t\tRequiresOf<\n\t\t\tLoadCtx & SubjectCtx & CheckCtx,\n\t\t\tAnnotatedAny<LoadCtx, SubjectCtx>\n\t\t>\n\t>()(async (ctx, next) => {\n\t\tconst who = subject === undefined ? userOf(ctx) : await subject(ctx);\n\t\tif (who === null) return ctx.reply(401, refuse('unauthenticated'));\n\t\tconst object = await loadOf(ctx);\n\t\tif (object === null) return ctx.reply(404, refuse('not_found'));\n\t\tconst allowed = await can(\n\t\t\twho,\n\t\t\tpermission,\n\t\t\tview(object, type),\n\t\t\tctxOf === undefined ? undefined : { ctx: await ctxOf(ctx, object) },\n\t\t);\n\t\tif (!allowed) return ctx.reply(403, refuse('forbidden'));\n\t\treturn next({ object });\n\t});\n}\n\n/**\n * The callback to name when `RequiresOf` refuses one annotated `any`: `load`\n * or `subject` when it is that one, `ctx` otherwise. An `any` in any of the\n * three makes their intersection `any`, so `RequiresOf` cannot tell which;\n * a `Record<string, any>` passes the same test.\n */\ntype AnnotatedAny<LoadCtx, SubjectCtx> = [LoadCtx] extends [\n\t{ readonly '~any': 1 },\n]\n\t? 'load'\n\t: [SubjectCtx] extends [{ readonly '~any': 1 }]\n\t\t? 'subject'\n\t\t: 'ctx';\n\n/**\n * A `load` reading one path parameter, as it arrived: `find(id)`, or `null`\n * — a 404 — when the route has no such parameter.\n */\nexport function byParam<O>(\n\tname: string,\n\tfind: (id: string) => Awaitable<O | null>,\n): (ctx: BaseContext) => Awaitable<O | null> {\n\treturn (ctx) => {\n\t\tconst id = ctx.pathParams[name];\n\t\treturn id === undefined ? null : find(id);\n\t};\n}\n\n/** The object as `can()` reads it: `type` added, every other field read from the object itself. */\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/** The `user` `session()` derived — or a wiring error when nothing did. */\nfunction userOf(ctx: BaseContext): SubjectRef<ModelConfig> | null {\n\tconst user = (ctx as { readonly user?: unknown }).user;\n\tif (user === undefined) {\n\t\tthrow new TypeError(\n\t\t\t'permission(): no user in the context — use session(auth) before it, or pass { subject }',\n\t\t);\n\t}\n\treturn user as SubjectRef<ModelConfig> | null;\n}\n",
6
+ "import {\n\ttype BaseContext,\n\tdefineMiddleware,\n\terrorFormat,\n\ttype Middleware,\n\tmarkFactory,\n\ttype Next,\n\ttype Problem,\n\tproblem,\n\tproblemOf,\n\ttype Reply,\n} from '@alxia/core';\nimport {\n\tJanusError,\n\ttype JanusErrorCode,\n\ttype JanusErrorStatus,\n\tstatusOf,\n} from '@nxgt/janus';\n\n/**\n * The body a refusal is answered with: its `code`, and only what the client\n * can act on. Never `reason`, `login`, a hash prefix or a cause: those are\n * for your logs. `@nxgt/janus-hono`'s `bodyOf`, kept twice on purpose.\n */\nexport interface JanusErrorBody {\n\treadonly code: JanusErrorCode;\n\treadonly issues?: JanusError['issues'];\n\treadonly minLength?: number;\n\treadonly attemptsLeft?: number;\n\treadonly retryAfter?: number;\n}\n\nexport function bodyOf(error: JanusError): JanusErrorBody {\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\nexport interface JanusErrorsOptions {\n\t/**\n\t * Called with every `JanusError` answered 5xx — `STORE_FAILED` and the\n\t * like, the server's to fix — as it is answered: started, not awaited,\n\t * so a slow report never holds the response. It cannot change the\n\t * answer: one that throws or rejects is a warning.\n\t */\n\treadonly report?: (error: JanusError, ctx: BaseContext) => unknown;\n}\n\n/**\n * A `JanusError` answered under `alxia({ errors: 'problem' })`: an RFC\n * 9457 problem, its `code` and the members of `JanusErrorBody` its\n * extensions.\n */\nexport type JanusErrorProblem = Problem<JanusErrorStatus, JanusErrorBody>;\n\n/** What `janusErrors()` makes: a middleware that answers what janus throws behind it. */\nexport type JanusErrors = Middleware<\n\tobject,\n\tPromise<Next | Reply<JanusErrorStatus, JanusErrorBody | JanusErrorProblem>>\n>;\n\n/**\n * Janus's errors answered, as a middleware: every `JanusError` thrown\n * behind it — by `session()`, `permission()`, a route — a sign-in\n * refused, a login taken, a store down — is answered with janus's status\n * and `bodyOf(error)` — under `alxia({ errors: 'problem' })`, a problem\n * whose extensions are that body — typed on the routes declared after it. A throttled\n * sign-in carries `Retry-After`. Anything else goes on, thrown, to the\n * middlewares before it, or to the route boundary's 500.\n *\n * Give it to `use` before `session()`, so that a store down while the\n * session is read is answered too:\n *\n * ```ts\n * app.use(janusErrors(), session(accounts)).post('/sign-in', ...);\n * ```\n *\n * **`STORE_FAILED` is a 503**, never a 401 or a 404: an outage is not an\n * answer.\n */\nexport function janusErrors(options: JanusErrorsOptions = {}): JanusErrors {\n\treturn defineMiddleware(async function janusErrors(ctx, next) {\n\t\ttry {\n\t\t\treturn await next();\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof JanusError)) throw error;\n\t\t\treturn answer(error, ctx, options);\n\t\t}\n\t});\n}\n\n/** A `JanusError`, answered: its status, its body, reported when a 5xx. */\nfunction answer(\n\terror: JanusError,\n\tctx: BaseContext,\n\toptions: JanusErrorsOptions,\n): Reply<JanusErrorStatus, JanusErrorBody | JanusErrorProblem> {\n\tconst status: JanusErrorStatus = statusOf(error.code);\n\tif (status >= 500 && options.report !== undefined) {\n\t\tPromise.resolve()\n\t\t\t.then(() => options.report?.(error, ctx))\n\t\t\t.catch((failure: unknown) =>\n\t\t\t\tprocess.emitWarning(`janusErrors(): report failed: ${String(failure)}`),\n\t\t\t);\n\t}\n\tconst body = bodyOf(error);\n\tconst init =\n\t\terror.retryAfter === undefined\n\t\t\t? {}\n\t\t\t: { headers: { 'retry-after': String(error.retryAfter) } };\n\tif (errorFormat(ctx) === 'problem') {\n\t\treturn problem(problemOf(ctx, { status, ...body }), init);\n\t}\n\treturn ctx.reply(status, body, init);\n}\n\nexport { statusOf };\n\nmarkFactory(janusErrors);\n",
7
+ "import {\n\ttype BaseContext,\n\tdefineMiddleware,\n\tmarkFactory,\n\ttype Problem,\n\ttype RequiresOf,\n} from '@alxia/core';\nimport type {\n\tCheckableOf,\n\tModelConfig,\n\tObjectTypeOf,\n\tPermissions,\n\tSubjectRef,\n} from '@nxgt/janus/permissions';\nimport type { Awaitable, ObjectData, OptionsArgs } from './permission-options';\nimport { refused } from './refused';\n\n/** The body of each refusal. */\nexport interface PermissionRefusedBody {\n\treadonly error: 'unauthenticated' | 'not_found' | 'forbidden';\n}\n\n/** A refusal under `alxia({ errors: 'problem' })`: its 401, 404 or 403 as a problem. */\nexport type PermissionRefusedProblem = Problem<401 | 403 | 404>;\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 * A guard, as a middleware: the routes declared after it run only if the\n * subject holds `permission` on an object of `type`. It loads the object\n * once, checks it with `access.can`, and hands it to them as `object`.\n *\n * | the request | answered |\n * | --- | --- |\n * | anonymous | 401 — before the object is loaded |\n * | `load` answers `null` | 404 |\n * | a denial | 403 |\n * | allowed | the route runs, `object` set |\n *\n * Each refusal is typed on those routes. **A failure throws**: a store that\n * cannot answer is `STORE_FAILED`, never a 403 — `janusErrors()` answers\n * it 503. Scope it with `group`, so it guards only its routes — given to\n * the app's `use`, it would refuse a request no route matches too:\n *\n * ```ts\n * app.use(janusErrors(), session(accounts)).group('/records/:id', (records) =>\n * records.use(permission(access, 'view', 'record', byParam('id', findRecord)))\n * .get('/', ({ object, reply }) => reply(200, object)));\n * ```\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\tLoadCtx extends object = BaseContext,\n\tSubjectCtx extends object = BaseContext,\n\tCheckCtx extends object = BaseContext,\n>(\n\taccess: Pick<Permissions<C>, 'can'>,\n\tpermission: P,\n\ttype: T,\n\tload: (ctx: BaseContext & LoadCtx) => Awaitable<O | null>,\n\t...options: OptionsArgs<C, T, P, O, SubjectCtx, CheckCtx>\n) {\n\t// `app.use` has checked that the app gives what `load`, `subject` and `ctx` read.\n\tconst loadOf = load as (ctx: BaseContext) => Awaitable<O | null>;\n\tconst { subject, ctx: ctxOf } = (options[0] ?? {}) as {\n\t\treadonly subject?: (\n\t\t\tctx: BaseContext,\n\t\t) => Awaitable<SubjectRef<ModelConfig> | null>;\n\t\treadonly ctx?: (ctx: BaseContext, object: unknown) => Awaitable<unknown>;\n\t};\n\tconst can = access.can as LooseCan;\n\tconst refuse = <Status extends 401 | 403 | 404>(\n\t\tctx: BaseContext,\n\t\tstatus: Status,\n\t\terror: PermissionRefusedBody['error'],\n\t\tdetail: string,\n\t) => refused(ctx, status, { error } as PermissionRefusedBody, detail);\n\treturn defineMiddleware<\n\t\tRequiresOf<\n\t\t\tLoadCtx & SubjectCtx & CheckCtx,\n\t\t\tAnnotatedAny<LoadCtx, SubjectCtx>\n\t\t>\n\t>()(async function permissionGuard(ctx, next) {\n\t\tconst who = subject === undefined ? userOf(ctx) : await subject(ctx);\n\t\tif (who === null) {\n\t\t\treturn refuse(ctx, 401, 'unauthenticated', 'The request has no session');\n\t\t}\n\t\tconst object = await loadOf(ctx);\n\t\tif (object === null) {\n\t\t\treturn refuse(ctx, 404, 'not_found', `No ${type} is found here`);\n\t\t}\n\t\tconst allowed = await can(\n\t\t\twho,\n\t\t\tpermission,\n\t\t\tview(object, type),\n\t\t\tctxOf === undefined ? undefined : { ctx: await ctxOf(ctx, object) },\n\t\t);\n\t\tif (!allowed) {\n\t\t\treturn refuse(\n\t\t\t\tctx,\n\t\t\t\t403,\n\t\t\t\t'forbidden',\n\t\t\t\t`The ${permission} permission on this ${type} is not granted`,\n\t\t\t);\n\t\t}\n\t\treturn next({ object });\n\t});\n}\n\n/**\n * The callback to name when `RequiresOf` refuses one annotated `any`: `load`\n * or `subject` when it is that one, `ctx` otherwise. An `any` in any of the\n * three makes their intersection `any`, so `RequiresOf` cannot tell which;\n * a `Record<string, any>` passes the same test.\n */\ntype AnnotatedAny<LoadCtx, SubjectCtx> = [LoadCtx] extends [\n\t{ readonly '~any': 1 },\n]\n\t? 'load'\n\t: [SubjectCtx] extends [{ readonly '~any': 1 }]\n\t\t? 'subject'\n\t\t: 'ctx';\n\n/**\n * A `load` reading one path parameter, as it arrived: `find(id)`, or `null`\n * — a 404 — when the route has no such parameter.\n */\nexport function byParam<O>(\n\tname: string,\n\tfind: (id: string) => Awaitable<O | null>,\n): (ctx: BaseContext) => Awaitable<O | null> {\n\treturn (ctx) => {\n\t\tconst id = ctx.pathParams[name];\n\t\treturn id === undefined ? null : find(id);\n\t};\n}\n\n/** The object as `can()` reads it: `type` added, every other field read from the object itself. */\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/** The `user` `session()` derived — or a wiring error when nothing did. */\nfunction userOf(ctx: BaseContext): SubjectRef<ModelConfig> | null {\n\tconst user = (ctx as { readonly user?: unknown }).user;\n\tif (user === undefined) {\n\t\tthrow new TypeError(\n\t\t\t'permission(): no user in the context — use session(auth) before it, or pass { subject }',\n\t\t);\n\t}\n\treturn user as SubjectRef<ModelConfig> | null;\n}\n\nmarkFactory(permission);\n",
8
+ "/**\n * A refusal of `session()` or `permission()`, in the format of the app\n * that serves the request: its `{ error }` body by default, an RFC 9457\n * problem under `alxia({ errors: 'problem' })`.\n */\nimport {\n\ttype BaseContext,\n\terrorFormat,\n\ttype Problem,\n\tproblem,\n\tproblemOf,\n\ttype Reply,\n} from '@alxia/core';\n\n/** `body` with `status`, or the problem of `status` saying `detail`. */\nexport function refused<Status extends 401 | 403 | 404, Body>(\n\tctx: BaseContext,\n\tstatus: Status,\n\tbody: Body,\n\tdetail: string,\n): Reply<Status, Body | Problem<Status>> {\n\tif (errorFormat(ctx) === 'problem') {\n\t\treturn problem(problemOf(ctx, { status, detail }));\n\t}\n\treturn ctx.reply(status, body);\n}\n",
8
9
  "import type { ResponseSettings } from '@alxia/core';\nimport { type DeviceCookieOptions, sendDevice } from './device';\nimport type { Auth, SessionOpened } from './types';\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`, anything that\n * answered a token and its session — and answers the user. The token is in\n * the cookie, never in the body. With a `deviceToken`, the device cookie\n * too.\n *\n * ```ts\n * const signedIn = await auth.signIn(body, { device: deviceOf(ctx) });\n * return reply(200, { id: sendSession(ctx, auth, signedIn).id });\n * ```\n */\nexport function sendSession<U>(\n\tctx: { readonly set: ResponseSettings },\n\tauth: Pick<Auth<{ readonly type: string }>, 'cookie'>,\n\tsignedIn: SessionOpened<U>,\n\toptions: SendSessionOptions = {},\n): U {\n\tctx.set.headers.append(\n\t\t'set-cookie',\n\t\tauth.cookie.serialize(signedIn.token, signedIn.session),\n\t);\n\tif (typeof signedIn.deviceToken === 'string') {\n\t\tsendDevice(ctx, 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 */\nexport async function signOut(\n\tctx: { readonly request: Request; readonly set: ResponseSettings },\n\tauth: Pick<Auth<{ readonly type: string }>, 'signOut' | 'cookie'>,\n): Promise<boolean> {\n\tconst revoked = await auth.signOut(ctx.request);\n\tctx.set.headers.append('set-cookie', auth.cookie.clear());\n\treturn revoked;\n}\n",
9
- "import {\n\tdefineMiddleware,\n\ttype Empty,\n\ttype Middleware,\n\ttype MiddlewareMark,\n\ttype Next,\n\ttype Reply,\n\tsettle,\n\twithHeaders,\n} from '@alxia/core';\nimport type { Session } from '@nxgt/janus';\nimport { type DeviceCookieOptions, deviceOf } from './device';\nimport { authenticateOnce, type Found } from './lookup';\nimport { sendSession, signOut } from './send';\nimport type { Auth, RequestAuth, UserOfAuth } from './types';\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 and the route never runs,\n\t * so `user` is never `null` in it — and the 401 is in its type. A\n\t * `boolean` known only at runtime keeps the 401 in the type, and `user`\n\t * may be `null`.\n\t */\n\treadonly required?: boolean;\n\t/** The device cookie `auth.device` reads and `auth.send` sets. */\n\treadonly device?: DeviceCookieOptions;\n}\n\n/** The body of the 401 a required session answers. */\nexport interface UnauthenticatedBody {\n\treadonly error: 'unauthenticated';\n}\n\ntype UserOf<A, T> = Extract<UserOfAuth<A>, { readonly type: T }>;\n\n/** What a session gives the routes after it. */\ntype Given<User, S> = {\n\treadonly user: User;\n\treadonly session: S;\n\treadonly auth: RequestAuth;\n};\n\n/** What `session()` makes: a middleware that gives `Added`, or answers `Refused`. */\nexport type SessionMiddleware<\n\tAdded extends object,\n\tRefused = never,\n> = Middleware<Empty, Promise<Refused | Next<Added>>> & MiddlewareMark;\n\n/**\n * Who a request belongs to, as a middleware: `auth.authenticate(request)`,\n * and the routes declared after it read `user` and `session` — typed by\n * the user schema, narrowed by `type`. Given to `app.use`, it runs on a\n * request no route matches too: a required session answers it 401 before\n * its 404.\n *\n * **An outage is not anonymous**: a store that cannot answer throws\n * `STORE_FAILED`, which `janusErrors()` — given to `use` before it —\n * answers 503, never 401.\n *\n * A session renewed in passing is sent again as a cookie, after the route —\n * only to a request that presented it as one (a `Bearer` client is never\n * handed a cookie), and never over a session cookie the route set itself.\n *\n * ```ts\n * app.use(janusErrors(), session(accounts, { required: true }))\n * .get('/me', ({ user, reply }) => reply.ok(user));\n * ```\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): SessionMiddleware<\n\tGiven<UserOf<A, T>, Session>,\n\tReply<401, UnauthenticatedBody>\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?: false },\n): SessionMiddleware<Given<UserOf<A, T> | null, Session | null>>;\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>,\n): SessionMiddleware<\n\tGiven<UserOf<A, T> | null, Session | null>,\n\tReply<401, UnauthenticatedBody>\n>;\nexport function session(\n\tauth: Auth<{ readonly type: string }>,\n\toptions: SessionOptions<string> = {},\n): unknown {\n\tconst unauthenticated: UnauthenticatedBody = { error: 'unauthenticated' };\n\treturn defineMiddleware(async (ctx, next) => {\n\t\tconst { request, reply } = ctx;\n\t\tconst found = await authenticateOnce(auth, request, options.type);\n\t\tif (found === null && options.required === true) {\n\t\t\treturn reply(401, unauthenticated);\n\t\t}\n\t\tconst bound: RequestAuth = {\n\t\t\tdevice: deviceOf(ctx, options.device),\n\t\t\tsend: (signedIn) => sendSession(ctx, auth, signedIn, options),\n\t\t\tsignOut: () => signOut(ctx, auth),\n\t\t};\n\t\t// Settled: a refusal's 400, an error's answer, still carry the renewal.\n\t\tconst response = await settle(\n\t\t\tctx,\n\t\t\tnext({\n\t\t\t\tuser: found?.user ?? null,\n\t\t\t\tsession: found?.session ?? null,\n\t\t\t\tauth: bound,\n\t\t\t}),\n\t\t);\n\t\treturn found?.renewed === true\n\t\t\t? renewed(auth, request, found, response)\n\t\t\t: response;\n\t});\n}\n\n/**\n * A session renewed in passing, sent again as a cookie: only to a request\n * that presented it as one, and never over a cookie the route set itself —\n * a sign-in or a sign-out behind it must not be undone by the session it\n * replaced.\n */\nfunction renewed<R extends Response>(\n\tauth: Auth<{ readonly type: string }>,\n\trequest: Request,\n\tfound: NonNullable<Found>,\n\tresponse: R,\n): R {\n\tconst name = `${auth.cookie.name}=`;\n\tconst presented = new Bun.CookieMap(request.headers.get('cookie') ?? '').get(\n\t\tauth.cookie.name,\n\t);\n\tif (\n\t\tpresented !== found.token ||\n\t\tresponse.headers.getSetCookie().some((value) => value.startsWith(name))\n\t) {\n\t\treturn response;\n\t}\n\treturn withHeaders(response, (headers) =>\n\t\theaders.append(\n\t\t\t'set-cookie',\n\t\t\tauth.cookie.serialize(found.token, found.session),\n\t\t),\n\t) as R;\n}\n",
10
+ "import {\n\tdefineMiddleware,\n\ttype Empty,\n\ttype Middleware,\n\tmarkFactory,\n\ttype Next,\n\ttype Problem,\n\ttype Reply,\n\tsettle,\n\twithHeaders,\n} from '@alxia/core';\nimport type { Session } from '@nxgt/janus';\nimport { type DeviceCookieOptions, deviceOf } from './device';\nimport { authenticateOnce, type Found } from './lookup';\nimport { refused } from './refused';\nimport { sendSession, signOut } from './send';\nimport type { Auth, RequestAuth, UserOfAuth } from './types';\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 and the route never runs,\n\t * so `user` is never `null` in it — and the 401 is in its type. A\n\t * `boolean` known only at runtime keeps the 401 in the type, and `user`\n\t * may be `null`.\n\t */\n\treadonly required?: boolean;\n\t/** The device cookie `auth.device` reads and `auth.send` sets. */\n\treadonly device?: DeviceCookieOptions;\n}\n\n/** The body of the 401 a required session answers. */\nexport interface UnauthenticatedBody {\n\treadonly error: 'unauthenticated';\n}\n\n/** The 401 a required session answers under `alxia({ errors: 'problem' })`. */\nexport type UnauthenticatedProblem = Problem<401>;\n\ntype UserOf<A, T> = Extract<UserOfAuth<A>, { readonly type: T }>;\n\n/** What a session gives the routes after it. */\ntype Given<User, S> = {\n\treadonly user: User;\n\treadonly session: S;\n\treadonly auth: RequestAuth;\n};\n\n/** What `session()` makes: a middleware that gives `Added`, or answers `Refused`. */\nexport type SessionMiddleware<\n\tAdded extends object,\n\tRefused = never,\n> = Middleware<Empty, Promise<Refused | Next<Added>>>;\n\n/**\n * Who a request belongs to, as a middleware: `auth.authenticate(request)`,\n * and the routes declared after it read `user` and `session` — typed by\n * the user schema, narrowed by `type`. Given to `app.use`, it runs on a\n * request no route matches too: a required session answers it 401 before\n * its 404.\n *\n * **An outage is not anonymous**: a store that cannot answer throws\n * `STORE_FAILED`, which `janusErrors()` — given to `use` before it —\n * answers 503, never 401.\n *\n * A session renewed in passing is sent again as a cookie, after the route —\n * only to a request that presented it as one (a `Bearer` client is never\n * handed a cookie), and never over a session cookie the route set itself.\n *\n * ```ts\n * app.use(janusErrors(), session(accounts, { required: true }))\n * .get('/me', ({ user, reply }) => reply.ok(user));\n * ```\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): SessionMiddleware<\n\tGiven<UserOf<A, T>, Session>,\n\tReply<401, UnauthenticatedBody | UnauthenticatedProblem>\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?: false },\n): SessionMiddleware<Given<UserOf<A, T> | null, Session | null>>;\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>,\n): SessionMiddleware<\n\tGiven<UserOf<A, T> | null, Session | null>,\n\tReply<401, UnauthenticatedBody | UnauthenticatedProblem>\n>;\nexport function session(\n\tauth: Auth<{ readonly type: string }>,\n\toptions: SessionOptions<string> = {},\n): unknown {\n\tconst unauthenticated: UnauthenticatedBody = { error: 'unauthenticated' };\n\treturn defineMiddleware(async function session(ctx, next) {\n\t\tconst { request } = ctx;\n\t\tconst found = await authenticateOnce(auth, request, options.type);\n\t\tif (found === null && options.required === true) {\n\t\t\treturn refused(ctx, 401, unauthenticated, 'The request has no session');\n\t\t}\n\t\tconst bound: RequestAuth = {\n\t\t\tdevice: deviceOf(ctx, options.device),\n\t\t\tsend: (signedIn) => sendSession(ctx, auth, signedIn, options),\n\t\t\tsignOut: () => signOut(ctx, auth),\n\t\t};\n\t\t// Settled: a refusal's 400, an error's answer, still carry the renewal.\n\t\tconst response = await settle(\n\t\t\tctx,\n\t\t\tnext({\n\t\t\t\tuser: found?.user ?? null,\n\t\t\t\tsession: found?.session ?? null,\n\t\t\t\tauth: bound,\n\t\t\t}),\n\t\t);\n\t\treturn found?.renewed === true\n\t\t\t? renewed(auth, request, found, response)\n\t\t\t: response;\n\t});\n}\n\n/**\n * A session renewed in passing, sent again as a cookie: only to a request\n * that presented it as one, and never over a cookie the route set itself —\n * a sign-in or a sign-out behind it must not be undone by the session it\n * replaced.\n */\nfunction renewed<R extends Response>(\n\tauth: Auth<{ readonly type: string }>,\n\trequest: Request,\n\tfound: NonNullable<Found>,\n\tresponse: R,\n): R {\n\tconst name = `${auth.cookie.name}=`;\n\tconst presented = new Bun.CookieMap(request.headers.get('cookie') ?? '').get(\n\t\tauth.cookie.name,\n\t);\n\tif (\n\t\tpresented !== found.token ||\n\t\tresponse.headers.getSetCookie().some((value) => value.startsWith(name))\n\t) {\n\t\treturn response;\n\t}\n\treturn withHeaders(response, (headers) =>\n\t\theaders.append(\n\t\t\t'set-cookie',\n\t\t\tauth.cookie.serialize(found.token, found.session),\n\t\t),\n\t) as R;\n}\n\nmarkFactory(session);\n",
10
11
  "import type { Authenticated } from '@nxgt/janus';\nimport type { Auth } from './types';\n\nexport type Found = Authenticated<{ readonly type: string }> | null;\n\n/**\n * Each request's lookups, by instance and by `type`: an open `session()`\n * and a required one behind it, of the same `type`, look the session up\n * once. They live as long as the `Request` object; a lookup that failed is\n * forgotten, so the next dispatch of that object asks again.\n */\nconst lookups = new WeakMap<\n\tobject,\n\tWeakMap<Request, Map<string, Promise<Found>>>\n>();\n\nexport function authenticateOnce(\n\tauth: Auth<{ readonly type: string }>,\n\trequest: Request,\n\ttype: string | undefined,\n): Promise<Found> {\n\tlet byRequest = lookups.get(auth);\n\tif (byRequest === undefined) {\n\t\tbyRequest = new WeakMap();\n\t\tlookups.set(auth, byRequest);\n\t}\n\tlet byType = byRequest.get(request);\n\tif (byType === undefined) {\n\t\tbyType = new Map();\n\t\tbyRequest.set(request, byType);\n\t}\n\tconst key = type ?? '';\n\tlet found = byType.get(key);\n\tif (found === undefined) {\n\t\tfound = auth.authenticate(\n\t\t\trequest,\n\t\t\ttype === undefined ? undefined : { type },\n\t\t);\n\t\tbyType.set(key, found);\n\t\tconst asked = found;\n\t\tasked.catch(() => {\n\t\t\tif (byType.get(key) === asked) byType.delete(key);\n\t\t});\n\t}\n\treturn found;\n}\n"
11
12
  ],
12
- "mappings": "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",
13
- "debugId": "B189B0B99B3C49B664756E2164756E21",
13
+ "mappings": "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",
14
+ "debugId": "E839E5A61B58A76164756E2164756E21",
14
15
  "names": []
15
16
  }
@@ -1,10 +1,12 @@
1
- import { type BaseContext, type RequiresOf } from '@alxia/core';
1
+ import { type BaseContext, type Problem, type RequiresOf } from '@alxia/core';
2
2
  import type { CheckableOf, ModelConfig, ObjectTypeOf, Permissions } from '@nxgt/janus/permissions';
3
3
  import type { Awaitable, ObjectData, OptionsArgs } from './permission-options';
4
4
  /** The body of each refusal. */
5
5
  export interface PermissionRefusedBody {
6
6
  readonly error: 'unauthenticated' | 'not_found' | 'forbidden';
7
7
  }
8
+ /** A refusal under `alxia({ errors: 'problem' })`: its 401, 404 or 403 as a problem. */
9
+ export type PermissionRefusedProblem = Problem<401 | 403 | 404>;
8
10
  /**
9
11
  * A guard, as a middleware: the routes declared after it run only if the
10
12
  * subject holds `permission` on an object of `type`. It loads the object
@@ -28,9 +30,9 @@ export interface PermissionRefusedBody {
28
30
  * .get('/', ({ object, reply }) => reply(200, object)));
29
31
  * ```
30
32
  */
31
- export declare function permission<C extends ModelConfig, const T extends ObjectTypeOf<C>, const P extends CheckableOf<C, T>, O extends ObjectData<C, T>, LoadCtx extends object = BaseContext, SubjectCtx extends object = BaseContext, CheckCtx extends object = BaseContext>(access: Pick<Permissions<C>, 'can'>, permission: P, type: T, load: (ctx: BaseContext & LoadCtx) => Awaitable<O | null>, ...options: OptionsArgs<C, T, P, O, SubjectCtx, CheckCtx>): import("@alxia/core").Middleware<RequiresOf<LoadCtx & SubjectCtx & CheckCtx, AnnotatedAny<LoadCtx, SubjectCtx>>, Promise<import("@alxia/core").Reply<401, PermissionRefusedBody> | import("@alxia/core").Reply<404, PermissionRefusedBody> | import("@alxia/core").Reply<403, PermissionRefusedBody> | import("@alxia/core").Next<{
33
+ export declare function permission<C extends ModelConfig, const T extends ObjectTypeOf<C>, const P extends CheckableOf<C, T>, O extends ObjectData<C, T>, LoadCtx extends object = BaseContext, SubjectCtx extends object = BaseContext, CheckCtx extends object = BaseContext>(access: Pick<Permissions<C>, 'can'>, permission: P, type: T, load: (ctx: BaseContext & LoadCtx) => Awaitable<O | null>, ...options: OptionsArgs<C, T, P, O, SubjectCtx, CheckCtx>): import("@alxia/core").Middleware<RequiresOf<LoadCtx & SubjectCtx & CheckCtx, AnnotatedAny<LoadCtx, SubjectCtx>>, Promise<import("@alxia/core").Reply<401, PermissionRefusedBody | Problem<401>> | import("@alxia/core").Reply<404, PermissionRefusedBody | Problem<404>> | import("@alxia/core").Reply<403, PermissionRefusedBody | Problem<403>> | import("@alxia/core").Next<{
32
34
  object: O;
33
- }, import("@alxia/core").Empty>>> & import("@alxia/core").MiddlewareMark;
35
+ }, import("@alxia/core").Empty>>>;
34
36
  /**
35
37
  * The callback to name when `RequiresOf` refuses one annotated `any`: `load`
36
38
  * or `subject` when it is that one, `ctx` otherwise. An `any` in any of the
@@ -1 +1 @@
1
- {"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,WAAW,EAEhB,KAAK,UAAU,EACf,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACX,WAAW,EACX,WAAW,EACX,YAAY,EACZ,WAAW,EAEX,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAE/E,gCAAgC;AAChC,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,KAAK,EAAE,iBAAiB,GAAG,WAAW,GAAG,WAAW,CAAC;CAC9D;AASD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,UAAU,CACzB,CAAC,SAAS,WAAW,EACrB,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,EAC1B,OAAO,SAAS,MAAM,GAAG,WAAW,EACpC,UAAU,SAAS,MAAM,GAAG,WAAW,EACvC,QAAQ,SAAS,MAAM,GAAG,WAAW,EAErC,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EACnC,UAAU,EAAE,CAAC,EACb,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,EACzD,GAAG,OAAO,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,QAAQ,CAAC;;yEAkCzD;AAED;;;;;GAKG;AACH,KAAK,YAAY,CAAC,OAAO,EAAE,UAAU,IAAI,CAAC,OAAO,CAAC,SAAS;IAC1D;QAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;KAAE;CACtB,GACE,MAAM,GACN,CAAC,UAAU,CAAC,SAAS,CAAC;IAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;CAAE,CAAC,GAC5C,SAAS,GACT,KAAK,CAAC;AAEV;;;GAGG;AACH,wBAAgB,OAAO,CAAC,CAAC,EACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,GACvC,CAAC,GAAG,EAAE,WAAW,KAAK,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAK3C"}
1
+ {"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,WAAW,EAGhB,KAAK,OAAO,EACZ,KAAK,UAAU,EACf,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACX,WAAW,EACX,WAAW,EACX,YAAY,EACZ,WAAW,EAEX,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAG/E,gCAAgC;AAChC,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,KAAK,EAAE,iBAAiB,GAAG,WAAW,GAAG,WAAW,CAAC;CAC9D;AAED,wFAAwF;AACxF,MAAM,MAAM,wBAAwB,GAAG,OAAO,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC;AAShE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,UAAU,CACzB,CAAC,SAAS,WAAW,EACrB,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,EAC1B,OAAO,SAAS,MAAM,GAAG,WAAW,EACpC,UAAU,SAAS,MAAM,GAAG,WAAW,EACvC,QAAQ,SAAS,MAAM,GAAG,WAAW,EAErC,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EACnC,UAAU,EAAE,CAAC,EACb,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,EACzD,GAAG,OAAO,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,QAAQ,CAAC;;kCA+CzD;AAED;;;;;GAKG;AACH,KAAK,YAAY,CAAC,OAAO,EAAE,UAAU,IAAI,CAAC,OAAO,CAAC,SAAS;IAC1D;QAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;KAAE;CACtB,GACE,MAAM,GACN,CAAC,UAAU,CAAC,SAAS,CAAC;IAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;CAAE,CAAC,GAC5C,SAAS,GACT,KAAK,CAAC;AAEV;;;GAGG;AACH,wBAAgB,OAAO,CAAC,CAAC,EACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,GACvC,CAAC,GAAG,EAAE,WAAW,KAAK,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAK3C"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A refusal of `session()` or `permission()`, in the format of the app
3
+ * that serves the request: its `{ error }` body by default, an RFC 9457
4
+ * problem under `alxia({ errors: 'problem' })`.
5
+ */
6
+ import { type BaseContext, type Problem, type Reply } from '@alxia/core';
7
+ /** `body` with `status`, or the problem of `status` saying `detail`. */
8
+ export declare function refused<Status extends 401 | 403 | 404, Body>(ctx: BaseContext, status: Status, body: Body, detail: string): Reply<Status, Body | Problem<Status>>;
9
+ //# sourceMappingURL=refused.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refused.d.ts","sourceRoot":"","sources":["../src/refused.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACN,KAAK,WAAW,EAEhB,KAAK,OAAO,EAGZ,KAAK,KAAK,EACV,MAAM,aAAa,CAAC;AAErB,wEAAwE;AACxE,wBAAgB,OAAO,CAAC,MAAM,SAAS,GAAG,GAAG,GAAG,GAAG,GAAG,EAAE,IAAI,EAC3D,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,MAAM,GACZ,KAAK,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAKvC"}
package/dist/session.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Empty, type Middleware, type MiddlewareMark, type Next, type Reply } from '@alxia/core';
1
+ import { type Empty, type Middleware, type Next, type Problem, type Reply } from '@alxia/core';
2
2
  import type { Session } from '@nxgt/janus';
3
3
  import { type DeviceCookieOptions } from './device';
4
4
  import type { Auth, RequestAuth, UserOfAuth } from './types';
@@ -19,6 +19,8 @@ export interface SessionOptions<T extends string> {
19
19
  export interface UnauthenticatedBody {
20
20
  readonly error: 'unauthenticated';
21
21
  }
22
+ /** The 401 a required session answers under `alxia({ errors: 'problem' })`. */
23
+ export type UnauthenticatedProblem = Problem<401>;
22
24
  type UserOf<A, T> = Extract<UserOfAuth<A>, {
23
25
  readonly type: T;
24
26
  }>;
@@ -29,7 +31,7 @@ type Given<User, S> = {
29
31
  readonly auth: RequestAuth;
30
32
  };
31
33
  /** What `session()` makes: a middleware that gives `Added`, or answers `Refused`. */
32
- export type SessionMiddleware<Added extends object, Refused = never> = Middleware<Empty, Promise<Refused | Next<Added>>> & MiddlewareMark;
34
+ export type SessionMiddleware<Added extends object, Refused = never> = Middleware<Empty, Promise<Refused | Next<Added>>>;
33
35
  /**
34
36
  * Who a request belongs to, as a middleware: `auth.authenticate(request)`,
35
37
  * and the routes declared after it read `user` and `session` — typed by
@@ -54,7 +56,7 @@ export declare function session<A extends Auth<{
54
56
  readonly type: string;
55
57
  }>, const T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']>(auth: A, options: SessionOptions<T> & {
56
58
  readonly required: true;
57
- }): SessionMiddleware<Given<UserOf<A, T>, Session>, Reply<401, UnauthenticatedBody>>;
59
+ }): SessionMiddleware<Given<UserOf<A, T>, Session>, Reply<401, UnauthenticatedBody | UnauthenticatedProblem>>;
58
60
  export declare function session<A extends Auth<{
59
61
  readonly type: string;
60
62
  }>, const T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']>(auth: A, options?: SessionOptions<T> & {
@@ -62,6 +64,6 @@ export declare function session<A extends Auth<{
62
64
  }): SessionMiddleware<Given<UserOf<A, T> | null, Session | null>>;
63
65
  export declare function session<A extends Auth<{
64
66
  readonly type: string;
65
- }>, const T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']>(auth: A, options?: SessionOptions<T>): SessionMiddleware<Given<UserOf<A, T> | null, Session | null>, Reply<401, UnauthenticatedBody>>;
67
+ }>, const T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']>(auth: A, options?: SessionOptions<T>): SessionMiddleware<Given<UserOf<A, T> | null, Session | null>, Reply<401, UnauthenticatedBody | UnauthenticatedProblem>>;
66
68
  export {};
67
69
  //# sourceMappingURL=session.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,KAAK,EACV,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,IAAI,EACT,KAAK,KAAK,EAGV,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,KAAK,mBAAmB,EAAY,MAAM,UAAU,CAAC;AAG9D,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE7D,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,MAAM;IAC/C,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,mBAAmB,CAAC;CACtC;AAED,sDAAsD;AACtD,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;CAClC;AAED,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC,CAAC;AAEjE,gDAAgD;AAChD,KAAK,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI;IACrB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;CAC3B,CAAC;AAEF,qFAAqF;AACrF,MAAM,MAAM,iBAAiB,CAC5B,KAAK,SAAS,MAAM,EACpB,OAAO,GAAG,KAAK,IACZ,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;GAmBG;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,CACnB,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,EAC5B,KAAK,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAC/B,CAAC;AACF,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,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC;AACjE,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,GACzB,iBAAiB,CACnB,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC,EAC1C,KAAK,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAC/B,CAAC"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,KAAK,EACV,KAAK,UAAU,EAEf,KAAK,IAAI,EACT,KAAK,OAAO,EACZ,KAAK,KAAK,EAGV,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,KAAK,mBAAmB,EAAY,MAAM,UAAU,CAAC;AAI9D,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE7D,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,MAAM;IAC/C,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,mBAAmB,CAAC;CACtC;AAED,sDAAsD;AACtD,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;CAClC;AAED,+EAA+E;AAC/E,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;AAElD,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC,CAAC;AAEjE,gDAAgD;AAChD,KAAK,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI;IACrB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;CAC3B,CAAC;AAEF,qFAAqF;AACrF,MAAM,MAAM,iBAAiB,CAC5B,KAAK,SAAS,MAAM,EACpB,OAAO,GAAG,KAAK,IACZ,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;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,CACnB,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,EAC5B,KAAK,CAAC,GAAG,EAAE,mBAAmB,GAAG,sBAAsB,CAAC,CACxD,CAAC;AACF,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,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC;AACjE,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,GACzB,iBAAiB,CACnB,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC,EAC1C,KAAK,CAAC,GAAG,EAAE,mBAAmB,GAAG,sBAAsB,CAAC,CACxD,CAAC"}
package/docs/README.md CHANGED
@@ -14,3 +14,10 @@ a realistic example for each.
14
14
  | [Permissions](guide/permissions.md) | guarding routes with a permission on an object, loading it by a path parameter, passing a condition's context, checking for another subject than the session's user, or reading what an earlier middleware added |
15
15
  | [Troubleshooting](troubleshooting.md) | something went wrong and you have the message, or a request is anonymous, refused or a 404 when you did not expect it |
16
16
  | [Roadmap](roadmap.md) | wondering what is coming, and what is not planned |
17
+
18
+ ## Recipes
19
+
20
+ A task that crosses packages, in [the repository's recipes](https://github.com/softistx/alxia/blob/develop/docs/recipes/README.md), each with a complete example:
21
+
22
+ - [Authenticate requests](https://github.com/softistx/alxia/blob/develop/docs/recipes/authentication.md): a bearer token or a cookie session, a role check, the user typed in every handler
23
+ - [Answer errors consistently](https://github.com/softistx/alxia/blob/develop/docs/recipes/errors.md): problem details, `HttpError`, a `try`/`catch` middleware
@@ -56,7 +56,8 @@ down while the session is read (`STORE_FAILED`) is the app's 500
56
56
  before it: a `JanusError` it throws is not behind `janusErrors()`.
57
57
 
58
58
  It answers only a `JanusError`. Anything else goes on, thrown, to the
59
- middlewares before it, then the app's `onError`, or its 500.
59
+ middlewares before it — a try/catch of your own around `await next()` — and,
60
+ when none answers it, to the route boundary: an `HttpError`'s status, else a 500.
60
61
 
61
62
  An observer that settles `next()` (`logger()`, `secureHeaders()`,
62
63
  `telemetry()`, `createI18n()`) reads the response the error would be
@@ -173,13 +174,37 @@ if (me.status === 401) {
173
174
  if (me.status === 503) console.log(body.code); // 'STORE_FAILED' and the like: try again later
174
175
  ```
175
176
 
177
+ ### As problems
178
+
179
+ On an app that answers its errors as RFC 9457 problems, every answer of
180
+ this package is one, sent as `application/problem+json`, with the
181
+ `Retry-After` header kept: `janusErrors()`'s carries `bodyOf(error)` as
182
+ its extensions, `code` first among them (`JanusErrorProblem`); a
183
+ required `session()`'s 401 (`UnauthenticatedProblem`) and
184
+ `permission()`'s 401, 404 and 403 (`PermissionRefusedProblem`) carry
185
+ their five members alone. A caller narrows on `code` rather than on
186
+ `error`:
187
+
188
+ ```text
189
+ 503 {"type":"about:blank","title":"Service Unavailable","status":503,"detail":"Service Unavailable","instance":"/me","code":"STORE_FAILED"}
190
+ 401 {"type":"about:blank","title":"Unauthorized","status":401,"detail":"The request has no session","instance":"/me"}
191
+ 403 {"type":"about:blank","title":"Forbidden","status":403,"detail":"The view permission on this record is not granted","instance":"/records/r1"}
192
+ ```
193
+
194
+ Each reads the format of the app serving the request, with
195
+ `@alxia/core`'s `errorFormat(ctx)`
196
+ ([core's errors guide](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/errors.md)).
197
+
176
198
  ## Signatures
177
199
 
178
200
  ```ts
179
201
  function janusErrors(options?: JanusErrorsOptions): JanusErrors;
180
202
 
181
203
  // a middleware: answers the JanusErrors thrown behind it
182
- type JanusErrors = Middleware<object, Promise<Next | Reply<JanusErrorStatus, JanusErrorBody>>> & MiddlewareMark;
204
+ type JanusErrors = Middleware<object, Promise<Next | Reply<JanusErrorStatus, JanusErrorBody | JanusErrorProblem>>>;
205
+
206
+ // under alxia({ errors: 'problem' })
207
+ type JanusErrorProblem = Problem<JanusErrorStatus, JanusErrorBody>;
183
208
 
184
209
  interface JanusErrorsOptions {
185
210
  readonly report?: (error: JanusError, ctx: BaseContext) => unknown;
@@ -199,7 +224,7 @@ function statusOf(code: JanusErrorCode): JanusErrorStatus; // @nxgt/janus's
199
224
  ```
200
225
 
201
226
  `JanusError`, `JanusErrorCode` and `JanusErrorStatus` are `@nxgt/janus`'s;
202
- `Middleware`, `MiddlewareMark`, `Next`, `Reply` and `BaseContext` are `@alxia/core`'s.
227
+ `Middleware`, `Next`, `Reply`, `Problem` and `BaseContext` are `@alxia/core`'s.
203
228
 
204
229
  The permission guard's own refusals — `{ error: 'forbidden' }` and the
205
230
  like — are not `JanusError`s; see [Permissions](permissions.md#refusals).
@@ -98,6 +98,11 @@ Each guarded route may answer these refusals, with the body
98
98
  | `can` answers `false` | `403 {"error":"forbidden"}` |
99
99
  | allowed | the route runs, `object` set |
100
100
 
101
+ On an app with `alxia({ errors: 'problem' })`, each refusal is an RFC 9457
102
+ problem instead, sent as `application/problem+json` — `"title":"Forbidden",
103
+ "detail":"The view permission on this record is not granted"` — typed
104
+ `PermissionRefusedProblem` ([Errors](errors.md#as-problems)).
105
+
101
106
  **A failure throws.** A relation store that cannot answer throws
102
107
  `STORE_FAILED`, never a 403; a check that walks past `maxDepth` throws
103
108
  `PERMISSION_DEPTH`. Both are `JanusError`s: [`janusErrors()`](errors.md)
@@ -321,12 +326,13 @@ function permission<
321
326
  type: T,
322
327
  load: (ctx: BaseContext & LoadCtx) => Awaitable<O | null>,
323
328
  ...options: OptionsArgs<C, T, P, O, SubjectCtx, CheckCtx>
324
- ): Alxia<
325
- RequiresOf<LoadCtx & SubjectCtx & CheckCtx> & { object: O },
326
- '',
327
- Reply<401, PermissionRefusedBody> | Reply<404, PermissionRefusedBody> | Reply<403, PermissionRefusedBody>
328
- > &
329
- Requiring<RequiresOf<LoadCtx & SubjectCtx & CheckCtx>>;
329
+ ): Middleware<
330
+ RequiresOf<LoadCtx & SubjectCtx & CheckCtx>,
331
+ Promise<
332
+ | Reply<401 | 404 | 403, PermissionRefusedBody | PermissionRefusedProblem>
333
+ | Next<{ object: O }>
334
+ >
335
+ >;
330
336
 
331
337
  function byParam<O>(name: string, find: (id: string) => Awaitable<O | null>): (ctx: BaseContext) => Awaitable<O | null>;
332
338
 
@@ -344,10 +350,13 @@ interface PermissionRefusedBody {
344
350
  readonly error: 'unauthenticated' | 'not_found' | 'forbidden';
345
351
  }
346
352
 
353
+ // a refusal under @alxia/core's alxia({ errors: 'problem' })
354
+ type PermissionRefusedProblem = Problem<401 | 403 | 404>;
355
+
347
356
  type Awaitable<V> = V | Promise<V>;
348
357
  ```
349
358
 
350
- `RequiresOf` and `Requiring` are `@alxia/core`'s. `LoadCtx`, `SubjectCtx`
359
+ `RequiresOf`, `Middleware`, `Next`, `Reply` and `Problem` are `@alxia/core`'s. `LoadCtx`, `SubjectCtx`
351
360
  and `CheckCtx` are inferred from the types `load`, `subject` and `ctx`'s
352
361
  parameters are annotated with, `BaseContext` when they are not. `RequiresOf<X>` is what `X` adds to `BaseContext` — `{ tenant: Tenant }`
353
362
  for `BaseContext & { tenant: Tenant }` — and `Empty` when it adds nothing;
@@ -313,27 +313,32 @@ test('the session reads the user, and anonymous is a 401', async () => {
313
313
  function session<A extends Auth<{ readonly type: string }>, const T extends UserOfAuth<A>['type']>(
314
314
  auth: A,
315
315
  options: SessionOptions<T> & { readonly required: true },
316
- ): Alxia<
316
+ ): SessionMiddleware<
317
317
  { readonly user: UserOf<A, T>; readonly session: Session; readonly auth: RequestAuth },
318
- '', Reply<401, UnauthenticatedBody>
318
+ Reply<401, UnauthenticatedBody | UnauthenticatedProblem>
319
319
  >;
320
320
 
321
321
  function session<A extends Auth<{ readonly type: string }>, const T extends UserOfAuth<A>['type']>(
322
322
  auth: A,
323
323
  options?: SessionOptions<T> & { readonly required?: false },
324
- ): Alxia<
325
- { readonly user: UserOf<A, T> | null; readonly session: Session | null; readonly auth: RequestAuth },
326
- '', never
324
+ ): SessionMiddleware<
325
+ { readonly user: UserOf<A, T> | null; readonly session: Session | null; readonly auth: RequestAuth }
327
326
  >;
328
327
 
329
328
  function session<A extends Auth<{ readonly type: string }>, const T extends UserOfAuth<A>['type']>(
330
329
  auth: A,
331
330
  options?: SessionOptions<T>,
332
- ): Alxia<
331
+ ): SessionMiddleware<
333
332
  { readonly user: UserOf<A, T> | null; readonly session: Session | null; readonly auth: RequestAuth },
334
- '', Reply<401, UnauthenticatedBody>
333
+ Reply<401, UnauthenticatedBody | UnauthenticatedProblem>
335
334
  >;
336
335
 
336
+ // a middleware that gives Added, or answers Refused
337
+ type SessionMiddleware<Added extends object, Refused = never> = Middleware<Empty, Promise<Refused | Next<Added>>>;
338
+
339
+ // the 401 under @alxia/core's alxia({ errors: 'problem' })
340
+ type UnauthenticatedProblem = Problem<401>;
341
+
337
342
  interface SessionOptions<T extends string> {
338
343
  readonly type?: T;
339
344
  readonly required?: boolean;
@@ -355,7 +360,7 @@ type UserOfAuth<A> = A extends Auth<infer U> ? U : never;
355
360
  `RequestAuth` and `DeviceCookieOptions` are shown in
356
361
  [Signing in and out](sign-in-and-out.md#signatures).
357
362
  `UserOf<A, T>` is `Extract<UserOfAuth<A>, { readonly type: T }>`; it is not
358
- exported. `Alxia`, `Empty` and `Reply` are `@alxia/core`'s; `Session` and
363
+ exported. `Middleware`, `Next`, `Empty`, `Reply` and `Problem` are `@alxia/core`'s; `Session` and
359
364
  `SharedApi` are `@nxgt/janus`'s.
360
365
 
361
366
  Next: [Signing in and out](sign-in-and-out.md) sets the cookie this middleware
package/docs/roadmap.md CHANGED
@@ -11,7 +11,13 @@ number on it. Every release, with each change it made, is in
11
11
  are middlewares given to `use`. `janusErrors()` is a try/catch: it answers
12
12
  the refusals thrown behind it, so it goes to `use` before `session()`. A
13
13
  required `session()` on the app answers an anonymous request to a missing
14
- path with its 401: scope it with a `group`. Lands in 0.4.
14
+ path with its 401: scope it with a `group`. Lands in 0.4. Their deprecated
15
+ plugin form, `app.plugin(session(…))`, was removed with alxia 0.5.
16
+ - **Refusals as problems.** On an app with `@alxia/core`'s
17
+ `alxia({ errors: 'problem' })`, `janusErrors()`, a required `session()`
18
+ and `permission()` answer RFC 9457 problems — `janusErrors()`'s with its
19
+ `code` as an extension — and keep their bodies otherwise
20
+ ([guide](guide/errors.md#as-problems)).
15
21
 
16
22
  ## Next
17
23
 
@@ -34,7 +34,7 @@ symptom, under [Traps](#traps).
34
34
  - [`503 {"code":"STORE_FAILED"}`](#503-codestore_failed)
35
35
  - [`401 {"error":"unauthenticated"}` from the sign-in route](#401-errorunauthenticated-from-the-sign-in-route)
36
36
  - [`401 {"error":"unauthenticated"}` on a path that does not exist](#401-errorunauthenticated-on-a-path-that-does-not-exist)
37
- - [The error is never seen by a `try`/`catch` around `logger()` or `secureHeaders()`](#the-error-is-never-seen-by-a-trycatch-around-logger-or-secureheaders)
37
+ - [`logger()` or `secureHeaders()` never shows the reply of `janusErrors()`](#logger-or-secureheaders-never-shows-the-reply-of-januserrors)
38
38
  - [`401 {"code":"CREDENTIALS_INVALID","retryAfter":900}` with the right password](#401-codecredentials_invalidretryafter900-with-the-right-password)
39
39
 
40
40
  **Traps**
@@ -254,7 +254,7 @@ before the guard added — a tenant, a member — and its parameter is not
254
254
  annotated: `load: (ctx) => ctx.tenant.records.get(…)`.
255
255
 
256
256
  **Why:** an unannotated callback is typed with the request's `BaseContext`
257
- — the request, the URL, the path parameters — not with what other hooks
257
+ — the request, the URL, the path parameters — not with what other middlewares
258
258
  added. `permission()` is built before it is used, so it cannot see the app
259
259
  it will be used on.
260
260
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alxia/janus",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Identities and sessions for alxia on @nxgt/janus: the user typed in the context, the session cookie kept and renewed, a typed 401",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,16 +41,16 @@
41
41
  ]
42
42
  },
43
43
  "devDependencies": {
44
- "@alxia/core": "^0.4.0",
45
- "@alxia/i18n": "^0.2.0",
46
- "@alxia/language": "^0.2.0",
44
+ "@alxia/core": "^0.6.0",
45
+ "@alxia/i18n": "^0.3.1",
46
+ "@alxia/language": "^0.3.1",
47
47
  "@nxgt/i18n": "^2.0.0",
48
48
  "@nxgt/janus": "^0.17.1",
49
49
  "@types/bun": "^1.4.2",
50
50
  "zod": "^4.6.5"
51
51
  },
52
52
  "peerDependencies": {
53
- "@alxia/core": "^0.4.0",
53
+ "@alxia/core": "^0.6.0",
54
54
  "@nxgt/janus": "^0.17.1",
55
55
  "typescript": "^6.0.3 || ^7.0.0"
56
56
  }