@kollors/deep-json-server 1.0.0-alpha.6 → 1.0.0-alpha.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 (237) hide show
  1. package/README.md +373 -230
  2. package/README.ru.md +370 -227
  3. package/dist/bin/deep-json-server.js +1 -1
  4. package/dist/bin/deep-json-server.js.map +1 -1
  5. package/dist/index.d.ts +10 -8
  6. package/dist/index.js +3 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/src/auth/contract.d.ts +6 -2
  9. package/dist/src/auth/contract.js +8 -1
  10. package/dist/src/auth/contract.js.map +1 -1
  11. package/dist/src/auth/input.d.ts +18 -0
  12. package/dist/src/auth/input.js +34 -0
  13. package/dist/src/auth/input.js.map +1 -0
  14. package/dist/src/auth/password.d.ts +13 -0
  15. package/dist/src/auth/password.js +17 -1
  16. package/dist/src/auth/password.js.map +1 -1
  17. package/dist/src/auth/routes.d.ts +3 -0
  18. package/dist/src/auth/routes.js +6 -0
  19. package/dist/src/auth/routes.js.map +1 -1
  20. package/dist/src/auth/service.d.ts +64 -4
  21. package/dist/src/auth/service.js +167 -61
  22. package/dist/src/auth/service.js.map +1 -1
  23. package/dist/src/auth/store.d.ts +26 -0
  24. package/dist/src/auth/store.js +82 -0
  25. package/dist/src/auth/store.js.map +1 -0
  26. package/dist/src/cli/index.d.ts +7 -0
  27. package/dist/src/{cli.js → cli/index.js} +31 -15
  28. package/dist/src/cli/index.js.map +1 -0
  29. package/dist/src/core/config-values.d.ts +28 -0
  30. package/dist/src/core/config-values.js +53 -0
  31. package/dist/src/core/config-values.js.map +1 -0
  32. package/dist/src/{constants.d.ts → core/constants.d.ts} +1 -1
  33. package/dist/src/{constants.js → core/constants.js} +1 -1
  34. package/dist/src/core/constants.js.map +1 -0
  35. package/dist/src/core/database.d.ts +53 -0
  36. package/dist/src/{database.js → core/database.js} +23 -3
  37. package/dist/src/core/database.js.map +1 -0
  38. package/dist/src/core/engine.d.ts +65 -0
  39. package/dist/src/{engine.js → core/engine.js} +114 -59
  40. package/dist/src/core/engine.js.map +1 -0
  41. package/dist/src/{errors.d.ts → core/errors.d.ts} +4 -1
  42. package/dist/src/core/errors.js +12 -0
  43. package/dist/src/core/errors.js.map +1 -0
  44. package/dist/src/core/http-errors.d.ts +9 -0
  45. package/dist/src/core/http-errors.js +15 -0
  46. package/dist/src/core/http-errors.js.map +1 -0
  47. package/dist/src/core/lifecycle/mutation.d.ts +49 -0
  48. package/dist/src/core/lifecycle/mutation.js +170 -0
  49. package/dist/src/core/lifecycle/mutation.js.map +1 -0
  50. package/dist/src/core/lifecycle/options.d.ts +21 -0
  51. package/dist/src/core/lifecycle/options.js +19 -0
  52. package/dist/src/core/lifecycle/options.js.map +1 -0
  53. package/dist/src/core/model.d.ts +131 -0
  54. package/dist/src/{model.js → core/model.js} +106 -10
  55. package/dist/src/core/model.js.map +1 -0
  56. package/dist/src/{mutations → core/mutations}/write.d.ts +4 -4
  57. package/dist/src/{mutations → core/mutations}/write.js +14 -4
  58. package/dist/src/core/mutations/write.js.map +1 -0
  59. package/dist/src/core/operations.d.ts +33 -0
  60. package/dist/src/core/operations.js +10 -0
  61. package/dist/src/core/operations.js.map +1 -0
  62. package/dist/src/core/pagination.d.ts +10 -0
  63. package/dist/src/core/pagination.js +13 -0
  64. package/dist/src/core/pagination.js.map +1 -0
  65. package/dist/src/core/paths.d.ts +16 -0
  66. package/dist/src/{paths.js → core/paths.js} +16 -0
  67. package/dist/src/core/paths.js.map +1 -0
  68. package/dist/src/core/query/contract.d.ts +6 -0
  69. package/dist/src/{query → core/query}/contract.js +3 -0
  70. package/dist/src/core/query/contract.js.map +1 -0
  71. package/dist/src/core/query/filter.d.ts +6 -0
  72. package/dist/src/{query → core/query}/filter.js +18 -4
  73. package/dist/src/core/query/filter.js.map +1 -0
  74. package/dist/src/core/query/options.d.ts +30 -0
  75. package/dist/src/{query → core/query}/options.js +12 -0
  76. package/dist/src/core/query/options.js.map +1 -0
  77. package/dist/src/core/records.d.ts +46 -0
  78. package/dist/src/{records.js → core/records.js} +24 -3
  79. package/dist/src/core/records.js.map +1 -0
  80. package/dist/src/core/relation-metadata.d.ts +15 -0
  81. package/dist/src/{relation-metadata.js → core/relation-metadata.js} +7 -1
  82. package/dist/src/core/relation-metadata.js.map +1 -0
  83. package/dist/src/core/types.d.ts +10 -0
  84. package/dist/src/{types.js.map → core/types.js.map} +1 -1
  85. package/dist/src/core/utils.d.ts +56 -0
  86. package/dist/src/core/utils.js +102 -0
  87. package/dist/src/core/utils.js.map +1 -0
  88. package/dist/src/files/contract.d.ts +33 -0
  89. package/dist/src/files/contract.js +25 -3
  90. package/dist/src/files/contract.js.map +1 -1
  91. package/dist/src/files/disk-store.d.ts +3 -0
  92. package/dist/src/files/disk-store.js +25 -18
  93. package/dist/src/files/disk-store.js.map +1 -1
  94. package/dist/src/files/http.d.ts +8 -3
  95. package/dist/src/files/http.js +9 -0
  96. package/dist/src/files/http.js.map +1 -1
  97. package/dist/src/files/index.d.ts +4 -3
  98. package/dist/src/files/index.js +3 -1
  99. package/dist/src/files/index.js.map +1 -1
  100. package/dist/src/files/memory-store.d.ts +4 -1
  101. package/dist/src/files/memory-store.js +21 -15
  102. package/dist/src/files/memory-store.js.map +1 -1
  103. package/dist/src/files/routes.d.ts +3 -0
  104. package/dist/src/files/routes.js +25 -1
  105. package/dist/src/files/routes.js.map +1 -1
  106. package/dist/src/files/streams.d.ts +5 -0
  107. package/dist/src/files/streams.js +20 -0
  108. package/dist/src/files/streams.js.map +1 -0
  109. package/dist/src/graphql/entry.d.ts +12 -3
  110. package/dist/src/graphql/entry.js +12 -2
  111. package/dist/src/graphql/entry.js.map +1 -1
  112. package/dist/src/graphql/generate.d.ts +11 -0
  113. package/dist/src/graphql/generate.js +17 -0
  114. package/dist/src/graphql/generate.js.map +1 -0
  115. package/dist/src/graphql/preflight.d.ts +4 -1
  116. package/dist/src/graphql/preflight.js +3 -0
  117. package/dist/src/graphql/preflight.js.map +1 -1
  118. package/dist/src/graphql/resolvers.d.ts +10 -2
  119. package/dist/src/graphql/resolvers.js +8 -2
  120. package/dist/src/graphql/resolvers.js.map +1 -1
  121. package/dist/src/graphql/routes.d.ts +6 -2
  122. package/dist/src/graphql/routes.js +6 -3
  123. package/dist/src/graphql/routes.js.map +1 -1
  124. package/dist/src/graphql/schema.d.ts +6 -0
  125. package/dist/src/{graphql.js → graphql/schema.js} +20 -14
  126. package/dist/src/graphql/schema.js.map +1 -0
  127. package/dist/src/graphql/write.d.ts +3 -0
  128. package/dist/src/graphql/write.js +3 -0
  129. package/dist/src/graphql/write.js.map +1 -1
  130. package/dist/src/openapi/auth.d.ts +13 -0
  131. package/dist/src/openapi/auth.js +139 -0
  132. package/dist/src/openapi/auth.js.map +1 -0
  133. package/dist/src/openapi/document.d.ts +5 -2
  134. package/dist/src/openapi/document.js +41 -42
  135. package/dist/src/openapi/document.js.map +1 -1
  136. package/dist/src/openapi/entry.d.ts +14 -3
  137. package/dist/src/openapi/entry.js +12 -1
  138. package/dist/src/openapi/entry.js.map +1 -1
  139. package/dist/src/openapi/files.d.ts +6 -0
  140. package/dist/src/{files/openapi.js → openapi/files.js} +6 -3
  141. package/dist/src/openapi/files.js.map +1 -0
  142. package/dist/src/openapi/generate.d.ts +19 -0
  143. package/dist/src/openapi/generate.js +39 -0
  144. package/dist/src/openapi/generate.js.map +1 -0
  145. package/dist/src/openapi/helpers.d.ts +10 -1
  146. package/dist/src/openapi/helpers.js +9 -0
  147. package/dist/src/openapi/helpers.js.map +1 -1
  148. package/dist/src/openapi/options.d.ts +20 -0
  149. package/dist/src/openapi/options.js +13 -0
  150. package/dist/src/openapi/options.js.map +1 -0
  151. package/dist/src/{types.d.ts → openapi/types.d.ts} +1 -10
  152. package/dist/src/openapi/types.js +2 -0
  153. package/dist/src/openapi/types.js.map +1 -0
  154. package/dist/src/openapi/write.d.ts +5 -0
  155. package/dist/src/openapi/write.js +12 -0
  156. package/dist/src/openapi/write.js.map +1 -0
  157. package/dist/src/rest/options.d.ts +11 -2
  158. package/dist/src/rest/options.js +12 -3
  159. package/dist/src/rest/options.js.map +1 -1
  160. package/dist/src/rest/projection.d.ts +10 -4
  161. package/dist/src/rest/projection.js +10 -4
  162. package/dist/src/rest/projection.js.map +1 -1
  163. package/dist/src/rest/routes.d.ts +6 -2
  164. package/dist/src/rest/routes.js +12 -11
  165. package/dist/src/rest/routes.js.map +1 -1
  166. package/dist/src/server/config.d.ts +76 -0
  167. package/dist/src/{config.js → server/config.js} +71 -74
  168. package/dist/src/server/config.js.map +1 -0
  169. package/dist/src/server/create.d.ts +18 -0
  170. package/dist/src/{server.js → server/create.js} +33 -28
  171. package/dist/src/server/create.js.map +1 -0
  172. package/dist/src/server/features.d.ts +15 -0
  173. package/dist/src/{features.js → server/features.js} +8 -2
  174. package/dist/src/server/features.js.map +1 -0
  175. package/dist/src/server/public.d.ts +3 -3
  176. package/dist/src/server/public.js +1 -1
  177. package/dist/src/server/public.js.map +1 -1
  178. package/package.json +1 -1
  179. package/dist/src/auth/openapi.d.ts +0 -10
  180. package/dist/src/auth/openapi.js +0 -62
  181. package/dist/src/auth/openapi.js.map +0 -1
  182. package/dist/src/cli.d.ts +0 -4
  183. package/dist/src/cli.js.map +0 -1
  184. package/dist/src/config.d.ts +0 -86
  185. package/dist/src/config.js.map +0 -1
  186. package/dist/src/constants.js.map +0 -1
  187. package/dist/src/database.d.ts +0 -20
  188. package/dist/src/database.js.map +0 -1
  189. package/dist/src/engine.d.ts +0 -36
  190. package/dist/src/engine.js.map +0 -1
  191. package/dist/src/errors.js +0 -9
  192. package/dist/src/errors.js.map +0 -1
  193. package/dist/src/features.d.ts +0 -9
  194. package/dist/src/features.js.map +0 -1
  195. package/dist/src/files/openapi.d.ts +0 -3
  196. package/dist/src/files/openapi.js.map +0 -1
  197. package/dist/src/graphql/public.d.ts +0 -3
  198. package/dist/src/graphql/public.js +0 -11
  199. package/dist/src/graphql/public.js.map +0 -1
  200. package/dist/src/graphql.d.ts +0 -3
  201. package/dist/src/graphql.js.map +0 -1
  202. package/dist/src/http/errors.d.ts +0 -6
  203. package/dist/src/http/errors.js +0 -3
  204. package/dist/src/http/errors.js.map +0 -1
  205. package/dist/src/model.d.ts +0 -72
  206. package/dist/src/model.js.map +0 -1
  207. package/dist/src/mutations/write.js.map +0 -1
  208. package/dist/src/openapi/index.d.ts +0 -7
  209. package/dist/src/openapi/index.js +0 -28
  210. package/dist/src/openapi/index.js.map +0 -1
  211. package/dist/src/openapi/public.d.ts +0 -18
  212. package/dist/src/openapi/public.js +0 -20
  213. package/dist/src/openapi/public.js.map +0 -1
  214. package/dist/src/pagination.d.ts +0 -7
  215. package/dist/src/pagination.js +0 -12
  216. package/dist/src/pagination.js.map +0 -1
  217. package/dist/src/paths.d.ts +0 -7
  218. package/dist/src/paths.js.map +0 -1
  219. package/dist/src/query/contract.d.ts +0 -3
  220. package/dist/src/query/contract.js.map +0 -1
  221. package/dist/src/query/filter.d.ts +0 -3
  222. package/dist/src/query/filter.js.map +0 -1
  223. package/dist/src/query/options.d.ts +0 -18
  224. package/dist/src/query/options.js.map +0 -1
  225. package/dist/src/records.d.ts +0 -25
  226. package/dist/src/records.js.map +0 -1
  227. package/dist/src/relation-metadata.d.ts +0 -9
  228. package/dist/src/relation-metadata.js.map +0 -1
  229. package/dist/src/schema.d.ts +0 -8
  230. package/dist/src/schema.js +0 -13
  231. package/dist/src/schema.js.map +0 -1
  232. package/dist/src/server.d.ts +0 -12
  233. package/dist/src/server.js.map +0 -1
  234. package/dist/src/utils.d.ts +0 -12
  235. package/dist/src/utils.js +0 -58
  236. package/dist/src/utils.js.map +0 -1
  237. /package/dist/src/{types.js → core/types.js} +0 -0
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import process from 'node:process';
3
- import { runCli } from '../src/cli.js';
3
+ import { runCli } from '../src/cli/index.js';
4
4
  try {
5
5
  await runCli();
6
6
  }
