@kollors/deep-json-server 1.0.0-rc.7 → 1.0.0-rc.8

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 +13 -0
  2. package/MIGRATION.md +6 -6
  3. package/MIGRATION.ru.md +6 -6
  4. package/README.md +23 -11
  5. package/README.ru.md +23 -11
  6. package/dist/index.d.ts +1 -1
  7. package/dist/package.json +104 -0
  8. package/dist/src/auth/contract.d.ts +8 -0
  9. package/dist/src/auth/contract.js.map +1 -1
  10. package/dist/src/auth/public.d.ts +1 -1
  11. package/dist/src/auth/service.d.ts +7 -16
  12. package/dist/src/auth/service.js +40 -48
  13. package/dist/src/auth/service.js.map +1 -1
  14. package/dist/src/auth/store.d.ts +11 -5
  15. package/dist/src/auth/store.js +37 -19
  16. package/dist/src/auth/store.js.map +1 -1
  17. package/dist/src/cli/index.js +2 -2
  18. package/dist/src/cli/index.js.map +1 -1
  19. package/dist/src/core/constants.d.ts +1 -1
  20. package/dist/src/core/constants.js +2 -1
  21. package/dist/src/core/constants.js.map +1 -1
  22. package/dist/src/core/engine.d.ts +2 -1
  23. package/dist/src/core/engine.js +7 -3
  24. package/dist/src/core/engine.js.map +1 -1
  25. package/dist/src/core/model/infer.js +8 -5
  26. package/dist/src/core/model/infer.js.map +1 -1
  27. package/dist/src/core/mutations/write.js +5 -2
  28. package/dist/src/core/mutations/write.js.map +1 -1
  29. package/dist/src/core/paths.d.ts +4 -0
  30. package/dist/src/core/paths.js +7 -0
  31. package/dist/src/core/paths.js.map +1 -1
  32. package/dist/src/core/query/budget.d.ts +24 -0
  33. package/dist/src/core/query/budget.js +69 -0
  34. package/dist/src/core/query/budget.js.map +1 -0
  35. package/dist/src/core/query/execute.d.ts +2 -1
  36. package/dist/src/core/query/execute.js +5 -2
  37. package/dist/src/core/query/execute.js.map +1 -1
  38. package/dist/src/core/query/filter.d.ts +2 -1
  39. package/dist/src/core/query/filter.js +27 -9
  40. package/dist/src/core/query/filter.js.map +1 -1
  41. package/dist/src/core/records.d.ts +2 -0
  42. package/dist/src/core/records.js +15 -4
  43. package/dist/src/core/records.js.map +1 -1
  44. package/dist/src/files/contract.js +1 -0
  45. package/dist/src/files/contract.js.map +1 -1
  46. package/dist/src/files/disk-store.js +7 -5
  47. package/dist/src/files/disk-store.js.map +1 -1
  48. package/dist/src/graphql/preflight.d.ts +11 -2
  49. package/dist/src/graphql/preflight.js +48 -2
  50. package/dist/src/graphql/preflight.js.map +1 -1
  51. package/dist/src/graphql/resolvers.d.ts +3 -0
  52. package/dist/src/graphql/resolvers.js +47 -8
  53. package/dist/src/graphql/resolvers.js.map +1 -1
  54. package/dist/src/graphql/routes.d.ts +1 -1
  55. package/dist/src/graphql/routes.js +16 -0
  56. package/dist/src/graphql/routes.js.map +1 -1
  57. package/dist/src/graphql/schema.js +5 -3
  58. package/dist/src/graphql/schema.js.map +1 -1
  59. package/dist/src/rest/options.d.ts +2 -1
  60. package/dist/src/rest/options.js +5 -3
  61. package/dist/src/rest/options.js.map +1 -1
  62. package/dist/src/rest/projection.d.ts +4 -3
  63. package/dist/src/rest/projection.js +31 -12
  64. package/dist/src/rest/projection.js.map +1 -1
  65. package/dist/src/rest/routes.js +13 -7
  66. package/dist/src/rest/routes.js.map +1 -1
  67. package/dist/src/server/bootstrap.js +15 -7
  68. package/dist/src/server/bootstrap.js.map +1 -1
  69. package/dist/src/server/input-paths.d.ts +14 -1
  70. package/dist/src/server/input-paths.js +18 -2
  71. package/dist/src/server/input-paths.js.map +1 -1
  72. package/dist/src/server/storage-lock.d.ts +7 -1
  73. package/dist/src/server/storage-lock.js +103 -99
  74. package/dist/src/server/storage-lock.js.map +1 -1
  75. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0-rc.8
4
+
5
+ - Persist hashed auth sessions with users in file storage so valid tokens survive restarts, with atomic logout and password-change revocation.
6
+ - Replace the lock directory with one readable `<source-name>-lock.json` file and native OS locking. Lock the database, auth store, upload root and file metadata; recover after crashes and reject competing writers.
7
+ - Protect configured files and their symlink targets from upload, deletion and schema export. Reject malformed Unicode file paths before changing storage.
8
+ - Preserve protected nested fields when a nullable ancestor is cleared, and infer relations across numeric seed IDs and generated string IDs.
9
+ - Use `key` for the primary-key argument of GraphQL update/replace mutations when the primary field is named `data`.
10
+ - Bound REST and GraphQL selection, expansion, query work and response size; document that GraphQL result errors do not undo committed mutations.
11
+ - Derive the CLI version from package metadata and verify the installed CLI over HTTP, including paths with spaces and Unicode and persistence after a crash.
12
+ - Run type checks, lint, coverage and installed-package checks on Linux, Windows and macOS with Node.js 22, 24 and 26. Publish only after the full matrix succeeds for the selected commit.
13
+
14
+ When upgrading from an earlier RC, stop all old server processes before starting RC8: the lock protocol has changed. Existing auth files without `sessions` remain valid. Regenerate GraphQL clients if a model uses `data` as its primary field.
15
+
3
16
  ## 1.0.0-rc.7
4
17
 
5
18
  - Treat nested objects containing only a primary key as references in POST, PUT and PATCH, preserving the target's fields and audit values. Actual changes, including inverse key changes, still require ownership.
package/MIGRATION.md CHANGED
@@ -1,8 +1,8 @@
1
- # Migrating from 0.9.0 to 1.0.0-rc.7
1
+ # Migrating from 0.9.0 to 1.0.0-rc.8
2
2
 
3
3
  [Русский](MIGRATION.ru.md) · [Current README](README.md)
4
4
 
5
- This guide compares `v0.9.0` with `1.0.0-rc.7` and also covers the relation changes since RC6. Update configuration and clients together. Node.js 22 or newer is still required. Back up the database, config, schema, uploaded files and metadata before migrating.
5
+ This guide compares `v0.9.0` with `1.0.0-rc.8` and also covers the relation changes since RC6. Update configuration and clients together. Node.js 22 or newer is still required. Back up the database, config, schema, uploaded files and metadata before migrating.
6
6
 
7
7
  ## 1. Choose whether you need a schema
8
8
 
