@mrxsys/mrx-core 2.6.0 → 2.7.0
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/CHANGELOG.md +59 -13
- package/dist/chunk-4mt568fz.js +26 -0
- package/dist/{chunk-7m70tmz2.js → chunk-qb6x364m.js} +1 -1
- package/dist/chunk-snqdnkk7.js +10 -0
- package/dist/{chunk-64d6w5kt.js → chunk-tm71j126.js} +1 -1
- package/dist/chunk-yd82hdxv.js +6 -0
- package/dist/{chunk-9wk2pajt.js → chunk-z6q192p8.js} +7 -6
- package/dist/modules/database/index.js +2 -2
- package/dist/modules/elysia/crud/index.js +3 -3
- package/dist/modules/elysia/dbResolver/index.js +3 -3
- package/dist/modules/elysia/rateLimit/enums/index.d.ts +1 -0
- package/dist/modules/elysia/rateLimit/enums/index.js +7 -0
- package/dist/modules/elysia/rateLimit/enums/rateLimitErrorKeys.d.ts +3 -0
- package/dist/modules/elysia/rateLimit/index.d.ts +1 -0
- package/dist/modules/elysia/rateLimit/index.js +139 -0
- package/dist/modules/elysia/{ratelimit/ratelimit.d.ts → rateLimit/rateLimit.d.ts} +5 -5
- package/dist/modules/elysia/rateLimit/stores/memoryStore.d.ts +47 -0
- package/dist/modules/elysia/rateLimit/types/memoryStoreEntry.d.ts +11 -0
- package/dist/modules/elysia/{ratelimit → rateLimit}/types/rateLimitOptions.d.ts +7 -4
- package/dist/modules/elysia/rateLimit/types/rateLimitStore.d.ts +21 -0
- package/dist/modules/repository/index.js +1 -1
- package/dist/modules/totp/enums/index.d.ts +1 -0
- package/dist/modules/totp/enums/index.js +7 -0
- package/dist/modules/totp/enums/totpErrorKeys.d.ts +7 -0
- package/dist/modules/totp/index.d.ts +1 -0
- package/dist/modules/totp/index.js +113 -0
- package/dist/modules/totp/totp.d.ts +63 -0
- package/dist/modules/totp/types/index.d.ts +3 -0
- package/dist/modules/totp/types/index.js +1 -0
- package/dist/modules/totp/types/otpAuthUri.d.ts +35 -0
- package/dist/modules/totp/types/totpOptions.d.ts +23 -0
- package/dist/modules/totp/types/verifyOptions.d.ts +18 -0
- package/dist/modules/totp/utils/base32.d.ts +19 -0
- package/dist/modules/totp/utils/createCounterBuffer.d.ts +8 -0
- package/dist/modules/totp/utils/dynamicTruncation.d.ts +9 -0
- package/dist/modules/totp/utils/generateHmac.d.ts +9 -0
- package/dist/modules/totp/utils/generateSecretBytes.d.ts +10 -0
- package/dist/modules/totp/utils/index.d.ts +5 -0
- package/dist/modules/totp/utils/index.js +75 -0
- package/dist/utils/types/index.d.ts +1 -0
- package/dist/utils/types/renameKey.d.ts +10 -0
- package/package.json +13 -9
- package/dist/chunk-twaga0fp.js +0 -6
- package/dist/modules/elysia/ratelimit/enums/index.d.ts +0 -1
- package/dist/modules/elysia/ratelimit/enums/index.js +0 -7
- package/dist/modules/elysia/ratelimit/enums/ratelimitErrorKeys.d.ts +0 -3
- package/dist/modules/elysia/ratelimit/index.d.ts +0 -1
- package/dist/modules/elysia/ratelimit/index.js +0 -52
- /package/dist/modules/elysia/{ratelimit → rateLimit}/types/index.d.ts +0 -0
- /package/dist/modules/elysia/{ratelimit → rateLimit}/types/index.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,33 +1,79 @@
|
|
|
1
1
|
|
|
2
|
-
## v2.
|
|
2
|
+
## v2.7.0
|
|
3
3
|
|
|
4
|
-
[compare changes](https://github.com/MRX-Systems/MRX-Core/compare/v2.6.0-canary-20250805-40e4647...v2.
|
|
4
|
+
[compare changes](https://github.com/MRX-Systems/MRX-Core/compare/v2.6.0-canary-20250805-40e4647...v2.7.0)
|
|
5
5
|
|
|
6
|
-
###
|
|
6
|
+
### 🚀 Enhancements
|
|
7
7
|
|
|
8
|
-
-
|
|
8
|
+
- **🚀:** AND-228 fix throwIfNoResult custom message ([a1228463](https://github.com/MRX-Systems/MRX-Core/commit/a1228463))
|
|
9
|
+
- **🚀:** [add Base32 encoding and decoding utilities] ([e64b06cd](https://github.com/MRX-Systems/MRX-Core/commit/e64b06cd))
|
|
10
|
+
- **🚀:** [add createCounterBuffer utility function] ([29af86e4](https://github.com/MRX-Systems/MRX-Core/commit/29af86e4))
|
|
11
|
+
- **🚀:** [add generateHmac utility function] ([30142a1e](https://github.com/MRX-Systems/MRX-Core/commit/30142a1e))
|
|
12
|
+
- **🚀:** [add unit tests for generateHmac utility function] ([9ff1643a](https://github.com/MRX-Systems/MRX-Core/commit/9ff1643a))
|
|
13
|
+
- **🚀:** [add TOTP error keys and generateSecretBytes utility function] ([527008ea](https://github.com/MRX-Systems/MRX-Core/commit/527008ea))
|
|
14
|
+
- **🚀:** [add dynamic truncation utility function for HMAC results] ([31c261b0](https://github.com/MRX-Systems/MRX-Core/commit/31c261b0))
|
|
15
|
+
- **🚀:** [add utility functions exports for TOTP module] ## Features - Exported utility functions: base32Decode, base32Encode, createCounterBuffer, generateSecretBytes, generateHmac, and dynamicTruncation. ([c92359bd](https://github.com/MRX-Systems/MRX-Core/commit/c92359bd))
|
|
16
|
+
- **🚀:** [implement TOTP and HOTP functionalities] ([9a9a3983](https://github.com/MRX-Systems/MRX-Core/commit/9a9a3983))
|
|
17
|
+
- **🚀:** [add TOTP module exports for better accessibility] ([aec89c93](https://github.com/MRX-Systems/MRX-Core/commit/aec89c93))
|
|
18
|
+
- **🚀:** [add TOTP_ERROR_KEYS export for error handling] ([73e361e3](https://github.com/MRX-Systems/MRX-Core/commit/73e361e3))
|
|
19
|
+
- **🚀:** [Add OtpAuth URI type and refactor related code] ([2c2d2b88](https://github.com/MRX-Systems/MRX-Core/commit/2c2d2b88))
|
|
20
|
+
- **🚀:** [refactor rate limiting implementation and add MemoryStore] ([158b7ddf](https://github.com/MRX-Systems/MRX-Core/commit/158b7ddf))
|
|
9
21
|
|
|
10
|
-
###
|
|
22
|
+
### 🔧 Fixes
|
|
11
23
|
|
|
12
|
-
-
|
|
24
|
+
- **🔧:** [Improve error message handling for no results] ([76c297fa](https://github.com/MRX-Systems/MRX-Core/commit/76c297fa))
|
|
25
|
+
- **🔧:** [Fix review] ([87c68deb](https://github.com/MRX-Systems/MRX-Core/commit/87c68deb))
|
|
13
26
|
|
|
14
|
-
###
|
|
27
|
+
### 🧹 Refactors
|
|
15
28
|
|
|
16
|
-
-
|
|
29
|
+
- **🧹:** [Change in tenary expression] ([4b4c4864](https://github.com/MRX-Systems/MRX-Core/commit/4b4c4864))
|
|
30
|
+
- **🧹:** [Update orderBy syntax for query builder] - Refactored the orderBy method to include table name in the query. - Ensured that selected fields are properly referenced with the table name for better clarity and to avoid potential conflicts. ([53eb355e](https://github.com/MRX-Systems/MRX-Core/commit/53eb355e))
|
|
31
|
+
- **🧹:** [Refactor query returning and selection logic] ([a6b46fb2](https://github.com/MRX-Systems/MRX-Core/commit/a6b46fb2))
|
|
32
|
+
- **🧹:** [Simplify query returning and selection logic] ([b6c04169](https://github.com/MRX-Systems/MRX-Core/commit/b6c04169))
|
|
33
|
+
- **🧹:** [Refactor HOTP options parameter initialization] ## Refactoring - Updated the HOTP function's options parameter to provide a default value. ([3937ae22](https://github.com/MRX-Systems/MRX-Core/commit/3937ae22))
|
|
34
|
+
- **🧹:** [correct export name for rate limiting module] ([cbb3e9f7](https://github.com/MRX-Systems/MRX-Core/commit/cbb3e9f7))
|
|
35
|
+
- **🧹:** [standardize rate limit module exports and structure] ## Refactoring - Corrected export names in the rate limiting module for consistency. - Removed outdated `ratelimitErrorKeys` file and integrated its functionality into the main rate limiting logic. - Updated test cases to reflect the new export structure and ensure proper functionality. - Introduced a new `rateLimit` function with comprehensive documentation. ([f192bb39](https://github.com/MRX-Systems/MRX-Core/commit/f192bb39))
|
|
17
36
|
|
|
18
|
-
|
|
37
|
+
### 📖 Documentation
|
|
19
38
|
|
|
20
|
-
[
|
|
39
|
+
- **📖:** [Update documentation for RenameKey type utility] ([379a2403](https://github.com/MRX-Systems/MRX-Core/commit/379a2403))
|
|
21
40
|
|
|
22
|
-
###
|
|
41
|
+
### 📦 Build
|
|
42
|
+
|
|
43
|
+
- **📦:** [update devDependencies to latest versions] Updated @eslint/js, @stylistic/eslint-plugin, @types/bun, eslint, and typescript-eslint to their latest versions for improved functionality and compatibility. ([1c40c8e7](https://github.com/MRX-Systems/MRX-Core/commit/1c40c8e7))
|
|
44
|
+
- **📦:** [add TOTP module paths to builder and package.json] ([d5ad0b26](https://github.com/MRX-Systems/MRX-Core/commit/d5ad0b26))
|
|
23
45
|
|
|
24
|
-
|
|
46
|
+
### 🌊 Types
|
|
47
|
+
|
|
48
|
+
- **🌊:** [fix TOperations with default CrudOperationsOptions] ([b8b6169d](https://github.com/MRX-Systems/MRX-Core/commit/b8b6169d))
|
|
49
|
+
- **🌊:** [add RenameKey type utility for key renaming in objects] ([0d661646](https://github.com/MRX-Systems/MRX-Core/commit/0d661646))
|
|
50
|
+
- **🌊:** [Add RenameKey type to type definitions] ## Type Changes - Added `RenameKey` type to the type definitions in `index.ts`. ([e389bcba](https://github.com/MRX-Systems/MRX-Core/commit/e389bcba))
|
|
51
|
+
- **🌊:** [add type definitions for TOTP module] ([03917acc](https://github.com/MRX-Systems/MRX-Core/commit/03917acc))
|
|
52
|
+
- **🌊:** [Update OtpAuthUri interface for optional properties] ## Type Changes - Made `algorithm`, `digits`, and `period` properties optional in the `OtpAuthUri` interface. ([d1d47681](https://github.com/MRX-Systems/MRX-Core/commit/d1d47681))
|
|
25
53
|
|
|
26
54
|
### 🦉 Chore
|
|
27
55
|
|
|
28
|
-
- **🦉:**
|
|
56
|
+
- **🦉:** V2.6.0 ([3a016766](https://github.com/MRX-Systems/MRX-Core/commit/3a016766))
|
|
57
|
+
- **🦉:** [clean up CHANGELOG.md for better readability] ([ab70e0fc](https://github.com/MRX-Systems/MRX-Core/commit/ab70e0fc))
|
|
58
|
+
|
|
59
|
+
### 🧪 Tests
|
|
60
|
+
|
|
61
|
+
- **🧪:** [Update error message for no result case] ## Tests - Changed the error message for the no result case in the repository tests. ([0f71ce8f](https://github.com/MRX-Systems/MRX-Core/commit/0f71ce8f))
|
|
62
|
+
- **🧪:** [add unit tests for Base32 encoding and decoding] ([2b25e11d](https://github.com/MRX-Systems/MRX-Core/commit/2b25e11d))
|
|
63
|
+
- **🧪:** [add unit tests for createCounterBuffer function] ([71153e42](https://github.com/MRX-Systems/MRX-Core/commit/71153e42))
|
|
64
|
+
- **🧪:** [add unit tests for generateSecretBytes utility function] ([bb33cd78](https://github.com/MRX-Systems/MRX-Core/commit/bb33cd78))
|
|
65
|
+
- **🧪:** [add unit tests for dynamic truncation utility function] ([ba6fee56](https://github.com/MRX-Systems/MRX-Core/commit/ba6fee56))
|
|
66
|
+
- **🧪:** [add comprehensive unit tests for TOTP functionalities] ([82d5f4d1](https://github.com/MRX-Systems/MRX-Core/commit/82d5f4d1))
|
|
67
|
+
- **🧪:** [add comprehensive tests for MemoryStore functionality] ([d1364340](https://github.com/MRX-Systems/MRX-Core/commit/d1364340))
|
|
68
|
+
- **🧪:** Enhance rate limiting tests for Redis and memory stores ([2df081e1](https://github.com/MRX-Systems/MRX-Core/commit/2df081e1))
|
|
69
|
+
|
|
70
|
+
### 🤖 CI
|
|
71
|
+
|
|
72
|
+
- **🤖:** [Add CI workflows for build, test, and deployment] ([7d98d68b](https://github.com/MRX-Systems/MRX-Core/commit/7d98d68b))
|
|
73
|
+
- **🤖:** [update copilot instructions] ([a8540b78](https://github.com/MRX-Systems/MRX-Core/commit/a8540b78))
|
|
29
74
|
|
|
30
75
|
### ❤️ Contributors
|
|
31
76
|
|
|
32
77
|
- Ruby <necrelox@proton.me>
|
|
78
|
+
- Github-actions <maxime.meriaux@mrxsys.com>
|
|
33
79
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// source/modules/totp/utils/createCounterBuffer.ts
|
|
2
|
+
var createCounterBuffer = (counter) => {
|
|
3
|
+
const counterBuffer = new ArrayBuffer(8);
|
|
4
|
+
const counterView = new DataView(counterBuffer);
|
|
5
|
+
if (typeof counter === "bigint")
|
|
6
|
+
counterView.setBigUint64(0, counter, false);
|
|
7
|
+
else
|
|
8
|
+
counterView.setUint32(4, counter, false);
|
|
9
|
+
return counterBuffer;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// source/modules/totp/utils/dynamicTruncation.ts
|
|
13
|
+
var dynamicTruncation = (hmacArray, digits) => {
|
|
14
|
+
const offset = hmacArray[hmacArray.length - 1] & 15;
|
|
15
|
+
const code = ((hmacArray[offset] & 127) << 24 | (hmacArray[offset + 1] & 255) << 16 | (hmacArray[offset + 2] & 255) << 8 | hmacArray[offset + 3] & 255) % 10 ** digits;
|
|
16
|
+
return code.toString().padStart(digits, "0");
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// source/modules/totp/utils/generateHmac.ts
|
|
20
|
+
import { webcrypto } from "crypto";
|
|
21
|
+
var generateHmac = async (key, data) => {
|
|
22
|
+
const hmac = await webcrypto.subtle.sign("HMAC", key, data);
|
|
23
|
+
return new Uint8Array(hmac);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export { createCounterBuffer, dynamicTruncation, generateHmac };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// source/modules/totp/enums/totpErrorKeys.ts
|
|
2
|
+
var TOTP_ERROR_KEYS = {
|
|
3
|
+
INVALID_BASE32_CHARACTER: "totp.error.invalid_base32_character",
|
|
4
|
+
INVALID_SECRET_LENGTH: "totp.error.invalid_secret_length",
|
|
5
|
+
INVALID_ALGORITHM: "totp.error.invalid_algorithm",
|
|
6
|
+
INVALID_OTP_AUTH_URI: "totp.error.invalid_otp_auth_uri",
|
|
7
|
+
MISSING_SECRET: "totp.error.missing_secret"
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export { TOTP_ERROR_KEYS };
|
|
@@ -124,10 +124,11 @@ class Repository {
|
|
|
124
124
|
}
|
|
125
125
|
_applySelectedFields(query, selectedFields) {
|
|
126
126
|
const qMethod = query._method;
|
|
127
|
+
const sanitizedFields = selectedFields ? Array.isArray(selectedFields) ? selectedFields.map((selectedField) => `${selectedField} as ${selectedField}`) : `${selectedFields} as ${selectedFields}` : "*";
|
|
127
128
|
if (qMethod === "del" || qMethod === "update" || qMethod === "insert")
|
|
128
|
-
query.returning(
|
|
129
|
+
query.returning(sanitizedFields);
|
|
129
130
|
else
|
|
130
|
-
query.select(
|
|
131
|
+
query.select(sanitizedFields);
|
|
131
132
|
}
|
|
132
133
|
_applyFilter(query, search) {
|
|
133
134
|
const processing = (query2, search2) => {
|
|
@@ -166,13 +167,13 @@ class Repository {
|
|
|
166
167
|
if (!(qMethod === "select"))
|
|
167
168
|
return;
|
|
168
169
|
if (!orderBy)
|
|
169
|
-
query.orderBy(this._table.primaryKey[0]
|
|
170
|
+
query.orderBy(`[${this._table.name}].${this._table.primaryKey[0]}`, "asc");
|
|
170
171
|
else if (Array.isArray(orderBy))
|
|
171
172
|
orderBy.forEach((item) => {
|
|
172
|
-
query.orderBy(item.selectedField
|
|
173
|
+
query.orderBy(`[${this._table.name}].${item.selectedField}`, item.direction);
|
|
173
174
|
});
|
|
174
175
|
else
|
|
175
|
-
query.orderBy(orderBy.selectedField
|
|
176
|
+
query.orderBy(`[${this._table.name}].${orderBy.selectedField}`, orderBy.direction);
|
|
176
177
|
}
|
|
177
178
|
_applyQueryOptions(query, options) {
|
|
178
179
|
this._applyFilter(query, options?.filters);
|
|
@@ -201,7 +202,7 @@ class Repository {
|
|
|
201
202
|
const result = await query;
|
|
202
203
|
if (throwIfNoResult && result.length === 0)
|
|
203
204
|
throw new HttpError({
|
|
204
|
-
message: DATABASE_ERROR_KEYS.MSSQL_NO_RESULT,
|
|
205
|
+
message: typeof throwIfNoResult === "string" ? throwIfNoResult : DATABASE_ERROR_KEYS.MSSQL_NO_RESULT,
|
|
205
206
|
cause: {
|
|
206
207
|
query: query.toSQL().sql
|
|
207
208
|
},
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import {
|
|
3
3
|
MSSQL,
|
|
4
4
|
Table
|
|
5
|
-
} from "../../chunk-
|
|
6
|
-
import"../../chunk-
|
|
5
|
+
} from "../../chunk-qb6x364m.js";
|
|
6
|
+
import"../../chunk-z6q192p8.js";
|
|
7
7
|
import"../../chunk-5qtpggzv.js";
|
|
8
8
|
import"../../chunk-yvyahr2h.js";
|
|
9
9
|
import"../../chunk-zaje5tv4.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
dbResolver
|
|
4
|
-
} from "../../../chunk-
|
|
4
|
+
} from "../../../chunk-tm71j126.js";
|
|
5
5
|
import"../../../chunk-7jsxw1ts.js";
|
|
6
6
|
import"../../../chunk-hcc6g1fd.js";
|
|
7
7
|
import"../../../chunk-dwfyt1m6.js";
|
|
@@ -11,8 +11,8 @@ import {
|
|
|
11
11
|
import"../../../chunk-26brdrbz.js";
|
|
12
12
|
import"../../../chunk-4n72ks5x.js";
|
|
13
13
|
import"../../../chunk-j28jpfv2.js";
|
|
14
|
-
import"../../../chunk-
|
|
15
|
-
import"../../../chunk-
|
|
14
|
+
import"../../../chunk-qb6x364m.js";
|
|
15
|
+
import"../../../chunk-z6q192p8.js";
|
|
16
16
|
import"../../../chunk-5qtpggzv.js";
|
|
17
17
|
import"../../../chunk-yvyahr2h.js";
|
|
18
18
|
import"../../../chunk-zaje5tv4.js";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
dbResolver
|
|
4
|
-
} from "../../../chunk-
|
|
4
|
+
} from "../../../chunk-tm71j126.js";
|
|
5
5
|
import"../../../chunk-7jsxw1ts.js";
|
|
6
6
|
import"../../../chunk-hcc6g1fd.js";
|
|
7
7
|
import"../../../chunk-dwfyt1m6.js";
|
|
8
|
-
import"../../../chunk-
|
|
9
|
-
import"../../../chunk-
|
|
8
|
+
import"../../../chunk-qb6x364m.js";
|
|
9
|
+
import"../../../chunk-z6q192p8.js";
|
|
10
10
|
import"../../../chunk-5qtpggzv.js";
|
|
11
11
|
import"../../../chunk-yvyahr2h.js";
|
|
12
12
|
import"../../../chunk-zaje5tv4.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { RATE_LIMIT_ERROR_KEYS } from './rateLimitErrorKeys';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { rateLimit } from './rateLimit';
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
RATE_LIMIT_ERROR_KEYS
|
|
4
|
+
} from "../../../chunk-yd82hdxv.js";
|
|
5
|
+
import {
|
|
6
|
+
HttpError
|
|
7
|
+
} from "../../../chunk-683sda6e.js";
|
|
8
|
+
import"../../../chunk-9nw6qekv.js";
|
|
9
|
+
import"../../../chunk-vknq69e0.js";
|
|
10
|
+
|
|
11
|
+
// source/modules/elysia/rateLimit/rateLimit.ts
|
|
12
|
+
import { Elysia } from "elysia";
|
|
13
|
+
|
|
14
|
+
// source/modules/elysia/rateLimit/stores/memoryStore.ts
|
|
15
|
+
class MemoryStore {
|
|
16
|
+
_store = new Map;
|
|
17
|
+
_cleanupInterval;
|
|
18
|
+
_cleanupTimer = null;
|
|
19
|
+
constructor(cleanupIntervalMs) {
|
|
20
|
+
this._cleanupInterval = cleanupIntervalMs ?? 300000;
|
|
21
|
+
this._startCleanup();
|
|
22
|
+
}
|
|
23
|
+
get(key) {
|
|
24
|
+
const entry = this._store.get(key);
|
|
25
|
+
if (!entry)
|
|
26
|
+
return null;
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
if (now > entry.expiresAt && entry.expiresAt !== -1) {
|
|
29
|
+
this._store.delete(key);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return entry.value;
|
|
33
|
+
}
|
|
34
|
+
setex(key, seconds, value) {
|
|
35
|
+
if (seconds <= 0)
|
|
36
|
+
return;
|
|
37
|
+
const expiresAt = Date.now() + seconds * 1000;
|
|
38
|
+
this._store.set(key, {
|
|
39
|
+
value,
|
|
40
|
+
expiresAt
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
incr(key) {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const entry = this._store.get(key);
|
|
46
|
+
if (!entry || now > entry.expiresAt && entry.expiresAt !== -1) {
|
|
47
|
+
if (entry)
|
|
48
|
+
this._store.delete(key);
|
|
49
|
+
this._store.set(key, {
|
|
50
|
+
value: "1",
|
|
51
|
+
expiresAt: -1
|
|
52
|
+
});
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
const currentValue = parseInt(entry.value) || 0;
|
|
56
|
+
const newValue = currentValue + 1;
|
|
57
|
+
this._store.set(key, {
|
|
58
|
+
value: newValue.toString(),
|
|
59
|
+
expiresAt: entry.expiresAt
|
|
60
|
+
});
|
|
61
|
+
return newValue;
|
|
62
|
+
}
|
|
63
|
+
ttl(key) {
|
|
64
|
+
const entry = this._store.get(key);
|
|
65
|
+
if (!entry)
|
|
66
|
+
return -1;
|
|
67
|
+
if (entry.expiresAt === -1)
|
|
68
|
+
return -1;
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
if (now > entry.expiresAt) {
|
|
71
|
+
this._store.delete(key);
|
|
72
|
+
return -1;
|
|
73
|
+
}
|
|
74
|
+
return Math.ceil((entry.expiresAt - now) / 1000);
|
|
75
|
+
}
|
|
76
|
+
_startCleanup() {
|
|
77
|
+
this._cleanupTimer = setInterval(() => {
|
|
78
|
+
this._cleanup();
|
|
79
|
+
}, this._cleanupInterval);
|
|
80
|
+
}
|
|
81
|
+
_cleanup() {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
for (const [key, entry] of this._store.entries())
|
|
84
|
+
if (now > entry.expiresAt && entry.expiresAt !== -1)
|
|
85
|
+
this._store.delete(key);
|
|
86
|
+
}
|
|
87
|
+
destroy() {
|
|
88
|
+
if (this._cleanupTimer) {
|
|
89
|
+
clearInterval(this._cleanupTimer);
|
|
90
|
+
this._cleanupTimer = null;
|
|
91
|
+
}
|
|
92
|
+
this._store.clear();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// source/modules/elysia/rateLimit/rateLimit.ts
|
|
97
|
+
var rateLimit = ({ store, limit, window }) => {
|
|
98
|
+
const storeInstance = store === ":memory:" || !store ? new MemoryStore : store;
|
|
99
|
+
return new Elysia({
|
|
100
|
+
name: "rateLimit",
|
|
101
|
+
seed: {
|
|
102
|
+
store,
|
|
103
|
+
limit,
|
|
104
|
+
window
|
|
105
|
+
}
|
|
106
|
+
}).onRequest(async ({ set, request, server }) => {
|
|
107
|
+
const ip = request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || server?.requestIP(request)?.address || "127.0.0.1";
|
|
108
|
+
const key = `rateLimit:${ip}`;
|
|
109
|
+
const current = await storeInstance.get(key);
|
|
110
|
+
const count = current ? parseInt(current) : 0;
|
|
111
|
+
if (count === 0)
|
|
112
|
+
await storeInstance.setex(key, window, "1");
|
|
113
|
+
else
|
|
114
|
+
await storeInstance.incr(key);
|
|
115
|
+
const newCount = await storeInstance.get(key);
|
|
116
|
+
const currentCount = newCount ? parseInt(newCount) : 0;
|
|
117
|
+
if (currentCount > limit) {
|
|
118
|
+
set.status = 429;
|
|
119
|
+
throw new HttpError({
|
|
120
|
+
message: RATE_LIMIT_ERROR_KEYS.RATE_LIMIT_EXCEEDED,
|
|
121
|
+
httpStatusCode: "TOO_MANY_REQUESTS",
|
|
122
|
+
cause: {
|
|
123
|
+
limit,
|
|
124
|
+
window,
|
|
125
|
+
remaining: 0,
|
|
126
|
+
reset: await storeInstance.ttl(key)
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
set.headers = {
|
|
131
|
+
"X-RateLimit-Limit": limit.toString(),
|
|
132
|
+
"X-RateLimit-Remaining": Math.max(0, limit - currentCount).toString(),
|
|
133
|
+
"X-RateLimit-Reset": (await storeInstance.ttl(key)).toString()
|
|
134
|
+
};
|
|
135
|
+
}).as("global");
|
|
136
|
+
};
|
|
137
|
+
export {
|
|
138
|
+
rateLimit
|
|
139
|
+
};
|
|
@@ -27,7 +27,7 @@ import type { RateLimitOptions } from './types/rateLimitOptions';
|
|
|
27
27
|
* // Create and configure the application with rate limiting
|
|
28
28
|
* const app = new Elysia()
|
|
29
29
|
* .use(rateLimit({
|
|
30
|
-
* redis,
|
|
30
|
+
* store: redis,
|
|
31
31
|
* limit: 100, // 100 requests
|
|
32
32
|
* window: 60, // per minute
|
|
33
33
|
* }))
|
|
@@ -39,7 +39,7 @@ import type { RateLimitOptions } from './types/rateLimitOptions';
|
|
|
39
39
|
* app.listen(3000);
|
|
40
40
|
* ```
|
|
41
41
|
*/
|
|
42
|
-
export declare const rateLimit: ({
|
|
42
|
+
export declare const rateLimit: ({ store, limit, window }: RateLimitOptions) => Elysia<"", {
|
|
43
43
|
decorator: {};
|
|
44
44
|
store: {};
|
|
45
45
|
derive: {};
|
|
@@ -48,7 +48,7 @@ export declare const rateLimit: ({ redis, limit, window }: RateLimitOptions) =>
|
|
|
48
48
|
typebox: {};
|
|
49
49
|
error: {};
|
|
50
50
|
}, {
|
|
51
|
-
schema: {}
|
|
51
|
+
schema: import("elysia").MergeSchema<import("elysia").MergeSchema<{}, {}, "">, {}, "">;
|
|
52
52
|
standaloneSchema: {};
|
|
53
53
|
macro: {};
|
|
54
54
|
macroFn: {};
|
|
@@ -56,8 +56,8 @@ export declare const rateLimit: ({ redis, limit, window }: RateLimitOptions) =>
|
|
|
56
56
|
}, {}, {
|
|
57
57
|
derive: {};
|
|
58
58
|
resolve: {};
|
|
59
|
-
schema:
|
|
60
|
-
standaloneSchema:
|
|
59
|
+
schema: {};
|
|
60
|
+
standaloneSchema: {};
|
|
61
61
|
}, {
|
|
62
62
|
derive: {};
|
|
63
63
|
resolve: {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { RateLimitStore } from '../../../../modules/elysia/rateLimit/types/rateLimitStore';
|
|
2
|
+
export declare class MemoryStore implements RateLimitStore {
|
|
3
|
+
/**
|
|
4
|
+
* Internal storage map.
|
|
5
|
+
*/
|
|
6
|
+
private readonly _store;
|
|
7
|
+
/**
|
|
8
|
+
* Cleanup interval (5 minutes by default).
|
|
9
|
+
*/
|
|
10
|
+
private readonly _cleanupInterval;
|
|
11
|
+
/**
|
|
12
|
+
* Timer for cleanup operations.
|
|
13
|
+
*/
|
|
14
|
+
private _cleanupTimer;
|
|
15
|
+
/**
|
|
16
|
+
* Creates instance and starts cleanup process.
|
|
17
|
+
*/
|
|
18
|
+
constructor(cleanupIntervalMs?: number);
|
|
19
|
+
/**
|
|
20
|
+
* Get current count for a key.
|
|
21
|
+
*/
|
|
22
|
+
get(key: string): string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Set key with expiration time.
|
|
25
|
+
*/
|
|
26
|
+
setex(key: string, seconds: number, value: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* Increment key value and return new count.
|
|
29
|
+
*/
|
|
30
|
+
incr(key: string): number;
|
|
31
|
+
/**
|
|
32
|
+
* Get time to live for key in seconds.
|
|
33
|
+
*/
|
|
34
|
+
ttl(key: string): number;
|
|
35
|
+
/**
|
|
36
|
+
* Start cleanup process.
|
|
37
|
+
*/
|
|
38
|
+
private _startCleanup;
|
|
39
|
+
/**
|
|
40
|
+
* Clean up expired entries.
|
|
41
|
+
*/
|
|
42
|
+
private _cleanup;
|
|
43
|
+
/**
|
|
44
|
+
* Stop cleanup and clear data.
|
|
45
|
+
*/
|
|
46
|
+
destroy(): void;
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface MemoryStoreEntry {
|
|
2
|
+
/**
|
|
3
|
+
* Current count value for the key.
|
|
4
|
+
*/
|
|
5
|
+
readonly value: string;
|
|
6
|
+
/**
|
|
7
|
+
* Timestamp when this entry expires (in milliseconds).
|
|
8
|
+
* -1 means no expiration (like Redis behavior).
|
|
9
|
+
*/
|
|
10
|
+
readonly expiresAt: number;
|
|
11
|
+
}
|
|
@@ -5,18 +5,21 @@ import type { Redis } from 'ioredis';
|
|
|
5
5
|
* @example
|
|
6
6
|
* ```ts
|
|
7
7
|
* const options: RateLimitOptions = {
|
|
8
|
-
*
|
|
8
|
+
* store: redisInstance, // Your Redis instance
|
|
9
9
|
* limit: 100, // Allow 100 requests
|
|
10
10
|
* window: 60, // Per 60 seconds
|
|
11
|
-
* message: 'You have exceeded the rate limit. Please try again later.'
|
|
12
11
|
* };
|
|
13
12
|
* ```
|
|
14
13
|
*/
|
|
15
14
|
export interface RateLimitOptions {
|
|
16
15
|
/**
|
|
17
|
-
*
|
|
16
|
+
* Storage backend for rate limit data.
|
|
17
|
+
*
|
|
18
|
+
* - If not specified, defaults to in-memory storage
|
|
19
|
+
* - Use ':memory:' to explicitly specify in-memory storage
|
|
20
|
+
* - Provide a Redis instance for persistent distributed storage
|
|
18
21
|
*/
|
|
19
|
-
readonly
|
|
22
|
+
readonly store?: ':memory:' | Redis;
|
|
20
23
|
/**
|
|
21
24
|
* Maximum number of requests allowed in the time window.
|
|
22
25
|
*
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common interface for rate limit storage backends (Memory, Redis, etc.).
|
|
3
|
+
*/
|
|
4
|
+
export interface RateLimitStore {
|
|
5
|
+
/**
|
|
6
|
+
* Get value for a key.
|
|
7
|
+
*/
|
|
8
|
+
get(key: string): string | null | Promise<string | null>;
|
|
9
|
+
/**
|
|
10
|
+
* Set key with expiration.
|
|
11
|
+
*/
|
|
12
|
+
setex(key: string, seconds: number, value: string): void | Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Increment key value.
|
|
15
|
+
*/
|
|
16
|
+
incr(key: string): number | Promise<number>;
|
|
17
|
+
/**
|
|
18
|
+
* Get time to live for key.
|
|
19
|
+
*/
|
|
20
|
+
ttl(key: string): number | Promise<number>;
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TOTP_ERROR_KEYS } from './totpErrorKeys';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const TOTP_ERROR_KEYS: {
|
|
2
|
+
readonly INVALID_BASE32_CHARACTER: "totp.error.invalid_base32_character";
|
|
3
|
+
readonly INVALID_SECRET_LENGTH: "totp.error.invalid_secret_length";
|
|
4
|
+
readonly INVALID_ALGORITHM: "totp.error.invalid_algorithm";
|
|
5
|
+
readonly INVALID_OTP_AUTH_URI: "totp.error.invalid_otp_auth_uri";
|
|
6
|
+
readonly MISSING_SECRET: "totp.error.missing_secret";
|
|
7
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { hotp, totp, verifyTotp, buildOtpAuthUri, parseOtpAuthUri, timeRemaining } from './totp';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
createCounterBuffer,
|
|
4
|
+
dynamicTruncation,
|
|
5
|
+
generateHmac
|
|
6
|
+
} from "../../chunk-4mt568fz.js";
|
|
7
|
+
import {
|
|
8
|
+
TOTP_ERROR_KEYS
|
|
9
|
+
} from "../../chunk-snqdnkk7.js";
|
|
10
|
+
import {
|
|
11
|
+
BaseError
|
|
12
|
+
} from "../../chunk-vknq69e0.js";
|
|
13
|
+
|
|
14
|
+
// source/modules/totp/totp.ts
|
|
15
|
+
import { webcrypto } from "crypto";
|
|
16
|
+
var hotp = async (secret, counter, {
|
|
17
|
+
algorithm = "SHA-1",
|
|
18
|
+
digits = 6
|
|
19
|
+
} = {}) => {
|
|
20
|
+
const counterBuffer = createCounterBuffer(counter);
|
|
21
|
+
const key = await webcrypto.subtle.importKey("raw", secret, { name: "HMAC", hash: algorithm }, false, ["sign"]);
|
|
22
|
+
const hmacArray = await generateHmac(key, counterBuffer);
|
|
23
|
+
return dynamicTruncation(hmacArray, digits);
|
|
24
|
+
};
|
|
25
|
+
var totp = async (secret, {
|
|
26
|
+
algorithm = "SHA-1",
|
|
27
|
+
digits = 6,
|
|
28
|
+
period = 30,
|
|
29
|
+
now = Date.now()
|
|
30
|
+
} = {}) => {
|
|
31
|
+
const timeStep = Math.floor(now / 1000 / period);
|
|
32
|
+
return hotp(secret, timeStep, { algorithm, digits });
|
|
33
|
+
};
|
|
34
|
+
var verifyTotp = async (secret, code, {
|
|
35
|
+
algorithm = "SHA-1",
|
|
36
|
+
digits = 6,
|
|
37
|
+
period = 30,
|
|
38
|
+
window = 0,
|
|
39
|
+
now = Date.now()
|
|
40
|
+
} = {}) => {
|
|
41
|
+
const currentTimeStep = Math.floor(now / 1000 / period);
|
|
42
|
+
for (let i = -window;i <= window; ++i) {
|
|
43
|
+
const timeStep = currentTimeStep + i;
|
|
44
|
+
const expectedCode = await hotp(secret, timeStep, { algorithm, digits });
|
|
45
|
+
if (expectedCode === code)
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
50
|
+
var buildOtpAuthUri = ({
|
|
51
|
+
secretBase32,
|
|
52
|
+
label,
|
|
53
|
+
issuer,
|
|
54
|
+
algorithm = "SHA-1",
|
|
55
|
+
digits = 6,
|
|
56
|
+
period = 30
|
|
57
|
+
}) => {
|
|
58
|
+
const encodedLabel = encodeURIComponent(label);
|
|
59
|
+
const encodedIssuer = issuer ? encodeURIComponent(issuer) : undefined;
|
|
60
|
+
let uri = `otpauth://totp/${encodedLabel}?secret=${secretBase32}`;
|
|
61
|
+
if (encodedIssuer)
|
|
62
|
+
uri += `&issuer=${encodedIssuer}`;
|
|
63
|
+
if (algorithm !== "SHA-1")
|
|
64
|
+
uri += `&algorithm=${algorithm}`;
|
|
65
|
+
if (digits !== 6)
|
|
66
|
+
uri += `&digits=${digits}`;
|
|
67
|
+
if (period !== 30)
|
|
68
|
+
uri += `&period=${period}`;
|
|
69
|
+
return uri;
|
|
70
|
+
};
|
|
71
|
+
var parseOtpAuthUri = (uri) => {
|
|
72
|
+
const url = new URL(uri);
|
|
73
|
+
if (url.protocol !== "otpauth:")
|
|
74
|
+
throw new BaseError({
|
|
75
|
+
message: TOTP_ERROR_KEYS.INVALID_OTP_AUTH_URI
|
|
76
|
+
});
|
|
77
|
+
if (url.hostname !== "totp")
|
|
78
|
+
throw new BaseError({
|
|
79
|
+
message: TOTP_ERROR_KEYS.INVALID_OTP_AUTH_URI
|
|
80
|
+
});
|
|
81
|
+
const label = decodeURIComponent(url.pathname.slice(1));
|
|
82
|
+
const secretBase32 = url.searchParams.get("secret");
|
|
83
|
+
if (!secretBase32)
|
|
84
|
+
throw new BaseError({
|
|
85
|
+
message: TOTP_ERROR_KEYS.MISSING_SECRET
|
|
86
|
+
});
|
|
87
|
+
const issuerParam = url.searchParams.get("issuer");
|
|
88
|
+
const issuer = issuerParam ? decodeURIComponent(issuerParam) : undefined;
|
|
89
|
+
const algorithm = url.searchParams.get("algorithm") || "SHA-1";
|
|
90
|
+
const digits = parseInt(url.searchParams.get("digits") || "6", 10);
|
|
91
|
+
const period = parseInt(url.searchParams.get("period") || "30", 10);
|
|
92
|
+
const result = {
|
|
93
|
+
secretBase32,
|
|
94
|
+
label,
|
|
95
|
+
algorithm,
|
|
96
|
+
digits,
|
|
97
|
+
period,
|
|
98
|
+
...issuer && { issuer }
|
|
99
|
+
};
|
|
100
|
+
return result;
|
|
101
|
+
};
|
|
102
|
+
var timeRemaining = (period = 30, now = Date.now()) => {
|
|
103
|
+
const elapsed = Math.floor(now / 1000) % period;
|
|
104
|
+
return period - elapsed;
|
|
105
|
+
};
|
|
106
|
+
export {
|
|
107
|
+
verifyTotp,
|
|
108
|
+
totp,
|
|
109
|
+
timeRemaining,
|
|
110
|
+
parseOtpAuthUri,
|
|
111
|
+
hotp,
|
|
112
|
+
buildOtpAuthUri
|
|
113
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { OtpAuthUri } from './types/otpAuthUri';
|
|
2
|
+
import type { TotpOptions } from './types/totpOptions';
|
|
3
|
+
import type { VerifyOptions } from './types/verifyOptions';
|
|
4
|
+
/**
|
|
5
|
+
* HMAC-based One-Time Password (HOTP) implementation
|
|
6
|
+
*
|
|
7
|
+
* @param secret - Secret key as bytes
|
|
8
|
+
* @param counter - Counter value
|
|
9
|
+
* @param opts - HOTP options
|
|
10
|
+
*
|
|
11
|
+
* @returns Promise resolving to the HOTP code
|
|
12
|
+
*/
|
|
13
|
+
export declare const hotp: (secret: Uint8Array, counter: number | bigint, { algorithm, digits }?: TotpOptions) => Promise<string>;
|
|
14
|
+
/**
|
|
15
|
+
* Time-based One-Time Password (TOTP) implementation
|
|
16
|
+
*
|
|
17
|
+
* @param secret - Secret key as bytes
|
|
18
|
+
* @param opts - TOTP options including current time
|
|
19
|
+
*
|
|
20
|
+
* @returns Promise resolving to the TOTP code
|
|
21
|
+
*/
|
|
22
|
+
export declare const totp: (secret: Uint8Array, { algorithm, digits, period, now }?: TotpOptions & {
|
|
23
|
+
now?: number;
|
|
24
|
+
}) => Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Verify a TOTP code against a secret
|
|
27
|
+
*
|
|
28
|
+
* @param secret - Secret key as bytes
|
|
29
|
+
* @param code - Code to verify
|
|
30
|
+
* @param opts - Verification options
|
|
31
|
+
*
|
|
32
|
+
* @returns Promise resolving to true if code is valid
|
|
33
|
+
*/
|
|
34
|
+
export declare const verifyTotp: (secret: Uint8Array, code: string, { algorithm, digits, period, window, now }?: VerifyOptions) => Promise<boolean>;
|
|
35
|
+
/**
|
|
36
|
+
* Build an OTPAuth URI for QR code generation
|
|
37
|
+
*
|
|
38
|
+
* @param params - URI parameters
|
|
39
|
+
*
|
|
40
|
+
* @returns OTPAuth URI string
|
|
41
|
+
*/
|
|
42
|
+
export declare const buildOtpAuthUri: ({ secretBase32, label, issuer, algorithm, digits, period }: OtpAuthUri) => string;
|
|
43
|
+
/**
|
|
44
|
+
* Parse an OTPAuth URI
|
|
45
|
+
*
|
|
46
|
+
* @param uri - OTPAuth URI to parse
|
|
47
|
+
*
|
|
48
|
+
* @throws ({@link BaseError}) if the URI is invalid or missing required parameters
|
|
49
|
+
*
|
|
50
|
+
* @returns Parsed URI parameters
|
|
51
|
+
*/
|
|
52
|
+
export declare const parseOtpAuthUri: (uri: string) => Required<Omit<OtpAuthUri, "issuer">> & {
|
|
53
|
+
issuer?: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Calculate remaining time until next TOTP code
|
|
57
|
+
*
|
|
58
|
+
* @param period - Time period in seconds (default: 30)
|
|
59
|
+
* @param now - Current timestamp in milliseconds (default: Date.now())
|
|
60
|
+
*
|
|
61
|
+
* @returns Seconds remaining until next code
|
|
62
|
+
*/
|
|
63
|
+
export declare const timeRemaining: (period?: number, now?: number) => number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complete OTPAuth URI data structure
|
|
3
|
+
*/
|
|
4
|
+
export interface OtpAuthUri {
|
|
5
|
+
/**
|
|
6
|
+
* Base32 encoded secret
|
|
7
|
+
*/
|
|
8
|
+
secretBase32: string;
|
|
9
|
+
/**
|
|
10
|
+
* Label for the account (usually email or username)
|
|
11
|
+
*/
|
|
12
|
+
label: string;
|
|
13
|
+
/**
|
|
14
|
+
* Issuer name (app/service name)
|
|
15
|
+
*/
|
|
16
|
+
issuer: string;
|
|
17
|
+
/**
|
|
18
|
+
* Hash algorithm
|
|
19
|
+
*
|
|
20
|
+
* @defaultValue 'SHA-1'
|
|
21
|
+
*/
|
|
22
|
+
algorithm?: 'SHA-1' | 'SHA-256' | 'SHA-512';
|
|
23
|
+
/**
|
|
24
|
+
* Number of digits
|
|
25
|
+
*
|
|
26
|
+
* @defaultValue 6
|
|
27
|
+
*/
|
|
28
|
+
digits?: 6 | 8;
|
|
29
|
+
/**
|
|
30
|
+
* Time period in seconds
|
|
31
|
+
*
|
|
32
|
+
* @defaultValue 30
|
|
33
|
+
*/
|
|
34
|
+
period?: number;
|
|
35
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for TOTP/HOTP generation
|
|
3
|
+
*/
|
|
4
|
+
export interface TotpOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Hash algorithm to use
|
|
7
|
+
*
|
|
8
|
+
* @defaultValue SHA-1
|
|
9
|
+
*/
|
|
10
|
+
algorithm?: 'SHA-1' | 'SHA-256' | 'SHA-512';
|
|
11
|
+
/**
|
|
12
|
+
* Number of digits in the code
|
|
13
|
+
*
|
|
14
|
+
* @defaultValue 6
|
|
15
|
+
*/
|
|
16
|
+
digits?: 6 | 8;
|
|
17
|
+
/**
|
|
18
|
+
* Time step in seconds for TOTP
|
|
19
|
+
*
|
|
20
|
+
* @defaultValue 30
|
|
21
|
+
*/
|
|
22
|
+
period?: number;
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TotpOptions } from './totpOptions';
|
|
2
|
+
/**
|
|
3
|
+
* Options for TOTP verification
|
|
4
|
+
*/
|
|
5
|
+
export interface VerifyOptions extends TotpOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Time window for verification (±window periods)
|
|
8
|
+
*
|
|
9
|
+
* @defaultValue 1
|
|
10
|
+
*/
|
|
11
|
+
window?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Current timestamp in milliseconds
|
|
14
|
+
*
|
|
15
|
+
* @defaultValue Date.now()
|
|
16
|
+
*/
|
|
17
|
+
now?: number;
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encode bytes to Base32 string
|
|
3
|
+
*
|
|
4
|
+
* @param input - Bytes or string to encode
|
|
5
|
+
* @param withPadding - Whether to include padding (default: true)
|
|
6
|
+
*
|
|
7
|
+
* @returns Base32 encoded string
|
|
8
|
+
*/
|
|
9
|
+
export declare const base32Encode: (input: string | Uint8Array, withPadding?: boolean) => string;
|
|
10
|
+
/**
|
|
11
|
+
* Decode Base32 string to bytes
|
|
12
|
+
*
|
|
13
|
+
* @param base32 - Base32 string to decode
|
|
14
|
+
*
|
|
15
|
+
* @throws ({@link BaseError}) if invalid Base32 character is found
|
|
16
|
+
*
|
|
17
|
+
* @returns Decoded bytes
|
|
18
|
+
*/
|
|
19
|
+
export declare const base32Decode: (base32: string) => Uint8Array;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a counter value to an 8-byte big-endian buffer
|
|
3
|
+
*
|
|
4
|
+
* @param counter - Counter value as number or bigint
|
|
5
|
+
*
|
|
6
|
+
* @returns ArrayBuffer containing the counter in big-endian format
|
|
7
|
+
*/
|
|
8
|
+
export declare const createCounterBuffer: (counter: number | bigint) => ArrayBuffer;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Perform dynamic truncation on HMAC result according to RFC 4226
|
|
3
|
+
*
|
|
4
|
+
* @param hmacArray - HMAC result as byte array
|
|
5
|
+
* @param digits - Number of digits in the final code
|
|
6
|
+
*
|
|
7
|
+
* @returns Truncated code as string with leading zeros
|
|
8
|
+
*/
|
|
9
|
+
export declare const dynamicTruncation: (hmacArray: Uint8Array, digits: number) => string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate HMAC for given data using a crypto key
|
|
3
|
+
*
|
|
4
|
+
* @param key - Crypto key for HMAC
|
|
5
|
+
* @param data - Data to sign
|
|
6
|
+
*
|
|
7
|
+
* @returns Promise resolving to HMAC as Uint8Array
|
|
8
|
+
*/
|
|
9
|
+
export declare const generateHmac: (key: CryptoKey, data: ArrayBuffer) => Promise<Uint8Array>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate cryptographically secure random bytes for TOTP secret
|
|
3
|
+
*
|
|
4
|
+
* @param length - Number of bytes to generate (default: 20)
|
|
5
|
+
*
|
|
6
|
+
* @throws ({@link BaseError}) if length is not positive
|
|
7
|
+
*
|
|
8
|
+
* @returns Uint8Array containing the random bytes
|
|
9
|
+
*/
|
|
10
|
+
export declare const generateSecretBytes: (length?: number) => Uint8Array;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { base32Decode, base32Encode } from './base32';
|
|
2
|
+
export { createCounterBuffer } from './createCounterBuffer';
|
|
3
|
+
export { generateSecretBytes } from './generateSecretBytes';
|
|
4
|
+
export { generateHmac } from './generateHmac';
|
|
5
|
+
export { dynamicTruncation } from './dynamicTruncation';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
createCounterBuffer,
|
|
4
|
+
dynamicTruncation,
|
|
5
|
+
generateHmac
|
|
6
|
+
} from "../../../chunk-4mt568fz.js";
|
|
7
|
+
import {
|
|
8
|
+
TOTP_ERROR_KEYS
|
|
9
|
+
} from "../../../chunk-snqdnkk7.js";
|
|
10
|
+
import {
|
|
11
|
+
BaseError
|
|
12
|
+
} from "../../../chunk-vknq69e0.js";
|
|
13
|
+
|
|
14
|
+
// source/modules/totp/utils/base32.ts
|
|
15
|
+
var BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
16
|
+
var base32Encode = (input, withPadding = true) => {
|
|
17
|
+
let result = "";
|
|
18
|
+
let bits = 0;
|
|
19
|
+
let value = 0;
|
|
20
|
+
const bytes = input instanceof Uint8Array ? input : new TextEncoder().encode(input);
|
|
21
|
+
for (const byte of bytes) {
|
|
22
|
+
value = value << 8 | byte;
|
|
23
|
+
bits += 8;
|
|
24
|
+
while (bits >= 5) {
|
|
25
|
+
result += BASE32_ALPHABET[value >>> bits - 5 & 31];
|
|
26
|
+
bits -= 5;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (bits > 0)
|
|
30
|
+
result += BASE32_ALPHABET[value << 5 - bits & 31];
|
|
31
|
+
if (withPadding)
|
|
32
|
+
while (result.length % 8 !== 0)
|
|
33
|
+
result += "=";
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
var base32Decode = (base32) => {
|
|
37
|
+
const cleanBase32 = base32.replace(/=+$/, "");
|
|
38
|
+
if (cleanBase32.length === 0)
|
|
39
|
+
return new Uint8Array(0);
|
|
40
|
+
const result = [];
|
|
41
|
+
let bits = 0;
|
|
42
|
+
let value = 0;
|
|
43
|
+
for (const char of cleanBase32) {
|
|
44
|
+
const charValue = BASE32_ALPHABET.indexOf(char);
|
|
45
|
+
if (charValue === -1)
|
|
46
|
+
throw new BaseError({
|
|
47
|
+
message: TOTP_ERROR_KEYS.INVALID_BASE32_CHARACTER,
|
|
48
|
+
cause: `Invalid Base32 character: ${char}`
|
|
49
|
+
});
|
|
50
|
+
value = value << 5 | charValue;
|
|
51
|
+
bits += 5;
|
|
52
|
+
if (bits >= 8) {
|
|
53
|
+
result.push(value >>> bits - 8 & 255);
|
|
54
|
+
bits -= 8;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return new Uint8Array(result);
|
|
58
|
+
};
|
|
59
|
+
// source/modules/totp/utils/generateSecretBytes.ts
|
|
60
|
+
import { getRandomValues } from "crypto";
|
|
61
|
+
var generateSecretBytes = (length = 20) => {
|
|
62
|
+
if (length <= 0)
|
|
63
|
+
throw new BaseError({
|
|
64
|
+
message: TOTP_ERROR_KEYS.INVALID_SECRET_LENGTH
|
|
65
|
+
});
|
|
66
|
+
return getRandomValues(new Uint8Array(length));
|
|
67
|
+
};
|
|
68
|
+
export {
|
|
69
|
+
generateSecretBytes,
|
|
70
|
+
generateHmac,
|
|
71
|
+
dynamicTruncation,
|
|
72
|
+
createCounterBuffer,
|
|
73
|
+
base32Encode,
|
|
74
|
+
base32Decode
|
|
75
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renames a key in an object type while preserving all other properties.
|
|
3
|
+
*
|
|
4
|
+
* @template T - The original object type
|
|
5
|
+
* @template From - The key to rename (must exist in T)
|
|
6
|
+
* @template To - The new key name
|
|
7
|
+
*/
|
|
8
|
+
export type RenameKey<T, From extends keyof T, To extends PropertyKey> = {
|
|
9
|
+
[K in keyof T as K extends From ? To : K]: T[K];
|
|
10
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrxsys/mrx-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"author": "Ruby",
|
|
5
5
|
"description": " Core provides a set of tools to help you build a microservice",
|
|
6
6
|
"type": "module",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"./modules/elysia/jwt/enums": "./dist/modules/elysia/jwt/enums/index.js",
|
|
36
36
|
"./modules/elysia/jwt/types": "./dist/modules/elysia/jwt/types/index.js",
|
|
37
37
|
"./modules/elysia/microservice": "./dist/modules/elysia/microservice/index.js",
|
|
38
|
-
"./modules/elysia/
|
|
39
|
-
"./modules/elysia/
|
|
40
|
-
"./modules/elysia/
|
|
38
|
+
"./modules/elysia/rateLimit": "./dist/modules/elysia/rateLimit/index.js",
|
|
39
|
+
"./modules/elysia/rateLimit/enums": "./dist/modules/elysia/rateLimit/enums/index.js",
|
|
40
|
+
"./modules/elysia/rateLimit/types": "./dist/modules/elysia/rateLimit/types/index.js",
|
|
41
41
|
"./modules/logger": "./dist/modules/logger/index.js",
|
|
42
42
|
"./modules/logger/enums": "./dist/modules/logger/enums/index.js",
|
|
43
43
|
"./modules/logger/events": "./dist/modules/logger/events/index.js",
|
|
@@ -50,6 +50,10 @@
|
|
|
50
50
|
"./modules/repository/types": "./dist/modules/repository/types/index.js",
|
|
51
51
|
"./modules/singletonManager": "./dist/modules/singletonManager/index.js",
|
|
52
52
|
"./modules/singletonManager/enums": "./dist/modules/singletonManager/enums/index.js",
|
|
53
|
+
"./modules/totp": "./dist/modules/totp/index.js",
|
|
54
|
+
"./modules/totp/enums": "./dist/modules/totp/enums/index.js",
|
|
55
|
+
"./modules/totp/types": "./dist/modules/totp/types/index.js",
|
|
56
|
+
"./modules/totp/utils": "./dist/modules/totp/utils/index.js",
|
|
53
57
|
"./modules/typedEventEmitter": "./dist/modules/typedEventEmitter/index.js",
|
|
54
58
|
"./modules/typedEventEmitter/types": "./dist/modules/typedEventEmitter/types/index.js",
|
|
55
59
|
"./utils": "./dist/utils/index.js",
|
|
@@ -67,20 +71,20 @@
|
|
|
67
71
|
"test": "bun test --coverage --timeout 5500"
|
|
68
72
|
},
|
|
69
73
|
"devDependencies": {
|
|
70
|
-
"@eslint/js": "^9.
|
|
74
|
+
"@eslint/js": "^9.33.0",
|
|
71
75
|
"@sinclair/typebox": "0.34.38",
|
|
72
|
-
"@stylistic/eslint-plugin": "^5.2.
|
|
73
|
-
"@types/bun": "^1.2.
|
|
76
|
+
"@stylistic/eslint-plugin": "^5.2.3",
|
|
77
|
+
"@types/bun": "^1.2.20",
|
|
74
78
|
"@types/nodemailer": "^6.4.17",
|
|
75
79
|
"elysia": "^1.3.8",
|
|
76
|
-
"eslint": "^9.
|
|
80
|
+
"eslint": "^9.33.0",
|
|
77
81
|
"globals": "^16.3.0",
|
|
78
82
|
"ioredis": "^5.7.0",
|
|
79
83
|
"jose": "^6.0.12",
|
|
80
84
|
"knex": "^3.1.0",
|
|
81
85
|
"mssql": "^11.0.1",
|
|
82
86
|
"nodemailer": "^7.0.5",
|
|
83
|
-
"typescript-eslint": "^8.
|
|
87
|
+
"typescript-eslint": "^8.39.0"
|
|
84
88
|
},
|
|
85
89
|
"peerDependencies": {
|
|
86
90
|
"@sinclair/typebox": "0.34.38",
|
package/dist/chunk-twaga0fp.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { RATELIMIT_ERROR_KEYS } from './ratelimitErrorKeys';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { rateLimit } from './ratelimit';
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
import {
|
|
3
|
-
RATELIMIT_ERROR_KEYS
|
|
4
|
-
} from "../../../chunk-twaga0fp.js";
|
|
5
|
-
import {
|
|
6
|
-
HttpError
|
|
7
|
-
} from "../../../chunk-683sda6e.js";
|
|
8
|
-
import"../../../chunk-9nw6qekv.js";
|
|
9
|
-
import"../../../chunk-vknq69e0.js";
|
|
10
|
-
|
|
11
|
-
// source/modules/elysia/ratelimit/ratelimit.ts
|
|
12
|
-
import { Elysia } from "elysia";
|
|
13
|
-
var rateLimit = ({ redis, limit, window }) => new Elysia({
|
|
14
|
-
name: "rateLimit",
|
|
15
|
-
seed: {
|
|
16
|
-
redis,
|
|
17
|
-
limit,
|
|
18
|
-
window
|
|
19
|
-
}
|
|
20
|
-
}).onRequest(async ({ set, request, server }) => {
|
|
21
|
-
const ip = request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || server?.requestIP(request)?.address || "127.0.0.1";
|
|
22
|
-
const key = `ratelimit:${ip}`;
|
|
23
|
-
const current = await redis.get(key);
|
|
24
|
-
const count = current ? parseInt(current) : 0;
|
|
25
|
-
if (count === 0)
|
|
26
|
-
await redis.setex(key, window, "1");
|
|
27
|
-
else
|
|
28
|
-
await redis.incr(key);
|
|
29
|
-
const newCount = await redis.get(key);
|
|
30
|
-
const currentCount = newCount ? parseInt(newCount) : 0;
|
|
31
|
-
if (currentCount > limit) {
|
|
32
|
-
set.status = 429;
|
|
33
|
-
throw new HttpError({
|
|
34
|
-
message: RATELIMIT_ERROR_KEYS.RATELIMIT_EXCEEDED,
|
|
35
|
-
httpStatusCode: "TOO_MANY_REQUESTS",
|
|
36
|
-
cause: {
|
|
37
|
-
limit,
|
|
38
|
-
window,
|
|
39
|
-
remaining: 0,
|
|
40
|
-
reset: await redis.ttl(key)
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
set.headers = {
|
|
45
|
-
"X-RateLimit-Limit": limit.toString(),
|
|
46
|
-
"X-RateLimit-Remaining": Math.max(0, limit - currentCount).toString(),
|
|
47
|
-
"X-RateLimit-Reset": (await redis.ttl(key)).toString()
|
|
48
|
-
};
|
|
49
|
-
}).as("scoped");
|
|
50
|
-
export {
|
|
51
|
-
rateLimit
|
|
52
|
-
};
|
|
File without changes
|
|
File without changes
|