@@ -1 +1 @@
1
- {"version":3,"file":"deep-json-server.js","sourceRoot":"","sources":["../../bin/deep-json-server.ts"],"names":[],"mappings":";AAEA,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,IAAI,CAAC;IACH,MAAM,MAAM,EAAE,CAAC;AACjB,CAAC;AAAC,OAAO,KAAK,EAAE,CAAC;IACf,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACpF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC"}
1
+ {"version":3,"file":"deep-json-server.js","sourceRoot":"","sources":["../../bin/deep-json-server.ts"],"names":[],"mappings":";AAEA,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAE7C,IAAI,CAAC;IACH,MAAM,MAAM,EAAE,CAAC;AACjB,CAAC;AAAC,OAAO,KAAK,EAAE,CAAC;IACf,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACpF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  export type { AuthConfig, AuthSession, AuthUser, AuthUserRecord } from './src/auth/contract.js';
2
2
  export { hashPassword } from './src/auth/public.js';
3
- export type { DatabaseConfig, DeepJsonServerConfig, FilesConfig, GraphqlConfig, MemoryFile, OpenapiConfig, ServerConfig } from './src/config.js';
4
- export type { ServerFeatures } from './src/features.js';
3
+ export type { EntityDefinition, Field, ModelSchema } from './src/core/model.js';
5
4
  export type { FileUpdate } from './src/files/contract.js';
6
5
  export type { FileMetadata } from './src/files/http.js';
7
- export type { EntityDefinition, Field, ModelSchema } from './src/model.js';
8
- export type { OpenapiOptions } from './src/schema.js';
9
- export { generateGraphql, generateOpenapi, writeGraphql, writeOpenapi } from './src/schema.js';
10
- export type { ServerFacade } from './src/server.js';
11
- export { createServer } from './src/server.js';
12
- export type { OpenapiDocument } from './src/types.js';
6
+ export type { GraphqlOptions } from './src/graphql/entry.js';
7
+ export { generateGraphql, writeGraphql } from './src/graphql/entry.js';
8
+ export type { OpenapiOptions } from './src/openapi/entry.js';
9
+ export { generateOpenapi, writeOpenapi } from './src/openapi/entry.js';
10
+ export type { OpenapiDocument } from './src/openapi/types.js';
11
+ export type { DatabaseConfig, DeepJsonServerConfig, FilesConfig, GraphqlConfig, MemoryFile, OpenapiConfig, ServerConfig } from './src/server/config.js';
12
+ export type { ServerFacade } from './src/server/create.js';
13
+ export { createServer } from './src/server/create.js';
14
+ export type { ServerFeatures } from './src/server/features.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { hashPassword } from './src/auth/public.js';
2
- export { generateGraphql, generateOpenapi, writeGraphql, writeOpenapi } from './src/schema.js';
3
- export { createServer } from './src/server.js';
2
+ export { generateGraphql, writeGraphql } from './src/graphql/entry.js';
3
+ export { generateOpenapi, writeOpenapi } from './src/openapi/entry.js';
4
+ export { createServer } from './src/server/create.js';
4
5
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAKpD,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEvE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAIvE,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC"}
@@ -1,12 +1,13 @@
1
1
  export interface AuthUser {
2
2
  id: string;
3
3
  username: string;
4
+ isAdmin: boolean;
4
5
  }
5
- export interface AuthUserRecord extends AuthUser {
6
+ export interface AuthUserRecord extends Omit<AuthUser, 'isAdmin'> {
7
+ isAdmin?: boolean;
6
8
  passwordHash: string;
7
9
  }
8
10
  export interface AuthConfig {
9
- enabled?: boolean;
10
11
  users: string | AuthUserRecord[];
11
12
  /** Session lifetime in seconds. Defaults to one hour. */
12
13
  expiresIn?: number;
@@ -18,8 +19,11 @@ export interface AuthSession {
18
19
  }
19
20
  export declare const AUTH_PATHS: {
20
21
  readonly login: '/auth/login';
22
+ readonly register: '/auth/register';
21
23
  readonly me: '/auth/me';
22
24
  readonly logout: '/auth/logout';
25
+ readonly password: '/auth/users/:id/password';
26
+ readonly admin: '/auth/users/:id/admin';
23
27
  };
24
28
  export declare const DEFAULT_SESSION_SECONDS = 3600;
25
29
  export declare const MAX_USERNAME_LENGTH = 256;
@@ -1,4 +1,11 @@
1
- export const AUTH_PATHS = { login: '/auth/login', me: '/auth/me', logout: '/auth/logout' };
1
+ export const AUTH_PATHS = {
2
+ login: '/auth/login',
3
+ register: '/auth/register',
4
+ me: '/auth/me',
5
+ logout: '/auth/logout',
6
+ password: '/auth/users/:id/password',
7
+ admin: '/auth/users/:id/admin',
8
+ };
2
9
  export const DEFAULT_SESSION_SECONDS = 3600;
3
10
  export const MAX_USERNAME_LENGTH = 256;
4
11
  export const MAX_PASSWORD_LENGTH = 1024;
@@ -1 +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"}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../src/auth/contract.ts"],"names":[],"mappings":"AAmBA,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"}
@@ -0,0 +1,18 @@
1
+ /** Принимает только непустое имя и допустимый пароль без дополнительных полей.
2
+ * @example { username: 'anna', password: 'secret' } → та же пара строк; дополнительный isAdmin → ошибка.
3
+ */
4
+ export declare function credentials(body: unknown): {
5
+ username: string;
6
+ password: string;
7
+ };
8
+ /** Проверяет новый пароль и необязательный текущий пароль; оба значения возвращает без преобразований.
9
+ * @example { newPassword: 'next' } → { newPassword: 'next', currentPassword: undefined }; пустой пароль → ошибка.
10
+ */
11
+ export declare function passwordChange(body: unknown): {
12
+ newPassword: string;
13
+ currentPassword?: string;
14
+ };
15
+ /** Принимает единственный логический флаг без преобразования строк и чисел.
16
+ * @example { isAdmin: false } → false; { isAdmin: 'false' } → ошибка.
17
+ */
18
+ export declare function adminChange(body: unknown): boolean;
@@ -0,0 +1,34 @@
1
+ import { domainError } from '../core/errors.js';
2
+ import { hasOnlyKeys, isObject } from '../core/utils.js';
3
+ import { MAX_USERNAME_LENGTH } from './contract.js';
4
+ import { validPassword } from './password.js';
5
+ /** Принимает только непустое имя и допустимый пароль без дополнительных полей.
6
+ * @example { username: 'anna', password: 'secret' } → та же пара строк; дополнительный isAdmin → ошибка.
7
+ */
8
+ export function credentials(body) {
9
+ if (!isObject(body) ||
10
+ !hasOnlyKeys(body, ['username', 'password']) ||
11
+ typeof body.username !== 'string' ||
12
+ !body.username.trim() ||
13
+ body.username.length > MAX_USERNAME_LENGTH ||
14
+ !validPassword(body.password))
15
+ throw domainError('INVALID_INPUT', 'Supply username and password');
16
+ return { username: body.username, password: body.password };
17
+ }
18
+ /** Проверяет новый пароль и необязательный текущий пароль; оба значения возвращает без преобразований.
19
+ * @example { newPassword: 'next' } → { newPassword: 'next', currentPassword: undefined }; пустой пароль → ошибка.
20
+ */
21
+ export function passwordChange(body) {
22
+ if (!isObject(body) || !hasOnlyKeys(body, ['currentPassword', 'newPassword']) || !validPassword(body.newPassword) || (body.currentPassword !== undefined && !validPassword(body.currentPassword)))
23
+ throw domainError('INVALID_INPUT', 'Supply newPassword and an optional currentPassword');
24
+ return { newPassword: body.newPassword, currentPassword: body.currentPassword };
25
+ }
26
+ /** Принимает единственный логический флаг без преобразования строк и чисел.
27
+ * @example { isAdmin: false } → false; { isAdmin: 'false' } → ошибка.
28
+ */
29
+ export function adminChange(body) {
30
+ if (!isObject(body) || !hasOnlyKeys(body, ['isAdmin']) || typeof body.isAdmin !== 'boolean')
31
+ throw domainError('INVALID_INPUT', 'Supply a boolean isAdmin');
32
+ return body.isAdmin;
33
+ }
34
+ //# sourceMappingURL=input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.js","sourceRoot":"","sources":["../../../src/auth/input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,IAAa;IACvC,IACE,CAAC,QAAQ,CAAC,IAAI,CAAC;QACf,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAC5C,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ;QACjC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE;QACrB,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,mBAAmB;QAC1C,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC;QAE7B,MAAM,WAAW,CAAC,eAAe,EAAE,8BAA8B,CAAC,CAAC;IACrE,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;AAC9D,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,IAAa;IAC1C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,iBAAiB,EAAE,aAAa,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,KAAK,SAAS,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAC/L,MAAM,WAAW,CAAC,eAAe,EAAE,oDAAoD,CAAC,CAAC;IAC3F,OAAO,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,eAAe,EAAE,IAAI,CAAC,eAAqC,EAAE,CAAC;AACxG,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,IAAa;IACvC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,SAAS,CAAC,CAAC,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,SAAS;QAAE,MAAM,WAAW,CAAC,eAAe,EAAE,0BAA0B,CAAC,CAAC;IAC5J,OAAO,IAAI,CAAC,OAAO,CAAC;AACtB,CAAC"}
@@ -1,4 +1,17 @@
1
+ /** Проверяет формат строки с параметрами scrypt, солью и хешем; пароль не проверяет.
2
+ * @example validPasswordHash('plain text') → false.
3
+ */
1
4
  export declare const validPasswordHash: (value: unknown) => value is string;
5
+ /** Проверяет строку пароля по длине без обрезки пробелов.
6
+ * @example validPassword(' secret ') → true; validPassword('') → false.
7
+ */
8
+ export declare const validPassword: (value: unknown) => value is string;
9
+ /** Создаёт хеш пароля со случайной солью и сохраняет параметры вычисления в строке.
10
+ * @example hashPassword('secret') → Promise<string> вида 'scrypt$16384$8$5$<соль>$<хеш>'; результат случаен.
11
+ */
2
12
  export declare function hashPassword(password: string): Promise<string>;
13
+ /** Сравнивает пароль с сохранённым хешем за постоянное время после вычисления scrypt.
14
+ * @example verifyPassword('secret', await hashPassword('secret')) → true; другой пароль → false.
15
+ */
3
16
  export declare function verifyPassword(password: string, encoded: string): Promise<boolean>;
4
17
  export declare const DUMMY_HASH: string;
@@ -3,17 +3,33 @@ import { MAX_PASSWORD_LENGTH } from './contract.js';
3
3
  // OWASP scrypt profile: N=2^14, r=8, p=5 (16 MiB memory).
4
4
  const PREFIX = 'scrypt$16384$8$5';
5
5
  const HASH_PATTERN = /^scrypt\$16384\$8\$5\$([0-9a-f]{32})\$([0-9a-f]{64})$/;
6
+ /** Проверяет формат строки с параметрами scrypt, солью и хешем; пароль не проверяет.
7
+ * @example validPasswordHash('plain text') → false.
8
+ */
6
9
  export const validPasswordHash = (value) => typeof value === 'string' && HASH_PATTERN.test(value);
10
+ /** Проверяет строку пароля по длине без обрезки пробелов.
11
+ * @example validPassword(' secret ') → true; validPassword('') → false.
12
+ */
13
+ export const validPassword = (value) => typeof value === 'string' && value.length > 0 && value.length <= MAX_PASSWORD_LENGTH;
14
+ /** Вычисляет 32 байта scrypt для пароля и заданной соли.
15
+ * @example derive('secret', Buffer.alloc(16)) → Promise<Buffer> длиной 32 байта; одинаковые входы дают одинаковый результат.
16
+ */
7
17
  const derive = (password, salt) => new Promise((resolve, reject) => {
8
18
  scrypt(password, salt, 32, { N: 16384, r: 8, p: 5 }, (error, key) => (error ? reject(error) : resolve(key)));
9
19
  });
20
+ /** Создаёт хеш пароля со случайной солью и сохраняет параметры вычисления в строке.
21
+ * @example hashPassword('secret') → Promise<string> вида 'scrypt$16384$8$5$<соль>$<хеш>'; результат случаен.
22
+ */
10
23
  export async function hashPassword(password) {
11
- if (typeof password !== 'string' || !password.length || password.length > MAX_PASSWORD_LENGTH)
24
+ if (!validPassword(password))
12
25
  throw new Error(`Password must contain 1..${MAX_PASSWORD_LENGTH} characters`);
13
26
  const salt = randomBytes(16);
14
27
  const hash = await derive(password, salt);
15
28
  return `${PREFIX}$${salt.toString('hex')}$${hash.toString('hex')}`;
16
29
  }
30
+ /** Сравнивает пароль с сохранённым хешем за постоянное время после вычисления scrypt.
31
+ * @example verifyPassword('secret', await hashPassword('secret')) → true; другой пароль → false.
32
+ */
17
33
  export async function verifyPassword(password, encoded) {
18
34
  const parts = HASH_PATTERN.exec(encoded);
19
35
  if (!parts)
@@ -1 +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"}
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;;GAEG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,KAAc,EAAmB,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC5H;;GAEG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAc,EAAmB,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,IAAI,mBAAmB,CAAC;AACvJ;;GAEG;AACH,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;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,mBAAmB,aAAa,CAAC,CAAC;IAC5G,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;;GAEG;AACH,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"}
@@ -1,3 +1,6 @@
1
1
  import type { FastifyInstance } from 'fastify';
2
2
  import type { AuthService } from './service.js';
3
+ /** Регистрирует управление учётными записями и сессиями, запрещая кеширование ответов.
4
+ * @example После регистрации ответы содержат Cache-Control: no-store; функция возвращает undefined.
5
+ */
3
6
  export declare function registerAuthRoutes(server: FastifyInstance, auth: AuthService): void;
@@ -1,12 +1,18 @@
1
1
  import { AUTH_PATHS } from './contract.js';
2
+ /** Регистрирует управление учётными записями и сессиями, запрещая кеширование ответов.
3
+ * @example После регистрации ответы содержат Cache-Control: no-store; функция возвращает undefined.
4
+ */
2
5
  export function registerAuthRoutes(server, auth) {
3
6
  server.register(async (app) => {
4
7
  app.addHook('onRequest', async (_request, reply) => {
5
8
  reply.header('Cache-Control', 'no-store');
6
9
  });
7
10
  app.post(AUTH_PATHS.login, async (request) => auth.login(request.body));
11
+ app.post(AUTH_PATHS.register, async (request, reply) => reply.code(201).send(await auth.register(request.body)));
8
12
  app.get(AUTH_PATHS.me, async (request) => auth.me(request.headers.authorization));
9
13
  app.post(AUTH_PATHS.logout, async (request) => auth.logout(request.headers.authorization));
14
+ app.patch(AUTH_PATHS.password, async (request) => auth.changePassword(request.headers.authorization, request.params.id, request.body));
15
+ app.patch(AUTH_PATHS.admin, async (request) => auth.changeAdmin(request.headers.authorization, request.params.id, request.body));
10
16
  });
11
17
  }
12
18
  //# sourceMappingURL=routes.js.map
@@ -1 +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"}
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../../src/auth/routes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAG3C;;GAEG;AACH,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,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACjH,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;QAC3F,GAAG,CAAC,KAAK,CAA6B,UAAU,CAAC,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACnK,GAAG,CAAC,KAAK,CAA6B,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/J,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -1,18 +1,78 @@
1
- import { type AuthConfig, type AuthSession, type AuthUser, type AuthUserRecord } from './contract.js';
1
+ import { type AuthConfig, type AuthSession, type AuthUser } from './contract.js';
2
+ import { type AuthStore } from './store.js';
2
3
  export declare class AuthService {
3
- private users;
4
+ private store;
4
5
  private expiresIn;
5
6
  private sessions;
6
- private pendingLogins;
7
+ private pendingPasswords;
7
8
  private closed;
8
- constructor(users: Map<string, AuthUserRecord>, expiresIn: number);
9
+ constructor(store: AuthStore, expiresIn: number);
10
+ /** Проверяет пароль и создаёт сессию с актуальными правами пользователя.
11
+ * @example Корректные username и password → { accessToken, expiresIn, user }; неверный пароль → исключение.
12
+ */
9
13
  login(body: unknown): Promise<AuthSession>;
14
+ /** Создаёт обычного пользователя с новым идентификатором и хешем пароля, не открывая сессию.
15
+ * @example { username: 'anna', password: 'secret' } → { id: '…', username: 'anna', isAdmin: false }.
16
+ */
17
+ register(body: unknown): Promise<AuthUser>;
18
+ /** Меняет свой пароль после проверки текущего либо пароль обычного пользователя по правам администратора.
19
+ * @example Свой id и два пароля → { success: true } с завершением всех своих сессий; чужой администратор → FORBIDDEN.
20
+ */
21
+ changePassword(authorization: unknown, id: string, body: unknown): Promise<{
22
+ success: boolean;
23
+ }>;
24
+ /** Меняет статус по правам действующего администратора и сохраняет хотя бы одного при отказе от собственных прав.
25
+ * @example Последний администратор и { isAdmin: false } для себя → CONFLICT; назначение другого → обновлённый пользователь.
26
+ */
27
+ changeAdmin(authorization: unknown, id: string, body: unknown): Promise<AuthUser>;
28
+ /** Проверяет токен и возвращает копию пользователя с текущими правами.
29
+ * @example После снятия статуса прежний токен возвращает isAdmin: false без повторного входа.
30
+ */
10
31
  me(authorization: unknown): AuthUser;
32
+ /** Проверяет токен и удаляет соответствующую сессию.
33
+ * @example Действительный токен → { success: true }; повторный выход с ним → ошибка.
34
+ */
11
35
  logout(authorization: unknown): {
12
36
  success: boolean;
13
37
  };
38
+ /** Запрещает новые обращения и очищает сессии в памяти.
39
+ * @example close() → undefined; последующий login() → отказ.
40
+ */
14
41
  close(): void;
42
+ /** Ограничивает число одновременных вычислений хешей и освобождает место даже при ошибке.
43
+ * @example Четыре незавершённых вычисления → пятое получает TOO_MANY_REQUESTS.
44
+ */
45
+ private passwordWork;
46
+ /** Отклоняет обращения после закрытия сервиса.
47
+ * @example closed = true → UNAUTHENTICATED; false → undefined.
48
+ */
49
+ private assertOpen;
50
+ /** Находит сессию по хешу токена и проверяет срок; просроченную сессию удаляет.
51
+ * @example Действительный Bearer-токен → [ключ, сессия]; отсутствующий или истёкший → ошибка.
52
+ */
15
53
  private session;
54
+ /** Получает текущую запись по идентификатору из проверенной сессии.
55
+ * @example Сессия с userId = '1' → актуальная запись с id = '1'; отсутствующая запись → ошибка.
56
+ */
57
+ private identity;
58
+ /** Проверяет текущие права администратора.
59
+ * @example Действующая сессия обычного пользователя → FORBIDDEN; администратора → его запись.
60
+ */
61
+ private requireAdmin;
62
+ /** Проверяет владельца пароля и запрет изменения пароля другого администратора.
63
+ * @example Администратор и обычная чужая запись → { actor, target }; два разных администратора → FORBIDDEN.
64
+ */
65
+ private passwordTarget;
66
+ /** Завершает все сессии одного пользователя после сохранения нового пароля.
67
+ * @example Две сессии id = '1' и одна id = '2' → остаётся сессия id = '2'.
68
+ */
69
+ private revoke;
70
+ /** Удаляет из памяти сессии, срок которых истёк к текущему моменту.
71
+ * @example Одна истёкшая и одна действующая сессия → остаётся одна действующая; результат undefined.
72
+ */
16
73
  private prune;
17
74
  }
75
+ /** Открывает источник учётных записей и создаёт хранилище сессий в памяти.
76
+ * @example createAuthService({ users: [] }) → Promise<AuthService>; повторяющиеся id → ошибка.
77
+ */
18
78
  export declare function createAuthService(config: AuthConfig): Promise<AuthService>;
@@ -1,71 +1,150 @@
1
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';
2
+ import { domainError } from '../core/errors.js';
3
+ import { createUniqueId } from '../core/utils.js';
4
+ import { DEFAULT_SESSION_SECONDS } from './contract.js';
5
+ import { adminChange, credentials, passwordChange } from './input.js';
6
+ import { DUMMY_HASH, hashPassword, verifyPassword } from './password.js';
7
+ import { createAuthStore } from './store.js';
8
+ /** Создаёт исключение отсутствия действительной сессии без раскрытия причины отказа.
9
+ * @example unauthorized() → Error с code = 'UNAUTHENTICATED'.
10
+ */
7
11
  const unauthorized = () => domainError('UNAUTHENTICATED', 'Invalid credentials or expired session');
12
+ /** Вычисляет SHA-256 строки в шестнадцатеричном виде для хранения вместо исходного токена.
13
+ * @example tokenKey('abc') → 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'.
14
+ */
8
15
  const tokenKey = (token) => createHash('sha256').update(token).digest('hex');
9
- const publicUser = (user) => ({ id: user.id, username: user.username });
16
+ /** Возвращает публичные поля пользователя в новом объекте.
17
+ * @example { id: '1', username: 'anna', isAdmin: false, passwordHash: '…' } → { id: '1', username: 'anna', isAdmin: false }.
18
+ */
19
+ const publicUser = (user) => ({ id: user.id, username: user.username, isAdmin: user.isAdmin });
10
20
  export class AuthService {
11
- users;
21
+ store;
12
22
  expiresIn;
13
23
  sessions = new Map();
14
- pendingLogins = 0;
24
+ pendingPasswords = 0;
15
25
  closed = false;
16
- constructor(users, expiresIn) {
17
- this.users = users;
26
+ constructor(store, expiresIn) {
27
+ this.store = store;
18
28
  this.expiresIn = expiresIn;
19
29
  }
30
+ /** Проверяет пароль и создаёт сессию с актуальными правами пользователя.
31
+ * @example Корректные username и password → { accessToken, expiresIn, user }; неверный пароль → исключение.
32
+ */
20
33
  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)
34
+ const { username, password } = credentials(body);
35
+ const user = this.store.read().byUsername.get(username);
36
+ const valid = await this.passwordWork(() => verifyPassword(password, user?.passwordHash ?? DUMMY_HASH));
37
+ this.assertOpen();
38
+ const current = user && this.store.read().byId.get(user.id);
39
+ // Смена пароля во время scrypt не должна выдавать новую сессию по прежнему паролю.
40
+ if (!user || !valid || !current || current.passwordHash !== user.passwordHash)
45
41
  throw unauthorized();
46
42
  this.prune();
47
43
  if (this.sessions.size >= 10000)
48
44
  throw domainError('TOO_MANY_REQUESTS', 'Session limit reached');
49
45
  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) };
46
+ this.sessions.set(tokenKey(accessToken), { userId: current.id, expiresAt: Date.now() + this.expiresIn * 1000 });
47
+ return { accessToken, expiresIn: this.expiresIn, user: publicUser(current) };
48
+ }
49
+ /** Создаёт обычного пользователя с новым идентификатором и хешем пароля, не открывая сессию.
50
+ * @example { username: 'anna', password: 'secret' } → { id: '…', username: 'anna', isAdmin: false }.
51
+ */
52
+ async register(body) {
53
+ const { username, password } = credentials(body);
54
+ const passwordHash = await this.passwordWork(() => hashPassword(password));
55
+ const user = await this.store.update((users) => {
56
+ this.assertOpen();
57
+ return { id: createUniqueId((id) => users.byId.has(id)), username, passwordHash, isAdmin: false };
58
+ });
59
+ return publicUser(user);
53
60
  }
