@nxgt/janus 0.1.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.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +554 -0
  3. package/dist/auth/config.d.ts +201 -0
  4. package/dist/auth/config.d.ts.map +1 -0
  5. package/dist/auth/context.d.ts +87 -0
  6. package/dist/auth/context.d.ts.map +1 -0
  7. package/dist/auth/hashers.d.ts +35 -0
  8. package/dist/auth/hashers.d.ts.map +1 -0
  9. package/dist/auth/index.d.ts +9 -0
  10. package/dist/auth/index.d.ts.map +1 -0
  11. package/dist/auth/janus.d.ts +38 -0
  12. package/dist/auth/janus.d.ts.map +1 -0
  13. package/dist/auth/outage.d.ts +61 -0
  14. package/dist/auth/outage.d.ts.map +1 -0
  15. package/dist/auth/port/assert-stores.d.ts +16 -0
  16. package/dist/auth/port/assert-stores.d.ts.map +1 -0
  17. package/dist/auth/port/memory.d.ts +25 -0
  18. package/dist/auth/port/memory.d.ts.map +1 -0
  19. package/dist/auth/port/types.d.ts +393 -0
  20. package/dist/auth/port/types.d.ts.map +1 -0
  21. package/dist/auth/secrets.d.ts +17 -0
  22. package/dist/auth/secrets.d.ts.map +1 -0
  23. package/dist/auth/sessions.d.ts +31 -0
  24. package/dist/auth/sessions.d.ts.map +1 -0
  25. package/dist/auth/standard-schema.d.ts +41 -0
  26. package/dist/auth/standard-schema.d.ts.map +1 -0
  27. package/dist/auth/types.d.ts +353 -0
  28. package/dist/auth/types.d.ts.map +1 -0
  29. package/dist/auth/users.d.ts +9 -0
  30. package/dist/auth/users.d.ts.map +1 -0
  31. package/dist/chunks/index-658b6mr2.js +269 -0
  32. package/dist/chunks/index-658b6mr2.js.map +11 -0
  33. package/dist/chunks/index-6dytvy3h.js +93 -0
  34. package/dist/chunks/index-6dytvy3h.js.map +11 -0
  35. package/dist/chunks/index-6p56fpbe.js +147 -0
  36. package/dist/chunks/index-6p56fpbe.js.map +12 -0
  37. package/dist/chunks/index-fgb3t64y.js +77 -0
  38. package/dist/chunks/index-fgb3t64y.js.map +10 -0
  39. package/dist/conformance/assert.d.ts +36 -0
  40. package/dist/conformance/assert.d.ts.map +1 -0
  41. package/dist/conformance/cases/outage.d.ts +3 -0
  42. package/dist/conformance/cases/outage.d.ts.map +1 -0
  43. package/dist/conformance/cases/sessions.d.ts +3 -0
  44. package/dist/conformance/cases/sessions.d.ts.map +1 -0
  45. package/dist/conformance/cases/tokens.d.ts +3 -0
  46. package/dist/conformance/cases/tokens.d.ts.map +1 -0
  47. package/dist/conformance/cases/users.d.ts +3 -0
  48. package/dist/conformance/cases/users.d.ts.map +1 -0
  49. package/dist/conformance/describe.d.ts +74 -0
  50. package/dist/conformance/describe.d.ts.map +1 -0
  51. package/dist/conformance/fixtures.d.ts +11 -0
  52. package/dist/conformance/fixtures.d.ts.map +1 -0
  53. package/dist/conformance/index.d.ts +33 -0
  54. package/dist/conformance/index.d.ts.map +1 -0
  55. package/dist/conformance/index.js +1113 -0
  56. package/dist/conformance/index.js.map +18 -0
  57. package/dist/conformance/reference.d.ts +13 -0
  58. package/dist/conformance/reference.d.ts.map +1 -0
  59. package/dist/conformance/relations.d.ts +67 -0
  60. package/dist/conformance/relations.d.ts.map +1 -0
  61. package/dist/conformance/types.d.ts +71 -0
  62. package/dist/conformance/types.d.ts.map +1 -0
  63. package/dist/errors/janus-error.d.ts +259 -0
  64. package/dist/errors/janus-error.d.ts.map +1 -0
  65. package/dist/ids/id.d.ts +55 -0
  66. package/dist/ids/id.d.ts.map +1 -0
  67. package/dist/index.d.ts +30 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +962 -0
  70. package/dist/index.js.map +19 -0
  71. package/dist/pagination/cursor-page.d.ts +53 -0
  72. package/dist/pagination/cursor-page.d.ts.map +1 -0
  73. package/dist/permissions/engine.d.ts +46 -0
  74. package/dist/permissions/engine.d.ts.map +1 -0
  75. package/dist/permissions/index.d.ts +24 -0
  76. package/dist/permissions/index.d.ts.map +1 -0
  77. package/dist/permissions/index.js +685 -0
  78. package/dist/permissions/index.js.map +15 -0
  79. package/dist/permissions/input.d.ts +29 -0
  80. package/dist/permissions/input.d.ts.map +1 -0
  81. package/dist/permissions/model.d.ts +368 -0
  82. package/dist/permissions/model.d.ts.map +1 -0
  83. package/dist/permissions/port/memory.d.ts +17 -0
  84. package/dist/permissions/port/memory.d.ts.map +1 -0
  85. package/dist/permissions/port/types.d.ts +84 -0
  86. package/dist/permissions/port/types.d.ts.map +1 -0
  87. package/dist/permissions/resolve.d.ts +59 -0
  88. package/dist/permissions/resolve.d.ts.map +1 -0
  89. package/dist/permissions/reverse.d.ts +53 -0
  90. package/dist/permissions/reverse.d.ts.map +1 -0
  91. package/dist/permissions/walk.d.ts +29 -0
  92. package/dist/permissions/walk.d.ts.map +1 -0
  93. package/dist/subjects/notation.d.ts +35 -0
  94. package/dist/subjects/notation.d.ts.map +1 -0
  95. package/dist/subjects/subject.d.ts +75 -0
  96. package/dist/subjects/subject.d.ts.map +1 -0
  97. package/dist/time/clock.d.ts +31 -0
  98. package/dist/time/clock.d.ts.map +1 -0
  99. package/dist/time/duration.d.ts +23 -0
  100. package/dist/time/duration.d.ts.map +1 -0
  101. package/docs/README.md +17 -0
  102. package/docs/guide/adapters.md +296 -0
  103. package/docs/guide/email-flows.md +146 -0
  104. package/docs/guide/errors.md +146 -0
  105. package/docs/guide/passwords.md +136 -0
  106. package/docs/guide/permissions.md +347 -0
  107. package/docs/guide/sessions.md +211 -0
  108. package/docs/guide/users.md +277 -0
  109. package/docs/guide/vocabulary.md +163 -0
  110. package/docs/roadmap.md +93 -0
  111. package/docs/troubleshooting.md +648 -0
  112. package/package.json +70 -0
