@alxia/janus 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +191 -0
- package/dist/device.d.ts +21 -0
- package/dist/device.d.ts.map +1 -0
- package/dist/errors.d.ts +37 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +188 -0
- package/dist/index.js.map +15 -0
- package/dist/lookup.d.ts +9 -0
- package/dist/lookup.d.ts.map +1 -0
- package/dist/permission-options.d.ts +45 -0
- package/dist/permission-options.d.ts.map +1 -0
- package/dist/permission.d.ts +52 -0
- package/dist/permission.d.ts.map +1 -0
- package/dist/send.d.ts +34 -0
- package/dist/send.d.ts.map +1 -0
- package/dist/session.d.ts +67 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -0
- package/docs/README.md +16 -0
- package/docs/guide/errors.md +176 -0
- package/docs/guide/permissions.md +362 -0
- package/docs/guide/sessions.md +350 -0
- package/docs/guide/sign-in-and-out.md +340 -0
- package/docs/roadmap.md +68 -0
- package/docs/troubleshooting.md +600 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Steve Tsala
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# @alxia/janus
|
|
2
|
+
|
|
3
|
+
Identities, sessions and permissions for [alxia](https://www.npmjs.com/package/@alxia/core),
|
|
4
|
+
on [`@nxgt/janus`](https://www.npmjs.com/package/@nxgt/janus): your process,
|
|
5
|
+
your database. The user typed in the context, the session cookie kept and
|
|
6
|
+
renewed, janus's refusals answered — every one typed for the client. No
|
|
7
|
+
dependency.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bun add @alxia/janus @nxgt/janus @alxia/core
|
|
11
|
+
bun add -d typescript
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
It mirrors [`@nxgt/janus-hono`](https://www.npmjs.com/package/@nxgt/janus-hono):
|
|
15
|
+
the same cookie, the same rules, the same bodies.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { alxia } from '@alxia/core';
|
|
21
|
+
import { janusErrors, session } from '@alxia/janus';
|
|
22
|
+
import { createMemoryStores, janus, scryptHasher } from '@nxgt/janus';
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
|
|
25
|
+
const accounts = janus({
|
|
26
|
+
user: z.object({ email: z.email(), name: z.string() }),
|
|
27
|
+
password: { login: 'email' },
|
|
28
|
+
store: createMemoryStores(), // your database's adapter in production
|
|
29
|
+
hasher: scryptHasher(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const SignIn = z.object({ email: z.string(), password: z.string() });
|
|
33
|
+
|
|
34
|
+
const app = alxia()
|
|
35
|
+
.use(janusErrors()) // janus's refusals, typed
|
|
36
|
+
.use(session(accounts)) // not required: anonymous may sign in
|
|
37
|
+
.post('/signin', { body: SignIn }, async ({ body, auth, reply }) => {
|
|
38
|
+
const signedIn = await accounts.signIn(body);
|
|
39
|
+
return reply.ok({ id: auth.send(signedIn).id }); // the token in the cookie
|
|
40
|
+
})
|
|
41
|
+
.post('/signout', async ({ auth, reply }) => reply.ok(await auth.signOut()))
|
|
42
|
+
.use(session(accounts, { required: true })) // every route after it
|
|
43
|
+
.get('/me', ({ user, reply }) => reply.ok(user)); // user typed by the schema
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## `session(accounts, options?)`
|
|
47
|
+
|
|
48
|
+
The routes after it read `user`, `session` and `auth`. With
|
|
49
|
+
`required: true`, an anonymous request is a 401
|
|
50
|
+
`{ error: 'unauthenticated' }` and `user` is never `null`; without, it is
|
|
51
|
+
`null` for an anonymous request. A `boolean` known only at run time types
|
|
52
|
+
both: the 401, and a `user` that may be `null`. `type` narrows to one user
|
|
53
|
+
type of a multi-type `janus()`; `device` names the device cookie.
|
|
54
|
+
|
|
55
|
+
The session is read from `Authorization: Bearer`, `X-Session-Token`, or the
|
|
56
|
+
cookie. One renewed in passing is sent again as a cookie after the route —
|
|
57
|
+
only to a request that presented it as one, and never over a session
|
|
58
|
+
cookie the route set itself. **An outage is not anonymous**: a store that
|
|
59
|
+
cannot answer is `STORE_FAILED`, answered 503 by `janusErrors()`.
|
|
60
|
+
|
|
61
|
+
## `ctx.auth`: signing in and out
|
|
62
|
+
|
|
63
|
+
Sign-in stays on your `janus()` instance — `accounts.signIn`, `accounts.signUp`,
|
|
64
|
+
`accounts.patient.signIn`, a code, a link — and `ctx.auth` does the request's
|
|
65
|
+
part, bound to it:
|
|
66
|
+
|
|
67
|
+
| `ctx.auth.` | |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `send(signedIn)` | sets the session cookie (and the device cookie, with a `deviceToken`) and returns the user: the token is never in a body |
|
|
70
|
+
| `signOut()` | revokes the request's session and clears its cookie, whatever the answer |
|
|
71
|
+
| `device` | the device cookie's value, or `null`, for `signIn(…, { device: ctx.auth.device })` with `janus({ devices })` |
|
|
72
|
+
|
|
73
|
+
Put the sign-in routes behind a `session(accounts)` that is **not** required:
|
|
74
|
+
behind `required: true`, an anonymous request — anyone signing in — is a
|
|
75
|
+
401. Outside a route (a job, a test), `sendSession(ctx, accounts, signedIn)`,
|
|
76
|
+
`signOut(ctx, accounts)` and `deviceOf(ctx)` are the same functions, unbound.
|
|
77
|
+
|
|
78
|
+
## `janusErrors(options?)`
|
|
79
|
+
|
|
80
|
+
Every `JanusError` a route after it throws is answered with janus's status
|
|
81
|
+
and a body holding its `code` and only what a client can act on — the
|
|
82
|
+
issues, a minimum length, attempts left, seconds to wait (with
|
|
83
|
+
`Retry-After`). Never a login, a reason or a cause. `report(error, ctx)` is
|
|
84
|
+
called for the 5xx.
|
|
85
|
+
|
|
86
|
+
| status | codes |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| 400 | `USER_INVALID`, `PASSWORD_TOO_SHORT`, `TOKEN_*`… |
|
|
89
|
+
| 401 | `CREDENTIALS_INVALID`, `CODE_INVALID` |
|
|
90
|
+
| 403 | `USER_INACTIVE`, `STEP_UP_REQUIRED` |
|
|
91
|
+
| 404 | `NOT_FOUND` |
|
|
92
|
+
| 409 | `LOGIN_TAKEN`, `VERSION_CONFLICT`, `SECOND_FACTOR_*` |
|
|
93
|
+
| 500 | `PERMISSION_DEPTH` |
|
|
94
|
+
| 501 | `UNSUPPORTED` |
|
|
95
|
+
| 503 | `STORE_FAILED` |
|
|
96
|
+
|
|
97
|
+
## `permission(access, permission, type, load, options?)`
|
|
98
|
+
|
|
99
|
+
A guard on `@nxgt/janus/permissions`: the routes after it run only if the
|
|
100
|
+
subject — the session's `user`, or `options.subject(ctx)` — holds
|
|
101
|
+
`permission` on the object `load` finds, which they read as `object`.
|
|
102
|
+
Anonymous is a 401, nothing loaded a 404, a denial a 403, each typed. Scope
|
|
103
|
+
it with `group`:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { alxia } from '@alxia/core';
|
|
107
|
+
import { byParam, janusErrors, permission, session } from '@alxia/janus';
|
|
108
|
+
import { createMemoryStores, janus, scryptHasher } from '@nxgt/janus';
|
|
109
|
+
import { createMemoryRelations, defineModel, permissions } from '@nxgt/janus/permissions';
|
|
110
|
+
import { z } from 'zod';
|
|
111
|
+
|
|
112
|
+
const relations = createMemoryRelations();
|
|
113
|
+
const accounts = janus({
|
|
114
|
+
user: z.object({ email: z.email() }),
|
|
115
|
+
password: { login: 'email' },
|
|
116
|
+
store: createMemoryStores(),
|
|
117
|
+
relations,
|
|
118
|
+
hasher: scryptHasher(),
|
|
119
|
+
});
|
|
120
|
+
const access = permissions({
|
|
121
|
+
model: defineModel({
|
|
122
|
+
subjects: accounts.types,
|
|
123
|
+
types: { record: { related: { owners: ['user'] }, permits: { view: ['owners'] } } },
|
|
124
|
+
}),
|
|
125
|
+
store: relations,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const records = new Map([['r1', { id: 'r1', title: 'Blood test' }]]);
|
|
129
|
+
const findRecord = (id: string) => records.get(id) ?? null;
|
|
130
|
+
|
|
131
|
+
const app = alxia()
|
|
132
|
+
.use(janusErrors())
|
|
133
|
+
.use(session(accounts))
|
|
134
|
+
.group('/records/:id', (record) =>
|
|
135
|
+
record
|
|
136
|
+
.use(permission(access, 'view', 'record', byParam('id', findRecord)))
|
|
137
|
+
.get('/', ({ object, reply }) => reply(200, object)), // object: what findRecord found
|
|
138
|
+
);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`byParam(name, find)` loads by a path parameter. A permission whose
|
|
142
|
+
condition needs a context takes `ctx: (ctx, object) => …`, required by the
|
|
143
|
+
types exactly then.
|
|
144
|
+
|
|
145
|
+
### Reading the app's context
|
|
146
|
+
|
|
147
|
+
Annotate `load`, `subject` or `ctx`'s parameter to read what an earlier
|
|
148
|
+
plugin added. The guard then requires it: an app that does not give it
|
|
149
|
+
before the guard cannot use it.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import type { BaseContext } from '@alxia/core';
|
|
153
|
+
|
|
154
|
+
const byTenant = permission(
|
|
155
|
+
access,
|
|
156
|
+
'view',
|
|
157
|
+
'record',
|
|
158
|
+
({ tenant, pathParams }: BaseContext & { tenant: { records: Map<string, { id: string; title: string }> } }) =>
|
|
159
|
+
tenant.records.get(pathParams['id'] ?? '') ?? null,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
alxia().use(tenancy).use(session(accounts)).use(byTenant); // tenancy derives tenant
|
|
163
|
+
alxia().use(session(accounts)).use(byTenant); // a compile error: this app gives no `tenant`
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
A callback annotated `any` would require nothing, so the guard is refused
|
|
167
|
+
on every app: annotate what it reads, or leave it unannotated.
|
|
168
|
+
|
|
169
|
+
## API
|
|
170
|
+
|
|
171
|
+
| export | |
|
|
172
|
+
| --- | --- |
|
|
173
|
+
| `session(accounts, options?)` | the plugin: `user`, `session`, `auth` |
|
|
174
|
+
| `SessionOptions` | its options: `type`, `required`, `device` |
|
|
175
|
+
| `RequestAuth`, `SessionOpened` | the type of `ctx.auth`: `send`, `signOut`, `device`; and what `send` takes: `token`, `session` and `user` from `@nxgt/janus`'s `SignedIn`, and `deviceToken?` |
|
|
176
|
+
| `sendSession`, `signOut`, `deviceOf`, `sendDevice` | the cookies, outside a route |
|
|
177
|
+
| `DEVICE_COOKIE` | the device cookie's name, `janus-device`: the one `@nxgt/janus-hono` uses, so a device one remembers the other does too |
|
|
178
|
+
| `SendSessionOptions`, `DeviceCookieOptions` | their options: `device`; `name`, `domain`, `path`, `sameSite`, `secure`, `maxAge` |
|
|
179
|
+
| `janusErrors(options?)` | the plugin: janus's refusals answered |
|
|
180
|
+
| `JanusErrorsOptions` | its options: `report`, called with every error answered 5xx |
|
|
181
|
+
| `permission(…)`, `byParam(…)` | the guard |
|
|
182
|
+
| `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 |
|
|
183
|
+
| `bodyOf`, `statusOf` | a refusal's body and status |
|
|
184
|
+
| `UnauthenticatedBody`, `JanusErrorBody`, `PermissionRefusedBody` | their types |
|
|
185
|
+
| `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 |
|
|
186
|
+
|
|
187
|
+
## Documentation
|
|
188
|
+
|
|
189
|
+
- [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 plugin added included.
|
|
190
|
+
- [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.
|
|
191
|
+
- [Roadmap](https://github.com/softistx/alxia/blob/develop/packages/janus/docs/roadmap.md): what is coming, and what is not planned.
|
package/dist/device.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ResponseSettings } from '@alxia/core';
|
|
2
|
+
/** `@nxgt/janus-hono`'s name for it, so a device remembered by one is by the other. */
|
|
3
|
+
export declare const DEVICE_COOKIE = "janus-device";
|
|
4
|
+
export interface DeviceCookieOptions {
|
|
5
|
+
readonly name?: string;
|
|
6
|
+
readonly domain?: string;
|
|
7
|
+
readonly path?: string;
|
|
8
|
+
readonly sameSite?: 'lax' | 'strict' | 'none';
|
|
9
|
+
readonly secure?: boolean;
|
|
10
|
+
/** Seconds. Four hundred days, the most a browser keeps, by default. */
|
|
11
|
+
readonly maxAge?: number;
|
|
12
|
+
}
|
|
13
|
+
/** The device token the request carries, for `signIn(…, { device })`: `null` when none. */
|
|
14
|
+
export declare function deviceOf(ctx: {
|
|
15
|
+
readonly request: Request;
|
|
16
|
+
}, options?: Pick<DeviceCookieOptions, 'name'>): string | null;
|
|
17
|
+
/** Sets the device cookie, or sets it again so it lasts another `maxAge`. */
|
|
18
|
+
export declare function sendDevice(ctx: {
|
|
19
|
+
readonly set: ResponseSettings;
|
|
20
|
+
}, token: string, options?: DeviceCookieOptions): void;
|
|
21
|
+
//# 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,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,uFAAuF;AACvF,eAAO,MAAM,aAAa,iBAAiB,CAAC;AAI5C,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC9C,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,2FAA2F;AAC3F,wBAAgB,QAAQ,CACvB,GAAG,EAAE;IAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,GAAE,IAAI,CAAC,mBAAmB,EAAE,MAAM,CAAM,GAC7C,MAAM,GAAG,IAAI,CAIf;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CACzB,GAAG,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,gBAAgB,CAAA;CAAE,EACvC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,mBAAwB,GAC/B,IAAI,CASN"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type BaseContext } from '@alxia/core';
|
|
2
|
+
import { JanusError, type JanusErrorCode, type JanusErrorStatus, statusOf } from '@nxgt/janus';
|
|
3
|
+
/**
|
|
4
|
+
* The body a refusal is answered with: its `code`, and only what the client
|
|
5
|
+
* can act on. Never `reason`, `login`, a hash prefix or a cause: those are
|
|
6
|
+
* for your logs. `@nxgt/janus-hono`'s `bodyOf`, kept twice on purpose.
|
|
7
|
+
*/
|
|
8
|
+
export interface JanusErrorBody {
|
|
9
|
+
readonly code: JanusErrorCode;
|
|
10
|
+
readonly issues?: JanusError['issues'];
|
|
11
|
+
readonly minLength?: number;
|
|
12
|
+
readonly attemptsLeft?: number;
|
|
13
|
+
readonly retryAfter?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function bodyOf(error: JanusError): JanusErrorBody;
|
|
16
|
+
export interface JanusErrorsOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Called with every `JanusError` answered 5xx — `STORE_FAILED` and the
|
|
19
|
+
* like, the server's to fix — as it is answered: started, not awaited,
|
|
20
|
+
* so a slow report never holds the response. It cannot change the
|
|
21
|
+
* answer: one that throws or rejects is a warning.
|
|
22
|
+
*/
|
|
23
|
+
readonly report?: (error: JanusError, ctx: BaseContext) => unknown;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Janus's errors answered, as a plugin: every `JanusError` a route declared
|
|
27
|
+
* after it throws — a sign-in refused, a login taken, a store down — is
|
|
28
|
+
* answered with janus's status and `bodyOf(error)`, typed on those routes.
|
|
29
|
+
* A throttled sign-in carries `Retry-After`. Anything else goes on to the
|
|
30
|
+
* app's next `onError`, or its 500.
|
|
31
|
+
*
|
|
32
|
+
* **`STORE_FAILED` is a 503**, never a 401 or a 404: an outage is not an
|
|
33
|
+
* answer.
|
|
34
|
+
*/
|
|
35
|
+
export declare function janusErrors(options?: JanusErrorsOptions): import("@alxia/core").Alxia<import("@alxia/core").Empty, import("@alxia/core").Empty, "", import("@alxia/core").Reply<JanusErrorStatus, JanusErrorBody>>;
|
|
36
|
+
export { statusOf };
|
|
37
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,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;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,OAAO,GAAE,kBAAuB,4JAsB3D;AAED,OAAO,EAAE,QAAQ,EAAE,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { DEVICE_COOKIE, type DeviceCookieOptions, deviceOf, sendDevice, } from './device';
|
|
2
|
+
export { bodyOf, type JanusErrorBody, type JanusErrorsOptions, janusErrors, statusOf, } from './errors';
|
|
3
|
+
export { byParam, type PermissionRefusedBody, permission, } from './permission';
|
|
4
|
+
export type { Awaitable, ObjectData, OptionsArgs, PermissionOptions, } from './permission-options';
|
|
5
|
+
export { type SendSessionOptions, sendSession, signOut } from './send';
|
|
6
|
+
export { type SessionOptions, session, type UnauthenticatedBody, } from './session';
|
|
7
|
+
export type { Auth, RequestAuth, SessionOpened, UserOfAuth } from './types';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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,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,cAAc,EACnB,OAAO,EACP,KAAK,mBAAmB,GACxB,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// src/device.ts
|
|
2
|
+
var DEVICE_COOKIE = "janus-device";
|
|
3
|
+
var MAX_AGE = 400 * 24 * 60 * 60;
|
|
4
|
+
function deviceOf(ctx, options = {}) {
|
|
5
|
+
const cookies = new Bun.CookieMap(ctx.request.headers.get("cookie") ?? "");
|
|
6
|
+
const value = cookies.get(options.name ?? DEVICE_COOKIE);
|
|
7
|
+
return value === null || value === "" ? null : value;
|
|
8
|
+
}
|
|
9
|
+
function sendDevice(ctx, token, options = {}) {
|
|
10
|
+
ctx.set.cookies.set(options.name ?? DEVICE_COOKIE, token, {
|
|
11
|
+
httpOnly: true,
|
|
12
|
+
path: options.path ?? "/",
|
|
13
|
+
sameSite: options.sameSite ?? "lax",
|
|
14
|
+
secure: options.secure ?? true,
|
|
15
|
+
maxAge: options.maxAge ?? MAX_AGE,
|
|
16
|
+
...options.domain === undefined ? {} : { domain: options.domain }
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
// src/errors.ts
|
|
20
|
+
import { alxia } from "@alxia/core";
|
|
21
|
+
import {
|
|
22
|
+
JanusError,
|
|
23
|
+
statusOf
|
|
24
|
+
} from "@nxgt/janus";
|
|
25
|
+
function bodyOf(error) {
|
|
26
|
+
switch (error.code) {
|
|
27
|
+
case "USER_INVALID":
|
|
28
|
+
return { code: error.code, issues: error.issues ?? [] };
|
|
29
|
+
case "PASSWORD_TOO_SHORT":
|
|
30
|
+
return error.minLength === undefined ? { code: error.code } : { code: error.code, minLength: error.minLength };
|
|
31
|
+
case "CODE_INVALID":
|
|
32
|
+
return error.attemptsLeft === undefined ? { code: error.code } : { code: error.code, attemptsLeft: error.attemptsLeft };
|
|
33
|
+
case "CREDENTIALS_INVALID":
|
|
34
|
+
return error.retryAfter === undefined ? { code: error.code } : { code: error.code, retryAfter: error.retryAfter };
|
|
35
|
+
default:
|
|
36
|
+
return { code: error.code };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function janusErrors(options = {}) {
|
|
40
|
+
return alxia().onError((error, ctx) => {
|
|
41
|
+
if (!(error instanceof JanusError))
|
|
42
|
+
return;
|
|
43
|
+
const status = statusOf(error.code);
|
|
44
|
+
if (status >= 500 && options.report !== undefined) {
|
|
45
|
+
Promise.resolve().then(() => options.report?.(error, ctx)).catch((failure) => process.emitWarning(`janusErrors(): report failed: ${String(failure)}`));
|
|
46
|
+
}
|
|
47
|
+
const body = bodyOf(error);
|
|
48
|
+
return ctx.reply(status, body, error.retryAfter === undefined ? {} : { headers: { "retry-after": String(error.retryAfter) } });
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// src/permission.ts
|
|
52
|
+
import { definePlugin } from "@alxia/core";
|
|
53
|
+
function permission(access, permission, type, load, ...options) {
|
|
54
|
+
const loadOf = load;
|
|
55
|
+
const { subject, ctx: ctxOf } = options[0] ?? {};
|
|
56
|
+
const can = access.can;
|
|
57
|
+
const refuse = (error) => {
|
|
58
|
+
const body = { error };
|
|
59
|
+
return body;
|
|
60
|
+
};
|
|
61
|
+
return definePlugin()((app) => app.derive(async (ctx) => {
|
|
62
|
+
const who = subject === undefined ? userOf(ctx) : await subject(ctx);
|
|
63
|
+
if (who === null)
|
|
64
|
+
return ctx.reply(401, refuse("unauthenticated"));
|
|
65
|
+
const object = await loadOf(ctx);
|
|
66
|
+
if (object === null)
|
|
67
|
+
return ctx.reply(404, refuse("not_found"));
|
|
68
|
+
const allowed = await can(who, permission, view(object, type), ctxOf === undefined ? undefined : { ctx: await ctxOf(ctx, object) });
|
|
69
|
+
if (!allowed)
|
|
70
|
+
return ctx.reply(403, refuse("forbidden"));
|
|
71
|
+
return { object };
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
function byParam(name, find) {
|
|
75
|
+
return (ctx) => {
|
|
76
|
+
const id = ctx.pathParams[name];
|
|
77
|
+
return id === undefined ? null : find(id);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function view(object, type) {
|
|
81
|
+
return new Proxy(object, {
|
|
82
|
+
get: (target, key) => key === "type" ? type : Reflect.get(target, key),
|
|
83
|
+
has: (target, key) => key === "type" || Reflect.has(target, key)
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function userOf(ctx) {
|
|
87
|
+
const user = ctx.user;
|
|
88
|
+
if (user === undefined) {
|
|
89
|
+
throw new TypeError("permission(): no user in the context — use session(auth) before it, or pass { subject }");
|
|
90
|
+
}
|
|
91
|
+
return user;
|
|
92
|
+
}
|
|
93
|
+
// src/send.ts
|
|
94
|
+
function sendSession(ctx, auth, signedIn, options = {}) {
|
|
95
|
+
ctx.set.headers.append("set-cookie", auth.cookie.serialize(signedIn.token, signedIn.session));
|
|
96
|
+
if (typeof signedIn.deviceToken === "string") {
|
|
97
|
+
sendDevice(ctx, signedIn.deviceToken, options.device);
|
|
98
|
+
}
|
|
99
|
+
return signedIn.user;
|
|
100
|
+
}
|
|
101
|
+
async function signOut(ctx, auth) {
|
|
102
|
+
const revoked = await auth.signOut(ctx.request);
|
|
103
|
+
ctx.set.headers.append("set-cookie", auth.cookie.clear());
|
|
104
|
+
return revoked;
|
|
105
|
+
}
|
|
106
|
+
// src/session.ts
|
|
107
|
+
import {
|
|
108
|
+
alxia as alxia2,
|
|
109
|
+
withHeaders
|
|
110
|
+
} from "@alxia/core";
|
|
111
|
+
|
|
112
|
+
// src/lookup.ts
|
|
113
|
+
var lookups = new WeakMap;
|
|
114
|
+
function authenticateOnce(auth, request, type) {
|
|
115
|
+
let byRequest = lookups.get(auth);
|
|
116
|
+
if (byRequest === undefined) {
|
|
117
|
+
byRequest = new WeakMap;
|
|
118
|
+
lookups.set(auth, byRequest);
|
|
119
|
+
}
|
|
120
|
+
let byType = byRequest.get(request);
|
|
121
|
+
if (byType === undefined) {
|
|
122
|
+
byType = new Map;
|
|
123
|
+
byRequest.set(request, byType);
|
|
124
|
+
}
|
|
125
|
+
const key = type ?? "";
|
|
126
|
+
let found = byType.get(key);
|
|
127
|
+
if (found === undefined) {
|
|
128
|
+
found = auth.authenticate(request, type === undefined ? undefined : { type });
|
|
129
|
+
byType.set(key, found);
|
|
130
|
+
const asked = found;
|
|
131
|
+
asked.catch(() => {
|
|
132
|
+
if (byType.get(key) === asked)
|
|
133
|
+
byType.delete(key);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return found;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/session.ts
|
|
140
|
+
function session(auth, options = {}) {
|
|
141
|
+
const current = new WeakMap;
|
|
142
|
+
const unauthenticated = { error: "unauthenticated" };
|
|
143
|
+
return alxia2().derive(async (ctx) => {
|
|
144
|
+
const { request, reply } = ctx;
|
|
145
|
+
const found = await authenticateOnce(auth, request, options.type);
|
|
146
|
+
current.set(request, found);
|
|
147
|
+
if (found === null && options.required === true) {
|
|
148
|
+
return reply(401, unauthenticated);
|
|
149
|
+
}
|
|
150
|
+
const bound = {
|
|
151
|
+
device: deviceOf(ctx, options.device),
|
|
152
|
+
send: (signedIn) => sendSession(ctx, auth, signedIn, options),
|
|
153
|
+
signOut: () => signOut(ctx, auth)
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
user: found?.user ?? null,
|
|
157
|
+
session: found?.session ?? null,
|
|
158
|
+
auth: bound
|
|
159
|
+
};
|
|
160
|
+
}).wrap(async ({ request }, next) => {
|
|
161
|
+
const response = await next();
|
|
162
|
+
const found = current.get(request);
|
|
163
|
+
if (found?.renewed !== true)
|
|
164
|
+
return response;
|
|
165
|
+
const name = `${auth.cookie.name}=`;
|
|
166
|
+
const presented = new Bun.CookieMap(request.headers.get("cookie") ?? "").get(auth.cookie.name);
|
|
167
|
+
if (presented !== found.token || response.headers.getSetCookie().some((value) => value.startsWith(name))) {
|
|
168
|
+
return response;
|
|
169
|
+
}
|
|
170
|
+
return withHeaders(response, (headers) => headers.append("set-cookie", auth.cookie.serialize(found.token, found.session)));
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
export {
|
|
174
|
+
DEVICE_COOKIE,
|
|
175
|
+
bodyOf,
|
|
176
|
+
byParam,
|
|
177
|
+
deviceOf,
|
|
178
|
+
janusErrors,
|
|
179
|
+
permission,
|
|
180
|
+
sendDevice,
|
|
181
|
+
sendSession,
|
|
182
|
+
session,
|
|
183
|
+
signOut,
|
|
184
|
+
statusOf
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
//# debugId=229D182CFDBE7A6B64756E2164756E21
|
|
188
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/device.ts", "../src/errors.ts", "../src/permission.ts", "../src/send.ts", "../src/session.ts", "../src/lookup.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
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 { alxia, type BaseContext } 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 * Janus's errors answered, as a plugin: every `JanusError` a route declared\n * after it throws — a sign-in refused, a login taken, a store down — is\n * answered with janus's status and `bodyOf(error)`, typed on those routes.\n * A throttled sign-in carries `Retry-After`. Anything else goes on to the\n * app's next `onError`, or its 500.\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 = {}) {\n\treturn alxia().onError((error, ctx) => {\n\t\tif (!(error instanceof JanusError)) return undefined;\n\t\tconst status: JanusErrorStatus = statusOf(error.code);\n\t\tif (status >= 500 && options.report !== undefined) {\n\t\t\tPromise.resolve()\n\t\t\t\t.then(() => options.report?.(error, ctx))\n\t\t\t\t.catch((failure: unknown) =>\n\t\t\t\t\tprocess.emitWarning(\n\t\t\t\t\t\t`janusErrors(): report failed: ${String(failure)}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t}\n\t\tconst body = bodyOf(error);\n\t\treturn ctx.reply(\n\t\t\tstatus,\n\t\t\tbody,\n\t\t\terror.retryAfter === undefined\n\t\t\t\t? {}\n\t\t\t\t: { headers: { 'retry-after': String(error.retryAfter) } },\n\t\t);\n\t});\n}\n\nexport { statusOf };\n",
|
|
7
|
+
"import { type BaseContext, definePlugin, type RequiresOf } 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 plugin: 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:\n *\n * ```ts\n * app.use(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// `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 definePlugin<\n\t\tRequiresOf<\n\t\t\tLoadCtx & SubjectCtx & CheckCtx,\n\t\t\tAnnotatedAny<LoadCtx, SubjectCtx>\n\t\t>\n\t>()((app) =>\n\t\tapp.derive(async (ctx) => {\n\t\t\tconst who = subject === undefined ? userOf(ctx) : await subject(ctx);\n\t\t\tif (who === null) return ctx.reply(401, refuse('unauthenticated'));\n\t\t\tconst object = await loadOf(ctx);\n\t\t\tif (object === null) return ctx.reply(404, refuse('not_found'));\n\t\t\tconst allowed = await can(\n\t\t\t\twho,\n\t\t\t\tpermission,\n\t\t\t\tview(object, type),\n\t\t\t\tctxOf === undefined ? undefined : { ctx: await ctxOf(ctx, object) },\n\t\t\t);\n\t\t\tif (!allowed) return ctx.reply(403, refuse('forbidden'));\n\t\t\treturn { object };\n\t\t}),\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",
|
|
8
|
+
"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\ttype Alxia,\n\talxia,\n\ttype Empty,\n\ttype Reply,\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/**\n * Who a request belongs to, as a plugin: `auth.authenticate(request)`, and\n * the routes declared after it read `user` and `session` — typed by the\n * user schema, narrowed by `type`.\n *\n * **An outage is not anonymous**: a store that cannot answer throws\n * `STORE_FAILED`, which `janusErrors()` 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(session(accounts, { required: true })).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): Alxia<\n\t{\n\t\treadonly user: UserOf<A, T>;\n\t\treadonly session: Session;\n\t\treadonly auth: RequestAuth;\n\t},\n\tEmpty,\n\t'',\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): Alxia<\n\t{\n\t\treadonly user: UserOf<A, T> | null;\n\t\treadonly session: Session | null;\n\t\treadonly auth: RequestAuth;\n\t},\n\tEmpty,\n\t'',\n\tnever\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>,\n): Alxia<\n\t{\n\t\treadonly user: UserOf<A, T> | null;\n\t\treadonly session: Session | null;\n\t\treadonly auth: RequestAuth;\n\t},\n\tEmpty,\n\t'',\n\tReply<401, UnauthenticatedBody>\n>;\nexport function session(\n\tauth: Auth<{ readonly type: string }>,\n\toptions: SessionOptions<string> = {},\n): unknown {\n\tconst current = new WeakMap<Request, Found>();\n\tconst unauthenticated: UnauthenticatedBody = { error: 'unauthenticated' };\n\treturn alxia()\n\t\t.derive(async (ctx) => {\n\t\t\tconst { request, reply } = ctx;\n\t\t\tconst found = await authenticateOnce(auth, request, options.type);\n\t\t\tcurrent.set(request, found);\n\t\t\tif (found === null && options.required === true) {\n\t\t\t\treturn reply(401, unauthenticated);\n\t\t\t}\n\t\t\tconst bound: RequestAuth = {\n\t\t\t\tdevice: deviceOf(ctx, options.device),\n\t\t\t\tsend: (signedIn) => sendSession(ctx, auth, signedIn, options),\n\t\t\t\tsignOut: () => signOut(ctx, auth),\n\t\t\t};\n\t\t\treturn {\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\t.wrap(async ({ request }, next) => {\n\t\t\tconst response = await next();\n\t\t\tconst found = current.get(request);\n\t\t\tif (found?.renewed !== true) return response;\n\t\t\t// The route's own cookie wins: a sign-in or a sign-out behind this\n\t\t\t// plugin must not be undone by the session it replaced.\n\t\t\tconst name = `${auth.cookie.name}=`;\n\t\t\tconst presented = new Bun.CookieMap(\n\t\t\t\trequest.headers.get('cookie') ?? '',\n\t\t\t).get(auth.cookie.name);\n\t\t\tif (\n\t\t\t\tpresented !== found.token ||\n\t\t\t\tresponse.headers.getSetCookie().some((value) => value.startsWith(name))\n\t\t\t) {\n\t\t\t\treturn response;\n\t\t\t}\n\t\t\treturn withHeaders(response, (headers) =>\n\t\t\t\theaders.append(\n\t\t\t\t\t'set-cookie',\n\t\t\t\t\tauth.cookie.serialize(found.token, found.session),\n\t\t\t\t),\n\t\t\t);\n\t\t});\n}\n",
|
|
10
|
+
"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
|
+
"mappings": "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",
|
|
13
|
+
"debugId": "229D182CFDBE7A6B64756E2164756E21",
|
|
14
|
+
"names": []
|
|
15
|
+
}
|
package/dist/lookup.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Authenticated } from '@nxgt/janus';
|
|
2
|
+
import type { Auth } from './types';
|
|
3
|
+
export type Found = Authenticated<{
|
|
4
|
+
readonly type: string;
|
|
5
|
+
}> | null;
|
|
6
|
+
export declare function authenticateOnce(auth: Auth<{
|
|
7
|
+
readonly type: string;
|
|
8
|
+
}>, request: Request, type: string | undefined): Promise<Found>;
|
|
9
|
+
//# sourceMappingURL=lookup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lookup.d.ts","sourceRoot":"","sources":["../src/lookup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAEpC,MAAM,MAAM,KAAK,GAAG,aAAa,CAAC;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,CAAC;AAapE,wBAAgB,gBAAgB,CAC/B,IAAI,EAAE,IAAI,CAAC;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EACrC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,GAAG,SAAS,GACtB,OAAO,CAAC,KAAK,CAAC,CAyBhB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { BaseContext } from '@alxia/core';
|
|
2
|
+
import type { CheckableOf, CtxOf, FieldsOf, ModelConfig, ObjectTypeOf, SubjectRef } from '@nxgt/janus/permissions';
|
|
3
|
+
/**
|
|
4
|
+
* An object of type `T` as the application loads it: its id, every field a
|
|
5
|
+
* `fromField` of its type reads, and whatever else it carries.
|
|
6
|
+
*/
|
|
7
|
+
export type ObjectData<C extends ModelConfig, T extends ObjectTypeOf<C>> = {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
} & {
|
|
10
|
+
readonly [F in FieldsOf<C, T>]: string | null;
|
|
11
|
+
};
|
|
12
|
+
export type Awaitable<V> = V | Promise<V>;
|
|
13
|
+
/**
|
|
14
|
+
* The options of `permission()`: `ctx` required exactly when a condition of
|
|
15
|
+
* the permission is reachable, as for `can()`. `@nxgt/janus-hono`'s, over
|
|
16
|
+
* alxia's context, kept twice on purpose — except that `SubjectCtx` and
|
|
17
|
+
* `CheckCtx`, what `subject` and `ctx` read beyond `BaseContext`, are
|
|
18
|
+
* inferred from their annotated parameters, which the Hono ones are not.
|
|
19
|
+
*/
|
|
20
|
+
export type PermissionOptions<C extends ModelConfig, T extends ObjectTypeOf<C>, P extends string, O, SubjectCtx extends object = BaseContext, CheckCtx extends object = BaseContext> = {
|
|
21
|
+
/**
|
|
22
|
+
* Who asks. The `user` `session()` derived when absent. `null` is
|
|
23
|
+
* anonymous. Annotate its parameter to read what an earlier plugin adds.
|
|
24
|
+
*/
|
|
25
|
+
readonly subject?: (ctx: BaseContext & SubjectCtx) => Awaitable<SubjectRef<C> | null>;
|
|
26
|
+
} & ([CtxOf<C, T, P>] extends [never] ? {
|
|
27
|
+
readonly ctx?: never;
|
|
28
|
+
} : {
|
|
29
|
+
/**
|
|
30
|
+
* The condition's context, read from the request and the loaded
|
|
31
|
+
* object. Annotate its parameter to read what an earlier plugin adds.
|
|
32
|
+
*/
|
|
33
|
+
readonly ctx: (ctx: BaseContext & CheckCtx, object: O) => Awaitable<CtxOf<C, T, P>>;
|
|
34
|
+
});
|
|
35
|
+
export type OptionsArgs<C extends ModelConfig, T extends ObjectTypeOf<C>, P extends string, O, SubjectCtx extends object = BaseContext, CheckCtx extends object = BaseContext> = [ObjectTypeOf<C>] extends [T] ? IsSingle<ObjectTypeOf<C>> extends true ? PermissionArgs<C, T, P, O, SubjectCtx, CheckCtx> : [options?: LooseOptions<SubjectCtx, CheckCtx>] : PermissionArgs<C, T, P, O, SubjectCtx, CheckCtx>;
|
|
36
|
+
type PermissionArgs<C extends ModelConfig, T extends ObjectTypeOf<C>, P extends string, O, SubjectCtx extends object, CheckCtx extends object> = [CheckableOf<C, T>] extends [P] ? IsSingle<CheckableOf<C, T>> extends true ? StrictArgs<C, T, P, O, SubjectCtx, CheckCtx> : [options?: LooseOptions<SubjectCtx, CheckCtx>] : StrictArgs<C, T, P, O, SubjectCtx, CheckCtx>;
|
|
37
|
+
type StrictArgs<C extends ModelConfig, T extends ObjectTypeOf<C>, P extends string, O, SubjectCtx extends object, CheckCtx extends object> = [CtxOf<C, T, P>] extends [never] ? [options?: PermissionOptions<C, T, P, O, SubjectCtx, CheckCtx>] : [options: PermissionOptions<C, T, P, O, SubjectCtx, CheckCtx>];
|
|
38
|
+
type LooseOptions<SubjectCtx extends object, CheckCtx extends object> = {
|
|
39
|
+
readonly subject?: (ctx: BaseContext & SubjectCtx) => unknown;
|
|
40
|
+
readonly ctx?: (ctx: BaseContext & CheckCtx, object: never) => unknown;
|
|
41
|
+
};
|
|
42
|
+
type UnionToIntersection<U> = (U extends unknown ? (union: U) => void : never) extends (intersection: infer I) => void ? I : never;
|
|
43
|
+
type IsSingle<U> = [U] extends [UnionToIntersection<U>] ? true : false;
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=permission-options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-options.d.ts","sourceRoot":"","sources":["../src/permission-options.ts"],"names":[],"mappings":"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"}
|