@kollors/deep-json-server 1.0.0-alpha.5 → 1.0.0-alpha.6

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 (50) hide show
  1. package/README.md +50 -6
  2. package/README.ru.md +50 -6
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +1 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/src/auth/contract.d.ts +26 -0
  7. package/dist/src/auth/contract.js +5 -0
  8. package/dist/src/auth/contract.js.map +1 -0
  9. package/dist/src/auth/openapi.d.ts +10 -0
  10. package/dist/src/auth/openapi.js +62 -0
  11. package/dist/src/auth/openapi.js.map +1 -0
  12. package/dist/src/auth/password.d.ts +4 -0
  13. package/dist/src/auth/password.js +25 -0
  14. package/dist/src/auth/password.js.map +1 -0
  15. package/dist/src/auth/public.d.ts +3 -0
  16. package/dist/src/auth/public.js +5 -0
  17. package/dist/src/auth/public.js.map +1 -0
  18. package/dist/src/auth/routes.d.ts +3 -0
  19. package/dist/src/auth/routes.js +12 -0
  20. package/dist/src/auth/routes.js.map +1 -0
  21. package/dist/src/auth/service.d.ts +18 -0
  22. package/dist/src/auth/service.js +112 -0
  23. package/dist/src/auth/service.js.map +1 -0
  24. package/dist/src/cli.js +4 -2
  25. package/dist/src/cli.js.map +1 -1
  26. package/dist/src/config.d.ts +5 -0
  27. package/dist/src/config.js +24 -1
  28. package/dist/src/config.js.map +1 -1
  29. package/dist/src/constants.d.ts +1 -1
  30. package/dist/src/constants.js +1 -1
  31. package/dist/src/errors.d.ts +1 -1
  32. package/dist/src/features.d.ts +1 -0
  33. package/dist/src/features.js +10 -3
  34. package/dist/src/features.js.map +1 -1
  35. package/dist/src/http/errors.js +1 -1
  36. package/dist/src/http/errors.js.map +1 -1
  37. package/dist/src/openapi/document.d.ts +2 -1
  38. package/dist/src/openapi/document.js +21 -2
  39. package/dist/src/openapi/document.js.map +1 -1
  40. package/dist/src/openapi/public.d.ts +1 -0
  41. package/dist/src/openapi/public.js +3 -1
  42. package/dist/src/openapi/public.js.map +1 -1
  43. package/dist/src/paths.d.ts +1 -0
  44. package/dist/src/paths.js +2 -1
  45. package/dist/src/paths.js.map +1 -1
  46. package/dist/src/server/public.d.ts +1 -1
  47. package/dist/src/server.js +12 -2
  48. package/dist/src/server.js.map +1 -1
  49. package/dist/src/types.d.ts +1 -0
  50. package/package.json +5 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  A JSON-backed mock server with REST, GraphQL, nested queries, binary files and schema exports. Requires Node.js 22 or newer.
6
6
 
7
- **1.0.0-alpha.5 is a prerelease.** REST queries use `scope=[fields, arguments?]` at every level. When upgrading from an earlier version, update query parameters using the examples below; upgrading from 0.x also requires the new model schema.
7
+ **1.0.0-alpha.6 is a prerelease.** REST queries use `scope=[fields, arguments?]` at every level. When upgrading from an earlier version, update query parameters using the examples below; upgrading from 0.x also requires the new model schema.
8
8
 
9
9
  ## Installation
10
10
 
@@ -12,7 +12,7 @@ A JSON-backed mock server with REST, GraphQL, nested queries, binary files and s
12
12
  npm install @kollors/deep-json-server@alpha
13
13
  ```
14
14
 
15
- To install a specific version, use `@1.0.0-alpha.5`.
15
+ To install a specific version, use `@1.0.0-alpha.6`.
16
16
 
17
17
  ## Quick start
18
18
 
@@ -55,6 +55,9 @@ The user list is available at `http://127.0.0.1:4001/users`.
55
55
  | `graphql.enabled` | Enable GraphQL HTTP endpoint; default `false` |
56
56
  | `graphql.endpoint` | Endpoint path; default `/graphql` |
57
57
  | `graphql.path` | GraphQL SDL export destination |
58
+ | `auth.enabled` | Enable authentication; default `false` |
59
+ | `auth.users` | Path to a JSON array of auth users, or an in-memory array |
60
+ | `auth.expiresIn` | Session lifetime in seconds; default 3600 |
58
61
  | `server.host`, `server.port` | Defaults `127.0.0.1`, `4001`; CLI also reads `HOST`/`PORT` |
59
62
  | `server.pageSize`, `server.maxPageSize` | Defaults 10 and 100; default size is capped by the maximum |
60
63
  | `server.cors`, `server.logger` | Default `true`; logger also accepts Fastify logger options |
