@nxgt/janus 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -12
- package/dist/auth/config/reserved.d.ts +1 -1
- package/dist/auth/config/reserved.d.ts.map +1 -1
- package/dist/auth/email-flows/any-type-change-email.d.ts +18 -0
- package/dist/auth/email-flows/any-type-change-email.d.ts.map +1 -0
- package/dist/auth/email-flows/change-email-token.d.ts +6 -1
- package/dist/auth/email-flows/change-email-token.d.ts.map +1 -1
- package/dist/auth/email-flows/change-email.d.ts +6 -0
- package/dist/auth/email-flows/change-email.d.ts.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/janus.d.ts.map +1 -1
- package/dist/auth/outage.d.ts +2 -2
- package/dist/auth/port/assert-stores.d.ts.map +1 -1
- package/dist/auth/port/memory/tokens.d.ts.map +1 -1
- package/dist/auth/port/types/stores.d.ts +2 -0
- package/dist/auth/port/types/stores.d.ts.map +1 -1
- package/dist/auth/port/types/tokens.d.ts +12 -0
- package/dist/auth/port/types/tokens.d.ts.map +1 -1
- package/dist/auth/sessions/collect-expired.d.ts +14 -0
- package/dist/auth/sessions/collect-expired.d.ts.map +1 -0
- package/dist/auth/sessions/shared-api.d.ts +1 -1
- package/dist/auth/sessions/shared-api.d.ts.map +1 -1
- package/dist/auth/types/change-email.d.ts +31 -0
- package/dist/auth/types/change-email.d.ts.map +1 -1
- package/dist/auth/types/index.d.ts +1 -1
- package/dist/auth/types/index.d.ts.map +1 -1
- package/dist/auth/types/janus.d.ts +13 -5
- package/dist/auth/types/janus.d.ts.map +1 -1
- package/dist/chunks/{index-6bjfv4xv.js → index-q3w3vr69.js} +13 -3
- package/dist/chunks/{index-6bjfv4xv.js.map → index-q3w3vr69.js.map} +3 -3
- package/dist/chunks/{index-cntabs9d.js → index-rpp0bw6e.js} +2 -2
- package/dist/chunks/{index-kppkcbn5.js → index-spwh8b9z.js} +2 -5
- package/dist/chunks/{index-kppkcbn5.js.map → index-spwh8b9z.js.map} +2 -2
- package/dist/chunks/{index-4khsm7mw.js → index-y9b327j8.js} +18 -18
- package/dist/chunks/{index-4khsm7mw.js.map → index-y9b327j8.js.map} +5 -5
- package/dist/conformance/cases/outage.d.ts.map +1 -1
- package/dist/conformance/cases/tokens/deletion.d.ts.map +1 -1
- package/dist/conformance/describe.d.ts +1 -0
- package/dist/conformance/describe.d.ts.map +1 -1
- package/dist/conformance/index.js +39 -5
- package/dist/conformance/index.js.map +5 -5
- package/dist/conformance/types.d.ts +1 -1
- package/dist/conformance/types.d.ts.map +1 -1
- package/dist/index.js +478 -453
- package/dist/index.js.map +21 -19
- package/dist/permissions/index.js +3 -3
- package/docs/guide/adapters.md +40 -3
- package/docs/guide/email-flows.md +16 -2
- package/docs/guide/errors.md +1 -1
- package/docs/guide/passwords.md +7 -10
- package/docs/guide/sessions.md +15 -4
- package/docs/guide/users.md +1 -1
- package/docs/roadmap.md +14 -20
- package/docs/troubleshooting.md +12 -10
- package/package.json +1 -1
- /package/dist/chunks/{index-cntabs9d.js.map → index-rpp0bw6e.js.map} +0 -0
|
@@ -4,16 +4,16 @@ import {
|
|
|
4
4
|
UNSTORABLE,
|
|
5
5
|
MAX_PAGE_SIZE2,
|
|
6
6
|
pageLimit2
|
|
7
|
-
} from "../chunks/index-
|
|
7
|
+
} from "../chunks/index-y9b327j8.js";
|
|
8
8
|
import {
|
|
9
9
|
PermissionDepthError2,
|
|
10
10
|
isSubjectSet2,
|
|
11
11
|
isSetOf2,
|
|
12
12
|
formatEntity2
|
|
13
|
-
} from "../chunks/index-
|
|
13
|
+
} from "../chunks/index-spwh8b9z.js";
|
|
14
14
|
import {
|
|
15
15
|
createMemoryRelations2
|
|
16
|
-
} from "../chunks/index-
|
|
16
|
+
} from "../chunks/index-rpp0bw6e.js";
|
|
17
17
|
|
|
18
18
|
// src/permissions/resolve/resolved.ts
|
|
19
19
|
function admits(holders, subject) {
|
package/docs/guide/adapters.md
CHANGED
|
@@ -31,7 +31,7 @@ describeJanusStores({
|
|
|
31
31
|
|
|
32
32
|
| Port | Taken by | Methods |
|
|
33
33
|
| --- | --- | --- |
|
|
34
|
-
| `JanusStores` — `{ users: UserStore, sessions: SessionStore, tokens: TokenStore }` | `janus({ store })` | 6 (+ 1 optional) + 7 (+ 1 optional) + 5 |
|
|
34
|
+
| `JanusStores` — `{ users: UserStore, sessions: SessionStore, tokens: TokenStore }` | `janus({ store })` | 6 (+ 1 optional) + 7 (+ 1 optional) + 5 (+ 1 optional) |
|
|
35
35
|
| `RelationStore` | `permissions({ store })`, `janus({ relations })` | 6 |
|
|
36
36
|
|
|
37
37
|
They are separate on purpose: an application that only authenticates
|
|
@@ -184,6 +184,7 @@ interface TokenStore {
|
|
|
184
184
|
countAttempt(tokenHash: string, kind: TokenKind): Promise<TokenRecord | null>;
|
|
185
185
|
spendUserTokens(userId: Id, kind: TokenKind, at: Date, except?: string): Promise<number>; // the unspent ones, but except
|
|
186
186
|
deleteUserTokens(userId: Id): Promise<number>;
|
|
187
|
+
deleteExpiredTokens?(before: Date): Promise<number>; // optional: omit it if the database expires on its own
|
|
187
188
|
}
|
|
188
189
|
```
|
|
189
190
|
|
|
@@ -368,6 +369,41 @@ of the right `kind` whose `spentAt` is empty, skipping the one named by
|
|
|
368
369
|
`except`. `spendUserTokens` has its own
|
|
369
370
|
outage case.
|
|
370
371
|
|
|
372
|
+
### `TokenStore.deleteExpiredTokens` (optional)
|
|
373
|
+
|
|
374
|
+
What `collectExpired()` asks for, beside `SessionStore.deleteExpiredSessions`:
|
|
375
|
+
**delete every token whose `expiresAt` is at or before `before`**, and answer
|
|
376
|
+
how many. Leave it out when your database expires records on its own (a TTL
|
|
377
|
+
index, a key TTL) — nothing breaks, the core compares `expiresAt` on every
|
|
378
|
+
redemption — and implement it when it does not, or the rows pile up.
|
|
379
|
+
|
|
380
|
+
| The stored token | Written |
|
|
381
|
+
| --- | --- |
|
|
382
|
+
| `expiresAt` at or before `before`, spent or not, of any kind and any user | deleted, and counted |
|
|
383
|
+
| `expiresAt` later than `before`, spent or not | **kept** |
|
|
384
|
+
| none at all | `0`, an absence — never a failure |
|
|
385
|
+
|
|
386
|
+
**Never delete a token whose `expiresAt` is later, spent or not**: a spent
|
|
387
|
+
token is kept, answered as spent, until it lapses, or a count would start
|
|
388
|
+
over. That includes the throttles' counts — ordinary `secondFactor` tokens
|
|
389
|
+
under a keyed-hash user id, which `deleteUserTokens` never reaches and this
|
|
390
|
+
method is the only thing to collect. The core calls it with `clock.now()`, no
|
|
391
|
+
margin: a link collected answers `TOKEN_UNKNOWN`.
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
import type { TokenStore } from '@nxgt/janus';
|
|
395
|
+
|
|
396
|
+
// PostgreSQL through Drizzle — one statement, over an index on expires_at
|
|
397
|
+
deleteExpiredTokens: async (before) =>
|
|
398
|
+
(await db.delete(tokens).where(lte(tokens.expiresAt, before)).returning({ tokenHash: tokens.tokenHash })).length,
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
`janus()` reads its presence once (`StoreCapabilities.collectExpiredTokens`),
|
|
402
|
+
and refuses a `deleteExpiredTokens` that is there but not a function with a
|
|
403
|
+
`TypeError`. The suite's `tokens.deleteExpired` and `outage.deleteExpiredTokens`
|
|
404
|
+
cases check it, and are skipped with their reason for a store that leaves it
|
|
405
|
+
out. A failure throws, as everywhere on the port.
|
|
406
|
+
|
|
371
407
|
### `RelationStore`
|
|
372
408
|
|
|
373
409
|
```ts
|
|
@@ -449,7 +485,7 @@ compile error naming the missing method.
|
|
|
449
485
|
|
|
450
486
|
| Suite | Cases | Harness opens |
|
|
451
487
|
| --- | --- | --- |
|
|
452
|
-
| `describeJanusStores({ name, harness, runner?, faults?, skip? })` |
|
|
488
|
+
| `describeJanusStores({ name, harness, runner?, faults?, skip? })` | 61: users, sessions, tokens, and one outage per method whose honest answer can be "nothing" — sixteen of them | `{ stores, faults?, close? }` |
|
|
453
489
|
| `describeRelationStores({ name, harness, runner?, faults?, skip? })` | 16: the relation store, ids of edge characters, and one outage per method | `{ store, faults?, close? }` |
|
|
454
490
|
|
|
455
491
|
`harness.open()` is called **once per case** and must answer fresh, empty
|
|
@@ -546,7 +582,8 @@ describeJanusStores({
|
|
|
546
582
|
|
|
547
583
|
A skipped case still appears in the run, with its reason in its name. A case
|
|
548
584
|
that cannot run on the stores it was given — an outage case with no `faults`,
|
|
549
|
-
`
|
|
585
|
+
`sessions.deleteExpired` on a store without `deleteExpiredSessions`,
|
|
586
|
+
`tokens.deleteExpired` on one without `deleteExpiredTokens`, a `findUsers`
|
|
550
587
|
case on a store without `findUsers` — passes, and emits a
|
|
551
588
|
`JANUS_CONFORMANCE_SKIPPED` warning with the reason. A case's `needs` says what
|
|
552
589
|
it cannot run without — one `CaseNeed` or a list: `outage.findUsers` needs
|
|
@@ -265,6 +265,20 @@ is no longer the one it was asked from although nothing spent it: a write
|
|
|
265
265
|
outside janus, or a `send` racing an `update`. `confirm` sends `user.emailChanged` (with
|
|
266
266
|
`formerEmail`) and `user.emailVerified`.
|
|
267
267
|
|
|
268
|
+
**With several user types**, a type's own `confirm` answers `TOKEN_UNKNOWN`
|
|
269
|
+
for a token issued to another type's user, and spends it — a later
|
|
270
|
+
confirmation through the right type is then `TOKEN_SPENT`. A page that does
|
|
271
|
+
not know the type calls the root's `confirm`, which reads the user the token
|
|
272
|
+
was issued to and confirms as their type does: same move, same events, same
|
|
273
|
+
errors, and `type` in the answer says which type it was. `send` stays on each
|
|
274
|
+
type. The root's exists when one type has an e-mail, which is why no type may
|
|
275
|
+
be named `changeEmail`.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
// one anonymous page for every type
|
|
279
|
+
const changed = await auth.changeEmail.confirm(token); // changed.type: 'admin' | 'member'
|
|
280
|
+
```
|
|
281
|
+
|
|
268
282
|
**Gate `send`, and tell the former address.** Whoever changes the e-mail
|
|
269
283
|
takes over every reset: call `send` behind a fresh sign-in or a step-up.
|
|
270
284
|
|
|
@@ -511,8 +525,8 @@ answers again. A refused request is counted too but cannot extend the wait.
|
|
|
511
525
|
**Where the counts live.** In the tokens store, as `secondFactor` tokens named
|
|
512
526
|
by a keyed hash of the flow and the address or user id: no port change, no new
|
|
513
527
|
infrastructure, and never the address. That is one row per flow per address or
|
|
514
|
-
user per window.
|
|
515
|
-
|
|
528
|
+
user per window. `collectExpired()` collects the lapsed ones on a tokens store that implements
|
|
529
|
+
`deleteExpiredTokens`; a store with a TTL expires them on its own. A flushed or evicting
|
|
516
530
|
Redis forgets the counts, and a tokens store that cannot count throws
|
|
517
531
|
`STORE_FAILED` with nothing issued: it fails closed.
|
|
518
532
|
|
package/docs/guide/errors.md
CHANGED
|
@@ -64,7 +64,7 @@ that wired the library, so no handler needs to tell it apart.
|
|
|
64
64
|
| `SECOND_FACTOR_NOT_ENROLLED` | `SecondFactorError` | 409 | `activate` before `enroll`; `regenerateRecoveryCodes` without an active factor; `confirm` or `recover` after the factor was disabled | `userId` |
|
|
65
65
|
| `SECOND_FACTOR_ACTIVE` | `SecondFactorError` | 409 | `enroll` or `activate` on a factor already active: `disable` it first | `userId` |
|
|
66
66
|
| `INVALID_CURSOR` | `InvalidCursorError` | 400 | A cursor this store did not mint. Never a silent first page | |
|
|
67
|
-
| `UNSUPPORTED` | `UnsupportedError` | 501 | The wired store lacks an optional capability — `collectExpired`
|
|
67
|
+
| `UNSUPPORTED` | `UnsupportedError` | 501 | The wired store lacks an optional capability — `collectExpired` with neither `deleteExpiredSessions` nor `deleteExpiredTokens` | `slot`, `operation` |
|
|
68
68
|
| `PERMISSION_DEPTH` | `PermissionDepthError` | 500 | A check or list walked past `maxDepth`. **Not a denial** | `permission`, `maxDepth` |
|
|
69
69
|
|
|
70
70
|
Every class extends `JanusError`, which extends `Error`, so no `catch` block
|
package/docs/guide/passwords.md
CHANGED
|
@@ -195,16 +195,13 @@ counts correctly. Each is named by a keyed hash of the login, never the login
|
|
|
195
195
|
otherwise, which only keeps the login out of plain sight: a dump of the
|
|
196
196
|
tokens then tells which logins were tried, for a guessed list. Each expires
|
|
197
197
|
two windows after its window starts — half an hour by default — **Redis and MongoDB drop it then;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
`tokens` has no index on `expires_at`, so the delete scans the table: add
|
|
207
|
-
`create index on tokens (expires_at)` in a migration of yours if it grows.
|
|
198
|
+
a store with no TTL keeps it** until `collectExpired()` deletes it (the
|
|
199
|
+
tokens store's optional `deleteExpiredTokens`), and every login tried,
|
|
200
|
+
registered or not, adds a row per window. Schedule `collectExpired()` on such
|
|
201
|
+
a store: `@nxgt/janus-drizzle` implements `deleteExpiredTokens` (since 0.9),
|
|
202
|
+
with an index on `expires_at` that its
|
|
203
|
+
[migration](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus-drizzle/docs/guide/migrations.md#to-09-an-index-on-tokensexpires_at)
|
|
204
|
+
adds. A store of your own implements the method, or its rows stay.
|
|
208
205
|
|
|
209
206
|
The key is your first `secondFactor` key: wiring
|
|
210
207
|
`secondFactor` for the first time, or putting a new key first, starts every
|
package/docs/guide/sessions.md
CHANGED
|
@@ -329,10 +329,21 @@ a link or a code already in the person's inbox still works afterwards.
|
|
|
329
329
|
const removed: number = await auth.collectExpired();
|
|
330
330
|
```
|
|
331
331
|
|
|
332
|
-
Deletes lapsed sessions
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
332
|
+
Deletes lapsed sessions and lapsed one-time tokens, from the stores that keep
|
|
333
|
+
them, and answers how many in all. The tokens include the sign-in and mail
|
|
334
|
+
throttles' counts, which are ordinary tokens. It runs whichever of the
|
|
335
|
+
optional `sessions.deleteExpiredSessions` and `tokens.deleteExpiredTokens` the
|
|
336
|
+
stores implement, sessions first, and rejects with `UNSUPPORTED` only when
|
|
337
|
+
neither does — a store with its own TTL, such as `@nxgt/janus-mongo`'s, does
|
|
338
|
+
not implement them. The reference store implements both.
|
|
339
|
+
|
|
340
|
+
A token is deleted once its `expiresAt` is at or before now, spent or not;
|
|
341
|
+
one that has not lapsed is never deleted, so a throttle keeps counting and a
|
|
342
|
+
link still redeems until its time is up. After a token is collected, its link
|
|
343
|
+
answers `TOKEN_UNKNOWN` rather than `TOKEN_EXPIRED` or `TOKEN_SPENT`, as it
|
|
344
|
+
already does on a store with a TTL. A failure in the tokens store, after the
|
|
345
|
+
sessions went, is `STORE_FAILED`; both deletions are idempotent, so call it
|
|
346
|
+
again.
|
|
336
347
|
|
|
337
348
|
## Testing expiry
|
|
338
349
|
|
package/docs/guide/users.md
CHANGED
|
@@ -168,7 +168,7 @@ and `schemaVersion`, with the defaults above. A login is unique **per type**:
|
|
|
168
168
|
the same e-mail may hold a patient user and a staff user. A type name is
|
|
169
169
|
camelCase, and may not be one of `janus()`'s own methods (`authenticate`,
|
|
170
170
|
`signOut`, `signOutEverywhere`, `findUser`, `getUser`, `cookie`,
|
|
171
|
-
`collectExpired`, `types`) — a compile error, and a `TypeError` for a
|
|
171
|
+
`collectExpired`, `types`, `changeEmail`) — a compile error, and a `TypeError` for a
|
|
172
172
|
JavaScript caller.
|
|
173
173
|
|
|
174
174
|
## What each user type answers
|
package/docs/roadmap.md
CHANGED
|
@@ -5,7 +5,7 @@ dates here, and the version something shipped in is the only number.
|
|
|
5
5
|
|
|
6
6
|
## Now
|
|
7
7
|
|
|
8
|
-
Nothing
|
|
8
|
+
Nothing yet.
|
|
9
9
|
|
|
10
10
|
## Next
|
|
11
11
|
|
|
@@ -13,13 +13,6 @@ Nothing yet.
|
|
|
13
13
|
|
|
14
14
|
## Later
|
|
15
15
|
|
|
16
|
-
- **Collecting lapsed one-time tokens** — `collectExpired()` deletes lapsed
|
|
17
|
-
sessions only. On a store with no TTL (PostgreSQL), lapsed tokens stay
|
|
18
|
-
until you delete them, and the sign-in throttle adds one per login tried
|
|
19
|
-
per window, and the mail throttle one per flow per address or user per
|
|
20
|
-
window. A port method to collect them, as sessions are, would let the
|
|
21
|
-
core do it.
|
|
22
|
-
|
|
23
16
|
- **A batch read across types** — `findMany` reads one user type, as `find`
|
|
24
17
|
does. The shared `findUser(id)` has no batch counterpart yet; one on the
|
|
25
18
|
instance, answering a union narrowed by `user.type`, would serve a list that
|
|
@@ -76,6 +69,19 @@ Nothing yet.
|
|
|
76
69
|
The last ten, newest first, each with the version it came in. Everything
|
|
77
70
|
before is in the [CHANGELOG](../CHANGELOG.md).
|
|
78
71
|
|
|
72
|
+
- **Root `changeEmail.confirm` for instances with several user types, v0.26.0** —
|
|
73
|
+
`auth.changeEmail.confirm(token)` confirms a change of e-mail for a page that
|
|
74
|
+
does not know the user's type: it reads the user the link was issued to and
|
|
75
|
+
confirms as their type does. A type's own `confirm` still answers
|
|
76
|
+
`TOKEN_UNKNOWN`, and spends the token, for another type's link; no user type
|
|
77
|
+
may be named `changeEmail`.
|
|
78
|
+
- **Collecting lapsed one-time tokens, v0.25.0** — `collectExpired()` deletes
|
|
79
|
+
lapsed sessions and, through a new optional `TokenStore.deleteExpiredTokens`,
|
|
80
|
+
lapsed one-time tokens too: the sign-in and mail throttles' counts
|
|
81
|
+
included, which a store with no TTL (PostgreSQL) kept for ever, one per
|
|
82
|
+
login tried per window and one per flow per address or user per window.
|
|
83
|
+
The reference store and `@nxgt/janus-drizzle` 0.9 implement it; MongoDB and
|
|
84
|
+
Redis expire tokens themselves and do not.
|
|
79
85
|
- **Finding a user, or creating them, v0.24.0** —
|
|
80
86
|
`auth.<type>.findOrCreate(key, input)` answers `{ user, created }`: the
|
|
81
87
|
holder of the key — the login on a type with a password, the e-mail on one
|
|
@@ -153,15 +159,3 @@ before is in the [CHANGELOG](../CHANGELOG.md).
|
|
|
153
159
|
optional `findUsers`, implemented by the reference store,
|
|
154
160
|
`@nxgt/janus-drizzle` 0.7.0 and `@nxgt/janus-mongo` 0.8.0; a store without it
|
|
155
161
|
is read with `findUser`, ten at a time, so third-party adapters keep working.
|
|
156
|
-
- **Requests that send e-mail are throttled, v0.18.0** —
|
|
157
|
-
`janus({ mail: { throttle: { attempts, window } } })`, five per ten
|
|
158
|
-
minutes by default and `false` to turn it off. `magicLink.request`,
|
|
159
|
-
`signInCode.request` and `resetPassword.request` count per address;
|
|
160
|
-
`verifyEmail.send` and an e-mailed `stepUp.request` count per user; each
|
|
161
|
-
flow on its own. Past the limit a request throws `MailThrottledError`
|
|
162
|
-
(`MAIL_THROTTLED`, 429) with `retryAfter` and issues, spends and rotates
|
|
163
|
-
nothing: the last link or code sent still works, so a loop on an address
|
|
164
|
-
cannot lock its owner out.
|
|
165
|
-
The window is no longer than the shortest default token lifetime. The counts
|
|
166
|
-
live in the tokens store, with no port change. `MailConfig` and
|
|
167
|
-
`MailThrottleConfig` are exported.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -75,7 +75,7 @@ How the messages are shaped:
|
|
|
75
75
|
- [`TOKEN_STALE` — `<call>: the token was sent to an e-mail the user no longer has`](#token_stale--call-the-token-was-sent-to-an-e-mail-the-user-no-longer-has)
|
|
76
76
|
- [`INVALID_CURSOR` — `<call>: this cursor was not minted by this store …`](#invalid_cursor--call-this-cursor-was-not-minted-by-this-store-or-was-minted-for-another-ordering-n-characters)
|
|
77
77
|
- [`<call>: limit must be an integer of at least 1, or absent`](#call-limit-must-be-an-integer-of-at-least-1-or-absent)
|
|
78
|
-
- [`UNSUPPORTED` — `collectExpired: store.sessions does not implement deleteExpiredSessions …`](#unsupported--collectexpired-storesessions-does-not-implement-deleteexpiredsessions--
|
|
78
|
+
- [`UNSUPPORTED` — `collectExpired: store.sessions does not implement deleteExpiredSessions, nor store.tokens deleteExpiredTokens …`](#unsupported--collectexpired-storesessions-does-not-implement-deleteexpiredsessions-nor-storetokens-deleteexpiredtokens--their-stores-expire-records-on-their-own-or-implement-the-methods)
|
|
79
79
|
- [`<call>: the <type> type does not sign in with a password …`](#call-the-type-type-does-not-sign-in-with-a-password--add-password--login--to-it)
|
|
80
80
|
- [`authenticate()` answers `null` although a valid cookie was sent](#authenticate-answers-null-although-a-valid-cookie-was-sent)
|
|
81
81
|
|
|
@@ -297,10 +297,10 @@ janus({ ..., hasher: scryptHasher(), verifiers: [legacyBcrypt] }); // not [scryp
|
|
|
297
297
|
|
|
298
298
|
### `janus: store.<slot> has no method <method>, which the port requires`
|
|
299
299
|
|
|
300
|
-
Also: `janus: store.<slot> is missing`, `janus: store must be an object with users, sessions and tokens`, `janus: store.sessions.deleteExpiredSessions must be a function or absent`, `janus: store.users.findUsers must be a function or absent`.
|
|
300
|
+
Also: `janus: store.<slot> is missing`, `janus: store must be an object with users, sessions and tokens`, `janus: store.sessions.deleteExpiredSessions must be a function or absent`, `janus: store.tokens.deleteExpiredTokens must be a function or absent`, `janus: store.users.findUsers must be a function or absent`.
|
|
301
301
|
|
|
302
302
|
**When:** `janus({...})`, from JavaScript or with a store typed loosely. TypeScript refuses a partial store at compile time and names the method.
|
|
303
|
-
**Why:** `store` is `{ users, sessions, tokens }`, and each slot must answer every method of the port.
|
|
303
|
+
**Why:** `store` is `{ users, sessions, tokens }`, and each slot must answer every method of the port. Three methods are optional — `sessions.deleteExpiredSessions`, `tokens.deleteExpiredTokens` and `users.findUsers` — and each is absent, or a function: a value in its place (`findUsers: true`, a property copied from a config) is a wiring mistake, not an absent capability.
|
|
304
304
|
An adapter written for an earlier `@nxgt/janus` reports the method a later
|
|
305
305
|
release added to the port: `store.tokens has no method countAttempt` for one
|
|
306
306
|
written against 0.3 (the method came in 0.4), and
|
|
@@ -366,8 +366,8 @@ janus({ user: z.object({ username: z.string() }), password: { login: 'username'
|
|
|
366
366
|
Also `janus: "<name>" cannot name a user type — janus() answers a method of that name` and `janus: users declares no user type`.
|
|
367
367
|
|
|
368
368
|
**When:** `janus({ users: {...} })`.
|
|
369
|
-
**Why:** each type becomes a property of what `janus()` answers — `auth.staff` — so it must be a name, and not one of the shared methods (`authenticate`, `signOut`, `signOutEverywhere`, `findUser`, `getUser`, `cookie`, `collectExpired`, `types`).
|
|
370
|
-
**Fix:** `users: { staffMember: {...} }`, not `'staff-member'` or `
|
|
369
|
+
**Why:** each type becomes a property of what `janus()` answers — `auth.staff` — so it must be a name, and not one of the shared methods (`authenticate`, `signOut`, `signOutEverywhere`, `findUser`, `getUser`, `cookie`, `collectExpired`, `types`, `changeEmail`).
|
|
370
|
+
**Fix:** `users: { staffMember: {...} }`, not `'staff-member'`, `cookie` or `changeEmail`.
|
|
371
371
|
|
|
372
372
|
### `janus: session.lifespan: "<value>" is not a duration; write a number followed by ms, s, m, h or d — for example "15m" or "720h"`
|
|
373
373
|
|
|
@@ -949,7 +949,7 @@ janus({ ..., hasher: scryptHasher(), verifiers: [bcryptVerifier] }); // a Passwo
|
|
|
949
949
|
`TokenError`: `<call>: no such token`, `<call>: the token was already used`, `<call>: the token has expired`.
|
|
950
950
|
|
|
951
951
|
**When:** `verifyEmail.confirm`, `resetPassword.confirm`, `changeEmail.confirm`.
|
|
952
|
-
**Why:** a token is spent by its first use, and an expired one is spent too. `TOKEN_UNKNOWN` also covers a token whose user was deleted, and a change link confirmed through another user type's `changeEmail` or passed to another flow. The defaults are 24 h for `verifyEmail`, 1 h for `resetPassword` and for `changeEmail`. A change link is also spent by a newer `changeEmail.send` for the same user (only the last one works), and by an `update` that changes the user's e-mail.
|
|
952
|
+
**Why:** a token is spent by its first use, and an expired one is spent too. `TOKEN_UNKNOWN` also covers a token whose user was deleted, and a change link confirmed through another user type's `changeEmail` (which spends it, so the right type then answers `TOKEN_SPENT`) or passed to another flow. The defaults are 24 h for `verifyEmail`, 1 h for `resetPassword` and for `changeEmail`. A change link is also spent by a newer `changeEmail.send` for the same user (only the last one works), and by an `update` that changes the user's e-mail.
|
|
953
953
|
A reset link answers `resetPassword.confirm: the token was already used` in
|
|
954
954
|
two more cases: a newer `resetPassword.request` for the same user spent it —
|
|
955
955
|
only the last link sent works, so a visitor who asked twice and clicked the
|
|
@@ -958,7 +958,9 @@ sent, by another link's `confirm`, `changePassword` or `setPassword`. When two
|
|
|
958
958
|
requests race, even the last link can be spent: at most one survives,
|
|
959
959
|
sometimes none.
|
|
960
960
|
**Fix:** answer 400 and offer to send a new link — and tell the visitor to use
|
|
961
|
-
the latest e-mail.
|
|
961
|
+
the latest e-mail. With several user types, confirm a change link from a page
|
|
962
|
+
that does not know the type with the root's `auth.changeEmail.confirm(token)`,
|
|
963
|
+
never through a guessed type. To change the lifetimes:
|
|
962
964
|
|
|
963
965
|
```ts
|
|
964
966
|
janus({ ..., tokens: { verifyEmail: '72h', resetPassword: '2h' } });
|
|
@@ -1069,11 +1071,11 @@ const page = await access.list(user, 'view', 'record', {
|
|
|
1069
1071
|
});
|
|
1070
1072
|
```
|
|
1071
1073
|
|
|
1072
|
-
### `UNSUPPORTED` — `collectExpired: store.sessions does not implement deleteExpiredSessions —
|
|
1074
|
+
### `UNSUPPORTED` — `collectExpired: store.sessions does not implement deleteExpiredSessions, nor store.tokens deleteExpiredTokens — their stores expire records on their own, or implement the methods`
|
|
1073
1075
|
|
|
1074
1076
|
**When:** `auth.collectExpired()`.
|
|
1075
|
-
**Why:** the sessions store
|
|
1076
|
-
**Fix:** do not schedule `collectExpired` for
|
|
1077
|
+
**Why:** neither the sessions store implements the optional `deleteExpiredSessions` nor the tokens store the optional `deleteExpiredTokens` — usually because they expire records themselves (a MongoDB TTL index, a Redis `EXPIRE`). With one of the two, `collectExpired` collects that one and does not throw.
|
|
1078
|
+
**Fix:** do not schedule `collectExpired` for those stores. Expiry does not depend on it: the core compares `expiresAt` on every `authenticate` and on every redemption. If your database has no TTL, implement the methods ([adapters](guide/adapters.md#tokenstoredeleteexpiredtokens-optional)).
|
|
1077
1079
|
|
|
1078
1080
|
### `<call>: the <type> type does not sign in with a password — add password: { login } to it`
|
|
1079
1081
|
|
package/package.json
CHANGED
|
File without changes
|