@@ -14,7 +14,7 @@ export default { database: { path: './database.json' } };
14
14
  ```
15
15
 
16
16
  ```js
17
- // 1.0.0-rc.7
17
+ // 1.0.0-rc.8
18
18
  export default { storage: 'file', database: { source: './database.json' } };
19
19
  ```
20
20
 
@@ -22,7 +22,7 @@ This keeps schemaless REST and relations inferred from stored `...Id` / `...Ids`
22
22
 
23
23
  ## 2. Convert configuration
24
24
 
25
- | 0.9.0 | 1.0.0-rc.7 |
25
+ | 0.9.0 | 1.0.0-rc.8 |
26
26
  |---|---|
27
27
  | `database.path` | `storage: 'file'` and `database.source` |
28
28
  | `database.data` | `storage: 'memory'` and `database.source` |
@@ -178,7 +178,7 @@ Omitted inverse (`keyOn: 'related'`) relations survive PUT. Explicit `users: []`
178
178
 
179
179
  Auth, timestamps and soft deletion are new, optional features. To first preserve unauthenticated writes and physical deletion, omit `auth` and leave `timestamps` / `softDelete` disabled (their defaults).
180
180
 
181
- If enabling auth, create a separate auth user store and migrate ownership deliberately. Old records without `createdById` can be changed only by administrators. The ownership field is server-managed through the API; prepare ownership in the copied database before startup if ordinary users must edit existing records. Auth does not close read endpoints or file operations. Sessions are in memory and end when the server restarts.
181
+ If enabling auth, create a separate auth user store and migrate ownership deliberately. Old records without `createdById` can be changed only by administrators. The ownership field is server-managed through the API; prepare ownership in the copied database before startup if ordinary users must edit existing records. Auth does not close read endpoints or file operations. In RC8, file storage persists hashed sessions in the auth user file and restores unexpired sessions after a restart. Existing users without `sessions` remain valid. When manually changing a password hash, also clear that user's `sessions`.
182
182
 
183
183
  With `softDelete`, DELETE retains records, lists hide deleted records by default, direct primary-key reads can retrieve them, and successful PUT/PATCH restores them. There is no automatic conversion of an old application archive flag. See the [lifecycle rules](README.md#record-dates-deletion-and-ownership) before enabling this feature.
184
184
 
@@ -207,5 +207,5 @@ Schema generation alone does not validate existing database records. Call `ready
207
207
  1. Pin the chosen RC version and migrate a **copy** of the 0.9.0 files. Run generation, then initialize the HTTP server to validate data too.
208
208
  2. Check representative reads: defaults, explicit foreign keys, nested lists, filters and pagination. Check create, PATCH, PUT, link changes and deletion using disposable copied records.
209
209
  3. If enabling auth, test an owner, an administrator and an ordinary non-owner against old records. Confirm uploaded files remain accessible through the retained metadata path.
210
- 4. Stop the old server before switching. Run one process per database/file store; current versions lock the disk database. Stop the process before manual edits.
210
+ 4. Stop every old server process before switching: RC8 replaces the old lock directory with native OS locks and readable `<source-name>-lock.json` files for the database, auth, upload root and file metadata. Old and new lock protocols do not coordinate. Run one process per writable store and stop it before manual edits.
211
211
  5. Keep the original package version, config, schema, data and file metadata together for rollback. Restore them together; do not point 0.9.0 at data already modified by the new server. Include new increment counters (`<database>.counters.json`) and soft-delete restoration metadata in subsequent backups when used.
package/MIGRATION.ru.md CHANGED
@@ -1,8 +1,8 @@
1
- # Переход с 0.9.0 на 1.0.0-rc.7
1
+ # Переход с 0.9.0 на 1.0.0-rc.8
2
2
 
3
3
  [English](MIGRATION.md) · [Актуальный README](README.ru.md)
4
4
 
5
- Руководство сравнивает релиз `v0.9.0` с `1.0.0-rc.7` и также описывает изменения связей после RC6. Конфигурацию и клиентов нужно обновлять вместе. Требование Node.js 22 или новее сохраняется. Перед переходом скопируйте базу, конфигурацию, схему, загруженные файлы и их метаданные.
5
+ Руководство сравнивает релиз `v0.9.0` с `1.0.0-rc.8` и также описывает изменения связей после RC6. Конфигурацию и клиентов нужно обновлять вместе. Требование Node.js 22 или новее сохраняется. Перед переходом скопируйте базу, конфигурацию, схему, загруженные файлы и их метаданные.
6
6
 
7
7
  ## 1. Выберите режим со схемой или без неё
8
8
 
@@ -14,7 +14,7 @@ export default { database: { path: './database.json' } };
14
14
  ```
15
15
 
16
16
  ```js
17
- // 1.0.0-rc.7
17
+ // 1.0.0-rc.8
18
18
  export default { storage: 'file', database: { source: './database.json' } };
19
19
  ```
20
20
 
@@ -22,7 +22,7 @@ REST без схемы и связи по именам хранимых поле
22
22
 
23
23
  ## 2. Обновите конфигурацию
24
24
 
25
- | В 0.9.0 | В 1.0.0-rc.7 |
25
+ | В 0.9.0 | В 1.0.0-rc.8 |
26
26
  |---|---|
27
27
  | `database.path` | `storage: 'file'` и `database.source` |
28
28
  | `database.data` | `storage: 'memory'` и `database.source` |
@@ -178,7 +178,7 @@ const url = `/users?${new URLSearchParams({ scope: JSON.stringify(scope) })}`;
178
178
 
179
179
  Auth, даты и мягкое удаление — новые необязательные возможности. Для первого перехода с сохранением записи без входа и физического удаления не добавляйте `auth`, оставьте `timestamps` / `softDelete` выключенными — это их значения по умолчанию.
180
180
 
181
- Если включаете auth, создайте отдельное хранилище учётных записей и перенесите владельцев осознанно. Старые записи без `createdById` может менять только администратор. Через API владелец управляется сервером; подготовьте его в копии базы до запуска, если старые записи должны редактировать обычные пользователи. Auth не закрывает чтение и файловые операции. Сессии хранятся в памяти и заканчиваются при перезапуске.
181
+ Если включаете auth, создайте отдельное хранилище учётных записей и перенесите владельцев осознанно. Старые записи без `createdById` может менять только администратор. Через API владелец управляется сервером; подготовьте его в копии базы до запуска, если старые записи должны редактировать обычные пользователи. Auth не закрывает чтение и файловые операции. В RC8 дисковый режим сохраняет хеши сессий в файле пользователей и восстанавливает действующие сессии после перезапуска. Старые пользователи без `sessions` остаются допустимыми. При ручной замене хеша пароля также очистите `sessions` этого пользователя.
182
182
 
183
183
  При `softDelete` DELETE сохраняет запись, списки по умолчанию скрывают удалённые записи, чтение по первичному ключу может вернуть их, а успешный PUT/PATCH восстанавливает. Старый пользовательский флаг архивирования не преобразуется автоматически. Перед включением прочитайте [правила дат, удаления и владения](README.ru.md#даты-записей-удаление-и-владельцы).
184
184
 
@@ -207,5 +207,5 @@ await app.close();
207
207
  1. Зафиксируйте выбранную RC-версию и переведите **копию** файлов 0.9.0. Выполните генерацию, затем инициализацию HTTP-сервера для проверки данных.
208
208
  2. Проверьте чтение по умолчанию, явные ключи, вложенные списки, фильтры и пагинацию. На одноразовых копиях записей проверьте создание, PATCH, PUT, изменение связей и удаление.
209
209
  3. При включении auth проверьте владельца, администратора и обычного пользователя без владения на старых записях. Убедитесь, что загруженные файлы доступны через прежний путь к метаданным.
210
- 4. Перед переключением остановите старый сервер. Используйте один процесс для базы и файлового хранилища: новая версия блокирует дисковую базу. Для ручных правок останавливайте процесс.
210
+ 4. Перед переключением остановите все старые процессы сервера: RC8 заменяет каталог блокировки системными блокировками и читаемыми файлами `<имя-источника>-lock.json` для базы, auth, каталога загрузок и метаданных файлов. Старый и новый протоколы блокировки не взаимодействуют. Используйте один процесс на записываемое хранилище и останавливайте его перед ручными правками.
211
211
  5. Для отката храните вместе старую версию пакета, конфигурацию, схему, базу и метаданные файлов. Восстанавливайте их вместе; не направляйте 0.9.0 на данные, уже изменённые новым сервером. В последующие резервные копии включайте новые счётчики (`<database>.counters.json`) и служебные данные восстановления мягкого удаления, если используете эти функции.
package/README.md CHANGED
@@ -14,7 +14,7 @@ A JSON mock server with REST, GraphQL, related records, file uploads and schema
14
14
  npm install @kollors/deep-json-server@rc
15
15
  ```
