@nxgt/janus 0.2.0 → 0.2.2
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 +3 -2
- package/dist/auth/context.d.ts +1 -1
- package/dist/auth/context.d.ts.map +1 -1
- package/dist/auth/port/types.d.ts +3 -1
- package/dist/auth/port/types.d.ts.map +1 -1
- package/dist/auth/sessions.d.ts.map +1 -1
- package/dist/auth/users.d.ts.map +1 -1
- package/dist/conformance/cases/users.d.ts.map +1 -1
- package/dist/conformance/index.js +16 -1
- package/dist/conformance/index.js.map +3 -3
- package/dist/index.js +52 -10
- package/dist/index.js.map +7 -6
- package/dist/stores/storable.d.ts +15 -0
- package/dist/stores/storable.d.ts.map +1 -0
- package/docs/guide/adapters.md +5 -2
- package/docs/guide/sessions.md +3 -1
- package/docs/guide/users.md +6 -2
- package/docs/guide/vocabulary.md +3 -2
- package/docs/roadmap.md +48 -16
- package/docs/troubleshooting.md +2 -2
- package/package.json +1 -1
package/docs/guide/users.md
CHANGED
|
@@ -66,7 +66,11 @@ time. A schema is any [Standard Schema](https://standardschema.dev) (Zod 4,
|
|
|
66
66
|
Valibot, ArkType), and its output must be JSON: a schema that produces a
|
|
67
67
|
`Date` is refused at compile time, because a `Date` round-trips through one
|
|
68
68
|
database and not the next. An optional field left `undefined` is dropped
|
|
69
|
-
before the store sees it.
|
|
69
|
+
before the store sees it. A string or a key holding a NUL character (`\u0000`)
|
|
70
|
+
or a lone surrogate is refused with `USER_INVALID`, on every adapter: PostgreSQL
|
|
71
|
+
keeps neither, so janus refuses them everywhere rather than fail on one. So is
|
|
72
|
+
a login your own `password.normalize` function turns into one — a `slice`
|
|
73
|
+
that cuts an emoji in half, say.
|
|
70
74
|
|
|
71
75
|
## Options
|
|
72
76
|
|
|
@@ -179,7 +183,7 @@ With a `password`, besides:
|
|
|
179
183
|
| --- | --- | --- |
|
|
180
184
|
| `signUp(fields & { password })` | `{ user, session, token }` | `USER_INVALID`, `PASSWORD_TOO_SHORT`, `LOGIN_TAKEN` |
|
|
181
185
|
| `signIn({ [login]: string, password })` | `{ user, session, token }` | `CREDENTIALS_INVALID`, `USER_INACTIVE`, `HASH_UNSUPPORTED` |
|
|
182
|
-
| `findByLogin(login)` | the user, or `null`; the login is normalised first | |
|
|
186
|
+
| `findByLogin(login)` | the user, or `null`; the login is normalised first, and one holding a NUL or a lone surrogate is nobody's | |
|
|
183
187
|
| `setPassword(user, password, { ifVersion? })` | the user — an admin's call | `PASSWORD_TOO_SHORT` |
|
|
184
188
|
| `changePassword(user, { current, next }, { ifVersion? })` | the user — the user's own call | `CREDENTIALS_INVALID`, `PASSWORD_TOO_SHORT` |
|
|
185
189
|
|
package/docs/guide/vocabulary.md
CHANGED
|
@@ -46,7 +46,8 @@ either finds this row.
|
|
|
46
46
|
| **lapsed**, **revoked**, **renewed** | A session past its `expiresAt`; one ended by a sign-out or a password reset; one whose `expiresAt` moved in passing (`renewAfter`) | "expired" — kept for `TOKEN_EXPIRED`, a one-time token |
|
|
47
47
|
| **anonymous** | A request that presents no session credential, or one that authenticates nobody: `authenticate` answers `null` | "unauthenticated", "guest", "logged out" |
|
|
48
48
|
| **bearer client** | A client that sends its session token as `Authorization: Bearer` rather than in a cookie | |
|
|
49
|
-
| **one-time token** | A single-use token sent by e-mail, for verification or a password reset | "code" — `code` is an error's code |
|
|
49
|
+
| **one-time token** | A single-use token sent by e-mail, for verification or a password reset | "code" alone — `code` is an error's code |
|
|
50
|
+
| **one-time code** | Planned, see [the roadmap](../roadmap.md#next): a one-time token short enough to type, sent by e-mail — or, for TOTP, computed by an authenticator app and never sent | "OTP", "PIN", "code" alone |
|
|
50
51
|
| **token** | Never alone in prose: a *session token* or a *one-time token*. The `tokens` store and the `TOKEN_*` codes are one-time tokens only | |
|
|
51
52
|
| **e-mail flow** | `verifyEmail` or `resetPassword`: send a one-time token, then confirm it | |
|
|
52
53
|
|
|
@@ -77,7 +78,7 @@ either finds this row.
|
|
|
77
78
|
| --- | --- | --- |
|
|
78
79
|
| **store** | Where a side keeps its data, behind a port. The **identity stores** are `users`, `sessions` and `tokens`; the **relation store** holds the tuples | "database", "repository" |
|
|
79
80
|
| **port** | The interface a store implements: `JanusStores` for the identity stores — named after the package, not the side — and `RelationStore` | "driver" |
|
|
80
|
-
| **adapter** | A package implementing the ports for one database: `@nxgt/janus-mongo`. What its `create…Adapter(db)` answers is its stores, keyed as `janus()` takes them — `{ store, relations }` | "plugin", "connector" |
|
|
81
|
+
| **adapter** | A package implementing the ports for one database: `@nxgt/janus-mongo`, `@nxgt/janus-drizzle`, `@nxgt/janus-redis`. What its `create…Adapter(db)` answers is its stores, keyed as `janus()` takes them — `{ store, relations }` | "plugin", "connector" |
|
|
81
82
|
| **integration** | A package fitting Janus into one web framework or one observability library: `@nxgt/janus-hono`, `@nxgt/janus-telemetry`. It implements no port | "plugin", "adapter" |
|
|
82
83
|
| **kit** | A package that opens the connections and wires adapters and integrations into one object for an application: `@nxgt/janus-kit`'s `connectKit` answers `{ auth, access, db, redis, ping, close }`. It implements no port, and `janus()` and `permissions()` are still written by the application | "framework", "starter" |
|
|
83
84
|
|
package/docs/roadmap.md
CHANGED
|
@@ -5,21 +5,37 @@ dates here, and the version something shipped in is the only number.
|
|
|
5
5
|
|
|
6
6
|
## Now
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
on Drizzle: both sides over one database, the tables in your own drizzle-kit
|
|
10
|
-
migrations. Built, not yet published.
|
|
11
|
-
- **Tracing and an audit trail** — in a package of its own,
|
|
12
|
-
`@nxgt/janus-telemetry`: a span per flow and per permission check, and the
|
|
13
|
-
security events worth keeping, never a login, a password or a token. Built,
|
|
14
|
-
not yet published.
|
|
15
|
-
- **A Redis adapter for sessions and tokens** — in a package of its own,
|
|
16
|
-
`@nxgt/janus-redis`: both are read on every request and ephemeral, so they
|
|
17
|
-
live in Redis, expired by Redis itself, while users stay in another store.
|
|
18
|
-
Built, not yet published.
|
|
8
|
+
Nothing yet.
|
|
19
9
|
|
|
20
10
|
## Next
|
|
21
11
|
|
|
22
|
-
|
|
12
|
+
- **One-time codes** — a one-time token short enough to type, sent by e-mail
|
|
13
|
+
to sign in without a password, or to confirm a sensitive action, issued
|
|
14
|
+
and redeemed by `janus` like the verification and reset tokens today; and
|
|
15
|
+
TOTP, the one-time codes of an authenticator app, as a second factor.
|
|
16
|
+
- **Sending the e-mails** — in a package of its own, `@nxgt/janus-mail`, built
|
|
17
|
+
on a general mail toolkit shared with applications that are not about
|
|
18
|
+
sign-in: a `Mailer` port you plug your transport into (SMTP, Resend, SES…) —
|
|
19
|
+
a transport that fails throws, like a store — and default templates for
|
|
20
|
+
verification, password reset and one-time codes, in English and French. One
|
|
21
|
+
template per e-mail, never one HTML file per language: the layout is built
|
|
22
|
+
once with Maizzle and Tailwind CSS 4 — CSS inlined for mail clients — and
|
|
23
|
+
its text lives in ICU message catalogues, one per language, plurals and
|
|
24
|
+
dates included. Both are compiled when the package is built into typed
|
|
25
|
+
functions: `templates.verifyEmail({ locale: 'fr', link })` answers
|
|
26
|
+
`{ subject, html, text }`, every value escaped, a missing variable or an
|
|
27
|
+
unknown locale a compile error, a message that fails to format a throw —
|
|
28
|
+
never an e-mail sent with `{link}` in it. No template engine at run time.
|
|
29
|
+
The defaults are a starting point, not a requirement: add a language with a
|
|
30
|
+
catalogue, or replace any one template with your own function of the same
|
|
31
|
+
shape — built with the same toolkit, React Email or a plain string — and
|
|
32
|
+
keep the defaults for the rest.
|
|
33
|
+
- **Webhooks** — signed HTTP events when something happens to a user
|
|
34
|
+
(created, e-mail verified, password reset, deleted), so another service can
|
|
35
|
+
follow without polling: a signature it can check, retries on failure, and
|
|
36
|
+
the same rule as the audit trail — the user named by id, never a login, a
|
|
37
|
+
password, a session token or a one-time token in a payload, and every key
|
|
38
|
+
camelCase. Retries that run out are reported, never dropped in silence.
|
|
23
39
|
|
|
24
40
|
## Later
|
|
25
41
|
|
|
@@ -66,7 +82,19 @@ Nothing yet.
|
|
|
66
82
|
|
|
67
83
|
## Shipped
|
|
68
84
|
|
|
69
|
-
|
|
85
|
+
Each entry names the version it came in.
|
|
86
|
+
|
|
87
|
+
- **The adapters and the kit, each at its first release, v0.1.0, beside
|
|
88
|
+
`@nxgt/janus` 0.2.2** —
|
|
89
|
+
[`@nxgt/janus-drizzle`](https://www.npmjs.com/package/@nxgt/janus-drizzle),
|
|
90
|
+
both sides over one PostgreSQL database on Drizzle;
|
|
91
|
+
[`@nxgt/janus-redis`](https://www.npmjs.com/package/@nxgt/janus-redis),
|
|
92
|
+
sessions and one-time tokens in Redis, expired by Redis itself;
|
|
93
|
+
[`@nxgt/janus-telemetry`](https://www.npmjs.com/package/@nxgt/janus-telemetry),
|
|
94
|
+
a span per flow and per permission check, and the security events worth an
|
|
95
|
+
audit trail, never a login, a password, a session token or a one-time token;
|
|
96
|
+
and [`@nxgt/janus-kit`](https://www.npmjs.com/package/@nxgt/janus-kit), all
|
|
97
|
+
of it wired in one call.
|
|
70
98
|
|
|
71
99
|
- **The model's keys read `related` and `permits`** — Keto's OPL words: an
|
|
72
100
|
object type declares `related: { members: ['staff', 'team#members'] }` and
|
|
@@ -74,13 +102,17 @@ The first public release, v0.1.
|
|
|
74
102
|
`relations` and `permissions` as keys are refused, by the compiler and by
|
|
75
103
|
`defineModel`, with a message naming the new key —
|
|
76
104
|
`types.team.relations is now related: rename the key`. The `permissions()`
|
|
77
|
-
function and `janus({ relations })` keep their names. —
|
|
105
|
+
function and `janus({ relations })` keep their names. — v0.2.0
|
|
78
106
|
- **`LOGIN_TAKEN` no longer quotes the login in its message**, in the memory
|
|
79
107
|
store and in both adapters; `error.login` still names it, and the
|
|
80
|
-
conformance suite checks it. —
|
|
108
|
+
conformance suite checks it. — v0.2.0
|
|
81
109
|
- **The conformance suite accepts a store with its own expiry** — a store
|
|
82
110
|
that drops a lapsed session at once, as a Redis TTL does, passes
|
|
83
|
-
`sessions.deleteUser`; one that still holds it must count it. —
|
|
111
|
+
`sessions.deleteUser`; one that still holds it must count it. — v0.2.0
|
|
112
|
+
- **A NUL character or a lone surrogate never reaches a store** — refused in
|
|
113
|
+
fields with `USER_INVALID` on every adapter, rather than `STORE_FAILED` on
|
|
114
|
+
PostgreSQL alone; a login holding one is nobody's. The conformance suite
|
|
115
|
+
holds every adapter to round-tripping every other character. — v0.2.1
|
|
84
116
|
|
|
85
117
|
- **A Hono integration** — [`@nxgt/janus-hono`](https://www.npmjs.com/package/@nxgt/janus-hono):
|
|
86
118
|
the session middleware, the cookie, a route guarded by a permission,
|
package/docs/troubleshooting.md
CHANGED
|
@@ -354,7 +354,7 @@ await auth.update(user, { name }, { ifVersion: user.version });
|
|
|
354
354
|
`UserInvalidError`, carrying `issues` — each a `path` and a `message`.
|
|
355
355
|
|
|
356
356
|
**When:** `signUp`, `create`, `update`.
|
|
357
|
-
**Why:** your schema refused the fields. `update` merges the patch over the stored fields and validates the whole, so the refusal can name a field the patch did not touch. An issue whose message is `set by janus, not by a request` means the input carried a field janus sets (`id`, `active`, `version`, …) and your schema let it through.
|
|
357
|
+
**Why:** your schema refused the fields. `update` merges the patch over the stored fields and validates the whole, so the refusal can name a field the patch did not touch. An issue whose message is `set by janus, not by a request` means the input carried a field janus sets (`id`, `active`, `version`, …) and your schema let it through. An issue whose message is `holds a NUL character or a lone surrogate, which no store can keep` means a string — or, with `a key holds …`, an object key — carried `\u0000` or half of a surrogate pair: PostgreSQL refuses both, so janus refuses them on every adapter rather than answer `STORE_FAILED` on one. A key is reported by the path of the object holding it, never by the key itself — in janus's issues and in your schema's alike. An issue at your login field whose message is `normalises to a login that holds …` means your own `password.normalize` function produced such a login from a valid field: fix the function. On MongoDB or the memory store, a user written before janus refused these characters may already hold one: every `update` of that user is refused, whatever it patches, until the field is cleaned — patch it with a clean value in the same `update`.
|
|
358
358
|
**Fix:** answer 400, field by field:
|
|
359
359
|
|
|
360
360
|
```ts
|
|
@@ -374,7 +374,7 @@ if (error instanceof UserInvalidError) {
|
|
|
374
374
|
Also `changePassword: the current password does not match`.
|
|
375
375
|
|
|
376
376
|
**When:** `signIn`, `changePassword`.
|
|
377
|
-
**Why:** no user holds the login, the user has no password, or the password is wrong — **one code for the three**. `error.reason` (`unknownLogin`, `noPassword`, `wrongPassword`) tells them apart for your logs and your rate limiter.
|
|
377
|
+
**Why:** no user holds the login, the user has no password, or the password is wrong — **one code for the three**. `error.reason` (`unknownLogin`, `noPassword`, `wrongPassword`) tells them apart for your logs and your rate limiter. A login holding a NUL character or a lone surrogate is `unknownLogin`: no user can hold one.
|
|
378
378
|
**Fix:** answer 401 with the same body whatever the reason:
|
|
379
379
|
|
|
380
380
|
```ts
|