@twin.org/api-auth-entity-storage-service 0.0.3-next.22 → 0.0.3-next.24

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 (80) hide show
  1. package/dist/es/entities/authenticationAuditEntry.js +101 -0
  2. package/dist/es/entities/authenticationAuditEntry.js.map +1 -0
  3. package/dist/es/entities/authenticationRateEntry.js +37 -0
  4. package/dist/es/entities/authenticationRateEntry.js.map +1 -0
  5. package/dist/es/index.js +10 -0
  6. package/dist/es/index.js.map +1 -1
  7. package/dist/es/models/IAuthHeaderProcessorConstructorOptions.js.map +1 -1
  8. package/dist/es/models/IEntityStorageAuthenticationAdminServiceConstructorOptions.js.map +1 -1
  9. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConfig.js +4 -0
  10. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConfig.js.map +1 -0
  11. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConstructorOptions.js +2 -0
  12. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConstructorOptions.js.map +1 -0
  13. package/dist/es/models/IEntityStorageAuthenticationRateServiceConfig.js +2 -0
  14. package/dist/es/models/IEntityStorageAuthenticationRateServiceConfig.js.map +1 -0
  15. package/dist/es/models/IEntityStorageAuthenticationRateServiceConstructorOptions.js +2 -0
  16. package/dist/es/models/IEntityStorageAuthenticationRateServiceConstructorOptions.js.map +1 -0
  17. package/dist/es/models/IEntityStorageAuthenticationServiceConfig.js +0 -2
  18. package/dist/es/models/IEntityStorageAuthenticationServiceConfig.js.map +1 -1
  19. package/dist/es/models/IEntityStorageAuthenticationServiceConstructorOptions.js.map +1 -1
  20. package/dist/es/processors/authHeaderProcessor.js +18 -4
  21. package/dist/es/processors/authHeaderProcessor.js.map +1 -1
  22. package/dist/es/restEntryPoints.js +7 -0
  23. package/dist/es/restEntryPoints.js.map +1 -1
  24. package/dist/es/routes/entityStorageAuthenticationAuditRoutes.js +174 -0
  25. package/dist/es/routes/entityStorageAuthenticationAuditRoutes.js.map +1 -0
  26. package/dist/es/schema.js +4 -0
  27. package/dist/es/schema.js.map +1 -1
  28. package/dist/es/services/entityStorageAuthenticationAdminService.js +53 -27
  29. package/dist/es/services/entityStorageAuthenticationAdminService.js.map +1 -1
  30. package/dist/es/services/entityStorageAuthenticationAuditService.js +178 -0
  31. package/dist/es/services/entityStorageAuthenticationAuditService.js.map +1 -0
  32. package/dist/es/services/entityStorageAuthenticationRateService.js +202 -0
  33. package/dist/es/services/entityStorageAuthenticationRateService.js.map +1 -0
  34. package/dist/es/services/entityStorageAuthenticationService.js +156 -11
  35. package/dist/es/services/entityStorageAuthenticationService.js.map +1 -1
  36. package/dist/es/utils/passwordHelper.js +57 -0
  37. package/dist/es/utils/passwordHelper.js.map +1 -0
  38. package/dist/es/utils/tokenHelper.js +11 -1
  39. package/dist/es/utils/tokenHelper.js.map +1 -1
  40. package/dist/types/entities/authenticationAuditEntry.d.ts +49 -0
  41. package/dist/types/entities/authenticationRateEntry.d.ts +17 -0
  42. package/dist/types/index.d.ts +10 -0
  43. package/dist/types/models/IAuthHeaderProcessorConstructorOptions.d.ts +5 -0
  44. package/dist/types/models/IEntityStorageAuthenticationAdminServiceConstructorOptions.d.ts +5 -0
  45. package/dist/types/models/IEntityStorageAuthenticationAuditServiceConfig.d.ts +9 -0
  46. package/dist/types/models/IEntityStorageAuthenticationAuditServiceConstructorOptions.d.ts +15 -0
  47. package/dist/types/models/IEntityStorageAuthenticationRateServiceConfig.d.ts +10 -0
  48. package/dist/types/models/IEntityStorageAuthenticationRateServiceConstructorOptions.d.ts +20 -0
  49. package/dist/types/models/IEntityStorageAuthenticationServiceConfig.d.ts +22 -1
  50. package/dist/types/models/IEntityStorageAuthenticationServiceConstructorOptions.d.ts +8 -3
  51. package/dist/types/routes/entityStorageAuthenticationAuditRoutes.d.ts +29 -0
  52. package/dist/types/services/entityStorageAuthenticationAuditService.d.ts +59 -0
  53. package/dist/types/services/entityStorageAuthenticationRateService.d.ts +60 -0
  54. package/dist/types/services/entityStorageAuthenticationService.d.ts +6 -0
  55. package/dist/types/utils/passwordHelper.d.ts +24 -0
  56. package/dist/types/utils/tokenHelper.d.ts +2 -1
  57. package/docs/changelog.md +32 -0
  58. package/docs/reference/classes/AuthenticationAuditEntry.md +101 -0
  59. package/docs/reference/classes/AuthenticationRateEntry.md +37 -0
  60. package/docs/reference/classes/EntityStorageAuthenticationAdminService.md +4 -4
  61. package/docs/reference/classes/EntityStorageAuthenticationAuditService.md +157 -0
  62. package/docs/reference/classes/EntityStorageAuthenticationRateService.md +227 -0
  63. package/docs/reference/classes/EntityStorageAuthenticationService.md +26 -0
  64. package/docs/reference/classes/PasswordHelper.md +74 -0
  65. package/docs/reference/classes/TokenHelper.md +7 -1
  66. package/docs/reference/functions/authenticationAuditCreate.md +31 -0
  67. package/docs/reference/functions/authenticationAuditQuery.md +31 -0
  68. package/docs/reference/functions/generateRestRoutesAuthenticationAudit.md +25 -0
  69. package/docs/reference/index.md +13 -0
  70. package/docs/reference/interfaces/IAuthHeaderProcessorConstructorOptions.md +14 -0
  71. package/docs/reference/interfaces/IEntityStorageAuthenticationAdminServiceConstructorOptions.md +14 -0
  72. package/docs/reference/interfaces/IEntityStorageAuthenticationAuditServiceConfig.md +11 -0
  73. package/docs/reference/interfaces/IEntityStorageAuthenticationAuditServiceConstructorOptions.md +25 -0
  74. package/docs/reference/interfaces/IEntityStorageAuthenticationRateServiceConfig.md +17 -0
  75. package/docs/reference/interfaces/IEntityStorageAuthenticationRateServiceConstructorOptions.md +39 -0
  76. package/docs/reference/interfaces/IEntityStorageAuthenticationServiceConfig.md +57 -1
  77. package/docs/reference/interfaces/IEntityStorageAuthenticationServiceConstructorOptions.md +18 -4
  78. package/docs/reference/variables/tagsAuthenticationAudit.md +5 -0
  79. package/locales/en.json +13 -2
  80. package/package.json +5 -4
package/dist/es/schema.js CHANGED
@@ -1,11 +1,15 @@
1
1
  // Copyright 2024 IOTA Stiftung.
2
2
  // SPDX-License-Identifier: Apache-2.0.
