@antelopejs/interface-dms 0.7.1 → 0.7.3

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 (75) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +70 -68
  3. package/dist/auth/db/models/sessions.model.d.ts +1 -0
  4. package/dist/auth/db/models/userExternalIdentities.model.d.ts +1 -0
  5. package/dist/auth/db/models/users.model.d.ts +1 -0
  6. package/dist/base/block-registry/index.d.ts +1 -0
  7. package/dist/base/block-registry/index.js +1 -0
  8. package/dist/base/block-registry/index.js.map +1 -1
  9. package/dist/base/block-registry/presentation.d.ts +21 -0
  10. package/dist/base/block-registry/presentation.js +108 -0
  11. package/dist/base/block-registry/presentation.js.map +1 -0
  12. package/dist/base/block-types.d.ts +1 -0
  13. package/dist/base/block-types.js +1 -0
  14. package/dist/base/block-types.js.map +1 -1
  15. package/dist/base/live-toggle.d.ts +2 -2
  16. package/dist/base/period-selector.d.ts +2 -2
  17. package/dist/base/placeholder.js +2 -2
  18. package/dist/base/placeholder.js.map +1 -1
  19. package/dist/base/table-view/schema.d.ts +4 -4
  20. package/dist/db/internal/exportJobs.model.d.ts +1 -0
  21. package/dist/db/internal/inviteResolutions.model.d.ts +1 -0
  22. package/dist/db/internal/tenantLifecycle.model.d.ts +1 -0
  23. package/dist/db/models/roles.model.d.ts +1 -0
  24. package/dist/db/models/tenantMembers.model.d.ts +1 -0
  25. package/dist/db/models/tenants.model.d.ts +1 -0
  26. package/dist/db/models/user_invites.model.d.ts +1 -0
  27. package/docs/.navigation.yml +2 -0
  28. package/docs/1.core/.navigation.yml +2 -0
  29. package/docs/1.core/1.introduction.md +169 -0
  30. package/docs/1.core/10.layout-banners.md +133 -0
  31. package/docs/1.core/2.pages-and-modules.md +649 -0
  32. package/docs/1.core/3.components.md +287 -0
  33. package/docs/1.core/4.permissions.md +279 -0
  34. package/docs/1.core/5.tenancy.md +358 -0
  35. package/docs/1.core/6.invites.md +239 -0
  36. package/docs/1.core/7.hooks.md +354 -0
  37. package/docs/1.core/8.realtime.md +147 -0
  38. package/docs/1.core/9.quick-actions.md +115 -0
  39. package/docs/2.auth/.navigation.yml +2 -0
  40. package/docs/2.auth/1.introduction.md +75 -0
  41. package/docs/2.auth/2.authentication.md +222 -0
  42. package/docs/2.auth/3.tokens-and-sessions.md +352 -0
  43. package/docs/2.auth/4.users.md +256 -0
  44. package/docs/2.auth/5.external-identities.md +122 -0
  45. package/docs/3.base/.navigation.yml +2 -0
  46. package/docs/3.base/1.introduction.md +202 -0
  47. package/docs/3.base/10.display-blocks.md +136 -0
  48. package/docs/3.base/2.types.md +402 -0
  49. package/docs/3.base/3.forms.md +349 -0
  50. package/docs/3.base/4.tables.md +608 -0
  51. package/docs/3.base/5.charts-and-widgets.md +466 -0
  52. package/docs/3.base/6.layout.md +437 -0
  53. package/docs/3.base/8.data-types.md +361 -0
  54. package/docs/3.base/9.helpers.md +414 -0
  55. package/docs/4.notifications/.navigation.yml +2 -0
  56. package/docs/4.notifications/1.introduction.md +81 -0
  57. package/docs/4.notifications/2.categories-and-subjects.md +214 -0
  58. package/docs/4.notifications/3.sending.md +285 -0
  59. package/docs/5.html-render/.navigation.yml +2 -0
  60. package/docs/5.html-render/1.introduction.md +386 -0
  61. package/docs/5.html-render/2.built-in-emails.md +157 -0
  62. package/docs/images/emails-admin-invite.png +0 -0
  63. package/docs/images/emails-change-notice.png +0 -0
  64. package/docs/images/emails-change-verification.png +0 -0
  65. package/docs/images/emails-digest-logo-dark.png +0 -0
  66. package/docs/images/emails-digest-logo-light.png +0 -0
  67. package/docs/images/emails-digest.png +0 -0
  68. package/docs/images/emails-export-partial.png +0 -0
  69. package/docs/images/emails-export-ready.png +0 -0
  70. package/docs/images/emails-reset-password.png +0 -0
  71. package/docs/images/emails-two-factor.png +0 -0
  72. package/docs/images/emails-user-validation.png +0 -0
  73. package/docs/index.md +109 -0
  74. package/docs/tsconfig.json +11 -0
  75. package/package.json +25 -18
