@nxgt/janus 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -24
- package/dist/auth/config.d.ts +3 -3
- package/dist/auth/config.d.ts.map +1 -1
- package/dist/auth/context.d.ts +1 -1
- package/dist/auth/context.d.ts.map +1 -1
- package/dist/auth/janus.d.ts +2 -2
- package/dist/auth/janus.d.ts.map +1 -1
- package/dist/auth/outage.d.ts +11 -17
- package/dist/auth/outage.d.ts.map +1 -1
- package/dist/auth/port/types.d.ts +6 -6
- package/dist/chunks/index-6p56fpbe.js.map +2 -2
- package/dist/chunks/{index-6dytvy3h.js → index-hm4v76kd.js} +7 -29
- package/dist/chunks/index-hm4v76kd.js.map +11 -0
- package/dist/conformance/assert.d.ts +1 -1
- package/dist/conformance/index.js +3 -3
- package/dist/conformance/index.js.map +5 -5
- package/dist/errors/janus-error.d.ts +1 -1
- package/dist/index.d.ts +9 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -12
- package/dist/index.js.map +7 -6
- package/dist/permissions/index.js +3 -3
- package/dist/permissions/index.js.map +4 -4
- package/dist/stores/guard.d.ts +24 -0
- package/dist/stores/guard.d.ts.map +1 -0
- package/dist/subjects/subject.d.ts +2 -2
- package/docs/README.md +19 -3
- package/docs/guide/adapters.md +4 -4
- package/docs/guide/errors.md +3 -3
- package/docs/guide/passwords.md +1 -1
- package/docs/guide/permissions.md +7 -2
- package/docs/guide/sessions.md +2 -2
- package/docs/guide/users.md +8 -7
- package/docs/guide/vocabulary.md +70 -3
- package/docs/roadmap.md +4 -4
- package/docs/troubleshooting.md +9 -9
- package/package.json +2 -2
- package/dist/chunks/index-6dytvy3h.js.map +0 -11
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @nxgt/janus
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
process, your database, behind a port you can implement.
|
|
3
|
+
Identities and permissions as an **embeddable** TypeScript library: your
|
|
4
|
+
process, your database, behind a port you can implement. Use **identities**
|
|
5
|
+
alone, **permissions** alone, or both — see [Three ways to use it](#three-ways-to-use-it).
|
|
5
6
|
|
|
6
7
|
```ts
|
|
7
8
|
import { z } from 'zod';
|
|
@@ -18,11 +19,7 @@ const { user, token } = await auth.signUp({ email, name, password });
|
|
|
18
19
|
const current = await auth.authenticate(request); // { user, session, token, renewed } | null
|
|
19
20
|
```
|
|
20
21
|
|
|
21
|
-
> **0.x.** A minor version may still change the surface
|
|
22
|
-
> permissions — errors, subjects, pagination, time, ids. `./permissions` is the
|
|
23
|
-
> ReBAC engine. `./conformance` is the suite an adapter runs. A subpath appears in `exports`
|
|
24
|
-
> only once it exports something you should call, because a published entry
|
|
25
|
-
> point is a promise.
|
|
22
|
+
> **0.x.** A minor version may still change the surface; the changelog says how.
|
|
26
23
|
|
|
27
24
|
## Install
|
|
28
25
|
|
|
@@ -39,9 +36,101 @@ supported.
|
|
|
39
36
|
|
|
40
37
|
| Import | What it holds |
|
|
41
38
|
| --- | --- |
|
|
42
|
-
| `@nxgt/janus` | `janus()`, the
|
|
43
|
-
| `@nxgt/janus/permissions` | `defineModel`, `fromField`, `when`, `permissions()` — `can`, `list`, `grant`, `revoke` — the
|
|
44
|
-
| `@nxgt/janus/conformance` |
|
|
39
|
+
| `@nxgt/janus` | **Identities**: `janus()`, the identity stores' port and its in-memory reference (`createMemoryStores`), the hashers. And the **shared vocabulary**: errors, subjects and the tuple notation, ids, pagination, time |
|
|
40
|
+
| `@nxgt/janus/permissions` | **Permissions**: `defineModel`, `fromField`, `when`, `permissions()` — `can`, `list`, `grant`, `revoke` — the relation store's port and its in-memory reference (`createMemoryRelations`) |
|
|
41
|
+
| `@nxgt/janus/conformance` | **For adapters**: the suites a store runs — `describeJanusStores`, `describeRelationStores` — their cases as data, and the reference harnesses |
|
|
42
|
+
|
|
43
|
+
A subpath appears in `exports` only once it exports something you should call:
|
|
44
|
+
a published entry point is a promise.
|
|
45
|
+
|
|
46
|
+
## Three ways to use it
|
|
47
|
+
|
|
48
|
+
Janus has two sides. **Identities** answers *who is this?* — users, their
|
|
49
|
+
logins and passwords, sessions, one-time tokens. **Permissions** answers *may
|
|
50
|
+
they?* — a model, the tuples stored against it, and `can`. Each side is usable
|
|
51
|
+
alone, and neither loads the other's code: a spec reads the import graph of
|
|
52
|
+
each entry point and fails if one reaches into the other.
|
|
53
|
+
|
|
54
|
+
**Identities only** — users, logins, passwords, sessions, one-time tokens:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { z } from 'zod';
|
|
58
|
+
import { createMemoryStores, janus, scryptHasher } from '@nxgt/janus';
|
|
59
|
+
|
|
60
|
+
const auth = janus({
|
|
61
|
+
user: z.object({ email: z.email() }),
|
|
62
|
+
password: { login: 'email' },
|
|
63
|
+
store: createMemoryStores(),
|
|
64
|
+
hasher: scryptHasher(),
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Permissions only** — your users live elsewhere; name their types as the
|
|
69
|
+
subjects:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import {
|
|
73
|
+
createMemoryRelations,
|
|
74
|
+
defineModel,
|
|
75
|
+
permissions,
|
|
76
|
+
} from '@nxgt/janus/permissions';
|
|
77
|
+
|
|
78
|
+
const access = permissions({
|
|
79
|
+
model: defineModel({
|
|
80
|
+
subjects: ['user'],
|
|
81
|
+
types: {
|
|
82
|
+
document: {
|
|
83
|
+
relations: { owner: ['user'], viewer: ['user'] },
|
|
84
|
+
permissions: { view: ['owner', 'viewer'] },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
store: createMemoryRelations(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await access.grant({ type: 'document', id: 'd1' }, 'viewer', { type: 'user', id: 'u1' });
|
|
92
|
+
await access.can({ type: 'user', id: 'u1' }, 'view', { type: 'document', id: 'd1' }); // true
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Both** — the user types become the subjects, and deleting a user deletes
|
|
96
|
+
every tuple naming them:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { z } from 'zod';
|
|
100
|
+
import { createMemoryStores, janus, scryptHasher } from '@nxgt/janus';
|
|
101
|
+
import {
|
|
102
|
+
createMemoryRelations,
|
|
103
|
+
defineModel,
|
|
104
|
+
permissions,
|
|
105
|
+
} from '@nxgt/janus/permissions';
|
|
106
|
+
|
|
107
|
+
const relations = createMemoryRelations();
|
|
108
|
+
|
|
109
|
+
const auth = janus({
|
|
110
|
+
user: z.object({ email: z.email() }),
|
|
111
|
+
password: { login: 'email' },
|
|
112
|
+
store: createMemoryStores(),
|
|
113
|
+
relations,
|
|
114
|
+
hasher: scryptHasher(),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const access = permissions({
|
|
118
|
+
model: defineModel({
|
|
119
|
+
subjects: auth.types,
|
|
120
|
+
types: {
|
|
121
|
+
document: {
|
|
122
|
+
relations: { owner: ['user'] },
|
|
123
|
+
permissions: { view: ['owner'] },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
store: relations,
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The words used throughout — side, subject, tuple, identity stores, relation
|
|
132
|
+
store, adapter — are defined once, in
|
|
133
|
+
[the shared vocabulary](docs/guide/vocabulary.md#words).
|
|
45
134
|
|
|
46
135
|
## The one rule
|
|
47
136
|
|
|
@@ -50,8 +139,8 @@ supported.
|
|
|
50
139
|
Everything else in this package is downstream of that sentence. A store that
|
|
51
140
|
cannot answer — a refused connection, a timeout, a primary stepping down, a bug
|
|
52
141
|
in the adapter — **throws**, and a caller answers 503. Mapping that to a 404, to
|
|
53
|
-
`null` or to `false` turns an outage into a silent lockout:
|
|
54
|
-
|
|
142
|
+
`null` or to `false` turns an outage into a silent lockout: every user is told
|
|
143
|
+
they do not exist. That has been measured twice in this organisation,
|
|
55
144
|
two days apart, which is why it is a term of the port here rather than a note in
|
|
56
145
|
the documentation.
|
|
57
146
|
|
|
@@ -132,7 +221,7 @@ types are `Entity`, `SubjectSet`, `Subject` (either) and `RelationTuple`.
|
|
|
132
221
|
In Ory, the equality between a Kratos identity id and Keto's `subject_id` is a
|
|
133
222
|
comment and a convention, restated in three repositories and enforced nowhere.
|
|
134
223
|
Here it is a type and a one-line function — and that shared vocabulary is the
|
|
135
|
-
reason
|
|
224
|
+
reason identities and permissions are one package rather than two.
|
|
136
225
|
|
|
137
226
|
**Subjects are typed**, unlike Keto's: `{ type, id }` for one entity, and
|
|
138
227
|
`{ type, id, relation }` for a subject set. One application has patients and
|
|
@@ -183,13 +272,13 @@ is the loop.
|
|
|
183
272
|
`fixedClock` is **shipped, not test-only** — testing session expiry needs it, and
|
|
184
273
|
so do your own tests.
|
|
185
274
|
|
|
186
|
-
###
|
|
275
|
+
### Identities — `janus()`
|
|
187
276
|
|
|
188
277
|
```ts
|
|
189
278
|
import { z } from 'zod';
|
|
190
279
|
import { createMemoryStores, janus, scryptHasher } from '@nxgt/janus';
|
|
191
280
|
|
|
192
|
-
// One
|
|
281
|
+
// One user type
|
|
193
282
|
const auth = janus({
|
|
194
283
|
user: z.object({ email: z.email(), name: z.string() }),
|
|
195
284
|
password: { login: 'email' },
|
|
@@ -206,7 +295,7 @@ await auth.verifyEmail.confirm(token);
|
|
|
206
295
|
await auth.resetPassword.request(email); // … | null
|
|
207
296
|
await auth.resetPassword.confirm(token, newPassword);
|
|
208
297
|
|
|
209
|
-
// Several
|
|
298
|
+
// Several user types
|
|
210
299
|
const clinic = janus({
|
|
211
300
|
users: {
|
|
212
301
|
patient: { schema: Patient, password: { login: 'email' } },
|
|
@@ -239,8 +328,8 @@ else reaches the store and is asynchronous.
|
|
|
239
328
|
schema producing a `Date` is refused at compile time.
|
|
240
329
|
- **Several user types** live in one instance: `auth.patient.*`, `auth.staff.*`,
|
|
241
330
|
and one `authenticate` whose answer is a union narrowed by `user.type`. A
|
|
242
|
-
login is unique **per type**: the same e-mail may hold a patient
|
|
243
|
-
|
|
331
|
+
login is unique **per type**: the same e-mail may hold a patient user and a
|
|
332
|
+
staff user.
|
|
244
333
|
- **`password.login`** names a top-level, required string field. A typo is a
|
|
245
334
|
compile error on `login`, and the message lists the fields you could have
|
|
246
335
|
meant. It is normalised with `'lowercaseTrim'` unless you say otherwise.
|
|
@@ -407,14 +496,14 @@ There are 37 cases. They cover:
|
|
|
407
496
|
|
|
408
497
|
The suite imports no test framework and no assertion library. It runs under
|
|
409
498
|
`bun test`, vitest and jest. Its cases are also exported as data
|
|
410
|
-
(`allCases`, or by
|
|
499
|
+
(`allCases`, or by store: `userStoreCases`, `sessionStoreCases`,
|
|
411
500
|
`tokenStoreCases`, `outageCases`), with `runCase` to run one without any
|
|
412
501
|
runner. `skip: { [caseId]: reason }` skips a case and reports why;
|
|
413
502
|
`SKIP_REASONS` holds the reasons the suite gives itself.
|
|
414
503
|
|
|
415
504
|
**`faults` is optional, and its absence is reported, never passed over.**
|
|
416
|
-
Without it, the outage cases are skipped under the reason *"
|
|
417
|
-
invariant is not proven for this adapter"*. Make your database fail the way it
|
|
505
|
+
Without it, the outage cases are skipped under the reason *"faults not
|
|
506
|
+
provided: the outage invariant is not proven for this adapter"*. Make your database fail the way it
|
|
418
507
|
really fails — for MongoDB, the `failCommand` failpoint with code 91. A wrapper
|
|
419
508
|
that throws in front of your adapter proves the wrapper, not the adapter.
|
|
420
509
|
Fail **only the method named**: `outage.write` reads the store back afterwards,
|
|
@@ -535,9 +624,9 @@ one gap, named.**
|
|
|
535
624
|
The lists are typechecked and never run, with one `@ts-expect-error` per
|
|
536
625
|
mistake beside the shapes that must keep compiling:
|
|
537
626
|
`test/types/refusals.ts` (fourteen, on the shared vocabulary),
|
|
538
|
-
`test/types/port.ts` (fifteen, on the
|
|
539
|
-
implementing it), `test/types/auth.ts` (twenty, on
|
|
540
|
-
of the application) and `test/types/permissions.ts` (twenty-six, on the
|
|
627
|
+
`test/types/port.ts` (fifteen, on the identity stores' port, from the point
|
|
628
|
+
of view of the person implementing it), `test/types/auth.ts` (twenty, on
|
|
629
|
+
`janus()`, from the point of view of the application) and `test/types/permissions.ts` (twenty-six, on the
|
|
541
630
|
permission model and the questions asked of it). The rule
|
|
542
631
|
comes from `nxgt-data`, and so does the reason to
|
|
543
632
|
distrust the claim without the files: when it was last measured on
|
package/dist/auth/config.d.ts
CHANGED
|
@@ -81,7 +81,7 @@ export interface SessionConfig {
|
|
|
81
81
|
*/
|
|
82
82
|
readonly renewAfter?: Duration | false;
|
|
83
83
|
}
|
|
84
|
-
/** One
|
|
84
|
+
/** One user type: its schema, and how it signs in. */
|
|
85
85
|
export interface UserTypeConfig {
|
|
86
86
|
readonly schema: UserSchema;
|
|
87
87
|
readonly password?: PasswordConfig;
|
|
@@ -138,12 +138,12 @@ interface SharedConfig {
|
|
|
138
138
|
readonly resetPassword?: Duration;
|
|
139
139
|
};
|
|
140
140
|
}
|
|
141
|
-
/** An application with one
|
|
141
|
+
/** An application with one user type: `user` is its schema. */
|
|
142
142
|
export interface SingleTypeConfig extends SharedConfig, Omit<UserTypeConfig, 'schema'> {
|
|
143
143
|
readonly user: UserSchema;
|
|
144
144
|
readonly users?: never;
|
|
145
145
|
}
|
|
146
|
-
/** An application with several
|
|
146
|
+
/** An application with several user types — patients and staff. */
|
|
147
147
|
export interface MultiTypeConfig extends SharedConfig {
|
|
148
148
|
readonly users: {
|
|
149
149
|
readonly [type: string]: UserTypeConfig;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/auth/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,KAAK,QAAQ,EAAiB,MAAM,kBAAkB,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;;GAMG;AACH,MAAM,MAAM,SAAS,GAClB,MAAM,GACN,WAAW,GACX,eAAe,GACf,mBAAmB,GACnB,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC;AAE/B;;;;;;;GAOG;AACH,MAAM,MAAM,UAAU,GACnB,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,SAAS,UAAU,EAAE,GACrB;IAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAA;CAAE,CAAC;AAEtD;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,gBAAgB,CACxC,OAAO,EACP;IAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAA;CAAE,CAClD,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC9B,iFAAiF;IACjF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACtD;;;;;OAKG;IACH,WAAW,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CACpC;AAED,kCAAkC;AAClC,MAAM,WAAW,cAAc;IAC9B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,qCAAqC;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,mCAAmC;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,wDAAwD;AACxD,MAAM,WAAW,aAAa;IAC7B,0BAA0B;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC;CACvC;AAED,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/auth/config.ts"],"names":[],"mappings":"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"}
|
package/dist/auth/context.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { JanusStores, JsonObject, StoreCapabilities, UserPatch, UserRecord
|
|
|
13
13
|
import type { User, UserRef, WriteOptions } from './types';
|
|
14
14
|
export interface Context {
|
|
15
15
|
readonly config: ResolvedConfig;
|
|
16
|
-
/** Already guarded by `
|
|
16
|
+
/** Already guarded, by `src/stores/guard.ts`. */
|
|
17
17
|
readonly store: JanusStores;
|
|
18
18
|
/** Already guarded. `null` when no relation store is wired. */
|
|
19
19
|
readonly relations: RelationStore | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/auth/context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAUH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAEN,KAAK,cAAc,EAEnB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,MAAM,UAAU,CAAC;AAElB,OAAO,KAAK,EACX,WAAW,EAEX,UAAU,EACV,iBAAiB,EACjB,SAAS,EACT,UAAU,EACV,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE3D,MAAM,WAAW,OAAO;IACvB,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/auth/context.ts"],"names":[],"mappings":"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"}
|
package/dist/auth/janus.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { type JanusConfig } from './config';
|
|
2
2
|
import type { Checked, Janus } from './types';
|
|
3
3
|
/**
|
|
4
|
-
* Wires
|
|
4
|
+
* Wires the identities side over the stores the application opened. **Synchronous,
|
|
5
5
|
* and does no I/O**: it connects to nothing.
|
|
6
6
|
*
|
|
7
7
|
* ```ts
|
|
8
|
-
* // One
|
|
8
|
+
* // One user type
|
|
9
9
|
* const auth = janus({
|
|
10
10
|
* user: z.object({ email: z.email(), name: z.string() }),
|
|
11
11
|
* password: { login: 'email' },
|
package/dist/auth/janus.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"janus.d.ts","sourceRoot":"","sources":["../../src/auth/janus.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"janus.d.ts","sourceRoot":"","sources":["../../src/auth/janus.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,WAAW,EAAiB,MAAM,UAAU,CAAC;AAK3D,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAG9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,KAAK,CAAC,KAAK,CAAC,CAAC,SAAS,WAAW,EAChD,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GACpB,KAAK,CAAC,CAAC,CAAC,CAyDV"}
|
package/dist/auth/outage.d.ts
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* **The
|
|
3
|
-
* error
|
|
4
|
-
*
|
|
5
|
-
* The transposition of `call.ts` in `nxgt-ory`'s SDK — "the only place a call
|
|
6
|
-
* is unwrapped" — into a core with no status codes. The invariant this whole
|
|
7
|
-
* package is built around is checked by reading this file:
|
|
2
|
+
* **The identity side's two conversions**, and the one place a `null` becomes
|
|
3
|
+
* an error.
|
|
8
4
|
*
|
|
9
5
|
* > An absence is `null`. A failure throws.
|
|
10
6
|
*
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
7
|
+
* {@link guardStores} puts the three identity stores behind the shared guard
|
|
8
|
+
* (`src/stores/guard.ts`), the one place a store's answer is caught.
|
|
9
|
+
* {@link required} turns an absence the caller cannot accept into its refusal,
|
|
10
|
+
* and {@link unlessVersionConflict} absorbs one named conflict and rethrows
|
|
11
|
+
* everything else.
|
|
12
|
+
*
|
|
13
|
+
* `outage.spec.ts` reads every other file of `src/auth/`, `src/permissions/`
|
|
14
|
+
* and `src/stores/` and fails if a `catch` appears in one, because the failure
|
|
15
|
+
* this design exists to prevent is a single careless `catch { return null }`.
|
|
16
16
|
*/
|
|
17
17
|
import { type JanusError } from '../errors/janus-error';
|
|
18
|
-
import type { RelationStore } from '../permissions/port/types';
|
|
19
18
|
import type { JanusStores } from './port/types';
|
|
20
19
|
/**
|
|
21
20
|
* The stores, with every method guarded.
|
|
@@ -34,11 +33,6 @@ import type { JanusStores } from './port/types';
|
|
|
34
33
|
* when absent, so capability detection still reads the truth.
|
|
35
34
|
*/
|
|
36
35
|
export declare function guardStores(stores: JanusStores): JanusStores;
|
|
37
|
-
/**
|
|
38
|
-
* The relation store, guarded the same way: the permission engine's only way
|
|
39
|
-
* to a store, so a failure there is `STORE_FAILED` and never a denial.
|
|
40
|
-
*/
|
|
41
|
-
export declare function guardRelations(store: RelationStore): RelationStore;
|
|
42
36
|
/**
|
|
43
37
|
* A value the caller requires, or the refusal an absence deserves.
|
|
44
38
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"outage.d.ts","sourceRoot":"","sources":["../../src/auth/outage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"outage.d.ts","sourceRoot":"","sources":["../../src/auth/outage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,KAAK,UAAU,EAAiB,MAAM,uBAAuB,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAM5D;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,UAAU,GAAG,CAAC,CAGxE;AAED;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAC5C,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GACf,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAOnB"}
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
* down, a bug in the adapter — **throws**, preferably `StoreFailure` with
|
|
19
19
|
* the driver error as `cause`. Never write `try { … } catch { return null }`
|
|
20
20
|
* in an implementation of this port: that one line turns an outage into
|
|
21
|
-
* "no such
|
|
22
|
-
*
|
|
21
|
+
* "no such user", and every caller above it answers 404 to a user who
|
|
22
|
+
* exists.
|
|
23
23
|
* 2. **`null`, not `undefined`.** `undefined` is what a missing property and a
|
|
24
24
|
* function with no `return` both produce, so a store that forgot to answer
|
|
25
25
|
* would report "not found" by accident. `null` has to be written on purpose.
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
*
|
|
42
42
|
* The seam is **where atomicity is not required**. A user and their password
|
|
43
43
|
* are written together — a sign-up that stores the user and loses the hash is
|
|
44
|
-
*
|
|
44
|
+
* a user nobody can sign in as — so they are one record. A session is derived
|
|
45
45
|
* state: losing them all signs everybody out, which recovers. A one-time token
|
|
46
46
|
* is ephemeral by construction. So `sessions` and `tokens` may live in Redis
|
|
47
47
|
* while `users` lives in MongoDB, with no distributed transaction anywhere.
|
|
@@ -114,7 +114,7 @@ export interface UserRecord {
|
|
|
114
114
|
/**
|
|
115
115
|
* What the user signs in with, **already normalised** by the core. Unique
|
|
116
116
|
* per `type`, and that uniqueness is a constraint the store enforces
|
|
117
|
-
* (rule 3): the same e-mail may hold a patient
|
|
117
|
+
* (rule 3): the same e-mail may hold a patient user and a staff user,
|
|
118
118
|
* and never two of either.
|
|
119
119
|
*/
|
|
120
120
|
readonly logins: readonly string[];
|
|
@@ -138,7 +138,7 @@ export interface UserRecord {
|
|
|
138
138
|
*
|
|
139
139
|
* **A field the patch does not name is left as it is.** There is no full
|
|
140
140
|
* replacement of a record anywhere on this port: in Kratos, an update that
|
|
141
|
-
* omits `state` deactivates the
|
|
141
|
+
* omits `state` deactivates the identity, and an edit form that omits a trait
|
|
142
142
|
* deletes it. A conformance case carries that trap's name.
|
|
143
143
|
*
|
|
144
144
|
* A field the patch *does* name is replaced whole — `fields` and `logins`
|
|
@@ -356,7 +356,7 @@ export interface TokenStore {
|
|
|
356
356
|
* - `null` means no token of this `kind` has this hash — including one the
|
|
357
357
|
* store has already dropped. A token of the other kind is not touched.
|
|
358
358
|
*
|
|
359
|
-
* **One conditional write, never a read followed by a write.** A reset
|
|
359
|
+
* **One conditional write, never a read followed by a write.** A reset token
|
|
360
360
|
* two concurrent requests both redeem is an account takeover: twenty
|
|
361
361
|
* concurrent calls must produce exactly one answer with `spentAt: null`, and
|
|
362
362
|
* the conformance suite runs exactly that. In MongoDB this is one
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/errors/janus-error.ts", "../src/subjects/subject.ts", "../src/subjects/notation.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
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: everybody who has an account is told they do not.\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. A `login` may appear, because the caller just sent it.\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
|
|
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. A `login` may appear, because the caller just sent it.\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
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/** Reads one subject back: `staff:u1`, or `team:t1#member`. */\nexport function parseSubject(text: string): Subject {\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 } : { type, id, relation };\n}\n"
|
|
8
8
|
],
|
|
9
9
|
"mappings": "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",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
JanusError2,
|
|
3
3
|
StoreFailure2,
|
|
4
|
-
StoreConflict2,
|
|
5
4
|
InvalidCursorError2
|
|
6
5
|
} from "./index-6p56fpbe.js";
|
|
7
6
|
|
|
@@ -20,19 +19,9 @@ function invalidCursor2(where, cursor) {
|
|
|
20
19
|
return new InvalidCursorError2(`${where}: this cursor was not minted by this store, or was minted for another ordering (${cursor.length} characters)`);
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
// src/
|
|
22
|
+
// src/stores/guard.ts
|
|
24
23
|
var ANSWERS_NOTHING = new Set(["insertSession", "insertToken", "write"]);
|
|
25
|
-
function
|
|
26
|
-
return {
|
|
27
|
-
users: guardSlot("users", stores.users),
|
|
28
|
-
sessions: guardSlot("sessions", stores.sessions),
|
|
29
|
-
tokens: guardSlot("tokens", stores.tokens)
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
function guardRelations(store) {
|
|
33
|
-
return guardSlot("relations", store);
|
|
34
|
-
}
|
|
35
|
-
function guardSlot(slot, store) {
|
|
24
|
+
function guardStore(slot, store) {
|
|
36
25
|
const guarded = {};
|
|
37
26
|
for (const method of methodsOf(store)) {
|
|
38
27
|
const original = store[method];
|
|
@@ -72,22 +61,11 @@ function asFailure(error, slot, method) {
|
|
|
72
61
|
cause: error
|
|
73
62
|
});
|
|
74
63
|
}
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
throw absent();
|
|
78
|
-
return value;
|
|
79
|
-
}
|
|
80
|
-
async function unlessVersionConflict(write) {
|
|
81
|
-
try {
|
|
82
|
-
return await write;
|
|
83
|
-
} catch (error) {
|
|
84
|
-
if (error instanceof StoreConflict2 && error.on === "version")
|
|
85
|
-
return null;
|
|
86
|
-
throw error;
|
|
87
|
-
}
|
|
64
|
+
function guardRelations(store) {
|
|
65
|
+
return guardStore("relations", store);
|
|
88
66
|
}
|
|
89
67
|
|
|
90
|
-
export {
|
|
68
|
+
export { guardStore, guardRelations, DEFAULT_PAGE_SIZE2, MAX_PAGE_SIZE2, pageLimit2, invalidCursor2 };
|
|
91
69
|
|
|
92
|
-
//# debugId=
|
|
93
|
-
//# sourceMappingURL=index-
|
|
70
|
+
//# debugId=78C1EC4522BC50BD64756E2164756E21
|
|
71
|
+
//# sourceMappingURL=index-hm4v76kd.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/pagination/cursor-page.ts", "../src/stores/guard.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { InvalidCursorError } from '../errors/janus-error';\n\n/**\n * One page of results, and where the next one starts.\n *\n * `nextCursor` is `null` on the last page and a string on every other, so\n * `while (cursor)` is the loop and there is no separate \"done\" flag to forget.\n *\n * **There is no `total`**, and that is deliberate — the same choice\n * `@nxgt/mongo`'s cursor page makes, for the same reason: an exact count over a\n * large table is a second scan, and no adapter should be made to promise one.\n * An application that needs a count asks its own store for one, where it can\n * decide what the count is allowed to cost.\n *\n * This type is **redefined here rather than imported** from `@nxgt/mongo`. That\n * is deliberate duplication, recorded in AGENTS.md: a port that every adapter\n * implements cannot make one database library a dependency of the contract.\n */\nexport interface CursorPage<T> {\n\treadonly items: readonly T[];\n\treadonly nextCursor: string | null;\n}\n\n/** How many a page holds when the caller did not say. */\nexport const DEFAULT_PAGE_SIZE = 20;\n\n/**\n * The most a page may hold, whatever the caller asked for.\n *\n * Bounded in the core rather than left to each adapter, so a caller cannot ask\n * one store for a million rows and be refused by another. An application that\n * wants everything pages for it.\n */\nexport const MAX_PAGE_SIZE = 100;\n\n/**\n * The limit a store will be given, from the limit a caller asked for.\n *\n * `where` names the call the consumer wrote — `listIdentities`, not an internal\n * function — because several calls in this package take a `limit` and a message\n * that does not say which one leaves the reader to guess. That naming rule is\n * `nxgt-data`'s, and it is the reason its errors read the way they do.\n *\n * A bare `TypeError`: a limit is written in the application's own code, so this\n * cannot come from a request.\n */\nexport function pageLimit(limit: number | undefined, where: string): number {\n\tif (limit === undefined) return DEFAULT_PAGE_SIZE;\n\n\tif (!Number.isInteger(limit) || limit < 1) {\n\t\tthrow new TypeError(\n\t\t\t`${where}: limit must be an integer of at least 1, or absent`,\n\t\t);\n\t}\n\n\treturn Math.min(limit, MAX_PAGE_SIZE);\n}\n\n/**\n * Refuses a cursor this store did not mint.\n *\n * Exported for adapter authors: a cursor from another store, another ordering\n * or another version of the adapter must be refused, **never treated as an\n * absent cursor**. A silent fall back to the first page makes a caller paging a\n * list loop for ever, and the loop looks like a slow query rather than a bug.\n */\nexport function invalidCursor(\n\twhere: string,\n\tcursor: string,\n): InvalidCursorError {\n\t// The cursor's own bytes are not in the message: it is opaque, it may be\n\t// long, and printing it tells the reader nothing they can act on. Its length\n\t// is enough to tell \"truncated in transit\" from \"written by another store\".\n\treturn new InvalidCursorError(\n\t\t`${where}: this cursor was not minted by this store, or was minted for another ordering (${cursor.length} characters)`,\n\t);\n}\n",
|
|
6
|
+
"/**\n * **The one place a store's answer is caught**, shared by both sides of the\n * package: the identity stores and the relation store reach a caller only\n * through {@link guardStore}.\n *\n * - A `JanusError` the store threw passes through, so `instanceof` holds.\n * - **Anything else it threw is a failure** — `StoreFailure`, naming the slot\n * and the method, with the original as `cause`. Never an absence.\n * - `undefined` where the port says `null` is a store that forgot to answer,\n * and becomes `StoreFailure` rather than \"not found\".\n *\n * The `catch` below always rethrows; `src/auth/outage.spec.ts` holds it to that.\n */\n\nimport {\n\ttype JanusError,\n\tJanusError as JanusErrorClass,\n\tStoreFailure,\n} from '../errors/janus-error';\nimport type { RelationStore } from '../permissions/port/types';\n\n/** A store the guard names in a `StoreFailure`: `users`, `sessions`, `tokens` or `relations`. */\nexport type StoreSlot = NonNullable<StoreFailure['slot']>;\n\n/**\n * The methods whose answer is legitimately nothing: `undefined` from them is\n * not a forgotten `return`. Every other method answers a value, `null`,\n * `false`, `0` or a page — **never `undefined`**.\n */\nconst ANSWERS_NOTHING = new Set(['insertSession', 'insertToken', 'write']);\n\nexport function guardStore<S extends object>(slot: StoreSlot, store: S): S {\n\tconst guarded: Record<string, unknown> = {};\n\n\tfor (const method of methodsOf(store)) {\n\t\tconst original = (store as Record<string, unknown>)[method];\n\t\tif (typeof original !== 'function') continue;\n\n\t\tguarded[method] = async (...args: unknown[]) => {\n\t\t\tlet answer: unknown;\n\n\t\t\ttry {\n\t\t\t\tanswer = await original.apply(store, args);\n\t\t\t} catch (error) {\n\t\t\t\t// Rethrown, always. The only question is under which class.\n\t\t\t\tthrow asFailure(error, slot, method);\n\t\t\t}\n\n\t\t\tif (answer === undefined && !ANSWERS_NOTHING.has(method)) {\n\t\t\t\tthrow new StoreFailure(\n\t\t\t\t\t`${slot}.${method} answered undefined: an absence is null, so this store forgot to answer`,\n\t\t\t\t\t{ slot, operation: method },\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn answer;\n\t\t};\n\t}\n\n\treturn guarded as S;\n}\n\n/** Own and prototype methods, so a class-based store is guarded like a literal. */\nfunction methodsOf(store: object): string[] {\n\tconst names = new Set<string>();\n\n\tfor (\n\t\tlet proto: object | null = store;\n\t\tproto !== null && proto !== Object.prototype;\n\t\tproto = Object.getPrototypeOf(proto)\n\t) {\n\t\tfor (const name of Object.getOwnPropertyNames(proto)) {\n\t\t\tif (name !== 'constructor') names.add(name);\n\t\t}\n\t}\n\n\treturn [...names];\n}\n\nfunction asFailure(\n\terror: unknown,\n\tslot: StoreSlot,\n\tmethod: string,\n): JanusError {\n\tif (error instanceof JanusErrorClass) return error;\n\n\t// The message names where, never what: a driver's message may hold a\n\t// connection string, and a connection string holds a password. It is kept\n\t// as `cause`, for the operator's logs.\n\treturn new StoreFailure(`${slot}.${method}: the store could not answer`, {\n\t\tslot,\n\t\toperation: method,\n\t\tcause: error,\n\t});\n}\n\n/**\n * The relation store, guarded the same way: the permission engine's only way\n * to a store, so a failure there is `STORE_FAILED` and never a denial.\n */\nexport function guardRelations(store: RelationStore): RelationStore {\n\treturn guardStore('relations', store);\n}\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": ";;;;;;;AAwBO,IAAM,qBAAoB;AAS1B,IAAM,iBAAgB;AAatB,SAAS,UAAS,CAAC,OAA2B,OAAuB;AAAA,EAC3E,IAAI,UAAU;AAAA,IAAW,OAAO;AAAA,EAEhC,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,GAAG;AAAA,IAC1C,MAAM,IAAI,UACT,GAAG,0DACJ;AAAA,EACD;AAAA,EAEA,OAAO,KAAK,IAAI,OAAO,cAAa;AAAA;AAW9B,SAAS,cAAa,CAC5B,OACA,QACqB;AAAA,EAIrB,OAAO,IAAI,oBACV,GAAG,wFAAwF,OAAO,oBACnG;AAAA;;;AC9CD,IAAM,kBAAkB,IAAI,IAAI,CAAC,iBAAiB,eAAe,OAAO,CAAC;AAElE,SAAS,UAA4B,CAAC,MAAiB,OAAa;AAAA,EAC1E,MAAM,UAAmC,CAAC;AAAA,EAE1C,WAAW,UAAU,UAAU,KAAK,GAAG;AAAA,IACtC,MAAM,WAAY,MAAkC;AAAA,IACpD,IAAI,OAAO,aAAa;AAAA,MAAY;AAAA,IAEpC,QAAQ,UAAU,UAAU,SAAoB;AAAA,MAC/C,IAAI;AAAA,MAEJ,IAAI;AAAA,QACH,SAAS,MAAM,SAAS,MAAM,OAAO,IAAI;AAAA,QACxC,OAAO,OAAO;AAAA,QAEf,MAAM,UAAU,OAAO,MAAM,MAAM;AAAA;AAAA,MAGpC,IAAI,WAAW,aAAa,CAAC,gBAAgB,IAAI,MAAM,GAAG;AAAA,QACzD,MAAM,IAAI,cACT,GAAG,QAAQ,iFACX,EAAE,MAAM,WAAW,OAAO,CAC3B;AAAA,MACD;AAAA,MAEA,OAAO;AAAA;AAAA,EAET;AAAA,EAEA,OAAO;AAAA;AAIR,SAAS,SAAS,CAAC,OAAyB;AAAA,EAC3C,MAAM,QAAQ,IAAI;AAAA,EAElB,SACK,QAAuB,MAC3B,UAAU,QAAQ,UAAU,OAAO,WACnC,QAAQ,OAAO,eAAe,KAAK,GAClC;AAAA,IACD,WAAW,QAAQ,OAAO,oBAAoB,KAAK,GAAG;AAAA,MACrD,IAAI,SAAS;AAAA,QAAe,MAAM,IAAI,IAAI;AAAA,IAC3C;AAAA,EACD;AAAA,EAEA,OAAO,CAAC,GAAG,KAAK;AAAA;AAGjB,SAAS,SAAS,CACjB,OACA,MACA,QACa;AAAA,EACb,IAAI,iBAAiB;AAAA,IAAiB,OAAO;AAAA,EAK7C,OAAO,IAAI,cAAa,GAAG,QAAQ,sCAAsC;AAAA,IACxE;AAAA,IACA,WAAW;AAAA,IACX,OAAO;AAAA,EACR,CAAC;AAAA;AAOK,SAAS,cAAc,CAAC,OAAqC;AAAA,EACnE,OAAO,WAAW,aAAa,KAAK;AAAA;",
|
|
9
|
+
"debugId": "78C1EC4522BC50BD64756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
|
@@ -26,7 +26,7 @@ export declare function rejects(promise: Promise<unknown>, what: string): Promis
|
|
|
26
26
|
* The `instanceof` probe: a class that is named like this package's but is not
|
|
27
27
|
* this package's class means **two copies of `@nxgt/janus`** are installed —
|
|
28
28
|
* the adapter depends on it instead of peering it. Then `StoreConflict` stops
|
|
29
|
-
* passing through the core, and
|
|
29
|
+
* passing through the core, and a login that is taken is answered as an
|
|
30
30
|
* outage.
|
|
31
31
|
*
|
|
32
32
|
* The probe says what breaks; `verify:artifacts` says whether a duplicate is
|
|
@@ -355,7 +355,7 @@ var tokenStoreCases = [
|
|
|
355
355
|
await stores.tokens.insertToken(record);
|
|
356
356
|
const now = at("2026-02-01T00:00:00.000Z");
|
|
357
357
|
const answers = await Promise.all(Array.from({ length: 20 }, () => stores.tokens.consumeToken(record.tokenHash, "resetPassword", now)));
|
|
358
|
-
equal(answers.filter((answer) => answer !== null && answer.spentAt === null).length, 1, "consumeToken: of twenty concurrent calls, exactly one should see spentAt null — a reset
|
|
358
|
+
equal(answers.filter((answer) => answer !== null && answer.spentAt === null).length, 1, "consumeToken: of twenty concurrent calls, exactly one should see spentAt null — a reset token redeemed twice is an account takeover");
|
|
359
359
|
}
|
|
360
360
|
},
|
|
361
361
|
{
|
|
@@ -475,7 +475,7 @@ var userStoreCases = [
|
|
|
475
475
|
}
|
|
476
476
|
equal((await stores.users.findUserByLogin("user", "Ada@Example.test"))?.id, upper.id, "findUserByLogin should find the login with its exact bytes");
|
|
477
477
|
equal((await stores.users.findUserByLogin("user", "ada@example.test"))?.id, lower.id, 'findUserByLogin should tell "Ada@…" from "ada@…": normalisation is the core’s');
|
|
478
|
-
equal((await stores.users.findUserByLogin("staff", "ada@example.test"))?.id, staff.id, "uniqueness is of (type, login): the same login under another type is another
|
|
478
|
+
equal((await stores.users.findUserByLogin("staff", "ada@example.test"))?.id, staff.id, "uniqueness is of (type, login): the same login under another type is another user");
|
|
479
479
|
}
|
|
480
480
|
},
|
|
481
481
|
{
|
|
@@ -1109,5 +1109,5 @@ export {
|
|
|
1109
1109
|
userStoreCases
|
|
1110
1110
|
};
|
|
1111
1111
|
|
|
1112
|
-
//# debugId=
|
|
1112
|
+
//# debugId=D0CD885A4DCD2B1E64756E2164756E21
|
|
1113
1113
|
//# sourceMappingURL=index.js.map
|