3
3
  import { EntitySchemaFactory, EntitySchemaHelper } from "@twin.org/entity";
4
+ import { AuthenticationAuditEntry } from "./entities/authenticationAuditEntry.js";
5
+ import { AuthenticationRateEntry } from "./entities/authenticationRateEntry.js";
4
6
  import { AuthenticationUser } from "./entities/authenticationUser.js";
5
7
  /**
6
8
  * Initialize the schema for the authentication service.
7
9
  */
8
10
  export function initSchema() {
9
11
  EntitySchemaFactory.register("AuthenticationUser", () => EntitySchemaHelper.getSchema(AuthenticationUser));
12
+ EntitySchemaFactory.register("AuthenticationAuditEntry", () => EntitySchemaHelper.getSchema(AuthenticationAuditEntry));
13
+ EntitySchemaFactory.register("AuthenticationRateEntry", () => EntitySchemaHelper.getSchema(AuthenticationRateEntry));
10
14
  }
11
15
  //# sourceMappingURL=schema.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/schema.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAEtE;;GAEG;AACH,MAAM,UAAU,UAAU;IACzB,mBAAmB,CAAC,QAAQ,uBAA+B,GAAG,EAAE,CAC/D,kBAAkB,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAChD,CAAC;AACH,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { EntitySchemaFactory, EntitySchemaHelper } from \"@twin.org/entity\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { AuthenticationUser } from \"./entities/authenticationUser.js\";\n\n/**\n * Initialize the schema for the authentication service.\n */\nexport function initSchema(): void {\n\tEntitySchemaFactory.register(nameof<AuthenticationUser>(), () =>\n\t\tEntitySchemaHelper.getSchema(AuthenticationUser)\n\t);\n}\n"]}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/schema.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3E,OAAO,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAC;AAClF,OAAO,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAC;AAChF,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAEtE;;GAEG;AACH,MAAM,UAAU,UAAU;IACzB,mBAAmB,CAAC,QAAQ,uBAA+B,GAAG,EAAE,CAC/D,kBAAkB,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAChD,CAAC;IACF,mBAAmB,CAAC,QAAQ,6BAAqC,GAAG,EAAE,CACrE,kBAAkB,CAAC,SAAS,CAAC,wBAAwB,CAAC,CACtD,CAAC;IACF,mBAAmB,CAAC,QAAQ,4BAAoC,GAAG,EAAE,CACpE,kBAAkB,CAAC,SAAS,CAAC,uBAAuB,CAAC,CACrD,CAAC;AACH,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { EntitySchemaFactory, EntitySchemaHelper } from \"@twin.org/entity\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { AuthenticationAuditEntry } from \"./entities/authenticationAuditEntry.js\";\nimport { AuthenticationRateEntry } from \"./entities/authenticationRateEntry.js\";\nimport { AuthenticationUser } from \"./entities/authenticationUser.js\";\n\n/**\n * Initialize the schema for the authentication service.\n */\nexport function initSchema(): void {\n\tEntitySchemaFactory.register(nameof<AuthenticationUser>(), () =>\n\t\tEntitySchemaHelper.getSchema(AuthenticationUser)\n\t);\n\tEntitySchemaFactory.register(nameof<AuthenticationAuditEntry>(), () =>\n\t\tEntitySchemaHelper.getSchema(AuthenticationAuditEntry)\n\t);\n\tEntitySchemaFactory.register(nameof<AuthenticationRateEntry>(), () =>\n\t\tEntitySchemaHelper.getSchema(AuthenticationRateEntry)\n\t);\n}\n"]}
@@ -1,6 +1,8 @@
1
- import { Converter, GeneralError, Guards, Is, NotFoundError, RandomHelper } from "@twin.org/core";
1
+ import { AuthAuditEvent } from "@twin.org/api-auth-entity-storage-models";
2
+ import { ComponentFactory, Converter, GeneralError, Guards, Is, NotFoundError, RandomHelper } from "@twin.org/core";
2
3
  import { PasswordGenerator, PasswordValidator } from "@twin.org/crypto";
3
4
  import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
5
+ import { PasswordHelper } from "../utils/passwordHelper.js";
4
6
  /**
5
7
  * Implementation of the authentication component using entity storage.
6
8
  */
