@nxgt/janus 0.1.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -32
- package/dist/auth/context.d.ts +1 -1
- package/dist/auth/context.d.ts.map +1 -1
- package/dist/auth/port/types.d.ts +9 -5
- 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/chunks/{index-658b6mr2.js → index-0xarpm7z.js} +3 -3
- package/dist/chunks/index-0xarpm7z.js.map +11 -0
- package/dist/chunks/index-6p56fpbe.js.map +2 -2
- package/dist/conformance/cases/sessions.d.ts.map +1 -1
- package/dist/conformance/cases/users.d.ts.map +1 -1
- package/dist/conformance/index.js +27 -4
- package/dist/conformance/index.js.map +4 -4
- package/dist/errors/janus-error.d.ts +2 -1
- package/dist/errors/janus-error.d.ts.map +1 -1
- package/dist/index.js +53 -11
- package/dist/index.js.map +7 -6
- package/dist/permissions/engine.d.ts +1 -1
- package/dist/permissions/index.js +17 -13
- package/dist/permissions/index.js.map +5 -5
- package/dist/permissions/model.d.ts +28 -28
- package/dist/permissions/model.d.ts.map +1 -1
- package/dist/permissions/resolve.d.ts.map +1 -1
- package/dist/stores/storable.d.ts +15 -0
- package/dist/stores/storable.d.ts.map +1 -0
- package/docs/guide/adapters.md +9 -4
- package/docs/guide/errors.md +3 -2
- package/docs/guide/permissions.md +287 -109
- package/docs/guide/sessions.md +3 -1
- package/docs/guide/users.md +6 -2
- package/docs/guide/vocabulary.md +23 -19
- package/docs/roadmap.md +62 -9
- package/docs/troubleshooting.md +36 -18
- package/package.json +1 -1
- package/dist/chunks/index-658b6mr2.js.map +0 -11
package/docs/guide/vocabulary.md
CHANGED
|
@@ -12,9 +12,9 @@ const user = { type: 'staff', id: mintId(), username: 'grace' };
|
|
|
12
12
|
subjectOf(user); // { type: 'staff', id: '…' }: the user IS the subject
|
|
13
13
|
formatTuple({
|
|
14
14
|
object: { type: 'record', id: 'r1' },
|
|
15
|
-
relation: '
|
|
16
|
-
subject: { type: 'team', id: 't1'
|
|
17
|
-
}); // 'record:r1#
|
|
15
|
+
relation: 'teams',
|
|
16
|
+
subject: { type: 'team', id: 't1' },
|
|
17
|
+
}); // 'record:r1#teams@team:t1'
|
|
18
18
|
parseDuration('8h', 'session.lifespan'); // 28800000
|
|
19
19
|
```
|
|
20
20
|
|
|
@@ -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
|
|
|
@@ -59,14 +60,16 @@ either finds this row.
|
|
|
59
60
|
| **object** | What a permission is about: `{ type: 'document', id }` | "resource" |
|
|
60
61
|
| **subject** | Who a permission is about: a user, an object, or a subject set | "principal", "actor" |
|
|
61
62
|
| **entity** | `{ type, id }`: a user or an object — a subject that is not a set (`Entity`, `deleteEntity`) | |
|
|
62
|
-
| **subject set** | Everyone holding one relation on one object: `team:t1#
|
|
63
|
-
| **relation** | A named link, stored as tuples or read from a field (`fromField`) | |
|
|
64
|
-
|
|
|
65
|
-
| **
|
|
63
|
+
| **subject set** | Everyone holding one relation on one object: `team:t1#members` | "group" — a group is an object with a `members` relation |
|
|
64
|
+
| **relation** | A named link, stored as tuples or read from a field (`fromField`). Declared under `related`, named in the plural: `members`, `doctors` | |
|
|
65
|
+
| **`related`** | The key of an object type that declares its relations: `related: { members: ['staff'] }`. It was `relations` before 0.2, and the old key is refused | "relations" as a key — the word stays for the idea, and for `janus({ relations })` |
|
|
66
|
+
| **holder** | What a relation admits: `'staff'`, or the subject set `'team#members'` | |
|
|
67
|
+
| **permission** | A name computed from relations and other permissions by its rules. Declared under `permits` | |
|
|
68
|
+
| **`permits`** | The key of an object type that declares its permissions, each a list of rules: `permits: { view: ['members'] }`. It was `permissions` before 0.2, and the old key is refused | "permissions" as a key — the word stays for the idea, and for `permissions()` |
|
|
66
69
|
| **rule** | One entry of a permission: a relation, another permission, an arrow, or one of them under a condition | |
|
|
67
|
-
| **arrow** | A rule that follows a relation to another object's permission: `'
|
|
70
|
+
| **arrow** | A rule that follows a relation to another object's permission or relation: `'teams->view'` | |
|
|
68
71
|
| **condition** | A predicate on a rule, written with `when`, run on the `ctx` passed to `can()` and `list()` | |
|
|
69
|
-
| **tuple** | One stored fact — object, relation, subject: `
|
|
72
|
+
| **tuple** | One stored fact — object, relation, subject: `team:t1#members@staff:u1` | "grant", "ACL entry" |
|
|
70
73
|
| **guarded route** | A route that runs only when its subject holds a permission on the object it serves — `permission()` in `@nxgt/janus-hono` | |
|
|
71
74
|
|
|
72
75
|
### Stores
|
|
@@ -76,7 +79,8 @@ either finds this row.
|
|
|
76
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" |
|
|
77
80
|
| **port** | The interface a store implements: `JanusStores` for the identity stores — named after the package, not the side — and `RelationStore` | "driver" |
|
|
78
81
|
| **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" |
|
|
79
|
-
| **integration** | A package fitting Janus into one web framework: `@nxgt/janus-hono`. It implements no port | "plugin", "adapter" |
|
|
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" |
|
|
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" |
|
|
80
84
|
|
|
81
85
|
### Answers
|
|
82
86
|
|
|
@@ -100,7 +104,7 @@ interface RelationTuple { readonly object: Entity; readonly relation: string; re
|
|
|
100
104
|
```
|
|
101
105
|
|
|
102
106
|
**Subjects are typed**: `{ type, id }` for one entity, `{ type, id, relation }`
|
|
103
|
-
for a subject set —
|
|
107
|
+
for a subject set — everyone holding `members` on `team:t1`. `type` is the same word as a
|
|
104
108
|
user's own, so a user id and a subject id are the same thing, and
|
|
105
109
|
`subjectOf(user)` is the one-line join between the two sides of the package.
|
|
106
110
|
It copies `type` and `id` only, so none of the user's own fields ever reaches
|
|
@@ -111,25 +115,25 @@ a tuple.
|
|
|
111
115
|
| `subjectOf(user)` | `{ type, id }` of any object carrying both |
|
|
112
116
|
| `isSubjectSet(subject)` | whether `relation` is a string |
|
|
113
117
|
| `formatEntity(entity)` | `'record:r1'` |
|
|
114
|
-
| `formatSubject(subject)` | `'staff:u1'`, or `'team:t1#
|
|
115
|
-
| `formatTuple(tuple)` | `'
|
|
118
|
+
| `formatSubject(subject)` | `'staff:u1'`, or `'team:t1#members'` |
|
|
119
|
+
| `formatTuple(tuple)` | `'team:t1#members@team:t2#members'` |
|
|
116
120
|
| `parseSubject(text)` | the `Subject` back |
|
|
117
121
|
| `parseTuple(text)` | the `RelationTuple` back |
|
|
118
122
|
|
|
119
123
|
```ts
|
|
120
124
|
import { isSubjectSet, parseSubject, parseTuple } from '@nxgt/janus';
|
|
121
125
|
|
|
122
|
-
parseTuple('
|
|
123
|
-
// { object: { type: '
|
|
126
|
+
parseTuple('team:t1#members@staff:u1');
|
|
127
|
+
// { object: { type: 'team', id: 't1' }, relation: 'members', subject: { type: 'staff', id: 'u1' } }
|
|
124
128
|
|
|
125
|
-
const subject = parseSubject('team:t1#
|
|
126
|
-
if (isSubjectSet(subject)) subject.relation; // '
|
|
129
|
+
const subject = parseSubject('team:t1#members');
|
|
130
|
+
if (isSubjectSet(subject)) subject.relation; // 'members'
|
|
127
131
|
```
|
|
128
132
|
|
|
129
133
|
The notation is for messages, logs and tests, not a wire format. No part may
|
|
130
134
|
hold `@`, `#` or a parenthesis, and a type may not hold a `:`, so every string
|
|
131
135
|
reads one way. `parseTuple` and `parseSubject` refuse anything else with a
|
|
132
|
-
**bare `TypeError`** — including Keto's untyped subject, `
|
|
136
|
+
**bare `TypeError`** — including Keto's untyped subject, `team:t1#members@grace`,
|
|
133
137
|
and the message says what a subject is. Nothing in this package reads a tuple
|
|
134
138
|
off the network, so a malformed string came from your own code.
|
|
135
139
|
|
package/docs/roadmap.md
CHANGED
|
@@ -5,24 +5,55 @@ dates here, and the version something shipped in is the only number.
|
|
|
5
5
|
|
|
6
6
|
## Now
|
|
7
7
|
|
|
8
|
-
- **A
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
- **A PostgreSQL adapter** — in a package of its own, `@nxgt/janus-drizzle`,
|
|
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, a session token
|
|
14
|
+
or a one-time token. Built, not yet published.
|
|
15
|
+
- **A Redis adapter for sessions and one-time tokens** — in a package of its
|
|
16
|
+
own, `@nxgt/janus-redis`: both are read on every request and ephemeral, so
|
|
17
|
+
they live in Redis, expired by Redis itself, while users stay in another
|
|
18
|
+
store. Built, not yet published.
|
|
12
19
|
|
|
13
20
|
## Next
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
- **One-time codes** — a one-time token short enough to type, sent by e-mail
|
|
23
|
+
to sign in without a password, or to confirm a sensitive action, issued
|
|
24
|
+
and redeemed by `janus` like the verification and reset tokens today; and
|
|
25
|
+
TOTP, the one-time codes of an authenticator app, as a second factor.
|
|
26
|
+
- **Sending the e-mails** — in a package of its own, `@nxgt/janus-mail`, built
|
|
27
|
+
on a general mail toolkit shared with applications that are not about
|
|
28
|
+
sign-in: a `Mailer` port you plug your transport into (SMTP, Resend, SES…) —
|
|
29
|
+
a transport that fails throws, like a store — and default templates for
|
|
30
|
+
verification, password reset and one-time codes, in English and French. One
|
|
31
|
+
template per e-mail, never one HTML file per language: the layout is built
|
|
32
|
+
once with Maizzle and Tailwind CSS 4 — CSS inlined for mail clients — and
|
|
33
|
+
its text lives in ICU message catalogues, one per language, plurals and
|
|
34
|
+
dates included. Both are compiled when the package is built into typed
|
|
35
|
+
functions: `templates.verifyEmail({ locale: 'fr', link })` answers
|
|
36
|
+
`{ subject, html, text }`, every value escaped, a missing variable or an
|
|
37
|
+
unknown locale a compile error, a message that fails to format a throw —
|
|
38
|
+
never an e-mail sent with `{link}` in it. No template engine at run time.
|
|
39
|
+
The defaults are a starting point, not a requirement: add a language with a
|
|
40
|
+
catalogue, or replace any one template with your own function of the same
|
|
41
|
+
shape — built with the same toolkit, React Email or a plain string — and
|
|
42
|
+
keep the defaults for the rest.
|
|
43
|
+
- **Webhooks** — signed HTTP events when something happens to a user
|
|
44
|
+
(created, e-mail verified, password reset, deleted), so another service can
|
|
45
|
+
follow without polling: a signature it can check, retries on failure, and
|
|
46
|
+
the same rule as the audit trail — the user named by id, never a login, a
|
|
47
|
+
password, a session token or a one-time token in a payload, and every key
|
|
48
|
+
camelCase. Retries that run out are reported, never dropped in silence.
|
|
16
49
|
|
|
17
50
|
## Later
|
|
18
51
|
|
|
19
52
|
- **More official adapters** — the ports are cut where atomicity is not
|
|
20
53
|
required, so users, sessions and permission tuples can each live in the
|
|
21
54
|
database that suits them. MongoDB is the first adapter
|
|
22
|
-
([`@nxgt/janus-mongo`](https://www.npmjs.com/package/@nxgt/janus-mongo))
|
|
23
|
-
|
|
24
|
-
every request, which is why the port gives them slots of their own: they can
|
|
25
|
-
live in Redis, with a native expiry, while users stay in another store.
|
|
55
|
+
([`@nxgt/janus-mongo`](https://www.npmjs.com/package/@nxgt/janus-mongo));
|
|
56
|
+
PostgreSQL is under Now.
|
|
26
57
|
|
|
27
58
|
## Not planned
|
|
28
59
|
|
|
@@ -63,6 +94,28 @@ Nothing yet.
|
|
|
63
94
|
|
|
64
95
|
The first public release, v0.1.
|
|
65
96
|
|
|
97
|
+
- **The model's keys read `related` and `permits`** — Keto's OPL words: an
|
|
98
|
+
object type declares `related: { members: ['staff', 'team#members'] }` and
|
|
99
|
+
`permits: { view: ['members'] }`, and relation names are plural by convention. Breaking:
|
|
100
|
+
`relations` and `permissions` as keys are refused, by the compiler and by
|
|
101
|
+
`defineModel`, with a message naming the new key —
|
|
102
|
+
`types.team.relations is now related: rename the key`. The `permissions()`
|
|
103
|
+
function and `janus({ relations })` keep their names. — next minor
|
|
104
|
+
- **`LOGIN_TAKEN` no longer quotes the login in its message**, in the memory
|
|
105
|
+
store and in both adapters; `error.login` still names it, and the
|
|
106
|
+
conformance suite checks it. — next patch
|
|
107
|
+
- **The conformance suite accepts a store with its own expiry** — a store
|
|
108
|
+
that drops a lapsed session at once, as a Redis TTL does, passes
|
|
109
|
+
`sessions.deleteUser`; one that still holds it must count it. — next patch
|
|
110
|
+
- **A NUL character or a lone surrogate never reaches a store** — refused in
|
|
111
|
+
fields with `USER_INVALID` on every adapter, rather than `STORE_FAILED` on
|
|
112
|
+
PostgreSQL alone; a login holding one is nobody's. The conformance suite
|
|
113
|
+
holds every adapter to round-tripping every other character. — next patch
|
|
114
|
+
|
|
115
|
+
- **A Hono integration** — [`@nxgt/janus-hono`](https://www.npmjs.com/package/@nxgt/janus-hono):
|
|
116
|
+
the session middleware, the cookie, a route guarded by a permission,
|
|
117
|
+
`bindJanus()` to bind the instances once, and every error as its status.
|
|
118
|
+
Its own 0.1.0, beside `@nxgt/janus` 0.1.3.
|
|
66
119
|
- **`defineModel` completed by your editor** — subject types and subject sets
|
|
67
120
|
in a relation, subject types in `fromField`, relations, permissions and
|
|
68
121
|
arrows in a rule and in `when`; a wrong name's error lists the names it
|
package/docs/troubleshooting.md
CHANGED
|
@@ -42,7 +42,7 @@ How the messages are shaped:
|
|
|
42
42
|
- [`STORE_FAILED` — `<slot>.<method>: the store could not answer`](#store_failed--slotmethod-the-store-could-not-answer)
|
|
43
43
|
- [`STORE_FAILED` — `<slot>.<method> answered undefined …`](#store_failed--slotmethod-answered-undefined-an-absence-is-null-so-this-store-forgot-to-answer)
|
|
44
44
|
- [`NOT_FOUND` — `<call>: no <type> has this id`](#not_found--call-no-type-has-this-id)
|
|
45
|
-
- [`LOGIN_TAKEN` — `<call>: the login
|
|
45
|
+
- [`LOGIN_TAKEN` — `<call>: the login is taken by another <type>`](#login_taken--call-the-login-is-taken-by-another-type)
|
|
46
46
|
- [`VERSION_CONFLICT` — `<call>: expected version <n>, found <m>`](#version_conflict--call-expected-version-n-found-m)
|
|
47
47
|
- [`USER_INVALID` — `<call>: the fields do not match the <type> schema …`](#user_invalid--call-the-fields-do-not-match-the-type-schema-n-issues-at-paths)
|
|
48
48
|
- [`PASSWORD_TOO_SHORT` — `<call>: the password is shorter than the policy's <n> characters`](#password_too_short--call-the-password-is-shorter-than-the-policys-n-characters)
|
|
@@ -328,9 +328,9 @@ try {
|
|
|
328
328
|
const user = await auth.find(id); // null when there is nobody
|
|
329
329
|
```
|
|
330
330
|
|
|
331
|
-
### `LOGIN_TAKEN` — `<call>: the login
|
|
331
|
+
### `LOGIN_TAKEN` — `<call>: the login is taken by another <type>`
|
|
332
332
|
|
|
333
|
-
`StoreConflict` with `on: 'login'`, carrying `login` and `userType`.
|
|
333
|
+
`StoreConflict` with `on: 'login'`, carrying `login` and `userType`. The login is not in the message, which never carries a value: an e-mail in a log line is personal data. Read it from `error.login`.
|
|
334
334
|
|
|
335
335
|
**When:** `signUp`, `create`, or an `update` that changes the login, when another user of **the same type** holds it after normalisation (`Ada@Example.com` and `ada@example.com` collide by default).
|
|
336
336
|
**Why:** the store's unique constraint refused the write. A login is unique per user type: one e-mail may hold a patient user and a staff user.
|
|
@@ -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
|
|
@@ -514,7 +514,7 @@ Also `can: <type>.<relation> reads <field>, which the object holds as something
|
|
|
514
514
|
**Fix:** pass the loaded object, spread:
|
|
515
515
|
|
|
516
516
|
```ts
|
|
517
|
-
await access.can(
|
|
517
|
+
await access.can(grace, 'view', { type: 'record', ...record });
|
|
518
518
|
```
|
|
519
519
|
|
|
520
520
|
### `can: <type>.<name> reaches a condition, and no ctx was passed — pass { ctx }`
|
|
@@ -526,7 +526,7 @@ Also `list: <type>.<name> reaches a condition, and no ctx was passed — pass {
|
|
|
526
526
|
**Fix:**
|
|
527
527
|
|
|
528
528
|
```ts
|
|
529
|
-
await access.can(
|
|
529
|
+
await access.can(grace, 'edit', { type: 'record', ...record }, { ctx: { onShift: true } });
|
|
530
530
|
```
|
|
531
531
|
|
|
532
532
|
### `list: <type>.<relation> is read from a field, and has no lookup to find the <type>s naming <id> — …`
|
|
@@ -536,7 +536,7 @@ await access.can(staff, 'edit', { type: 'record', ...record }, { ctx: { onShift
|
|
|
536
536
|
**Fix:**
|
|
537
537
|
|
|
538
538
|
```ts
|
|
539
|
-
fromField('doctorId', 'staff', { lookup: (
|
|
539
|
+
doctors: fromField('doctorId', 'staff', { lookup: (staffId) => db.records.idsByDoctor(staffId) }),
|
|
540
540
|
```
|
|
541
541
|
|
|
542
542
|
A `lookup` is your code and is not guarded: if it throws, `list()` rejects with that error. Never answer `[]` for a database that could not answer — that is an outage turned into a denial.
|
|
@@ -560,8 +560,8 @@ Also with `revoke:`.
|
|
|
560
560
|
Also with `revoke:`.
|
|
561
561
|
|
|
562
562
|
**When:** `access.grant(object, relation, subject)`.
|
|
563
|
-
**Why:** the model's relation does not admit that kind of subject: `
|
|
564
|
-
**Fix:** grant a subject the relation admits, or add the holder to the model: `
|
|
563
|
+
**Why:** the model's relation does not admit that kind of subject: `members: ['staff']` refuses a `patient`, and refuses the subject set `team#members` unless it is listed.
|
|
564
|
+
**Fix:** grant a subject the relation admits, or add the holder to the model: `members: ['staff', 'team#members']`.
|
|
565
565
|
With `revoke:` on a tuple stored before the model stopped admitting it, the tuple already grants nothing; remove it through the store: `relations.write({ remove: [tuple] })`.
|
|
566
566
|
|
|
567
567
|
### Other `can:`, `list:`, `grant:` and `revoke:` messages
|
|
@@ -575,7 +575,7 @@ Each is a `TypeError` naming the call. TypeScript refuses most of them on the ar
|
|
|
575
575
|
| `<call>: the object must be { type, id, …its fields }` | `{ type: 'record', ...record }`. |
|
|
576
576
|
| `<call>: the subject must be a user, or { type, id }` | Pass the user from `janus()`, or `{ type, id }`. `null` is anonymous and answers `false`. |
|
|
577
577
|
| `<call>: the object id must be a non-empty string without @, # or parentheses` | Also for `the subject id`. Those characters belong to the tuple notation. |
|
|
578
|
-
| `<call>: "<relation>" is not a relation of <type>, so <type>:<id>#<relation> is no subject set` | A subject set names a relation of its type: `{ type: 'team', id, relation: '
|
|
578
|
+
| `<call>: "<relation>" is not a relation of <type>, so <type>:<id>#<relation> is no subject set` | A subject set names a relation of its type: `{ type: 'team', id, relation: 'members' }`. |
|
|
579
579
|
| `grant: "<relation>" is not a relation of <type>` | Grant a relation, never a permission. |
|
|
580
580
|
| `list: the type must be an object type of the model` | The third argument is a type name: `'record'`. |
|
|
581
581
|
| `list: after must be the nextCursor of a page, or null` | Pass `nextCursor` back as it came. |
|
|
@@ -587,15 +587,33 @@ most common:
|
|
|
587
587
|
|
|
588
588
|
| Message | Fix |
|
|
589
589
|
| --- | --- |
|
|
590
|
+
| `defineModel: types.<type>.relations is now related: rename the key` | The key before 0.2. Rename it, nothing else: `related: { members: ['staff'] }`. The compiler refuses it first: `team.relations is now related: rename the key`. |
|
|
591
|
+
| `defineModel: types.<type>.permissions is now permits: rename the key` | The same for the permissions: `permits: { view: ['members'] }`. The compiler refuses it first: `team.permissions is now permits: rename the key`. |
|
|
592
|
+
| `defineModel: types.<type>.<key> is not a key of an object type: related or permits` | An object type has two keys, `related` and `permits` — `permit:`, singular, is a typo. |
|
|
590
593
|
| `defineModel: subjects must be an array of subject type names — auth.types from janus(), or your own` | `defineModel({ subjects: auth.types, types })` with `janus()`, or your own names alone: `subjects: ['user']`. |
|
|
591
594
|
| `defineModel: types declares no object type` | Declare at least one type under `types`. |
|
|
592
|
-
| `defineModel:
|
|
593
|
-
| `defineModel: "<name>"
|
|
595
|
+
| `defineModel: types.<type> must be an object` | Also `types.<type>.related must be an object` and `types.<type>.permits must be an object`: each is keyed by name — `related: { members: ['staff'] }`. |
|
|
596
|
+
| `defineModel: the object type "<name>" must be a camelCase name — letters and digits, starting with a lowercase letter` | Also `types.<type>.related: "<name>" must be a camelCase name — …` for a relation, and `types.<type>.permits: …` for a permission. |
|
|
597
|
+
| `defineModel: "<name>" names a user type and an object type; a subject of type "<name>" would be ambiguous` | Rename the object type. For permissions on a user, see [the guide](guide/permissions.md#permissions-on-a-user). |
|
|
594
598
|
| `defineModel: types.<type>: "<name>" names a relation and a permission; rename one` | One name, one meaning. |
|
|
595
|
-
| `defineModel: types.<type>.
|
|
596
|
-
| `defineModel:
|
|
599
|
+
| `defineModel: types.<type>.related.<relation> must be a non-empty array of subject types, or fromField()` | `members: ['staff']`, or `doctors: fromField('doctorId', 'staff')`. |
|
|
600
|
+
| `defineModel: types.<type>.related.<relation>: "<holder>" is not a subject type` | Also `"<holder>" is not a subject set — it must name an object type and one of its relations`: `'team#members'`, a declared type and one of its relations. |
|
|
601
|
+
| `defineModel: types.<type>.permits.<permission> must be a non-empty array of rules` | `view: ['members']`. |
|
|
602
|
+
| `defineModel: pass { subjects, types }` | From JavaScript: `defineModel` was given something other than an object. |
|
|
603
|
+
| `defineModel: the subject type "<name>" must be a camelCase name` | A user type from `janus()` is always one; check a list of your own. |
|
|
604
|
+
| `defineModel: types.<type>.related.<relation>: fromField names "<name>", which is not a subject type` | `fromField('doctorId', 'staff')`: the second argument is a user type or an object type of the model. |
|
|
605
|
+
| `defineModel: types.<type>.related.<relation>: fromField must name a top-level field` | `fromField('doctorId', …)`, never `'doctor.id'`: the field is read from the object passed to `can()`. |
|
|
606
|
+
| `defineModel: types.<type>.related.<relation>: fromField's lookup must be a function` | `{ lookup: (staffId) => db.records.idsByDoctor(staffId) }`. |
|
|
607
|
+
| `defineModel: types.<type>.related.<relation>: a holder must be a string` | `members: ['staff', 'team#members']`. |
|
|
608
|
+
| `defineModel: types.<type>.permits.<permission>[<i>]: a rule is a name, an arrow, or when()` | `view: ['doctors', 'teams->view', when('doctors', test)]`. |
|
|
609
|
+
| `defineModel: types.<type>.permits.<permission>[<i>]: when() takes a function as its test` | `when('doctors', (ctx: { onShift: boolean }) => ctx.onShift)`. |
|
|
610
|
+
| `defineModel: types.<type>.permits.<permission>[<i>]: "<rule>" is not a relation or a permission of <type>` | Name a relation or a permission the type declares — `'members'`, `'manage'`. |
|
|
611
|
+
| `defineModel: types.<type>.permits: <a> → <b> → <a> is a loop no relation ends` | A permission must cross a relation before it reaches itself again. |
|
|
612
|
+
| `defineModel: … "<rule>" goes through "<relation>", which is not a relation of <type>` | An arrow starts from a relation of the same type: `'teams->view'` needs `teams` under `related`. |
|
|
613
|
+
| `defineModel: … "<rule>" goes through "<relation>", which can hold a subject set; an arrow follows object types only` — also `which can hold a <user type>` | An arrow's relation must hold object types: `teams: ['team']`. A user has no permissions to follow. The compiler refuses it first. |
|
|
597
614
|
| `defineModel: … "<rule>" names "<target>", which <type> does not declare` | Arrow to a relation or permission of the target type. |
|
|
598
615
|
| `defineModel: … reads <type>.<field>, and a subject set reaches <type>s nobody passed to can() — store that relation instead of reading it` | Only the object passed to `can()` carries data: a `fromField` there cannot be reached through a subject set or an arrow. Store it as a tuple. |
|
|
616
|
+
| `defineModel: types.<type>.permits.<permission>: "<relation>-><target>" reaches <type>.<target>, which reads <type>.<field>, and only the object passed to can() carries its data — store that relation instead of reading it` | The same through an arrow: `'teams->leads'` where the team's `leads` is a `fromField`. Store it as a tuple. |
|
|
599
617
|
|
|
600
618
|
---
|
|
601
619
|
|
|
@@ -606,14 +624,14 @@ most common:
|
|
|
606
624
|
Also `parseSubject: "<text>" is not a subject; expected type:id, or type:id#relation for a subject set`.
|
|
607
625
|
|
|
608
626
|
**When:** `parseTuple(...)` or `parseSubject(...)`, a `TypeError`.
|
|
609
|
-
**Why:** subjects are typed. `
|
|
627
|
+
**Why:** subjects are typed. `team:t1#members@grace` — Keto's untyped subject — is refused. No part may hold `@`, `#` or a parenthesis, and a type may not hold `:`.
|
|
610
628
|
**Fix:**
|
|
611
629
|
|
|
612
630
|
```ts
|
|
613
631
|
import { parseTuple } from '@nxgt/janus';
|
|
614
632
|
|
|
615
|
-
parseTuple('record:r1#
|
|
616
|
-
parseTuple('
|
|
633
|
+
parseTuple('record:r1#teams@team:t1');
|
|
634
|
+
parseTuple('team:t1#members@team:t2#members');
|
|
617
635
|
```
|
|
618
636
|
|
|
619
637
|
---
|
package/package.json
CHANGED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/ids/id.ts", "../src/auth/port/memory.ts"],
|
|
4
|
-
"sourcesContent": [
|
|
5
|
-
"/**\n * An id — of a user, a session: a UUIDv7, lowercase, hyphenated.\n *\n * **The core mints it, not the store**, and that decision pays for itself three\n * times over:\n *\n * - A UUIDv7 leads with a 48-bit millisecond timestamp, so ids sort in\n * creation order as strings. The pagination cursor is therefore *the last\n * id* — one index on the id, and the ordering is already total. A\n * store-minted id would need a `createdAt` index plus the id as a tiebreak,\n * because a timestamp alone is not a total order, and an opaque cursor per\n * adapter.\n * - `insertUser` becomes **idempotent under retry**: the id is decided\n * before the call, so a retry after a timeout writes the same row rather\n * than a second user.\n * - Every adapter reports the same shape, so moving an application from one\n * adapter to another is a copy rather than a rewrite of every stored\n * reference.\n *\n * The price, stated plainly: an adapter cannot reuse an existing numeric primary\n * key. It gets a `uuid` column, or a 36-character string, and the monotonic\n * prefix gives it the index locality a UUIDv4 destroys.\n */\nexport type Id = string;\n\nconst HEX: readonly string[] = Array.from({ length: 256 }, (_, i) =>\n\ti.toString(16).padStart(2, '0'),\n);\n\nlet lastMs = -1;\nlet sequence = 0;\n\n/**\n * The 12-bit sequence UUIDv7 keeps for ordering inside one millisecond.\n *\n * RFC 9562 calls this the \"replace leftmost random bits with an increased clock\n * precision\" method. Without it, two ids minted in the same millisecond order\n * by their random tail — still a total order, which is all the port requires,\n * but not creation order, and \"newest first\" would then be wrong for anything\n * created in a burst. A signup storm is exactly such a burst.\n */\nconst SEQUENCE_MAX = 0xfff;\n\n/**\n * A fresh id.\n *\n * Strictly increasing: within one millisecond it uses the sequence, and if that\n * overflows — more than 4096 ids in a millisecond, which no real\n * application reaches — it borrows the next millisecond rather than repeating\n * one. Across processes the 62 random bits of the tail are what keep two machines\n * apart.\n */\nexport function mintId(now: number = Date.now()): Id {\n\tlet ms = now;\n\n\tif (ms === lastMs) {\n\t\tsequence += 1;\n\t\tif (sequence > SEQUENCE_MAX) {\n\t\t\t// Borrow from the next millisecond. This keeps ids strictly\n\t\t\t// increasing at the cost of a timestamp a hair ahead of the clock,\n\t\t\t// which is the right trade: a repeated id would break the cursor.\n\t\t\tms = lastMs + 1;\n\t\t\tsequence = 0;\n\t\t}\n\t} else if (ms > lastMs) {\n\t\tsequence = 0;\n\t} else {\n\t\t// The clock went backwards — an NTP step, or a container resumed. Hold\n\t\t// the last millisecond and keep counting, so ids stay ordered even\n\t\t// though they stop matching the wall clock for a moment.\n\t\tms = lastMs;\n\t\tsequence += 1;\n\t\tif (sequence > SEQUENCE_MAX) {\n\t\t\tms = lastMs + 1;\n\t\t\tsequence = 0;\n\t\t}\n\t}\n\n\tlastMs = ms;\n\n\tconst bytes = new Uint8Array(16);\n\tcrypto.getRandomValues(bytes.subarray(8));\n\n\tbytes[0] = (ms / 2 ** 40) & 0xff;\n\tbytes[1] = (ms / 2 ** 32) & 0xff;\n\tbytes[2] = (ms / 2 ** 24) & 0xff;\n\tbytes[3] = (ms / 2 ** 16) & 0xff;\n\tbytes[4] = (ms / 2 ** 8) & 0xff;\n\tbytes[5] = ms & 0xff;\n\t// Version 7 in the high nibble, the sequence's top 4 bits below it.\n\tbytes[6] = 0x70 | ((sequence >> 8) & 0x0f);\n\tbytes[7] = sequence & 0xff;\n\t// Variant 10 in the two high bits of byte 8; the rest of it stays random.\n\tbytes[8] = 0x80 | ((bytes[8] as number) & 0x3f);\n\n\treturn `${HEX[bytes[0] as number]}${HEX[bytes[1] as number]}${HEX[bytes[2] as number]}${HEX[bytes[3] as number]}-${HEX[bytes[4] as number]}${HEX[bytes[5] as number]}-${HEX[bytes[6] as number]}${HEX[bytes[7] as number]}-${HEX[bytes[8] as number]}${HEX[bytes[9] as number]}-${HEX[bytes[10] as number]}${HEX[bytes[11] as number]}${HEX[bytes[12] as number]}${HEX[bytes[13] as number]}${HEX[bytes[14] as number]}${HEX[bytes[15] as number]}`;\n}\n\nconst UUID_V7 =\n\t/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;\n\n/**\n * Whether this string is an id this package could have minted.\n *\n * Used by an adapter to answer `null` for a malformed id **without reaching the\n * store**: an id arrives off a URL, and one of the wrong shape is \"no such\n * user\", not an outage and not a query. Rejecting it here also keeps a\n * hand-written id out of a store that would then hold something the cursor\n * cannot order.\n */\nexport function isId(value: string): boolean {\n\treturn UUID_V7.test(value);\n}\n\n/**\n * When this id was minted, from the id itself.\n *\n * Offered because the timestamp is *in* the id, so a `createdAt` column is a\n * convenience rather than the truth — and an adapter that loses one can still\n * answer. It is not a substitute for `createdAt`: the sequence may have borrowed\n * a millisecond, and a clock that stepped backwards is held rather than\n * followed, so this is accurate to the millisecond and no further.\n */\nexport function mintedAt(id: Id): Date {\n\tconst hex = id.replace(/-/g, '').slice(0, 12);\n\treturn new Date(Number.parseInt(hex, 16));\n}\n",
|
|
6
|
-
"import { NotFoundError, StoreConflict } from '../../errors/janus-error';\nimport type { Id } from '../../ids/id';\nimport type {\n\tJanusStores,\n\tSessionId,\n\tSessionRecord,\n\tSessionStore,\n\tTokenRecord,\n\tTokenStore,\n\tUserPatch,\n\tUserRecord,\n\tUserStore,\n} from './types';\n\n/**\n * The reference implementation of the store port, in memory.\n *\n * **Shipped and documented, not a test helper.** It is what a consumer uses in\n * their own unit tests, and what an adapter author compares against when a\n * conformance case they do not understand turns red. So it keeps the six rules\n * for real rather than approximately:\n *\n * - uniqueness is a constraint — an index checked and written inside the same\n * synchronous step, which is what atomic means on one event loop;\n * - `consumeToken` reads and writes with no `await` between the two, so twenty\n * concurrent calls see exactly one unspent token;\n * - every record is **copied in and copied out**, so a caller that mutates what\n * it passed or what it got back cannot reach the store — the in-memory\n * version of \"bytes round-trip\";\n * - a patch applies only the fields it names, and only the fields the port\n * declares: a stray `version` or `id` in a patch from JavaScript is ignored,\n * not written.\n *\n * Every method is `async` even though none waits on anything: a caller that\n * forgot an `await` must fail here the way it would against a real database.\n */\nexport function createMemoryStores(): JanusStores {\n\treturn {\n\t\tusers: memoryUserStore(),\n\t\tsessions: memorySessionStore(),\n\t\ttokens: memoryTokenStore(),\n\t};\n}\n\nconst copy = <T>(value: T): T => structuredClone(value);\n\n/** The unique key of one login. `\\u0000` cannot occur in a type name the core accepts. */\nconst keyOf = (type: string, login: string): string => `${type}\\u0000${login}`;\n\nfunction memoryUserStore(): UserStore {\n\tconst byId = new Map<Id, UserRecord>();\n\t// The unique index: (type, login) key → the id holding it.\n\tconst byLogin = new Map<string, Id>();\n\n\t/** The first of `logins` held by a user of `type` other than `id`. */\n\tconst takenBy = (\n\t\ttype: string,\n\t\tlogins: readonly string[],\n\t\tid: Id,\n\t): string | undefined =>\n\t\tlogins.find((login) => {\n\t\t\tconst holder = byLogin.get(keyOf(type, login));\n\t\t\treturn holder !== undefined && holder !== id;\n\t\t});\n\n\tconst taken = (operation: string, type: string, login: string) =>\n\t\tnew StoreConflict(\n\t\t\t'login',\n\t\t\t`${operation}: the login \"${login}\" is taken by another ${type}`,\n\t\t\t{ login, userType: type, operation },\n\t\t);\n\n\treturn {\n\t\tasync insertUser(record) {\n\t\t\tconst stored = byId.get(record.id);\n\t\t\tif (stored !== undefined) return copy(stored);\n\n\t\t\tconst collision = takenBy(record.type, record.logins, record.id);\n\t\t\tif (collision !== undefined) {\n\t\t\t\tthrow taken('insertUser', record.type, collision);\n\t\t\t}\n\n\t\t\tconst written = copy(record);\n\t\t\tbyId.set(written.id, written);\n\t\t\tfor (const login of written.logins) {\n\t\t\t\tbyLogin.set(keyOf(written.type, login), written.id);\n\t\t\t}\n\n\t\t\treturn copy(written);\n\t\t},\n\n\t\tasync findUser(id) {\n\t\t\tconst stored = byId.get(id);\n\t\t\treturn stored === undefined ? null : copy(stored);\n\t\t},\n\n\t\tasync findUserByLogin(type, login) {\n\t\t\tconst id = byLogin.get(keyOf(type, login));\n\t\t\tconst stored = id === undefined ? undefined : byId.get(id);\n\t\t\treturn stored === undefined ? null : copy(stored);\n\t\t},\n\n\t\tasync listUsers({ type, after, limit }) {\n\t\t\t// A Map keeps insertion order, not id order: a retried insert or an\n\t\t\t// id minted on another machine can land out of sequence.\n\t\t\tconst ids = [...byId.values()]\n\t\t\t\t.filter((user) => user.type === type)\n\t\t\t\t.map((user) => user.id)\n\t\t\t\t.filter((id) => after === null || id > after)\n\t\t\t\t.sort();\n\t\t\tconst pageIds = ids.slice(0, limit);\n\t\t\tconst last = pageIds.at(-1);\n\n\t\t\treturn {\n\t\t\t\titems: pageIds.map((id) => copy(byId.get(id) as UserRecord)),\n\t\t\t\tnextCursor: ids.length > limit && last !== undefined ? last : null,\n\t\t\t};\n\t\t},\n\n\t\tasync updateUser(id, patch, ifVersion) {\n\t\t\tconst stored = byId.get(id);\n\n\t\t\tif (stored === undefined) {\n\t\t\t\tthrow new NotFoundError('updateUser: no user has this id', {\n\t\t\t\t\tuserId: id,\n\t\t\t\t\toperation: 'updateUser',\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (stored.version !== ifVersion) {\n\t\t\t\tthrow new StoreConflict(\n\t\t\t\t\t'version',\n\t\t\t\t\t`updateUser: expected version ${ifVersion}, found ${stored.version}`,\n\t\t\t\t\t{\n\t\t\t\t\t\tuserId: id,\n\t\t\t\t\t\texpectedVersion: ifVersion,\n\t\t\t\t\t\tactualVersion: stored.version,\n\t\t\t\t\t\toperation: 'updateUser',\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (patch.logins !== undefined) {\n\t\t\t\tconst collision = takenBy(stored.type, patch.logins, id);\n\t\t\t\tif (collision !== undefined) {\n\t\t\t\t\tthrow taken('updateUser', stored.type, collision);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst written = applyPatch(stored, copy(patch));\n\n\t\t\tbyId.set(id, written);\n\t\t\tif (patch.logins !== undefined) {\n\t\t\t\tfor (const login of stored.logins) {\n\t\t\t\t\tbyLogin.delete(keyOf(stored.type, login));\n\t\t\t\t}\n\t\t\t\tfor (const login of written.logins) {\n\t\t\t\t\tbyLogin.set(keyOf(written.type, login), id);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn copy(written);\n\t\t},\n\n\t\tasync deleteUser(id) {\n\t\t\tconst stored = byId.get(id);\n\t\t\tif (stored === undefined) return false;\n\n\t\t\tbyId.delete(id);\n\t\t\tfor (const login of stored.logins) {\n\t\t\t\tbyLogin.delete(keyOf(stored.type, login));\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\t};\n}\n\n/**\n * The record a patch produces: the fields it names, replaced whole; the fields\n * it does not name, untouched.\n *\n * Reads each field by name rather than spreading the patch, so a key the port\n * does not declare — `version`, `id`, `type`, `createdAt`, or a `snake_case`\n * typo from JavaScript — never reaches the record. A key present as\n * `undefined` is absent, never an erasure.\n */\nfunction applyPatch(stored: UserRecord, patch: UserPatch): UserRecord {\n\treturn {\n\t\t...stored,\n\t\tschemaVersion: patch.schemaVersion ?? stored.schemaVersion,\n\t\tactive: patch.active ?? stored.active,\n\t\tfields: patch.fields ?? stored.fields,\n\t\tlogins: patch.logins ?? stored.logins,\n\t\tpassword: patch.password === undefined ? stored.password : patch.password,\n\t\temailVerifiedAt:\n\t\t\tpatch.emailVerifiedAt === undefined\n\t\t\t\t? stored.emailVerifiedAt\n\t\t\t\t: patch.emailVerifiedAt,\n\t\tversion: stored.version + 1,\n\t\tupdatedAt: patch.updatedAt,\n\t};\n}\n\nfunction memorySessionStore(): SessionStore {\n\tconst byId = new Map<SessionId, SessionRecord>();\n\tconst byTokenHash = new Map<string, SessionId>();\n\n\tconst revoke = (id: SessionId, stored: SessionRecord, at: Date): void => {\n\t\tbyId.set(id, { ...stored, revokedAt: new Date(at) });\n\t};\n\n\treturn {\n\t\tasync insertSession(record) {\n\t\t\tif (byId.has(record.id)) return;\n\n\t\t\tconst written = copy(record);\n\t\t\tbyId.set(written.id, written);\n\t\t\tbyTokenHash.set(written.tokenHash, written.id);\n\t\t},\n\n\t\tasync findSessionByTokenHash(tokenHash) {\n\t\t\tconst id = byTokenHash.get(tokenHash);\n\t\t\tconst stored = id === undefined ? undefined : byId.get(id);\n\t\t\treturn stored === undefined ? null : copy(stored);\n\t\t},\n\n\t\tasync extendSession(id, expiresAt) {\n\t\t\tconst stored = byId.get(id);\n\t\t\tif (stored === undefined || stored.revokedAt !== null) return null;\n\n\t\t\tconst written: SessionRecord = {\n\t\t\t\t...stored,\n\t\t\t\texpiresAt: new Date(expiresAt),\n\t\t\t};\n\t\t\tbyId.set(id, written);\n\t\t\treturn copy(written);\n\t\t},\n\n\t\tasync revokeSession(id, at) {\n\t\t\tconst stored = byId.get(id);\n\t\t\tif (stored === undefined) return false;\n\n\t\t\tif (stored.revokedAt === null) revoke(id, stored, at);\n\t\t\treturn true;\n\t\t},\n\n\t\tasync revokeUserSessions(userId, at, except) {\n\t\t\tlet revoked = 0;\n\n\t\t\tfor (const [id, stored] of byId) {\n\t\t\t\tif (\n\t\t\t\t\tstored.userId === userId &&\n\t\t\t\t\tstored.revokedAt === null &&\n\t\t\t\t\tid !== except\n\t\t\t\t) {\n\t\t\t\t\trevoke(id, stored, at);\n\t\t\t\t\trevoked += 1;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn revoked;\n\t\t},\n\n\t\tasync deleteUserSessions(userId) {\n\t\t\tlet deleted = 0;\n\n\t\t\tfor (const [id, stored] of byId) {\n\t\t\t\tif (stored.userId === userId) {\n\t\t\t\t\tbyId.delete(id);\n\t\t\t\t\tbyTokenHash.delete(stored.tokenHash);\n\t\t\t\t\tdeleted += 1;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn deleted;\n\t\t},\n\n\t\tasync deleteExpiredSessions(before) {\n\t\t\tlet deleted = 0;\n\n\t\t\tfor (const [id, stored] of byId) {\n\t\t\t\tif (stored.expiresAt.getTime() <= before.getTime()) {\n\t\t\t\t\tbyId.delete(id);\n\t\t\t\t\tbyTokenHash.delete(stored.tokenHash);\n\t\t\t\t\tdeleted += 1;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn deleted;\n\t\t},\n\t};\n}\n\nfunction memoryTokenStore(): TokenStore {\n\tconst byTokenHash = new Map<string, TokenRecord>();\n\n\treturn {\n\t\tasync insertToken(record) {\n\t\t\tif (byTokenHash.has(record.tokenHash)) return;\n\t\t\tbyTokenHash.set(record.tokenHash, copy(record));\n\t\t},\n\n\t\tasync consumeToken(tokenHash, kind, at) {\n\t\t\t// No `await` between this read and the write below: on one event loop\n\t\t\t// that is what makes the pair a single conditional write.\n\t\t\tconst stored = byTokenHash.get(tokenHash);\n\t\t\tif (stored === undefined || stored.kind !== kind) return null;\n\n\t\t\tconst before = copy(stored);\n\t\t\tif (stored.spentAt === null) {\n\t\t\t\tbyTokenHash.set(tokenHash, { ...stored, spentAt: new Date(at) });\n\t\t\t}\n\n\t\t\treturn before;\n\t\t},\n\n\t\tasync deleteUserTokens(userId) {\n\t\t\tlet deleted = 0;\n\n\t\t\tfor (const [tokenHash, stored] of byTokenHash) {\n\t\t\t\tif (stored.userId === userId) {\n\t\t\t\t\tbyTokenHash.delete(tokenHash);\n\t\t\t\t\tdeleted += 1;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn deleted;\n\t\t},\n\t};\n}\n"
|
|
7
|
-
],
|
|
8
|
-
"mappings": "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",
|
|
9
|
-
"debugId": "127433514BF2BB4B64756E2164756E21",
|
|
10
|
-
"names": []
|
|
11
|
-
}
|