@@ -71,6 +74,7 @@ Set `server.port` to `0` to let the operating system choose an available port. T
71
74
  | `--files` | Enable file routes |
72
75
  | `--graphql` | Enable the GraphQL API |
73
76
  | `--openapi` | Enable the OpenAPI endpoint |
77
+ | `--auth` | Enable authentication using `auth.users` |
74
78
  | `--host <host>` | Server address |
75
79
  | `--port <port>` | Server port |
76
80
  | `--help`, `-h` | Show help |
@@ -96,7 +100,7 @@ export default {
96
100
  };
97
101
  ```
98
102
 
99
- Each format needs its own output file. The command rejects destinations that would overwrite the configuration, database, schema or file metadata.
103
+ Each format needs its own output file. The command rejects destinations that would overwrite the configuration, database, schema, auth users or file metadata.
100
104
 
101
105
  ## Model schema
102
106
 
@@ -534,10 +538,48 @@ Content-Type: application/json
534
538
 
535
539
  In disk mode, the binary is stored at `<files.directory>/<directory>/<name>`. Metadata stores `directory`, `mimeType` and `name`; the server reads the size from the file and builds its URLs. Directories and the metadata file are created when needed.
536
540
 
537
- Use one server process per disk database and file store. Stop it before editing stored files or metadata manually. Storage paths cannot contain symbolic links. Uploads and renames cannot overwrite the database, counters, schema, loaded configuration or metadata file.
541
+ Use one server process per disk database and file store. Stop it before editing stored files or metadata manually. Storage paths cannot contain symbolic links. Uploads and renames cannot overwrite the database, counters, schema, auth users, loaded configuration or metadata file.
538
542
 
539
543
  Send the file as a binary request body. In a browser, use `xhr.send(file)` and track progress through `XMLHttpRequest.upload.onprogress`. The default maximum size is 100 MiB and can be changed through `server.maxFileSize`. Missing or unsafe headers and paths return `400`, an exceeded limit returns `413`, and a missing, malformed, or Fastify-unsupported `Content-Type` returns `400` or `415`, depending on which validation stage rejects it.
540
544
 
545
+ ## Authentication
546
+
547
+ The optional auth module provides REST routes for login, current user and logout. **It does not restrict access to REST records, files or GraphQL.**
548
+
549
+ Create an auth user in a separate file with `setup-auth.mjs`:
550
+
551
+ ```js
552
+ import { writeFile } from 'node:fs/promises';
553
+ import { hashPassword } from '@kollors/deep-json-server/auth';
554
+
555
+ const password = process.env.DJS_PASSWORD;
556
+ if (!password) throw new Error('Set DJS_PASSWORD');
557
+ await writeFile('./auth.json', JSON.stringify([
558
+ { id: '1', username: 'admin', passwordHash: await hashPassword(password) },
559
+ ], null, 2), { flag: 'wx', mode: 0o600 });
560
+ ```
561
+
562
+ Set `DJS_PASSWORD` and run `node setup-auth.mjs`. Add the file to your server configuration:
563
+
564
+ ```js
565
+ export default {
566
+ database: { path: './database.json' },
567
+ auth: { users: './auth.json', expiresIn: 3600 },
568
+ };
569
+ ```
570
+
571
+ Start with `npx deep-json-server --auth server.config.js`, or set `auth.enabled: true`. Each user needs a unique string `id`, a unique `username` and a `passwordHash` created by the helper. Passwords use salted scrypt hashes. The file is read at startup; restart the server after editing it.
572
+
573
+ | REST request | Input | Response |
574
+ |---|---|---|
575
+ | `POST /auth/login` | JSON `{ "username": "admin", "password": "…" }` | `{ accessToken, expiresIn, user: { id, username } }` |
576
+ | `GET /auth/me` | Bearer token | `{ id, username }` |
577
+ | `POST /auth/logout` | Bearer token | `{ success: true }` |
578
+
579
+ Pass the token in `Authorization: Bearer <accessToken>`. Incorrect credentials and invalid or expired tokens return HTTP 401. Sessions are kept in memory and disappear on restart; logout revokes the supplied token. Login can return HTTP 429 when the server has too many concurrent login attempts or active sessions.
580
+
581
+ OpenAPI includes these REST operations and a Bearer security scheme for `/auth/me` and `/auth/logout`. In Swagger UI, paste a token from login into **Authorize**. For schema exports, enable auth in the configuration or use `generate openapi --auth server.config.js`; the users file is not read during generation. GraphQL and OpenAPI still require `database.schema`.
582
+
541
583
  ## Programmatic API
542
584
 
543
585
  ```js
@@ -550,7 +592,7 @@ await server.listen();
550
592
  // await server.close();
