rlz-engine 1.0.53 → 1.0.55
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 +225 -0
- package/dist/back/auth/admin-secret.d.ts +4 -0
- package/dist/back/auth/admin-secret.js +86 -0
- package/dist/back/auth/admin-secret.js.map +1 -0
- package/dist/back/auth/cookies.d.ts +12 -0
- package/dist/back/auth/cookies.js +20 -0
- package/dist/back/auth/cookies.js.map +1 -0
- package/dist/back/auth/csrf.d.ts +8 -0
- package/dist/back/auth/csrf.js +47 -0
- package/dist/back/auth/csrf.js.map +1 -0
- package/dist/back/auth/model.d.ts +7 -3
- package/dist/back/auth/module.d.ts +14 -0
- package/dist/back/auth/module.js +121 -0
- package/dist/back/auth/module.js.map +1 -0
- package/dist/back/auth/rate-limit.d.ts +11 -0
- package/dist/back/auth/rate-limit.js +59 -0
- package/dist/back/auth/rate-limit.js.map +1 -0
- package/dist/back/auth/session.d.ts +22 -0
- package/dist/back/auth/session.js +55 -0
- package/dist/back/auth/session.js.map +1 -0
- package/dist/back/auth/storage.d.ts +9 -9
- package/dist/back/auth/storage.js +39 -36
- package/dist/back/auth/storage.js.map +1 -1
- package/dist/back/auth/utils.d.ts +10 -8
- package/dist/back/auth/utils.js +43 -56
- package/dist/back/auth/utils.js.map +1 -1
- package/dist/back/certificate/adm-rpc.d.ts +5 -0
- package/dist/back/certificate/adm-rpc.js +52 -0
- package/dist/back/certificate/adm-rpc.js.map +1 -0
- package/dist/back/certificate/module.d.ts +13 -0
- package/dist/back/certificate/module.js +19 -0
- package/dist/back/certificate/module.js.map +1 -0
- package/dist/back/cli/cli-engine.d.ts +25 -0
- package/dist/back/cli/cli-engine.js +68 -0
- package/dist/back/cli/cli-engine.js.map +1 -0
- package/dist/back/http/module.d.ts +7 -0
- package/dist/back/http/module.js +34 -0
- package/dist/back/http/module.js.map +1 -0
- package/dist/back/http/server.d.ts +22 -0
- package/dist/back/http/server.js +126 -0
- package/dist/back/http/server.js.map +1 -0
- package/dist/back/logger/adm-rpc.d.ts +2 -0
- package/dist/back/logger/adm-rpc.js +50 -0
- package/dist/back/logger/adm-rpc.js.map +1 -0
- package/dist/back/logger/audit-logger.d.ts +97 -0
- package/dist/back/logger/audit-logger.js +155 -0
- package/dist/back/logger/audit-logger.js.map +1 -0
- package/dist/back/logger/cli.d.ts +2 -0
- package/dist/back/logger/cli.js +43 -0
- package/dist/back/logger/cli.js.map +1 -0
- package/dist/back/logger/format.d.ts +3 -0
- package/dist/back/logger/format.js +27 -0
- package/dist/back/logger/format.js.map +1 -0
- package/dist/back/logger/module.d.ts +14 -0
- package/dist/back/logger/module.js +24 -0
- package/dist/back/logger/module.js.map +1 -0
- package/dist/back/logger/sqlite-storage.d.ts +27 -0
- package/dist/back/logger/sqlite-storage.js +169 -0
- package/dist/back/logger/sqlite-storage.js.map +1 -0
- package/dist/back/logger/system-logger.d.ts +27 -0
- package/dist/back/logger/system-logger.js +82 -0
- package/dist/back/logger/system-logger.js.map +1 -0
- package/dist/back/module/composer.d.ts +34 -0
- package/dist/back/module/composer.js +57 -0
- package/dist/back/module/composer.js.map +1 -0
- package/dist/back/rpc/gen.d.ts +2 -1
- package/dist/back/rpc/gen.js +38 -17
- package/dist/back/rpc/gen.js.map +1 -1
- package/dist/back/rpc/module.d.ts +14 -0
- package/dist/back/rpc/module.js +43 -0
- package/dist/back/rpc/module.js.map +1 -0
- package/dist/back/rpc/rpc.d.ts +53 -6
- package/dist/back/rpc/rpc.js +72 -29
- package/dist/back/rpc/rpc.js.map +1 -1
- package/dist/back/storage/db.js +5 -3
- package/dist/back/storage/db.js.map +1 -1
- package/dist/back/storage/model.d.ts +2 -2
- package/dist/back/storage/model.js +2 -2
- package/dist/back/storage/model.js.map +1 -1
- package/dist/back/storage/sync.d.ts +5 -2
- package/dist/back/storage/sync.js +1 -3
- package/dist/back/storage/sync.js.map +1 -1
- package/dist/client/adm/admin.d.ts +17 -0
- package/dist/client/adm/admin.js +62 -0
- package/dist/client/adm/admin.js.map +1 -0
- package/dist/client/adm/audit-logs.d.ts +5 -0
- package/dist/client/adm/audit-logs.js +68 -0
- package/dist/client/adm/audit-logs.js.map +1 -0
- package/dist/client/adm/certificate.d.ts +5 -0
- package/dist/client/adm/certificate.js +48 -0
- package/dist/client/adm/certificate.js.map +1 -0
- package/dist/client/config.d.ts +2 -0
- package/dist/client/config.js +7 -0
- package/dist/client/config.js.map +1 -1
- package/dist/client/rpc.d.ts +4 -4
- package/dist/client/rpc.js +19 -7
- package/dist/client/rpc.js.map +1 -1
- package/dist/client/screens/404.js.map +1 -1
- package/dist/client/screens/SignupSigninScreen.d.ts +13 -1
- package/dist/client/screens/SignupSigninScreen.js +41 -37
- package/dist/client/screens/SignupSigninScreen.js.map +1 -1
- package/dist/client/state/auth.d.ts +13 -7
- package/dist/client/state/auth.js +24 -19
- package/dist/client/state/auth.js.map +1 -1
- package/dist/client/sync.d.ts +7 -2
- package/dist/client/sync.js +5 -3
- package/dist/client/sync.js.map +1 -1
- package/dist/client/widgets/ItemsSelect.js +15 -8
- package/dist/client/widgets/ItemsSelect.js.map +1 -1
- package/package.json +87 -77
- package/dist/back/auth/controllers.d.ts +0 -7
- package/dist/back/auth/controllers.js +0 -39
- package/dist/back/auth/controllers.js.map +0 -1
- package/dist/back/esbuild-plugin-pino.d.ts +0 -6
- package/dist/back/esbuild-plugin-pino.js +0 -87
- package/dist/back/esbuild-plugin-pino.js.map +0 -1
- package/dist/back/logger.d.ts +0 -7
- package/dist/back/logger.js +0 -43
- package/dist/back/logger.js.map +0 -1
- package/dist/back/server.d.ts +0 -17
- package/dist/back/server.js +0 -94
- package/dist/back/server.js.map +0 -1
- package/dist/client/api/api.d.ts +0 -22
- package/dist/client/api/api.js +0 -87
- package/dist/client/api/api.js.map +0 -1
- package/dist/client/api/auth.d.ts +0 -5
- package/dist/client/api/auth.js +0 -22
- package/dist/client/api/auth.js.map +0 -1
- package/dist/shared/api/auth.d.ts +0 -19
- package/dist/shared/api/auth.js +0 -17
- package/dist/shared/api/auth.js.map +0 -1
- package/dist/shared/api/sync.d.ts +0 -21
- package/dist/shared/api/sync.js +0 -22
- package/dist/shared/api/sync.js.map +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# rlz-engine
|
|
2
|
+
|
|
3
|
+
`rlz-engine` is a TypeScript toolkit for small web applications. It provides Fastify server setup, MongoDB helpers, schema-validated RPC, browser RPC transport, authentication helpers, audit logging, synchronization utilities, and reusable CLI modules.
|
|
4
|
+
|
|
5
|
+
The package is an opt-in library: applications choose which capabilities to compose. It does not provide an application executable or automatically register its routes and CLI commands.
|
|
6
|
+
|
|
7
|
+
## Requirements and installation
|
|
8
|
+
|
|
9
|
+
- Node.js 22.13 or newer
|
|
10
|
+
- MongoDB for the MongoDB storage helpers
|
|
11
|
+
- React and React DOM when using the React client components
|
|
12
|
+
|
|
13
|
+
Install the package and its peer dependencies as needed:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install rlz-engine
|
|
17
|
+
npm install react react-dom
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The package exports compiled files through subpaths under `rlz-engine/back/*`, `rlz-engine/client/*`, and `rlz-engine/shared/*`. Import implementation modules directly; for example:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { rpc } from 'rlz-engine/back/rpc/rpc.js'
|
|
24
|
+
import { HTTP_MODULE } from 'rlz-engine/back/http/module.js'
|
|
25
|
+
import { syncItems } from 'rlz-engine/client/sync.js'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Feature modules
|
|
29
|
+
|
|
30
|
+
Each backend feature lives in a cohesive directory and exposes an uppercase `<FEATURE>_MODULE` object from `module.ts`. The object contains only the integration factories that feature supports. Applications invoke the factories for the integrations they want, then pass the returned facet values to `composeModules()`.
|
|
31
|
+
|
|
32
|
+
| Module | Facets | Purpose |
|
|
33
|
+
| --------------- | ----------------------------------------- | ------------------------------------------------------------------- |
|
|
34
|
+
| `AUTH_MODULE` | `makeRpc(storage, options?)`, `makeCli()` | Signup/signin/session/logout RPC endpoints and admin secret command |
|
|
35
|
+
| `HTTP_MODULE` | `makeCli(params, init)` | `http start` server command |
|
|
36
|
+
| `LOGGER_MODULE` | `makeCli()`, `makeAdmRpc()` | Audit-log reader command and protected audit query endpoint |
|
|
37
|
+
| `RPC_MODULE` | `makeDevCli(options)` | Generate user and administrative RPC clients during development |
|
|
38
|
+
|
|
39
|
+
The composer groups selected operational CLI, development CLI, user RPC, and administrative RPC facets. It rejects duplicate names within each facet kind before startup. It deliberately leaves factory arguments with the application: server settings, route handlers, working directories, and generated-client paths are application-specific. The React administration UI and certificate-status module remain planned; the administrative RPC transport and audit-log endpoint are available now.
|
|
40
|
+
|
|
41
|
+
### CLI composition
|
|
42
|
+
|
|
43
|
+
`CliEngine` provides operational commands. `DevCliEngine` is a separate entry point for development commands, such as generated-client updates.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { CliEngine, DevCliEngine } from 'rlz-engine/back/cli/cli-engine.js'
|
|
47
|
+
import { composeModules } from 'rlz-engine/back/module/composer.js'
|
|
48
|
+
import { AUTH_MODULE } from 'rlz-engine/back/auth/module.js'
|
|
49
|
+
import { HTTP_MODULE } from 'rlz-engine/back/http/module.js'
|
|
50
|
+
import { LOGGER_MODULE } from 'rlz-engine/back/logger/module.js'
|
|
51
|
+
import { RPC_MODULE } from 'rlz-engine/back/rpc/module.js'
|
|
52
|
+
|
|
53
|
+
const applicationModules = composeModules({
|
|
54
|
+
cli: [AUTH_MODULE.makeCli(), HTTP_MODULE.makeCli(serverParams, initializeRoutes), LOGGER_MODULE.makeCli()],
|
|
55
|
+
rpc: userRpcPlugins,
|
|
56
|
+
admRpc: [LOGGER_MODULE.makeAdmRpc()]
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const cli = applicationModules.registerCli(new CliEngine('app.js'))
|
|
60
|
+
await cli.run()
|
|
61
|
+
|
|
62
|
+
async function initializeRoutes(server) {
|
|
63
|
+
await applicationModules.registerRpc(server)
|
|
64
|
+
await applicationModules.registerAdmRpc(server)
|
|
65
|
+
// Register application-owned routes and plugins here.
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const devModules = composeModules({
|
|
69
|
+
devCli: [
|
|
70
|
+
RPC_MODULE.makeDevCli({
|
|
71
|
+
rpcBuilders: userRpcPlugins,
|
|
72
|
+
admRpcBuilders: adminRpcPlugins,
|
|
73
|
+
rpcOutput: './src/client/generated/rpc-client.ts',
|
|
74
|
+
admRpcOutput: './src/client/generated/admin-rpc-client.ts'
|
|
75
|
+
})
|
|
76
|
+
]
|
|
77
|
+
})
|
|
78
|
+
const devCli = devModules.registerDevCli(new DevCliEngine('dev.js'))
|
|
79
|
+
await devCli.run()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`composeModules()` accepts any combination of `cli`, `devCli`, `rpc`, and `admRpc` arrays; omit facets the application does not use. For RPC builders that need Fastify plugin options, provide `rpcOptions` or `admRpcOptions`, keyed by each builder's `name` (for example, `{ audit: { workingDirectory: appDirectory } }`). The returned object has `registerCli()`, `registerDevCli()`, `registerRpc()`, and `registerAdmRpc()` methods. RPC registration methods are asynchronous and register plugins in array order. Duplicate module or plugin names within the same category throw during composition. Operational and development commands remain separate so client generation does not need to be included in production command setup. Each engine generates nested help from module and command descriptions. For example, `app.js --help`, `app.js http --help`, and `app.js http start --help` show progressively narrower command help.
|
|
83
|
+
|
|
84
|
+
`AUTH_MODULE.makeRpc(storage, options?)` requires an initialized `AuthStorage` and returns the authentication RPC builder. `AUTH_MODULE.makeCli(workingDirectory?)` accepts a directory or a callback returning one; it defaults to the current working directory. `HTTP_MODULE.makeCli(params, init)` accepts `RunServerParams` without `init`, or a callback that returns those settings, and the route initialization callback. `LOGGER_MODULE.makeCli(workingDirectory?)` accepts the directory containing `logs.sqlite` and defaults to the current working directory. `RPC_MODULE.makeDevCli(options)` requires at least one user or administrative RPC builder and an output path for each configured builder kind.
|
|
85
|
+
|
|
86
|
+
## HTTP server
|
|
87
|
+
|
|
88
|
+
The server implementation is in `back/http/server.js`. Its `runServer()` function accepts `RunServerParams`:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { runServer } from 'rlz-engine/back/http/server.js'
|
|
92
|
+
|
|
93
|
+
await runServer({
|
|
94
|
+
production: process.env.NODE_ENV === 'production',
|
|
95
|
+
domain: 'example.com',
|
|
96
|
+
certDir: './certs',
|
|
97
|
+
staticDir: './dist',
|
|
98
|
+
init: async (server) => {
|
|
99
|
+
// Register application routes and plugins here.
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`domain`, `certDir`, `staticDir`, and `init` are required. Optional settings include `debug`, `unsecurePort`, `securePort`, `auditRetentionMs`, `auditMaxRecords`, `auditSqlite`, and `acmeProvider`. Development serves HTTP with CORS and static files. Production runs an HTTP-01 challenge and redirect server alongside a TLS server with HTTP/2 and HTTP/1 support, and renews certificates periodically. CORS is available only in development and does not allow credentials; production serves static files and RPC from the same HTTPS origin. The returned shutdown function closes the servers and logging storage. The `http start` CLI command keeps the process active until a termination signal.
|
|
105
|
+
|
|
106
|
+
By default, non-debug production writes audit logs to `logs.sqlite`. Development and debug mode use colored console output. Set `auditSqlite: true` (or pass `--audit-sqlite` to `http start`) to write SQLite records alongside console output. Retention defaults to 30 days and the maximum record count defaults to 100,000.
|
|
107
|
+
|
|
108
|
+
## RPC endpoints and generated clients
|
|
109
|
+
|
|
110
|
+
Define user RPC plugins with `rpc()` and administrative plugins with `admRpc()`. Every endpoint has a version, a name, Zod body and response schemas, and an action. User endpoints may be anonymous or use a `cookieAuth` policy with both `authenticate` and `verifyCsrf` callbacks. The cookie-auth callbacks receive request cookies/headers and safe reply cookie methods via `RpcRequestContext`.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import z from 'zod'
|
|
114
|
+
import { rpc, admRpc } from 'rlz-engine/back/rpc/rpc.js'
|
|
115
|
+
import { authenticateSession, SESSION_IDLE_TIMEOUT_MS, verifySessionCsrf } from 'rlz-engine/back/auth/session.js'
|
|
116
|
+
|
|
117
|
+
// Assume storage is an initialized AuthStorage instance.
|
|
118
|
+
|
|
119
|
+
export const userRpc = rpc('users').add({
|
|
120
|
+
name: 'get-profile',
|
|
121
|
+
v: 1,
|
|
122
|
+
bodySchema: z.object({}),
|
|
123
|
+
respSchema: z.object({ name: z.string() }),
|
|
124
|
+
cookieAuth: {
|
|
125
|
+
authenticate: (context) => authenticateSession(storage, context, SESSION_IDLE_TIMEOUT_MS),
|
|
126
|
+
verifyCsrf: (context, session) => verifySessionCsrf(storage, context, session)
|
|
127
|
+
},
|
|
128
|
+
action: (session) => ({ name: session.user.name })
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
export const adminRpc = admRpc('operations').add({
|
|
132
|
+
name: 'health',
|
|
133
|
+
v: 1,
|
|
134
|
+
bodySchema: z.object({}),
|
|
135
|
+
respSchema: z.object({ status: z.string() }),
|
|
136
|
+
action: async () => ({ status: 'ok' })
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Register the generated Fastify plugins from the application initialization callback:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
await server.register(userRpc.makeFastifyPlugin())
|
|
144
|
+
await server.register(adminRpc.makeFastifyPlugin())
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
User endpoints are served at `/rpc/<plugin>_<endpoint>_v<version>`. Authenticated user routes use cookie sessions and require both a session authenticator and a CSRF verifier.
|
|
148
|
+
|
|
149
|
+
Cookie-auth endpoints use the same route and receive a generated client method with no credential parameter. Their `authenticate` callback reads the session cookie; `verifyCsrf` must validate the request before the action runs. `runServer()` registers `@fastify/cookie` before application routes. `back/auth/cookies.js` exports session-cookie helpers; they always use `HttpOnly`, `Secure`, `SameSite=Lax`, and `Path=/` without a `Domain`. The caller can set only the cookie lifetime. `back/auth/csrf.js` provides 32-byte CSRF token generation, constant-time token comparison, and same-origin validation.
|
|
150
|
+
|
|
151
|
+
Administrative endpoints are served at `/.adm/rpc/<plugin>_<endpoint>_v<version>` and accept only `Authorization: Bearer <admin-secret>`. The secret is checked against the salted scrypt hash in `./auth/admin-secret` relative to the process working directory. Use the operational `adm reset-secret` command to create or replace it; the generated plaintext is printed once. The server caches the hash per process and path, so restart a running server after resetting the secret.
|
|
152
|
+
|
|
153
|
+
`generateClient(fileName, ...rpcBuilders)` and `generateAdmClient(fileName, ...admRpcBuilders)` in `back/rpc/gen.js` generate typed clients from endpoint schemas. The `rpc generate-client` command exposed by `RPC_MODULE.makeDevCli()` invokes these generators and writes outputs to the configured paths. Generated user methods use `rpcCall()`; generated administrative methods require `AdmAuthParam` and use `admRpcCall()`.
|
|
154
|
+
|
|
155
|
+
### RPC transport
|
|
156
|
+
|
|
157
|
+
`client/rpc.js` exports `rpcCall()`, `admRpcCall()`, `AdmAuthParam`, and `HttpError`, `Unauthorized`, `Forbidden`, and `NotFound` errors. Requests and responses are JSON. Request bodies larger than 16 KiB are gzip-compressed. User RPC uses same-origin cookie credentials and attaches the CSRF token held in client memory. Admin RPC uses a separate secret-only Bearer contract. `initFrontConfig()` configures the RPC base URL, which defaults to `/` in production and `http://localhost:8080/` otherwise.
|
|
158
|
+
|
|
159
|
+
## Authentication RPC
|
|
160
|
+
|
|
161
|
+
`back/auth` contains `AuthStorage`, signup/signin helpers, opaque server-side sessions, and the administrative-secret implementation. `AUTH_MODULE.makeRpc(storage, options?)` provides `signup`, `signin`, `session`, and CSRF-protected `logout` endpoints under `/rpc/auth_*_v1`. Set `options.sessionIdleTimeoutMs` to configure the idle timeout; it defaults to 30 minutes and must be a positive integer no greater than the fixed seven-day absolute timeout. Signup and signin set an HttpOnly session cookie and return the user profile plus a CSRF token. `session` restores the profile and token from the cookie. Session IDs are random, only their SHA-256 hashes are stored, and logout revokes the current session. Auth endpoints require a matching same-origin `Origin` header. Register the builder with `composeModules()` like other RPC feature modules.
|
|
162
|
+
|
|
163
|
+
`SignupSigninScreen` accepts an `authClient` adapter. Pass callbacks backed by the application's generated `RPC_CLIENT.auth.signup()` and `RPC_CLIENT.auth.signin()` methods:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const authClient = {
|
|
167
|
+
signup: (name, email, password) => RPC_CLIENT.auth.signup({ name, email, password }),
|
|
168
|
+
signin: (name, password) => RPC_CLIENT.auth.signin({ name, password }),
|
|
169
|
+
session: () => RPC_CLIENT.auth.session({})
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Login profile state is available through `useAuthState` in `client/state/auth.js` and is persisted with Zustand. The CSRF token is kept only in memory; call the generated `auth.session()` endpoint on app startup to restore it from the server session before making protected RPC calls. `SignupSigninScreen` performs this restoration when mounted. Existing local profile data does not authenticate a user, and the old temporary-password field is discarded during state migration. Existing passwords remain valid: legacy scrypt hashes are verified with their original parameters and upgraded to hash version 1 on successful signin. New hashes use scrypt with `N=32768`, `r=8`, and `p=3`. The `/api/...` routes and `apiCall()` transport have been removed; RPC is the only supported application protocol.
|
|
174
|
+
|
|
175
|
+
## Logging and audit records
|
|
176
|
+
|
|
177
|
+
`LOGGER_MODULE.makeCli()` supplies `logger logs`, which reads an existing SQLite audit database in read-only mode. Supported filters are `--from`, `--to`, `--module`, `--level`, `--limit`, and `--offset`.
|
|
178
|
+
|
|
179
|
+
`LOGGER_MODULE.makeAdmRpc()` creates the `audit_logs_v1` administrative endpoint, which can be registered like other administrative RPC plugins. It accepts optional time, module, level, limit, and offset filters. Pages are newest-first, ordered stably, and capped at 1,000 records; defaults are 100 records and offset 0.
|
|
180
|
+
|
|
181
|
+
Applications can write typed audit events using `getAuditLogger<EventUnion>(module)`:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { getAuditLogger, type AuditEvent } from 'rlz-engine/back/logger/audit-logger.js'
|
|
185
|
+
|
|
186
|
+
type UserEvent = AuditEvent & { type: 'user-registered'; userId: string }
|
|
187
|
+
const audit = getAuditLogger<UserEvent>('USERS')
|
|
188
|
+
audit.info({ type: 'user-registered', userId: 'user-id' })
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Each event has a `type` and structured fields. Audit records include time, level, module, type, data, and optional error details. `initAuditLogger()` selects console, SQLite, or combined console and SQLite sinks and returns a shutdown function. Loggers obtained before initialization are no-op until a sink is selected, and become no-op again after shutdown. `getSystemLogger(name)` creates a Fastify-compatible logger that forwards records as `SYSTEM` audit events and writes readable single-line output in production.
|
|
192
|
+
|
|
193
|
+
Structured fields are persisted as provided; do not include passwords, admin secrets, or other sensitive values in log events. SQLite uses Node's built-in synchronous database API. Storage failures are reported to stderr and do not interrupt SystemLogger output.
|
|
194
|
+
|
|
195
|
+
## MongoDB and synchronization
|
|
196
|
+
|
|
197
|
+
`MongoStorage` in `back/storage/db.js` wraps MongoDB collections and reconciles indexes to the configured descriptions: non-system indexes not listed in the configuration are removed, and missing configured indexes are created. `back/storage/model.js` defines `MongoObject<T>` and the shared sync-object schema. `AuthStorage` is separate and stores authentication data.
|
|
198
|
+
|
|
199
|
+
`back/storage/sync.js` provides `getAll()` for listing an owner's object IDs and modification times. The client-side `syncItems()` helper in `client/sync.js` compares these timestamps with local items, pushes newer local objects, and fetches newer or unknown remote objects. The sync helpers do not define application-specific merging, RPC endpoint names, or durable local storage; callers provide RPC-backed item access and update behavior.
|
|
200
|
+
|
|
201
|
+
## React client components and state
|
|
202
|
+
|
|
203
|
+
- `SignupSigninScreen` provides the signup/signin screen; applications supply its `appName` and generated-client-backed `authClient` props.
|
|
204
|
+
- `NotFound` is the 404 screen.
|
|
205
|
+
- `ItemsSelect` is a reusable item selector; its `ItemType` and `ItemsType` types are exported alongside it.
|
|
206
|
+
- `useAuthState` holds persisted authentication state.
|
|
207
|
+
- `cacheLastResult()` wraps a function to cache its last result.
|
|
208
|
+
|
|
209
|
+
These are building blocks for an application UI. The package does not provide a complete application shell or the planned administration interface.
|
|
210
|
+
|
|
211
|
+
## Shared utilities
|
|
212
|
+
|
|
213
|
+
`shared/utils/datetime.js` exports `isValidDateTime()`, `assertValidDateTime()`, `toValidDateTime()`, and `utcToday()` for Luxon `DateTime` values. Authentication RPC schemas are owned by `back/auth/module.js`; applications generate their client types from that builder.
|
|
214
|
+
|
|
215
|
+
## Development
|
|
216
|
+
|
|
217
|
+
```sh
|
|
218
|
+
npm install
|
|
219
|
+
npm run build
|
|
220
|
+
npm test
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Auth storage integration tests can run against a local MongoDB by setting `MONGODB_TEST_URI`, for example `MONGODB_TEST_URI=mongodb://127.0.0.1:27017 npm test -- --run tests/auth-storage.integration.test.ts`. They create and drop a uniquely named test database.
|
|
224
|
+
|
|
225
|
+
The package uses TypeScript, Vitest, ESLint, and Prettier. Read `spec/` before changing behavior, keep it current when a design or contract changes, and track accepted future work in `todo.md`.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function generateAdminSecret(): Promise<string>;
|
|
2
|
+
export declare function resetAdminSecret(workingDirectory?: string): Promise<string>;
|
|
3
|
+
export declare function verifyAdminSecret(secret: string, workingDirectory?: string): Promise<boolean>;
|
|
4
|
+
export declare function writeAdminSecretHash(path: string, secret: string): Promise<void>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { randomBytes, randomInt, scrypt as scryptCallback, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
const SECRET_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789';
|
|
5
|
+
const HASH_LENGTH = 64;
|
|
6
|
+
const SCRYPT_N = 16384;
|
|
7
|
+
const SCRYPT_R = 8;
|
|
8
|
+
const SCRYPT_P = 1;
|
|
9
|
+
const adminSecretRecords = new Map();
|
|
10
|
+
export async function generateAdminSecret() {
|
|
11
|
+
return Array.from({ length: 32 }, () => SECRET_ALPHABET[randomInt(SECRET_ALPHABET.length)]).join('');
|
|
12
|
+
}
|
|
13
|
+
export async function resetAdminSecret(workingDirectory = process.cwd()) {
|
|
14
|
+
const secret = await generateAdminSecret();
|
|
15
|
+
await writeAdminSecretHash(join(workingDirectory, 'auth', 'admin-secret'), secret);
|
|
16
|
+
return secret;
|
|
17
|
+
}
|
|
18
|
+
export async function verifyAdminSecret(secret, workingDirectory = process.cwd()) {
|
|
19
|
+
const path = resolve(workingDirectory, 'auth', 'admin-secret');
|
|
20
|
+
let recordPromise = adminSecretRecords.get(path);
|
|
21
|
+
if (recordPromise === undefined) {
|
|
22
|
+
recordPromise = readAdminSecretRecord(path);
|
|
23
|
+
adminSecretRecords.set(path, recordPromise);
|
|
24
|
+
}
|
|
25
|
+
const record = await recordPromise;
|
|
26
|
+
if (record === null) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const actualHash = await deriveSecretHash(secret, record.salt);
|
|
30
|
+
return timingSafeEqual(actualHash, record.hash);
|
|
31
|
+
}
|
|
32
|
+
async function readAdminSecretRecord(path) {
|
|
33
|
+
let contents;
|
|
34
|
+
try {
|
|
35
|
+
contents = await readFile(path, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const match = /^scrypt\$(\d+)\$(\d+)\$(\d+)\$([A-Za-z0-9+/]+={0,2})\$([A-Za-z0-9+/]+={0,2})\n?$/.exec(contents);
|
|
41
|
+
if (match === null || match[1] !== String(SCRYPT_N) || match[2] !== String(SCRYPT_R) || match[3] !== String(SCRYPT_P)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const salt = Buffer.from(match[4], 'base64');
|
|
45
|
+
const hash = Buffer.from(match[5], 'base64');
|
|
46
|
+
if (salt.length !== 16 || hash.length !== HASH_LENGTH) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return { salt, hash };
|
|
50
|
+
}
|
|
51
|
+
function deriveSecretHash(secret, salt) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
scryptCallback(secret, salt, HASH_LENGTH, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }, (error, derivedKey) => {
|
|
54
|
+
if (error !== null) {
|
|
55
|
+
reject(error);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
resolve(derivedKey);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export async function writeAdminSecretHash(path, secret) {
|
|
64
|
+
const salt = randomBytes(16);
|
|
65
|
+
const hash = await deriveSecretHash(secret, salt);
|
|
66
|
+
const record = `scrypt$${SCRYPT_N}$${SCRYPT_R}$${SCRYPT_P}$${salt.toString('base64')}$${hash.toString('base64')}\n`;
|
|
67
|
+
const directory = dirname(path);
|
|
68
|
+
const temporaryPath = join(directory, `.admin-secret-${process.pid}-${randomBytes(8).toString('hex')}.tmp`);
|
|
69
|
+
await mkdir(directory, { recursive: true });
|
|
70
|
+
try {
|
|
71
|
+
const file = await open(temporaryPath, 'wx', 0o600);
|
|
72
|
+
try {
|
|
73
|
+
await file.writeFile(record, 'utf8');
|
|
74
|
+
await file.sync();
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
await file.close();
|
|
78
|
+
}
|
|
79
|
+
await rename(temporaryPath, path);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
await rm(temporaryPath, { force: true });
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=admin-secret.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-secret.js","sourceRoot":"","sources":["../../../src/back/auth/admin-secret.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CookieSerializeOptions } from '@fastify/cookie';
|
|
2
|
+
export declare const SESSION_COOKIE_NAME = "__Host-session";
|
|
3
|
+
export declare const SESSION_COOKIE_MAX_AGE_SECONDS: number;
|
|
4
|
+
/** Set the HttpOnly session cookie without a Domain attribute. */
|
|
5
|
+
export declare function setSessionCookie(reply: SessionCookieReply, value: string): void;
|
|
6
|
+
/** Clear the session cookie using the same path and security attributes used when setting it. */
|
|
7
|
+
export declare function clearSessionCookie(reply: SessionCookieReply): void;
|
|
8
|
+
interface SessionCookieReply {
|
|
9
|
+
setCookie(name: string, value: string, options: CookieSerializeOptions): unknown;
|
|
10
|
+
clearCookie(name: string, options: CookieSerializeOptions): unknown;
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const SESSION_COOKIE_NAME = '__Host-session';
|
|
2
|
+
export const SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 7;
|
|
3
|
+
/** Set the HttpOnly session cookie without a Domain attribute. */
|
|
4
|
+
export function setSessionCookie(reply, value) {
|
|
5
|
+
reply.setCookie(SESSION_COOKIE_NAME, value, makeOptions());
|
|
6
|
+
}
|
|
7
|
+
/** Clear the session cookie using the same path and security attributes used when setting it. */
|
|
8
|
+
export function clearSessionCookie(reply) {
|
|
9
|
+
reply.clearCookie(SESSION_COOKIE_NAME, makeOptions());
|
|
10
|
+
}
|
|
11
|
+
function makeOptions() {
|
|
12
|
+
return {
|
|
13
|
+
httpOnly: true,
|
|
14
|
+
secure: true,
|
|
15
|
+
sameSite: 'lax',
|
|
16
|
+
path: '/',
|
|
17
|
+
maxAge: SESSION_COOKIE_MAX_AGE_SECONDS
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=cookies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookies.js","sourceRoot":"","sources":["../../../src/back/auth/cookies.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAA;AACnD,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;AAE9D,kEAAkE;AAClE,MAAM,UAAU,gBAAgB,CAAC,KAAyB,EAAE,KAAa;IACrE,KAAK,CAAC,SAAS,CAAC,mBAAmB,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAA;AAC9D,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,kBAAkB,CAAC,KAAyB;IACxD,KAAK,CAAC,WAAW,CAAC,mBAAmB,EAAE,WAAW,EAAE,CAAC,CAAA;AACzD,CAAC;AAOD,SAAS,WAAW;IAChB,OAAO;QACH,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,IAAI;QACZ,QAAQ,EAAE,KAAK;QACf,IAAI,EAAE,GAAG;QACT,MAAM,EAAE,8BAA8B;KACzC,CAAA;AACL,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FastifyRequest } from 'fastify';
|
|
2
|
+
export declare const CSRF_TOKEN_HEADER = "x-csrf-token";
|
|
3
|
+
/** Create a secret token suitable for storing with one server-side session. */
|
|
4
|
+
export declare function makeCsrfToken(): string;
|
|
5
|
+
/** Compare a session's expected CSRF token with the token submitted in a request header. */
|
|
6
|
+
export declare function verifyCsrfToken(expected: string, provided: string | string[] | undefined): boolean;
|
|
7
|
+
/** Reject requests whose browser Origin does not match the request's own origin. */
|
|
8
|
+
export declare function assertSameOrigin(request: Pick<FastifyRequest, 'headers' | 'protocol'>): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { httpErrors } from '@fastify/sensible';
|
|
3
|
+
export const CSRF_TOKEN_HEADER = 'x-csrf-token';
|
|
4
|
+
/** Create a secret token suitable for storing with one server-side session. */
|
|
5
|
+
export function makeCsrfToken() {
|
|
6
|
+
return randomBytes(32).toString('base64url');
|
|
7
|
+
}
|
|
8
|
+
/** Compare a session's expected CSRF token with the token submitted in a request header. */
|
|
9
|
+
export function verifyCsrfToken(expected, provided) {
|
|
10
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(expected) || typeof provided !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(provided)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const expectedBytes = Buffer.from(expected);
|
|
14
|
+
const providedBytes = Buffer.from(provided);
|
|
15
|
+
const expectedHash = createHash('sha256').update(expectedBytes).digest();
|
|
16
|
+
const providedHash = createHash('sha256').update(providedBytes).digest();
|
|
17
|
+
return timingSafeEqual(expectedHash, providedHash);
|
|
18
|
+
}
|
|
19
|
+
/** Reject requests whose browser Origin does not match the request's own origin. */
|
|
20
|
+
export function assertSameOrigin(request) {
|
|
21
|
+
const originHeader = request.headers.origin;
|
|
22
|
+
const hostHeader = request.headers.host;
|
|
23
|
+
const origin = Array.isArray(originHeader) ? undefined : originHeader;
|
|
24
|
+
const host = Array.isArray(hostHeader) ? undefined : hostHeader;
|
|
25
|
+
if (origin === undefined || origin === 'null' || host === undefined || !isCanonicalOrigin(origin)) {
|
|
26
|
+
throw httpErrors.forbidden();
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
if (new URL(origin).origin !== new URL(`${request.protocol}://${host}`).origin)
|
|
30
|
+
throw httpErrors.forbidden();
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof Error && 'statusCode' in error)
|
|
34
|
+
throw error;
|
|
35
|
+
throw httpErrors.forbidden();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function isCanonicalOrigin(value) {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = new URL(value);
|
|
41
|
+
return (parsed.protocol === 'http:' || parsed.protocol === 'https:') && parsed.origin === value;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=csrf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf.js","sourceRoot":"","sources":["../../../src/back/auth/csrf.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAEtE,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAG9C,MAAM,CAAC,MAAM,iBAAiB,GAAG,cAAc,CAAA;AAE/C,+EAA+E;AAC/E,MAAM,UAAU,aAAa;IACzB,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;AAChD,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,QAAuC;IACrF,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjH,OAAO,KAAK,CAAA;IAChB,CAAC;IAED,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAC3C,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAC3C,MAAM,YAAY,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,MAAM,EAAE,CAAA;IACxE,MAAM,YAAY,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,MAAM,EAAE,CAAA;IACxE,OAAO,eAAe,CAAC,YAAY,EAAE,YAAY,CAAC,CAAA;AACtD,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,gBAAgB,CAAC,OAAqD;IAClF,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAA;IAC3C,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAA;IACvC,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAA;IACrE,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAA;IAE/D,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,MAAM,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,EAAE,CAAC;QAChG,MAAM,UAAU,CAAC,SAAS,EAAE,CAAA;IAChC,CAAC;IAED,IAAI,CAAC;QACD,IAAI,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,IAAI,GAAG,CAAC,GAAG,OAAO,CAAC,QAAQ,MAAM,IAAI,EAAE,CAAC,CAAC,MAAM;YAAE,MAAM,UAAU,CAAC,SAAS,EAAE,CAAA;IAChH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,IAAI,KAAK,YAAY,KAAK,IAAI,YAAY,IAAI,KAAK;YAAE,MAAM,KAAK,CAAA;QAChE,MAAM,UAAU,CAAC,SAAS,EAAE,CAAA;IAChC,CAAC;AACL,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAa;IACpC,IAAI,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAA;QAC7B,OAAO,CAAC,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,KAAK,CAAA;IACnG,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,KAAK,CAAA;IAChB,CAAC;AACL,CAAC"}
|
|
@@ -5,10 +5,14 @@ export interface StorageUser {
|
|
|
5
5
|
email: string;
|
|
6
6
|
passwordSalt: Binary;
|
|
7
7
|
passwordHash: Binary;
|
|
8
|
+
passwordHashVersion?: number;
|
|
8
9
|
lastActivityDate: Date;
|
|
9
10
|
}
|
|
10
|
-
export interface
|
|
11
|
+
export interface StorageSession {
|
|
11
12
|
userId: string;
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
sessionHash: Binary;
|
|
14
|
+
csrfToken: string;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
lastActivityAt: Date;
|
|
17
|
+
expiresAt: Date;
|
|
14
18
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CliModule } from '../cli/cli-engine.js';
|
|
2
|
+
import { RpcPluginBuilder } from '../rpc/rpc.js';
|
|
3
|
+
import { AuthStorage } from './storage.js';
|
|
4
|
+
export interface AuthRpcOptions {
|
|
5
|
+
/** Maximum time without an authenticated request before a session expires. Defaults to 30 minutes. */
|
|
6
|
+
sessionIdleTimeoutMs?: number;
|
|
7
|
+
}
|
|
8
|
+
/** Authentication RPC and administrative maintenance integrations. */
|
|
9
|
+
export declare const AUTH_MODULE: {
|
|
10
|
+
/** Create signup/signin, session restoration, and cookie-authenticated logout RPC endpoints. */
|
|
11
|
+
makeRpc(storage: AuthStorage, options?: AuthRpcOptions): RpcPluginBuilder;
|
|
12
|
+
/** Create the `adm reset-secret` command for the supplied application directory. */
|
|
13
|
+
makeCli(workingDirectory?: string | (() => string)): CliModule;
|
|
14
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { httpErrors } from '@fastify/sensible';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { resetAdminSecret } from './admin-secret.js';
|
|
4
|
+
import { clearSessionCookie, setSessionCookie } from './cookies.js';
|
|
5
|
+
import { assertSameOrigin } from './csrf.js';
|
|
6
|
+
import { AuthRateLimiter } from './rate-limit.js';
|
|
7
|
+
import { authenticateSession, createSession, revokeCurrentSession, SESSION_ABSOLUTE_TIMEOUT_MS, SESSION_IDLE_TIMEOUT_MS, verifySessionCsrf } from './session.js';
|
|
8
|
+
import { rpc } from '../rpc/rpc.js';
|
|
9
|
+
import { signin, signup } from './utils.js';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
const AUTH_RESPONSE_SCHEMA = z.object({ id: z.uuid(), name: z.string(), email: z.email(), csrfToken: z.string() });
|
|
12
|
+
const SIGNUP_REQUEST_SCHEMA = z.object({
|
|
13
|
+
name: z.string().min(1).max(64),
|
|
14
|
+
email: z.email().max(254),
|
|
15
|
+
password: z.string().min(12).max(1024)
|
|
16
|
+
});
|
|
17
|
+
const SIGNIN_REQUEST_SCHEMA = z.object({ name: z.string().min(1).max(64), password: z.string().max(1024) });
|
|
18
|
+
const EMPTY_REQUEST_SCHEMA = z.object({});
|
|
19
|
+
const EMPTY_RESPONSE_SCHEMA = z.object({});
|
|
20
|
+
/** Authentication RPC and administrative maintenance integrations. */
|
|
21
|
+
export const AUTH_MODULE = {
|
|
22
|
+
/** Create signup/signin, session restoration, and cookie-authenticated logout RPC endpoints. */
|
|
23
|
+
makeRpc(storage, options = {}) {
|
|
24
|
+
const sessionIdleTimeoutMs = options.sessionIdleTimeoutMs ?? SESSION_IDLE_TIMEOUT_MS;
|
|
25
|
+
if (!Number.isSafeInteger(sessionIdleTimeoutMs) ||
|
|
26
|
+
sessionIdleTimeoutMs <= 0 ||
|
|
27
|
+
sessionIdleTimeoutMs > SESSION_ABSOLUTE_TIMEOUT_MS) {
|
|
28
|
+
throw new RangeError(`sessionIdleTimeoutMs must be a positive integer no greater than ${SESSION_ABSOLUTE_TIMEOUT_MS}`);
|
|
29
|
+
}
|
|
30
|
+
const rateLimiter = new AuthRateLimiter();
|
|
31
|
+
return rpc('auth')
|
|
32
|
+
.add({
|
|
33
|
+
v: 1,
|
|
34
|
+
name: 'signup',
|
|
35
|
+
anonimous: true,
|
|
36
|
+
bodySchema: SIGNUP_REQUEST_SCHEMA,
|
|
37
|
+
respSchema: EMPTY_RESPONSE_SCHEMA,
|
|
38
|
+
action: async (body, _opts, context) => {
|
|
39
|
+
assertSameOrigin(context.request);
|
|
40
|
+
await rateLimiter.run('signup', context.request.ip, async () => {
|
|
41
|
+
try {
|
|
42
|
+
await signup(storage, body.name, body.email, body.password);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
// Keep a duplicate name indistinguishable from a successful registration.
|
|
46
|
+
if (!isConflictError(error))
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
.add({
|
|
54
|
+
v: 1,
|
|
55
|
+
name: 'signin',
|
|
56
|
+
anonimous: true,
|
|
57
|
+
bodySchema: SIGNIN_REQUEST_SCHEMA,
|
|
58
|
+
respSchema: AUTH_RESPONSE_SCHEMA,
|
|
59
|
+
action: async (body, _opts, context) => {
|
|
60
|
+
assertSameOrigin(context.request);
|
|
61
|
+
const response = await rateLimiter.run('signin', context.request.ip, () => signin(storage, body.name, body.password));
|
|
62
|
+
if (response === null)
|
|
63
|
+
throw httpErrors.unauthorized();
|
|
64
|
+
await revokeCurrentSession(storage, context);
|
|
65
|
+
const session = await createSession(storage, response.id);
|
|
66
|
+
setSessionCookie(context.reply, session.sessionId);
|
|
67
|
+
return { ...response, csrfToken: session.csrfToken };
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
.add({
|
|
71
|
+
v: 1,
|
|
72
|
+
name: 'session',
|
|
73
|
+
bodySchema: EMPTY_REQUEST_SCHEMA,
|
|
74
|
+
respSchema: AUTH_RESPONSE_SCHEMA,
|
|
75
|
+
cookieAuth: {
|
|
76
|
+
authenticate: (context) => authenticateSession(storage, context, sessionIdleTimeoutMs),
|
|
77
|
+
verifyCsrf: async (context, session) => {
|
|
78
|
+
assertSameOrigin(context.request);
|
|
79
|
+
await storage.touchSession(session.sessionHash, new Date());
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
action: (session) => ({ ...session.user, csrfToken: session.csrfToken })
|
|
83
|
+
})
|
|
84
|
+
.add({
|
|
85
|
+
v: 1,
|
|
86
|
+
name: 'logout',
|
|
87
|
+
bodySchema: EMPTY_REQUEST_SCHEMA,
|
|
88
|
+
respSchema: EMPTY_RESPONSE_SCHEMA,
|
|
89
|
+
cookieAuth: {
|
|
90
|
+
authenticate: (context) => authenticateSession(storage, context, sessionIdleTimeoutMs),
|
|
91
|
+
verifyCsrf: (context, session) => verifySessionCsrf(storage, context, session)
|
|
92
|
+
},
|
|
93
|
+
action: async (session, _body, _opts, context) => {
|
|
94
|
+
await storage.deleteSession(session.sessionHash);
|
|
95
|
+
clearSessionCookie(context.reply);
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
/** Create the `adm reset-secret` command for the supplied application directory. */
|
|
101
|
+
makeCli(workingDirectory = () => process.cwd()) {
|
|
102
|
+
return {
|
|
103
|
+
name: 'adm',
|
|
104
|
+
description: 'Administrative maintenance commands.',
|
|
105
|
+
commands: {
|
|
106
|
+
'reset-secret': {
|
|
107
|
+
description: 'Generate a keyboard-friendly administrative secret and atomically replace the stored password hash. The plaintext is printed once after the hash is safely written.',
|
|
108
|
+
handler: async () => {
|
|
109
|
+
const directory = typeof workingDirectory === 'function' ? workingDirectory() : workingDirectory;
|
|
110
|
+
const secret = await resetAdminSecret(resolve(directory));
|
|
111
|
+
process.stdout.write(`${secret}\n`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
function isConflictError(error) {
|
|
119
|
+
return error instanceof Error && 'statusCode' in error && error.statusCode === 409;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module.js","sourceRoot":"","sources":["../../../src/back/auth/module.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type AuthOperation = 'signin' | 'signup';
|
|
2
|
+
/** Bounds anonymous password-hash work per peer and across this server process. */
|
|
3
|
+
export declare class AuthRateLimiter {
|
|
4
|
+
private readonly buckets;
|
|
5
|
+
private activeOperations;
|
|
6
|
+
private bucketInsertionsSinceCleanup;
|
|
7
|
+
run<T>(operation: AuthOperation, ip: string, action: () => Promise<T>): Promise<T>;
|
|
8
|
+
private checkRate;
|
|
9
|
+
private removeExpiredBuckets;
|
|
10
|
+
}
|
|
11
|
+
export {};
|