61
+ /** Меняет свой пароль после проверки текущего либо пароль обычного пользователя по правам администратора.
62
+ * @example Свой id и два пароля → { success: true } с завершением всех своих сессий; чужой администратор → FORBIDDEN.
63
+ */
64
+ async changePassword(authorization, id, body) {
65
+ const { actor, target } = this.passwordTarget(this.store.read(), authorization, id);
66
+ const { newPassword, currentPassword } = passwordChange(body);
67
+ const own = actor.id === target.id;
68
+ if (own && currentPassword === undefined)
69
+ throw domainError('INVALID_INPUT', 'Supply currentPassword to change your own password');
70
+ const passwordHash = await this.passwordWork(async () => {
71
+ if (own && !(await verifyPassword(currentPassword, target.passwordHash)))
72
+ throw unauthorized();
73
+ return hashPassword(newPassword);
74
+ });
75
+ await this.store.update((users) => {
76
+ const { target: current } = this.passwordTarget(users, authorization, id);
77
+ if (current.passwordHash !== target.passwordHash)
78
+ throw domainError('CONFLICT', 'Password changed during the request');
79
+ return { ...current, passwordHash };
80
+ }, () => this.revoke(id));
81
+ return { success: true };
82
+ }
83
+ /** Меняет статус по правам действующего администратора и сохраняет хотя бы одного при отказе от собственных прав.
84
+ * @example Последний администратор и { isAdmin: false } для себя → CONFLICT; назначение другого → обновлённый пользователь.
85
+ */
86
+ async changeAdmin(authorization, id, body) {
87
+ this.requireAdmin(authorization);
88
+ const isAdmin = adminChange(body);
89
+ const user = await this.store.update((users) => {
90
+ const actor = this.requireAdmin(authorization);
91
+ const target = users.byId.get(id);
92
+ if (!target)
93
+ throw domainError('NOT_FOUND', 'Auth user not found');
94
+ if (!isAdmin && actor.id === target.id && ![...users.byId.values()].some((user) => user.id !== id && user.isAdmin))
95
+ throw domainError('CONFLICT', 'The last administrator cannot remove their own admin status');
96
+ return { ...target, isAdmin };
97
+ });
98
+ return publicUser(user);
99
+ }
100
+ /** Проверяет токен и возвращает копию пользователя с текущими правами.
101
+ * @example После снятия статуса прежний токен возвращает isAdmin: false без повторного входа.
102
+ */
54
103
  me(authorization) {
55
- const [, session] = this.session(authorization);
56
- return publicUser(session.user);
104
+ return publicUser(this.identity(authorization));
57
105
  }