551
593
  ```
552
594
 
553
- The `openapi()` and `graphql()` methods return schemas and require `database.schema`. `fastify()` returns the server instance for configuration and startup. The database and enabled services initialize on `ready()`, `listen()` or the first `inject()`; initialization errors stop startup. Override server features with `createServer(config, { files: false, graphql: true, openapi: true })`.
595
+ The `openapi()` and `graphql()` methods return schemas and require `database.schema`. `fastify()` returns the server instance for configuration and startup. The database and enabled services initialize on `ready()`, `listen()` or the first `inject()`; initialization errors stop startup. Override server features with `createServer(config, { files: false, graphql: true, openapi: true, auth: true })`.
554
596
 
555
597
  The root import `@kollors/deep-json-server` also provides these functions. Server adapters load when enabled. Generators can be used independently:
556
598
 
@@ -564,13 +606,15 @@ await writeOpenapi(document, './generated/openapi.yaml');
564
606
  await writeGraphql(sdl, './generated/schema.graphql');
565
607
  ```
566
608
 
609
+ `generateOpenapi()` accepts `{ auth: true }` to include auth operations. `hashPassword()` is also available from the root package.
610
+
567
611
  `generateOpenapi()` also accepts `host`, `port`, `pageSize`, `maxPageSize` and `info`. Pass a schema object instead of a path if preferred. Servers and generators use their own copy of the model. Pagination sizes must be positive integers; `pageSize` cannot exceed `maxPageSize`.
568
612
 
569
613
  ## Storage and development
570
614
 
571
615
  Updates run sequentially within one server instance and are validated on a copy of the data before saving. Use one server process per database file. `increment` counters are stored next to the database in `<database path>.counters.json`; keep that file with the database. Numbers are reserved before the data write, so a failed write can leave gaps but cannot reuse a reserved number.
572
616
 
573
- The server is intended for mocking APIs. Implement authentication and password hashing in your application if needed.
617
+ The server is intended for mocking APIs. Access rules for application data remain the responsibility of your application.
574
618
 
575
619
  ```sh
576
620
  npm ci
package/README.ru.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  JSON-сервер для имитации API: REST, GraphQL, вложенные запросы, бинарные файлы и экспорт схем. Требуется Node.js 22 или новее.
6
6
 
7
- **1.0.0-alpha.5 — предварительная версия.** REST-запросы используют `scope=[поля, аргументы?]` на всех уровнях. При обновлении измените параметры запросов по примерам ниже; для перехода с 0.x также нужна новая схема моделей.
7
+ **1.0.0-alpha.6 — предварительная версия.** REST-запросы используют `scope=[поля, аргументы?]` на всех уровнях. При обновлении измените параметры запросов по примерам ниже; для перехода с 0.x также нужна новая схема моделей.
8
8
 
9
9
  ## Установка
10
10
 
@@ -12,7 +12,7 @@ JSON-сервер для имитации API: REST, GraphQL, вложенные
12
12
  npm install @kollors/deep-json-server@alpha
13
13
  ```
14
14
 
15
- Для установки конкретной версии укажите `@1.0.0-alpha.5`.
15
+ Для установки конкретной версии укажите `@1.0.0-alpha.6`.
16
16
 
17
17
  ## Быстрый старт
18
18
 
@@ -55,6 +55,9 @@ npx deep-json-server server.config.js
55
55
  | `graphql.enabled` | Включить GraphQL HTTP API; по умолчанию `false` |
56
56
  | `graphql.endpoint` | Путь GraphQL; по умолчанию `/graphql` |
57
57
  | `graphql.path` | Путь экспорта GraphQL SDL |
58
+ | `auth.enabled` | Включить аутентификацию; по умолчанию `false` |
59
+ | `auth.users` | Путь к JSON-массиву учётных записей или массив в памяти |
60
+ | `auth.expiresIn` | Срок действия сессии в секундах; по умолчанию 3600 |
58
61
  | `server.host`, `server.port` | По умолчанию `127.0.0.1`, `4001`; CLI также читает `HOST`/`PORT` |
59
62
  | `server.pageSize`, `server.maxPageSize` | По умолчанию 10 и 100; размер по умолчанию ограничен максимумом |
60
63
  | `server.cors`, `server.logger` | По умолчанию `true`; logger также принимает настройки Fastify |
@@ -71,6 +74,7 @@ npx deep-json-server server.config.js
71
74
  | `--files` | Включить файловые маршруты |
72
75
  | `--graphql` | Включить GraphQL API |
73
76
  | `--openapi` | Включить HTTP-маршрут OpenAPI |
77
+ | `--auth` | Включить аутентификацию с учётными записями из `auth.users` |
74
78
  | `--host <host>` | Адрес сервера |
75
79
  | `--port <port>` | Порт сервера |
76
80
  | `--help`, `-h` | Справка |
@@ -96,7 +100,7 @@ export default {
96
100
  };
