@twin.org/api-auth-entity-storage-service 0.0.3-next.50 → 0.0.3-next.52
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.
- package/dist/es/models/IEntityStorageAuthenticationRateServiceConstructorOptions.js.map +1 -1
- package/dist/es/services/entityStorageAuthenticationRateService.js +40 -29
- package/dist/es/services/entityStorageAuthenticationRateService.js.map +1 -1
- package/dist/types/models/IEntityStorageAuthenticationRateServiceConstructorOptions.d.ts +5 -0
- package/docs/changelog.md +32 -0
- package/docs/reference/interfaces/IEntityStorageAuthenticationRateServiceConstructorOptions.md +14 -0
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IEntityStorageAuthenticationRateServiceConstructorOptions.js","sourceRoot":"","sources":["../../../src/models/IEntityStorageAuthenticationRateServiceConstructorOptions.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { IEntityStorageAuthenticationRateServiceConfig } from \"./IEntityStorageAuthenticationRateServiceConfig.js\";\n\n/**\n * Options for the EntityStorageAuthenticationRateService constructor.\n */\nexport interface IEntityStorageAuthenticationRateServiceConstructorOptions {\n\t/**\n\t * The entity storage for authentication rate entries.\n\t * @default authentication-rate-entry\n\t */\n\tauthenticationRateEntryStorageType?: string;\n\n\t/**\n\t * The task scheduler component type.\n\t * @default task-scheduler\n\t */\n\ttaskSchedulerComponentType?: string;\n\n\t/**\n\t * The configuration for the authentication rate service.\n\t */\n\tconfig?: IEntityStorageAuthenticationRateServiceConfig;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"IEntityStorageAuthenticationRateServiceConstructorOptions.js","sourceRoot":"","sources":["../../../src/models/IEntityStorageAuthenticationRateServiceConstructorOptions.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { IEntityStorageAuthenticationRateServiceConfig } from \"./IEntityStorageAuthenticationRateServiceConfig.js\";\n\n/**\n * Options for the EntityStorageAuthenticationRateService constructor.\n */\nexport interface IEntityStorageAuthenticationRateServiceConstructorOptions {\n\t/**\n\t * The entity storage for authentication rate entries.\n\t * @default authentication-rate-entry\n\t */\n\tauthenticationRateEntryStorageType?: string;\n\n\t/**\n\t * The task scheduler component type.\n\t * @default task-scheduler\n\t */\n\ttaskSchedulerComponentType?: string;\n\n\t/**\n\t * The platform component type, used to run the periodic cleanup per tenant.\n\t * @default platform\n\t */\n\tplatformComponentType?: string;\n\n\t/**\n\t * The configuration for the authentication rate service.\n\t */\n\tconfig?: IEntityStorageAuthenticationRateServiceConfig;\n}\n"]}
|
|
@@ -46,6 +46,11 @@ export class EntityStorageAuthenticationRateService {
|
|
|
46
46
|
* @internal
|
|
47
47
|
*/
|
|
48
48
|
_cleanupIntervalMinutes;
|
|
49
|
+
/**
|
|
50
|
+
* Platform component used to run the periodic cleanup with the correct tenant context.
|
|
51
|
+
* @internal
|
|
52
|
+
*/
|
|
53
|
+
_platformComponent;
|
|
49
54
|
/**
|
|
50
55
|
* Create a new instance of EntityStorageAuthenticationRateService.
|
|
51
56
|
* @param options The constructor options.
|
|
@@ -57,6 +62,7 @@ export class EntityStorageAuthenticationRateService {
|
|
|
57
62
|
this._cleanupIntervalMinutes =
|
|
58
63
|
options?.config?.cleanupIntervalMinutes ??
|
|
59
64
|
EntityStorageAuthenticationRateService._DEFAULT_CLEANUP_INTERVAL_MINUTES;
|
|
65
|
+
this._platformComponent = ComponentFactory.get(options?.platformComponentType ?? "platform");
|
|
60
66
|
}
|
|
61
67
|
/**
|
|
62
68
|
* Register or update rate-limit configuration for an action.
|
|
@@ -164,39 +170,44 @@ export class EntityStorageAuthenticationRateService {
|
|
|
164
170
|
await this._authenticationRateEntryStorage.remove(compositeId);
|
|
165
171
|
}
|
|
166
172
|
/**
|
|
167
|
-
* Cleanup expired rate limit entries.
|
|
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).
|
|
168
175
|
* @returns A promise that resolves when all expired entries have been removed from storage.
|
|
169
176
|
* @internal
|
|
170
177
|
*/
|
|
171
178
|
async cleanupExpiredEntries() {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
});
|
|
200
211
|
}
|
|
201
212
|
}
|
|
202
213
|
//# sourceMappingURL=entityStorageAuthenticationRateService.js.map
|
|
@@ -1 +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 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, 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 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, 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 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, 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 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, 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 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\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"]}
|
|
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, type IPlatformComponent } 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 * 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, 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 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, 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 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, 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 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, 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. 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"]}
|
|
@@ -13,6 +13,11 @@ export interface IEntityStorageAuthenticationRateServiceConstructorOptions {
|
|
|
13
13
|
* @default task-scheduler
|
|
14
14
|
*/
|
|
15
15
|
taskSchedulerComponentType?: string;
|
|
16
|
+
/**
|
|
17
|
+
* The platform component type, used to run the periodic cleanup per tenant.
|
|
18
|
+
* @default platform
|
|
19
|
+
*/
|
|
20
|
+
platformComponentType?: string;
|
|
16
21
|
/**
|
|
17
22
|
* The configuration for the authentication rate service.
|
|
18
23
|
*/
|
package/docs/changelog.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.0.3-next.52](https://github.com/iotaledger/twin-api/compare/api-auth-entity-storage-service-v0.0.3-next.51...api-auth-entity-storage-service-v0.0.3-next.52) (2026-06-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* run authentication rate cleanup per tenant ([#186](https://github.com/iotaledger/twin-api/issues/186)) ([d69c7aa](https://github.com/iotaledger/twin-api/commit/d69c7aa4ade77f1a52f1f554d8f7a3bd20f1b330))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* The following workspace dependencies were updated
|
|
14
|
+
* dependencies
|
|
15
|
+
* @twin.org/api-auth-entity-storage-models bumped from 0.0.3-next.51 to 0.0.3-next.52
|
|
16
|
+
* @twin.org/api-core bumped from 0.0.3-next.51 to 0.0.3-next.52
|
|
17
|
+
* @twin.org/api-models bumped from 0.0.3-next.51 to 0.0.3-next.52
|
|
18
|
+
|
|
19
|
+
## [0.0.3-next.51](https://github.com/iotaledger/twin-api/compare/api-auth-entity-storage-service-v0.0.3-next.50...api-auth-entity-storage-service-v0.0.3-next.51) (2026-06-20)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Miscellaneous Chores
|
|
23
|
+
|
|
24
|
+
* **api-auth-entity-storage-service:** Synchronize repo versions
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Dependencies
|
|
28
|
+
|
|
29
|
+
* The following workspace dependencies were updated
|
|
30
|
+
* dependencies
|
|
31
|
+
* @twin.org/api-auth-entity-storage-models bumped from 0.0.3-next.50 to 0.0.3-next.51
|
|
32
|
+
* @twin.org/api-core bumped from 0.0.3-next.50 to 0.0.3-next.51
|
|
33
|
+
* @twin.org/api-models bumped from 0.0.3-next.50 to 0.0.3-next.51
|
|
34
|
+
|
|
3
35
|
## [0.0.3-next.50](https://github.com/iotaledger/twin-api/compare/api-auth-entity-storage-service-v0.0.3-next.49...api-auth-entity-storage-service-v0.0.3-next.50) (2026-06-19)
|
|
4
36
|
|
|
5
37
|
|
package/docs/reference/interfaces/IEntityStorageAuthenticationRateServiceConstructorOptions.md
CHANGED
|
@@ -32,6 +32,20 @@ task-scheduler
|
|
|
32
32
|
|
|
33
33
|
***
|
|
34
34
|
|
|
35
|
+
### platformComponentType? {#platformcomponenttype}
|
|
36
|
+
|
|
37
|
+
> `optional` **platformComponentType?**: `string`
|
|
38
|
+
|
|
39
|
+
The platform component type, used to run the periodic cleanup per tenant.
|
|
40
|
+
|
|
41
|
+
#### Default
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
platform
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
***
|
|
48
|
+
|
|
35
49
|
### config? {#config}
|
|
36
50
|
|
|
37
51
|
> `optional` **config?**: [`IEntityStorageAuthenticationRateServiceConfig`](IEntityStorageAuthenticationRateServiceConfig.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twin.org/api-auth-entity-storage-service",
|
|
3
|
-
"version": "0.0.3-next.
|
|
3
|
+
"version": "0.0.3-next.52",
|
|
4
4
|
"description": "Authentication service implementation and REST routes backed by entity storage.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
"node": ">=20.0.0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@twin.org/api-auth-entity-storage-models": "0.0.3-next.
|
|
18
|
-
"@twin.org/api-core": "0.0.3-next.
|
|
19
|
-
"@twin.org/api-models": "0.0.3-next.
|
|
17
|
+
"@twin.org/api-auth-entity-storage-models": "0.0.3-next.52",
|
|
18
|
+
"@twin.org/api-core": "0.0.3-next.52",
|
|
19
|
+
"@twin.org/api-models": "0.0.3-next.52",
|
|
20
20
|
"@twin.org/background-task-models": "next",
|
|
21
21
|
"@twin.org/context": "next",
|
|
22
22
|
"@twin.org/core": "next",
|