@@ -0,0 +1,75 @@
1
+ ---
2
+ title: "Introduction"
3
+ description: "The auth domain answers who is calling: authentication decorators, tokens and sessions, users and external identities."
4
+ ---
5
+
6
+ # Introduction
7
+
8
+ ## Overview
9
+
10
+ The `auth` domain of `@antelopejs/interface-dms` answers one question for the DMS: who is calling. It resolves the bearer token on an HTTP request into a `User` row, issues and validates the tokens that make such a request possible, records logins as sessions, binds the accounts a user owns on external login providers, and owns the `users`, `sessions` and `user_external_identities` tables together with their models.
11
+
12
+ It stops at identity. Deciding what an authenticated caller is allowed to do — permissions, roles, tenant membership — belongs to the [Core](/docs/interfaces/core/introduction) domain, whose tenant guards build on the `authenticateRequestUser` function documented here.
13
+
14
+ The domain is published under three subpaths:
15
+
16
+ | Subpath | Contents |
17
+ | ------- | -------- |
18
+ | `@antelopejs/interface-dms/auth` | The authentication decorators, `authenticateRequestUser`, the token generators and validators, `createSession`, `sanitizeUser`, the external-identity functions, `announceRegistration`, and the account mail entry points. |
19
+ | `@antelopejs/interface-dms/auth/db` | The `User`, `Session` and `UserExternalIdentity` tables, their models, and the table-name constants. |
20
+ | `@antelopejs/interface-dms/auth/password` | The password policy: `PASSWORD_MIN_LENGTH`, `PASSWORD_PATTERN` and `isPasswordCompliant`. Pure values, usable without the DMS loaded. |
21
+
22
+ ## Key Features
23
+
24
+ - **Request Authentication Decorators** — Gate a route, a controller property, or a whole controller class on a valid bearer token with `AuthUser` and `AuthRawUser`, and receive the resolved `User`.
25
+ - **Optional Authentication** — Serve the same route to signed-in and anonymous callers with `IfAuthUser`, which injects `User | undefined` instead of rejecting the request.
26
+ - **Owner-Only Gate** — Restrict administration surfaces to platform owners with `AuthOwnerOnly`, which answers `403` to an authenticated non-owner.
27
+ - **Composable Authentication** — Build your own parameter providers on `authenticateRequestUser`, the same primitive the DMS tenant guards start from.
28
+ - **Token Lifecycle** — Issue and validate tenant-scoped access and refresh tokens, plus the short-lived two-factor and tenant-assignment tokens that stand between a password check and a real session.
29
+ - **Revocable Sessions** — Record each login as a `Session` row carrying its device, IP and current refresh token, so a single session or every session of a user can be revoked.
30
+ - **User Table and Model** — A `User` table with hashed passwords through `HashModifier`, and a `UserModel` with lookups by email and by owner flag.
31
+ - **External Login Providers** — Bind the accounts a user owns on GitHub, Google or any configured provider with `linkExternalIdentity`, and resolve a provider profile back to a user on the next login.
32
+ - **Safe Responses** — Strip authentication secrets from a user with `sanitizeUser` before returning it over HTTP.
33
+ - **One Password Policy** — Check a password against the policy the DMS enforces with `isPasswordCompliant`, so a module's own signup accepts exactly what the DMS accepts.
34
+ - **Account Emails** — Send the second-factor, email-validation, password-reset and invite messages the authentication flows depend on.
35
+ - **One Registration Path** — Run the notifications and the `USER_REGISTERED` hook through `announceRegistration`, whichever flow created the account.
36
+
37
+ ## Dependencies
38
+
39
+ This interface relies on the following AntelopeJS packages:
40
+
41
+ - [@antelopejs/interface-auth](https://github.com/AntelopeJS/interface-auth) - `CreateAuthDecorator`, which builds the authentication decorators from a source, an authenticator and a validator.
42
+ - [@antelopejs/interface-api](https://github.com/AntelopeJS/interface-api) - Controllers, `RequestContext` and `HTTPResult`, used to read the request and to reject it.
43
+ - [@antelopejs/interface-database-decorators](https://github.com/AntelopeJS/interface-database-decorators) - Table definitions, the `HashModifier` mixin, and `BasicDataModel` for the user and session models.
44
+ - [@antelopejs/interface-database](https://github.com/AntelopeJS/interface-database) - Query value proxies used by the owner lookups on `UserModel`.
45
+ - [@antelopejs/interface-core](https://github.com/AntelopeJS/interface-core) - `InterfaceFunction`, which wires the token, sanitization and mail entry points to their implementation.
46
+
47
+ ## Quick Start
48
+
49
+ The following controller gates a route on an authenticated user and returns the account with its secrets stripped.
50
+
51
+ ```typescript
52
+ import { Controller, Get } from "@antelopejs/interface-api";
53
+ import {
54
+ AuthUser,
55
+ sanitizeUser,
56
+ } from "@antelopejs/interface-dms/auth";
57
+ import type { User } from "@antelopejs/interface-dms/auth/db";
58
+
59
+ export class ProfileController extends Controller("/api/profile") {
60
+ @Get("")
61
+ profile(@AuthUser() user: User): Promise<Partial<User>> {
62
+ // 401 before this line runs when the token is missing or invalid
63
+ return sanitizeUser(user);
64
+ }
65
+ }
66
+ ```
67
+
68
+ A client reaches it by sending the access token issued at login in the `Authorization` header, as `Bearer <access token>`.
69
+
70
+ ## Documentation Sections
71
+
72
+ - [Authentication](/docs/interfaces/auth/authentication) - Resolve a request to a user with the authentication decorators and `authenticateRequestUser`
73
+ - [Tokens and Sessions](/docs/interfaces/auth/tokens-and-sessions) - Issue and validate access, refresh, two-factor and tenant-assignment tokens, and record login sessions
74
+ - [Users](/docs/interfaces/auth/users) - The user table and model, password hashing, the password policy, sanitization, the account mail entry points, and announcing a registration
75
+ - [External Identities](/docs/interfaces/auth/external-identities) - Bind and read the accounts a user owns on external login providers
@@ -0,0 +1,222 @@
1
+ ---
2
+ title: "Authentication"
3
+ description: "Authentication turns an incoming HTTP request into a `User` row."
4
+ ---
5
+
6
+ # Authentication
7
+
8
+ ## Overview
9
+
10
+ Authentication turns an incoming HTTP request into a `User` row. The interface exposes one plain function, `authenticateRequestUser`, and four decorators built with `CreateAuthDecorator` from `@antelopejs/interface-auth`: `AuthRawUser`, `AuthUser`, `IfAuthUser` and `AuthOwnerOnly`.
11
+
12
+ All of them read the bearer token from the request's `Authorization` header (`Authorization: Bearer <token>`) and run the same two steps:
13
+
14
+ 1. **Authenticator** — decodes the token into a `TenantTokenInput`: the user id, the tenant id, and the undecoded token itself.
15
+ 2. **Validator** — loads the user named by that id, verifies the token signature against that user, and applies the extra requirement the decorator stands for.
16
+
17
+ Whatever the validator returns is what gets injected. The four decorators differ only in the requirement they enforce and in what they do when it fails.
18
+
19
+ The `User` type they inject comes from the `/db` subpath and is documented in [Users](/docs/interfaces/auth/users). The tokens they consume are produced by the generators in [Tokens and Sessions](/docs/interfaces/auth/tokens-and-sessions).
20
+
21
+ ## Require an Authenticated User
22
+
23
+ `AuthUser` injects the `User` and enforces email validation when the deployment's authentication configuration requires it. Use it for any route that must have a signed-in caller.
24
+
25
+ ```typescript
26
+ import { Controller, Get } from "@antelopejs/interface-api";
27
+ import { AuthUser } from "@antelopejs/interface-dms/auth";
28
+ import type { User } from "@antelopejs/interface-dms/auth/db";
29
+
30
+ export class ProfileController extends Controller("/api/profile") {
31
+ @Get("")
32
+ getProfile(@AuthUser() user: User): { id: string; email: string } {
33
+ // 401 before the handler runs when the token is missing or invalid
34
+ return { id: user._id, email: user.email };
35
+ }
36
+ }
37
+ ```
38
+
39
+ The injected value is the full `User` row, including fields you do not want to return over HTTP. Pass it through `sanitizeUser` before serializing it — see [Users](/docs/interfaces/auth/users).
40
+
41
+ ## Skip the Email-Validation Gate
42
+
43
+ `AuthRawUser` resolves the same `User` but does not apply the email-validation requirement. It is what the routes that exist *because* the email is not validated yet need — reading the current account, logging out, requesting a new validation mail — and it is also what modules use to gate their own API routes on a signed-in caller.
44
+
45
+ ```typescript
46
+ import { Controller, Get, Post } from "@antelopejs/interface-api";
47
+ import {
48
+ AuthRawUser,
49
+ sendEmailValidationEmail,
50
+ } from "@antelopejs/interface-dms/auth";
51
+ import type { User } from "@antelopejs/interface-dms/auth/db";
52
+
53
+ export class AccountController extends Controller("/api/account") {
54
+ @Get("/me")
55
+ me(@AuthRawUser() user: User): { email: string; isValidated: boolean } {
56
+ return { email: user.email, isValidated: user.isValidated };
57
+ }
58
+
59
+ @Post("/resend-validation")
60
+ async resendValidation(@AuthRawUser() user: User): Promise<{ sent: true }> {
61
+ // reachable while user.isValidated is still false
62
+ await sendEmailValidationEmail(user);
63
+ return { sent: true };
64
+ }
65
+ }
66
+ ```
67
+
68
+ Both decorators inject the raw database row; neither strips sensitive fields. "Raw" here refers to skipping the email-validation step, not to the shape of the object.
69
+
70
+ ## Accept Anonymous Callers
71
+
72
+ `IfAuthUser` is the optional variant. It injects `User | undefined` and does not reject the request: a missing token, a malformed token and a token that fails validation all resolve to `undefined`.
73
+
74
+ ```typescript
75
+ import { Controller, Get } from "@antelopejs/interface-api";
76
+ import { IfAuthUser } from "@antelopejs/interface-dms/auth";
77
+ import type { User } from "@antelopejs/interface-dms/auth/db";
78
+
79
+ export class ArticleController extends Controller("/api/articles") {
80
+ @Get("")
81
+ list(@IfAuthUser() user: User | undefined) {
82
+ // a missing, malformed or expired token all land here as `undefined`
83
+ return user
84
+ ? { scope: "member", readerId: user._id }
85
+ : { scope: "anonymous" };
86
+ }
87
+ }
88
+ ```
89
+
90
+ Because an expired token resolves to `undefined` rather than a 401, a client-side refresh interceptor keyed on 401 responses does not fire for a route guarded this way. When a route must hard-fail on an expired session, guard it with `AuthUser` and give the anonymous path its own route.
91
+
92
+ ## Restrict a Route to Platform Owners
93
+
94
+ `AuthOwnerOnly` requires the authenticated user to be a platform owner (`User.owner === true`). It answers `401` when no token is provided and `403` when the caller is authenticated but not an owner. The DMS applies it to pages under `/modules/`, which are owner-only administration spaces.
95
+
96
+ ```typescript
97
+ import { Controller, Get } from "@antelopejs/interface-api";
98
+ import { AuthOwnerOnly } from "@antelopejs/interface-dms/auth";
99
+ import type { User } from "@antelopejs/interface-dms/auth/db";
100
+
101
+ export class MaintenanceController extends Controller("/api/maintenance") {
102
+ private jobs: string[] = ["reindex", "purge-sessions"];
103
+
104
+ @Get("/jobs")
105
+ listJobs(@AuthOwnerOnly() owner: User) {
106
+ // only reached when owner.owner === true
107
+ return { requestedBy: owner.email, jobs: this.jobs };
108
+ }
109
+ }
110
+ ```
111
+
112
+ `AuthOwnerOnly` is a plain function that returns a ready-to-apply decorator, so it is written `@AuthOwnerOnly()` exactly like the three decorator factories above. It takes no arguments, because it pre-applies its own validator.
113
+
114
+ ## Gate Every Route on a Controller
115
+
116
+ `AuthRawUser`, `AuthUser` and `IfAuthUser` are built with `CreateAuthDecorator`, so they apply to a parameter, to a property, or to the controller class itself. A property provider is a computed property of the controller, resolved on every request the controller handles whether or not the handler reads it — so a decorated property gates the whole controller and gives the handlers a typed `this.user`.
117
+
118
+ ```typescript
119
+ import { Controller, Get, JSONBody, Post } from "@antelopejs/interface-api";
120
+ import { AuthUser } from "@antelopejs/interface-dms/auth";
121
+ import type { User } from "@antelopejs/interface-dms/auth/db";
122
+
123
+ interface Settings {
124
+ theme: string;
125
+ }
126
+
127
+ export class SettingsController extends Controller("/api/settings") {
128
+ @AuthUser()
129
+ declare user: User;
130
+
131
+ private settings: Record<string, Settings> = {};
132
+
133
+ @Get("")
134
+ read(): Settings {
135
+ return this.settings[this.user._id] ?? { theme: "light" };
136
+ }
137
+
138
+ @Post("")
139
+ write(@JSONBody() body: Settings): Settings {
140
+ // also authenticated: the provider ran before the handler
141
+ this.settings[this.user._id] = body;
142
+ return body;
143
+ }
144
+ }
145
+ ```
146
+
147
+ This is the form the DMS uses when it wires authentication onto a page controller: the decorator is applied to the `user` property of the page class.
148
+
149
+ ## Read the Token Payload
150
+
151
+ `TenantTokenInput` is the value the authenticator step produces and the validator step consumes. It is exported from the root subpath.
152
+
153
+ ```typescript
154
+ export interface TenantTokenInput {
155
+ tenantId: string;
156
+ id: string;
157
+ rawToken: string;
158
+ }
159
+ ```
160
+
161
+ | Field | Type | Description |
162
+ | ----- | ---- | ----------- |
163
+ | `tenantId` | `string` | The tenant the token was issued for, taken from the token's `tenantId` claim. |
164
+ | `id` | `string` | The `_id` of the user the token was issued for. |
165
+ | `rawToken` | `string` | The undecoded bearer token, used to verify the signature against the user. |
166
+
167
+ The decoded payload carries no signature guarantee on its own: the validator is the step that verifies `rawToken` against the user it names.
168
+
169
+ ## Authenticate Inside a Custom Provider
170
+
171
+ `authenticateRequestUser` performs the same authenticate-then-validate sequence outside the decorator machinery. Use it when you write your own parameter provider and need the user plus something else in one place.
172
+
173
+ ```typescript
174
+ authenticateRequestUser(ctx: RequestContext): Promise<User>
175
+ ```
176
+
177
+ | Parameter | Description |
178
+ | --------- | ----------- |
179
+ | `ctx` | The request context handed to a parameter provider, from `@antelopejs/interface-api`. The function reads `ctx.rawRequest.headers.authorization`. |
180
+
181
+ It runs the same validation as `AuthUser`, so the email-validation gate applies: a caller whose email is not validated is rejected when the deployment's configuration requires validation. Every failure throws an `HTTPResult` with status `401`.
182
+
183
+ ```typescript
184
+ import {
185
+ HTTPResult,
186
+ type RequestContext,
187
+ SetParameterProvider,
188
+ } from "@antelopejs/interface-api";
189
+ import { MakeParameterAndPropertyDecorator } from "@antelopejs/interface-core/decorators";
190
+ import { authenticateRequestUser } from "@antelopejs/interface-dms/auth";
191
+
192
+ const FORBIDDEN = 403;
193
+
194
+ export const AuthBetaTester = MakeParameterAndPropertyDecorator(
195
+ (target, key, index) => {
196
+ SetParameterProvider(target, key, index, async (ctx: RequestContext) => {
197
+ const user = await authenticateRequestUser(ctx);
198
+ if (!user.email.endsWith("@example.com")) {
199
+ throw new HTTPResult(FORBIDDEN, "Beta testers only");
200
+ }
201
+ return user;
202
+ });
203
+ },
204
+ );
205
+ ```
206
+
207
+ This is how the DMS builds its tenant guards: `AuthTenantOwner` and `AuthTenantMember` in `@antelopejs/interface-dms/guards` both start from `authenticateRequestUser`, then resolve the request's tenant and check the caller's membership on top of it.
208
+
209
+ A provider is also the way to add a requirement without losing the built-in one. The `CreateAuthDecorator` contract lets a call site pass a replacement validator — `@AuthUser(callback)` — but that callback replaces the whole validation step, signature check included, and its return type is constrained to `boolean | User`. This interface exports no token-verification primitive, so a replacement validator cannot redo the signature check it removes.
210
+
211
+ ## Decorator Reference
212
+
213
+ | Decorator | Injects | Email validation | On failure |
214
+ | --------- | ------- | ---------------- | ---------- |
215
+ | `AuthRawUser()` | `User` | Not checked | `401` |
216
+ | `AuthUser()` | `User` | Enforced when the configuration requires it | `401` |
217
+ | `IfAuthUser()` | `User \| undefined` | Enforced when the configuration requires it | Resolves to `undefined` |
218
+ | `AuthOwnerOnly()` | `User` | Enforced when the configuration requires it | `401` without a token, `403` when not an owner |
219
+
220
+ ## Next Steps
221
+
222
+ Continue with [Tokens and Sessions](/docs/interfaces/auth/tokens-and-sessions) to see how the tokens these decorators consume are issued and validated.
@@ -0,0 +1,352 @@
1
+ ---
2
+ title: "Tokens and Sessions"
3
+ description: "The interface issues four kinds of token, and it records a login as a row in the `sessions` table."
4
+ ---
5
+
6
+ # Tokens and Sessions
7
+
8
+ ## Overview
9
+
10
+ The interface issues four kinds of token, and it records a login as a row in the `sessions` table. Three of the four come with an exported validator; the access token is validated by the authentication decorators instead.
11
+
12
+ | Token | Generated by | Validated by | Carries |
13
+ | ----- | ------------ | ------------ | ------- |
14
+ | Access | `generateAccessToken` | The authentication decorators in [Authentication](/docs/interfaces/auth/authentication) | `id`, `tenantId`, optional `sessionId` |
15
+ | Refresh | `generateRefreshToken` | `validateRefreshToken` | `id`, `tenantId`, optional `sessionId` |
16
+ | Two-factor | `generateTwoFactorToken` | `validateTwoFactorToken` | `id`, `tenantId`, a purpose marker |
17
+ | Tenant assignment | `generateTenantAssignmentToken` | `validateTenantAssignmentToken` | `id`, a purpose marker |
18
+
19
+ Every generator takes the `User` it signs for, and every validator loads that user back before it returns. All seven are interface functions, so every call returns a promise.
20
+
21
+ Access and refresh tokens are the pair a client holds: the access token goes in the `Authorization` header of ordinary requests, and the refresh token buys a new access token when it expires. Two-factor and tenant-assignment tokens are short-lived hand-offs that stand between a password check and a real session.
22
+
23
+ ## Issue an Access and Refresh Token Pair
24
+
25
+ ```typescript
26
+ generateAccessToken(
27
+ tenantId: string,
28
+ user: User,
29
+ sessionId?: string,
30
+ ): Promise<{ expiresIn: number; token: string }>
31
+
32
+ generateRefreshToken(
33
+ tenantId: string,
34
+ user: User,
35
+ sessionId?: string,
36
+ ): Promise<{ expiresIn: number; token: string }>
37
+ ```
38
+
39
+ | Parameter | Description |
40
+ | --------- | ----------- |
41
+ | `tenantId` | The tenant the token is scoped to. It lands in the token's `tenantId` claim and is what the request's tenant resolution reads back. |
42
+ | `user` | The `User` the token is issued for. The user's `authKey` takes part in the signing secret, so rotating `authKey` invalidates every token previously issued to that user. |
43
+ | `sessionId` | The `_id` of the `Session` row that records this login. Optional: a token issued without one is valid but is not tied to a revocable session. An access token issued with one is refused as soon as that row is deleted. |
44
+
45
+ Both return the signed `token` and its lifetime as `expiresIn` in milliseconds, taken from the deployment's `accessTokenLifetime` and `refreshTokenLifetime` configuration.
46
+
47
+ ```typescript
48
+ import {
49
+ createSession,
50
+ generateAccessToken,
51
+ generateRefreshToken,
52
+ } from "@antelopejs/interface-dms/auth";
53
+ import type {
54
+ SessionModel,
55
+ User,
56
+ } from "@antelopejs/interface-dms/auth/db";
57
+
58
+ export async function startSession(
59
+ sessionModel: SessionModel,
60
+ user: User,
61
+ tenantId: string,
62
+ userAgent: string,
63
+ ip: string,
64
+ ) {
65
+ const sessionId = await createSession(sessionModel, user._id, userAgent, ip);
66
+
67
+ const access = await generateAccessToken(tenantId, user, sessionId);
68
+ const refresh = await generateRefreshToken(tenantId, user, sessionId);
69
+
70
+ // store the token's hash on the session so it can be matched and revoked
71
+ await sessionModel.replaceRefreshToken(sessionId, refresh.token);
72
+
73
+ return {
74
+ token_type: "Bearer",
75
+ access_token: access.token,
76
+ expires_in: access.expiresIn,
77
+ refresh_token: refresh.token,
78
+ };
79
+ }
80
+ ```
81
+
82
+ ## Validate a Refresh Token
83
+
84
+ ```typescript
85
+ validateRefreshToken(
86
+ token: string,
87
+ ): Promise<{ id: string; tenantId: string; sessionId?: string }>
88
+ ```
89
+
90
+ The function decodes the token, loads the user it names, and verifies the signature against that user. It throws `HTTPResult(401)` when the user no longer exists or the signature does not check out. It returns the claims only — confirming that the session still exists, that the token is still its current one (`isCurrentRefreshToken`), and that the user still has access to `tenantId` is the caller's job.
91
+
92
+ ```typescript
93
+ import { HTTPResult } from "@antelopejs/interface-api";
94
+ import {
95
+ generateAccessToken,
96
+ validateRefreshToken,
97
+ } from "@antelopejs/interface-dms/auth";
98
+ import {
99
+ isCurrentRefreshToken,
100
+ type SessionModel,
101
+ type UserModel,
102
+ } from "@antelopejs/interface-dms/auth/db";
103
+
104
+ const UNAUTHORIZED = 401;
105
+
106
+ export async function refresh(
107
+ userModel: UserModel,
108
+ sessionModel: SessionModel,
109
+ token: string,
110
+ ) {
111
+ const payload = await validateRefreshToken(token);
112
+
113
+ const user = await userModel.get(payload.id);
114
+ if (!user) throw new HTTPResult(UNAUTHORIZED, "error.invalid_token");
115
+
116
+ if (payload.sessionId) {
117
+ const session = await sessionModel.get(payload.sessionId);
118
+ if (!session || !isCurrentRefreshToken(session, token)) {
119
+ throw new HTTPResult(UNAUTHORIZED, "error.session_expired");
120
+ }
121
+ await sessionModel.update(payload.sessionId, { lastActiveAt: new Date() });
122
+ }
123
+
124
+ const access = await generateAccessToken(
125
+ payload.tenantId,
126
+ user,
127
+ payload.sessionId,
128
+ );
129
+ return { access_token: access.token, expires_in: access.expiresIn };
130
+ }
131
+ ```
132
+
133
+ Because the returned payload names a `tenantId`, the same call backs a tenant switch: validate the refresh token, check the user's membership of the target tenant, then issue a new pair for that tenant instead.
134
+
135
+ ## Hand Off to a Second Factor
136
+
137
+ When a user's credentials check out but the account has second factors enabled, issue a two-factor token instead of a session and let the client come back with the code.
138
+
139
+ ```typescript
140
+ generateTwoFactorToken(
141
+ tenantId: string,
142
+ user: User,
143
+ ): Promise<{ token: string; expiresIn: number }>
144
+
145
+ validateTwoFactorToken(token: string): Promise<{
146
+ id: string;
147
+ tenantId: string;
148
+ purpose?: string;
149
+ user: User;
150
+ }>
151
+ ```
152
+
153
+ `validateTwoFactorToken` rejects a token that was not issued for the two-factor purpose, and resolves the `User` for you so the verification route can read `twoFactorMethods`, `twoFactorSecret` and the rest without a second lookup. Whatever is wrong with the token (not a JWT, unknown user, bad signature, expired, another purpose), it throws the same `HTTPResult(401, "error.invalid_2fa_token")`, so a caller cannot tell which user ids exist. `expiresIn` is the token's lifetime in milliseconds.
154
+
155
+ ```typescript
156
+ import { createHash } from "node:crypto";
157
+ import {
158
+ generateTwoFactorToken,
159
+ send2FAEmail,
160
+ validateTwoFactorToken,
161
+ } from "@antelopejs/interface-dms/auth";
162
+ import type {
163
+ User,
164
+ UserModel,
165
+ } from "@antelopejs/interface-dms/auth/db";
166
+
167
+ export async function beginTwoFactor(tenantId: string, user: User) {
168
+ const { token, expiresIn } = await generateTwoFactorToken(tenantId, user);
169
+ return {
170
+ requires_2fa: true,
171
+ two_factor_token: token,
172
+ expires_in: expiresIn,
173
+ methods: user.twoFactorMethods,
174
+ };
175
+ }
176
+
177
+ export async function requestEmailCode(
178
+ userModel: UserModel,
179
+ twoFactorToken: string,
180
+ code: string,
181
+ ) {
182
+ const { user } = await validateTwoFactorToken(twoFactorToken);
183
+
184
+ // Stored hashed, the way the DMS's own verification route reads it back.
185
+ user.twoFactorEmailCode = createHash("sha256")
186
+ .update(JSON.stringify([user._id, code]))
187
+ .digest("hex");
188
+ user.twoFactorEmailCodeRequestedAt = new Date();
189
+ await userModel.update(user);
190
+
191
+ await send2FAEmail(user, code);
192
+ }
193
+ ```
194
+
195
+ Once the code checks out, the route issues a real access and refresh pair with the `tenantId` the two-factor token carried. `send2FAEmail` and the other mail entry points are covered in [Users](/docs/interfaces/auth/users).
196
+
197
+ ## Hand Off Tenant Assignment
198
+
199
+ A user whose credentials are valid but who belongs to no tenant yet cannot receive an access token, because an access token is always scoped to a tenant. `generateTenantAssignmentToken` covers that gap: it signs the user without a tenant claim so an onboarding flow can pick or create one.
200
+
201
+ ```typescript
202
+ generateTenantAssignmentToken(
203
+ user: User,
204
+ ): Promise<{ token: string; expiresIn: number }>
205
+
206
+ validateTenantAssignmentToken(token: string): Promise<{ id: string; user: User }>
207
+ ```
208
+
209
+ `validateTenantAssignmentToken` rejects a token that was not issued for the tenant-assignment purpose and returns the user id together with the loaded `User`. Like the two-factor check, every refusal is the same `HTTPResult(401, "Invalid tenant assignment token")`.
210
+
211
+ ```typescript
212
+ import {
213
+ generateAccessToken,
214
+ generateTenantAssignmentToken,
215
+ validateTenantAssignmentToken,
216
+ } from "@antelopejs/interface-dms/auth";
217
+ import type { User } from "@antelopejs/interface-dms/auth/db";
218
+
219
+ export async function offerTenantAssignment(user: User) {
220
+ const { token } = await generateTenantAssignmentToken(user);
221
+ return { requires_tenant_assignment: true, tenant_assignment_token: token };
222
+ }
223
+
224
+ export async function completeTenantAssignment(
225
+ assignmentToken: string,
226
+ tenantId: string,
227
+ ) {
228
+ const { user } = await validateTenantAssignmentToken(assignmentToken);
229
+ // the caller attaches the user to `tenantId` before issuing real tokens
230
+ return generateAccessToken(tenantId, user);
231
+ }
232
+ ```
233
+
234
+ ## The Token Payload
235
+
236
+ `JwtUserPayload` describes the claims an access or refresh token carries about its subject. Use it as the target type when you decode a token yourself.
237
+
238
+ ```typescript
239
+ export interface JwtUserPayload {
240
+ id: string;
241
+ tenantId: string;
242
+ sessionId?: string;
243
+ }
244
+ ```
245
+
246
+ | Field | Type | Description |
247
+ | ----- | ---- | ----------- |
248
+ | `id` | `string` | The `_id` of the user the token was issued for. |
249
+ | `tenantId` | `string` | The tenant the token is scoped to. |
250
+ | `sessionId` | `string \| undefined` | The `Session` row backing the token, when the token was issued with one. |
251
+
252
+ `TenantTokenInput` from [Authentication](/docs/interfaces/auth/authentication) names the same subject from the other side: it is what a validator receives — `id` and `tenantId` plus the undecoded `rawToken` — while `JwtUserPayload` is what the token itself says. The two overlap on `id` and `tenantId`; neither is a subset of the other.
253
+
254
+ ## Record a Login Session
255
+
256
+ `createSession` inserts a `Session` row for a login and returns its `_id`, ready to be passed as the `sessionId` of the token pair.
257
+
258
+ ```typescript
259
+ createSession(
260
+ sessionModel: SessionModel,
261
+ userId: string,
262
+ userAgent: string,
263
+ ip: string,
264
+ ): Promise<string>
265
+ ```
266
+
267
+ | Parameter | Description |
268
+ | --------- | ----------- |
269
+ | `sessionModel` | The `SessionModel` instance to insert through. |
270
+ | `userId` | The `_id` of the user logging in. |
271
+ | `userAgent` | The request's `User-Agent` header. It is parsed into the `browser`, `os` and `deviceType` fields; the raw string is kept in `userAgent`. |
272
+ | `ip` | The caller's IP address, stored as-is. |
273
+
274
+ The row starts with an empty `refreshToken` and `location`, and with `createdAt` and `lastActiveAt` set to the current time. Record the refresh token with `sessionModel.replaceRefreshToken` once you have generated it, as in the first example on this page.
275
+
276
+ ## The Session Table
277
+
278
+ `Session` is registered as the `sessions` table under the DMS core schema, so its rows are shared across tenants and its model is retrieved without a tenant instance. The class, its table-name constant and its model all live on the `/db` subpath.
279
+
280
+ ```typescript
281
+ import {
282
+ SESSIONS_TABLE_NAME,
283
+ Session,
284
+ SessionModel,
285
+ } from "@antelopejs/interface-dms/auth/db";
286
+ ```
287
+
288
+ | Field | Type | Description |
289
+ | ----- | ---- | ----------- |
290
+ | `_id` | `string` | Primary key. This is the value that travels in the token's `sessionId` claim. |
291
+ | `userId` | `string` | The owning user's `_id`. Indexed, and declared as a relation to the `User` table. |
292
+ | `refreshTokenHash` | `string \| null` | sha256 of the refresh token currently valid for this session. Matching it on refresh is what makes a session revocable; the token itself is never stored. |
293
+ | `sealedRefreshToken` | `string \| null` | The current refresh token encrypted under a key derived from its predecessor, so a client still holding the predecessor during the rotation grace window can be handed the current token. Unreadable without the predecessor. |
294
+ | `userAgent` | `string` | The raw `User-Agent` string captured at login. |
295
+ | `ip` | `string` | The IP address captured at login. |
296
+ | `browser` | `string` | Browser name and version parsed from `userAgent`, or `"Unknown"`. |
297
+ | `os` | `string` | OS name and version parsed from `userAgent`, or `"Unknown"`. |
298
+ | `deviceType` | `string` | Device class parsed from `userAgent`: `"mobile"`, `"tablet"`, or `"desktop"` when neither applies. |
299
+ | `location` | `string` | Free-form location label. `createSession` leaves it empty. |
300
+ | `createdAt` | `Date` | Set on insert by the creation-time decorator. Indexed. |
301
+ | `lastActiveAt` | `Date` | Last time the session was seen. Refresh handlers update it. Indexed: a daily job deletes the sessions last active longer ago than `refreshTokenLifetime`. |
302
+
303
+ `SESSIONS_TABLE_NAME` holds the table name, `"sessions"`.
304
+
305
+ `isCurrentRefreshToken(session, token)` tells whether a presented token is the session's current one, comparing hashes in constant time and still honouring a session written before hashing.
306
+
307
+ ## Query Sessions with SessionModel
308
+
309
+ `SessionModel` extends `BasicDataModel(Session, SESSIONS_TABLE_NAME)`, so it has the standard `get`, `getBy`, `getAll`, `insert`, `update` and `delete` operations, plus the refresh-token operations and two lookups by user.
310
+
311
+ | Method | Signature | Description |
312
+ | ------ | --------- | ----------- |
313
+ | `replaceRefreshToken` | `(sessionId: string, refreshToken: string) => Promise<void>` | Make a token the session's current one, storing only its hash. |
314
+ | `rotateRefreshToken` | `(sessionId: string, presentedToken: string, refreshToken: string, rotatedAt?: Date) => Promise<string \| null>` | Atomically replace the presented token with a new one. A client presenting the immediate predecessor within the grace window gets the current token back; any other token gets `null`. |
315
+ | `deleteInactiveSince` | `(threshold: Date) => Promise<void>` | Delete the sessions last active before a date. |
316
+ | `getByUserId` | `(userId: string) => Promise<Session[]>` | Every session belonging to a user, read through the `userId` index. |
317
+ | `deleteByUserId` | `(userId: string) => Promise<void>` | Delete every session belonging to a user. |
318
+
319
+ ```typescript
320
+ import { Controller, Delete, Get } from "@antelopejs/interface-api";
321
+ import { Model } from "@antelopejs/interface-database-decorators";
322
+ import { AuthUser } from "@antelopejs/interface-dms/auth";
323
+ import {
324
+ SessionModel,
325
+ type User,
326
+ } from "@antelopejs/interface-dms/auth/db";
327
+
328
+ export class SessionsController extends Controller("/api/sessions") {
329
+ @AuthUser()
330
+ declare user: User;
331
+
332
+ @Model(SessionModel)
333
+ declare sessionModel: SessionModel;
334
+
335
+ @Get("")
336
+ list() {
337
+ return this.sessionModel.getByUserId(this.user._id);
338
+ }
339
+
340
+ @Delete("")
341
+ revokeAll() {
342
+ // drops every session; their access tokens stop working with them
343
+ return this.sessionModel.deleteByUserId(this.user._id);
344
+ }
345
+ }
346
+ ```
347
+
348
+ Deleting a session stops refreshes from succeeding and cuts off the access tokens issued for it at once: the authentication decorators refuse an access token whose `sessionId` no longer names a session of its user. A token issued without a `sessionId` stays valid until it expires; to cut those off too, rotate the user's `authKey` as described in [Users](/docs/interfaces/auth/users).
349
+
350
+ ## Next Steps
351
+
352
+ Continue with [Users](/docs/interfaces/auth/users) for the `User` table, `UserModel`, `sanitizeUser`, and the account mail entry points.