@pondoknusa/telegram-2fa 2.2.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/dist/code-generator.d.ts +27 -0
- package/dist/code-generator.d.ts.map +1 -0
- package/dist/code-generator.js +47 -0
- package/dist/code-generator.js.map +1 -0
- package/dist/facade.d.ts +28 -0
- package/dist/facade.d.ts.map +1 -0
- package/dist/facade.js +33 -0
- package/dist/facade.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/telegram-2fa-service-provider.d.ts +17 -0
- package/dist/telegram-2fa-service-provider.d.ts.map +1 -0
- package/dist/telegram-2fa-service-provider.js +31 -0
- package/dist/telegram-2fa-service-provider.js.map +1 -0
- package/dist/two-factor-manager.d.ts +47 -0
- package/dist/two-factor-manager.d.ts.map +1 -0
- package/dist/two-factor-manager.js +150 -0
- package/dist/two-factor-manager.js.map +1 -0
- package/dist/two-factor-middleware.d.ts +51 -0
- package/dist/two-factor-middleware.d.ts.map +1 -0
- package/dist/two-factor-middleware.js +67 -0
- package/dist/two-factor-middleware.js.map +1 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pondoknusa/telegram-2fa — Secure code generator.
|
|
3
|
+
*
|
|
4
|
+
* Generates cryptographically random numeric codes and hashes them
|
|
5
|
+
* with SHA-256 so plaintext codes are never stored.
|
|
6
|
+
*/
|
|
7
|
+
export interface CodePair {
|
|
8
|
+
/** Plaintext code — sent to the user, never stored */
|
|
9
|
+
plaintext: string;
|
|
10
|
+
/** SHA-256 hex digest — stored in cache for verification */
|
|
11
|
+
hashed: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Generate a cryptographically random numeric code.
|
|
15
|
+
* @param length Number of digits (default: 6)
|
|
16
|
+
*/
|
|
17
|
+
export declare function generateCode(length?: number): CodePair;
|
|
18
|
+
/**
|
|
19
|
+
* Hash a plaintext code with SHA-256.
|
|
20
|
+
*/
|
|
21
|
+
export declare function hashCode(code: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Constant-time comparison to prevent timing attacks.
|
|
24
|
+
* Returns true if both strings are identical.
|
|
25
|
+
*/
|
|
26
|
+
export declare function constantTimeEqual(a: string, b: string): boolean;
|
|
27
|
+
//# sourceMappingURL=code-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code-generator.d.ts","sourceRoot":"","sources":["../src/code-generator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,WAAW,QAAQ;IACvB,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,GAAE,MAAU,GAAG,QAAQ,CAUzD;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAc/D"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pondoknusa/telegram-2fa — Secure code generator.
|
|
3
|
+
*
|
|
4
|
+
* Generates cryptographically random numeric codes and hashes them
|
|
5
|
+
* with SHA-256 so plaintext codes are never stored.
|
|
6
|
+
*/
|
|
7
|
+
import { randomInt, createHash } from 'node:crypto';
|
|
8
|
+
/**
|
|
9
|
+
* Generate a cryptographically random numeric code.
|
|
10
|
+
* @param length Number of digits (default: 6)
|
|
11
|
+
*/
|
|
12
|
+
export function generateCode(length = 6) {
|
|
13
|
+
const digits = [];
|
|
14
|
+
for (let i = 0; i < length; i++) {
|
|
15
|
+
digits.push(String(randomInt(0, 10)));
|
|
16
|
+
}
|
|
17
|
+
const plaintext = digits.join('');
|
|
18
|
+
return {
|
|
19
|
+
plaintext,
|
|
20
|
+
hashed: hashCode(plaintext),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Hash a plaintext code with SHA-256.
|
|
25
|
+
*/
|
|
26
|
+
export function hashCode(code) {
|
|
27
|
+
return createHash('sha256').update(code).digest('hex');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Constant-time comparison to prevent timing attacks.
|
|
31
|
+
* Returns true if both strings are identical.
|
|
32
|
+
*/
|
|
33
|
+
export function constantTimeEqual(a, b) {
|
|
34
|
+
if (a.length !== b.length) {
|
|
35
|
+
// Compare a dummy length to avoid leaking the actual length
|
|
36
|
+
const dummy = createHash('sha256').update('').digest();
|
|
37
|
+
createHash('sha256').update(a).digest(); // still consume a
|
|
38
|
+
createHash('sha256').update(b).digest(); // still consume b
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
let result = 0;
|
|
42
|
+
for (let i = 0; i < a.length; i++) {
|
|
43
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
44
|
+
}
|
|
45
|
+
return result === 0;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=code-generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code-generator.js","sourceRoot":"","sources":["../src/code-generator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AASpD;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,SAAiB,CAAC;IAC7C,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IACxC,CAAC;IACD,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClC,OAAO;QACL,SAAS;QACT,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC;KAC5B,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACzD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,CAAS,EAAE,CAAS;IACpD,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;QAC1B,4DAA4D;QAC5D,MAAM,KAAK,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;QACvD,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,kBAAkB;QAC3D,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,kBAAkB;QAC3D,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,MAAM,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,MAAM,KAAK,CAAC,CAAC;AACtB,CAAC"}
|
package/dist/facade.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pondoknusa/telegram-2fa — Facade.
|
|
3
|
+
*/
|
|
4
|
+
import type { Application } from '@pondoknusa/core';
|
|
5
|
+
import type { TwoFactorManager } from './two-factor-manager.js';
|
|
6
|
+
export declare class TwoFactor {
|
|
7
|
+
private static app;
|
|
8
|
+
static setApplication(app: Application): void;
|
|
9
|
+
static manager(): TwoFactorManager;
|
|
10
|
+
/**
|
|
11
|
+
* Mark the current session as 2FA-verified.
|
|
12
|
+
* Call this after a successful code verification.
|
|
13
|
+
*/
|
|
14
|
+
static markVerified(request: {
|
|
15
|
+
session?: {
|
|
16
|
+
put(key: string, value: unknown): void;
|
|
17
|
+
};
|
|
18
|
+
}, sessionKey?: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Check whether the current request's session is 2FA-verified.
|
|
21
|
+
*/
|
|
22
|
+
static isVerified(request: {
|
|
23
|
+
session?: {
|
|
24
|
+
get<T>(key: string): T | undefined;
|
|
25
|
+
};
|
|
26
|
+
}, sessionKey?: string): boolean;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=facade.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"facade.d.ts","sourceRoot":"","sources":["../src/facade.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAC,GAAG,CAA4B;IAE9C,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI;IAI7C,MAAM,CAAC,OAAO,IAAI,gBAAgB;IAOlC;;;OAGG;IACH,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE;QAAE,OAAO,CAAC,EAAE;YAAE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAA;SAAE,CAAA;KAAE,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI;IAKjH;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE;QAAE,OAAO,CAAC,EAAE;YAAE,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAA;SAAE,CAAA;KAAE,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO;CAK/G"}
|
package/dist/facade.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pondoknusa/telegram-2fa — Facade.
|
|
3
|
+
*/
|
|
4
|
+
import { TWO_FACTOR_MANAGER } from './telegram-2fa-service-provider.js';
|
|
5
|
+
export class TwoFactor {
|
|
6
|
+
static app = null;
|
|
7
|
+
static setApplication(app) {
|
|
8
|
+
TwoFactor.app = app;
|
|
9
|
+
}
|
|
10
|
+
static manager() {
|
|
11
|
+
if (!TwoFactor.app) {
|
|
12
|
+
throw new Error('TwoFactor facade not initialized. Call TwoFactor.setApplication(app) first.');
|
|
13
|
+
}
|
|
14
|
+
return TwoFactor.app.make(TWO_FACTOR_MANAGER);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Mark the current session as 2FA-verified.
|
|
18
|
+
* Call this after a successful code verification.
|
|
19
|
+
*/
|
|
20
|
+
static markVerified(request, sessionKey) {
|
|
21
|
+
const key = sessionKey ?? 'telegram_2fa_verified';
|
|
22
|
+
request.session?.put(key, Date.now());
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Check whether the current request's session is 2FA-verified.
|
|
26
|
+
*/
|
|
27
|
+
static isVerified(request, sessionKey) {
|
|
28
|
+
const key = sessionKey ?? 'telegram_2fa_verified';
|
|
29
|
+
const verified = request.session?.get(key);
|
|
30
|
+
return verified !== undefined && verified !== null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=facade.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"facade.js","sourceRoot":"","sources":["../src/facade.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAExE,MAAM,OAAO,SAAS;IACZ,MAAM,CAAC,GAAG,GAAuB,IAAI,CAAC;IAE9C,MAAM,CAAC,cAAc,CAAC,GAAgB;QACpC,SAAS,CAAC,GAAG,GAAG,GAAG,CAAC;IACtB,CAAC;IAED,MAAM,CAAC,OAAO;QACZ,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,6EAA6E,CAAC,CAAC;QACjG,CAAC;QACD,OAAO,SAAS,CAAC,GAAG,CAAC,IAAI,CAAmB,kBAAkB,CAAC,CAAC;IAClE,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,YAAY,CAAC,OAAiE,EAAE,UAAmB;QACxG,MAAM,GAAG,GAAG,UAAU,IAAI,uBAAuB,CAAC;QAClD,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IACxC,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,OAA6D,EAAE,UAAmB;QAClG,MAAM,GAAG,GAAG,UAAU,IAAI,uBAAuB,CAAC;QAClD,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,EAAE,GAAG,CAAS,GAAG,CAAC,CAAC;QACnD,OAAO,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI,CAAC;IACrD,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type { Telegram2FaConfig, TwoFactorCodePayload, TwoFactorVerifyResult, TelegramChatIdResolver, TwoFactorUser, } from './types.js';
|
|
2
|
+
export { generateCode, hashCode, constantTimeEqual } from './code-generator.js';
|
|
3
|
+
export type { CodePair } from './code-generator.js';
|
|
4
|
+
export { TwoFactorManager, TwoFactorError } from './two-factor-manager.js';
|
|
5
|
+
export { Telegram2FaServiceProvider, TWO_FACTOR_MANAGER, TWO_FACTOR_CONFIG, } from './telegram-2fa-service-provider.js';
|
|
6
|
+
export { createRequireTwoFactorMiddleware } from './two-factor-middleware.js';
|
|
7
|
+
export type { RequireTwoFactorOptions } from './two-factor-middleware.js';
|
|
8
|
+
export { TwoFactor } from './facade.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,EACtB,aAAa,GACd,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAChF,YAAY,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGpD,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAG3E,OAAO,EACL,0BAA0B,EAC1B,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,oCAAoC,CAAC;AAG5C,OAAO,EAAE,gCAAgC,EAAE,MAAM,4BAA4B,CAAC;AAC9E,YAAY,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAG1E,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// ── Code generator ──────────────────────────────────────────
|
|
2
|
+
export { generateCode, hashCode, constantTimeEqual } from './code-generator.js';
|
|
3
|
+
// ── Two-factor manager ──────────────────────────────────────
|
|
4
|
+
export { TwoFactorManager, TwoFactorError } from './two-factor-manager.js';
|
|
5
|
+
// ── Service provider ─────────────────────────────────────────
|
|
6
|
+
export { Telegram2FaServiceProvider, TWO_FACTOR_MANAGER, TWO_FACTOR_CONFIG, } from './telegram-2fa-service-provider.js';
|
|
7
|
+
// ── Middleware ───────────────────────────────────────────────
|
|
8
|
+
export { createRequireTwoFactorMiddleware } from './two-factor-middleware.js';
|
|
9
|
+
// ── Facade ───────────────────────────────────────────────────
|
|
10
|
+
export { TwoFactor } from './facade.js';
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,+DAA+D;AAC/D,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGhF,+DAA+D;AAC/D,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE3E,gEAAgE;AAChE,OAAO,EACL,0BAA0B,EAC1B,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,oCAAoC,CAAC;AAE5C,gEAAgE;AAChE,OAAO,EAAE,gCAAgC,EAAE,MAAM,4BAA4B,CAAC;AAG9E,gEAAgE;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pondoknusa/telegram-2fa — Service provider.
|
|
3
|
+
*/
|
|
4
|
+
import { ServiceProvider } from '@pondoknusa/core';
|
|
5
|
+
import type { TelegramChatIdResolver } from './types.js';
|
|
6
|
+
export declare const TWO_FACTOR_MANAGER = "telegram-2fa.manager";
|
|
7
|
+
export declare const TWO_FACTOR_CONFIG = "telegram-2fa.config";
|
|
8
|
+
export declare class Telegram2FaServiceProvider extends ServiceProvider {
|
|
9
|
+
register(): void;
|
|
10
|
+
/**
|
|
11
|
+
* Override to provide a custom Telegram chat ID resolver.
|
|
12
|
+
* By default, reads `telegram_chat_id` from the user model attribute
|
|
13
|
+
* or calls `routeNotificationForTelegram()`.
|
|
14
|
+
*/
|
|
15
|
+
protected resolveChatIdResolver(): TelegramChatIdResolver | undefined;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=telegram-2fa-service-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telegram-2fa-service-provider.d.ts","sourceRoot":"","sources":["../src/telegram-2fa-service-provider.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAMnD,OAAO,KAAK,EAAqB,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAE5E,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AACzD,eAAO,MAAM,iBAAiB,wBAAwB,CAAC;AAEvD,qBAAa,0BAA2B,SAAQ,eAAe;IACpD,QAAQ,IAAI,IAAI;IAezB;;;;OAIG;IACH,SAAS,CAAC,qBAAqB,IAAI,sBAAsB,GAAG,SAAS;CAGtE"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pondoknusa/telegram-2fa — Service provider.
|
|
3
|
+
*/
|
|
4
|
+
import { ServiceProvider } from '@pondoknusa/core';
|
|
5
|
+
import { TELEGRAM_BOT } from '@pondoknusa/telegram';
|
|
6
|
+
import { TwoFactorManager } from './two-factor-manager.js';
|
|
7
|
+
export const TWO_FACTOR_MANAGER = 'telegram-2fa.manager';
|
|
8
|
+
export const TWO_FACTOR_CONFIG = 'telegram-2fa.config';
|
|
9
|
+
export class Telegram2FaServiceProvider extends ServiceProvider {
|
|
10
|
+
register() {
|
|
11
|
+
this.app.bind(TWO_FACTOR_CONFIG, () => {
|
|
12
|
+
return this.app.make('config').get('telegram-2fa') ?? {};
|
|
13
|
+
});
|
|
14
|
+
this.app.bind(TWO_FACTOR_MANAGER, () => {
|
|
15
|
+
const config = this.app.make(TWO_FACTOR_CONFIG);
|
|
16
|
+
const cache = this.app.make('cache');
|
|
17
|
+
const bot = this.app.make(TELEGRAM_BOT);
|
|
18
|
+
const resolver = this.resolveChatIdResolver();
|
|
19
|
+
return new TwoFactorManager(config, cache, bot, resolver);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Override to provide a custom Telegram chat ID resolver.
|
|
24
|
+
* By default, reads `telegram_chat_id` from the user model attribute
|
|
25
|
+
* or calls `routeNotificationForTelegram()`.
|
|
26
|
+
*/
|
|
27
|
+
resolveChatIdResolver() {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=telegram-2fa-service-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telegram-2fa-service-provider.js","sourceRoot":"","sources":["../src/telegram-2fa-service-provider.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAG3D,MAAM,CAAC,MAAM,kBAAkB,GAAG,sBAAsB,CAAC;AACzD,MAAM,CAAC,MAAM,iBAAiB,GAAG,qBAAqB,CAAC;AAEvD,MAAM,OAAO,0BAA2B,SAAQ,eAAe;IACpD,QAAQ;QACf,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,iBAAiB,EAAE,GAAG,EAAE;YACpC,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAmB,QAAQ,CAAC,CAAC,GAAG,CAAoB,cAAc,CAAC,IAAI,EAAE,CAAC;QAChG,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,kBAAkB,EAAE,GAAG,EAAE;YACrC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAoB,iBAAiB,CAAC,CAAC;YACnE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAkB,OAAO,CAAC,CAAC;YACtD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAc,YAAY,CAAC,CAAC;YACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAC;YAE9C,OAAO,IAAI,gBAAgB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC5D,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;OAIG;IACO,qBAAqB;QAC7B,OAAO,SAAS,CAAC;IACnB,CAAC;CACF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pondoknusa/telegram-2fa — Two-factor authentication manager.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the 2FA lifecycle: send a code, verify, and invalidate.
|
|
5
|
+
* Codes are hashed with SHA-256 before storage in the cache store.
|
|
6
|
+
*/
|
|
7
|
+
import type { CacheRepository } from '@pondoknusa/cache';
|
|
8
|
+
import type { TelegramBot } from '@pondoknusa/telegram';
|
|
9
|
+
import type { Telegram2FaConfig, TwoFactorVerifyResult, TelegramChatIdResolver, TwoFactorUser } from './types.js';
|
|
10
|
+
export declare class TwoFactorManager {
|
|
11
|
+
private readonly config;
|
|
12
|
+
private readonly cache;
|
|
13
|
+
private readonly bot;
|
|
14
|
+
private readonly chatIdResolver?;
|
|
15
|
+
constructor(config: Telegram2FaConfig, cache: CacheRepository, bot: TelegramBot, chatIdResolver?: TelegramChatIdResolver | undefined);
|
|
16
|
+
get codeLength(): number;
|
|
17
|
+
get codeTtlSeconds(): number;
|
|
18
|
+
get maxAttempts(): number;
|
|
19
|
+
get cachePrefix(): string;
|
|
20
|
+
get sessionKey(): string;
|
|
21
|
+
get sessionTtlSeconds(): number;
|
|
22
|
+
get messageTemplate(): string;
|
|
23
|
+
private cacheKey;
|
|
24
|
+
/**
|
|
25
|
+
* Generate a new 2FA code and send it to the user's Telegram chat.
|
|
26
|
+
* Returns the plaintext code on success (for logging/testing), or the
|
|
27
|
+
* Telegram message result.
|
|
28
|
+
*/
|
|
29
|
+
send(user: TwoFactorUser): Promise<{
|
|
30
|
+
code: string;
|
|
31
|
+
messageId?: number;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Verify a user-submitted code against the stored hash.
|
|
35
|
+
* Returns a detailed result with the reason.
|
|
36
|
+
*/
|
|
37
|
+
verify(user: TwoFactorUser, code: string): Promise<TwoFactorVerifyResult>;
|
|
38
|
+
/**
|
|
39
|
+
* Invalidate any pending 2FA code for the user.
|
|
40
|
+
*/
|
|
41
|
+
invalidate(user: TwoFactorUser): Promise<void>;
|
|
42
|
+
private resolveChatId;
|
|
43
|
+
}
|
|
44
|
+
export declare class TwoFactorError extends Error {
|
|
45
|
+
constructor(message: string);
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=two-factor-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"two-factor-manager.d.ts","sourceRoot":"","sources":["../src/two-factor-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAKxD,OAAO,KAAK,EACV,iBAAiB,EAEjB,qBAAqB,EACrB,sBAAsB,EACtB,aAAa,EACd,MAAM,YAAY,CAAC;AAEpB,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;gBAHf,MAAM,EAAE,iBAAiB,EACzB,KAAK,EAAE,eAAe,EACtB,GAAG,EAAE,WAAW,EAChB,cAAc,CAAC,EAAE,sBAAsB,YAAA;IAK1D,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,IAAI,iBAAiB,IAAI,MAAM,CAE9B;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED,OAAO,CAAC,QAAQ;IAMhB;;;;OAIG;IACG,IAAI,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAuC9E;;;OAGG;IACG,MAAM,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAgD/E;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;YAMtC,aAAa;CAqB5B;AAID,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI5B"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pondoknusa/telegram-2fa — Two-factor authentication manager.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the 2FA lifecycle: send a code, verify, and invalidate.
|
|
5
|
+
* Codes are hashed with SHA-256 before storage in the cache store.
|
|
6
|
+
*/
|
|
7
|
+
import { generateCode, hashCode, constantTimeEqual } from './code-generator.js';
|
|
8
|
+
export class TwoFactorManager {
|
|
9
|
+
config;
|
|
10
|
+
cache;
|
|
11
|
+
bot;
|
|
12
|
+
chatIdResolver;
|
|
13
|
+
constructor(config, cache, bot, chatIdResolver) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.cache = cache;
|
|
16
|
+
this.bot = bot;
|
|
17
|
+
this.chatIdResolver = chatIdResolver;
|
|
18
|
+
}
|
|
19
|
+
// ── Configuration helpers ──────────────────────────────────
|
|
20
|
+
get codeLength() {
|
|
21
|
+
return this.config.code_length ?? 6;
|
|
22
|
+
}
|
|
23
|
+
get codeTtlSeconds() {
|
|
24
|
+
return this.config.code_ttl_seconds ?? 300;
|
|
25
|
+
}
|
|
26
|
+
get maxAttempts() {
|
|
27
|
+
return this.config.max_attempts ?? 3;
|
|
28
|
+
}
|
|
29
|
+
get cachePrefix() {
|
|
30
|
+
return this.config.cache_prefix ?? 'telegram-2fa';
|
|
31
|
+
}
|
|
32
|
+
get sessionKey() {
|
|
33
|
+
return this.config.session_key ?? 'telegram_2fa_verified';
|
|
34
|
+
}
|
|
35
|
+
get sessionTtlSeconds() {
|
|
36
|
+
return this.config.session_ttl_seconds ?? 3600;
|
|
37
|
+
}
|
|
38
|
+
get messageTemplate() {
|
|
39
|
+
return this.config.message_template ?? 'Your verification code: {{code}}';
|
|
40
|
+
}
|
|
41
|
+
cacheKey(userId) {
|
|
42
|
+
return `${this.cachePrefix}:${userId}`;
|
|
43
|
+
}
|
|
44
|
+
// ── Send a code ────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Generate a new 2FA code and send it to the user's Telegram chat.
|
|
47
|
+
* Returns the plaintext code on success (for logging/testing), or the
|
|
48
|
+
* Telegram message result.
|
|
49
|
+
*/
|
|
50
|
+
async send(user) {
|
|
51
|
+
// Invalidate any existing code first
|
|
52
|
+
await this.invalidate(user);
|
|
53
|
+
const { plaintext, hashed } = generateCode(this.codeLength);
|
|
54
|
+
const chatId = await this.resolveChatId(user);
|
|
55
|
+
if (!chatId) {
|
|
56
|
+
throw new TwoFactorError(`Cannot send 2FA code: user ${user.getAuthIdentifier()} has no Telegram chat ID configured.`);
|
|
57
|
+
}
|
|
58
|
+
// Store the hash in cache with TTL
|
|
59
|
+
const payload = {
|
|
60
|
+
hash: hashed,
|
|
61
|
+
attempts_remaining: this.maxAttempts,
|
|
62
|
+
issued_at: Date.now(),
|
|
63
|
+
};
|
|
64
|
+
await this.cache.put(this.cacheKey(user.getAuthIdentifier()), payload, this.codeTtlSeconds);
|
|
65
|
+
// Send via Telegram
|
|
66
|
+
const message = this.messageTemplate.replace(/\{\{code\}\}/g, plaintext);
|
|
67
|
+
const result = await this.bot.sendMessage({
|
|
68
|
+
chat_id: chatId,
|
|
69
|
+
text: message,
|
|
70
|
+
parse_mode: this.config.parse_mode ?? 'Markdown',
|
|
71
|
+
});
|
|
72
|
+
return { code: plaintext, messageId: result.message_id };
|
|
73
|
+
}
|
|
74
|
+
// ── Verify a code ──────────────────────────────────────────
|
|
75
|
+
/**
|
|
76
|
+
* Verify a user-submitted code against the stored hash.
|
|
77
|
+
* Returns a detailed result with the reason.
|
|
78
|
+
*/
|
|
79
|
+
async verify(user, code) {
|
|
80
|
+
const userId = user.getAuthIdentifier();
|
|
81
|
+
const payload = await this.cache.get(this.cacheKey(userId));
|
|
82
|
+
if (!payload) {
|
|
83
|
+
return { success: false, reason: 'expired' };
|
|
84
|
+
}
|
|
85
|
+
// Check expiry (belt-and-suspenders — cache TTL should handle it, but
|
|
86
|
+
// verify in case the cache backend doesn't expire reliably)
|
|
87
|
+
const elapsed = Date.now() - payload.issued_at;
|
|
88
|
+
if (elapsed > this.codeTtlSeconds * 1000) {
|
|
89
|
+
await this.cache.forget(this.cacheKey(userId));
|
|
90
|
+
return { success: false, reason: 'expired' };
|
|
91
|
+
}
|
|
92
|
+
if (payload.attempts_remaining <= 0) {
|
|
93
|
+
await this.cache.forget(this.cacheKey(userId));
|
|
94
|
+
return { success: false, reason: 'max_attempts' };
|
|
95
|
+
}
|
|
96
|
+
// Decrement attempts
|
|
97
|
+
payload.attempts_remaining -= 1;
|
|
98
|
+
await this.cache.put(this.cacheKey(userId), payload, this.codeTtlSeconds);
|
|
99
|
+
if (!constantTimeEqual(code, payload.hash)) {
|
|
100
|
+
// Actually compare the hash of the submitted code against the stored hash
|
|
101
|
+
const submittedHash = hashCode(code);
|
|
102
|
+
if (!constantTimeEqual(submittedHash, payload.hash)) {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
reason: 'invalid',
|
|
106
|
+
attempts_remaining: payload.attempts_remaining,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Success — invalidate the code so it can't be reused
|
|
111
|
+
await this.cache.forget(this.cacheKey(userId));
|
|
112
|
+
// Mark the session as 2FA verified (if session is available)
|
|
113
|
+
// This is done by the middleware or caller
|
|
114
|
+
return { success: true, reason: 'ok' };
|
|
115
|
+
}
|
|
116
|
+
// ── Invalidate ─────────────────────────────────────────────
|
|
117
|
+
/**
|
|
118
|
+
* Invalidate any pending 2FA code for the user.
|
|
119
|
+
*/
|
|
120
|
+
async invalidate(user) {
|
|
121
|
+
await this.cache.forget(this.cacheKey(user.getAuthIdentifier()));
|
|
122
|
+
}
|
|
123
|
+
// ── Chat ID resolution ─────────────────────────────────────
|
|
124
|
+
async resolveChatId(user) {
|
|
125
|
+
// Use the custom resolver if provided
|
|
126
|
+
if (this.chatIdResolver) {
|
|
127
|
+
return this.chatIdResolver(user);
|
|
128
|
+
}
|
|
129
|
+
// Try checking the user model for a telegram_chat_id attribute
|
|
130
|
+
if (typeof user.getAttribute === 'function') {
|
|
131
|
+
const chatId = user.getAttribute('telegram_chat_id');
|
|
132
|
+
if (chatId !== undefined && chatId !== null) {
|
|
133
|
+
return chatId;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Try routeNotificationForTelegram
|
|
137
|
+
if (typeof user.routeNotificationForTelegram === 'function') {
|
|
138
|
+
return user.routeNotificationForTelegram();
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ── Error ────────────────────────────────────────────────────
|
|
144
|
+
export class TwoFactorError extends Error {
|
|
145
|
+
constructor(message) {
|
|
146
|
+
super(message);
|
|
147
|
+
this.name = 'TwoFactorError';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=two-factor-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"two-factor-manager.js","sourceRoot":"","sources":["../src/two-factor-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pondoknusa/telegram-2fa — Middleware for 2FA challenge enforcement.
|
|
3
|
+
*
|
|
4
|
+
* Attach to routes or groups that should require a verified 2FA session.
|
|
5
|
+
* The user must have completed a 2FA verification within the session TTL
|
|
6
|
+
* window, otherwise the middleware returns a 403 JSON error or redirects.
|
|
7
|
+
*
|
|
8
|
+
* Usage in routes/web.ts:
|
|
9
|
+
*
|
|
10
|
+
* import { createRequireTwoFactorMiddleware } from '@pondoknusa/telegram-2fa';
|
|
11
|
+
*
|
|
12
|
+
* Route.middleware('2fa', createRequireTwoFactorMiddleware()).get('/admin', handler);
|
|
13
|
+
*
|
|
14
|
+
* Or with custom behaviour:
|
|
15
|
+
*
|
|
16
|
+
* Route.middleware('2fa', createRequireTwoFactorMiddleware({
|
|
17
|
+
* onChallengeRequired: (request) => Response.json({ error: '2FA required' }, 401),
|
|
18
|
+
* })).get('/admin', handler);
|
|
19
|
+
*
|
|
20
|
+
* The verification flow:
|
|
21
|
+
* 1. User authenticates (password / session login)
|
|
22
|
+
* 2. Application calls TwoFactor.send(user) to send code via Telegram
|
|
23
|
+
* 3. User submits the code to a verify endpoint
|
|
24
|
+
* 4. Application calls TwoFactor.verify(user, code)
|
|
25
|
+
* 5. On success, mark the session: TwoFactor.markVerified(request, user)
|
|
26
|
+
* 6. The middleware checks this session flag on subsequent requests
|
|
27
|
+
*/
|
|
28
|
+
import type { Middleware, PondoknusaRequest } from '@pondoknusa/http';
|
|
29
|
+
export interface RequireTwoFactorOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Custom JSON body when 2FA challenge is required.
|
|
32
|
+
* Default: { error: '2FA required', code: '2FA_REQUIRED' }
|
|
33
|
+
*/
|
|
34
|
+
onChallengeRequired?: (request: PondoknusaRequest) => Response;
|
|
35
|
+
/**
|
|
36
|
+
* Session key to check for 2FA verification.
|
|
37
|
+
* Default: 'telegram_2fa_verified'
|
|
38
|
+
*/
|
|
39
|
+
sessionKey?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Whether to skip 2FA for GET/HEAD requests (allows page loads).
|
|
42
|
+
* The user will get challenged when they try a mutating action.
|
|
43
|
+
* Default: false
|
|
44
|
+
*/
|
|
45
|
+
skipSafeMethods?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Create a middleware that requires 2FA verification in the session.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createRequireTwoFactorMiddleware(options?: RequireTwoFactorOptions): Middleware;
|
|
51
|
+
//# sourceMappingURL=two-factor-middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"two-factor-middleware.d.ts","sourceRoot":"","sources":["../src/two-factor-middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAGtE,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,QAAQ,CAAC;IAE/D;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;GAEG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,GAAE,uBAA4B,GACpC,UAAU,CA2CZ"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pondoknusa/telegram-2fa — Middleware for 2FA challenge enforcement.
|
|
3
|
+
*
|
|
4
|
+
* Attach to routes or groups that should require a verified 2FA session.
|
|
5
|
+
* The user must have completed a 2FA verification within the session TTL
|
|
6
|
+
* window, otherwise the middleware returns a 403 JSON error or redirects.
|
|
7
|
+
*
|
|
8
|
+
* Usage in routes/web.ts:
|
|
9
|
+
*
|
|
10
|
+
* import { createRequireTwoFactorMiddleware } from '@pondoknusa/telegram-2fa';
|
|
11
|
+
*
|
|
12
|
+
* Route.middleware('2fa', createRequireTwoFactorMiddleware()).get('/admin', handler);
|
|
13
|
+
*
|
|
14
|
+
* Or with custom behaviour:
|
|
15
|
+
*
|
|
16
|
+
* Route.middleware('2fa', createRequireTwoFactorMiddleware({
|
|
17
|
+
* onChallengeRequired: (request) => Response.json({ error: '2FA required' }, 401),
|
|
18
|
+
* })).get('/admin', handler);
|
|
19
|
+
*
|
|
20
|
+
* The verification flow:
|
|
21
|
+
* 1. User authenticates (password / session login)
|
|
22
|
+
* 2. Application calls TwoFactor.send(user) to send code via Telegram
|
|
23
|
+
* 3. User submits the code to a verify endpoint
|
|
24
|
+
* 4. Application calls TwoFactor.verify(user, code)
|
|
25
|
+
* 5. On success, mark the session: TwoFactor.markVerified(request, user)
|
|
26
|
+
* 6. The middleware checks this session flag on subsequent requests
|
|
27
|
+
*/
|
|
28
|
+
import { Response } from '@pondoknusa/http';
|
|
29
|
+
/**
|
|
30
|
+
* Create a middleware that requires 2FA verification in the session.
|
|
31
|
+
*/
|
|
32
|
+
export function createRequireTwoFactorMiddleware(options = {}) {
|
|
33
|
+
const sessionKey = options.sessionKey ?? 'telegram_2fa_verified';
|
|
34
|
+
return async (request, next) => {
|
|
35
|
+
// Skip safe methods if configured
|
|
36
|
+
if (options.skipSafeMethods && ['GET', 'HEAD', 'OPTIONS'].includes(request.method)) {
|
|
37
|
+
return next();
|
|
38
|
+
}
|
|
39
|
+
const user = request.user;
|
|
40
|
+
// No authenticated user — let other auth middleware handle it
|
|
41
|
+
if (!user) {
|
|
42
|
+
return next();
|
|
43
|
+
}
|
|
44
|
+
// Check for verified flag in session
|
|
45
|
+
const verified = request.session?.get(sessionKey);
|
|
46
|
+
if (verified) {
|
|
47
|
+
// Check if the verification is still within the TTL window
|
|
48
|
+
const ttl = options.sessionTtl ?? 3600;
|
|
49
|
+
const elapsed = (Date.now() - verified) / 1000;
|
|
50
|
+
if (elapsed < ttl) {
|
|
51
|
+
return next();
|
|
52
|
+
}
|
|
53
|
+
// Session TTL expired — clear and require re-verification
|
|
54
|
+
request.session?.forget(sessionKey);
|
|
55
|
+
}
|
|
56
|
+
// 2FA required
|
|
57
|
+
if (options.onChallengeRequired) {
|
|
58
|
+
return options.onChallengeRequired(request);
|
|
59
|
+
}
|
|
60
|
+
return Response.json({
|
|
61
|
+
error: 'Two-factor authentication required.',
|
|
62
|
+
message: 'Please verify your identity via Telegram.',
|
|
63
|
+
code: '2FA_REQUIRED',
|
|
64
|
+
}, { status: 403 });
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=two-factor-middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"two-factor-middleware.js","sourceRoot":"","sources":["../src/two-factor-middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAyB5C;;GAEG;AACH,MAAM,UAAU,gCAAgC,CAC9C,UAAmC,EAAE;IAErC,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,uBAAuB,CAAC;IAEjE,OAAO,KAAK,EAAE,OAA0B,EAAE,IAA6B,EAAE,EAAE;QACzE,kCAAkC;QAClC,IAAI,OAAO,CAAC,eAAe,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACnF,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;QAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAA4B,CAAC;QAElD,8DAA8D;QAC9D,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;QAED,qCAAqC;QACrC,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,EAAE,GAAG,CAAS,UAAU,CAAC,CAAC;QAC1D,IAAI,QAAQ,EAAE,CAAC;YACb,2DAA2D;YAC3D,MAAM,GAAG,GAAI,OAAe,CAAC,UAAU,IAAI,IAAI,CAAC;YAChD,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC,GAAG,IAAI,CAAC;YAC/C,IAAI,OAAO,GAAG,GAAG,EAAE,CAAC;gBAClB,OAAO,IAAI,EAAE,CAAC;YAChB,CAAC;YACD,0DAA0D;YAC1D,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;QACtC,CAAC;QAED,eAAe;QACf,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;YAChC,OAAO,OAAO,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;QAC9C,CAAC;QAED,OAAO,QAAQ,CAAC,IAAI,CAClB;YACE,KAAK,EAAE,qCAAqC;YAC5C,OAAO,EAAE,2CAA2C;YACpD,IAAI,EAAE,cAAc;SACrB,EACD,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pondoknusa/telegram-2fa — Type definitions.
|
|
3
|
+
*/
|
|
4
|
+
import type { Model } from '@pondoknusa/database';
|
|
5
|
+
export interface Telegram2FaConfig {
|
|
6
|
+
/** Length of the generated code. Default: 6 */
|
|
7
|
+
code_length?: number;
|
|
8
|
+
/** Code expiration in seconds. Default: 300 (5 min) */
|
|
9
|
+
code_ttl_seconds?: number;
|
|
10
|
+
/** Max failed verification attempts before invalidation. Default: 3 */
|
|
11
|
+
max_attempts?: number;
|
|
12
|
+
/** Cache store connection name. Default: uses app default */
|
|
13
|
+
cache_connection?: string;
|
|
14
|
+
/** Cache key prefix. Default: 'telegram-2fa' */
|
|
15
|
+
cache_prefix?: string;
|
|
16
|
+
/** Telegram message template. Default: 'Your verification code: {{code}}' */
|
|
17
|
+
message_template?: string;
|
|
18
|
+
/** Parse mode for the Telegram message. Default: 'Markdown' */
|
|
19
|
+
parse_mode?: 'Markdown' | 'MarkdownV2' | 'HTML';
|
|
20
|
+
/** Whether to require 2FA for all authenticated routes by default. Default: false */
|
|
21
|
+
require_all?: boolean;
|
|
22
|
+
/** Session key used to store 2FA verification state. Default: 'telegram_2fa_verified' */
|
|
23
|
+
session_key?: string;
|
|
24
|
+
/** How long a 2FA session verification lasts (seconds). Default: 3600 (1 hour) */
|
|
25
|
+
session_ttl_seconds?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface TwoFactorCodePayload {
|
|
28
|
+
/** SHA-256 hash of the code */
|
|
29
|
+
hash: string;
|
|
30
|
+
/** Remaining verification attempts */
|
|
31
|
+
attempts_remaining: number;
|
|
32
|
+
/** When the code was issued (Unix ms) */
|
|
33
|
+
issued_at: number;
|
|
34
|
+
}
|
|
35
|
+
export interface TwoFactorVerifyResult {
|
|
36
|
+
success: boolean;
|
|
37
|
+
reason?: 'expired' | 'max_attempts' | 'invalid' | 'ok';
|
|
38
|
+
attempts_remaining?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface TelegramChatIdResolver {
|
|
41
|
+
(user: Model & Record<string, unknown>): number | string | Promise<number | string>;
|
|
42
|
+
}
|
|
43
|
+
/** A user that can authenticate and has model attributes */
|
|
44
|
+
export type TwoFactorUser = Model & {
|
|
45
|
+
getAuthIdentifier(): string | number;
|
|
46
|
+
} & Record<string, unknown>;
|
|
47
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAIlD,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gDAAgD;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,UAAU,CAAC,EAAE,UAAU,GAAG,YAAY,GAAG,MAAM,CAAC;IAChD,qFAAqF;IACrF,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yFAAyF;IACzF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kFAAkF;IAClF,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAID,MAAM,WAAW,oBAAoB;IACnC,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,sCAAsC;IACtC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;CACnB;AAID,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,SAAS,GAAG,cAAc,GAAG,SAAS,GAAG,IAAI,CAAC;IACvD,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAID,MAAM,WAAW,sBAAsB;IACrC,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;CACrF;AAED,4DAA4D;AAC5D,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG;IAAE,iBAAiB,IAAI,MAAM,GAAG,MAAM,CAAC;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pondoknusa/telegram-2fa",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc -p tsconfig.json",
|
|
15
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@pondoknusa/auth": "2.2.0",
|
|
19
|
+
"@pondoknusa/cache": "2.2.0",
|
|
20
|
+
"@pondoknusa/config": "2.2.0",
|
|
21
|
+
"@pondoknusa/core": "2.2.0",
|
|
22
|
+
"@pondoknusa/http": "2.2.0",
|
|
23
|
+
"@pondoknusa/support": "2.2.0",
|
|
24
|
+
"@pondoknusa/telegram": "2.2.0"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=26"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^26.0.1"
|
|
34
|
+
},
|
|
35
|
+
"description": "Two-factor auth via Telegram for Pondoknusa",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/pondoknusa/pondoknusa.git",
|
|
40
|
+
"directory": "packages/telegram-2fa"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
}
|
|
45
|
+
}
|