@3sixty/api-auth-service 0.20.0-next.0 → 0.20.0-next.1

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 (73) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +24 -2
  3. package/dist/es/entities/authenticationAuditEntry.js +121 -0
  4. package/dist/es/entities/authenticationAuditEntry.js.map +1 -0
  5. package/dist/es/entities/authenticationRateEntry.js +37 -0
  6. package/dist/es/entities/authenticationRateEntry.js.map +1 -0
  7. package/dist/es/index.js +17 -0
  8. package/dist/es/index.js.map +1 -0
  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/ITenantOverrideProcessorConfig.js +4 -0
  18. package/dist/es/models/ITenantOverrideProcessorConfig.js.map +1 -0
  19. package/dist/es/models/ITenantOverrideProcessorConstructorOptions.js +2 -0
  20. package/dist/es/models/ITenantOverrideProcessorConstructorOptions.js.map +1 -0
  21. package/dist/es/processors/tenantOverrideProcessor.js +144 -0
  22. package/dist/es/processors/tenantOverrideProcessor.js.map +1 -0
  23. package/dist/es/restEntryPoints.js +13 -0
  24. package/dist/es/restEntryPoints.js.map +1 -0
  25. package/dist/es/routes/entityStorageAuthenticationAuditRoutes.js +355 -0
  26. package/dist/es/routes/entityStorageAuthenticationAuditRoutes.js.map +1 -0
  27. package/dist/es/schema.js +13 -0
  28. package/dist/es/schema.js.map +1 -0
  29. package/dist/es/services/entityStorageAuthenticationAuditService.js +240 -0
  30. package/dist/es/services/entityStorageAuthenticationAuditService.js.map +1 -0
  31. package/dist/es/services/entityStorageAuthenticationRateService.js +213 -0
  32. package/dist/es/services/entityStorageAuthenticationRateService.js.map +1 -0
  33. package/dist/types/entities/authenticationAuditEntry.d.ts +49 -0
  34. package/dist/types/entities/authenticationRateEntry.d.ts +17 -0
  35. package/dist/types/index.d.ts +14 -0
  36. package/dist/types/models/IEntityStorageAuthenticationAuditServiceConfig.d.ts +9 -0
  37. package/dist/types/models/IEntityStorageAuthenticationAuditServiceConstructorOptions.d.ts +15 -0
  38. package/dist/types/models/IEntityStorageAuthenticationRateServiceConfig.d.ts +10 -0
  39. package/dist/types/models/IEntityStorageAuthenticationRateServiceConstructorOptions.d.ts +25 -0
  40. package/dist/types/models/ITenantOverrideProcessorConfig.d.ts +19 -0
  41. package/dist/types/models/ITenantOverrideProcessorConstructorOptions.d.ts +20 -0
  42. package/dist/types/processors/tenantOverrideProcessor.d.ts +48 -0
  43. package/dist/types/restEntryPoints.d.ts +5 -0
  44. package/dist/types/routes/entityStorageAuthenticationAuditRoutes.d.ts +54 -0
  45. package/dist/types/schema.d.ts +4 -0
  46. package/dist/types/services/entityStorageAuthenticationAuditService.d.ts +72 -0
  47. package/dist/types/services/entityStorageAuthenticationRateService.d.ts +60 -0
  48. package/docs/changelog.md +1633 -0
  49. package/docs/examples.md +137 -0
  50. package/docs/open-api/spec.json +911 -0
  51. package/docs/reference/classes/AuthenticationAuditEntry.md +101 -0
  52. package/docs/reference/classes/AuthenticationRateEntry.md +37 -0
  53. package/docs/reference/classes/EntityStorageAuthenticationAuditService.md +241 -0
  54. package/docs/reference/classes/EntityStorageAuthenticationRateService.md +227 -0
  55. package/docs/reference/classes/TenantOverrideProcessor.md +129 -0
  56. package/docs/reference/functions/authenticationAuditCreate.md +37 -0
  57. package/docs/reference/functions/authenticationAuditGet.md +31 -0
  58. package/docs/reference/functions/authenticationAuditQuery.md +31 -0
  59. package/docs/reference/functions/authenticationAuditRemove.md +31 -0
  60. package/docs/reference/functions/authenticationAuditUpdate.md +31 -0
  61. package/docs/reference/functions/generateRestRoutesAuthenticationAudit.md +25 -0
  62. package/docs/reference/functions/initSchema.md +9 -0
  63. package/docs/reference/index.md +33 -0
  64. package/docs/reference/interfaces/IEntityStorageAuthenticationAuditServiceConfig.md +11 -0
  65. package/docs/reference/interfaces/IEntityStorageAuthenticationAuditServiceConstructorOptions.md +25 -0
  66. package/docs/reference/interfaces/IEntityStorageAuthenticationRateServiceConfig.md +17 -0
  67. package/docs/reference/interfaces/IEntityStorageAuthenticationRateServiceConstructorOptions.md +53 -0
  68. package/docs/reference/interfaces/ITenantOverrideProcessorConfig.md +39 -0
  69. package/docs/reference/interfaces/ITenantOverrideProcessorConstructorOptions.md +39 -0
  70. package/docs/reference/variables/restEntryPoints.md +5 -0
  71. package/docs/reference/variables/tagsAuthenticationAudit.md +5 -0
  72. package/locales/en.json +22 -0
  73. package/package.json +63 -6