@@ -14,6 +16,11 @@ export class EntityStorageAuthenticationAdminService {
14
16
  * @internal
15
17
  */
16
18
  _userEntityStorage;
19
+ /**
20
+ * The audit service.
21
+ * @internal
22
+ */
23
+ _authenticationAuditService;
17
24
  /**
18
25
  * The minimum password length.
19
26
  * @internal
@@ -25,6 +32,7 @@ export class EntityStorageAuthenticationAdminService {
25
32
  */
26
33
  constructor(options) {
27
34
  this._userEntityStorage = EntityStorageConnectorFactory.get(options?.userEntityStorageType ?? "authentication-user");
35
+ this._authenticationAuditService = ComponentFactory.getIfExists(options?.authenticationAuditServiceType ?? "authentication-audit");
28
36
  this._minPasswordLength = options?.config?.minPasswordLength;
29
37
  }
30
38
  /**
@@ -66,6 +74,15 @@ export class EntityStorageAuthenticationAdminService {
66
74
  scope: user.scope.map(s => s.trim().toLocaleLowerCase()).join(",")
67
75
  };
68
76
  await this._userEntityStorage.set(newUser);
77
+ await this._authenticationAuditService?.create({
78
+ actorId: user.email,
79
+ event: AuthAuditEvent.AccountCreated,
80
+ data: {
81
+ userIdentity: user.userIdentity,
82
+ organizationIdentity: user.organizationIdentity,
83
+ scope: user.scope
84
+ }
85
+ });
69
86
  }
70
87
  catch (error) {
71
88
  throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "createUserFailed", undefined, error);
@@ -93,12 +110,34 @@ export class EntityStorageAuthenticationAdminService {
93
110
  if (!Is.object(existingUser)) {
94
111
  throw new NotFoundError(EntityStorageAuthenticationAdminService.CLASS_NAME, "userNotFound", user.email);
95
112
  }
113
+ const updatedFields = [];
114
+ const updatedScope = Is.array(user.scope)
115
+ ? user.scope.map(s => s.trim().toLocaleLowerCase()).join(",")
116
+ : existingUser.scope;
117
+ if (user.userIdentity !== undefined && user.userIdentity !== existingUser.identity) {
118
+ updatedFields.push("userIdentity");
119
+ }
120
+ if (user.organizationIdentity !== undefined &&
121
+ user.organizationIdentity !== existingUser.organization) {
122
+ updatedFields.push("organizationIdentity");
123
+ }
124
+ if (Is.array(user.scope) && updatedScope !== existingUser.scope) {
125
+ updatedFields.push("scope");
126
+ }
96
127
  existingUser.identity = user.userIdentity ?? existingUser.identity;
97
128
  existingUser.organization = user.organizationIdentity ?? existingUser.organization;
98
- existingUser.scope = Is.array(user.scope)
99
- ? user.scope.map(s => s.trim().toLocaleLowerCase()).join(",")
100
- : user.scope;
129
+ existingUser.scope = Is.array(user.scope) ? updatedScope : existingUser.scope;
101
130
  await this._userEntityStorage.set(existingUser);
131
+ await this._authenticationAuditService?.create({
132
+ actorId: existingUser.email,
133
+ event: AuthAuditEvent.AccountUpdated,
134
+ data: {
135
+ updatedFields,
136
+ userIdentity: existingUser.identity,
137
+ organizationIdentity: existingUser.organization,
138
+ scope: existingUser.scope.split(",")
139
+ }
140
+ });
102
141
  }
103
142
  catch (error) {
104
143
  throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "updateUserFailed", undefined, error);
@@ -163,6 +202,15 @@ export class EntityStorageAuthenticationAdminService {
163
202
  throw new NotFoundError(EntityStorageAuthenticationAdminService.CLASS_NAME, "userNotFound", email);
164
203
  }
165
204
  await this._userEntityStorage.remove(email);
205
+ await this._authenticationAuditService?.create({
206
+ actorId: email,
207
+ event: AuthAuditEvent.AccountDeleted,
208
+ data: {
209
+ userIdentity: user.identity,
210
+ organizationIdentity: user.organization,
211
+ scope: user.scope.split(",")
212
+ }
213
+ });
166
214
  }
167
215
  catch (error) {
168
216
  throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "removeUserFailed", undefined, error);
@@ -179,33 +227,11 @@ export class EntityStorageAuthenticationAdminService {
179
227
  Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "email", email);
180
228
  Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "newPassword", newPassword);
181
229
  try {
182
- PasswordValidator.validatePassword(newPassword, {
183
- minLength: this._minPasswordLength
184
- });
185
230
  const user = await this._userEntityStorage.get(email);
186
231
  if (!Is.object(user)) {
187
232
  throw new NotFoundError(EntityStorageAuthenticationAdminService.CLASS_NAME, "userNotFound", email);
188
233
  }
189
- if (Is.stringValue(currentPassword)) {
190
- const saltBytes = Converter.base64ToBytes(user.salt);
191
- const passwordBytes = Converter.utf8ToBytes(currentPassword);
192
- const hashedPassword = await PasswordGenerator.hashPassword(passwordBytes, saltBytes);
193
- if (!PasswordValidator.comparePasswordHashes(hashedPassword, user.password)) {
194
- throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "currentPasswordMismatch");
195
- }
196
- }
197
- const saltBytes = RandomHelper.generate(16);
198
- const passwordBytes = Converter.utf8ToBytes(newPassword);
199
- const hashedPassword = await PasswordGenerator.hashPassword(passwordBytes, saltBytes);
200
- const updatedUser = {
201
- email,
202
- salt: Converter.bytesToBase64(saltBytes),
203
- password: hashedPassword,
204
- identity: user.identity,
205
- organization: user.organization,
206
- scope: user.scope
207
- };
208
- await this._userEntityStorage.set(updatedUser);
234
+ await PasswordHelper.updatePassword(this._userEntityStorage, this._authenticationAuditService, user, newPassword, currentPassword, this._minPasswordLength);
209
235
  }
210
236
  catch (error) {
211
237
  throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "updatePasswordFailed", undefined, error);
@@ -1 +1 @@
1
- {"version":3,"file":"entityStorageAuthenticationAdminService.js","sourceRoot":"","sources":["../../../src/services/entityStorageAuthenticationAdminService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type {\n\tIAuthenticationAdminComponent,\n\tIAuthenticationUser\n} from \"@twin.org/api-auth-entity-storage-models\";\nimport { Converter, GeneralError, Guards, Is, NotFoundError, RandomHelper } from \"@twin.org/core\";\nimport { PasswordGenerator, PasswordValidator } from \"@twin.org/crypto\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport type { AuthenticationUser } from \"../entities/authenticationUser.js\";\nimport type { IEntityStorageAuthenticationAdminServiceConstructorOptions } from \"../models/IEntityStorageAuthenticationAdminServiceConstructorOptions.js\";\n\n/**\n * Implementation of the authentication component using entity storage.\n */\nexport class EntityStorageAuthenticationAdminService implements IAuthenticationAdminComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<EntityStorageAuthenticationAdminService>();\n\n\t/**\n\t * The entity storage for users.\n\t * @internal\n\t */\n\tprivate readonly _userEntityStorage: IEntityStorageConnector<AuthenticationUser>;\n\n\t/**\n\t * The minimum password length.\n\t * @internal\n\t */\n\tprivate readonly _minPasswordLength?: number;\n\n\t/**\n\t * Create a new instance of EntityStorageAuthentication.\n\t * @param options The dependencies for the identity connector.\n\t */\n\tconstructor(options?: IEntityStorageAuthenticationAdminServiceConstructorOptions) {\n\t\tthis._userEntityStorage = EntityStorageConnectorFactory.get(\n\t\t\toptions?.userEntityStorageType ?? \"authentication-user\"\n\t\t);\n\n\t\tthis._minPasswordLength = options?.config?.minPasswordLength;\n\t}\n\n\t/**\n\t * Returns the class name of the component.\n\t * @returns The class name of the component.\n\t */\n\tpublic className(): string {\n\t\treturn EntityStorageAuthenticationAdminService.CLASS_NAME;\n\t}\n\n\t/**\n\t * Create a login for the user.\n\t * @param user The user to create.\n\t * @returns Nothing.\n\t */\n\tpublic async create(user: Omit<IAuthenticationUser, \"salt\">): Promise<void> {\n\t\tGuards.object<IAuthenticationUser>(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user),\n\t\t\tuser\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.email),\n\t\t\tuser.email\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.password),\n\t\t\tuser.password\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.userIdentity),\n\t\t\tuser.userIdentity\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.organizationIdentity),\n\t\t\tuser.organizationIdentity\n\t\t);\n\t\tGuards.array<string>(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.scope),\n\t\t\tuser.scope\n\t\t);\n\n\t\ttry {\n\t\t\tPasswordValidator.validatePassword(user.password, {\n\t\t\t\tminLength: this._minPasswordLength\n\t\t\t});\n\n\t\t\tconst existingUser = await this._userEntityStorage.get(user.email);\n\t\t\tif (Is.object<AuthenticationUser>(existingUser)) {\n\t\t\t\tthrow new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, \"userExists\");\n\t\t\t}\n\n\t\t\tconst saltBytes = RandomHelper.generate(16);\n\t\t\tconst passwordBytes = Converter.utf8ToBytes(user.password);\n\n\t\t\tconst hashedPassword = await PasswordGenerator.hashPassword(passwordBytes, saltBytes);\n\n\t\t\tconst newUser: AuthenticationUser = {\n\t\t\t\temail: user.email,\n\t\t\t\tsalt: Converter.bytesToBase64(saltBytes),\n\t\t\t\tpassword: hashedPassword,\n\t\t\t\tidentity: user.userIdentity,\n\t\t\t\torganization: user.organizationIdentity,\n\t\t\t\tscope: user.scope.map(s => s.trim().toLocaleLowerCase()).join(\",\")\n\t\t\t};\n\n\t\t\tawait this._userEntityStorage.set(newUser);\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"createUserFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Update a login for the user.\n\t * @param user The user to update.\n\t * @returns Nothing.\n\t */\n\tpublic async update(\n\t\tuser: Partial<Omit<IAuthenticationUser, \"password\" | \"salt\">>\n\t): Promise<void> {\n\t\tGuards.object<IAuthenticationUser>(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user),\n\t\t\tuser\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.email),\n\t\t\tuser.email\n\t\t);\n\n\t\tif (!Is.empty(user.userIdentity)) {\n\t\t\tGuards.stringValue(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\tnameof(user.userIdentity),\n\t\t\t\tuser.userIdentity\n\t\t\t);\n\t\t}\n\t\tif (!Is.empty(user.organizationIdentity)) {\n\t\t\tGuards.stringValue(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\tnameof(user.organizationIdentity),\n\t\t\t\tuser.organizationIdentity\n\t\t\t);\n\t\t}\n\t\tif (!Is.empty(user.scope)) {\n\t\t\tGuards.array<string>(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\tnameof(user.scope),\n\t\t\t\tuser.scope\n\t\t\t);\n\t\t}\n\n\t\ttry {\n\t\t\tconst existingUser = await this._userEntityStorage.get(user.email);\n\t\t\tif (!Is.object<AuthenticationUser>(existingUser)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"userNotFound\",\n\t\t\t\t\tuser.email\n\t\t\t\t);\n\t\t\t}\n\n\t\t\texistingUser.identity = user.userIdentity ?? existingUser.identity;\n\t\t\texistingUser.organization = user.organizationIdentity ?? existingUser.organization;\n\t\t\texistingUser.scope = Is.array(user.scope)\n\t\t\t\t? user.scope.map(s => s.trim().toLocaleLowerCase()).join(\",\")\n\t\t\t\t: user.scope;\n\n\t\t\tawait this._userEntityStorage.set(existingUser);\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"updateUserFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Get a user by email.\n\t * @param email The email address of the user to get.\n\t * @returns The user details.\n\t */\n\tpublic async get(email: string): Promise<Omit<IAuthenticationUser, \"password\" | \"salt\">> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, nameof(email), email);\n\n\t\ttry {\n\t\t\tconst user = await this._userEntityStorage.get(email);\n\t\t\tif (!Is.object<AuthenticationUser>(user)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"userNotFound\",\n\t\t\t\t\temail\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\temail: user.email,\n\t\t\t\tuserIdentity: user.identity,\n\t\t\t\torganizationIdentity: user.organization,\n\t\t\t\tscope: user.scope.split(\",\")\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"getUserFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Get a user by identity.\n\t * @param identity The identity of the user to get.\n\t * @returns The user details.\n\t */\n\tpublic async getByIdentity(\n\t\tidentity: string\n\t): Promise<Omit<IAuthenticationUser, \"password\" | \"salt\">> {\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(identity),\n\t\t\tidentity\n\t\t);\n\n\t\ttry {\n\t\t\tconst user = await this._userEntityStorage.get(identity, \"identity\");\n\t\t\tif (!Is.object<AuthenticationUser>(user)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"userNotFound\",\n\t\t\t\t\tidentity\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\temail: user.email,\n\t\t\t\tuserIdentity: user.identity,\n\t\t\t\torganizationIdentity: user.organization,\n\t\t\t\tscope: user.scope.split(\",\")\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"getUserFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Remove the current user.\n\t * @param email The email address of the user to remove.\n\t * @returns Nothing.\n\t */\n\tpublic async remove(email: string): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, nameof(email), email);\n\n\t\ttry {\n\t\t\tconst user = await this._userEntityStorage.get(email);\n\t\t\tif (!Is.object<AuthenticationUser>(user)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"userNotFound\",\n\t\t\t\t\temail\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tawait this._userEntityStorage.remove(email);\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"removeUserFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Update the user's password.\n\t * @param email The email address of the user to update.\n\t * @param newPassword The new password for the user.\n\t * @param currentPassword The current password, optional, if supplied will check against existing.\n\t * @returns Nothing.\n\t */\n\tpublic async updatePassword(\n\t\temail: string,\n\t\tnewPassword: string,\n\t\tcurrentPassword?: string\n\t): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, nameof(email), email);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(newPassword),\n\t\t\tnewPassword\n\t\t);\n\n\t\ttry {\n\t\t\tPasswordValidator.validatePassword(newPassword, {\n\t\t\t\tminLength: this._minPasswordLength\n\t\t\t});\n\n\t\t\tconst user = await this._userEntityStorage.get(email);\n\t\t\tif (!Is.object<AuthenticationUser>(user)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"userNotFound\",\n\t\t\t\t\temail\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (Is.stringValue(currentPassword)) {\n\t\t\t\tconst saltBytes = Converter.base64ToBytes(user.salt);\n\t\t\t\tconst passwordBytes = Converter.utf8ToBytes(currentPassword);\n\n\t\t\t\tconst hashedPassword = await PasswordGenerator.hashPassword(passwordBytes, saltBytes);\n\n\t\t\t\tif (!PasswordValidator.comparePasswordHashes(hashedPassword, user.password)) {\n\t\t\t\t\tthrow new GeneralError(\n\t\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\t\"currentPasswordMismatch\"\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst saltBytes = RandomHelper.generate(16);\n\t\t\tconst passwordBytes = Converter.utf8ToBytes(newPassword);\n\n\t\t\tconst hashedPassword = await PasswordGenerator.hashPassword(passwordBytes, saltBytes);\n\n\t\t\tconst updatedUser: AuthenticationUser = {\n\t\t\t\temail,\n\t\t\t\tsalt: Converter.bytesToBase64(saltBytes),\n\t\t\t\tpassword: hashedPassword,\n\t\t\t\tidentity: user.identity,\n\t\t\t\torganization: user.organization,\n\t\t\t\tscope: user.scope\n\t\t\t};\n\n\t\t\tawait this._userEntityStorage.set(updatedUser);\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"updatePasswordFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"entityStorageAuthenticationAdminService.js","sourceRoot":"","sources":["../../../src/services/entityStorageAuthenticationAdminService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type {\n\tIAuthenticationAuditComponent,\n\tIAuthenticationAdminComponent,\n\tIAuthenticationUser\n} from \"@twin.org/api-auth-entity-storage-models\";\nimport { AuthAuditEvent } from \"@twin.org/api-auth-entity-storage-models\";\nimport {\n\tComponentFactory,\n\tConverter,\n\tGeneralError,\n\tGuards,\n\tIs,\n\tNotFoundError,\n\tRandomHelper\n} from \"@twin.org/core\";\nimport { PasswordGenerator, PasswordValidator } from \"@twin.org/crypto\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport type { AuthenticationUser } from \"../entities/authenticationUser.js\";\nimport type { IEntityStorageAuthenticationAdminServiceConstructorOptions } from \"../models/IEntityStorageAuthenticationAdminServiceConstructorOptions.js\";\nimport { PasswordHelper } from \"../utils/passwordHelper.js\";\n\n/**\n * Implementation of the authentication component using entity storage.\n */\nexport class EntityStorageAuthenticationAdminService implements IAuthenticationAdminComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<EntityStorageAuthenticationAdminService>();\n\n\t/**\n\t * The entity storage for users.\n\t * @internal\n\t */\n\tprivate readonly _userEntityStorage: IEntityStorageConnector<AuthenticationUser>;\n\n\t/**\n\t * The audit service.\n\t * @internal\n\t */\n\tprivate readonly _authenticationAuditService?: IAuthenticationAuditComponent;\n\n\t/**\n\t * The minimum password length.\n\t * @internal\n\t */\n\tprivate readonly _minPasswordLength?: number;\n\n\t/**\n\t * Create a new instance of EntityStorageAuthentication.\n\t * @param options The dependencies for the identity connector.\n\t */\n\tconstructor(options?: IEntityStorageAuthenticationAdminServiceConstructorOptions) {\n\t\tthis._userEntityStorage = EntityStorageConnectorFactory.get(\n\t\t\toptions?.userEntityStorageType ?? \"authentication-user\"\n\t\t);\n\n\t\tthis._authenticationAuditService = ComponentFactory.getIfExists<IAuthenticationAuditComponent>(\n\t\t\toptions?.authenticationAuditServiceType ?? \"authentication-audit\"\n\t\t);\n\n\t\tthis._minPasswordLength = options?.config?.minPasswordLength;\n\t}\n\n\t/**\n\t * Returns the class name of the component.\n\t * @returns The class name of the component.\n\t */\n\tpublic className(): string {\n\t\treturn EntityStorageAuthenticationAdminService.CLASS_NAME;\n\t}\n\n\t/**\n\t * Create a login for the user.\n\t * @param user The user to create.\n\t * @returns Nothing.\n\t */\n\tpublic async create(user: Omit<IAuthenticationUser, \"salt\">): Promise<void> {\n\t\tGuards.object<IAuthenticationUser>(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user),\n\t\t\tuser\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.email),\n\t\t\tuser.email\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.password),\n\t\t\tuser.password\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.userIdentity),\n\t\t\tuser.userIdentity\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.organizationIdentity),\n\t\t\tuser.organizationIdentity\n\t\t);\n\t\tGuards.array<string>(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.scope),\n\t\t\tuser.scope\n\t\t);\n\n\t\ttry {\n\t\t\tPasswordValidator.validatePassword(user.password, {\n\t\t\t\tminLength: this._minPasswordLength\n\t\t\t});\n\n\t\t\tconst existingUser = await this._userEntityStorage.get(user.email);\n\t\t\tif (Is.object<AuthenticationUser>(existingUser)) {\n\t\t\t\tthrow new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, \"userExists\");\n\t\t\t}\n\n\t\t\tconst saltBytes = RandomHelper.generate(16);\n\t\t\tconst passwordBytes = Converter.utf8ToBytes(user.password);\n\n\t\t\tconst hashedPassword = await PasswordGenerator.hashPassword(passwordBytes, saltBytes);\n\n\t\t\tconst newUser: AuthenticationUser = {\n\t\t\t\temail: user.email,\n\t\t\t\tsalt: Converter.bytesToBase64(saltBytes),\n\t\t\t\tpassword: hashedPassword,\n\t\t\t\tidentity: user.userIdentity,\n\t\t\t\torganization: user.organizationIdentity,\n\t\t\t\tscope: user.scope.map(s => s.trim().toLocaleLowerCase()).join(\",\")\n\t\t\t};\n\n\t\t\tawait this._userEntityStorage.set(newUser);\n\t\t\tawait this._authenticationAuditService?.create({\n\t\t\t\tactorId: user.email,\n\t\t\t\tevent: AuthAuditEvent.AccountCreated,\n\t\t\t\tdata: {\n\t\t\t\t\tuserIdentity: user.userIdentity,\n\t\t\t\t\torganizationIdentity: user.organizationIdentity,\n\t\t\t\t\tscope: user.scope\n\t\t\t\t}\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"createUserFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Update a login for the user.\n\t * @param user The user to update.\n\t * @returns Nothing.\n\t */\n\tpublic async update(\n\t\tuser: Partial<Omit<IAuthenticationUser, \"password\" | \"salt\">>\n\t): Promise<void> {\n\t\tGuards.object<IAuthenticationUser>(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user),\n\t\t\tuser\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.email),\n\t\t\tuser.email\n\t\t);\n\n\t\tif (!Is.empty(user.userIdentity)) {\n\t\t\tGuards.stringValue(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\tnameof(user.userIdentity),\n\t\t\t\tuser.userIdentity\n\t\t\t);\n\t\t}\n\t\tif (!Is.empty(user.organizationIdentity)) {\n\t\t\tGuards.stringValue(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\tnameof(user.organizationIdentity),\n\t\t\t\tuser.organizationIdentity\n\t\t\t);\n\t\t}\n\t\tif (!Is.empty(user.scope)) {\n\t\t\tGuards.array<string>(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\tnameof(user.scope),\n\t\t\t\tuser.scope\n\t\t\t);\n\t\t}\n\n\t\ttry {\n\t\t\tconst existingUser = await this._userEntityStorage.get(user.email);\n\t\t\tif (!Is.object<AuthenticationUser>(existingUser)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"userNotFound\",\n\t\t\t\t\tuser.email\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst updatedFields: string[] = [];\n\t\t\tconst updatedScope = Is.array(user.scope)\n\t\t\t\t? user.scope.map(s => s.trim().toLocaleLowerCase()).join(\",\")\n\t\t\t\t: existingUser.scope;\n\n\t\t\tif (user.userIdentity !== undefined && user.userIdentity !== existingUser.identity) {\n\t\t\t\tupdatedFields.push(\"userIdentity\");\n\t\t\t}\n\t\t\tif (\n\t\t\t\tuser.organizationIdentity !== undefined &&\n\t\t\t\tuser.organizationIdentity !== existingUser.organization\n\t\t\t) {\n\t\t\t\tupdatedFields.push(\"organizationIdentity\");\n\t\t\t}\n\t\t\tif (Is.array(user.scope) && updatedScope !== existingUser.scope) {\n\t\t\t\tupdatedFields.push(\"scope\");\n\t\t\t}\n\n\t\t\texistingUser.identity = user.userIdentity ?? existingUser.identity;\n\t\t\texistingUser.organization = user.organizationIdentity ?? existingUser.organization;\n\t\t\texistingUser.scope = Is.array(user.scope) ? updatedScope : existingUser.scope;\n\n\t\t\tawait this._userEntityStorage.set(existingUser);\n\t\t\tawait this._authenticationAuditService?.create({\n\t\t\t\tactorId: existingUser.email,\n\t\t\t\tevent: AuthAuditEvent.AccountUpdated,\n\t\t\t\tdata: {\n\t\t\t\t\tupdatedFields,\n\t\t\t\t\tuserIdentity: existingUser.identity,\n\t\t\t\t\torganizationIdentity: existingUser.organization,\n\t\t\t\t\tscope: existingUser.scope.split(\",\")\n\t\t\t\t}\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"updateUserFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Get a user by email.\n\t * @param email The email address of the user to get.\n\t * @returns The user details.\n\t */\n\tpublic async get(email: string): Promise<Omit<IAuthenticationUser, \"password\" | \"salt\">> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, nameof(email), email);\n\n\t\ttry {\n\t\t\tconst user = await this._userEntityStorage.get(email);\n\t\t\tif (!Is.object<AuthenticationUser>(user)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"userNotFound\",\n\t\t\t\t\temail\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\temail: user.email,\n\t\t\t\tuserIdentity: user.identity,\n\t\t\t\torganizationIdentity: user.organization,\n\t\t\t\tscope: user.scope.split(\",\")\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"getUserFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Get a user by identity.\n\t * @param identity The identity of the user to get.\n\t * @returns The user details.\n\t */\n\tpublic async getByIdentity(\n\t\tidentity: string\n\t): Promise<Omit<IAuthenticationUser, \"password\" | \"salt\">> {\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(identity),\n\t\t\tidentity\n\t\t);\n\n\t\ttry {\n\t\t\tconst user = await this._userEntityStorage.get(identity, \"identity\");\n\t\t\tif (!Is.object<AuthenticationUser>(user)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"userNotFound\",\n\t\t\t\t\tidentity\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\temail: user.email,\n\t\t\t\tuserIdentity: user.identity,\n\t\t\t\torganizationIdentity: user.organization,\n\t\t\t\tscope: user.scope.split(\",\")\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"getUserFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Remove the current user.\n\t * @param email The email address of the user to remove.\n\t * @returns Nothing.\n\t */\n\tpublic async remove(email: string): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, nameof(email), email);\n\n\t\ttry {\n\t\t\tconst user = await this._userEntityStorage.get(email);\n\t\t\tif (!Is.object<AuthenticationUser>(user)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"userNotFound\",\n\t\t\t\t\temail\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tawait this._userEntityStorage.remove(email);\n\t\t\tawait this._authenticationAuditService?.create({\n\t\t\t\tactorId: email,\n\t\t\t\tevent: AuthAuditEvent.AccountDeleted,\n\t\t\t\tdata: {\n\t\t\t\t\tuserIdentity: user.identity,\n\t\t\t\t\torganizationIdentity: user.organization,\n\t\t\t\t\tscope: user.scope.split(\",\")\n\t\t\t\t}\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"removeUserFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Update the user's password.\n\t * @param email The email address of the user to update.\n\t * @param newPassword The new password for the user.\n\t * @param currentPassword The current password, optional, if supplied will check against existing.\n\t * @returns Nothing.\n\t */\n\tpublic async updatePassword(\n\t\temail: string,\n\t\tnewPassword: string,\n\t\tcurrentPassword?: string\n\t): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, nameof(email), email);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(newPassword),\n\t\t\tnewPassword\n\t\t);\n\n\t\ttry {\n\t\t\tconst user = await this._userEntityStorage.get(email);\n\t\t\tif (!Is.object<AuthenticationUser>(user)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"userNotFound\",\n\t\t\t\t\temail\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tawait PasswordHelper.updatePassword(\n\t\t\t\tthis._userEntityStorage,\n\t\t\t\tthis._authenticationAuditService,\n\t\t\t\tuser,\n\t\t\t\tnewPassword,\n\t\t\t\tcurrentPassword,\n\t\t\t\tthis._minPasswordLength\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"updatePasswordFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,178 @@
1
+ import { HttpContextIdKeys } from "@twin.org/api-models";
2
+ import { ContextIdStore, ContextIdKeys } from "@twin.org/context";
3
+ import { Converter, Guards, Is, RandomHelper, Validation } from "@twin.org/core";
4
+ import { Sha256 } from "@twin.org/crypto";
5
+ import { ComparisonOperator } from "@twin.org/entity";
6
+ import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
7
+ /**
8
+ * Implementation of the authentication audit component using entity storage.
9
+ */
10
+ export class EntityStorageAuthenticationAuditService {
11
+ /**
12
+ * Runtime name for the class.
13
+ */
14
+ static CLASS_NAME = "EntityStorageAuthenticationAuditService";
15
+ /**
16
+ * The entity storage for authentication audit entries.
17
+ * @internal
18
+ */
19
+ _authenticationAuditEntryEntityStorage;
20
+ /**
21
+ * The server-side salt for hashing IP addresses in audit logs, if configured.
22
+ * @internal
23
+ */
24
+ _ipHashSalt;
25
+ /**
26
+ * Create a new instance of EntityStorageAuthenticationAuditService.
27
+ * @param options The dependencies for the identity connector.
28
+ */
29
+ constructor(options) {
30
+ this._authenticationAuditEntryEntityStorage = EntityStorageConnectorFactory.get(options?.authenticationAuditEntryStorageType ?? "authentication-audit-entry");
31
+ const salt = options?.config?.ipHashSalt?.trim();
32
+ if (Is.stringValue(salt)) {
33
+ const validationFailures = [];
34
+ Validation.stringValue("options.config.ipHashSalt", salt, validationFailures, undefined, { minLength: 32 });
35
+ const entropy = new Set(salt).size;
36
+ if (entropy < 8) {
37
+ validationFailures.push({
38
+ property: "options.config.ipHashSalt",
39
+ reason: "validation.saltEntropyTooLow"
40
+ });
41
+ }
42
+ Validation.asValidationError(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.config.ipHashSalt", validationFailures);
43
+ this._ipHashSalt = salt;
44
+ }
45
+ }
46
+ /**
47
+ * Returns the class name of the component.
48
+ * @returns The class name of the component.
49
+ */
50
+ className() {
51
+ return EntityStorageAuthenticationAuditService.CLASS_NAME;
52
+ }
53
+ /**
54
+ * Create a new audit entry.
55
+ * @param entry The audit entry to be logged.
56
+ * @returns The unique identifier of the created audit entry.
57
+ */
58
+ async create(entry) {
59
+ Guards.object(EntityStorageAuthenticationAuditService.CLASS_NAME, "entry", entry);
60
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "entry.event", entry.event);
61
+ const contextIds = await ContextIdStore.getContextIds();
62
+ const newAuditEntry = {
63
+ id: RandomHelper.generateUuidV7("compact"),
64
+ dateCreated: new Date().toISOString(),
65
+ event: entry.event,
66
+ actorId: entry.actorId ?? contextIds?.[ContextIdKeys.User],
67
+ nodeId: entry.nodeId ?? contextIds?.[ContextIdKeys.Node],
68
+ organizationId: entry.organizationId ?? contextIds?.[ContextIdKeys.Organization],
69
+ tenantId: entry.tenantId ?? contextIds?.[ContextIdKeys.Tenant],
70
+ data: entry.data,
71
+ ipAddressHashes: this.hashIpAddresses(contextIds?.[HttpContextIdKeys.IpAddress]?.split("|")),
72
+ userAgent: contextIds?.[HttpContextIdKeys.UserAgent],
73
+ correlationId: contextIds?.[HttpContextIdKeys.CorrelationId]
74
+ };
75
+ try {
76
+ await this._authenticationAuditEntryEntityStorage.set(newAuditEntry);
77
+ }
78
+ catch {
79
+ // Best-effort audit logging: do not interrupt auth/admin flows if persistence fails.
80
+ }
81
+ return newAuditEntry.id;
82
+ }
83
+ /**
84
+ * Query the audit entries.
85
+ * @param options The query options.
86
+ * @param options.actorId The actor identifier to filter the audit entries, optional.
87
+ * @param options.organizationId The organization identifier to filter the audit entries, optional.
88
+ * @param options.tenantId The tenant identifier to filter the audit entries, optional.
89
+ * @param options.nodeId The node identifier to filter the audit entries, optional.
90
+ * @param options.event The audit event to filter the audit entries, optional.
91
+ * @param options.startDate The start date to filter the audit entries, optional.
92
+ * @param options.endDate The end date to filter the audit entries, optional.
93
+ * @param cursor The cursor for pagination.
94
+ * @param limit The maximum number of entries to return.
95
+ * @returns The audit entries.
96
+ */
97
+ async query(options, cursor, limit) {
98
+ const conditions = [];
99
+ if (Is.object(options)) {
100
+ if (!Is.empty(options.actorId)) {
101
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.actorId", options.actorId);
102
+ conditions.push({
103
+ property: "actorId",
104
+ value: options.actorId,
105
+ comparison: ComparisonOperator.Equals
106
+ });
107
+ }
108
+ if (!Is.empty(options.organizationId)) {
109
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.organizationId", options.organizationId);
110
+ conditions.push({
111
+ property: "organizationId",
112
+ value: options.organizationId,
113
+ comparison: ComparisonOperator.Equals
114
+ });
115
+ }
116
+ if (!Is.empty(options.tenantId)) {
117
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.tenantId", options.tenantId);
118
+ conditions.push({
119
+ property: "tenantId",
120
+ value: options.tenantId,
121
+ comparison: ComparisonOperator.Equals
122
+ });
123
+ }
124
+ if (!Is.empty(options.nodeId)) {
125
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.nodeId", options.nodeId);
126
+ conditions.push({
127
+ property: "nodeId",
128
+ value: options.nodeId,
129
+ comparison: ComparisonOperator.Equals
130
+ });
131
+ }
132
+ if (!Is.empty(options.event)) {
133
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.event", options.event);
134
+ conditions.push({
135
+ property: "event",
136
+ value: options.event,
137
+ comparison: ComparisonOperator.Equals
138
+ });
139
+ }
140
+ if (!Is.empty(options.startDate)) {
141
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.startDate", options.startDate);
142
+ conditions.push({
143
+ property: "dateCreated",
144
+ value: options.startDate,
145
+ comparison: ComparisonOperator.GreaterThanOrEqual
146
+ });
147
+ }
148
+ if (!Is.empty(options.endDate)) {
149
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.endDate", options.endDate);
150
+ conditions.push({
151
+ property: "dateCreated",
152
+ value: options.endDate,
153
+ comparison: ComparisonOperator.LessThanOrEqual
154
+ });
155
+ }
156
+ }
157
+ const result = await this._authenticationAuditEntryEntityStorage.query(conditions.length > 0 ? { conditions } : undefined, undefined, undefined, cursor, limit);
158
+ return {
159
+ entries: result.entities,
160
+ cursor: result.cursor
161
+ };
162
+ }
163
+ /**
164
+ * Hash a list of IP addresses using SHA-256.
165
+ * @param ipAddresses The IP addresses to hash.
166
+ * @returns The hexadecimal hashes of the salted IPs.
167
+ */
168
+ hashIpAddresses(ipAddresses) {
169
+ if (!Is.stringValue(this._ipHashSalt) || !Is.array(ipAddresses)) {
170
+ return undefined;
171
+ }
172
+ return ipAddresses.map(ip => {
173
+ const hash = Sha256.sum256(Converter.utf8ToBytes(`${this._ipHashSalt}:${ip}`));
174
+ return Converter.bytesToHex(hash);
175
+ });
176
+ }
177
+ }
178
+ //# sourceMappingURL=entityStorageAuthenticationAuditService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entityStorageAuthenticationAuditService.js","sourceRoot":"","sources":["../../../src/services/entityStorageAuthenticationAuditService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type {\n\tAuthAuditEvent,\n\tIAuthenticationAuditComponent,\n\tIAuthenticationAuditEntry\n} from \"@twin.org/api-auth-entity-storage-models\";\nimport { HttpContextIdKeys } from \"@twin.org/api-models\";\nimport { ContextIdStore, ContextIdKeys } from \"@twin.org/context\";\nimport {\n\tConverter,\n\tGuards,\n\tIs,\n\tRandomHelper,\n\tValidation,\n\ttype IValidationFailure\n} from \"@twin.org/core\";\nimport { Sha256 } from \"@twin.org/crypto\";\nimport { ComparisonOperator } from \"@twin.org/entity\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport type { AuthenticationAuditEntry } from \"../entities/authenticationAuditEntry.js\";\nimport type { IEntityStorageAuthenticationAuditServiceConstructorOptions } from \"../models/IEntityStorageAuthenticationAuditServiceConstructorOptions.js\";\n\n/**\n * Implementation of the authentication audit component using entity storage.\n */\nexport class EntityStorageAuthenticationAuditService implements IAuthenticationAuditComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<EntityStorageAuthenticationAuditService>();\n\n\t/**\n\t * The entity storage for authentication audit entries.\n\t * @internal\n\t */\n\tprivate readonly _authenticationAuditEntryEntityStorage: IEntityStorageConnector<AuthenticationAuditEntry>;\n\n\t/**\n\t * The server-side salt for hashing IP addresses in audit logs, if configured.\n\t * @internal\n\t */\n\tprivate readonly _ipHashSalt?: string;\n\n\t/**\n\t * Create a new instance of EntityStorageAuthenticationAuditService.\n\t * @param options The dependencies for the identity connector.\n\t */\n\tconstructor(options?: IEntityStorageAuthenticationAuditServiceConstructorOptions) {\n\t\tthis._authenticationAuditEntryEntityStorage = EntityStorageConnectorFactory.get(\n\t\t\toptions?.authenticationAuditEntryStorageType ?? \"authentication-audit-entry\"\n\t\t);\n\t\tconst salt = options?.config?.ipHashSalt?.trim();\n\t\tif (Is.stringValue(salt)) {\n\t\t\tconst validationFailures: IValidationFailure[] = [];\n\t\t\tValidation.stringValue(\n\t\t\t\tnameof(options?.config?.ipHashSalt),\n\t\t\t\tsalt,\n\t\t\t\tvalidationFailures,\n\t\t\t\tundefined,\n\t\t\t\t{ minLength: 32 }\n\t\t\t);\n\t\t\tconst entropy = new Set(salt).size;\n\t\t\tif (entropy < 8) {\n\t\t\t\tvalidationFailures.push({\n\t\t\t\t\tproperty: nameof(options?.config?.ipHashSalt),\n\t\t\t\t\treason: \"validation.saltEntropyTooLow\"\n\t\t\t\t});\n\t\t\t}\n\t\t\tValidation.asValidationError(\n\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\tnameof(options?.config?.ipHashSalt),\n\t\t\t\tvalidationFailures\n\t\t\t);\n\n\t\t\tthis._ipHashSalt = salt;\n\t\t}\n\t}\n\n\t/**\n\t * Returns the class name of the component.\n\t * @returns The class name of the component.\n\t */\n\tpublic className(): string {\n\t\treturn EntityStorageAuthenticationAuditService.CLASS_NAME;\n\t}\n\n\t/**\n\t * Create a new audit entry.\n\t * @param entry The audit entry to be logged.\n\t * @returns The unique identifier of the created audit entry.\n\t */\n\tpublic async create(\n\t\tentry: Omit<IAuthenticationAuditEntry, \"id\" | \"dateCreated\">\n\t): Promise<string> {\n\t\tGuards.object<IAuthenticationAuditEntry>(\n\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\tnameof(entry),\n\t\t\tentry\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\tnameof(entry.event),\n\t\t\tentry.event\n\t\t);\n\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\n\t\tconst newAuditEntry: AuthenticationAuditEntry = {\n\t\t\tid: RandomHelper.generateUuidV7(\"compact\"),\n\t\t\tdateCreated: new Date().toISOString(),\n\t\t\tevent: entry.event,\n\t\t\tactorId: entry.actorId ?? contextIds?.[ContextIdKeys.User],\n\t\t\tnodeId: entry.nodeId ?? contextIds?.[ContextIdKeys.Node],\n\t\t\torganizationId: entry.organizationId ?? contextIds?.[ContextIdKeys.Organization],\n\t\t\ttenantId: entry.tenantId ?? contextIds?.[ContextIdKeys.Tenant],\n\t\t\tdata: entry.data,\n\t\t\tipAddressHashes: this.hashIpAddresses(contextIds?.[HttpContextIdKeys.IpAddress]?.split(\"|\")),\n\t\t\tuserAgent: contextIds?.[HttpContextIdKeys.UserAgent],\n\t\t\tcorrelationId: contextIds?.[HttpContextIdKeys.CorrelationId]\n\t\t};\n\n\t\ttry {\n\t\t\tawait this._authenticationAuditEntryEntityStorage.set(newAuditEntry);\n\t\t} catch {\n\t\t\t// Best-effort audit logging: do not interrupt auth/admin flows if persistence fails.\n\t\t}\n\n\t\treturn newAuditEntry.id;\n\t}\n\n\t/**\n\t * Query the audit entries.\n\t * @param options The query options.\n\t * @param options.actorId The actor identifier to filter the audit entries, optional.\n\t * @param options.organizationId The organization identifier to filter the audit entries, optional.\n\t * @param options.tenantId The tenant identifier to filter the audit entries, optional.\n\t * @param options.nodeId The node identifier to filter the audit entries, optional.\n\t * @param options.event The audit event to filter the audit entries, optional.\n\t * @param options.startDate The start date to filter the audit entries, optional.\n\t * @param options.endDate The end date to filter the audit entries, optional.\n\t * @param cursor The cursor for pagination.\n\t * @param limit The maximum number of entries to return.\n\t * @returns The audit entries.\n\t */\n\tpublic async query(\n\t\toptions?: {\n\t\t\tactorId?: string;\n\t\t\torganizationId?: string;\n\t\t\ttenantId?: string;\n\t\t\tnodeId?: string;\n\t\t\tevent?: AuthAuditEvent | string;\n\t\t\tstartDate?: string;\n\t\t\tendDate?: string;\n\t\t},\n\t\tcursor?: string,\n\t\tlimit?: number\n\t): Promise<{\n\t\tentries: IAuthenticationAuditEntry[];\n\t\tcursor?: string;\n\t}> {\n\t\tconst conditions: {\n\t\t\tproperty: string;\n\t\t\tvalue: string;\n\t\t\tcomparison: (typeof ComparisonOperator)[keyof typeof ComparisonOperator];\n\t\t}[] = [];\n\n\t\tif (Is.object(options)) {\n\t\t\tif (!Is.empty(options.actorId)) {\n\t\t\t\tGuards.stringValue(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\tnameof(options.actorId),\n\t\t\t\t\toptions.actorId\n\t\t\t\t);\n\t\t\t\tconditions.push({\n\t\t\t\t\tproperty: \"actorId\",\n\t\t\t\t\tvalue: options.actorId,\n\t\t\t\t\tcomparison: ComparisonOperator.Equals\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (!Is.empty(options.organizationId)) {\n\t\t\t\tGuards.stringValue(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\tnameof(options.organizationId),\n\t\t\t\t\toptions.organizationId\n\t\t\t\t);\n\t\t\t\tconditions.push({\n\t\t\t\t\tproperty: \"organizationId\",\n\t\t\t\t\tvalue: options.organizationId,\n\t\t\t\t\tcomparison: ComparisonOperator.Equals\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (!Is.empty(options.tenantId)) {\n\t\t\t\tGuards.stringValue(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\tnameof(options.tenantId),\n\t\t\t\t\toptions.tenantId\n\t\t\t\t);\n\t\t\t\tconditions.push({\n\t\t\t\t\tproperty: \"tenantId\",\n\t\t\t\t\tvalue: options.tenantId,\n\t\t\t\t\tcomparison: ComparisonOperator.Equals\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (!Is.empty(options.nodeId)) {\n\t\t\t\tGuards.stringValue(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\tnameof(options.nodeId),\n\t\t\t\t\toptions.nodeId\n\t\t\t\t);\n\t\t\t\tconditions.push({\n\t\t\t\t\tproperty: \"nodeId\",\n\t\t\t\t\tvalue: options.nodeId,\n\t\t\t\t\tcomparison: ComparisonOperator.Equals\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (!Is.empty(options.event)) {\n\t\t\t\tGuards.stringValue(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\tnameof(options.event),\n\t\t\t\t\toptions.event\n\t\t\t\t);\n\t\t\t\tconditions.push({\n\t\t\t\t\tproperty: \"event\",\n\t\t\t\t\tvalue: options.event,\n\t\t\t\t\tcomparison: ComparisonOperator.Equals\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (!Is.empty(options.startDate)) {\n\t\t\t\tGuards.stringValue(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\tnameof(options.startDate),\n\t\t\t\t\toptions.startDate\n\t\t\t\t);\n\t\t\t\tconditions.push({\n\t\t\t\t\tproperty: \"dateCreated\",\n\t\t\t\t\tvalue: options.startDate,\n\t\t\t\t\tcomparison: ComparisonOperator.GreaterThanOrEqual\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (!Is.empty(options.endDate)) {\n\t\t\t\tGuards.stringValue(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\tnameof(options.endDate),\n\t\t\t\t\toptions.endDate\n\t\t\t\t);\n\t\t\t\tconditions.push({\n\t\t\t\t\tproperty: \"dateCreated\",\n\t\t\t\t\tvalue: options.endDate,\n\t\t\t\t\tcomparison: ComparisonOperator.LessThanOrEqual\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tconst result = await this._authenticationAuditEntryEntityStorage.query(\n\t\t\tconditions.length > 0 ? { conditions } : undefined,\n\t\t\tundefined,\n\t\t\tundefined,\n\t\t\tcursor,\n\t\t\tlimit\n\t\t);\n\n\t\treturn {\n\t\t\tentries: result.entities as IAuthenticationAuditEntry[],\n\t\t\tcursor: result.cursor\n\t\t};\n\t}\n\n\t/**\n\t * Hash a list of IP addresses using SHA-256.\n\t * @param ipAddresses The IP addresses to hash.\n\t * @returns The hexadecimal hashes of the salted IPs.\n\t */\n\tprivate hashIpAddresses(ipAddresses: string[] | undefined): string[] | undefined {\n\t\tif (!Is.stringValue(this._ipHashSalt) || !Is.array(ipAddresses)) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn ipAddresses.map(ip => {\n\t\t\tconst hash = Sha256.sum256(Converter.utf8ToBytes(`${this._ipHashSalt}:${ip}`));\n\t\t\treturn Converter.bytesToHex(hash);\n\t\t});\n\t}\n}\n"]}