97
101
  ```
98
102
 
99
- Для каждого формата нужен отдельный файл. Команда отклонит путь, который перезапишет конфигурацию, базу, схему или метаданные файлов.
103
+ Для каждого формата нужен отдельный файл. Команда отклонит путь, который перезапишет конфигурацию, базу, схему, учётные записи auth или метаданные файлов.
100
104
 
101
105
  ## Схема моделей
102
106
 
@@ -534,10 +538,48 @@ Content-Type: application/json
534
538
 
535
539
  При хранении на диске бинарный файл находится по пути `<files.directory>/<directory>/<name>`. Метаданные содержат `directory`, `mimeType` и `name`; размер сервер читает из файла, а URL формирует сам. Директории и файл метаданных создаются по мере необходимости.
536
540
 
537
- Используйте один процесс сервера для дисковой базы и файлового хранилища. Перед ручным изменением файлов или метаданных остановите его. Пути в хранилище не могут содержать символические ссылки. Загрузка и переименование не могут перезаписать базу, счётчики, схему, загруженную конфигурацию или файл метаданных.
541
+ Используйте один процесс сервера для дисковой базы и файлового хранилища. Перед ручным изменением файлов или метаданных остановите его. Пути в хранилище не могут содержать символические ссылки. Загрузка и переименование не могут перезаписать базу, счётчики, схему, учётные записи auth, загруженную конфигурацию или файл метаданных.
538
542
 
539
543
  Отправляйте файл как бинарное тело запроса. В браузере для этого можно использовать `xhr.send(file)`, а прогресс отслеживать через `XMLHttpRequest.upload.onprogress`. Максимальный размер по умолчанию равен 100 МиБ и настраивается через `server.maxFileSize`. Отсутствующие или небезопасные заголовки и пути возвращают `400`, превышение лимита — `413`, а отсутствующий, некорректный или не поддерживаемый Fastify `Content-Type` — `400` либо `415` в зависимости от этапа проверки.
540
544
 
545
+ ## Аутентификация
546
+
547
+ Модуль auth добавляет REST-маршруты входа, получения текущего пользователя и выхода. **Он не ограничивает доступ к REST-записям, файлам и GraphQL.**
548
+
549
+ Создайте учётную запись в отдельном файле с помощью `setup-auth.mjs`:
550
+
551
+ ```js
552
+ import { writeFile } from 'node:fs/promises';
553
+ import { hashPassword } from '@kollors/deep-json-server/auth';
554
+
555
+ const password = process.env.DJS_PASSWORD;
556
+ if (!password) throw new Error('Set DJS_PASSWORD');
557
+ await writeFile('./auth.json', JSON.stringify([
558
+ { id: '1', username: 'admin', passwordHash: await hashPassword(password) },
559
+ ], null, 2), { flag: 'wx', mode: 0o600 });
560
+ ```
561
+
562
+ Задайте `DJS_PASSWORD` и выполните `node setup-auth.mjs`. Добавьте файл в конфигурацию сервера:
563
+
564
+ ```js
565
+ export default {
566
+ database: { path: './database.json' },
567
+ auth: { users: './auth.json', expiresIn: 3600 },
568
+ };
569
+ ```
570
+
571
+ Запустите `npx deep-json-server --auth server.config.js` или задайте `auth.enabled: true`. Каждой записи нужны уникальный строковый `id`, уникальный `username` и `passwordHash`, созданный функцией выше. Пароли хешируются через scrypt со случайной солью. Файл читается при запуске; после его изменения перезапустите сервер.
572
+
573
+ | REST-запрос | Входные данные | Ответ |
574
+ |---|---|---|
575
+ | `POST /auth/login` | JSON `{ "username": "admin", "password": "…" }` | `{ accessToken, expiresIn, user: { id, username } }` |
576
+ | `GET /auth/me` | Bearer-токен | `{ id, username }` |
577
+ | `POST /auth/logout` | Bearer-токен | `{ success: true }` |
578
+
579
+ Передавайте токен в заголовке `Authorization: Bearer <accessToken>`. Неверные учётные данные, недействительный или истёкший токен возвращают HTTP 401. Сессии хранятся в памяти и исчезают при перезапуске; выход отзывает переданный токен. Вход может вернуть HTTP 429 при слишком большом числе одновременных попыток или активных сессий.
580
+
581
+ В OpenAPI добавляются эти REST-операции и Bearer-схема для `/auth/me` и `/auth/logout`. В Swagger UI токен из ответа на вход можно вставить в **Authorize**. Для экспорта схем включите auth в конфигурации или используйте `generate openapi --auth server.config.js`; файл учётных записей при генерации не читается. Для GraphQL и OpenAPI по-прежнему нужна `database.schema`.
582
+
541
583
  ## Программный API
542
584
 
543
585
  ```js
@@ -550,7 +592,7 @@ await server.listen();
550
592
  // await server.close();