106
+ /** Проверяет токен и удаляет соответствующую сессию.
107
+ * @example Действительный токен → { success: true }; повторный выход с ним → ошибка.
108
+ */
58
109
  logout(authorization) {
59
110
  const [key] = this.session(authorization);
60
111
  this.sessions.delete(key);
61
112
  return { success: true };
62
113
  }
114
+ /** Запрещает новые обращения и очищает сессии в памяти.
115
+ * @example close() → undefined; последующий login() → отказ.
116
+ */
63
117
  close() {
64
118
  this.closed = true;
65
119
  this.sessions.clear();
66
- this.users.clear();
67
120
  }
121
+ /** Ограничивает число одновременных вычислений хешей и освобождает место даже при ошибке.
122
+ * @example Четыре незавершённых вычисления → пятое получает TOO_MANY_REQUESTS.
123
+ */
124
+ async passwordWork(operation) {
125
+ this.assertOpen();
126
+ if (this.pendingPasswords >= 4)
127
+ throw domainError('TOO_MANY_REQUESTS', 'Too many simultaneous password operations');
128
+ this.pendingPasswords++;
129
+ try {
130
+ return await operation();
131
+ }
132
+ finally {
133
+ this.pendingPasswords--;
134
+ }
135
+ }
136
+ /** Отклоняет обращения после закрытия сервиса.
137
+ * @example closed = true → UNAUTHENTICATED; false → undefined.
138
+ */
139
+ assertOpen() {
140
+ if (this.closed)
141
+ throw unauthorized();
142
+ }
143
+ /** Находит сессию по хешу токена и проверяет срок; просроченную сессию удаляет.
144
+ * @example Действительный Bearer-токен → [ключ, сессия]; отсутствующий или истёкший → ошибка.
145
+ */
68
146
  session(authorization) {
147
+ this.assertOpen();
69
148
  if (typeof authorization !== 'string')
70
149
  throw unauthorized();
71
150
  const match = /^Bearer ([A-Za-z0-9_-]{43})$/i.exec(authorization);
@@ -79,6 +158,50 @@ export class AuthService {
79
158
  }
80
159
  return [key, session];
81
160
  }
