@twin.org/api-auth-entity-storage-service 0.0.3-next.4 → 0.0.3-next.41

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 (104) hide show
  1. package/README.md +2 -2
  2. package/dist/es/entities/authenticationAuditEntry.js +101 -0
  3. package/dist/es/entities/authenticationAuditEntry.js.map +1 -0
  4. package/dist/es/entities/authenticationRateEntry.js +37 -0
  5. package/dist/es/entities/authenticationRateEntry.js.map +1 -0
  6. package/dist/es/entities/authenticationUser.js +17 -1
  7. package/dist/es/entities/authenticationUser.js.map +1 -1
  8. package/dist/es/index.js +11 -1
  9. package/dist/es/index.js.map +1 -1
  10. package/dist/es/models/IAuthHeaderProcessorConstructorOptions.js.map +1 -1
  11. package/dist/es/models/IEntityStorageAuthenticationAdminServiceConstructorOptions.js.map +1 -1
  12. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConfig.js +4 -0
  13. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConfig.js.map +1 -0
  14. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConstructorOptions.js +2 -0
  15. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConstructorOptions.js.map +1 -0
  16. package/dist/es/models/IEntityStorageAuthenticationRateServiceConfig.js +2 -0
  17. package/dist/es/models/IEntityStorageAuthenticationRateServiceConfig.js.map +1 -0
  18. package/dist/es/models/IEntityStorageAuthenticationRateServiceConstructorOptions.js +2 -0
  19. package/dist/es/models/IEntityStorageAuthenticationRateServiceConstructorOptions.js.map +1 -0
  20. package/dist/es/models/IEntityStorageAuthenticationServiceConfig.js +0 -2
  21. package/dist/es/models/IEntityStorageAuthenticationServiceConfig.js.map +1 -1
  22. package/dist/es/models/IEntityStorageAuthenticationServiceConstructorOptions.js.map +1 -1
  23. package/dist/es/processors/authHeaderProcessor.js +62 -10
  24. package/dist/es/processors/authHeaderProcessor.js.map +1 -1
  25. package/dist/es/restEntryPoints.js +14 -0
  26. package/dist/es/restEntryPoints.js.map +1 -1
  27. package/dist/es/routes/entityStorageAuthenticationAdminRoutes.js +362 -0
  28. package/dist/es/routes/entityStorageAuthenticationAdminRoutes.js.map +1 -0
  29. package/dist/es/routes/entityStorageAuthenticationAuditRoutes.js +174 -0
  30. package/dist/es/routes/entityStorageAuthenticationAuditRoutes.js.map +1 -0
  31. package/dist/es/routes/entityStorageAuthenticationRoutes.js +20 -21
  32. package/dist/es/routes/entityStorageAuthenticationRoutes.js.map +1 -1
  33. package/dist/es/schema.js +4 -0
  34. package/dist/es/schema.js.map +1 -1
  35. package/dist/es/services/entityStorageAuthenticationAdminService.js +161 -55
  36. package/dist/es/services/entityStorageAuthenticationAdminService.js.map +1 -1
  37. package/dist/es/services/entityStorageAuthenticationAuditService.js +179 -0
  38. package/dist/es/services/entityStorageAuthenticationAuditService.js.map +1 -0
  39. package/dist/es/services/entityStorageAuthenticationRateService.js +202 -0
  40. package/dist/es/services/entityStorageAuthenticationRateService.js.map +1 -0
  41. package/dist/es/services/entityStorageAuthenticationService.js +200 -14
  42. package/dist/es/services/entityStorageAuthenticationService.js.map +1 -1
  43. package/dist/es/utils/passwordHelper.js +45 -16
  44. package/dist/es/utils/passwordHelper.js.map +1 -1
  45. package/dist/es/utils/tokenHelper.js +45 -21
  46. package/dist/es/utils/tokenHelper.js.map +1 -1
  47. package/dist/types/entities/authenticationAuditEntry.d.ts +49 -0
  48. package/dist/types/entities/authenticationRateEntry.d.ts +17 -0
  49. package/dist/types/entities/authenticationUser.d.ts +8 -0
  50. package/dist/types/index.d.ts +11 -1
  51. package/dist/types/models/IAuthHeaderProcessorConstructorOptions.d.ts +14 -0
  52. package/dist/types/models/IEntityStorageAuthenticationAdminServiceConstructorOptions.d.ts +5 -0
  53. package/dist/types/models/IEntityStorageAuthenticationAuditServiceConfig.d.ts +9 -0
  54. package/dist/types/models/IEntityStorageAuthenticationAuditServiceConstructorOptions.d.ts +15 -0
  55. package/dist/types/models/IEntityStorageAuthenticationRateServiceConfig.d.ts +10 -0
  56. package/dist/types/models/IEntityStorageAuthenticationRateServiceConstructorOptions.d.ts +20 -0
  57. package/dist/types/models/IEntityStorageAuthenticationServiceConfig.d.ts +22 -1
  58. package/dist/types/models/IEntityStorageAuthenticationServiceConstructorOptions.d.ts +17 -3
  59. package/dist/types/processors/authHeaderProcessor.d.ts +1 -1
  60. package/dist/types/routes/entityStorageAuthenticationAdminRoutes.d.ts +61 -0
  61. package/dist/types/routes/entityStorageAuthenticationAuditRoutes.d.ts +29 -0
  62. package/dist/types/services/entityStorageAuthenticationAdminService.d.ts +23 -6
  63. package/dist/types/services/entityStorageAuthenticationAuditService.d.ts +53 -0
  64. package/dist/types/services/entityStorageAuthenticationRateService.d.ts +60 -0
  65. package/dist/types/services/entityStorageAuthenticationService.d.ts +8 -3
  66. package/dist/types/utils/passwordHelper.d.ts +13 -5
  67. package/dist/types/utils/tokenHelper.d.ts +9 -2
  68. package/docs/changelog.md +674 -64
  69. package/docs/examples.md +178 -1
  70. package/docs/reference/classes/AuthHeaderProcessor.md +10 -10
  71. package/docs/reference/classes/AuthenticationAuditEntry.md +101 -0
  72. package/docs/reference/classes/AuthenticationRateEntry.md +37 -0
  73. package/docs/reference/classes/AuthenticationUser.md +21 -5
  74. package/docs/reference/classes/EntityStorageAuthenticationAdminService.md +78 -18
  75. package/docs/reference/classes/EntityStorageAuthenticationAuditService.md +157 -0
  76. package/docs/reference/classes/EntityStorageAuthenticationRateService.md +227 -0
  77. package/docs/reference/classes/EntityStorageAuthenticationService.md +36 -16
  78. package/docs/reference/classes/PasswordHelper.md +37 -12
  79. package/docs/reference/classes/TokenHelper.md +44 -8
  80. package/docs/reference/functions/authenticationAdminCreateUser.md +31 -0
  81. package/docs/reference/functions/authenticationAdminGetUser.md +31 -0
  82. package/docs/reference/functions/authenticationAdminGetUserByIdentity.md +31 -0
  83. package/docs/reference/functions/authenticationAdminRemoveUser.md +31 -0
  84. package/docs/reference/functions/authenticationAdminUpdateUser.md +31 -0
  85. package/docs/reference/functions/authenticationAdminUpdateUserPassword.md +31 -0
  86. package/docs/reference/functions/authenticationAuditCreate.md +31 -0
  87. package/docs/reference/functions/authenticationAuditQuery.md +31 -0
  88. package/docs/reference/functions/generateRestRoutesAuthenticationAdmin.md +25 -0
  89. package/docs/reference/functions/generateRestRoutesAuthenticationAudit.md +25 -0
  90. package/docs/reference/index.md +20 -0
  91. package/docs/reference/interfaces/IAuthHeaderProcessorConfig.md +4 -4
  92. package/docs/reference/interfaces/IAuthHeaderProcessorConstructorOptions.md +40 -4
  93. package/docs/reference/interfaces/IEntityStorageAuthenticationAdminServiceConfig.md +2 -2
  94. package/docs/reference/interfaces/IEntityStorageAuthenticationAdminServiceConstructorOptions.md +18 -4
  95. package/docs/reference/interfaces/IEntityStorageAuthenticationAuditServiceConfig.md +11 -0
  96. package/docs/reference/interfaces/IEntityStorageAuthenticationAuditServiceConstructorOptions.md +25 -0
  97. package/docs/reference/interfaces/IEntityStorageAuthenticationRateServiceConfig.md +17 -0
  98. package/docs/reference/interfaces/IEntityStorageAuthenticationRateServiceConstructorOptions.md +39 -0
  99. package/docs/reference/interfaces/IEntityStorageAuthenticationServiceConfig.md +61 -5
  100. package/docs/reference/interfaces/IEntityStorageAuthenticationServiceConstructorOptions.md +46 -10
  101. package/docs/reference/variables/tagsAuthenticationAdmin.md +5 -0
  102. package/docs/reference/variables/tagsAuthenticationAudit.md +5 -0
  103. package/locales/en.json +17 -3
  104. package/package.json +8 -7