551
593
  ```
552
594
 
553
- Методы `openapi()` и `graphql()` возвращают схемы и требуют `database.schema`. `fastify()` возвращает экземпляр сервера для настройки и запуска. База и включённые сервисы инициализируются при `ready()`, `listen()` или первом `inject()`; ошибка инициализации останавливает запуск. Возможности сервера можно переопределить через `createServer(config, { files: false, graphql: true, openapi: true })`.
595
+ Методы `openapi()` и `graphql()` возвращают схемы и требуют `database.schema`. `fastify()` возвращает экземпляр сервера для настройки и запуска. База и включённые сервисы инициализируются при `ready()`, `listen()` или первом `inject()`; ошибка инициализации останавливает запуск. Возможности сервера можно переопределить через `createServer(config, { files: false, graphql: true, openapi: true, auth: true })`.
554
596
 
555
597
  Эти функции доступны и через общий импорт `@kollors/deep-json-server`. Адаптеры сервера загружаются при включении. Генераторы можно использовать отдельно:
556
598
 
@@ -564,13 +606,15 @@ await writeOpenapi(document, './generated/openapi.yaml');
564
606
  await writeGraphql(sdl, './generated/schema.graphql');
565
607
  ```
566
608
 
609
+ `generateOpenapi()` принимает `{ auth: true }`, чтобы добавить операции auth. Функция `hashPassword()` доступна и через общий импорт пакета.
610
+
567
611
  `generateOpenapi()` также принимает `host`, `port`, `pageSize`, `maxPageSize` и `info`. Вместо пути можно передать объект схемы. Сервер и генераторы работают с собственной копией модели. Размеры страниц должны быть положительными целыми числами; `pageSize` не может превышать `maxPageSize`.
568
612
 
569
613
  ## Хранение и разработка
570
614
 
571
615
  Изменения выполняются последовательно внутри экземпляра сервера и проверяются на копии данных до сохранения. Для одного файла базы используйте один процесс сервера. Счётчики `increment` хранятся рядом с базой в `<путь базы>.counters.json`; сохраняйте этот файл вместе с базой. Номера резервируются до записи данных: сбой может оставить пропуск, но не приводит к повторной выдаче номера.
572
616
 
573
- Сервер предназначен для имитации API. Авторизацию и хеширование паролей при необходимости реализуйте в приложении.
617
+ Сервер предназначен для имитации API. Правила доступа к данным задаются в вашем приложении.
574
618
 