161
+ /** Получает текущую запись по идентификатору из проверенной сессии.
162
+ * @example Сессия с userId = '1' → актуальная запись с id = '1'; отсутствующая запись → ошибка.
163
+ */
164
+ identity(authorization) {
165
+ const [, session] = this.session(authorization);
166
+ const user = this.store.read().byId.get(session.userId);
167
+ if (!user)
168
+ throw unauthorized();
169
+ return user;
170
+ }
171
+ /** Проверяет текущие права администратора.
172
+ * @example Действующая сессия обычного пользователя → FORBIDDEN; администратора → его запись.
173
+ */
174
+ requireAdmin(authorization) {
175
+ const user = this.identity(authorization);
176
+ if (!user.isAdmin)
177
+ throw domainError('FORBIDDEN', 'Administrator access required');
178
+ return user;
179
+ }
180
+ /** Проверяет владельца пароля и запрет изменения пароля другого администратора.
181
+ * @example Администратор и обычная чужая запись → { actor, target }; два разных администратора → FORBIDDEN.
182
+ */
183
+ passwordTarget(users, authorization, id) {
184
+ const actor = this.identity(authorization);
185
+ if (actor.id !== id && !actor.isAdmin)
186
+ throw domainError('FORBIDDEN', 'You can only change your own password');
187
+ const target = users.byId.get(id);
188
+ if (!target)
189
+ throw domainError('NOT_FOUND', 'Auth user not found');
190
+ if (actor.id !== id && target.isAdmin)
191
+ throw domainError('FORBIDDEN', 'Cannot change another administrator password');
192
+ return { actor, target };
193
+ }
194
+ /** Завершает все сессии одного пользователя после сохранения нового пароля.
195
+ * @example Две сессии id = '1' и одна id = '2' → остаётся сессия id = '2'.
196
+ */
197
+ revoke(id) {
198
+ for (const [key, session] of this.sessions)
199
+ if (session.userId === id)
200
+ this.sessions.delete(key);
201
+ }
202
+ /** Удаляет из памяти сессии, срок которых истёк к текущему моменту.
203
+ * @example Одна истёкшая и одна действующая сессия → остаётся одна действующая; результат undefined.
204
+ */
82
205
  prune() {
83
206
  const now = Date.now();
84
207
  for (const [key, session] of this.sessions)
@@ -86,27 +209,10 @@ export class AuthService {
86
209
  this.sessions.delete(key);
87
210
  }
88
211
  }
212
+ /** Открывает источник учётных записей и создаёт хранилище сессий в памяти.
213
+ * @example createAuthService({ users: [] }) → Promise<AuthService>; повторяющиеся id → ошибка.
214
+ */
89
215
  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);
216
+ return new AuthService(await createAuthStore(config.users), config.expiresIn ?? DEFAULT_SESSION_SECONDS);
111
217
  }
112
218
  //# sourceMappingURL=service.js.map