16
16
 
17
- To install this release candidate, use `@1.0.0-rc.7`.
17
+ To install this release candidate, use `@1.0.0-rc.8`.
18
18
 
19
19
  ## Quick start
20
20
 
@@ -232,7 +232,7 @@ String and numeric constraints on `string[]`/`number[]` apply to every element.
232
232
 
233
233
  Each `enum` value must match the field type and its constraints. Use `nullable: true` to allow `null` for the field; do not include `null` in `enum`. Invalid values are rejected when the schema is loaded.
234
234
 
235
- Each model requires exactly one primary key of type `string` or `number`, declared at the top level. The name is arbitrary: `id`, `username`, `code`. If `generated` is omitted, the client supplies the value on creation. Generated fields must be declared at the top level, are excluded from input types and cannot have `default`. Replacing a record preserves generated values and read-only fields, including nested objects. To protect fields inside an array, mark the entire array or its containing object as `readOnly`. Objects containing only server-managed fields are output-only.
235
+ Each model requires exactly one primary key of type `string` or `number`, declared at the top level. The name is arbitrary: `id`, `username`, `code`. If `generated` is omitted, the client supplies the value on creation. Generated fields must be declared at the top level, are excluded from input types and cannot have `default`. Replacing a record preserves generated values and read-only fields, including nested objects. Setting a nullable parent to `null` is rejected if it would remove stored read-only descendants. To protect fields inside an array, mark the entire array or its containing object as `readOnly`. Objects containing only server-managed fields are output-only.
236
236
 
237
237
  For example, a `LocalUser` with primary key `username` and `password: {"type":"string","required":true,"writeOnly":true}` has `localUser(username: ...)` and `/localUsers/{username}`. A `writeOnly` field accepts input and is excluded from responses, `scope`, filters and ordering.
238
238
 
@@ -474,6 +474,8 @@ Omitting `scope` returns scalar fields, as with `[{"*":true}]`. An empty selecti
474
474
 
475
475
  Arguments are available only on lists. A list can instead use `{ "union": [scope, ...] }`: each part is a normal list scope, parts run in array order, and the first record for each primary key is kept. For embedded object arrays without primary keys, each source element is kept once, even if several parts select it. Distinct elements with equal contents remain separate. Single-record queries and mutation responses can set arguments on their embedded lists. Parameters are validated even on empty data; an invalid response selection rolls back record changes. Invalid scopes return `400`. The JSON length limit is 10,000 characters; selection depth is limited to 32 levels.
476
476
 
477
+ REST and GraphQL also enforce a shared budget for each request: at most 1,000 selection nodes, 10,000 expanded records, 100,000 accounted execution steps, and a conservative JSON size estimate of 8 MiB. Nested lists, `union` parts and GraphQL aliases consume the same budget. Exceeding a limit returns REST `400` or GraphQL `INVALID_QUERY`; reduce selected fields, nesting or page sizes. REST mutations roll back if their response projection exceeds the budget.
478
+
477
479
  ### Nested writes
478
480
 
479
481
  Explicitly declared storage keys such as `genreIds: ["1"]` only set a relation. Relation fields also accept records to create or update:
@@ -571,9 +573,13 @@ query {
571
573
 
572
574
  The query `user(id: ...)` returns one record or `null` if it is missing. Mutations are `userCreate(data: ...)`, `userReplace(id: ..., data: ...)`, `userUpdate(id: ..., data: ...)`, `userDelete(id: ...)`. For models containing only generated fields, the create mutation takes no `data` argument. Writes and validation follow the same rules as REST. Relations selected in a mutation result determine the response contents.
573
575
 
576
+ When the primary key is named `data`, Update/Replace mutations accept its value as `key` and the input object as `data`, for example `entryUpdate(key: "one", data: { name: "Updated" })`. Find/Delete keep the primary-key argument named `data`.
577
+
574
578
  String primary keys use GraphQL `ID`; ordinary strings use `String`, numbers use `Float`, and pagination parameters use `Int`. Schema enums preserve valid string labels; other values receive `VALUE_0`, `VALUE_1`, etc. String lengths, formats and other model constraints are validated by the server during request execution. Introspection is available for exploring the schema. Selected list arguments are validated before executing mutations.
575
579
 
576
- Errors include `extensions.code`: `INVALID_INPUT`, `INVALID_QUERY`, `NOT_FOUND`, `CONFLICT`, `UNAUTHENTICATED`, `FORBIDDEN` or `INTERNAL_ERROR`. GraphQL syntax and type errors appear in the standard `errors` array. Query depth is limited to 32 levels.
580
+ Errors include `extensions.code`: `INVALID_INPUT`, `INVALID_QUERY`, `NOT_FOUND`, `CONFLICT`, `UNAUTHENTICATED`, `FORBIDDEN` or `INTERNAL_ERROR`. GraphQL syntax and type errors appear in the standard `errors` array. Query depth is limited to 32 levels. The request budgets described for REST also apply to GraphQL. Selection limits and list arguments are checked before mutations run. Errors while resolving a mutation result, including runtime budget exhaustion, do not undo a mutation that has already committed; query the record to check its state before retrying.
581
+
582
+ A GraphQL operation containing several mutation fields is not one transaction: fields execute in order, and earlier committed changes remain if a later result fails. Runtime budget exhaustion returns `data: null` and stops subsequent mutation fields, but `data: null` does not mean writes were rolled back. Recover the saved state with a smaller query. Automatically retrying a create mutation with a generated key can create a second record.
577
583
 
578
584
  ## OpenAPI and schema exports
579
585
 
@@ -615,7 +621,7 @@ if (!password) throw new Error('Set DJS_PASSWORD');
615
621
  await writeFile(
616
622
  './auth.json',
617
623
  JSON.stringify(
618
- [{ id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true }],
624
+ [{ id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true, sessions: [] }],
619
625
  null,
620
626
  2,
621
627
  ),
@@ -633,7 +639,7 @@ export default {
633
639
  };
634
640
  ```
635
641
 
636
- Start with `npx deep-json-server server.config.js`. Each initial user needs a unique string `id`, a unique `username` and a `passwordHash` created by the helper. `isAdmin` defaults to `false`. Passwords use salted scrypt hashes.
642
+ Start with `npx deep-json-server server.config.js`. Each initial user needs a unique string `id`, a unique `username` and a `passwordHash` created by the helper. `isAdmin` defaults to `false`, and `sessions` defaults to `[]`, so existing user files remain valid. Passwords use salted scrypt hashes.
637
643
 
638
644
  With `storage: 'memory'`, pass an array in `auth.source`:
639
645
 
@@ -648,13 +654,19 @@ export default {
648
654
  database: { source: { items: [] } },
649
655
  auth: {
650
656
  source: [
651
- { id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true },
657
+ { id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true, sessions: [] },
652
658
  ],
653
659
  },
654
660
  };
655
661
  ```
656
662
 
657
- Auth users are stored separately from the database. In file mode, changes are saved to `auth.source`; in memory mode, they disappear on restart. The supplied user array is not modified. Restart the server after editing the file manually.
663
+ Auth users and their sessions are stored together in `auth.source`, separately from the database. Each entry in a user's `sessions` array contains `{ tokenHash, expiresAt }`: `tokenHash` is the SHA-256 hash of the access token as 64 lowercase hexadecimal characters, and `expiresAt` is an absolute Unix timestamp in milliseconds. Raw tokens are never stored, and auth responses expose neither password hashes nor session records. Token hashes must be unique across all users.
664
+
665
+ With `storage: 'file'`, login, logout and user changes are saved to the same JSON file before a successful response. Valid sessions survive server restarts: the client can keep using its existing token. A password change and removal of that user's sessions are saved together. A failed write leaves the previous users and sessions unchanged. Restart the server after editing the file manually; if you replace a password hash manually, also clear that user's `sessions`.
666
+
667
+ With `storage: 'memory'`, the supplied user array is not modified. Every startup loads the initial users and sessions again. Sessions created at runtime disappear on restart; unexpired sessions supplied in the initial data are restored, including any that were logged out during the previous run. The client still needs the corresponding raw token; it cannot recover it from `tokenHash`.
668
+
669
+ `auth.expiresIn` sets the lifetime of new sessions and defaults to 3600 seconds. Existing sessions keep their saved expiration time, including across restarts. Expired sessions are ignored on startup, rejected on every request and removed from all users on the next successful auth update, such as login or logout. There is no automatic extension of the expiration time.
658
670
 
659
671
  | REST request | JSON body | Response |
660
672
  |---|---|---|
@@ -665,13 +677,13 @@ Auth users are stored separately from the database. In file mode, changes are sa
665
677
  | `PATCH /auth/users/:id/password` | `{ "currentPassword": "…", "newPassword": "…" }` | `{ success: true }` |
666
678
  | `PATCH /auth/users/:id/admin` | `{ "isAdmin": true }` | `{ id, username, isAdmin }` |
667
679
 
668
- Registration and login are public. Send `Authorization: Bearer <accessToken>` for the other methods. Registration creates an ordinary user with a generated `id`; requests cannot include `id`, `passwordHash` or `isAdmin`. Usernames are case-sensitive and unique; duplicates return `409`. A username must contain a non-whitespace character and be at most 256 characters long. Passwords must contain 1–1024 characters. Values are not trimmed. Registration does not create a session: log in afterwards.
680
+ Registration and login are public. Send `Authorization: Bearer <accessToken>` for the other methods. Registration creates an ordinary user with a generated `id`; requests cannot include `id`, `passwordHash`, `isAdmin` or `sessions`. Usernames are case-sensitive and unique; duplicates return `409`. A username must contain a non-whitespace character and be at most 256 characters long. Passwords must contain 1–1024 characters. Values are not trimmed. Registration does not create a session: log in afterwards.
669
681
 
670
682
  Users can change only their own password by supplying `currentPassword` and `newPassword`. Administrators follow the same rule for their own password. An administrator can change an ordinary user's password with just `newPassword`. Changing another administrator's password returns `403`. A successful password change ends all sessions of the target user, including the current session when changing your own password; log in again. An incorrect current password returns `401` without changing sessions.
671
683
 
672
684
  Only administrators can change `isAdmin`. They can grant or remove another user's admin status. An administrator can remove their own status only if another administrator remains; otherwise the request returns `409`. This check accounts for concurrent requests. Existing tokens use the new permissions as soon as the change is saved, including for GraphQL mutations. An administrator may demote another administrator and then change their password as an ordinary user.
673
685
 
674
- Invalid or expired tokens return `401`, insufficient permissions return `403`, and an absent user for an otherwise permitted operation returns `404`. Invalid request bodies return `400`. Login, registration and password changes may return `429` when too many password computations are running; login also limits active sessions. Sessions are kept in memory and disappear on restart. Logout revokes only the supplied token.
686
+ Invalid or expired tokens return `401`, insufficient permissions return `403`, and an absent user for an otherwise permitted operation returns `404`. Invalid request bodies return `400`. Login, registration and password changes may return `429` when too many password computations are running; login also returns `429` when there are already 10,000 active sessions across all users, including sessions loaded from the source. Logout revokes only the supplied token.
675
687
 
676
688
  Every model record has a virtual `actions` object when auth is enabled. It is calculated for the current user and is never stored. In REST, request it explicitly through `scope`:
677
689
 
@@ -842,7 +854,7 @@ Content-Type: application/json
842
854
 
843
855
  In disk mode, the binary is stored at `<files.source>/<directory>/<name>`. Metadata defaults to `<files.source>/.files.json`; set `files.metadata` for another location. Directories and the metadata file are created when needed.
844
856
 
845
- Use one server process per disk database and file store. A second server using the same database file fails at startup; closing the first server releases its lock, and a lock left by a crashed process is recovered on the next start. Stop the server before editing stored files or metadata manually. Paths inside the file storage directory cannot contain symbolic links. Uploads and renames cannot overwrite the database, its lock, counters, schema, auth users, loaded configuration or metadata file.
857
+ Use one server process per writable disk source. A second server sharing the database, auth users, file storage directory or metadata fails at startup. Locks are single JSON files next to their sources: `database.json` uses `database-lock.json`, with the last extension removed from the configured source name. A lock records its type, process ID and owner token. Closing the server removes the lock files; after a crash the operating system releases the locks and the next server reuses the abandoned files. Disk locking uses the native `fs-native-extensions` dependency; memory storage and schema generation do not load it. Symbolic-link sources lock both the original entry and its target. Stop the server before editing stored files or metadata manually. Paths inside the file storage directory cannot contain symbolic links. Uploads and renames cannot overwrite the database, its lock, counters, schema, auth users, loaded configuration or metadata file.
846
858
 
847
859
  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.
848
860
 
@@ -895,6 +907,6 @@ npm run verify
895
907
 
896
908
  `npm run verify` checks types, code style, test coverage and installation from the package archive.
897
909
 
898
- To publish a prerelease, update the version in `package.json`, `package-lock.json` and `src/core/constants.ts`, then push the commit to `main`. GitHub Actions creates its `v<version>` tag and publishes through trusted publishing to the `alpha`, `beta` or `rc` channel. Stable versions publish to `latest` from an explicitly pushed version tag.
910
+ To publish a prerelease, update the version in `package.json` and `package-lock.json` (the runtime and CLI read it from package metadata), then push the commit to `main`. GitHub Actions creates its `v<version>` tag and publishes through trusted publishing to the `alpha`, `beta` or `rc` channel. Stable versions publish to `latest` from an explicitly pushed version tag.
899
911
 
900
912
  License: MIT.
package/README.ru.md CHANGED
@@ -14,7 +14,7 @@ JSON-сервер для создания тестового API с REST, GraphQ
14
14
  npm install @kollors/deep-json-server@rc
15
15
  ```
16
16
 
17
- Для установки этого релиз-кандидата укажите `@1.0.0-rc.7`.
17
+ Для установки этого релиз-кандидата укажите `@1.0.0-rc.8`.
18
18
 
19
19
  ## Быстрый старт
20
20
 
@@ -232,7 +232,7 @@ REST без схемы создаёт ключ `id` и сохраняет про
232
232
 
233
233
  Каждое значение `enum` должно соответствовать типу поля и его ограничениям. Чтобы разрешить `null` для поля, укажите `nullable: true`; добавлять `null` в `enum` не нужно. Некорректные значения отклоняются при загрузке схемы.
234
234
 
235
- У модели ровно один первичный ключ типа `string` или `number`, объявленный на верхнем уровне. Имя произвольное: `id`, `username`, `code`. Если `generated` не задан, значение передаёт клиент при создании. Генерируемые поля объявляются на верхнем уровне, исключаются из входных типов и несовместимы с `default`. При замене записи сохраняются генерируемые значения и поля `readOnly`, включая вложенные объекты. Для защиты полей внутри массива задайте `readOnly` всему массиву или содержащему его объекту. Объекты, состоящие из серверных полей, доступны только в ответах.
235
+ У модели ровно один первичный ключ типа `string` или `number`, объявленный на верхнем уровне. Имя произвольное: `id`, `username`, `code`. Если `generated` не задан, значение передаёт клиент при создании. Генерируемые поля объявляются на верхнем уровне, исключаются из входных типов и несовместимы с `default`. При замене записи сохраняются генерируемые значения и поля `readOnly`, включая вложенные объекты. Обнуление nullable-родителя отклоняется, если оно удалит сохранённые поля `readOnly` внутри него. Для защиты полей внутри массива задайте `readOnly` всему массиву или содержащему его объекту. Объекты, состоящие из серверных полей, доступны только в ответах.
236
236
 
237
237
  Например, `LocalUser` с первичным ключом `username` и полем `password: {"type":"string","required":true,"writeOnly":true}` получает запрос `localUser(username: ...)` и маршрут `/localUsers/{username}`. Поле `writeOnly` доступно для записи и исключено из ответов, `scope`, фильтров и сортировки.
238
238
 
@@ -474,6 +474,8 @@ const response = await fetch(`/users?${params}`);
474
474
 
475
475
  Аргументы доступны только у списков. Вместо них список может использовать `{ "union": [scope, ...] }`: каждая часть — обычный scope списка, части выполняются в порядке массива, а для каждого первичного ключа остаётся первая запись. Во вложенных массивах объектов без первичных ключей каждый исходный элемент включается один раз, даже если его выбрали несколько частей. Разные элементы с одинаковым содержимым сохраняются отдельно. В запросе отдельной записи и в ответе мутации аргументы можно задать для вложенных списков. Параметры проверяются даже на пустых данных; ошибка в выборе ответа отменяет изменения записи. Некорректный `scope` возвращает `400`. Максимальная длина JSON — 10 000 символов, глубина выбора — 32 уровня.
476
476
 
477
+ REST и GraphQL также ограничивают общий объём работы каждого запроса: до 1 000 узлов выбора, 10 000 развёрнутых записей, 100 000 учитываемых шагов выполнения и консервативной оценки размера JSON в 8 МиБ. Вложенные списки, части `union` и GraphQL aliases расходуют общий бюджет. При превышении сервер возвращает REST `400` или GraphQL `INVALID_QUERY`; уменьшите выборку полей, вложенность или размер страниц. Мутация REST откатывается, если построение её ответа превышает бюджет.
478
+
477
479
  ### Вложенная запись
478
480
 
479
481
  Явно объявленные поля ключей, например `genreIds: ["1"]`, только задают связь. В поля связей можно передавать записи для создания или обновления:
@@ -571,9 +573,13 @@ query {
571
573
 
572
574
  Запрос `user(id: ...)` возвращает одну запись или `null`, если она отсутствует. Мутации: `userCreate(data: ...)`, `userReplace(id: ..., data: ...)`, `userUpdate(id: ..., data: ...)`, `userDelete(id: ...)`. Для модели, состоящей из генерируемых полей, мутация создания вызывается без аргумента `data`. Правила записи и валидации совпадают с REST. Связи, выбранные в результате мутации, определяют содержимое ответа.
573
575
 
576
+ Если первичный ключ называется `data`, мутации Update/Replace принимают его значение в аргументе `key`, а объект изменений — в `data`, например `entryUpdate(key: "one", data: { name: "Updated" })`. В Find/Delete аргумент ключа по-прежнему называется `data`.
577
+
574
578
  Строковый первичный ключ представлен типом GraphQL `ID`, обычные строки — `String`, числа — `Float`, параметры пагинации — `Int`. Enum сохраняет допустимые строковые имена; остальные значения получают имена `VALUE_0`, `VALUE_1` и т. д. Длины строк, форматы и другие ограничения модели проверяются сервером при выполнении запроса. Для просмотра схемы доступна интроспекция. Параметры выбранных списков проверяются до выполнения мутаций.
575
579
 
576
- Ошибки содержат `extensions.code`: `INVALID_INPUT`, `INVALID_QUERY`, `NOT_FOUND`, `CONFLICT`, `UNAUTHENTICATED`, `FORBIDDEN` или `INTERNAL_ERROR`. Ошибки синтаксиса и типов GraphQL возвращаются в стандартном массиве `errors`. Максимальная глубина запроса — 32 уровня.
580
+ Ошибки содержат `extensions.code`: `INVALID_INPUT`, `INVALID_QUERY`, `NOT_FOUND`, `CONFLICT`, `UNAUTHENTICATED`, `FORBIDDEN` или `INTERNAL_ERROR`. Ошибки синтаксиса и типов GraphQL возвращаются в стандартном массиве `errors`. Максимальная глубина запроса — 32 уровня. Ограничения общего объёма работы, описанные для REST, действуют и в GraphQL. Лимит узлов выбора и аргументы списков проверяются до выполнения мутаций. Ошибки при построении результата мутации, включая превышение бюджета во время выполнения, не отменяют уже сохранённую мутацию; перед повтором запроса проверьте состояние записи.
581
+
582
+ Операция GraphQL с несколькими полями мутаций не является одной транзакцией: поля выполняются по порядку, а ранее сохранённые изменения остаются при ошибке последующего результата. Превышение бюджета во время выполнения возвращает `data: null` и останавливает следующие поля мутаций, но `data: null` не означает откат записей. Прочитайте сохранённое состояние менее объёмным запросом. Автоматический повтор создания с генерируемым ключом может создать вторую запись.
577
583
 
578
584
  ## OpenAPI и экспорт схем
579
585
 
@@ -615,7 +621,7 @@ if (!password) throw new Error('Set DJS_PASSWORD');
615
621
  await writeFile(
616
622
  './auth.json',
617
623
  JSON.stringify(
618
- [{ id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true }],
624
+ [{ id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true, sessions: [] }],
619
625
  null,
620
626
  2,
621
627
  ),
@@ -633,7 +639,7 @@ export default {
633
639
  };
634
640
  ```
635
641
 
636
- Запустите `npx deep-json-server server.config.js`. Каждой исходной учётной записи нужны уникальный строковый `id`, уникальный `username` и `passwordHash`, созданный функцией выше. `isAdmin` по умолчанию равен `false`. Пароли хешируются через scrypt со случайной солью.
642
+ Запустите `npx deep-json-server server.config.js`. Каждой исходной учётной записи нужны уникальный строковый `id`, уникальный `username` и `passwordHash`, созданный функцией выше. `isAdmin` по умолчанию равен `false`, а `sessions` — `[]`, поэтому существующие файлы пользователей остаются допустимыми. Пароли хешируются через scrypt со случайной солью.
637
643
 
638
644
  При `storage: 'memory'` передайте массив в `auth.source`:
639
645
 
@@ -648,13 +654,19 @@ export default {
648
654
  database: { source: { items: [] } },
649
655
  auth: {
650
656
  source: [
651
- { id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true },
657
+ { id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true, sessions: [] },
652
658
  ],
653
659
  },
654
660
  };
655
661
  ```
656
662
 
657
- Учётные записи auth хранятся отдельно от базы. В файловом режиме изменения сохраняются в `auth.source`; в режиме памяти они исчезают после перезапуска. Переданный массив пользователей не изменяется. После ручного изменения файла перезапустите сервер.
663
+ Учётные записи auth и их сессии хранятся вместе в `auth.source`, отдельно от базы. Каждый элемент массива `sessions` пользователя содержит `{ tokenHash, expiresAt }`: `tokenHash` — SHA-256 хеш токена в виде 64 шестнадцатеричных символов нижнего регистра, а `expiresAt` — абсолютное время истечения сессии в миллисекундах Unix. Исходные токены не сохраняются, а ответы auth не раскрывают ни хеши паролей, ни записи сессий. Хеши токенов должны быть уникальны среди всех пользователей.
664
+
665
+ При `storage: 'file'` вход, выход и изменения пользователей сохраняются в один JSON-файл до успешного ответа. Действующие сессии переживают перезапуск сервера: клиент может продолжать использовать прежний токен. Новый пароль и удаление всех сессий пользователя сохраняются вместе. При ошибке записи прежние пользователи и сессии сохраняются без изменений. После ручного редактирования файла перезапустите сервер; если меняете хеш пароля вручную, также очистите `sessions` этого пользователя.
666
+
667
+ При `storage: 'memory'` переданный массив пользователей не изменяется. Каждый запуск заново загружает исходных пользователей и сессии. Сессии, созданные во время работы, после перезапуска исчезают; заданные в исходных данных неистёкшие сессии восстанавливаются, включая завершённые через logout во время предыдущего запуска. Клиенту по-прежнему нужен соответствующий исходный токен: восстановить его из `tokenHash` нельзя.
668
+
669
+ `auth.expiresIn` задаёт срок новых сессий и по умолчанию равен 3600 секундам. У существующих сессий сохраняется записанное время истечения, в том числе после перезапуска. Истёкшие сессии игнорируются при загрузке, отклоняются при каждом запросе и удаляются у всех пользователей при следующем успешном изменении auth, например входе или выходе. Срок сессии автоматически не продлевается.
658
670
 
659
671
  | REST-запрос | JSON тела | Ответ |
660
672
  |---|---|---|
@@ -665,13 +677,13 @@ export default {
665
677
  | `PATCH /auth/users/:id/password` | `{ "currentPassword": "…", "newPassword": "…" }` | `{ success: true }` |
666
678
  | `PATCH /auth/users/:id/admin` | `{ "isAdmin": true }` | `{ id, username, isAdmin }` |
667
679
 
668
- Регистрация и вход доступны без токена. Для остальных методов передавайте `Authorization: Bearer <accessToken>`. Регистрация создаёт обычного пользователя с новым `id`; поля `id`, `passwordHash` и `isAdmin` в запросе запрещены. `username` уникален с учётом регистра, повтор возвращает `409`. Имя должно содержать хотя бы один непробельный символ и не более 256 символов, пароль — от 1 до 1024 символов. Значения не обрезаются. Регистрация не открывает сессию: после неё выполните вход.
680
+ Регистрация и вход доступны без токена. Для остальных методов передавайте `Authorization: Bearer <accessToken>`. Регистрация создаёт обычного пользователя с новым `id`; поля `id`, `passwordHash`, `isAdmin` и `sessions` в запросе запрещены. `username` уникален с учётом регистра, повтор возвращает `409`. Имя должно содержать хотя бы один непробельный символ и не более 256 символов, пароль — от 1 до 1024 символов. Значения не обрезаются. Регистрация не открывает сессию: после неё выполните вход.
669
681
 
670
682
  Пользователь меняет только свой пароль, передавая `currentPassword` и `newPassword`. Для администратора при смене своего пароля действует то же правило. Администратор может сменить пароль обычному пользователю, передав только `newPassword`. Изменение пароля другого администратора возвращает `403`. После успешной смены завершаются все сессии этого пользователя, включая текущую при смене своего пароля; требуется новый вход. Неверный текущий пароль возвращает `401` без изменения сессий.
671
683
 
672
684
  Статус `isAdmin` меняет только администратор. Можно назначать других администраторов и снимать с них статус. Снять статус с себя можно, только если остаётся другой администратор; иначе возвращается `409`. Проверка учитывает параллельные запросы. Новые права действуют с прежними токенами сразу после сохранения, в том числе для мутаций GraphQL. Администратор может снять статус с другого администратора, а затем сменить ему пароль как обычному пользователю.
673
685
 
674
- Недействительный или истёкший токен возвращает `401`, недостаток прав — `403`, отсутствующий пользователь при разрешённой операции — `404`. Ошибки тела запроса возвращают `400`. Вход, регистрация и смена пароля могут вернуть `429` при превышении числа одновременных вычислений паролей; вход также ограничивает число активных сессий. Сессии хранятся в памяти и исчезают после перезапуска. Выход завершает только сессию переданного токена.
686
+ Недействительный или истёкший токен возвращает `401`, недостаток прав — `403`, отсутствующий пользователь при разрешённой операции — `404`. Ошибки тела запроса возвращают `400`. Вход, регистрация и смена пароля могут вернуть `429` при превышении числа одновременных вычислений паролей; вход также возвращает `429`, если уже есть 10 000 активных сессий среди всех пользователей, включая загруженные из источника. Выход завершает только сессию переданного токена.
675
687
 
676
688
  При включённом auth у каждой записи модели доступен виртуальный объект `actions`. Он вычисляется для текущего пользователя и не сохраняется в базе. В REST его нужно явно запросить через `scope`:
677
689
 
@@ -842,7 +854,7 @@ Content-Type: application/json
842
854
 
843
855
  При хранении на диске бинарный файл находится по пути `<files.source>/<directory>/<name>`. Метаданные по умолчанию хранятся в `<files.source>/.files.json`; другой путь можно задать через `files.metadata`. Директории и файл метаданных создаются по мере необходимости.
844
856
 
845
- Используйте один процесс сервера для дисковой базы и файлового хранилища. Второй сервер с тем же файлом базы не запустится; закрытие первого освобождает блокировку, а после аварийного завершения она снимается при следующем запуске. Перед ручным изменением файлов или метаданных остановите сервер. Пути внутри каталога файлового хранилища не могут содержать символические ссылки. Загрузка и переименование не могут перезаписать базу, её блокировку, счётчики, схему, учётные записи auth, загруженную конфигурацию или файл метаданных.
857
+ Используйте один процесс сервера для каждого записываемого дискового источника. Второй сервер с общей базой, файлом пользователей auth, каталогом файлового хранилища или метаданными не запустится. Блокировки — отдельные JSON-файлы рядом с источниками: для `database.json` создаётся `database-lock.json`; последнее расширение имени из конфигурации убирается. Файл содержит тип блокировки, PID процесса и токен владельца. При штатном закрытии файлы блокировок удаляются; после аварии ОС освобождает блокировки, а следующий сервер повторно использует оставшиеся файлы. Для дисковых блокировок используется нативная зависимость `fs-native-extensions`; хранение в памяти и генерация схем её не загружают. Для источника с символической ссылкой блокируются исходная запись и конечная цель. Перед ручным изменением файлов или метаданных остановите сервер. Пути внутри каталога файлового хранилища не могут содержать символические ссылки. Загрузка и переименование не могут перезаписать базу, её блокировку, счётчики, схему, учётные записи auth, загруженную конфигурацию или файл метаданных.
846
858
 
847
859
  Отправляйте файл как бинарное тело запроса. В браузере для этого можно использовать `xhr.send(file)`, а прогресс отслеживать через `XMLHttpRequest.upload.onprogress`. Максимальный размер по умолчанию равен 100 МиБ и настраивается через `server.maxFileSize`. Отсутствующие или небезопасные заголовки и пути возвращают `400`, превышение лимита — `413`, а отсутствующий, некорректный или не поддерживаемый Fastify `Content-Type` — `400` либо `415` в зависимости от этапа проверки.
848
860
 
@@ -895,6 +907,6 @@ npm run verify
895
907
 
896
908
  `npm run verify` проверяет типы, стиль кода, покрытие тестами и установку пакета из архива.
897
909
 
898
- Для публикации предварительной версии обновите номер в `package.json`, `package-lock.json` и `src/core/constants.ts`, затем отправьте коммит в `main`. GitHub Actions создаст тег `v<version>` и опубликует пакет через trusted publishing в канал `alpha`, `beta` или `rc`. Стабильные версии публикуются в `latest` из явно отправленного тега версии.
910
+ Для публикации предварительной версии обновите номер в `package.json` и `package-lock.json` (сервер и CLI читают его из метаданных пакета), затем отправьте коммит в `main`. GitHub Actions создаст тег `v<version>` и опубликует пакет через trusted publishing в канал `alpha`, `beta` или `rc`. Стабильные версии публикуются в `latest` из явно отправленного тега версии.
899
911
 
900
912
  Лицензия: MIT.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { AuthConfig, AuthSession, AuthUser, AuthUserRecord } from './src/auth/contract.js';
1
+ export type { AuthConfig, AuthSession, AuthSessionRecord, AuthUser, AuthUserRecord } from './src/auth/contract.js';
2
2
  export { hashPassword } from './src/auth/public.js';
3
3
  export type { EntityDefinition, Field, ModelSchema } from './src/core/model.js';
4
4
  export type { FileUpdate } from './src/files/contract.js';
@@ -0,0 +1,104 @@
1
+ {
2
+ "name": "@kollors/deep-json-server",
3
+ "version": "1.0.0-rc.8",
4
+ "description": "JSON mock server with schema-driven REST, GraphQL, OpenAPI generation and file uploads",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "deep-json-server": "dist/bin/deep-json-server.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ },
16
+ "./server": {
17
+ "types": "./dist/src/server/public.d.ts",
18
+ "import": "./dist/src/server/public.js"
19
+ },
20
+ "./openapi": {
21
+ "types": "./dist/src/openapi/entry.d.ts",
22
+ "import": "./dist/src/openapi/entry.js"
23
+ },
24
+ "./graphql": {
25
+ "types": "./dist/src/graphql/entry.d.ts",
26
+ "import": "./dist/src/graphql/entry.js"
27
+ },
28
+ "./auth": {
29
+ "types": "./dist/src/auth/public.d.ts",
30
+ "import": "./dist/src/auth/public.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "LICENSE",
36
+ "README.md",
37
+ "README.ru.md",
38
+ "CHANGELOG.md",
39
+ "MIGRATION.md",
40
+ "MIGRATION.ru.md",
41
+ "examples"
42
+ ],
43
+ "scripts": {
44
+ "build": "node scripts/clean-dist.js && tsc && node scripts/prepare-dist.js",
45
+ "lint": "biome check . --error-on-warnings",
46
+ "lint:fix": "biome check --write .",
47
+ "pack:check": "node scripts/check-package.js",
48
+ "prepack": "npm run build",
49
+ "prepublishOnly": "npm run verify",
50
+ "test": "npm run build && node --test",
51
+ "test:coverage": "npm run build && node scripts/run-coverage.js",
52
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.type-tests.json",
53
+ "verify": "npm run typecheck && npm run lint && npm run test:coverage && npm run pack:check"
54
+ },
55
+ "engines": {
56
+ "node": ">=22"
57
+ },
58
+ "dependencies": {
59
+ "ajv": "^8.20.0",
60
+ "ajv-formats": "^3.0.1",
61
+ "fastify": "^5.12.3",
62
+ "fs-native-extensions": "^1.5.1",
63
+ "graphql": "^16.14.2",
64
+ "lowdb": "^7.0.1",
65
+ "mercurius": "^16.10.0",
66
+ "pluralize": "^8.0.0",
67
+ "yaml": "^2.8.1"
68
+ },
69
+ "repository": {
70
+ "type": "git",
71
+ "url": "git+https://github.com/kollors/deep-json-server.git"
72
+ },
73
+ "homepage": "https://github.com/kollors/deep-json-server",
74
+ "bugs": {
75
+ "url": "https://github.com/kollors/deep-json-server/issues"
76
+ },
77
+ "keywords": [
78
+ "api",
79
+ "crud",
80
+ "deep-filter",
81
+ "file-upload",
82
+ "json",
83
+ "mock",
84
+ "mock-api",
85
+ "mock-server",
86
+ "openapi",
87
+ "relations",
88
+ "rest",
89
+ "rest-api",
90
+ "graphql",
91
+ "openapi-generator"
92
+ ],
93
+ "author": "kollors",
94
+ "license": "MIT",
95
+ "publishConfig": {
96
+ "access": "public"
97
+ },
98
+ "devDependencies": {
99
+ "@biomejs/biome": "^2.5.12",
100
+ "@types/node": "^22.0.0",
101
+ "@types/pluralize": "^0.0.33",
102
+ "typescript": "^7.0.2"
103
+ }
104
+ }
@@ -4,9 +4,17 @@ export interface AuthUser {
4
4
  username: string;
5
5
  isAdmin: boolean;
6
6
  }
7
+ export interface AuthSessionRecord {
8
+ /** SHA-256 of the access token, encoded as 64 lowercase hexadecimal characters. */
9
+ tokenHash: string;
10
+ /** Absolute expiration time in milliseconds since the Unix epoch. */
11
+ expiresAt: number;
12
+ }
7
13
  export interface AuthUserRecord extends Omit<AuthUser, 'isAdmin'> {
8
14
  isAdmin?: boolean;
9
15
  passwordHash: string;
16
+ /** Initial or persisted sessions. Defaults to an empty array. */
17
+ sessions?: AuthSessionRecord[];
10
18
  }
11
19
  export interface AuthConfig<S extends Storage = Storage> {
12
20
  source: Source<S, AuthUserRecord[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../src/auth/contract.ts"],"names":[],"mappings":"AAoBA,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,KAAK,EAAE,aAAa;IACpB,QAAQ,EAAE,gBAAgB;IAC1B,EAAE,EAAE,UAAU;IACd,MAAM,EAAE,cAAc;IACtB,QAAQ,EAAE,0BAA0B;IACpC,KAAK,EAAE,uBAAuB;CACtB,CAAC;AACX,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAC5C,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AACvC,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC"}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../src/auth/contract.ts"],"names":[],"mappings":"AA4BA,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,KAAK,EAAE,aAAa;IACpB,QAAQ,EAAE,gBAAgB;IAC1B,EAAE,EAAE,UAAU;IACd,MAAM,EAAE,cAAc;IACtB,QAAQ,EAAE,0BAA0B;IACpC,KAAK,EAAE,uBAAuB;CACtB,CAAC;AACX,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAC5C,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AACvC,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC"}
@@ -1,3 +1,3 @@
1
- export type { AuthConfig, AuthSession, AuthUser, AuthUserRecord } from './contract.js';
1
+ export type { AuthConfig, AuthSession, AuthSessionRecord, AuthUser, AuthUserRecord } from './contract.js';
2
2
  /** Creates a salted password hash for an auth user fixture. */
3
3
  export declare function hashPassword(password: string): Promise<string>;
@@ -3,7 +3,6 @@ import { type AuthStore } from './store.js';
3
3
  export declare class AuthService {
4
4
  private store;
5
5
  private expiresIn;
6
- private sessions;
7
6
  private pendingPasswords;
8
7
  private closed;
9
8
  constructor(store: AuthStore, expiresIn: number);
@@ -29,13 +28,13 @@ export declare class AuthService {
29
28
  * @example После снятия статуса прежний токен возвращает isAdmin: false без повторного входа.
30
29
  */
31
30
  me(authorization: unknown): AuthUser;
32
- /** Проверяет токен и удаляет соответствующую сессию.
31
+ /** Проверяет токен и сохраняет удаление соответствующей сессии.
33
32
  * @example Действительный токен → { success: true }; повторный выход с ним → ошибка.
34
33
  */
35
- logout(authorization: unknown): {
34
+ logout(authorization: unknown): Promise<{
36
35
  success: boolean;
37
- };
38
- /** Запрещает новые обращения и очищает сессии в памяти.
36
+ }>;
37
+ /** Запрещает новые обращения, сохраняя записанные сессии для следующего запуска.
39
38
  * @example close() → undefined; последующий login() → отказ.
40
39
  */
41
40
  close(): void;
@@ -47,8 +46,8 @@ export declare class AuthService {
47
46
  * @example closed = true → UNAUTHENTICATED; false → undefined.
48
47
  */
49
48
  private assertOpen;
50
- /** Находит сессию по хешу токена и проверяет срок; просроченную сессию удаляет.
51
- * @example Действительный Bearer-токен → [ключ, сессия]; отсутствующий или истёкший → ошибка.
49
+ /** Находит сессию по хешу токена и проверяет срок, не изменяя сохранённое состояние.
50
+ * @example Действительный Bearer-токен → [ключ, пользователь]; отсутствующий или истёкший → ошибка.
52
51
  */
53
52
  private session;
54
53
  /** Получает текущую запись по идентификатору из проверенной сессии.
@@ -63,16 +62,8 @@ export declare class AuthService {
63
62
  * @example Администратор и обычная чужая запись → { actor, target }; два разных администратора → FORBIDDEN.
64
63
  */
65
64
  private passwordTarget;
66
- /** Завершает все сессии одного пользователя после сохранения нового пароля.
67
- * @example Две сессии id = '1' и одна id = '2' → остаётся сессия id = '2'.
68
- */
69
- private revoke;
70
- /** Удаляет из памяти сессии, срок которых истёк к текущему моменту.
71
- * @example Одна истёкшая и одна действующая сессия → остаётся одна действующая; результат undefined.
72
- */
73
- private prune;
74
65
  }
75
- /** Открывает источник учётных записей и создаёт хранилище сессий в памяти.
66
+ /** Открывает источник учётных записей вместе с сохранёнными или исходными сессиями.
76
67
  * @example createAuthService({ source: [] }) → Promise<AuthService>; повторяющиеся id → ошибка.
77
68
  */
78
69
  export declare function createAuthService(config: AuthConfig): Promise<AuthService>;