@@ -0,0 +1,240 @@
1
+ import { HttpContextIdKeys } from "@3sixty/api-models";
2
+ import { ContextIdStore, ContextIdKeys } from "@3sixty/context";
3
+ import { Converter, GeneralError, Guards, Is, NotFoundError, RandomHelper, Validation } from "@3sixty/core";
4
+ import { Sha256 } from "@3sixty/crypto";
5
+ import { ComparisonOperator } from "@3sixty/entity";
6
+ import { EntityStorageConnectorFactory } from "@3sixty/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
+ * Get an audit entry by id.
85
+ * @param id The unique identifier of the audit entry.
86
+ * @returns The audit entry.
87
+ */
88
+ async get(id) {
89
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "id", id);
90
+ try {
91
+ const entry = await this._authenticationAuditEntryEntityStorage.get(id);
92
+ if (Is.empty(entry)) {
93
+ throw new NotFoundError(EntityStorageAuthenticationAuditService.CLASS_NAME, "auditEntryNotFound", id);
94
+ }
95
+ return entry;
96
+ }
97
+ catch (error) {
98
+ throw new GeneralError(EntityStorageAuthenticationAuditService.CLASS_NAME, "getAuditEntryFailed", undefined, error);
99
+ }
100
+ }
101
+ /**
102
+ * Update an audit entry.
103
+ * @param id The unique identifier of the audit entry to update.
104
+ * @param entry The fields to update on the audit entry.
105
+ * @returns A promise that resolves when the audit entry has been updated.
106
+ */
107
+ async update(id, entry) {
108
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "id", id);
109
+ Guards.object(EntityStorageAuthenticationAuditService.CLASS_NAME, "entry", entry);
110
+ try {
111
+ const existing = await this._authenticationAuditEntryEntityStorage.get(id);
112
+ if (Is.empty(existing)) {
113
+ throw new NotFoundError(EntityStorageAuthenticationAuditService.CLASS_NAME, "auditEntryNotFound", id);
114
+ }
115
+ await this._authenticationAuditEntryEntityStorage.set({
116
+ ...existing,
117
+ ...entry,
118
+ id,
119
+ dateCreated: existing.dateCreated
120
+ });
121
+ }
122
+ catch (error) {
123
+ throw new GeneralError(EntityStorageAuthenticationAuditService.CLASS_NAME, "updateAuditEntryFailed", undefined, error);
124
+ }
125
+ }
126
+ /**
127
+ * Remove an audit entry.
128
+ * @param id The unique identifier of the audit entry to remove.
129
+ * @returns A promise that resolves when the audit entry has been removed.
130
+ */
131
+ async remove(id) {
132
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "id", id);
133
+ try {
134
+ const existing = await this._authenticationAuditEntryEntityStorage.get(id);
135
+ if (Is.empty(existing)) {
136
+ throw new NotFoundError(EntityStorageAuthenticationAuditService.CLASS_NAME, "auditEntryNotFound", id);
137
+ }
138
+ await this._authenticationAuditEntryEntityStorage.remove(id);
139
+ }
140
+ catch (error) {
141
+ throw new GeneralError(EntityStorageAuthenticationAuditService.CLASS_NAME, "removeAuditEntryFailed", undefined, error);
142
+ }
143
+ }
144
+ /**
145
+ * Query the audit entries.
146
+ * @param options The query options.
147
+ * @param options.actorId The actor identifier to filter the audit entries, optional.
148
+ * @param options.organizationId The organization identifier to filter the audit entries, optional.
149
+ * @param options.tenantId The tenant identifier to filter the audit entries, optional.
150
+ * @param options.nodeId The node identifier to filter the audit entries, optional.
151
+ * @param options.event The audit event to filter the audit entries, optional.
152
+ * @param options.startDate The start date to filter the audit entries, optional.
153
+ * @param options.endDate The end date to filter the audit entries, optional.
154
+ * @param cursor The cursor for pagination.
155
+ * @param limit The maximum number of entries to return.
156
+ * @returns The audit entries.
157
+ */
158
+ async query(options, cursor, limit) {
159
+ const conditions = [];
160
+ if (Is.object(options)) {
161
+ if (!Is.empty(options.actorId)) {
162
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.actorId", options.actorId);
163
+ conditions.push({
164
+ property: "actorId",
165
+ value: options.actorId,
166
+ comparison: ComparisonOperator.Equals
167
+ });
168
+ }
169
+ if (!Is.empty(options.organizationId)) {
170
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.organizationId", options.organizationId);
171
+ conditions.push({
172
+ property: "organizationId",
173
+ value: options.organizationId,
174
+ comparison: ComparisonOperator.Equals
175
+ });
176
+ }
177
+ if (!Is.empty(options.tenantId)) {
178
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.tenantId", options.tenantId);
179
+ conditions.push({
180
+ property: "tenantId",
181
+ value: options.tenantId,
182
+ comparison: ComparisonOperator.Equals
183
+ });
184
+ }
185
+ if (!Is.empty(options.nodeId)) {
186
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.nodeId", options.nodeId);
187
+ conditions.push({
188
+ property: "nodeId",
189
+ value: options.nodeId,
190
+ comparison: ComparisonOperator.Equals
191
+ });
192
+ }
193
+ if (!Is.empty(options.event)) {
194
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.event", options.event);
195
+ conditions.push({
196
+ property: "event",
197
+ value: options.event,
198
+ comparison: ComparisonOperator.Equals
199
+ });
200
+ }
201
+ if (!Is.empty(options.startDate)) {
202
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.startDate", options.startDate);
203
+ conditions.push({
204
+ property: "dateCreated",
205
+ value: options.startDate,
206
+ comparison: ComparisonOperator.GreaterThanOrEqual
207
+ });
208
+ }
209
+ if (!Is.empty(options.endDate)) {
210
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.endDate", options.endDate);
211
+ conditions.push({
212
+ property: "dateCreated",
213
+ value: options.endDate,
214
+ comparison: ComparisonOperator.LessThanOrEqual
215
+ });
216
+ }
217
+ }
218
+ const result = await this._authenticationAuditEntryEntityStorage.query(conditions.length > 0 ? { conditions } : undefined, undefined, undefined, cursor, limit);
219
+ return {
220
+ entries: result.entities,
221
+ cursor: result.cursor
222
+ };
223
+ }
224
+ /**
225
+ * Hash a list of IP addresses using SHA-256.
226
+ * @param ipAddresses The IP addresses to hash.
227
+ * @returns The hexadecimal hashes of the salted IPs.
228
+ * @internal
229
+ */
230
+ hashIpAddresses(ipAddresses) {
231
+ if (!Is.stringValue(this._ipHashSalt) || !Is.array(ipAddresses)) {
232
+ return undefined;
233
+ }
234
+ return ipAddresses.map(ip => {
235
+ const hash = Sha256.sum256(Converter.utf8ToBytes(`${this._ipHashSalt}:${ip}`));
236
+ return Converter.bytesToHex(hash);
237
+ });
238
+ }
239
+ }
240
+ //# sourceMappingURL=entityStorageAuthenticationAuditService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entityStorageAuthenticationAuditService.js","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 \"@3sixty/api-auth-models\";\nimport { HttpContextIdKeys } from \"@3sixty/api-models\";\nimport { ContextIdStore, ContextIdKeys } from \"@3sixty/context\";\nimport {\n\tConverter,\n\tGeneralError,\n\tGuards,\n\tIs,\n\tNotFoundError,\n\tRandomHelper,\n\tValidation,\n\ttype IValidationFailure\n} from \"@3sixty/core\";\nimport { Sha256 } from \"@3sixty/crypto\";\nimport { ComparisonOperator } from \"@3sixty/entity\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@3sixty/entity-storage-models\";\n\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 = \"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\t\"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: \"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\t\"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\t\"entry\",\n\t\t\tentry\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\"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 * Get an audit entry by id.\n\t * @param id The unique identifier of the audit entry.\n\t * @returns The audit entry.\n\t */\n\tpublic async get(id: string): Promise<IAuthenticationAuditEntry> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, \"id\", id);\n\n\t\ttry {\n\t\t\tconst entry = await this._authenticationAuditEntryEntityStorage.get(id);\n\t\t\tif (Is.empty(entry)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\t\"auditEntryNotFound\",\n\t\t\t\t\tid\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn entry;\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\"getAuditEntryFailed\",\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 an audit entry.\n\t * @param id The unique identifier of the audit entry to update.\n\t * @param entry The fields to update on the audit entry.\n\t * @returns A promise that resolves when the audit entry has been updated.\n\t */\n\tpublic async update(\n\t\tid: string,\n\t\tentry: Partial<Omit<IAuthenticationAuditEntry, \"id\" | \"dateCreated\">>\n\t): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, \"id\", id);\n\t\tGuards.object(EntityStorageAuthenticationAuditService.CLASS_NAME, \"entry\", entry);\n\n\t\ttry {\n\t\t\tconst existing = await this._authenticationAuditEntryEntityStorage.get(id);\n\t\t\tif (Is.empty(existing)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\t\"auditEntryNotFound\",\n\t\t\t\t\tid\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tawait this._authenticationAuditEntryEntityStorage.set({\n\t\t\t\t...existing,\n\t\t\t\t...entry,\n\t\t\t\tid,\n\t\t\t\tdateCreated: existing.dateCreated\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\"updateAuditEntryFailed\",\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 an audit entry.\n\t * @param id The unique identifier of the audit entry to remove.\n\t * @returns A promise that resolves when the audit entry has been removed.\n\t */\n\tpublic async remove(id: string): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, \"id\", id);\n\n\t\ttry {\n\t\t\tconst existing = await this._authenticationAuditEntryEntityStorage.get(id);\n\t\t\tif (Is.empty(existing)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\t\"auditEntryNotFound\",\n\t\t\t\t\tid\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tawait this._authenticationAuditEntryEntityStorage.remove(id);\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\"removeAuditEntryFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\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\t\"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\t\"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\t\"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\t\"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\t\"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\t\"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\t\"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 * @internal\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"]}
@@ -0,0 +1,213 @@
1
+ import { TooManyRequestsError } from "@3sixty/api-models";
2
+ import { ComponentFactory, Converter, GeneralError, Guards, Is } from "@3sixty/core";
3
+ import { Sha256 } from "@3sixty/crypto";
4
+ import { ComparisonOperator } from "@3sixty/entity";
5
+ import { EntityStorageConnectorFactory } from "@3sixty/entity-storage-models";
6
+ /**
7
+ * Implementation of the authentication rate component using entity storage.
8
+ */
9
+ export class EntityStorageAuthenticationRateService {
10
+ /**
11
+ * Runtime name for the class.
12
+ */
13
+ static CLASS_NAME = "EntityStorageAuthenticationRateService";
14
+ /**
15
+ * Cleanup task id.
16
+ * @internal
17
+ */
18
+ static _CLEANUP_TASK_ID = "authentication-rate-cleanup";
19
+ /**
20
+ * Default cleanup interval in minutes.
21
+ * @internal
22
+ */
23
+ static _DEFAULT_CLEANUP_INTERVAL_MINUTES = 5;
24
+ /**
25
+ * Number of entries to retrieve in each cleanup page.
26
+ * @internal
27
+ */
28
+ static _CLEANUP_PAGE_SIZE = 250;
29
+ /**
30
+ * The entity storage for authentication rate entries.
31
+ * @internal
32
+ */
33
+ _authenticationRateEntryStorage;
34
+ /**
35
+ * The rate limits for each action.
36
+ * @internal
37
+ */
38
+ _actionConfigs;
39
+ /**
40
+ * The task scheduler.
41
+ * @internal
42
+ */
43
+ _taskScheduler;
44
+ /**
45
+ * The cleanup interval in minutes.
46
+ * @internal
47
+ */
48
+ _cleanupIntervalMinutes;
49
+ /**
50
+ * Platform component used to run the periodic cleanup with the correct tenant context.
51
+ * @internal
52
+ */
53
+ _platformComponent;
54
+ /**
55
+ * Create a new instance of EntityStorageAuthenticationRateService.
56
+ * @param options The constructor options.
57
+ */
58
+ constructor(options) {
59
+ this._authenticationRateEntryStorage = EntityStorageConnectorFactory.get(options?.authenticationRateEntryStorageType ?? "authentication-rate-entry");
60
+ this._actionConfigs = {};
61
+ this._taskScheduler = ComponentFactory.get(options?.taskSchedulerComponentType ?? "task-scheduler");
62
+ this._cleanupIntervalMinutes =
63
+ options?.config?.cleanupIntervalMinutes ??
64
+ EntityStorageAuthenticationRateService._DEFAULT_CLEANUP_INTERVAL_MINUTES;
65
+ this._platformComponent = ComponentFactory.get(options?.platformComponentType ?? "platform");
66
+ }
67
+ /**
68
+ * Register or update rate-limit configuration for an action.
69
+ * @param action The action name.
70
+ * @param config The action configuration.
71
+ * @returns A promise that resolves when the action configuration has been stored.
72
+ */
73
+ async registerAction(action, config) {
74
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "action", action);
75
+ Guards.object(EntityStorageAuthenticationRateService.CLASS_NAME, "config", config);
76
+ this._actionConfigs[action] = {
77
+ maxAttempts: config.maxAttempts,
78
+ windowMinutes: config.windowMinutes
79
+ };
80
+ }
81
+ /**
82
+ * Unregister rate-limit configuration for an action.
83
+ * @param action The action name.
84
+ * @returns A promise that resolves when the action configuration has been removed.
85
+ */
86
+ async unregisterAction(action) {
87
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "action", action);
88
+ delete this._actionConfigs[action];
89
+ }
90
+ /**
91
+ * Returns the class name of the component.
92
+ * @returns The class name of the component.
93
+ */
94
+ className() {
95
+ return EntityStorageAuthenticationRateService.CLASS_NAME;
96
+ }
97
+ /**
98
+ * The service needs to be started when the application is initialized.
99
+ * @param nodeLoggingComponentType The node logging component type.
100
+ * @returns A promise that resolves when the periodic cleanup task has been registered.
101
+ */
102
+ async start(nodeLoggingComponentType) {
103
+ await this._taskScheduler.addTask(EntityStorageAuthenticationRateService._CLEANUP_TASK_ID, [
104
+ {
105
+ intervalMinutes: this._cleanupIntervalMinutes
106
+ }
107
+ ], async () => this.cleanupExpiredEntries());
108
+ }
109
+ /**
110
+ * The component needs to be stopped when the node is closed.
111
+ * @param nodeLoggingComponentType The node logging component type.
112
+ * @returns A promise that resolves when the periodic cleanup task has been removed.
113
+ */
114
+ async stop(nodeLoggingComponentType) {
115
+ await this._taskScheduler.removeTask(EntityStorageAuthenticationRateService._CLEANUP_TASK_ID);
116
+ }
117
+ /**
118
+ * Check the authentication rate for a given action and identifier.
119
+ * @param action The action to be checked.
120
+ * @param identifier The identifier to be checked.
121
+ * @returns The rate entry id.
122
+ */
123
+ async check(action, identifier) {
124
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "action", action);
125
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "identifier", identifier);
126
+ const actionConfig = this._actionConfigs[action];
127
+ if (!Is.object(actionConfig)) {
128
+ throw new GeneralError(EntityStorageAuthenticationRateService.CLASS_NAME, "actionConfigMissing", {
129
+ action
130
+ });
131
+ }
132
+ const hashedIdentifier = Converter.bytesToHex(Sha256.sum256(Converter.utf8ToBytes(identifier)));
133
+ const compositeId = `|${action}|${hashedIdentifier}|`;
134
+ const now = Date.now();
135
+ const nowIso = new Date(now).toISOString();
136
+ const windowMs = actionConfig.windowMinutes * 60 * 1000;
137
+ const cutoff = now - windowMs;
138
+ // Resolve the existing rate entry for this action + identifier pair.
139
+ const existing = await this._authenticationRateEntryStorage.get(compositeId);
140
+ // Keep only attempts inside the configured sliding window.
141
+ const activeTimestamps = (existing?.timestamps ?? []).filter(timestamp => new Date(timestamp).getTime() > cutoff);
142
+ if (activeTimestamps.length >= actionConfig.maxAttempts) {
143
+ const oldestTimestamp = new Date(activeTimestamps[0]).getTime();
144
+ const nextRequestTime = new Date(oldestTimestamp + windowMs).toISOString();
145
+ const retryAfterSeconds = Math.ceil((oldestTimestamp + windowMs - now) / 1000);
146
+ throw new TooManyRequestsError(EntityStorageAuthenticationRateService.CLASS_NAME, "rateLimitExceeded", activeTimestamps.length, nextRequestTime, {
147
+ action,
148
+ retryAfterSeconds
149
+ });
150
+ }
151
+ activeTimestamps.push(nowIso);
152
+ await this._authenticationRateEntryStorage.set({
153
+ id: compositeId,
154
+ timestamps: activeTimestamps,
155
+ dateModified: nowIso
156
+ });
157
+ return compositeId;
158
+ }
159
+ /**
160
+ * Clear the authentication rate entry for the given action and identifier.
161
+ * @param action The action to clear.
162
+ * @param identifier The identifier to clear.
163
+ * @returns A promise that resolves when the rate entry has been removed.
164
+ */
165
+ async clear(action, identifier) {
166
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "action", action);
167
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "identifier", identifier);
168
+ const hashedIdentifier = Converter.bytesToHex(Sha256.sum256(Converter.utf8ToBytes(identifier)));
169
+ const compositeId = `|${action}|${hashedIdentifier}|`;
170
+ await this._authenticationRateEntryStorage.remove(compositeId);
171
+ }
172
+ /**
173
+ * Cleanup expired rate limit entries. Runs via the platform component's execute so the work runs with
174
+ * the correct context (once in single-tenant mode, once per tenant in multi-tenant mode).
175
+ * @returns A promise that resolves when all expired entries have been removed from storage.
176
+ * @internal
177
+ */
178
+ async cleanupExpiredEntries() {
179
+ // Run via the platform component's execute so the work runs with the correct context (once in
180
+ // single-tenant mode, once per tenant in multi-tenant mode).
181
+ await this._platformComponent.execute(async () => {
182
+ const now = Date.now();
183
+ for (const action of Object.keys(this._actionConfigs)) {
184
+ const actionConfig = this._actionConfigs[action];
185
+ const windowMs = actionConfig.windowMinutes * 60 * 1000;
186
+ const cutoffIso = new Date(now - windowMs).toISOString();
187
+ let cursor;
188
+ do {
189
+ // Query by composite rate entry id prefix and expiry window for this action.
190
+ const result = await this._authenticationRateEntryStorage.query({
191
+ conditions: [
192
+ {
193
+ property: "id",
194
+ value: `|${action}|`,
195
+ comparison: ComparisonOperator.Includes
196
+ },
197
+ {
198
+ property: "dateModified",
199
+ value: cutoffIso,
200
+ comparison: ComparisonOperator.LessThanOrEqual
201
+ }
202
+ ]
203
+ }, undefined, undefined, cursor, EntityStorageAuthenticationRateService._CLEANUP_PAGE_SIZE);
204
+ for (const entity of result.entities) {
205
+ await this._authenticationRateEntryStorage.remove(entity.id);
206
+ }
207
+ cursor = result.cursor;
208
+ } while (!Is.empty(cursor));
209
+ }
210
+ });
211
+ }
212
+ }
213
+ //# sourceMappingURL=entityStorageAuthenticationRateService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entityStorageAuthenticationRateService.js","sources":["../../../src/services/entityStorageAuthenticationRateService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type {\n\tIAuthenticationRateActionConfig,\n\tIAuthenticationRateComponent\n} from \"@3sixty/api-auth-models\";\nimport { TooManyRequestsError, type IPlatformComponent } from \"@3sixty/api-models\";\nimport type { ITaskSchedulerComponent } from \"@3sixty/background-task-models\";\nimport { ComponentFactory, Converter, GeneralError, Guards, Is } from \"@3sixty/core\";\nimport { Sha256 } from \"@3sixty/crypto\";\nimport { ComparisonOperator } from \"@3sixty/entity\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@3sixty/entity-storage-models\";\n\nimport type { AuthenticationRateEntry } from \"../entities/authenticationRateEntry.js\";\nimport type { IEntityStorageAuthenticationRateServiceConstructorOptions } from \"../models/IEntityStorageAuthenticationRateServiceConstructorOptions.js\";\n\n/**\n * Implementation of the authentication rate component using entity storage.\n */\nexport class EntityStorageAuthenticationRateService implements IAuthenticationRateComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = \"EntityStorageAuthenticationRateService\";\n\n\t/**\n\t * Cleanup task id.\n\t * @internal\n\t */\n\tprivate static readonly _CLEANUP_TASK_ID = \"authentication-rate-cleanup\";\n\n\t/**\n\t * Default cleanup interval in minutes.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_CLEANUP_INTERVAL_MINUTES = 5;\n\n\t/**\n\t * Number of entries to retrieve in each cleanup page.\n\t * @internal\n\t */\n\tprivate static readonly _CLEANUP_PAGE_SIZE = 250;\n\n\t/**\n\t * The entity storage for authentication rate entries.\n\t * @internal\n\t */\n\tprivate readonly _authenticationRateEntryStorage: IEntityStorageConnector<AuthenticationRateEntry>;\n\n\t/**\n\t * The rate limits for each action.\n\t * @internal\n\t */\n\tprivate readonly _actionConfigs: { [action: string]: IAuthenticationRateActionConfig };\n\n\t/**\n\t * The task scheduler.\n\t * @internal\n\t */\n\tprivate readonly _taskScheduler: ITaskSchedulerComponent;\n\n\t/**\n\t * The cleanup interval in minutes.\n\t * @internal\n\t */\n\tprivate readonly _cleanupIntervalMinutes: number;\n\n\t/**\n\t * Platform component used to run the periodic cleanup with the correct tenant context.\n\t * @internal\n\t */\n\tprivate readonly _platformComponent: IPlatformComponent;\n\n\t/**\n\t * Create a new instance of EntityStorageAuthenticationRateService.\n\t * @param options The constructor options.\n\t */\n\tconstructor(options?: IEntityStorageAuthenticationRateServiceConstructorOptions) {\n\t\tthis._authenticationRateEntryStorage = EntityStorageConnectorFactory.get(\n\t\t\toptions?.authenticationRateEntryStorageType ?? \"authentication-rate-entry\"\n\t\t);\n\t\tthis._actionConfigs = {};\n\t\tthis._taskScheduler = ComponentFactory.get<ITaskSchedulerComponent>(\n\t\t\toptions?.taskSchedulerComponentType ?? \"task-scheduler\"\n\t\t);\n\t\tthis._cleanupIntervalMinutes =\n\t\t\toptions?.config?.cleanupIntervalMinutes ??\n\t\t\tEntityStorageAuthenticationRateService._DEFAULT_CLEANUP_INTERVAL_MINUTES;\n\t\tthis._platformComponent = ComponentFactory.get<IPlatformComponent>(\n\t\t\toptions?.platformComponentType ?? \"platform\"\n\t\t);\n\t}\n\n\t/**\n\t * Register or update rate-limit configuration for an action.\n\t * @param action The action name.\n\t * @param config The action configuration.\n\t * @returns A promise that resolves when the action configuration has been stored.\n\t */\n\tpublic async registerAction(\n\t\taction: string,\n\t\tconfig: IAuthenticationRateActionConfig\n\t): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, \"action\", action);\n\t\tGuards.object<IAuthenticationRateActionConfig>(\n\t\t\tEntityStorageAuthenticationRateService.CLASS_NAME,\n\t\t\t\"config\",\n\t\t\tconfig\n\t\t);\n\n\t\tthis._actionConfigs[action] = {\n\t\t\tmaxAttempts: config.maxAttempts,\n\t\t\twindowMinutes: config.windowMinutes\n\t\t};\n\t}\n\n\t/**\n\t * Unregister rate-limit configuration for an action.\n\t * @param action The action name.\n\t * @returns A promise that resolves when the action configuration has been removed.\n\t */\n\tpublic async unregisterAction(action: string): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, \"action\", action);\n\n\t\tdelete this._actionConfigs[action];\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 EntityStorageAuthenticationRateService.CLASS_NAME;\n\t}\n\n\t/**\n\t * The service needs to be started when the application is initialized.\n\t * @param nodeLoggingComponentType The node logging component type.\n\t * @returns A promise that resolves when the periodic cleanup task has been registered.\n\t */\n\tpublic async start(nodeLoggingComponentType?: string): Promise<void> {\n\t\tawait this._taskScheduler.addTask(\n\t\t\tEntityStorageAuthenticationRateService._CLEANUP_TASK_ID,\n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\tintervalMinutes: this._cleanupIntervalMinutes\n\t\t\t\t}\n\t\t\t],\n\t\t\tasync () => this.cleanupExpiredEntries()\n\t\t);\n\t}\n\n\t/**\n\t * The component needs to be stopped when the node is closed.\n\t * @param nodeLoggingComponentType The node logging component type.\n\t * @returns A promise that resolves when the periodic cleanup task has been removed.\n\t */\n\tpublic async stop(nodeLoggingComponentType?: string): Promise<void> {\n\t\tawait this._taskScheduler.removeTask(EntityStorageAuthenticationRateService._CLEANUP_TASK_ID);\n\t}\n\n\t/**\n\t * Check the authentication rate for a given action and identifier.\n\t * @param action The action to be checked.\n\t * @param identifier The identifier to be checked.\n\t * @returns The rate entry id.\n\t */\n\tpublic async check(action: string, identifier: string): Promise<string> {\n\t\tGuards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, \"action\", action);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationRateService.CLASS_NAME,\n\t\t\t\"identifier\",\n\t\t\tidentifier\n\t\t);\n\n\t\tconst actionConfig = this._actionConfigs[action];\n\n\t\tif (!Is.object(actionConfig)) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationRateService.CLASS_NAME,\n\t\t\t\t\"actionConfigMissing\",\n\t\t\t\t{\n\t\t\t\t\taction\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\n\t\tconst hashedIdentifier = Converter.bytesToHex(Sha256.sum256(Converter.utf8ToBytes(identifier)));\n\t\tconst compositeId = `|${action}|${hashedIdentifier}|`;\n\n\t\tconst now = Date.now();\n\t\tconst nowIso = new Date(now).toISOString();\n\t\tconst windowMs = actionConfig.windowMinutes * 60 * 1000;\n\t\tconst cutoff = now - windowMs;\n\n\t\t// Resolve the existing rate entry for this action + identifier pair.\n\t\tconst existing = await this._authenticationRateEntryStorage.get(compositeId);\n\t\t// Keep only attempts inside the configured sliding window.\n\t\tconst activeTimestamps = (existing?.timestamps ?? []).filter(\n\t\t\ttimestamp => new Date(timestamp).getTime() > cutoff\n\t\t);\n\n\t\tif (activeTimestamps.length >= actionConfig.maxAttempts) {\n\t\t\tconst oldestTimestamp = new Date(activeTimestamps[0]).getTime();\n\t\t\tconst nextRequestTime = new Date(oldestTimestamp + windowMs).toISOString();\n\t\t\tconst retryAfterSeconds = Math.ceil((oldestTimestamp + windowMs - now) / 1000);\n\n\t\t\tthrow new TooManyRequestsError(\n\t\t\t\tEntityStorageAuthenticationRateService.CLASS_NAME,\n\t\t\t\t\"rateLimitExceeded\",\n\t\t\t\tactiveTimestamps.length,\n\t\t\t\tnextRequestTime,\n\t\t\t\t{\n\t\t\t\t\taction,\n\t\t\t\t\tretryAfterSeconds\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\n\t\tactiveTimestamps.push(nowIso);\n\n\t\tawait this._authenticationRateEntryStorage.set({\n\t\t\tid: compositeId,\n\t\t\ttimestamps: activeTimestamps,\n\t\t\tdateModified: nowIso\n\t\t});\n\n\t\treturn compositeId;\n\t}\n\n\t/**\n\t * Clear the authentication rate entry for the given action and identifier.\n\t * @param action The action to clear.\n\t * @param identifier The identifier to clear.\n\t * @returns A promise that resolves when the rate entry has been removed.\n\t */\n\tpublic async clear(action: string, identifier: string): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, \"action\", action);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationRateService.CLASS_NAME,\n\t\t\t\"identifier\",\n\t\t\tidentifier\n\t\t);\n\n\t\tconst hashedIdentifier = Converter.bytesToHex(Sha256.sum256(Converter.utf8ToBytes(identifier)));\n\t\tconst compositeId = `|${action}|${hashedIdentifier}|`;\n\n\t\tawait this._authenticationRateEntryStorage.remove(compositeId);\n\t}\n\n\t/**\n\t * Cleanup expired rate limit entries. Runs via the platform component's execute so the work runs with\n\t * the correct context (once in single-tenant mode, once per tenant in multi-tenant mode).\n\t * @returns A promise that resolves when all expired entries have been removed from storage.\n\t * @internal\n\t */\n\tprivate async cleanupExpiredEntries(): Promise<void> {\n\t\t// Run via the platform component's execute so the work runs with the correct context (once in\n\t\t// single-tenant mode, once per tenant in multi-tenant mode).\n\t\tawait this._platformComponent.execute(async () => {\n\t\t\tconst now = Date.now();\n\n\t\t\tfor (const action of Object.keys(this._actionConfigs)) {\n\t\t\t\tconst actionConfig = this._actionConfigs[action];\n\t\t\t\tconst windowMs = actionConfig.windowMinutes * 60 * 1000;\n\t\t\t\tconst cutoffIso = new Date(now - windowMs).toISOString();\n\t\t\t\tlet cursor: string | undefined;\n\n\t\t\t\tdo {\n\t\t\t\t\t// Query by composite rate entry id prefix and expiry window for this action.\n\t\t\t\t\tconst result = await this._authenticationRateEntryStorage.query(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tconditions: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tproperty: \"id\",\n\t\t\t\t\t\t\t\t\tvalue: `|${action}|`,\n\t\t\t\t\t\t\t\t\tcomparison: ComparisonOperator.Includes\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tproperty: \"dateModified\",\n\t\t\t\t\t\t\t\t\tvalue: cutoffIso,\n\t\t\t\t\t\t\t\t\tcomparison: ComparisonOperator.LessThanOrEqual\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\tcursor,\n\t\t\t\t\t\tEntityStorageAuthenticationRateService._CLEANUP_PAGE_SIZE\n\t\t\t\t\t);\n\n\t\t\t\t\tfor (const entity of result.entities as AuthenticationRateEntry[]) {\n\t\t\t\t\t\tawait this._authenticationRateEntryStorage.remove(entity.id);\n\t\t\t\t\t}\n\n\t\t\t\t\tcursor = result.cursor;\n\t\t\t\t} while (!Is.empty(cursor));\n\t\t\t}\n\t\t});\n\t}\n}\n"]}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Class defining the storage for authentication audit entries.
3
+ */
4
+ export declare class AuthenticationAuditEntry {
5
+ /**
6
+ * The unique identifier for the audit entry.
7
+ */
8
+ id: string;
9
+ /**
10
+ * The timestamp of the audit entry in ISO 8601 format.
11
+ */
12
+ dateCreated: string;
13
+ /**
14
+ * The audit event that occurred.
15
+ */
16
+ event: string;
17
+ /**
18
+ * The actor identifier, could be e-mail, username, or other unique identifier.
19
+ */
20
+ actorId?: string;
21
+ /**
22
+ * The node identifier associated with the audit entry, if applicable.
23
+ */
24
+ nodeId?: string;
25
+ /**
26
+ * The organization identifier associated with the audit entry, if applicable.
27
+ */
28
+ organizationId?: string;
29
+ /**
30
+ * The tenant identifier associated with the audit entry, if applicable.
31
+ */
32
+ tenantId?: string;
33
+ /**
34
+ * The hashed IP addresses of the client.
35
+ */
36
+ ipAddressHashes?: string[];
37
+ /**
38
+ * The user agent string of the client.
39
+ */
40
+ userAgent?: string;
41
+ /**
42
+ * The correlation ID for request tracing.
43
+ */
44
+ correlationId?: string;
45
+ /**
46
+ * Additional data related to the audit entry, such as IP address, user agent, etc.
47
+ */
48
+ data?: unknown;
49
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Class defining the storage for authentication rate entries.
3
+ */
4
+ export declare class AuthenticationRateEntry {
5
+ /**
6
+ * The id for the rate entry.
7
+ */
8
+ id: string;
9
+ /**
10
+ * Array of ISO date strings representing timestamps of failed attempts.
11
+ */
12
+ timestamps: string[];
13
+ /**
14
+ * Last modification time in ISO date format.
15
+ */
16
+ dateModified: string;
17
+ }
@@ -0,0 +1,14 @@
1
+ export * from "./entities/authenticationAuditEntry.js";
2
+ export * from "./entities/authenticationRateEntry.js";
3
+ export * from "./models/IEntityStorageAuthenticationAuditServiceConfig.js";
4
+ export * from "./models/IEntityStorageAuthenticationAuditServiceConstructorOptions.js";
5
+ export * from "./models/IEntityStorageAuthenticationRateServiceConfig.js";
6
+ export * from "./models/IEntityStorageAuthenticationRateServiceConstructorOptions.js";
7
+ export * from "./models/ITenantOverrideProcessorConfig.js";
8
+ export * from "./models/ITenantOverrideProcessorConstructorOptions.js";
9
+ export * from "./processors/tenantOverrideProcessor.js";
10
+ export * from "./restEntryPoints.js";
11
+ export * from "./routes/entityStorageAuthenticationAuditRoutes.js";
12
+ export * from "./schema.js";
13
+ export * from "./services/entityStorageAuthenticationAuditService.js";
14
+ export * from "./services/entityStorageAuthenticationRateService.js";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Config for the EntityStorageAuthenticationAuditService constructor.
3
+ */
4
+ export interface IEntityStorageAuthenticationAuditServiceConfig {
5
+ /**
6
+ * The server-side salt for hashing IP addresses in audit logs, if configured.
7
+ */
8
+ ipHashSalt?: string;
9
+ }
@@ -0,0 +1,15 @@
1
+ import type { IEntityStorageAuthenticationAuditServiceConfig } from "./IEntityStorageAuthenticationAuditServiceConfig.js";
2
+ /**
3
+ * Options for the EntityStorageAuthenticationAuditService constructor.
4
+ */
5
+ export interface IEntityStorageAuthenticationAuditServiceConstructorOptions {
6
+ /**
7
+ * The entity storage for the audit entries.
8
+ * @default authentication-audit-entry
9
+ */
10
+ authenticationAuditEntryStorageType?: string;
11
+ /**
12
+ * The configuration for the authentication audit service.
13
+ */
14
+ config?: IEntityStorageAuthenticationAuditServiceConfig;
15
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Configuration for the entity storage authentication rate service.
3
+ */
4
+ export interface IEntityStorageAuthenticationRateServiceConfig {
5
+ /**
6
+ * Interval between cleanup runs in minutes.
7
+ * @default 5
8
+ */
9
+ cleanupIntervalMinutes?: number;
10
+ }