575
619
  ```sh
576
620
  npm ci
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export type { AuthConfig, AuthSession, AuthUser, AuthUserRecord } from './src/auth/contract.js';
2
+ export { hashPassword } from './src/auth/public.js';
1
3
  export type { DatabaseConfig, DeepJsonServerConfig, FilesConfig, GraphqlConfig, MemoryFile, OpenapiConfig, ServerConfig } from './src/config.js';
2
4
  export type { ServerFeatures } from './src/features.js';
3
5
  export type { FileUpdate } from './src/files/contract.js';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { hashPassword } from './src/auth/public.js';
1
2
  export { generateGraphql, generateOpenapi, writeGraphql, writeOpenapi } from './src/schema.js';
2
3
  export { createServer } from './src/server.js';
3
4
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/F,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAOpD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/F,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,26 @@
1
+ export interface AuthUser {
2
+ id: string;
3
+ username: string;
4
+ }
5
+ export interface AuthUserRecord extends AuthUser {
6
+ passwordHash: string;
7
+ }
8
+ export interface AuthConfig {
9
+ enabled?: boolean;
10
+ users: string | AuthUserRecord[];
11
+ /** Session lifetime in seconds. Defaults to one hour. */
12
+ expiresIn?: number;
13
+ }
14
+ export interface AuthSession {
15
+ accessToken: string;
16
+ expiresIn: number;
17
+ user: AuthUser;
18
+ }
19
+ export declare const AUTH_PATHS: {
20
+ readonly login: '/auth/login';
21
+ readonly me: '/auth/me';
22
+ readonly logout: '/auth/logout';
23
+ };
24
+ export declare const DEFAULT_SESSION_SECONDS = 3600;
25
+ export declare const MAX_USERNAME_LENGTH = 256;
26
+ export declare const MAX_PASSWORD_LENGTH = 1024;
@@ -0,0 +1,5 @@
1
+ export const AUTH_PATHS = { login: '/auth/login', me: '/auth/me', logout: '/auth/logout' };
2
+ export const DEFAULT_SESSION_SECONDS = 3600;
3
+ export const MAX_USERNAME_LENGTH = 256;
4
+ export const MAX_PASSWORD_LENGTH = 1024;
5
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../src/auth/contract.ts"],"names":[],"mappings":"AAkBA,MAAM,CAAC,MAAM,UAAU,GAAG,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,EAAW,CAAC;AACpG,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAC5C,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AACvC,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { OpenapiDocument, OpenapiSchema } from '../types.js';
2
+ export declare const AUTH_SCHEMAS: Record<string, OpenapiSchema>;
3
+ export declare const AUTH_SECURITY_SCHEMES: {
4
+ AuthBearer: {
5
+ type: string;
6
+ scheme: string;
7
+ description: string;
8
+ };
9
+ };
10
+ export declare function authOpenapiPaths(): OpenapiDocument['paths'];
@@ -0,0 +1,62 @@
1
+ import { json, ref, response } from '../openapi/helpers.js';
2
+ import { AUTH_PATHS, MAX_PASSWORD_LENGTH, MAX_USERNAME_LENGTH } from './contract.js';
3
+ export const AUTH_SCHEMAS = {
4
+ AuthUser: { type: 'object', additionalProperties: false, required: ['id', 'username'], properties: { id: { type: 'string' }, username: { type: 'string' } } },
5
+ AuthLoginInput: {
6
+ type: 'object',
7
+ additionalProperties: false,
8
+ required: ['username', 'password'],
9
+ properties: {
10
+ username: { type: 'string', minLength: 1, maxLength: MAX_USERNAME_LENGTH },
11
+ password: { type: 'string', format: 'password', writeOnly: true, minLength: 1, maxLength: MAX_PASSWORD_LENGTH },
12
+ },
13
+ },
14
+ AuthSession: {
15
+ type: 'object',
16
+ additionalProperties: false,
17
+ required: ['accessToken', 'expiresIn', 'user'],
18
+ properties: {
19
+ accessToken: { type: 'string' },
20
+ expiresIn: { type: 'integer', minimum: 1 },
21
+ user: ref('AuthUser'),
22
+ },
23
+ },
24
+ AuthLogoutResult: { type: 'object', additionalProperties: false, required: ['success'], properties: { success: { type: 'boolean' } } },
25
+ };
26
+ export const AUTH_SECURITY_SCHEMES = { AuthBearer: { type: 'http', scheme: 'bearer', description: 'Session token returned by POST /auth/login. Used only by GET /auth/me and POST /auth/logout.' } };
27
+ export function authOpenapiPaths() {
28
+ const unauthorized = response('Invalid credentials or expired session', ref('Error'));
29
+ return {
30
+ [AUTH_PATHS.login]: {
31
+ post: {
32
+ operationId: 'authLogin',
33
+ tags: ['auth'],
34
+ security: [],
35
+ requestBody: { required: true, ...json(ref('AuthLoginInput')) },
36
+ responses: {
37
+ 200: response('Session created', ref('AuthSession')),
38
+ 400: response('Invalid input', ref('Error')),
39
+ 401: unauthorized,
40
+ 429: response('Too many login attempts or sessions', ref('Error')),
41
+ },
42
+ },
43
+ },
44
+ [AUTH_PATHS.me]: {
45
+ get: {
46
+ operationId: 'authMe',
47
+ tags: ['auth'],
48
+ security: [{ AuthBearer: [] }],
49
+ responses: { 200: response('Current auth user', ref('AuthUser')), 401: unauthorized },
50
+ },
51
+ },
52
+ [AUTH_PATHS.logout]: {
53
+ post: {
54
+ operationId: 'authLogout',
55
+ tags: ['auth'],
56
+ security: [{ AuthBearer: [] }],
57
+ responses: { 200: response('Session revoked', ref('AuthLogoutResult')), 401: unauthorized },
58
+ },
59
+ },
60
+ };
61
+ }
62
+ //# sourceMappingURL=openapi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi.js","sourceRoot":"","sources":["../../../src/auth/openapi.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export declare const validPasswordHash: (value: unknown) => value is string;
2
+ export declare function hashPassword(password: string): Promise<string>;
3
+ export declare function verifyPassword(password: string, encoded: string): Promise<boolean>;
4
+ export declare const DUMMY_HASH: string;
@@ -0,0 +1,25 @@
1
+ import { randomBytes, scrypt, timingSafeEqual } from 'node:crypto';
2
+ import { MAX_PASSWORD_LENGTH } from './contract.js';
3
+ // OWASP scrypt profile: N=2^14, r=8, p=5 (16 MiB memory).
4
+ const PREFIX = 'scrypt$16384$8$5';
5
+ const HASH_PATTERN = /^scrypt\$16384\$8\$5\$([0-9a-f]{32})\$([0-9a-f]{64})$/;
6
+ export const validPasswordHash = (value) => typeof value === 'string' && HASH_PATTERN.test(value);
7
+ const derive = (password, salt) => new Promise((resolve, reject) => {
8
+ scrypt(password, salt, 32, { N: 16384, r: 8, p: 5 }, (error, key) => (error ? reject(error) : resolve(key)));
9
+ });
10
+ export async function hashPassword(password) {
11
+ if (typeof password !== 'string' || !password.length || password.length > MAX_PASSWORD_LENGTH)
12
+ throw new Error(`Password must contain 1..${MAX_PASSWORD_LENGTH} characters`);
13
+ const salt = randomBytes(16);
14
+ const hash = await derive(password, salt);
15
+ return `${PREFIX}$${salt.toString('hex')}$${hash.toString('hex')}`;
16
+ }
17
+ export async function verifyPassword(password, encoded) {
18
+ const parts = HASH_PATTERN.exec(encoded);
19
+ if (!parts)
20
+ return false;
21
+ const actual = await derive(password, Buffer.from(parts[1], 'hex'));
22
+ return timingSafeEqual(actual, Buffer.from(parts[2], 'hex'));
23
+ }
24
+ export const DUMMY_HASH = `${PREFIX}$${'0'.repeat(32)}$${'0'.repeat(64)}`;
25
+ //# sourceMappingURL=password.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"password.js","sourceRoot":"","sources":["../../../src/auth/password.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEpD,0DAA0D;AAC1D,MAAM,MAAM,GAAG,kBAAkB,CAAC;AAClC,MAAM,YAAY,GAAG,uDAAuD,CAAC;AAC7E,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,KAAc,EAAmB,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC5H,MAAM,MAAM,GAAG,CAAC,QAAgB,EAAE,IAAY,EAAmB,EAAE,CACjE,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;IAC9B,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC/G,CAAC,CAAC,CAAC;AACL,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,GAAG,mBAAmB;QAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,mBAAmB,aAAa,CAAC,CAAC;IAC7K,MAAM,IAAI,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAC7B,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC1C,OAAO,GAAG,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;AACrE,CAAC;AACD,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,QAAgB,EAAE,OAAe;IACpE,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzC,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IACpE,OAAO,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;AAC/D,CAAC;AACD,MAAM,CAAC,MAAM,UAAU,GAAG,GAAG,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC"}
@@ -0,0 +1,3 @@
1
+ export type { AuthConfig, AuthSession, AuthUser, AuthUserRecord } from './contract.js';
2
+ /** Creates a salted password hash for an auth user fixture. */
3
+ export declare function hashPassword(password: string): Promise<string>;
@@ -0,0 +1,5 @@
1
+ /** Creates a salted password hash for an auth user fixture. */
2
+ export async function hashPassword(password) {
3
+ return (await import('./password.js')).hashPassword(password);
4
+ }
5
+ //# sourceMappingURL=public.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public.js","sourceRoot":"","sources":["../../../src/auth/public.ts"],"names":[],"mappings":"AACA,+DAA+D;AAC/D,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,OAAO,CAAC,MAAM,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;AAChE,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { AuthService } from './service.js';
3
+ export declare function registerAuthRoutes(server: FastifyInstance, auth: AuthService): void;
@@ -0,0 +1,12 @@
1
+ import { AUTH_PATHS } from './contract.js';
2
+ export function registerAuthRoutes(server, auth) {
3
+ server.register(async (app) => {
4
+ app.addHook('onRequest', async (_request, reply) => {
5
+ reply.header('Cache-Control', 'no-store');
6
+ });
7
+ app.post(AUTH_PATHS.login, async (request) => auth.login(request.body));
8
+ app.get(AUTH_PATHS.me, async (request) => auth.me(request.headers.authorization));
9
+ app.post(AUTH_PATHS.logout, async (request) => auth.logout(request.headers.authorization));
10
+ });
11
+ }
12
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../../src/auth/routes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAG3C,MAAM,UAAU,kBAAkB,CAAC,MAAuB,EAAE,IAAiB;IAC3E,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QAC5B,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;YACjD,KAAK,CAAC,MAAM,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;QACH,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACxE,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;QAClF,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;IAC7F,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { type AuthConfig, type AuthSession, type AuthUser, type AuthUserRecord } from './contract.js';
2
+ export declare class AuthService {
3
+ private users;
4
+ private expiresIn;
5
+ private sessions;
6
+ private pendingLogins;
7
+ private closed;
8
+ constructor(users: Map<string, AuthUserRecord>, expiresIn: number);
9
+ login(body: unknown): Promise<AuthSession>;
10
+ me(authorization: unknown): AuthUser;
11
+ logout(authorization: unknown): {
12
+ success: boolean;
13
+ };
14
+ close(): void;
15
+ private session;
16
+ private prune;
17
+ }
18
+ export declare function createAuthService(config: AuthConfig): Promise<AuthService>;
@@ -0,0 +1,112 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { domainError } from '../errors.js';
4
+ import { isObject } from '../utils.js';
5
+ import { DEFAULT_SESSION_SECONDS, MAX_PASSWORD_LENGTH, MAX_USERNAME_LENGTH } from './contract.js';
6
+ import { DUMMY_HASH, validPasswordHash, verifyPassword } from './password.js';
7
+ const unauthorized = () => domainError('UNAUTHENTICATED', 'Invalid credentials or expired session');
8
+ const tokenKey = (token) => createHash('sha256').update(token).digest('hex');
9
+ const publicUser = (user) => ({ id: user.id, username: user.username });
10
+ export class AuthService {
11
+ users;
12
+ expiresIn;
13
+ sessions = new Map();
14
+ pendingLogins = 0;
15
+ closed = false;
16
+ constructor(users, expiresIn) {
17
+ this.users = users;
18
+ this.expiresIn = expiresIn;
19
+ }
20
+ async login(body) {
21
+ if (!isObject(body) ||
22
+ Object.keys(body).some((key) => !['username', 'password'].includes(key)) ||
23
+ typeof body.username !== 'string' ||
24
+ !body.username.length ||
25
+ body.username.length > MAX_USERNAME_LENGTH ||
26
+ typeof body.password !== 'string' ||
27
+ !body.password.length ||
28
+ body.password.length > MAX_PASSWORD_LENGTH)
29
+ throw domainError('INVALID_INPUT', 'Supply username and password');
30
+ if (this.closed)
31
+ throw unauthorized();
32
+ if (this.pendingLogins >= 4)
33
+ throw domainError('TOO_MANY_REQUESTS', 'Too many simultaneous login attempts');
34
+ this.pendingLogins++;
35
+ let user;
36
+ let valid;
37
+ try {
38
+ user = this.users.get(body.username);
39
+ valid = await verifyPassword(body.password, user?.passwordHash ?? DUMMY_HASH);
40
+ }
41
+ finally {
42
+ this.pendingLogins--;
43
+ }
44
+ if (!user || !valid || this.closed)
45
+ throw unauthorized();
46
+ this.prune();
47
+ if (this.sessions.size >= 10000)
48
+ throw domainError('TOO_MANY_REQUESTS', 'Session limit reached');
49
+ const accessToken = randomBytes(32).toString('base64url');
50
+ const identity = publicUser(user);
51
+ this.sessions.set(tokenKey(accessToken), { user: identity, expiresAt: Date.now() + this.expiresIn * 1000 });
52
+ return { accessToken, expiresIn: this.expiresIn, user: publicUser(identity) };
53
+ }
54
+ me(authorization) {
55
+ const [, session] = this.session(authorization);
56
+ return publicUser(session.user);
57
+ }
58
+ logout(authorization) {
59
+ const [key] = this.session(authorization);
60
+ this.sessions.delete(key);
61
+ return { success: true };
62
+ }
63
+ close() {
64
+ this.closed = true;
65
+ this.sessions.clear();
66
+ this.users.clear();
67
+ }
68
+ session(authorization) {
69
+ if (typeof authorization !== 'string')
70
+ throw unauthorized();
71
+ const match = /^Bearer ([A-Za-z0-9_-]{43})$/i.exec(authorization);
72
+ if (!match)
73
+ throw unauthorized();
74
+ const key = tokenKey(match[1]);
75
+ const session = this.sessions.get(key);
76
+ if (!session || session.expiresAt <= Date.now()) {
77
+ this.sessions.delete(key);
78
+ throw unauthorized();
79
+ }
80
+ return [key, session];
81
+ }
82
+ prune() {
83
+ const now = Date.now();
84
+ for (const [key, session] of this.sessions)
85
+ if (session.expiresAt <= now)
86
+ this.sessions.delete(key);
87
+ }
88
+ }
89
+ export async function createAuthService(config) {
90
+ const records = typeof config.users === 'string' ? JSON.parse(await readFile(config.users, 'utf8')) : config.users;
91
+ if (!Array.isArray(records))
92
+ throw new Error('Auth users must be an array');
93
+ const users = new Map();
94
+ const ids = new Set();
95
+ for (const record of records) {
96
+ if (!isObject(record) ||
97
+ Object.keys(record).some((key) => !['id', 'username', 'passwordHash'].includes(key)) ||
98
+ typeof record.id !== 'string' ||
99
+ !record.id.trim() ||
100
+ typeof record.username !== 'string' ||
101
+ !record.username.trim() ||
102
+ record.username.length > MAX_USERNAME_LENGTH ||
103
+ !validPasswordHash(record.passwordHash))
104
+ throw new Error('Auth users require id, username and a valid passwordHash');
105
+ if (ids.has(record.id) || users.has(record.username))
106
+ throw new Error('Auth user ids and usernames must be unique');
107
+ ids.add(record.id);
108
+ users.set(record.username, { id: record.id, username: record.username, passwordHash: record.passwordHash });
109
+ }
110
+ return new AuthService(users, config.expiresIn ?? DEFAULT_SESSION_SECONDS);
111
+ }
112
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../../../src/auth/service.ts"],"names":[],"mappings":"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"}