@@ -0,0 +1,179 @@
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
+ * @internal
168
+ */
169
+ hashIpAddresses(ipAddresses) {
170
+ if (!Is.stringValue(this._ipHashSalt) || !Is.array(ipAddresses)) {
171
+ return undefined;
172
+ }
173
+ return ipAddresses.map(ip => {
174
+ const hash = Sha256.sum256(Converter.utf8ToBytes(`${this._ipHashSalt}:${ip}`));
175
+ return Converter.bytesToHex(hash);
176
+ });
177
+ }
178
+ }
179
+ //# sourceMappingURL=entityStorageAuthenticationAuditService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entityStorageAuthenticationAuditService.js","sourceRoot":"","sources":["../../../src/services/entityStorageAuthenticationAuditService.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EACN,SAAS,EACT,MAAM,EACN,EAAE,EACF,YAAY,EACZ,UAAU,EAEV,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EACN,6BAA6B,EAE7B,MAAM,iCAAiC,CAAC;AAKzC;;GAEG;AACH,MAAM,OAAO,uCAAuC;IACnD;;OAEG;IACI,MAAM,CAAU,UAAU,6CAA6D;IAE9F;;;OAGG;IACc,sCAAsC,CAAoD;IAE3G;;;OAGG;IACc,WAAW,CAAU;IAEtC;;;OAGG;IACH,YAAY,OAAoE;QAC/E,IAAI,CAAC,sCAAsC,GAAG,6BAA6B,CAAC,GAAG,CAC9E,OAAO,EAAE,mCAAmC,IAAI,4BAA4B,CAC5E,CAAC;QACF,MAAM,IAAI,GAAG,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;QACjD,IAAI,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1B,MAAM,kBAAkB,GAAyB,EAAE,CAAC;YACpD,UAAU,CAAC,WAAW,8BAErB,IAAI,EACJ,kBAAkB,EAClB,SAAS,EACT,EAAE,SAAS,EAAE,EAAE,EAAE,CACjB,CAAC;YACF,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC;YACnC,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;gBACjB,kBAAkB,CAAC,IAAI,CAAC;oBACvB,QAAQ,6BAAqC;oBAC7C,MAAM,EAAE,8BAA8B;iBACtC,CAAC,CAAC;YACJ,CAAC;YACD,UAAU,CAAC,iBAAiB,CAC3B,uCAAuC,CAAC,UAAU,+BAElD,kBAAkB,CAClB,CAAC;YAEF,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACzB,CAAC;IACF,CAAC;IAED;;;OAGG;IACI,SAAS;QACf,OAAO,uCAAuC,CAAC,UAAU,CAAC;IAC3D,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,MAAM,CAClB,KAA4D;QAE5D,MAAM,CAAC,MAAM,CACZ,uCAAuC,CAAC,UAAU,WAElD,KAAK,CACL,CAAC;QACF,MAAM,CAAC,WAAW,CACjB,uCAAuC,CAAC,UAAU,iBAElD,KAAK,CAAC,KAAK,CACX,CAAC;QAEF,MAAM,UAAU,GAAG,MAAM,cAAc,CAAC,aAAa,EAAE,CAAC;QAExD,MAAM,aAAa,GAA6B;YAC/C,EAAE,EAAE,YAAY,CAAC,cAAc,CAAC,SAAS,CAAC;YAC1C,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACrC,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,UAAU,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC;YAC1D,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,UAAU,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC;YACxD,cAAc,EAAE,KAAK,CAAC,cAAc,IAAI,UAAU,EAAE,CAAC,aAAa,CAAC,YAAY,CAAC;YAChF,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,UAAU,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC;YAC9D,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,eAAe,EAAE,IAAI,CAAC,eAAe,CAAC,UAAU,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;YAC5F,SAAS,EAAE,UAAU,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC;YACpD,aAAa,EAAE,UAAU,EAAE,CAAC,iBAAiB,CAAC,aAAa,CAAC;SAC5D,CAAC;QAEF,IAAI,CAAC;YACJ,MAAM,IAAI,CAAC,sCAAsC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACtE,CAAC;QAAC,MAAM,CAAC;YACR,qFAAqF;QACtF,CAAC;QAED,OAAO,aAAa,CAAC,EAAE,CAAC;IACzB,CAAC;IAED;;;;;;;;;;;;;OAaG;IACI,KAAK,CAAC,KAAK,CACjB,OAQC,EACD,MAAe,EACf,KAAc;QAKd,MAAM,UAAU,GAIV,EAAE,CAAC;QAET,IAAI,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YACxB,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;gBAChC,MAAM,CAAC,WAAW,CACjB,uCAAuC,CAAC,UAAU,qBAElD,OAAO,CAAC,OAAO,CACf,CAAC;gBACF,UAAU,CAAC,IAAI,CAAC;oBACf,QAAQ,EAAE,SAAS;oBACnB,KAAK,EAAE,OAAO,CAAC,OAAO;oBACtB,UAAU,EAAE,kBAAkB,CAAC,MAAM;iBACrC,CAAC,CAAC;YACJ,CAAC;YAED,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC;gBACvC,MAAM,CAAC,WAAW,CACjB,uCAAuC,CAAC,UAAU,4BAElD,OAAO,CAAC,cAAc,CACtB,CAAC;gBACF,UAAU,CAAC,IAAI,CAAC;oBACf,QAAQ,EAAE,gBAAgB;oBAC1B,KAAK,EAAE,OAAO,CAAC,cAAc;oBAC7B,UAAU,EAAE,kBAAkB,CAAC,MAAM;iBACrC,CAAC,CAAC;YACJ,CAAC;YAED,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACjC,MAAM,CAAC,WAAW,CACjB,uCAAuC,CAAC,UAAU,sBAElD,OAAO,CAAC,QAAQ,CAChB,CAAC;gBACF,UAAU,CAAC,IAAI,CAAC;oBACf,QAAQ,EAAE,UAAU;oBACpB,KAAK,EAAE,OAAO,CAAC,QAAQ;oBACvB,UAAU,EAAE,kBAAkB,CAAC,MAAM;iBACrC,CAAC,CAAC;YACJ,CAAC;YAED,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC/B,MAAM,CAAC,WAAW,CACjB,uCAAuC,CAAC,UAAU,oBAElD,OAAO,CAAC,MAAM,CACd,CAAC;gBACF,UAAU,CAAC,IAAI,CAAC;oBACf,QAAQ,EAAE,QAAQ;oBAClB,KAAK,EAAE,OAAO,CAAC,MAAM;oBACrB,UAAU,EAAE,kBAAkB,CAAC,MAAM;iBACrC,CAAC,CAAC;YACJ,CAAC;YAED,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC9B,MAAM,CAAC,WAAW,CACjB,uCAAuC,CAAC,UAAU,mBAElD,OAAO,CAAC,KAAK,CACb,CAAC;gBACF,UAAU,CAAC,IAAI,CAAC;oBACf,QAAQ,EAAE,OAAO;oBACjB,KAAK,EAAE,OAAO,CAAC,KAAK;oBACpB,UAAU,EAAE,kBAAkB,CAAC,MAAM;iBACrC,CAAC,CAAC;YACJ,CAAC;YAED,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;gBAClC,MAAM,CAAC,WAAW,CACjB,uCAAuC,CAAC,UAAU,uBAElD,OAAO,CAAC,SAAS,CACjB,CAAC;gBACF,UAAU,CAAC,IAAI,CAAC;oBACf,QAAQ,EAAE,aAAa;oBACvB,KAAK,EAAE,OAAO,CAAC,SAAS;oBACxB,UAAU,EAAE,kBAAkB,CAAC,kBAAkB;iBACjD,CAAC,CAAC;YACJ,CAAC;YAED,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;gBAChC,MAAM,CAAC,WAAW,CACjB,uCAAuC,CAAC,UAAU,qBAElD,OAAO,CAAC,OAAO,CACf,CAAC;gBACF,UAAU,CAAC,IAAI,CAAC;oBACf,QAAQ,EAAE,aAAa;oBACvB,KAAK,EAAE,OAAO,CAAC,OAAO;oBACtB,UAAU,EAAE,kBAAkB,CAAC,eAAe;iBAC9C,CAAC,CAAC;YACJ,CAAC;QACF,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,sCAAsC,CAAC,KAAK,CACrE,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,SAAS,EAClD,SAAS,EACT,SAAS,EACT,MAAM,EACN,KAAK,CACL,CAAC;QAEF,OAAO;YACN,OAAO,EAAE,MAAM,CAAC,QAAuC;YACvD,MAAM,EAAE,MAAM,CAAC,MAAM;SACrB,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACK,eAAe,CAAC,WAAiC;QACxD,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;YACjE,OAAO,SAAS,CAAC;QAClB,CAAC;QACD,OAAO,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;YAC3B,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC,WAAW,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;YAC/E,OAAO,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;IACJ,CAAC","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 * @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,202 @@
1
+ import { TooManyRequestsError } from "@twin.org/api-models";
2
+ import { ComponentFactory, Converter, GeneralError, Guards, Is } from "@twin.org/core";
3
+ import { Sha256 } from "@twin.org/crypto";
4
+ import { ComparisonOperator } from "@twin.org/entity";
5
+ import { EntityStorageConnectorFactory } from "@twin.org/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
+ * Create a new instance of EntityStorageAuthenticationRateService.
51
+ * @param options The constructor options.
52
+ */
53
+ constructor(options) {
54
+ this._authenticationRateEntryStorage = EntityStorageConnectorFactory.get(options?.authenticationRateEntryStorageType ?? "authentication-rate-entry");
55
+ this._actionConfigs = {};
56
+ this._taskScheduler = ComponentFactory.get(options?.taskSchedulerComponentType ?? "task-scheduler");
57
+ this._cleanupIntervalMinutes =
58
+ options?.config?.cleanupIntervalMinutes ??
59
+ EntityStorageAuthenticationRateService._DEFAULT_CLEANUP_INTERVAL_MINUTES;
60
+ }
61
+ /**
62
+ * Register or update rate-limit configuration for an action.
63
+ * @param action The action name.
64
+ * @param config The action configuration.
65
+ * @returns Nothing.
66
+ */
67
+ async registerAction(action, config) {
68
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "action", action);
69
+ Guards.object(EntityStorageAuthenticationRateService.CLASS_NAME, "config", config);
70
+ this._actionConfigs[action] = {
71
+ maxAttempts: config.maxAttempts,
72
+ windowMinutes: config.windowMinutes
73
+ };
74
+ }
75
+ /**
76
+ * Unregister rate-limit configuration for an action.
77
+ * @param action The action name.
78
+ * @returns Nothing.
79
+ */
80
+ async unregisterAction(action) {
81
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "action", action);
82
+ delete this._actionConfigs[action];
83
+ }
84
+ /**
85
+ * Returns the class name of the component.
86
+ * @returns The class name of the component.
87
+ */
88
+ className() {
89
+ return EntityStorageAuthenticationRateService.CLASS_NAME;
90
+ }
91
+ /**
92
+ * The service needs to be started when the application is initialized.
93
+ * @param nodeLoggingComponentType The node logging component type.
94
+ * @returns Nothing.
95
+ */
96
+ async start(nodeLoggingComponentType) {
97
+ await this._taskScheduler.addTask(EntityStorageAuthenticationRateService._CLEANUP_TASK_ID, [
98
+ {
99
+ intervalMinutes: this._cleanupIntervalMinutes
100
+ }
101
+ ], async () => this.cleanupExpiredEntries());
102
+ }
103
+ /**
104
+ * The component needs to be stopped when the node is closed.
105
+ * @param nodeLoggingComponentType The node logging component type.
106
+ * @returns Nothing.
107
+ */
108
+ async stop(nodeLoggingComponentType) {
109
+ await this._taskScheduler.removeTask(EntityStorageAuthenticationRateService._CLEANUP_TASK_ID);
110
+ }
111
+ /**
112
+ * Check the authentication rate for a given action and identifier.
113
+ * @param action The action to be checked.
114
+ * @param identifier The identifier to be checked.
115
+ * @returns The rate entry id.
116
+ */
117
+ async check(action, identifier) {
118
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "action", action);
119
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "identifier", identifier);
120
+ const actionConfig = this._actionConfigs[action];
121
+ if (!Is.object(actionConfig)) {
122
+ throw new GeneralError(EntityStorageAuthenticationRateService.CLASS_NAME, "actionConfigMissing", {
123
+ action
124
+ });
125
+ }
126
+ const hashedIdentifier = Converter.bytesToHex(Sha256.sum256(Converter.utf8ToBytes(identifier)));
127
+ const compositeId = `|${action}|${hashedIdentifier}|`;
128
+ const now = Date.now();
129
+ const nowIso = new Date(now).toISOString();
130
+ const windowMs = actionConfig.windowMinutes * 60 * 1000;
131
+ const cutoff = now - windowMs;
132
+ // Resolve the existing rate entry for this action + identifier pair.
133
+ const existing = await this._authenticationRateEntryStorage.get(compositeId);
134
+ // Keep only attempts inside the configured sliding window.
135
+ const activeTimestamps = (existing?.timestamps ?? []).filter(timestamp => new Date(timestamp).getTime() > cutoff);
136
+ if (activeTimestamps.length >= actionConfig.maxAttempts) {
137
+ const oldestTimestamp = new Date(activeTimestamps[0]).getTime();
138
+ const nextRequestTime = new Date(oldestTimestamp + windowMs).toISOString();
139
+ const retryAfterSeconds = Math.ceil((oldestTimestamp + windowMs - now) / 1000);
140
+ throw new TooManyRequestsError(EntityStorageAuthenticationRateService.CLASS_NAME, "rateLimitExceeded", activeTimestamps.length, nextRequestTime, {
141
+ action,
142
+ retryAfterSeconds
143
+ });
144
+ }
145
+ activeTimestamps.push(nowIso);
146
+ await this._authenticationRateEntryStorage.set({
147
+ id: compositeId,
148
+ timestamps: activeTimestamps,
149
+ dateModified: nowIso
150
+ });
151
+ return compositeId;
152
+ }
153
+ /**
154
+ * Clear the authentication rate entry for the given action and identifier.
155
+ * @param action The action to clear.
156
+ * @param identifier The identifier to clear.
157
+ * @returns Nothing.
158
+ */
159
+ async clear(action, identifier) {
160
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "action", action);
161
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "identifier", identifier);
162
+ const hashedIdentifier = Converter.bytesToHex(Sha256.sum256(Converter.utf8ToBytes(identifier)));
163
+ const compositeId = `|${action}|${hashedIdentifier}|`;
164
+ await this._authenticationRateEntryStorage.remove(compositeId);
165
+ }
166
+ /**
167
+ * Cleanup expired rate limit entries.
168
+ * @returns Nothing.
169
+ * @internal
170
+ */
171
+ async cleanupExpiredEntries() {
172
+ const now = Date.now();
173
+ for (const action of Object.keys(this._actionConfigs)) {
174
+ const actionConfig = this._actionConfigs[action];
175
+ const windowMs = actionConfig.windowMinutes * 60 * 1000;
176
+ const cutoffIso = new Date(now - windowMs).toISOString();
177
+ let cursor;
178
+ do {
179
+ // Query by composite rate entry id prefix and expiry window for this action.
180
+ const result = await this._authenticationRateEntryStorage.query({
181
+ conditions: [
182
+ {
183
+ property: "id",
184
+ value: `|${action}|`,
185
+ comparison: ComparisonOperator.Includes
186
+ },
187
+ {
188
+ property: "dateModified",
189
+ value: cutoffIso,
190
+ comparison: ComparisonOperator.LessThanOrEqual
191
+ }
192
+ ]
193
+ }, undefined, undefined, cursor, EntityStorageAuthenticationRateService._CLEANUP_PAGE_SIZE);
194
+ for (const entity of result.entities) {
195
+ await this._authenticationRateEntryStorage.remove(entity.id);
196
+ }
197
+ cursor = result.cursor;
198
+ } while (!Is.empty(cursor));
199
+ }
200
+ }
201
+ }
202
+ //# sourceMappingURL=entityStorageAuthenticationRateService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entityStorageAuthenticationRateService.js","sourceRoot":"","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 \"@twin.org/api-auth-entity-storage-models\";\nimport { TooManyRequestsError } from \"@twin.org/api-models\";\nimport type { ITaskSchedulerComponent } from \"@twin.org/background-task-models\";\nimport { ComponentFactory, Converter, GeneralError, Guards, Is } 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 { 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 = nameof<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 * 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}\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 Nothing.\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, nameof(action), action);\n\t\tGuards.object<IAuthenticationRateActionConfig>(\n\t\t\tEntityStorageAuthenticationRateService.CLASS_NAME,\n\t\t\tnameof(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 Nothing.\n\t */\n\tpublic async unregisterAction(action: string): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, nameof(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 Nothing.\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 Nothing.\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, nameof(action), action);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationRateService.CLASS_NAME,\n\t\t\tnameof(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 Nothing.\n\t */\n\tpublic async clear(action: string, identifier: string): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, nameof(action), action);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationRateService.CLASS_NAME,\n\t\t\tnameof(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.\n\t * @returns Nothing.\n\t * @internal\n\t */\n\tprivate async cleanupExpiredEntries(): Promise<void> {\n\t\tconst now = Date.now();\n\n\t\tfor (const action of Object.keys(this._actionConfigs)) {\n\t\t\tconst actionConfig = this._actionConfigs[action];\n\t\t\tconst windowMs = actionConfig.windowMinutes * 60 * 1000;\n\t\t\tconst cutoffIso = new Date(now - windowMs).toISOString();\n\t\t\tlet cursor: string | undefined;\n\n\t\t\tdo {\n\t\t\t\t// Query by composite rate entry id prefix and expiry window for this action.\n\t\t\t\tconst result = await this._authenticationRateEntryStorage.query(\n\t\t\t\t\t{\n\t\t\t\t\t\tconditions: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tproperty: \"id\",\n\t\t\t\t\t\t\t\tvalue: `|${action}|`,\n\t\t\t\t\t\t\t\tcomparison: ComparisonOperator.Includes\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tproperty: \"dateModified\",\n\t\t\t\t\t\t\t\tvalue: cutoffIso,\n\t\t\t\t\t\t\t\tcomparison: ComparisonOperator.LessThanOrEqual\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t},\n\t\t\t\t\tundefined,\n\t\t\t\t\tundefined,\n\t\t\t\t\tcursor,\n\t\t\t\t\tEntityStorageAuthenticationRateService._CLEANUP_PAGE_SIZE\n\t\t\t\t);\n\n\t\t\t\tfor (const entity of result.entities as AuthenticationRateEntry[]) {\n\t\t\t\t\tawait this._authenticationRateEntryStorage.remove(entity.id);\n\t\t\t\t}\n\n\t\t\t\tcursor = result.cursor;\n\t\t\t} while (!Is.empty(cursor));\n\t\t}\n\t}\n}\n"]}