@nxgt/janus 0.1.3 → 0.2.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 -30
- package/dist/auth/port/types.d.ts +6 -4
- package/dist/auth/port/types.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 +12 -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 +1 -1
- 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/docs/guide/adapters.md +6 -2
- package/docs/guide/errors.md +3 -2
- package/docs/guide/permissions.md +287 -109
- package/docs/guide/vocabulary.md +21 -18
- package/docs/roadmap.md +31 -8
- package/docs/troubleshooting.md +34 -16
- 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
|
|
|
@@ -59,14 +59,16 @@ either finds this row.
|
|
|
59
59
|
| **object** | What a permission is about: `{ type: 'document', id }` | "resource" |
|
|
60
60
|
| **subject** | Who a permission is about: a user, an object, or a subject set | "principal", "actor" |
|
|
61
61
|
| **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
|
-
| **
|
|
62
|
+
| **subject set** | Everyone holding one relation on one object: `team:t1#members` | "group" — a group is an object with a `members` relation |
|
|
63
|
+
| **relation** | A named link, stored as tuples or read from a field (`fromField`). Declared under `related`, named in the plural: `members`, `doctors` | |
|
|
64
|
+
| **`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 })` |
|
|
65
|
+
| **holder** | What a relation admits: `'staff'`, or the subject set `'team#members'` | |
|
|
66
|
+
| **permission** | A name computed from relations and other permissions by its rules. Declared under `permits` | |
|
|
67
|
+
| **`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
68
|
| **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: `'
|
|
69
|
+
| **arrow** | A rule that follows a relation to another object's permission or relation: `'teams->view'` | |
|
|
68
70
|
| **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: `
|
|
71
|
+
| **tuple** | One stored fact — object, relation, subject: `team:t1#members@staff:u1` | "grant", "ACL entry" |
|
|
70
72
|
| **guarded route** | A route that runs only when its subject holds a permission on the object it serves — `permission()` in `@nxgt/janus-hono` | |
|
|
71
73
|
|
|
72
74
|
### Stores
|
|
@@ -76,7 +78,8 @@ either finds this row.
|
|
|
76
78
|
| **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
79
|
| **port** | The interface a store implements: `JanusStores` for the identity stores — named after the package, not the side — and `RelationStore` | "driver" |
|
|
78
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" |
|
|
79
|
-
| **integration** | A package fitting Janus into one web framework: `@nxgt/janus-hono`. It implements no port | "plugin", "adapter" |
|
|
81
|
+
| **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
|
+
| **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
83
|
|
|
81
84
|
### Answers
|
|
82
85
|
|
|
@@ -100,7 +103,7 @@ interface RelationTuple { readonly object: Entity; readonly relation: string; re
|
|
|
100
103
|
```
|
|
101
104
|
|
|
102
105
|
**Subjects are typed**: `{ type, id }` for one entity, `{ type, id, relation }`
|
|
103
|
-
for a subject set —
|
|
106
|
+
for a subject set — everyone holding `members` on `team:t1`. `type` is the same word as a
|
|
104
107
|
user's own, so a user id and a subject id are the same thing, and
|
|
105
108
|
`subjectOf(user)` is the one-line join between the two sides of the package.
|
|
106
109
|
It copies `type` and `id` only, so none of the user's own fields ever reaches
|
|
@@ -111,25 +114,25 @@ a tuple.
|
|
|
111
114
|
| `subjectOf(user)` | `{ type, id }` of any object carrying both |
|
|
112
115
|
| `isSubjectSet(subject)` | whether `relation` is a string |
|
|
113
116
|
| `formatEntity(entity)` | `'record:r1'` |
|
|
114
|
-
| `formatSubject(subject)` | `'staff:u1'`, or `'team:t1#
|
|
115
|
-
| `formatTuple(tuple)` | `'
|
|
117
|
+
| `formatSubject(subject)` | `'staff:u1'`, or `'team:t1#members'` |
|
|
118
|
+
| `formatTuple(tuple)` | `'team:t1#members@team:t2#members'` |
|
|
116
119
|
| `parseSubject(text)` | the `Subject` back |
|
|
117
120
|
| `parseTuple(text)` | the `RelationTuple` back |
|
|
118
121
|
|
|
119
122
|
```ts
|
|
120
123
|
import { isSubjectSet, parseSubject, parseTuple } from '@nxgt/janus';
|
|
121
124
|
|
|
122
|
-
parseTuple('
|
|
123
|
-
// { object: { type: '
|
|
125
|
+
parseTuple('team:t1#members@staff:u1');
|
|
126
|
+
// { object: { type: 'team', id: 't1' }, relation: 'members', subject: { type: 'staff', id: 'u1' } }
|
|
124
127
|
|
|
125
|
-
const subject = parseSubject('team:t1#
|
|
126
|
-
if (isSubjectSet(subject)) subject.relation; // '
|
|
128
|
+
const subject = parseSubject('team:t1#members');
|
|
129
|
+
if (isSubjectSet(subject)) subject.relation; // 'members'
|
|
127
130
|
```
|
|
128
131
|
|
|
129
132
|
The notation is for messages, logs and tests, not a wire format. No part may
|
|
130
133
|
hold `@`, `#` or a parenthesis, and a type may not hold a `:`, so every string
|
|
131
134
|
reads one way. `parseTuple` and `parseSubject` refuse anything else with a
|
|
132
|
-
**bare `TypeError`** — including Keto's untyped subject, `
|
|
135
|
+
**bare `TypeError`** — including Keto's untyped subject, `team:t1#members@grace`,
|
|
133
136
|
and the message says what a subject is. Nothing in this package reads a tuple
|
|
134
137
|
off the network, so a malformed string came from your own code.
|
|
135
138
|
|
package/docs/roadmap.md
CHANGED
|
@@ -5,10 +5,17 @@ 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 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.
|
|
12
19
|
|
|
13
20
|
## Next
|
|
14
21
|
|
|
@@ -19,10 +26,8 @@ Nothing yet.
|
|
|
19
26
|
- **More official adapters** — the ports are cut where atomicity is not
|
|
20
27
|
required, so users, sessions and permission tuples can each live in the
|
|
21
28
|
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.
|
|
29
|
+
([`@nxgt/janus-mongo`](https://www.npmjs.com/package/@nxgt/janus-mongo));
|
|
30
|
+
PostgreSQL is under Now.
|
|
26
31
|
|
|
27
32
|
## Not planned
|
|
28
33
|
|
|
@@ -63,6 +68,24 @@ Nothing yet.
|
|
|
63
68
|
|
|
64
69
|
The first public release, v0.1.
|
|
65
70
|
|
|
71
|
+
- **The model's keys read `related` and `permits`** — Keto's OPL words: an
|
|
72
|
+
object type declares `related: { members: ['staff', 'team#members'] }` and
|
|
73
|
+
`permits: { view: ['members'] }`, and relation names are plural by convention. Breaking:
|
|
74
|
+
`relations` and `permissions` as keys are refused, by the compiler and by
|
|
75
|
+
`defineModel`, with a message naming the new key —
|
|
76
|
+
`types.team.relations is now related: rename the key`. The `permissions()`
|
|
77
|
+
function and `janus({ relations })` keep their names. — next minor
|
|
78
|
+
- **`LOGIN_TAKEN` no longer quotes the login in its message**, in the memory
|
|
79
|
+
store and in both adapters; `error.login` still names it, and the
|
|
80
|
+
conformance suite checks it. — next patch
|
|
81
|
+
- **The conformance suite accepts a store with its own expiry** — a store
|
|
82
|
+
that drops a lapsed session at once, as a Redis TTL does, passes
|
|
83
|
+
`sessions.deleteUser`; one that still holds it must count it. — next patch
|
|
84
|
+
|
|
85
|
+
- **A Hono integration** — [`@nxgt/janus-hono`](https://www.npmjs.com/package/@nxgt/janus-hono):
|
|
86
|
+
the session middleware, the cookie, a route guarded by a permission,
|
|
87
|
+
`bindJanus()` to bind the instances once, and every error as its status.
|
|
88
|
+
Its own 0.1.0, beside `@nxgt/janus` 0.1.3.
|
|
66
89
|
- **`defineModel` completed by your editor** — subject types and subject sets
|
|
67
90
|
in a relation, subject types in `fromField`, relations, permissions and
|
|
68
91
|
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.
|
|
@@ -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
|
-
}
|