@nxgt/janus 0.2.2 → 0.4.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 +87 -14
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/port/types.d.ts +70 -2
- package/dist/auth/port/types.d.ts.map +1 -1
- package/dist/auth/users.d.ts.map +1 -1
- package/dist/chunks/{index-hm4v76kd.js → index-53y1afjz.js} +2 -2
- package/dist/chunks/{index-fgb3t64y.js → index-mgh85djb.js} +2 -2
- package/dist/chunks/{index-6p56fpbe.js → index-qwfkhqkk.js} +22 -4
- package/dist/chunks/{index-6p56fpbe.js.map → index-qwfkhqkk.js.map} +4 -4
- package/dist/chunks/{index-0xarpm7z.js → index-thtyq7a9.js} +14 -3
- package/dist/chunks/{index-0xarpm7z.js.map → index-thtyq7a9.js.map} +3 -3
- package/dist/conformance/cases/outage.d.ts.map +1 -1
- package/dist/conformance/cases/tokens.d.ts.map +1 -1
- package/dist/conformance/cases/users.d.ts.map +1 -1
- package/dist/conformance/fixtures.d.ts.map +1 -1
- package/dist/conformance/index.js +111 -5
- package/dist/conformance/index.js.map +6 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -5
- package/dist/index.js.map +4 -4
- package/dist/permissions/index.js +47 -21
- package/dist/permissions/index.js.map +6 -6
- package/dist/permissions/input.d.ts +5 -3
- package/dist/permissions/input.d.ts.map +1 -1
- package/dist/permissions/model.d.ts +19 -8
- package/dist/permissions/model.d.ts.map +1 -1
- package/dist/permissions/resolve.d.ts.map +1 -1
- package/dist/permissions/reverse.d.ts.map +1 -1
- package/dist/subjects/notation.d.ts +7 -3
- package/dist/subjects/notation.d.ts.map +1 -1
- package/dist/subjects/subject.d.ts +53 -0
- package/dist/subjects/subject.d.ts.map +1 -1
- package/docs/README.md +1 -1
- package/docs/guide/adapters.md +124 -3
- package/docs/guide/permissions.md +50 -18
- package/docs/guide/vocabulary.md +10 -5
- package/docs/roadmap.md +19 -5
- package/docs/troubleshooting.md +34 -3
- package/package.json +1 -1
- /package/dist/chunks/{index-hm4v76kd.js.map → index-53y1afjz.js.map} +0 -0
- /package/dist/chunks/{index-fgb3t64y.js.map → index-mgh85djb.js.map} +0 -0
package/README.md
CHANGED
|
@@ -202,9 +202,10 @@ request handler should ever answer one, so no handler needs to tell it apart.
|
|
|
202
202
|
### Subjects
|
|
203
203
|
|
|
204
204
|
```ts
|
|
205
|
-
import { type Subject, subjectOf, formatTuple, parseTuple, isSubjectSet } from '@nxgt/janus';
|
|
205
|
+
import { type Subject, subjectOf, setOf, formatTuple, parseTuple, isSubjectSet } from '@nxgt/janus';
|
|
206
206
|
|
|
207
207
|
subjectOf(user); // { type: 'staff', id: '…' }: the user IS the subject
|
|
208
|
+
setOf(user, 'managers'); // { type: 'staff', id: '…', relation: 'managers' }: everyone who manages them
|
|
208
209
|
formatTuple({
|
|
209
210
|
object: { type: 'record', id: 'r1' },
|
|
210
211
|
relation: 'members',
|
|
@@ -215,8 +216,20 @@ parseTuple('team:t1#members@staff:u1'); // the RelationTuple back
|
|
|
215
216
|
```
|
|
216
217
|
|
|
217
218
|
`formatEntity`, `formatSubject` and `parseSubject` do the same for one part,
|
|
218
|
-
and `isSubjectSet` tells `{ type, id, relation }` from `{ type, id }
|
|
219
|
-
|
|
219
|
+
and `isSubjectSet` tells `{ type, id, relation }` from `{ type, id }` by
|
|
220
|
+
shape alone: pass a user through `subjectOf` first, so a field named
|
|
221
|
+
`relation` does not read as a set. `isSetOf` answers whether `setOf` made a
|
|
222
|
+
value — what `can()` and `grant()` read on a user type. The types are
|
|
223
|
+
`Entity`, `SubjectSet`, `Subject` (either), `SetOf` (what `setOf` answers) and
|
|
224
|
+
`RelationTuple`.
|
|
225
|
+
|
|
226
|
+
A user passed as it is, is that user, even with a field named `relation`:
|
|
227
|
+
**`setOf` is the one way to write a set on a user type** the model also
|
|
228
|
+
declares as an object type — `grant(note, 'readers', setOf(bob, 'managers'))`.
|
|
229
|
+
On an object type, `{ type, id, relation }` written out is a set too.
|
|
230
|
+
`parseSubject` and `parseTuple` answer a set as `setOf` makes it — frozen, and
|
|
231
|
+
marked — so compare a parsed set with `setOf(…)` or through `formatSubject`,
|
|
232
|
+
not with a plain `{ type, id, relation }`.
|
|
220
233
|
|
|
221
234
|
In Ory, the equality between a Kratos identity id and Keto's `subject_id` is a
|
|
222
235
|
comment and a convention, restated in three repositories and enforced nowhere.
|
|
@@ -383,6 +396,38 @@ adapter. `assertStores(store, where)` is the check `janus()` runs on it, for an
|
|
|
383
396
|
adapter that wants to fail as early. The six rules an adapter keeps are
|
|
384
397
|
written on the port's types.
|
|
385
398
|
|
|
399
|
+
The port already holds what one-time codes need, before any flow uses it: a
|
|
400
|
+
user's `secondFactor` (a TOTP secret, or `null`; the core will seal it before
|
|
401
|
+
a store sees it, once the second factor ships), a token's `codeHash` and
|
|
402
|
+
`attempts`, and `TokenStore.countAttempt`, which counts one attempt in one
|
|
403
|
+
conditional write:
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
import { createMemoryStores, mintId } from '@nxgt/janus';
|
|
407
|
+
|
|
408
|
+
const { tokens } = createMemoryStores();
|
|
409
|
+
const tokenHash = 'a'.repeat(64); // the core stores sha256 of the secret
|
|
410
|
+
await tokens.insertToken({
|
|
411
|
+
tokenHash,
|
|
412
|
+
kind: 'signInCode',
|
|
413
|
+
userId: mintId(),
|
|
414
|
+
address: 'ada@example.test',
|
|
415
|
+
codeHash: 'c'.repeat(64),
|
|
416
|
+
attempts: 0,
|
|
417
|
+
expiresAt: new Date(Date.now() + 10 * 60_000),
|
|
418
|
+
spentAt: null,
|
|
419
|
+
createdAt: new Date(),
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
await tokens.countAttempt(tokenHash, 'signInCode'); // { …, attempts: 1 }
|
|
423
|
+
await tokens.countAttempt(tokenHash, 'verifyEmail'); // null: no token of that kind
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
An adapter written against `@nxgt/janus` 0.3 does not compile against this
|
|
427
|
+
port until it implements `countAttempt`, and `janus()` refuses it at wiring —
|
|
428
|
+
[Writing an adapter](docs/guide/adapters.md#tokenstorecountattempt) has the
|
|
429
|
+
contract.
|
|
430
|
+
|
|
386
431
|
### Permissions — `@nxgt/janus/permissions`
|
|
387
432
|
|
|
388
433
|
```ts
|
|
@@ -448,7 +493,8 @@ or an empty page, before any store call.
|
|
|
448
493
|
exist, a rule naming nothing, an arrow to a permission its target lacks, a
|
|
449
494
|
permission asked of the wrong type, an object missing a field, a missing
|
|
450
495
|
`ctx`, a `grant` of a relation read from a field or to a holder it does not
|
|
451
|
-
admit, a
|
|
496
|
+
admit, a subject set naming a relation its type lacks or a user type not
|
|
497
|
+
declared under `types`, a `list()` through a `fromField` without a `lookup`: each is a compile
|
|
452
498
|
error, on the offending argument. So are the keys before 0.2, `relations` and
|
|
453
499
|
`permissions`: the error names the new one — `team.relations is now related:
|
|
454
500
|
rename the key` — and `defineModel` refuses them the same way at run time. `defineModel` refuses with a `TypeError`
|
|
@@ -456,6 +502,10 @@ what only running it can see: names that are not camelCase, a permission that
|
|
|
456
502
|
reaches itself without crossing a relation, a subject set or an arrow that
|
|
457
503
|
would have to read another object's field.
|
|
458
504
|
|
|
505
|
+
**A user type may also be an object type**: declare `staff` under `types`, and
|
|
506
|
+
a staff member is an object too — who may edit them is a relation on them. See
|
|
507
|
+
[permissions on a user](docs/guide/permissions.md#permissions-on-a-user).
|
|
508
|
+
|
|
459
509
|
**Wire the relation store into `janus()` too** — `janus({ …, relations })` —
|
|
460
510
|
and deleting a user deletes every tuple naming them. Deleting an object's
|
|
461
511
|
tuples is `store.deleteEntity({ type, id })`, from your own code.
|
|
@@ -488,7 +538,7 @@ describeJanusStores({
|
|
|
488
538
|
});
|
|
489
539
|
```
|
|
490
540
|
|
|
491
|
-
There are
|
|
541
|
+
There are 44 cases. They cover:
|
|
492
542
|
- round-trip, byte for byte — including every edge character the core lets
|
|
493
543
|
through (control characters, U+FFFF, a surrogate pair);
|
|
494
544
|
- uniqueness, as a constraint: of twenty concurrent inserts of one login,
|
|
@@ -499,9 +549,13 @@ There are 38 cases. They cover:
|
|
|
499
549
|
- pagination;
|
|
500
550
|
- sessions;
|
|
501
551
|
- one-time tokens: of twenty concurrent redemptions, exactly one succeeds;
|
|
552
|
+
of twenty concurrent `countAttempt` calls, each answers a distinct count,
|
|
553
|
+
and none is counted once a racing redemption spent the token;
|
|
554
|
+
- a user's second factor: round-trip, kept by a patch that does not name it,
|
|
555
|
+
removed by one that names `null`;
|
|
502
556
|
- deletion: a user's logins are freed, and every session and token of theirs
|
|
503
557
|
goes, with a replay answering `false` or `0` rather than failing;
|
|
504
|
-
- **outages**, one case for each of the
|
|
558
|
+
- **outages**, one case for each of the twelve methods whose honest answer can
|
|
505
559
|
be "nothing".
|
|
506
560
|
|
|
507
561
|
The suite imports no test framework and no assertion library. It runs under
|
|
@@ -533,6 +587,15 @@ example; `allRelationCases`, `relationStoreCases`, `relationOutageCases` and
|
|
|
533
587
|
|
|
534
588
|
## Traps
|
|
535
589
|
|
|
590
|
+
**On a user type, only `setOf` makes a set.** A user passed as it is — or
|
|
591
|
+
`{ type: 'staff', id, relation: 'managers' }` written out — is that one user,
|
|
592
|
+
even with a field named `relation`. The compiler refuses the written-out set
|
|
593
|
+
in `grant()` and `revoke()` where the relation admits one. From JavaScript it grants that user, silently,
|
|
594
|
+
where the relation also admits the user — and is refused where it does not.
|
|
595
|
+
`grant(note, 'readers', setOf(bob, 'managers'))`. A spread of a set is still
|
|
596
|
+
the set; **through `JSON` or `structuredClone` it comes back as the user** —
|
|
597
|
+
call `setOf` again, or `parseSubject` on its notation (`staff:u1#managers`).
|
|
598
|
+
|
|
536
599
|
**Narrowing a model hides stored tuples; it does not delete them.** A tuple
|
|
537
600
|
the model no longer admits grants nothing, and `revoke()` refuses it — remove
|
|
538
601
|
it with `relations.write({ remove: [tuple] })`, or widening the model again
|
|
@@ -628,15 +691,15 @@ could not answer: that is a denial made of an outage.
|
|
|
628
691
|
|
|
629
692
|
## Type safety, counted
|
|
630
693
|
|
|
631
|
-
**
|
|
632
|
-
|
|
694
|
+
**One hundred and three plausible mistakes, one hundred and three refused at compile time — and
|
|
695
|
+
two gaps, named.**
|
|
633
696
|
|
|
634
697
|
The lists are typechecked and never run, with one `@ts-expect-error` per
|
|
635
698
|
mistake beside the shapes that must keep compiling:
|
|
636
699
|
`test/types/refusals.ts` (fourteen, on the shared vocabulary),
|
|
637
|
-
`test/types/port.ts` (
|
|
700
|
+
`test/types/port.ts` (twenty-one, on the identity stores' port, from the point
|
|
638
701
|
of view of the person implementing it), `test/types/auth.ts` (twenty, on
|
|
639
|
-
`janus()`, from the point of view of the application) and `test/types/permissions.ts` (forty-
|
|
702
|
+
`janus()`, from the point of view of the application) and `test/types/permissions.ts` (forty-eight, on the
|
|
640
703
|
permission model and the questions asked of it). The rule
|
|
641
704
|
comes from `nxgt-data`, and so does the reason to
|
|
642
705
|
distrust the claim without the files: when it was last measured on
|
|
@@ -651,10 +714,20 @@ permissions and arrows in a rule and in `when`; and in the questions, what
|
|
|
651
714
|
`can`, `list` and `grant` accept for the object's type. It also checks that a wrong
|
|
652
715
|
name's error lists the names it could have been.
|
|
653
716
|
|
|
654
|
-
The
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
717
|
+
The gaps, since a measurement that only reports wins is not a measurement:
|
|
718
|
+
|
|
719
|
+
- `'30 m'` **satisfies `Duration`**, because TypeScript's `${number}`
|
|
720
|
+
placeholder tolerates trailing whitespace inside the number.
|
|
721
|
+
`parseDuration` refuses it, and `duration.spec.ts` asserts that.
|
|
722
|
+
- **`can()` and `list()` accept `{ type: 'staff', id, relation: 'managers' }`
|
|
723
|
+
written out** on a user type that is also an object type, and ask about that
|
|
724
|
+
one user — `grant()` and `revoke()` refuse it (case 42). The anonymous
|
|
725
|
+
`null` their subject also takes turns off TypeScript's check of the object
|
|
726
|
+
literal, and a check of its own would refuse a user whose schema has a
|
|
727
|
+
`relation` field. `engine.spec.ts` asserts the run time reads it as the
|
|
728
|
+
user. Write the set with `setOf`.
|
|
729
|
+
|
|
730
|
+
Each is written down rather than omitted.
|
|
658
731
|
|
|
659
732
|
## Licence
|
|
660
733
|
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { bunHasher, scryptHasher } from './hashers';
|
|
|
3
3
|
export { janus } from './janus';
|
|
4
4
|
export { assertStores } from './port/assert-stores';
|
|
5
5
|
export { createMemoryStores } from './port/memory';
|
|
6
|
-
export type { JanusStores, Json, JsonObject, PasswordRecord, SessionId, SessionRecord, SessionStore, StoreCapabilities, TokenKind, TokenRecord, TokenStore, UserPageRequest, UserPatch, UserRecord, UserStore, } from './port/types';
|
|
6
|
+
export type { JanusStores, Json, JsonObject, PasswordRecord, SecondFactorRecord, SessionId, SessionRecord, SessionStore, StoreCapabilities, TokenKind, TokenRecord, TokenStore, UserPageRequest, UserPatch, UserRecord, UserStore, } from './port/types';
|
|
7
7
|
export type { StandardSchemaV1 } from './standard-schema';
|
|
8
8
|
export type { Authenticated, Checked, EmailOf, HeaderRecord, IssuedToken, Janus, LoginOf, PasswordApi, RequestLike, RequiredStringKeys, ResetPasswordApi, Session, SharedApi, SignedIn, TypeApi, TypesOf, User, UserBase, UserOf, UserRef, UserTypeApi, VerifyEmailApi, WriteOptions, } from './types';
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/auth/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACX,YAAY,EACZ,UAAU,EACV,WAAW,EACX,eAAe,EACf,SAAS,EACT,cAAc,EACd,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,cAAc,GACd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,YAAY,EACX,WAAW,EACX,IAAI,EACJ,UAAU,EACV,cAAc,EACd,SAAS,EACT,aAAa,EACb,YAAY,EACZ,iBAAiB,EACjB,SAAS,EACT,WAAW,EACX,UAAU,EACV,eAAe,EACf,SAAS,EACT,UAAU,EACV,SAAS,GACT,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,YAAY,EACX,aAAa,EACb,OAAO,EACP,OAAO,EACP,YAAY,EACZ,WAAW,EACX,KAAK,EACL,OAAO,EACP,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,gBAAgB,EAChB,OAAO,EACP,SAAS,EACT,QAAQ,EACR,OAAO,EACP,OAAO,EACP,IAAI,EACJ,QAAQ,EACR,MAAM,EACN,OAAO,EACP,WAAW,EACX,cAAc,EACd,YAAY,GACZ,MAAM,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACX,YAAY,EACZ,UAAU,EACV,WAAW,EACX,eAAe,EACf,SAAS,EACT,cAAc,EACd,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,cAAc,GACd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,YAAY,EACX,WAAW,EACX,IAAI,EACJ,UAAU,EACV,cAAc,EACd,kBAAkB,EAClB,SAAS,EACT,aAAa,EACb,YAAY,EACZ,iBAAiB,EACjB,SAAS,EACT,WAAW,EACX,UAAU,EACV,eAAe,EACf,SAAS,EACT,UAAU,EACV,SAAS,GACT,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,YAAY,EACX,aAAa,EACb,OAAO,EACP,OAAO,EACP,YAAY,EACZ,WAAW,EACX,KAAK,EACL,OAAO,EACP,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,gBAAgB,EAChB,OAAO,EACP,SAAS,EACT,QAAQ,EACR,OAAO,EACP,OAAO,EACP,IAAI,EACJ,QAAQ,EACR,MAAM,EACN,OAAO,EACP,WAAW,EACX,cAAc,EACd,YAAY,GACZ,MAAM,SAAS,CAAC"}
|
|
@@ -84,6 +84,31 @@ export interface PasswordRecord {
|
|
|
84
84
|
readonly hash: string;
|
|
85
85
|
readonly updatedAt: Date;
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* A second factor, as the store holds it: a TOTP secret.
|
|
89
|
+
*
|
|
90
|
+
* **The secret is opaque to a store.** Once the second factor ships, the core
|
|
91
|
+
* will seal it with a key the application holds before a store ever sees it,
|
|
92
|
+
* so a dump of the users cannot produce a code. A store keeps the string byte
|
|
93
|
+
* for byte, like a password hash, and never parses it.
|
|
94
|
+
*/
|
|
95
|
+
export interface SecondFactorRecord {
|
|
96
|
+
/** How the codes are made. `'totp'`, the codes of an authenticator app, is the only one. */
|
|
97
|
+
readonly method: 'totp';
|
|
98
|
+
/** The secret, opaque to a store: kept byte for byte. */
|
|
99
|
+
readonly secret: string;
|
|
100
|
+
/**
|
|
101
|
+
* When the user proved their app holds the secret, with a first code — or
|
|
102
|
+
* `null` while the enrolment waits for it. A second factor is asked for at
|
|
103
|
+
* sign-in only once confirmed.
|
|
104
|
+
*/
|
|
105
|
+
readonly confirmedAt: Date | null;
|
|
106
|
+
/**
|
|
107
|
+
* The time step of the last code accepted, or `null` before the first. A
|
|
108
|
+
* code of this step or an earlier one is refused, so a code works once.
|
|
109
|
+
*/
|
|
110
|
+
readonly lastStep: number | null;
|
|
111
|
+
}
|
|
87
112
|
/**
|
|
88
113
|
* A user, as a store holds it.
|
|
89
114
|
*
|
|
@@ -122,6 +147,8 @@ export interface UserRecord {
|
|
|
122
147
|
readonly logins: readonly string[];
|
|
123
148
|
/** `null` when the user has no password. */
|
|
124
149
|
readonly password: PasswordRecord | null;
|
|
150
|
+
/** `null` when the user has no second factor. */
|
|
151
|
+
readonly secondFactor: SecondFactorRecord | null;
|
|
125
152
|
/**
|
|
126
153
|
* When the user proved they hold their current e-mail, or `null`. The core
|
|
127
154
|
* sets it back to `null` in the same write that changes the e-mail.
|
|
@@ -166,6 +193,7 @@ export interface UserPatch {
|
|
|
166
193
|
readonly fields?: JsonObject;
|
|
167
194
|
readonly logins?: readonly string[];
|
|
168
195
|
readonly password?: PasswordRecord | null;
|
|
196
|
+
readonly secondFactor?: SecondFactorRecord | null;
|
|
169
197
|
readonly emailVerifiedAt?: Date | null;
|
|
170
198
|
}
|
|
171
199
|
/** Which users a page lists, where it starts, and how much of it to read. */
|
|
@@ -328,8 +356,16 @@ export interface SessionStore {
|
|
|
328
356
|
*/
|
|
329
357
|
deleteExpiredSessions?(before: Date): Promise<number>;
|
|
330
358
|
}
|
|
331
|
-
/**
|
|
332
|
-
|
|
359
|
+
/**
|
|
360
|
+
* What a one-time token is for. A token redeemed for another purpose is
|
|
361
|
+
* unknown.
|
|
362
|
+
*
|
|
363
|
+
* - `verifyEmail`, `resetPassword`: a link sent by e-mail.
|
|
364
|
+
* - `secondFactor`: the challenge a sign-in answers when the user has a second
|
|
365
|
+
* factor, redeemed with a code from their app.
|
|
366
|
+
* - `signInCode`: a code sent by e-mail to sign in without a password.
|
|
367
|
+
*/
|
|
368
|
+
export type TokenKind = 'verifyEmail' | 'resetPassword' | 'secondFactor' | 'signInCode';
|
|
333
369
|
/** A one-time token, as a store holds it: its hash, never its secret. */
|
|
334
370
|
export interface TokenRecord {
|
|
335
371
|
/** `sha256` of the token's secret, hex. Unique across the store. */
|
|
@@ -338,6 +374,19 @@ export interface TokenRecord {
|
|
|
338
374
|
readonly userId: Id;
|
|
339
375
|
/** The e-mail the token was sent to — the one a verification marks verified. */
|
|
340
376
|
readonly address: string;
|
|
377
|
+
/**
|
|
378
|
+
* For a token redeemed with a code — `signInCode` — the code's hash, keyed
|
|
379
|
+
* by the token's secret, so the tokens alone do not reveal it. `null` for
|
|
380
|
+
* every other kind: the core writes it so, and a store keeps what it is
|
|
381
|
+
* given.
|
|
382
|
+
*/
|
|
383
|
+
readonly codeHash: string | null;
|
|
384
|
+
/**
|
|
385
|
+
* How many codes were tried against it: `0` at insertion, and one more on
|
|
386
|
+
* every {@link TokenStore.countAttempt}. What bounds guessing a six-digit
|
|
387
|
+
* code.
|
|
388
|
+
*/
|
|
389
|
+
readonly attempts: number;
|
|
341
390
|
readonly expiresAt: Date;
|
|
342
391
|
/** `null` until spent. Once set, never changes. */
|
|
343
392
|
readonly spentAt: Date | null;
|
|
@@ -370,6 +419,25 @@ export interface TokenStore {
|
|
|
370
419
|
* job, after the call, so a lapsed token can never be retried.
|
|
371
420
|
*/
|
|
372
421
|
consumeToken(tokenHash: string, kind: TokenKind, at: Date): Promise<TokenRecord | null>;
|
|
422
|
+
/**
|
|
423
|
+
* Counts one attempt at a code against a token, and answers the token **as
|
|
424
|
+
* it is after this call**.
|
|
425
|
+
*
|
|
426
|
+
* - An **unspent** token of this `kind` has `attempts` one higher, and is
|
|
427
|
+
* answered with it. Twenty concurrent calls answer twenty distinct
|
|
428
|
+
* counts: one conditional write, never a read followed by a write — a
|
|
429
|
+
* count two guesses both read is a guess for free.
|
|
430
|
+
* - A **spent** token is answered as it is, and nothing is written.
|
|
431
|
+
* - `null` means no token of this `kind` has this hash.
|
|
432
|
+
*
|
|
433
|
+
* A **lapsed** token is counted all the same, or answered `null` by a store
|
|
434
|
+
* that already dropped it — as for `consumeToken`, comparing `expiresAt` is
|
|
435
|
+
* the core's job, after the call.
|
|
436
|
+
*
|
|
437
|
+
* Whether the count is past the limit, and whether the code matches, is the
|
|
438
|
+
* core's decision, after the call; spending the token is `consumeToken`.
|
|
439
|
+
*/
|
|
440
|
+
countAttempt(tokenHash: string, kind: TokenKind): Promise<TokenRecord | null>;
|
|
373
441
|
/**
|
|
374
442
|
* Deletes every token of one user, spent or not, and answers how many.
|
|
375
443
|
* What deleting a user calls: a token holds the e-mail it was sent to, which
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/auth/port/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/auth/port/types.ts"],"names":[],"mappings":"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"}
|
package/dist/auth/users.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/auth/users.ts"],"names":[],"mappings":"AASA,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7D,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,OAAO,EAaZ,MAAM,WAAW,CAAC;AAInB,OAAO,KAAK,EAEX,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,MAAM,SAAS,CAAC;AAEjB,KAAK,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAErC,mGAAmG;AACnG,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC,GACnD,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,GACnC,cAAc,CAAC,OAAO,CAAC,GACvB,gBAAgB,CAAC,OAAO,CAAC,CAAC;AAE3B,wBAAgB,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/auth/users.ts"],"names":[],"mappings":"AASA,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7D,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,OAAO,EAaZ,MAAM,WAAW,CAAC;AAInB,OAAO,KAAK,EAEX,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,MAAM,SAAS,CAAC;AAEjB,KAAK,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAErC,mGAAmG;AACnG,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC,GACnD,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,GACnC,cAAc,CAAC,OAAO,CAAC,GACvB,gBAAgB,CAAC,OAAO,CAAC,CAAC;AAE3B,wBAAgB,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,GAAG,UAAU,CAibxE"}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
JanusError2,
|
|
3
3
|
StoreFailure2,
|
|
4
4
|
InvalidCursorError2
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-qwfkhqkk.js";
|
|
6
6
|
|
|
7
7
|
// src/pagination/cursor-page.ts
|
|
8
8
|
var DEFAULT_PAGE_SIZE2 = 20;
|
|
@@ -68,4 +68,4 @@ function guardRelations(store) {
|
|
|
68
68
|
export { guardStore, guardRelations, DEFAULT_PAGE_SIZE2, MAX_PAGE_SIZE2, pageLimit2, invalidCursor2 };
|
|
69
69
|
|
|
70
70
|
//# debugId=78C1EC4522BC50BD64756E2164756E21
|
|
71
|
-
//# sourceMappingURL=index-
|
|
71
|
+
//# sourceMappingURL=index-53y1afjz.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isSubjectSet2,
|
|
3
3
|
formatTuple2
|
|
4
|
-
} from "./index-
|
|
4
|
+
} from "./index-qwfkhqkk.js";
|
|
5
5
|
|
|
6
6
|
// src/permissions/port/memory.ts
|
|
7
7
|
function createMemoryRelations2() {
|
|
@@ -74,4 +74,4 @@ function copyTuple(tuple) {
|
|
|
74
74
|
export { createMemoryRelations2 };
|
|
75
75
|
|
|
76
76
|
//# debugId=9168B1A5A9C0688D64756E2164756E21
|
|
77
|
-
//# sourceMappingURL=index-
|
|
77
|
+
//# sourceMappingURL=index-mgh85djb.js.map
|
|
@@ -108,6 +108,24 @@ function isSubjectSet2(subject) {
|
|
|
108
108
|
function subjectOf2(user) {
|
|
109
109
|
return { type: user.type, id: user.id };
|
|
110
110
|
}
|
|
111
|
+
var SET_OF = Symbol.for("@nxgt/janus/setOf");
|
|
112
|
+
function setOf2(entity, relation) {
|
|
113
|
+
if (typeof entity !== "object" || entity === null || typeof entity.type !== "string" || typeof entity.id !== "string") {
|
|
114
|
+
throw new TypeError("setOf: pass a user or { type, id }, then a relation");
|
|
115
|
+
}
|
|
116
|
+
if (typeof relation !== "string" || relation === "") {
|
|
117
|
+
throw new TypeError("setOf: the relation must be a non-empty string");
|
|
118
|
+
}
|
|
119
|
+
return Object.freeze({
|
|
120
|
+
type: entity.type,
|
|
121
|
+
id: entity.id,
|
|
122
|
+
relation,
|
|
123
|
+
[SET_OF]: true
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function isSetOf2(value) {
|
|
127
|
+
return typeof value === "object" && value !== null && value[SET_OF] === true;
|
|
128
|
+
}
|
|
111
129
|
|
|
112
130
|
// src/subjects/notation.ts
|
|
113
131
|
function formatEntity2(entity) {
|
|
@@ -138,10 +156,10 @@ function parseSubject2(text) {
|
|
|
138
156
|
throw new TypeError(`parseSubject: "${text}" is not a subject; expected type:id, or type:id#relation for a subject set`);
|
|
139
157
|
}
|
|
140
158
|
const [, type, id, relation] = match;
|
|
141
|
-
return relation === undefined ? { type, id } : { type, id, relation
|
|
159
|
+
return relation === undefined ? { type, id } : setOf2({ type, id }, relation);
|
|
142
160
|
}
|
|
143
161
|
|
|
144
|
-
export { JanusError2, StoreFailure2, StoreConflict2, NotFoundError2, UserInvalidError2, CredentialError2, UserInactiveError2, TokenError2, InvalidCursorError2, UnsupportedError2, PermissionDepthError2, isSubjectSet2, subjectOf2, formatEntity2, formatSubject2, formatTuple2, parseTuple2, parseSubject2 };
|
|
162
|
+
export { JanusError2, StoreFailure2, StoreConflict2, NotFoundError2, UserInvalidError2, CredentialError2, UserInactiveError2, TokenError2, InvalidCursorError2, UnsupportedError2, PermissionDepthError2, isSubjectSet2, subjectOf2, setOf2, isSetOf2, formatEntity2, formatSubject2, formatTuple2, parseTuple2, parseSubject2 };
|
|
145
163
|
|
|
146
|
-
//# debugId=
|
|
147
|
-
//# sourceMappingURL=index-
|
|
164
|
+
//# debugId=49525DE3EB2EE26A64756E2164756E21
|
|
165
|
+
//# sourceMappingURL=index-qwfkhqkk.js.map
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
"sources": ["../src/errors/janus-error.ts", "../src/subjects/subject.ts", "../src/subjects/notation.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * What this package refuses, as a string a caller can switch on.\n *\n * Every code is a **refusal at call time, on a value that could have come from\n * a request** — which is the rule that decides whether something belongs here\n * or stays a bare `TypeError`. A refusal that can only come from how the\n * application was wired (`janus()` with no schema, a lifespan that is\n * not a duration, a store missing a method) throws a plain `TypeError`\n * instead: no request handler should ever answer one, so no handler needs to\n * tell it apart from the others.\n *\n * The codes are `SCREAMING_SNAKE`, and that is not an exception to this\n * repository's camelCase rule — they are data values, not API identifiers, the\n * same shape `code` has in `@nxgt/mongo` and `@nxgt/redis`. Every *key* in\n * this package is camelCase.\n */\nexport type JanusErrorCode =\n\t/**\n\t * The store could not answer — a refused connection, a timeout, a primary\n\t * stepping down, a deserialisation failure, a bug in the adapter.\n\t *\n\t * **Never a negative answer.** A handler answers 503 and lets the visitor\n\t * retry. Mapping this to a 404, to `null` or to `false` turns an outage\n\t * into a silent lockout: every user is told they do not exist.\n\t * That failure has been measured twice in this organisation, two days\n\t * apart, and it is the reason this package's port is specified rather than\n\t * merely documented.\n\t */\n\t| 'STORE_FAILED'\n\t/**\n\t * The store answered, and there is no such record.\n\t *\n\t * Raised by the `get*` calls, never by the `find*` calls — those return\n\t * `null`, which is a value the caller decides what to do with.\n\t */\n\t| 'NOT_FOUND'\n\t/**\n\t * The login — an e-mail, a username — is already held by another user of\n\t * the same type.\n\t *\n\t * Raised by the **store's own unique constraint** and surfaced here, never\n\t * decided by reading first: two concurrent sign-ups both pass a read, and\n\t * only a constraint refuses one of them. Carries `login` and `userType`.\n\t */\n\t| 'LOGIN_TAKEN'\n\t/**\n\t * The record changed since it was read: the version it was expected to\n\t * hold is no longer the version it holds, and **nothing was written**.\n\t * Read it again and retry. Carries `expectedVersion` and `actualVersion`.\n\t */\n\t| 'VERSION_CONFLICT'\n\t/**\n\t * The fields failed the user type's schema. Carries `issues`, whose paths\n\t * are the fields' own, so a handler can answer 400 field by field.\n\t */\n\t| 'USER_INVALID'\n\t/**\n\t * The password is shorter than the policy's minimum. Reports the policy,\n\t * never the password.\n\t */\n\t| 'PASSWORD_TOO_SHORT'\n\t/**\n\t * The login and the password do not match: no such login, no password set,\n\t * or the wrong one — **one code for the three**, so a response cannot tell\n\t * which accounts exist. `reason` tells them apart for your logs and your\n\t * rate limiter, and never belongs in a response body.\n\t */\n\t| 'CREDENTIALS_INVALID'\n\t/**\n\t * A stored hash whose prefix names no wired verifier — typically an import\n\t * from a system whose format this core cannot read. Reports the prefix,\n\t * never the hash.\n\t */\n\t| 'HASH_UNSUPPORTED'\n\t/**\n\t * The user is inactive: the record and its password are kept, and every\n\t * sign-in is refused. Only told to somebody who gave the right password.\n\t */\n\t| 'USER_INACTIVE'\n\t/** No token holds that secret. */\n\t| 'TOKEN_UNKNOWN'\n\t/**\n\t * The token was already spent. Told apart from `TOKEN_UNKNOWN` for the\n\t * message only — both are refusals, and the outcome is the same.\n\t */\n\t| 'TOKEN_SPENT'\n\t/**\n\t * The token existed and its expiry has passed. It is spent all the same,\n\t * so it cannot be retried.\n\t */\n\t| 'TOKEN_EXPIRED'\n\t/**\n\t * The token was sent to an e-mail the user no longer has. Confirming it\n\t * would verify an address nobody holds any more, so it is spent and refused.\n\t */\n\t| 'TOKEN_STALE'\n\t/** A cursor this store did not mint, or one written for another ordering.\n\t * Never a silent first page: a caller paging a list would loop for ever. */\n\t| 'INVALID_CURSOR'\n\t/**\n\t * The wired store does not implement the optional capability this call\n\t * needs. Names the method and the slot, so the sentence says which store to\n\t * change or which call to stop making.\n\t */\n\t| 'UNSUPPORTED'\n\t/**\n\t * A permission check walked deeper than `maxDepth` relations without an\n\t * answer. **Not a refusal**: an evaluation that stopped half-way has not\n\t * decided anything, and answering `false` would hide a model that is too\n\t * deep behind denials nobody can explain. A cycle in the data is not this —\n\t * it is cut, silently. Carries `permission` and `maxDepth`.\n\t */\n\t| 'PERMISSION_DEPTH';\n\n/** One thing that was wrong with a user's fields, at one path. */\nexport interface Issue {\n\t/** The path inside the fields, as the schema reported it: `['address', 'city']`. */\n\treadonly path: readonly (string | number)[];\n\treadonly message: string;\n}\n\n/**\n * Why a sign-in was refused, for your logs and rate limiter.\n *\n * **Never put it in a response body.** `unknownLogin` is an account\n * enumeration oracle.\n */\nexport type CredentialRefusal = 'unknownLogin' | 'noPassword' | 'wrongPassword';\n\n/**\n * What an error may carry beside its code.\n *\n * **No field here ever holds a secret.** Not a password, not a hash, not a\n * session token, not a token secret, not a token's hash, and not a connection\n * URI — a connection string holds a password, and the specs assert its absence\n * from every message. Nor a login: a message reports a shape, never a value,\n * and a conflict carries the login in `login`.\n */\nexport interface JanusErrorOptions {\n\treadonly userId?: string;\n\treadonly userType?: string;\n\t/** The login a conflict names: an address, never a secret. */\n\treadonly login?: string;\n\treadonly reason?: CredentialRefusal;\n\t/** The prefix of a hash whose format is unknown. Never the hash. */\n\treadonly hashPrefix?: string;\n\treadonly expectedVersion?: number;\n\treadonly actualVersion?: number;\n\treadonly issues?: readonly Issue[];\n\t/** The minimum the policy requires. Never the password that failed it. */\n\treadonly minLength?: number;\n\t/** The port method being called: `insertUser`, `consumeToken`. */\n\treadonly operation?: string;\n\t/** Which store slot: the sentence should say which store to change. */\n\treadonly slot?: 'users' | 'sessions' | 'tokens' | 'relations';\n\t/** The permission being checked, in the notation: `record:r1#view`. */\n\treadonly permission?: string;\n\t/** The depth a check may walk. */\n\treadonly maxDepth?: number;\n\treadonly cause?: unknown;\n}\n\n/**\n * The base of everything this package throws at call time.\n *\n * It extends `Error` and not `TypeError`, and the rule behind that is\n * `nxgt-data`'s: *extend whichever class the refusals it replaces already\n * threw, so no consumer's `catch` stops working*. These replace nothing — the\n * package is new — and `DataError`, `RedisError` and `S3Error` all extend\n * `Error`, so nobody has to order their `catch` blocks.\n *\n * **There is exactly one definition of this class**, and that matters more here\n * than it looks: an adapter in another package throws `StoreFailure` and this\n * package tests it with `instanceof`. Two copies and the product is wrong about\n * what an outage is. `build.ts` shares the module across entry points with\n * `splitting: true`, and `scripts/verify-artifacts.ts` fails the build if any\n * class name appears in two entry bundles of the packed tarball.\n */\nexport class JanusError extends Error {\n\toverride name = 'JanusError';\n\treadonly code: JanusErrorCode = 'STORE_FAILED';\n\treadonly userId: string | undefined;\n\treadonly userType: string | undefined;\n\treadonly login: string | undefined;\n\treadonly reason: CredentialRefusal | undefined;\n\treadonly hashPrefix: string | undefined;\n\treadonly expectedVersion: number | undefined;\n\treadonly actualVersion: number | undefined;\n\treadonly issues: readonly Issue[] | undefined;\n\treadonly minLength: number | undefined;\n\treadonly operation: string | undefined;\n\treadonly slot: 'users' | 'sessions' | 'tokens' | 'relations' | undefined;\n\treadonly permission: string | undefined;\n\treadonly maxDepth: number | undefined;\n\n\tconstructor(message: string, options?: JanusErrorOptions) {\n\t\tsuper(message, { cause: options?.cause });\n\t\tthis.userId = options?.userId;\n\t\tthis.userType = options?.userType;\n\t\tthis.login = options?.login;\n\t\tthis.reason = options?.reason;\n\t\tthis.hashPrefix = options?.hashPrefix;\n\t\tthis.expectedVersion = options?.expectedVersion;\n\t\tthis.actualVersion = options?.actualVersion;\n\t\tthis.issues = options?.issues;\n\t\tthis.minLength = options?.minLength;\n\t\tthis.operation = options?.operation;\n\t\tthis.slot = options?.slot;\n\t\tthis.permission = options?.permission;\n\t\tthis.maxDepth = options?.maxDepth;\n\t}\n}\n\n/**\n * The store could not answer.\n *\n * **This is the class an adapter throws**, and it is exported for that reason:\n * an adapter defines no error class of its own, so `instanceof` holds across\n * the two packages. Any other throw from a store is treated as a failure too —\n * throwing this one is how an adapter says so precisely, and sets `cause`.\n */\nexport class StoreFailure extends JanusError {\n\toverride name = 'StoreFailure';\n\toverride readonly code = 'STORE_FAILED' as const;\n}\n\n/**\n * A uniqueness or a version constraint the store refused.\n *\n * Also thrown by an adapter, and also for the `instanceof` reason. `on` says\n * which constraint, because the two are answered differently: a login\n * collision is the caller's to fix, a version conflict is a retry.\n */\nexport class StoreConflict extends JanusError {\n\toverride name = 'StoreConflict';\n\toverride readonly code: JanusErrorCode;\n\treadonly on: 'login' | 'version';\n\n\tconstructor(\n\t\ton: 'login' | 'version',\n\t\tmessage: string,\n\t\toptions?: JanusErrorOptions,\n\t) {\n\t\tsuper(message, options);\n\t\tthis.on = on;\n\t\tthis.code = on === 'login' ? 'LOGIN_TAKEN' : 'VERSION_CONFLICT';\n\t}\n}\n\n/** There is no such record, and the store said so. */\nexport class NotFoundError extends JanusError {\n\toverride name = 'NotFoundError';\n\toverride readonly code = 'NOT_FOUND' as const;\n}\n\n/** The fields failed the user type's schema. */\nexport class UserInvalidError extends JanusError {\n\toverride name = 'UserInvalidError';\n\toverride readonly code = 'USER_INVALID' as const;\n}\n\n/** A password too short, credentials that do not match, or a hash format nobody reads. */\nexport class CredentialError extends JanusError {\n\toverride name = 'CredentialError';\n\toverride readonly code: JanusErrorCode;\n\n\tconstructor(\n\t\tcode: Extract<\n\t\t\tJanusErrorCode,\n\t\t\t'PASSWORD_TOO_SHORT' | 'CREDENTIALS_INVALID' | 'HASH_UNSUPPORTED'\n\t\t>,\n\t\tmessage: string,\n\t\toptions?: JanusErrorOptions,\n\t) {\n\t\tsuper(message, options);\n\t\tthis.code = code;\n\t}\n}\n\n/** The user is inactive, and the password given was the right one. */\nexport class UserInactiveError extends JanusError {\n\toverride name = 'UserInactiveError';\n\toverride readonly code = 'USER_INACTIVE' as const;\n}\n\n/** A one-time token that is unknown, already spent, lapsed, or sent to an e-mail the user no longer has. */\nexport class TokenError extends JanusError {\n\toverride name = 'TokenError';\n\toverride readonly code: JanusErrorCode;\n\n\tconstructor(\n\t\tcode: Extract<\n\t\t\tJanusErrorCode,\n\t\t\t'TOKEN_UNKNOWN' | 'TOKEN_SPENT' | 'TOKEN_EXPIRED' | 'TOKEN_STALE'\n\t\t>,\n\t\tmessage: string,\n\t\toptions?: JanusErrorOptions,\n\t) {\n\t\tsuper(message, options);\n\t\tthis.code = code;\n\t}\n}\n\n/** A cursor this store did not mint, or one for another ordering. */\nexport class InvalidCursorError extends JanusError {\n\toverride name = 'InvalidCursorError';\n\toverride readonly code = 'INVALID_CURSOR' as const;\n}\n\n/** The wired store does not implement the optional capability asked for. */\nexport class UnsupportedError extends JanusError {\n\toverride name = 'UnsupportedError';\n\toverride readonly code = 'UNSUPPORTED' as const;\n}\n\n/** A permission check walked deeper than `maxDepth` without an answer. */\nexport class PermissionDepthError extends JanusError {\n\toverride name = 'PermissionDepthError';\n\toverride readonly code = 'PERMISSION_DEPTH' as const;\n}\n",
|
|
6
|
-
"/**\n * The id every decision in this package hangs on: a user's `id`, as `janus()`\n * mints it.\n *\n * In Ory, the equality between a Kratos identity id and Keto's `subject_id` is\n * a comment and a convention, restated in three repositories and enforced\n * nowhere. Here a user **is** a subject — see {@link subjectOf} — and that\n * shared vocabulary is the whole reason users and permissions are one package\n * rather than two: two packages would make this contract implicit, and an\n * implicit contract between independently versioned packages breaks silently.\n *\n * Opaque: compared with `===`, carried in a URL, logged, never parsed.\n */\nexport type SubjectId = string;\n\n/**\n * Something a permission is about or held by — a user, a record, a team — by\n * its type and its id.\n *\n * **Typed, unlike Keto's subjects.** One application has patients and staff,\n * and an object can hold a relation too (a record's team), so an id alone does\n * not say who. `type` is a user type of `janus()` or an object type of the\n * permission model, and the same word as a user's own `type`.\n */\nexport interface Entity {\n\treadonly type: string;\n\treadonly id: string;\n}\n\n/**\n * Everyone who holds `relation` on an entity — `team:t1#member` — which is how\n * a permission is inherited rather than granted.\n *\n * A tuple whose subject is a subject set lets a whole group be given access\n * with one write: granting a record's `viewer` to `team:t1#member` means every\n * member of that team can view it, and no member holds a tuple on the record.\n */\nexport interface SubjectSet extends Entity {\n\treadonly relation: string;\n}\n\n/**\n * Who a tuple is about: one entity, or everyone who holds a relation on one.\n * {@link isSubjectSet} tells them apart.\n */\nexport type Subject = Entity | SubjectSet;\n\n/**\n * A relation tuple: `subject` holds `relation` on `object`.\n *\n * `relation` is Zanzibar's word, and a tuple is Zanzibar's unit. What is not\n * carried over is Keto's `namespace`/`object` pair: here the object is an\n * {@link Entity}, with the `type` and `id` a user already has.\n */\nexport interface RelationTuple {\n\treadonly object: Entity;\n\treadonly relation: string;\n\treadonly subject: Subject;\n}\n\n/**\n * Whether this subject is a set rather than one entity. Narrows.\n *\n * It reads the shape, so pass a user through {@link subjectOf} first: a user\n * whose fields include a string `relation` would otherwise read as a set.\n */\nexport function isSubjectSet(subject: Subject): subject is SubjectSet {\n\treturn typeof (subject as { relation?: unknown }).relation === 'string';\n}\n\n/**\n * The subject a user is: their `type` and `id`, and nothing else.\n *\n * One line, and the entire join between the two sides of the package. It\n * takes the narrowest shape it reads, so the permissions side never has to\n * know what a user is, and it copies those two fields so none of the user's\n * own fields ever reaches a tuple.\n */\nexport function subjectOf(user: {\n\treadonly type: string;\n\treadonly id: SubjectId;\n}): Entity {\n\treturn { type: user.type, id: user.id };\n}\n",
|
|
7
|
-
"import {\n\ttype Entity,\n\tisSubjectSet,\n\ttype RelationTuple,\n\ttype Subject,\n} from './subject';\n\n/**\n * Zanzibar's notation, with typed subjects:\n *\n * ```\n * record:r1#viewer@staff:u1\n * record:r1#viewer@team:t1#member\n * ```\n *\n * It exists for **messages, logs and documentation**, not as a wire format:\n * nothing in this package parses a tuple off the network, and a tuple that\n * crosses a boundary crosses it as an object. Keeping the notation is worth it\n * because every piece of Zanzibar writing uses it, so an error message in it is\n * an error message a reader has already learned to read.\n *\n * A type holds no `:`, and no part holds `@`, `#` or a parenthesis, so a\n * subject set needs no parentheses: after the `@`, a `#` can only begin its\n * relation. {@link parseTuple} refuses a string where that would not hold,\n * rather than reading it two ways.\n */\nexport function formatEntity(entity: Entity): string {\n\treturn `${entity.type}:${entity.id}`;\n}\n\n/** One subject: `staff:u1`, or `team:t1#member`. */\nexport function formatSubject(subject: Subject): string {\n\treturn isSubjectSet(subject)\n\t\t? `${formatEntity(subject)}#${subject.relation}`\n\t\t: formatEntity(subject);\n}\n\n/** One relation tuple, in the notation above. */\nexport function formatTuple(tuple: RelationTuple): string {\n\treturn `${formatEntity(tuple.object)}#${tuple.relation}@${formatSubject(tuple.subject)}`;\n}\n\nconst TYPE = '([^:@#()]+)';\nconst ID = '([^@#()]+)';\nconst RELATION = '([^:@#()]+)';\n\nconst TUPLE = new RegExp(`^${TYPE}:${ID}#${RELATION}@(.+)$`);\nconst SUBJECT = new RegExp(`^${TYPE}:${ID}(?:#${RELATION})?$`);\n\n/**\n * Reads a tuple back. Refuses, with a `TypeError`, a string that is not one —\n * a permission question built from a malformed string is a question whose\n * answer means nothing, and no request is behind it: the string came from a\n * developer's own code.\n */\nexport function parseTuple(text: string): RelationTuple {\n\tconst match = TUPLE.exec(text);\n\n\tif (!match) {\n\t\tthrow new TypeError(\n\t\t\t`parseTuple: \"${text}\" is not a relation tuple; expected type:id#relation@subject`,\n\t\t);\n\t}\n\n\tconst [, type, id, relation, subject] = match as unknown as [\n\t\tstring,\n\t\tstring,\n\t\tstring,\n\t\tstring,\n\t\tstring,\n\t];\n\n\treturn { object: { type, id }, relation, subject: parseSubject(subject) };\n}\n\n
|
|
6
|
+
"/**\n * The id every decision in this package hangs on: a user's `id`, as `janus()`\n * mints it.\n *\n * In Ory, the equality between a Kratos identity id and Keto's `subject_id` is\n * a comment and a convention, restated in three repositories and enforced\n * nowhere. Here a user **is** a subject — see {@link subjectOf} — and that\n * shared vocabulary is the whole reason users and permissions are one package\n * rather than two: two packages would make this contract implicit, and an\n * implicit contract between independently versioned packages breaks silently.\n *\n * Opaque: compared with `===`, carried in a URL, logged, never parsed.\n */\nexport type SubjectId = string;\n\n/**\n * Something a permission is about or held by — a user, a record, a team — by\n * its type and its id.\n *\n * **Typed, unlike Keto's subjects.** One application has patients and staff,\n * and an object can hold a relation too (a record's team), so an id alone does\n * not say who. `type` is a user type of `janus()` or an object type of the\n * permission model, and the same word as a user's own `type`.\n */\nexport interface Entity {\n\treadonly type: string;\n\treadonly id: string;\n}\n\n/**\n * Everyone who holds `relation` on an entity — `team:t1#member` — which is how\n * a permission is inherited rather than granted.\n *\n * A tuple whose subject is a subject set lets a whole group be given access\n * with one write: granting a record's `viewer` to `team:t1#member` means every\n * member of that team can view it, and no member holds a tuple on the record.\n */\nexport interface SubjectSet extends Entity {\n\treadonly relation: string;\n}\n\n/**\n * Who a tuple is about: one entity, or everyone who holds a relation on one.\n * {@link isSubjectSet} tells them apart.\n */\nexport type Subject = Entity | SubjectSet;\n\n/**\n * A relation tuple: `subject` holds `relation` on `object`.\n *\n * `relation` is Zanzibar's word, and a tuple is Zanzibar's unit. What is not\n * carried over is Keto's `namespace`/`object` pair: here the object is an\n * {@link Entity}, with the `type` and `id` a user already has.\n */\nexport interface RelationTuple {\n\treadonly object: Entity;\n\treadonly relation: string;\n\treadonly subject: Subject;\n}\n\n/**\n * Whether this subject is a set rather than one entity. Narrows.\n *\n * It reads the shape, so pass a user through {@link subjectOf} first: a user\n * whose fields include a string `relation` would otherwise read as a set.\n */\nexport function isSubjectSet(subject: Subject): subject is SubjectSet {\n\treturn typeof (subject as { relation?: unknown }).relation === 'string';\n}\n\n/**\n * The subject a user is: their `type` and `id`, and nothing else.\n *\n * One line, and the entire join between the two sides of the package. It\n * takes the narrowest shape it reads, so the permissions side never has to\n * know what a user is, and it copies those two fields so none of the user's\n * own fields ever reaches a tuple.\n */\nexport function subjectOf(user: {\n\treadonly type: string;\n\treadonly id: SubjectId;\n}): Entity {\n\treturn { type: user.type, id: user.id };\n}\n\n/**\n * The mark `setOf()` puts on a set: a symbol property, so it survives a spread\n * and `Object.assign`, and no value read from a database or from JSON can\n * carry it. Registered with `Symbol.for`, so two copies of this module — two\n * bundles, two entry points — still agree on it.\n */\nconst SET_OF: unique symbol = Symbol.for('@nxgt/janus/setOf');\n\n/**\n * A subject set `setOf()` made: everyone who holds `relation` on `type:id`.\n *\n * Marked, because a user is passed to `can()` and `grant()` as it is, fields\n * flat on it: a user whose fields include `relation` must stay that user, and\n * never read as the set of whoever holds that relation on them. For an object\n * type, `{ type, id, relation }` is a set as written; for a user type — one\n * the model also declares as an object type — only `setOf()` makes one.\n *\n * A spread keeps the mark. `JSON.stringify` and `structuredClone` drop it:\n * through either, a set comes back as a user — call `setOf()` again, or\n * `parseSubject()` on its notation.\n */\nexport type SetOf<\n\tType extends string = string,\n\tRelation extends string = string,\n> = SubjectSet & {\n\treadonly type: Type;\n\treadonly relation: Relation;\n\treadonly [SET_OF]: true;\n};\n\n/**\n * A user or an object, never a set `setOf()` made: where a relation admits\n * `staff` but not `staff#managers`, a set passed there is refused at compile\n * time as it is at run time.\n */\nexport type NotASet = { readonly [SET_OF]?: never };\n\n/**\n * Everyone who holds `relation` on this user or object — `staff:s1#managers`.\n *\n * ```ts\n * await access.grant(record, 'viewers', setOf(ada, 'managers'));\n * ```\n *\n * Copies `type` and `id` only, like {@link subjectOf}: none of a user's own\n * fields reaches a tuple.\n */\nexport function setOf<const Type extends string, const Relation extends string>(\n\tentity: { readonly type: Type; readonly id: SubjectId },\n\trelation: Relation,\n): SetOf<Type, Relation> {\n\tif (\n\t\ttypeof entity !== 'object' ||\n\t\tentity === null ||\n\t\ttypeof entity.type !== 'string' ||\n\t\ttypeof entity.id !== 'string'\n\t) {\n\t\tthrow new TypeError('setOf: pass a user or { type, id }, then a relation');\n\t}\n\tif (typeof relation !== 'string' || relation === '') {\n\t\tthrow new TypeError('setOf: the relation must be a non-empty string');\n\t}\n\treturn Object.freeze({\n\t\ttype: entity.type,\n\t\tid: entity.id,\n\t\trelation,\n\t\t[SET_OF]: true as const,\n\t});\n}\n\n/**\n * Whether `setOf()` made this value, or a spread of one — what `can()` and\n * `grant()` read to tell a set on a user type from a user.\n */\nexport function isSetOf(value: unknown): value is SetOf {\n\treturn (\n\t\ttypeof value === 'object' &&\n\t\tvalue !== null &&\n\t\t(value as { readonly [SET_OF]?: unknown })[SET_OF] === true\n\t);\n}\n",
|
|
7
|
+
"import {\n\ttype Entity,\n\tisSubjectSet,\n\ttype RelationTuple,\n\ttype SetOf,\n\ttype Subject,\n\tsetOf,\n} from './subject';\n\n/**\n * Zanzibar's notation, with typed subjects:\n *\n * ```\n * record:r1#viewer@staff:u1\n * record:r1#viewer@team:t1#member\n * ```\n *\n * It exists for **messages, logs and documentation**, not as a wire format:\n * nothing in this package parses a tuple off the network, and a tuple that\n * crosses a boundary crosses it as an object. Keeping the notation is worth it\n * because every piece of Zanzibar writing uses it, so an error message in it is\n * an error message a reader has already learned to read.\n *\n * A type holds no `:`, and no part holds `@`, `#` or a parenthesis, so a\n * subject set needs no parentheses: after the `@`, a `#` can only begin its\n * relation. {@link parseTuple} refuses a string where that would not hold,\n * rather than reading it two ways.\n */\nexport function formatEntity(entity: Entity): string {\n\treturn `${entity.type}:${entity.id}`;\n}\n\n/** One subject: `staff:u1`, or `team:t1#member`. */\nexport function formatSubject(subject: Subject): string {\n\treturn isSubjectSet(subject)\n\t\t? `${formatEntity(subject)}#${subject.relation}`\n\t\t: formatEntity(subject);\n}\n\n/** One relation tuple, in the notation above. */\nexport function formatTuple(tuple: RelationTuple): string {\n\treturn `${formatEntity(tuple.object)}#${tuple.relation}@${formatSubject(tuple.subject)}`;\n}\n\nconst TYPE = '([^:@#()]+)';\nconst ID = '([^@#()]+)';\nconst RELATION = '([^:@#()]+)';\n\nconst TUPLE = new RegExp(`^${TYPE}:${ID}#${RELATION}@(.+)$`);\nconst SUBJECT = new RegExp(`^${TYPE}:${ID}(?:#${RELATION})?$`);\n\n/**\n * Reads a tuple back. Refuses, with a `TypeError`, a string that is not one —\n * a permission question built from a malformed string is a question whose\n * answer means nothing, and no request is behind it: the string came from a\n * developer's own code.\n */\nexport function parseTuple(text: string): RelationTuple {\n\tconst match = TUPLE.exec(text);\n\n\tif (!match) {\n\t\tthrow new TypeError(\n\t\t\t`parseTuple: \"${text}\" is not a relation tuple; expected type:id#relation@subject`,\n\t\t);\n\t}\n\n\tconst [, type, id, relation, subject] = match as unknown as [\n\t\tstring,\n\t\tstring,\n\t\tstring,\n\t\tstring,\n\t\tstring,\n\t];\n\n\treturn { object: { type, id }, relation, subject: parseSubject(subject) };\n}\n\n/**\n * Reads one subject back: `staff:u1`, or `team:t1#member`. A set is answered\n * as `setOf()` makes it, so `can()` and `grant()` read it as the set it names\n * on a user type too.\n */\nexport function parseSubject(text: string): Entity | SetOf {\n\tconst match = SUBJECT.exec(text);\n\n\tif (!match) {\n\t\tthrow new TypeError(\n\t\t\t`parseSubject: \"${text}\" is not a subject; expected type:id, or type:id#relation for a subject set`,\n\t\t);\n\t}\n\n\tconst [, type, id, relation] = match as unknown as [\n\t\tstring,\n\t\tstring,\n\t\tstring,\n\t\tstring | undefined,\n\t];\n\n\treturn relation === undefined ? { type, id } : setOf({ type, id }, relation);\n}\n"
|
|
8
8
|
],
|
|
9
|
-
"mappings": ";AAkLO,MAAM,oBAAmB,MAAM;AAAA,EAiBrC,WAAW,CAAC,SAAiB,SAA6B;AAAA,IACzD,MAAM,SAAS,EAAE,OAAO,SAAS,MAAM,CAAC;AAAA,IAjBhC,YAAO;AAAA,IACP,YAAuB;AAAA,IAiB/B,KAAK,SAAS,SAAS;AAAA,IACvB,KAAK,WAAW,SAAS;AAAA,IACzB,KAAK,QAAQ,SAAS;AAAA,IACtB,KAAK,SAAS,SAAS;AAAA,IACvB,KAAK,aAAa,SAAS;AAAA,IAC3B,KAAK,kBAAkB,SAAS;AAAA,IAChC,KAAK,gBAAgB,SAAS;AAAA,IAC9B,KAAK,SAAS,SAAS;AAAA,IACvB,KAAK,YAAY,SAAS;AAAA,IAC1B,KAAK,YAAY,SAAS;AAAA,IAC1B,KAAK,OAAO,SAAS;AAAA,IACrB,KAAK,aAAa,SAAS;AAAA,IAC3B,KAAK,WAAW,SAAS;AAAA;AAE3B;AAAA;AAUO,MAAM,sBAAqB,YAAW;AAAA;AAAA;AAAA,IACnC,YAAO;AAAA,IACE,YAAO;AAAA;AAC1B;AAAA;AASO,MAAM,uBAAsB,YAAW;AAAA,EAK7C,WAAW,CACV,IACA,SACA,SACC;AAAA,IACD,MAAM,SAAS,OAAO;AAAA,IATd,YAAO;AAAA,IAUf,KAAK,KAAK;AAAA,IACV,KAAK,OAAO,OAAO,UAAU,gBAAgB;AAAA;AAE/C;AAAA;AAGO,MAAM,uBAAsB,YAAW;AAAA;AAAA;AAAA,IACpC,YAAO;AAAA,IACE,YAAO;AAAA;AAC1B;AAAA;AAGO,MAAM,0BAAyB,YAAW;AAAA;AAAA;AAAA,IACvC,YAAO;AAAA,IACE,YAAO;AAAA;AAC1B;AAAA;AAGO,MAAM,yBAAwB,YAAW;AAAA,EAI/C,WAAW,CACV,MAIA,SACA,SACC;AAAA,IACD,MAAM,SAAS,OAAO;AAAA,IAXd,YAAO;AAAA,IAYf,KAAK,OAAO;AAAA;AAEd;AAAA;AAGO,MAAM,2BAA0B,YAAW;AAAA;AAAA;AAAA,IACxC,YAAO;AAAA,IACE,YAAO;AAAA;AAC1B;AAAA;AAGO,MAAM,oBAAmB,YAAW;AAAA,EAI1C,WAAW,CACV,MAIA,SACA,SACC;AAAA,IACD,MAAM,SAAS,OAAO;AAAA,IAXd,YAAO;AAAA,IAYf,KAAK,OAAO;AAAA;AAEd;AAAA;AAGO,MAAM,4BAA2B,YAAW;AAAA;AAAA;AAAA,IACzC,YAAO;AAAA,IACE,YAAO;AAAA;AAC1B;AAAA;AAGO,MAAM,0BAAyB,YAAW;AAAA;AAAA;AAAA,IACvC,YAAO;AAAA,IACE,YAAO;AAAA;AAC1B;AAAA;AAGO,MAAM,8BAA6B,YAAW;AAAA;AAAA;AAAA,IAC3C,YAAO;AAAA,IACE,YAAO;AAAA;AAC1B;;;AC7PO,SAAS,aAAY,CAAC,SAAyC;AAAA,EACrE,OAAO,OAAQ,QAAmC,aAAa;AAAA;AAWzD,SAAS,UAAS,CAAC,MAGf;AAAA,EACV,OAAO,EAAE,MAAM,KAAK,MAAM,IAAI,KAAK,GAAG;AAAA;;;
|
|
10
|
-
"debugId": "
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "49525DE3EB2EE26A64756E2164756E21",
|
|
11
11
|
"names": []
|
|
12
12
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
StoreConflict2,
|
|
3
3
|
NotFoundError2
|
|
4
|
-
} from "./index-
|
|
4
|
+
} from "./index-qwfkhqkk.js";
|
|
5
5
|
|
|
6
6
|
// src/ids/id.ts
|
|
7
7
|
var HEX = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
|
|
@@ -155,6 +155,7 @@ function applyPatch(stored, patch) {
|
|
|
155
155
|
fields: patch.fields ?? stored.fields,
|
|
156
156
|
logins: patch.logins ?? stored.logins,
|
|
157
157
|
password: patch.password === undefined ? stored.password : patch.password,
|
|
158
|
+
secondFactor: patch.secondFactor === undefined ? stored.secondFactor : patch.secondFactor,
|
|
158
159
|
emailVerifiedAt: patch.emailVerifiedAt === undefined ? stored.emailVerifiedAt : patch.emailVerifiedAt,
|
|
159
160
|
version: stored.version + 1,
|
|
160
161
|
updatedAt: patch.updatedAt
|
|
@@ -250,6 +251,16 @@ function memoryTokenStore() {
|
|
|
250
251
|
}
|
|
251
252
|
return before;
|
|
252
253
|
},
|
|
254
|
+
async countAttempt(tokenHash, kind) {
|
|
255
|
+
const stored = byTokenHash.get(tokenHash);
|
|
256
|
+
if (stored === undefined || stored.kind !== kind)
|
|
257
|
+
return null;
|
|
258
|
+
if (stored.spentAt !== null)
|
|
259
|
+
return copy(stored);
|
|
260
|
+
const counted = { ...stored, attempts: stored.attempts + 1 };
|
|
261
|
+
byTokenHash.set(tokenHash, counted);
|
|
262
|
+
return copy(counted);
|
|
263
|
+
},
|
|
253
264
|
async deleteUserTokens(userId) {
|
|
254
265
|
let deleted = 0;
|
|
255
266
|
for (const [tokenHash, stored] of byTokenHash) {
|
|
@@ -265,5 +276,5 @@ function memoryTokenStore() {
|
|
|
265
276
|
|
|
266
277
|
export { mintId2, isId2, mintedAt2, createMemoryStores2 };
|
|
267
278
|
|
|
268
|
-
//# debugId=
|
|
269
|
-
//# sourceMappingURL=index-
|
|
279
|
+
//# debugId=A6E6F8D3147D2C5864756E2164756E21
|
|
280
|
+
//# sourceMappingURL=index-thtyq7a9.js.map
|