@@ -0,0 +1,393 @@
1
+ /**
2
+ * The store port: what an adapter implements, and nothing else.
3
+ *
4
+ * **This is the contract strangers are asked to implement**, so every line of
5
+ * it is a promise. It is small on purpose — anything the core can derive from
6
+ * what the port already offers (merging fields, deciding that an e-mail is no
7
+ * longer verified, deciding that a session has lapsed) lives in the core, where
8
+ * it is written once, rather than here, where every adapter would write it
9
+ * again, differently.
10
+ *
11
+ * ## The six rules an implementation keeps
12
+ *
13
+ * All of them are checked by `@nxgt/janus/conformance`.
14
+ *
15
+ * 1. **An absence is `null`. A failure throws.** A method that can
16
+ * legitimately find nothing answers `null`, `false`, `0` or an empty page.
17
+ * Everything else — a refused connection, a timeout, a primary stepping
18
+ * down, a bug in the adapter — **throws**, preferably `StoreFailure` with
19
+ * the driver error as `cause`. Never write `try { … } catch { return null }`
20
+ * in an implementation of this port: that one line turns an outage into
21
+ * "no such account", and every caller above it answers 404 to somebody
22
+ * whose account exists.
23
+ * 2. **`null`, not `undefined`.** `undefined` is what a missing property and a
24
+ * function with no `return` both produce, so a store that forgot to answer
25
+ * would report "not found" by accident. `null` has to be written on purpose.
26
+ * 3. **Uniqueness is yours, and it is a constraint.** A unique index, a
27
+ * constraint, an atomic `SET NX` — never a read followed by a write, which
28
+ * two concurrent sign-ups both pass.
29
+ * 4. **Bytes round-trip.** Logins, hashes and fields come back exactly as they
30
+ * were written: no normalisation, no trimming, no `1` turned into `'1'`. The
31
+ * core normalises a login before a store ever sees it, so uniqueness is
32
+ * uniqueness of bytes and no adapter needs a collation.
33
+ * 5. **Every method is atomic on its own.** Nothing composes into a
34
+ * transaction, and the core never opens one. An adapter may open one
35
+ * *inside* a method — a normalised SQL schema writes several rows per
36
+ * user — but the port exposes none.
37
+ * 6. **Schema management is not on this interface.** An adapter exposes its own
38
+ * `sync()`; the core never calls it.
39
+ *
40
+ * ## Why three stores and not one
41
+ *
42
+ * The seam is **where atomicity is not required**. A user and their password
43
+ * are written together — a sign-up that stores the user and loses the hash is
44
+ * an account nobody can enter — so they are one record. A session is derived
45
+ * state: losing them all signs everybody out, which recovers. A one-time token
46
+ * is ephemeral by construction. So `sessions` and `tokens` may live in Redis
47
+ * while `users` lives in MongoDB, with no distributed transaction anywhere.
48
+ *
49
+ * ## Why the port is not generic
50
+ *
51
+ * A store does not know the application's schemas, and must not have to:
52
+ * fields are a {@link JsonObject} here, and the core casts once, at the
53
+ * boundary, after the schema has validated them. That is the shape `nxgt-data`
54
+ * uses — a generic public form, a degenericised mirror inside — and it keeps
55
+ * every adapter free of type parameters it could only pass through.
56
+ */
57
+ import type { Id } from '../../ids/id';
58
+ import type { CursorPage } from '../../pagination/cursor-page';
59
+ /**
60
+ * A value a store must be able to round-trip byte for byte.
61
+ *
62
+ * JSON and nothing else. A `Date` inside fields round-trips through MongoDB and
63
+ * not through a JSON column or Redis, so an adapter could pass the conformance
64
+ * suite on one database and corrupt fields on the next. The record's own
65
+ * timestamps are `Date`s, because every adapter stores those in a column it
66
+ * chose for them.
67
+ */
68
+ export type Json = string | number | boolean | null | readonly Json[] | JsonObject;
69
+ /**
70
+ * An object of {@link Json} values.
71
+ *
72
+ * An `interface` declared by the application is not assignable to this — an
73
+ * index signature is only satisfied by a type alias. The core never asks a
74
+ * caller to write one: it casts at the boundary, after validation.
75
+ */
76
+ export type JsonObject = {
77
+ readonly [key: string]: Json;
78
+ };
79
+ /** A password, as the store holds it: a self-describing hash, never the plain text. */
80
+ export interface PasswordRecord {
81
+ /** Self-describing — `$argon2id$…`, `$scrypt$…` — so any wired verifier can read it. */
82
+ readonly hash: string;
83
+ readonly updatedAt: Date;
84
+ }
85
+ /**
86
+ * A user, as a store holds it.
87
+ *
88
+ * Everything is `readonly`: the core hands records to application code, and a
89
+ * mutation would not reach the store — it could only mislead whoever wrote it.
90
+ */
91
+ export interface UserRecord {
92
+ /** A UUIDv7 minted by the core. The store never mints an id. */
93
+ readonly id: Id;
94
+ /**
95
+ * Which of the application's user types — `'patient'`, `'staff'`, or
96
+ * `'user'` when it declares one. Never changes after insertion.
97
+ */
98
+ readonly type: string;
99
+ /**
100
+ * Which version of the type's schema these fields were last validated
101
+ * against.
102
+ *
103
+ * **Nothing reads it yet**, and it is here from v1 on purpose. Tightening a
104
+ * schema changes what an update accepts, and every stored user was validated
105
+ * against the old one: without this field there is no way to find the users
106
+ * that are now unmodifiable. Adding a field to the port later breaks every
107
+ * adapter; adding it now costs one column.
108
+ */
109
+ readonly schemaVersion: string;
110
+ /** `false` keeps the record and its password, and refuses every sign-in. */
111
+ readonly active: boolean;
112
+ /** The application's own fields, as the type's schema validated them. */
113
+ readonly fields: JsonObject;
114
+ /**
115
+ * What the user signs in with, **already normalised** by the core. Unique
116
+ * per `type`, and that uniqueness is a constraint the store enforces
117
+ * (rule 3): the same e-mail may hold a patient account and a staff account,
118
+ * and never two of either.
119
+ */
120
+ readonly logins: readonly string[];
121
+ /** `null` when the user has no password. */
122
+ readonly password: PasswordRecord | null;
123
+ /**
124
+ * When the user proved they hold their current e-mail, or `null`. The core
125
+ * sets it back to `null` in the same write that changes the e-mail.
126
+ */
127
+ readonly emailVerifiedAt: Date | null;
128
+ /**
129
+ * `0` at insertion, and one more on every accepted write. What
130
+ * {@link UserStore.updateUser}'s `ifVersion` is compared against.
131
+ */
132
+ readonly version: number;
133
+ readonly createdAt: Date;
134
+ readonly updatedAt: Date;
135
+ }
136
+ /**
137
+ * What one {@link UserStore.updateUser} changes.
138
+ *
139
+ * **A field the patch does not name is left as it is.** There is no full
140
+ * replacement of a record anywhere on this port: in Kratos, an update that
141
+ * omits `state` deactivates the account, and an edit form that omits a trait
142
+ * deletes it. A conformance case carries that trap's name.
143
+ *
144
+ * A field the patch *does* name is replaced whole — `fields` and `logins`
145
+ * included. A deep merge would make every adapter implement a merge on nested
146
+ * values, differently on every database. The core computes the next value from
147
+ * the record it just read, under `ifVersion`, so no write is lost by it.
148
+ *
149
+ * `password: null` removes the password; a patch that does not name `password`
150
+ * keeps it.
151
+ *
152
+ * `id`, `type`, `version` and `createdAt` are not patchable. `updatedAt` is
153
+ * **required**, and comes from the core's clock rather than the database's, so
154
+ * every timestamp on a record comes from one clock.
155
+ *
156
+ * A key present with the value `undefined` is absent. This package is compiled
157
+ * with `exactOptionalPropertyTypes`, so the core cannot write one; an adapter
158
+ * receiving one from JavaScript still treats it as absent, never as an erasure.
159
+ */
160
+ export interface UserPatch {
161
+ readonly updatedAt: Date;
162
+ readonly schemaVersion?: string;
163
+ readonly active?: boolean;
164
+ readonly fields?: JsonObject;
165
+ readonly logins?: readonly string[];
166
+ readonly password?: PasswordRecord | null;
167
+ readonly emailVerifiedAt?: Date | null;
168
+ }
169
+ /** Which users a page lists, where it starts, and how much of it to read. */
170
+ export interface UserPageRequest {
171
+ /** Only users of this type. */
172
+ readonly type: string;
173
+ /**
174
+ * The last id of the previous page, or `null` for the first.
175
+ *
176
+ * Already checked by the core to be an id this package could have minted,
177
+ * so the store never parses a cursor. It need not name a stored user: the
178
+ * page is every id strictly greater than it.
179
+ */
180
+ readonly after: Id | null;
181
+ /** Already bounded by the core, between 1 and `MAX_PAGE_SIZE`. */
182
+ readonly limit: number;
183
+ }
184
+ /** Users and their passwords: one unit of atomicity. */
185
+ export interface UserStore {
186
+ /**
187
+ * Stores a new user, verbatim, and answers what is stored.
188
+ *
189
+ * **Idempotent under retry.** When a user with this `id` already exists, it
190
+ * answers the stored record and writes nothing — a retry after a timeout
191
+ * whose first attempt landed is a success, not a conflict. That includes the
192
+ * logins: a login held by the user with this same `id` is not taken.
193
+ *
194
+ * A login held by **another** user of the same type rejects with
195
+ * `StoreConflict('login', …)`, carrying `login` and `userType`, and writes
196
+ * nothing. That refusal comes from the store's own constraint, never from a
197
+ * read made first.
198
+ */
199
+ insertUser(record: UserRecord): Promise<UserRecord>;
200
+ /** The user with this id, whatever their type, or `null`. */
201
+ findUser(id: Id): Promise<UserRecord | null>;
202
+ /**
203
+ * The user of this type holding this login, or `null`.
204
+ *
205
+ * `login` is compared byte for byte: the core normalised it with the same
206
+ * rule it used when the login was written.
207
+ */
208
+ findUserByLogin(type: string, login: string): Promise<UserRecord | null>;
209
+ /**
210
+ * One page of users of one type, in ascending id order — which is creation
211
+ * order.
212
+ *
213
+ * `nextCursor` is the last id of the page when more follow, and `null` on
214
+ * the last page. An empty store answers an empty page, never `null`.
215
+ */
216
+ listUsers(page: UserPageRequest): Promise<CursorPage<UserRecord>>;
217
+ /**
218
+ * Applies a patch, **only if the stored version is exactly `ifVersion`**,
219
+ * and answers the record as written, with `version` one higher.
220
+ *
221
+ * `ifVersion` is required: every update in the core comes from a record it
222
+ * has just read, so a version is always at hand — and an optional check is
223
+ * the one somebody forgets on the one write where it mattered. An adapter
224
+ * therefore has no unchecked path to write.
225
+ *
226
+ * Rejects, **writing nothing**, with:
227
+ *
228
+ * - `NotFoundError` when no user has this id. The one method on this port
229
+ * that throws for an absence: it always follows a read, so an absence here
230
+ * is a race, not an answer;
231
+ * - `StoreConflict('version', …)` with `expectedVersion` and
232
+ * `actualVersion` when the version moved. A conditional write cannot tell
233
+ * this from an absent id on its own, so the adapter reads again after a
234
+ * write that matched nothing;
235
+ * - `StoreConflict('login', …)` when the patch's logins collide with another
236
+ * user's of the same type.
237
+ */
238
+ updateUser(id: Id, patch: UserPatch, ifVersion: number): Promise<UserRecord>;
239
+ /**
240
+ * Deletes the user with this id, whatever their type, and frees their
241
+ * logins. `true` when there was one, `false` when there was none.
242
+ *
243
+ * **Idempotent**, so a deletion interrupted half-way can be replayed: the
244
+ * replay answers `false` here and goes on to the sessions and the tokens.
245
+ * This deletes the record only — those two are other stores', and the core
246
+ * deletes them next.
247
+ */
248
+ deleteUser(id: Id): Promise<boolean>;
249
+ }
250
+ /** A session's id: a UUIDv7 minted by the core, as a user's is. */
251
+ export type SessionId = string;
252
+ /**
253
+ * A session, as a store holds it.
254
+ *
255
+ * **No secret is stored, ever.** The core mints 32 random bytes, hands the store
256
+ * `sha256(secret)` as `tokenHash`, and gives the plain secret to the
257
+ * application once. A dump of the store cannot be replayed.
258
+ */
259
+ export interface SessionRecord {
260
+ readonly id: SessionId;
261
+ /** `sha256` of the session token, hex. Unique across the store. */
262
+ readonly tokenHash: string;
263
+ readonly userId: Id;
264
+ /** When credentials were last presented — not when the session was last extended. */
265
+ readonly authenticatedAt: Date;
266
+ readonly expiresAt: Date;
267
+ /** `null` while the session stands. */
268
+ readonly revokedAt: Date | null;
269
+ readonly createdAt: Date;
270
+ }
271
+ /**
272
+ * Sessions. Derived state: losing them all signs everybody out, which recovers.
273
+ *
274
+ * **Expiry is the core's decision, not the store's.** A store may still hold a
275
+ * lapsed session or may already have dropped it — a TTL index is an
276
+ * optimisation, and this interface treats it as one. A read answers a present
277
+ * record **verbatim**, lapsed or revoked, and may answer `null` once its expiry
278
+ * has passed. What it must never do is answer a record it has *changed*.
279
+ */
280
+ export interface SessionStore {
281
+ /**
282
+ * Stores a new session. Idempotent under retry: when a session with this id
283
+ * exists, it writes nothing.
284
+ */
285
+ insertSession(record: SessionRecord): Promise<void>;
286
+ /** The session whose token hashes to this, verbatim, or `null`. */
287
+ findSessionByTokenHash(tokenHash: string): Promise<SessionRecord | null>;
288
+ /**
289
+ * Moves `expiresAt`, **only while the session is not revoked**, and answers
290
+ * the record as written.
291
+ *
292
+ * `null` when there is no such session or it has been revoked — an extension
293
+ * racing a revocation must never bring the session back. Whether it is
294
+ * *allowed* to be extended yet is the core's decision, made before the call.
295
+ */
296
+ extendSession(id: SessionId, expiresAt: Date): Promise<SessionRecord | null>;
297
+ /**
298
+ * Revokes one session. `true` when the session exists — revoked by this call
299
+ * or already — and `false` when there is none.
300
+ *
301
+ * A session already revoked keeps its first `revokedAt`.
302
+ */
303
+ revokeSession(id: SessionId, at: Date): Promise<boolean>;
304
+ /**
305
+ * Revokes every standing session of one user, except `except` when given —
306
+ * "sign out everywhere else". Answers how many this call revoked; `0` is an
307
+ * answer, not a failure.
308
+ */
309
+ revokeUserSessions(userId: Id, at: Date, except?: SessionId): Promise<number>;
310
+ /**
311
+ * Deletes every session of one user — standing, revoked or lapsed — and
312
+ * answers how many. `0` is an answer, not a failure. What deleting a user
313
+ * calls: a revoked session still names who held it.
314
+ */
315
+ deleteUserSessions(userId: Id): Promise<number>;
316
+ /**
317
+ * **Optional capability.** Deletes every session whose `expiresAt` is at or
318
+ * before `before`, and answers how many.
319
+ *
320
+ * A store with its own expiry — a TTL index, a key TTL — does not implement
321
+ * it. The core reads its presence rather than assuming it, and
322
+ * `collectExpired()` throws `UNSUPPORTED`, naming this method and the
323
+ * `sessions` slot, when it is absent.
324
+ */
325
+ deleteExpiredSessions?(before: Date): Promise<number>;
326
+ }
327
+ /** What a one-time token is for. A token redeemed for the other purpose is unknown. */
328
+ export type TokenKind = 'verifyEmail' | 'resetPassword';
329
+ /** A one-time token, as a store holds it: its hash, never its secret. */
330
+ export interface TokenRecord {
331
+ /** `sha256` of the token's secret, hex. Unique across the store. */
332
+ readonly tokenHash: string;
333
+ readonly kind: TokenKind;
334
+ readonly userId: Id;
335
+ /** The e-mail the token was sent to — the one a verification marks verified. */
336
+ readonly address: string;
337
+ readonly expiresAt: Date;
338
+ /** `null` until spent. Once set, never changes. */
339
+ readonly spentAt: Date | null;
340
+ readonly createdAt: Date;
341
+ }
342
+ /** One-time tokens. Ephemeral by construction. */
343
+ export interface TokenStore {
344
+ /**
345
+ * Stores a new token. Idempotent under retry: when a token with this hash
346
+ * exists, it writes nothing.
347
+ */
348
+ insertToken(record: TokenRecord): Promise<void>;
349
+ /**
350
+ * **The most important method on this port.** Spends the token and answers
351
+ * it **as it was before this call**.
352
+ *
353
+ * - `spentAt: null` in the answer means *this call* spent it. Exactly one
354
+ * call ever sees that.
355
+ * - `spentAt` set means it was already spent, and nothing was written.
356
+ * - `null` means no token of this `kind` has this hash — including one the
357
+ * store has already dropped. A token of the other kind is not touched.
358
+ *
359
+ * **One conditional write, never a read followed by a write.** A reset code
360
+ * two concurrent requests both redeem is an account takeover: twenty
361
+ * concurrent calls must produce exactly one answer with `spentAt: null`, and
362
+ * the conformance suite runs exactly that. In MongoDB this is one
363
+ * `findOneAndUpdate` returning the document *before* the update.
364
+ *
365
+ * A lapsed token is spent all the same. Comparing `expiresAt` is the core's
366
+ * job, after the call, so a lapsed token can never be retried.
367
+ */
368
+ consumeToken(tokenHash: string, kind: TokenKind, at: Date): Promise<TokenRecord | null>;
369
+ /**
370
+ * Deletes every token of one user, spent or not, and answers how many.
371
+ * What deleting a user calls: a token holds the e-mail it was sent to, which
372
+ * must not outlive the user until its expiry.
373
+ */
374
+ deleteUserTokens(userId: Id): Promise<number>;
375
+ }
376
+ /**
377
+ * The three stores `janus()` takes.
378
+ *
379
+ * Each slot may come from a different adapter. That is the point of the seam:
380
+ * `@nxgt/janus-redis` can serve `sessions` and `tokens` while
381
+ * `@nxgt/janus-mongo` serves `users`.
382
+ */
383
+ export interface JanusStores {
384
+ readonly users: UserStore;
385
+ readonly sessions: SessionStore;
386
+ readonly tokens: TokenStore;
387
+ }
388
+ /** What `assertStores` found beyond the required methods. */
389
+ export interface StoreCapabilities {
390
+ /** Whether `sessions.deleteExpiredSessions` is implemented. */
391
+ readonly collectExpired: boolean;
392
+ }
393
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/auth/port/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A fresh secret: 32 random bytes, base64url — safe in a header, a cookie and a
3
+ * URL without escaping.
4
+ *
5
+ * Given to the application **once**. No store ever holds it.
6
+ */
7
+ export declare function mintSecret(): string;
8
+ /**
9
+ * What a store holds instead of the secret: `sha256`, hex.
10
+ *
11
+ * SHA-256 and not argon2, on purpose. The secret carries 256 bits of entropy, so
12
+ * there is nothing to brute-force, and an argon2 per request would put ~50 ms on
13
+ * every authenticated call. What the hash buys is that a dump of the store
14
+ * cannot be replayed.
15
+ */
16
+ export declare function hashSecret(secret: string): string;
17
+ //# sourceMappingURL=secrets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../src/auth/secrets.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEjD"}
@@ -0,0 +1,31 @@
1
+ import type { ResolvedType } from './config';
2
+ import { type AnyUser, type Context } from './context';
3
+ import type { SessionRecord, UserRecord } from './port/types';
4
+ import type { Authenticated, RequestLike, Session, SharedApi, SignedIn } from './types';
5
+ /** `SharedApi`, degenericised: `janus()` casts it to the typed form once. */
6
+ export type InternalSharedApi = Omit<SharedApi<AnyUser>, 'authenticate'> & {
7
+ authenticate(request: RequestLike, options?: {
8
+ readonly type?: string;
9
+ }): Promise<Authenticated<AnyUser> | null>;
10
+ };
11
+ /** A session as application code sees it: everything but the token's hash. */
12
+ export declare function toSession(record: SessionRecord): Session;
13
+ /**
14
+ * Opens a session for a user who just proved who they are. The token is in
15
+ * the answer and nowhere else: the store holds its hash.
16
+ */
17
+ export declare function openSession(context: Context, type: ResolvedType, user: UserRecord): Promise<SignedIn<AnyUser>>;
18
+ /** Everything `janus()` answers whatever its user types. */
19
+ export declare function sharedApi(context: Context): InternalSharedApi;
20
+ /**
21
+ * The session token a request presents: `Authorization: Bearer`, then
22
+ * `X-Session-Token`, then the cookie.
23
+ *
24
+ * **The first credential present wins, not the first valid one.** A browser
25
+ * sending a lapsed bearer beside a live cookie is anonymous, and should fix its
26
+ * header rather than be rescued in silence — the rule `resolve` in `nxgt-ory`'s
27
+ * SDK learned. An `Authorization` header of another scheme (`Basic`) is not a
28
+ * session credential, and does not count as one.
29
+ */
30
+ export declare function presentedToken(request: RequestLike, cookieName: string): string | null;
31
+ //# sourceMappingURL=sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/auth/sessions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,OAAO,EAKZ,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE9D,OAAO,KAAK,EACX,aAAa,EAEb,WAAW,EACX,OAAO,EACP,SAAS,EACT,QAAQ,EACR,MAAM,SAAS,CAAC;AAEjB,6EAA6E;AAC7E,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC,GAAG;IAC1E,YAAY,CACX,OAAO,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAClC,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;CAC1C,CAAC;AAEF,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAGxD;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAChC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE,UAAU,GACd,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAe5B;AAED,4DAA4D;AAC5D,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,iBAAiB,CA4G7D;AAqBD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC7B,OAAO,EAAE,WAAW,EACpB,UAAU,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CA+Bf"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The Standard Schema interface, v1 — copied, as the specification asks.
3
+ *
4
+ * <https://standardschema.dev>. Zod 4, Valibot and ArkType implement it, so a
5
+ * definition accepts whichever the application already uses and this package
6
+ * has **no validation peer at all**. The specification is written to be copied
7
+ * rather than depended on: it is a type, and a type has nothing to install.
8
+ */
9
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
10
+ readonly '~standard': StandardSchemaV1.Props<Input, Output>;
11
+ }
12
+ export declare namespace StandardSchemaV1 {
13
+ interface Props<Input = unknown, Output = Input> {
14
+ readonly version: 1;
15
+ readonly vendor: string;
16
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
17
+ readonly types?: Types<Input, Output> | undefined;
18
+ }
19
+ type Result<Output> = SuccessResult<Output> | FailureResult;
20
+ interface SuccessResult<Output> {
21
+ readonly value: Output;
22
+ readonly issues?: undefined;
23
+ }
24
+ interface FailureResult {
25
+ readonly issues: ReadonlyArray<Issue>;
26
+ }
27
+ interface Issue {
28
+ readonly message: string;
29
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
30
+ }
31
+ interface PathSegment {
32
+ readonly key: PropertyKey;
33
+ }
34
+ interface Types<Input = unknown, Output = Input> {
35
+ readonly input: Input;
36
+ readonly output: Output;
37
+ }
38
+ type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input'];
39
+ type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output'];
40
+ }
41
+ //# sourceMappingURL=standard-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standard-schema.d.ts","sourceRoot":"","sources":["../../src/auth/standard-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,KAAK;IAChE,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;CAC5D;AAED,MAAM,CAAC,OAAO,WAAW,gBAAgB,CAAC;IACzC,UAAiB,KAAK,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,KAAK;QACrD,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;QACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,QAAQ,EAAE,CAClB,KAAK,EAAE,OAAO,KACV,MAAM,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;QAC9C,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;KAClD;IAED,KAAY,MAAM,CAAC,MAAM,IAAI,aAAa,CAAC,MAAM,CAAC,GAAG,aAAa,CAAC;IAEnE,UAAiB,aAAa,CAAC,MAAM;QACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;KAC5B;IAED,UAAiB,aAAa;QAC7B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC;KACtC;IAED,UAAiB,KAAK;QACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,WAAW,GAAG,WAAW,CAAC,GAAG,SAAS,CAAC;KACrE;IAED,UAAiB,WAAW;QAC3B,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;KAC1B;IAED,UAAiB,KAAK,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,KAAK;QACrD,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;QACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;KACxB;IAED,KAAY,UAAU,CAAC,MAAM,SAAS,gBAAgB,IAAI,WAAW,CACpE,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,CAC5B,CAAC,OAAO,CAAC,CAAC;IAEX,KAAY,WAAW,CAAC,MAAM,SAAS,gBAAgB,IAAI,WAAW,CACrE,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,CAC5B,CAAC,QAAQ,CAAC,CAAC;CACZ"}