@nauth-toolkit/core 0.1.0 → 0.1.3
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/LICENSE +90 -0
- package/README.md +30 -0
- package/package.json +7 -2
- package/jest.config.js +0 -15
- package/jest.setup.ts +0 -6
- package/src/adapters/database-columns.ts +0 -165
- package/src/adapters/express.adapter.ts +0 -385
- package/src/adapters/fastify.adapter.ts +0 -416
- package/src/adapters/index.ts +0 -16
- package/src/adapters/storage.factory.ts +0 -143
- package/src/bootstrap.ts +0 -374
- package/src/dto/auth-challenge.dto.ts +0 -231
- package/src/dto/auth-response.dto.ts +0 -253
- package/src/dto/challenge-response.dto.ts +0 -234
- package/src/dto/change-password-request.dto.ts +0 -50
- package/src/dto/change-password-response.dto.ts +0 -29
- package/src/dto/change-password.dto.ts +0 -57
- package/src/dto/error-response.dto.ts +0 -136
- package/src/dto/get-available-methods.dto.ts +0 -55
- package/src/dto/get-challenge-data-response.dto.ts +0 -28
- package/src/dto/get-challenge-data.dto.ts +0 -69
- package/src/dto/get-client-info.dto.ts +0 -104
- package/src/dto/get-device-token-response.dto.ts +0 -25
- package/src/dto/get-events-by-type.dto.ts +0 -76
- package/src/dto/get-ip-address-response.dto.ts +0 -24
- package/src/dto/get-mfa-status.dto.ts +0 -94
- package/src/dto/get-risk-assessment-history.dto.ts +0 -39
- package/src/dto/get-session-id-response.dto.ts +0 -25
- package/src/dto/get-setup-data-response.dto.ts +0 -31
- package/src/dto/get-setup-data.dto.ts +0 -75
- package/src/dto/get-suspicious-activity.dto.ts +0 -42
- package/src/dto/get-user-agent-response.dto.ts +0 -23
- package/src/dto/get-user-auth-history.dto.ts +0 -95
- package/src/dto/get-user-by-email.dto.ts +0 -61
- package/src/dto/get-user-by-id.dto.ts +0 -46
- package/src/dto/get-user-devices.dto.ts +0 -53
- package/src/dto/get-user-response.dto.ts +0 -17
- package/src/dto/has-provider.dto.ts +0 -56
- package/src/dto/index.ts +0 -57
- package/src/dto/is-trusted-device-response.dto.ts +0 -34
- package/src/dto/list-providers-response.dto.ts +0 -23
- package/src/dto/login.dto.ts +0 -95
- package/src/dto/logout-all-response.dto.ts +0 -24
- package/src/dto/logout-all.dto.ts +0 -65
- package/src/dto/logout-response.dto.ts +0 -25
- package/src/dto/logout.dto.ts +0 -64
- package/src/dto/refresh-token.dto.ts +0 -36
- package/src/dto/remove-devices.dto.ts +0 -85
- package/src/dto/resend-code-response.dto.ts +0 -32
- package/src/dto/resend-code.dto.ts +0 -51
- package/src/dto/reset-password.dto.ts +0 -115
- package/src/dto/respond-challenge.dto.ts +0 -272
- package/src/dto/set-mfa-exemption.dto.ts +0 -112
- package/src/dto/set-must-change-password-response.dto.ts +0 -27
- package/src/dto/set-must-change-password.dto.ts +0 -46
- package/src/dto/set-preferred-method.dto.ts +0 -80
- package/src/dto/setup-mfa.dto.ts +0 -98
- package/src/dto/signup.dto.ts +0 -174
- package/src/dto/social-auth.dto.ts +0 -422
- package/src/dto/trust-device-response.dto.ts +0 -30
- package/src/dto/trust-device.dto.ts +0 -9
- package/src/dto/update-user-attributes-request.dto.ts +0 -51
- package/src/dto/user-response.dto.ts +0 -138
- package/src/dto/user-update.dto.ts +0 -222
- package/src/dto/verify-email.dto.ts +0 -313
- package/src/dto/verify-mfa-code.dto.ts +0 -103
- package/src/dto/verify-phone-by-sub.dto.ts +0 -78
- package/src/dto/verify-phone.dto.ts +0 -245
- package/src/entities/auth-audit.entity.ts +0 -232
- package/src/entities/challenge-session.entity.ts +0 -116
- package/src/entities/index.ts +0 -29
- package/src/entities/login-attempt.entity.ts +0 -64
- package/src/entities/mfa-device.entity.ts +0 -151
- package/src/entities/rate-limit.entity.ts +0 -44
- package/src/entities/session.entity.ts +0 -180
- package/src/entities/social-account.entity.ts +0 -96
- package/src/entities/storage-lock.entity.ts +0 -39
- package/src/entities/trusted-device.entity.ts +0 -112
- package/src/entities/user.entity.ts +0 -243
- package/src/entities/verification-token.entity.ts +0 -141
- package/src/enums/auth-audit-event-type.enum.ts +0 -360
- package/src/enums/error-codes.enum.ts +0 -420
- package/src/enums/mfa-method.enum.ts +0 -97
- package/src/enums/risk-factor.enum.ts +0 -111
- package/src/exceptions/nauth.exception.ts +0 -231
- package/src/handlers/auth.handler.ts +0 -260
- package/src/handlers/client-info.handler.ts +0 -101
- package/src/handlers/csrf.handler.ts +0 -156
- package/src/handlers/token-delivery.handler.ts +0 -118
- package/src/index.ts +0 -118
- package/src/interfaces/client-info.interface.ts +0 -85
- package/src/interfaces/config.interface.ts +0 -2135
- package/src/interfaces/entities.interface.ts +0 -226
- package/src/interfaces/index.ts +0 -15
- package/src/interfaces/logger.interface.ts +0 -283
- package/src/interfaces/mfa-provider.interface.ts +0 -154
- package/src/interfaces/oauth.interface.ts +0 -148
- package/src/interfaces/provider.interface.ts +0 -47
- package/src/interfaces/social-auth-provider.interface.ts +0 -131
- package/src/interfaces/storage-adapter.interface.ts +0 -82
- package/src/interfaces/template.interface.ts +0 -510
- package/src/interfaces/token-verifier.interface.ts +0 -110
- package/src/internal.ts +0 -178
- package/src/platform/interfaces.ts +0 -299
- package/src/schemas/auth-config.schema.ts +0 -646
- package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
- package/src/services/adaptive-mfa-decision.service.ts +0 -457
- package/src/services/auth-audit.service.spec.ts +0 -675
- package/src/services/auth-audit.service.ts +0 -558
- package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
- package/src/services/auth-challenge-helper.service.ts +0 -825
- package/src/services/auth-flow-context-builder.service.ts +0 -520
- package/src/services/auth-flow-rules.ts +0 -202
- package/src/services/auth-flow-state-definitions.ts +0 -190
- package/src/services/auth-flow-state-machine.service.ts +0 -207
- package/src/services/auth-flow-state-machine.types.ts +0 -316
- package/src/services/auth.service.spec.ts +0 -4195
- package/src/services/auth.service.ts +0 -3727
- package/src/services/challenge.service.spec.ts +0 -1363
- package/src/services/challenge.service.ts +0 -696
- package/src/services/client-info.service.spec.ts +0 -572
- package/src/services/client-info.service.ts +0 -374
- package/src/services/csrf.service.ts +0 -54
- package/src/services/email-verification.service.spec.ts +0 -1229
- package/src/services/email-verification.service.ts +0 -578
- package/src/services/geo-location.service.spec.ts +0 -603
- package/src/services/geo-location.service.ts +0 -599
- package/src/services/index.ts +0 -13
- package/src/services/jwt.service.spec.ts +0 -882
- package/src/services/jwt.service.ts +0 -621
- package/src/services/mfa-base.service.spec.ts +0 -246
- package/src/services/mfa-base.service.ts +0 -611
- package/src/services/mfa.service.spec.ts +0 -693
- package/src/services/mfa.service.ts +0 -960
- package/src/services/password.service.spec.ts +0 -166
- package/src/services/password.service.ts +0 -309
- package/src/services/phone-verification.service.spec.ts +0 -1120
- package/src/services/phone-verification.service.ts +0 -751
- package/src/services/risk-detection.service.spec.ts +0 -1292
- package/src/services/risk-detection.service.ts +0 -1012
- package/src/services/risk-scoring.service.spec.ts +0 -204
- package/src/services/risk-scoring.service.ts +0 -131
- package/src/services/session.service.spec.ts +0 -1293
- package/src/services/session.service.ts +0 -803
- package/src/services/social-account.service.spec.ts +0 -725
- package/src/services/social-auth-base.service.spec.ts +0 -418
- package/src/services/social-auth-base.service.ts +0 -581
- package/src/services/social-auth.service.spec.ts +0 -238
- package/src/services/social-auth.service.ts +0 -436
- package/src/services/social-provider-registry.service.spec.ts +0 -238
- package/src/services/social-provider-registry.service.ts +0 -122
- package/src/services/trusted-device.service.spec.ts +0 -505
- package/src/services/trusted-device.service.ts +0 -339
- package/src/storage/account-lockout-storage.service.spec.ts +0 -310
- package/src/storage/account-lockout-storage.service.ts +0 -89
- package/src/storage/index.ts +0 -3
- package/src/storage/memory-storage.adapter.ts +0 -443
- package/src/storage/rate-limit-storage.service.spec.ts +0 -247
- package/src/storage/rate-limit-storage.service.ts +0 -38
- package/src/templates/html-template.engine.spec.ts +0 -161
- package/src/templates/html-template.engine.ts +0 -688
- package/src/templates/index.ts +0 -7
- package/src/utils/common-passwords.spec.ts +0 -230
- package/src/utils/common-passwords.ts +0 -170
- package/src/utils/context-storage.ts +0 -188
- package/src/utils/cookie-names.util.ts +0 -67
- package/src/utils/cookies.util.ts +0 -94
- package/src/utils/index.ts +0 -12
- package/src/utils/ip-extractor.spec.ts +0 -330
- package/src/utils/ip-extractor.ts +0 -220
- package/src/utils/nauth-logger.spec.ts +0 -388
- package/src/utils/nauth-logger.ts +0 -215
- package/src/utils/pii-redactor.spec.ts +0 -130
- package/src/utils/pii-redactor.ts +0 -288
- package/src/utils/setup/get-repositories.ts +0 -140
- package/src/utils/setup/init-services.ts +0 -422
- package/src/utils/setup/init-social.ts +0 -189
- package/src/utils/setup/init-storage.ts +0 -94
- package/src/utils/setup/register-mfa.ts +0 -165
- package/src/utils/setup/run-nauth-migrations.ts +0 -61
- package/src/utils/token-delivery-policy.ts +0 -38
- package/src/validators/template.validator.ts +0 -219
- package/tsconfig.json +0 -37
- package/tsconfig.lint.json +0 -6
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { AccountLockoutStorage, StorageAdapter } from '../interfaces/storage-adapter.interface';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Account lockout storage implementation using StorageAdapter (Platform-Agnostic)
|
|
5
|
-
*
|
|
6
|
-
* SECURITY: Uses IP addresses instead of user identifiers to prevent
|
|
7
|
-
* attackers from locking out legitimate users by guessing their email/username.
|
|
8
|
-
*/
|
|
9
|
-
export class AccountLockoutStorageService implements AccountLockoutStorage {
|
|
10
|
-
private readonly keyPrefix = 'nauth:lockout:ip:';
|
|
11
|
-
private readonly lockKeyPrefix = 'nauth:locked:ip:';
|
|
12
|
-
|
|
13
|
-
constructor(private readonly storageAdapter: StorageAdapter) {}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Record failed login attempt for an IP address
|
|
17
|
-
* @param ipAddress - IP address that made the failed attempt
|
|
18
|
-
* @returns Number of failed attempts for this IP
|
|
19
|
-
*/
|
|
20
|
-
async recordFailedAttempt(ipAddress: string): Promise<number> {
|
|
21
|
-
const key = this.getKey(ipAddress);
|
|
22
|
-
return await this.storageAdapter.incr(key);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Get failed attempts count for an IP address
|
|
27
|
-
* @param ipAddress - IP address to check
|
|
28
|
-
* @returns Number of failed attempts
|
|
29
|
-
*/
|
|
30
|
-
async getFailedAttempts(ipAddress: string): Promise<number> {
|
|
31
|
-
const key = this.getKey(ipAddress);
|
|
32
|
-
const value = await this.storageAdapter.get(key);
|
|
33
|
-
return value ? parseInt(value, 10) : 0;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Check if an IP address is locked out
|
|
38
|
-
* @param ipAddress - IP address to check
|
|
39
|
-
* @returns True if IP is locked out
|
|
40
|
-
*/
|
|
41
|
-
async isAccountLocked(ipAddress: string): Promise<boolean> {
|
|
42
|
-
const lockKey = this.getLockKey(ipAddress);
|
|
43
|
-
return await this.storageAdapter.exists(lockKey);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Lock an IP address for a specified duration
|
|
48
|
-
* @param ipAddress - IP address to lock
|
|
49
|
-
* @param duration - Lock duration in seconds
|
|
50
|
-
* @param reason - Reason for lockout
|
|
51
|
-
*/
|
|
52
|
-
async blockIpAdresss(ipAddress: string, duration: number, reason: string): Promise<void> {
|
|
53
|
-
const lockKey = this.getLockKey(ipAddress);
|
|
54
|
-
const lockData = JSON.stringify({
|
|
55
|
-
reason,
|
|
56
|
-
lockedAt: new Date().toISOString(),
|
|
57
|
-
lockedUntil: new Date(Date.now() + duration * 1000).toISOString(),
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
await this.storageAdapter.set(lockKey, lockData, duration);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Unlock an IP address and reset failed attempts
|
|
65
|
-
* @param ipAddress - IP address to unlock
|
|
66
|
-
*/
|
|
67
|
-
async unblockIPAdress(ipAddress: string): Promise<void> {
|
|
68
|
-
const lockKey = this.getLockKey(ipAddress);
|
|
69
|
-
await this.storageAdapter.del(lockKey);
|
|
70
|
-
await this.resetFailedAttempts(ipAddress);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Reset failed attempts counter for an IP address
|
|
75
|
-
* @param ipAddress - IP address to reset
|
|
76
|
-
*/
|
|
77
|
-
async resetFailedAttempts(ipAddress: string): Promise<void> {
|
|
78
|
-
const key = this.getKey(ipAddress);
|
|
79
|
-
await this.storageAdapter.del(key);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private getKey(ipAddress: string): string {
|
|
83
|
-
return `${this.keyPrefix}${ipAddress}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
private getLockKey(ipAddress: string): string {
|
|
87
|
-
return `${this.lockKeyPrefix}${ipAddress}`;
|
|
88
|
-
}
|
|
89
|
-
}
|
package/src/storage/index.ts
DELETED
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
import { StorageAdapter } from '../interfaces/storage-adapter.interface';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Internal structure for storing values with optional expiration
|
|
5
|
-
*/
|
|
6
|
-
interface StoredValue {
|
|
7
|
-
value: string;
|
|
8
|
-
expiresAt?: number; // Unix timestamp in milliseconds
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* In-memory storage adapter for development and single-server deployments
|
|
13
|
-
*
|
|
14
|
-
* ⚠️ CRITICAL LIMITATIONS FOR PRODUCTION:
|
|
15
|
-
* - Data is lost on server restart
|
|
16
|
-
* - Data is NOT shared across multiple server instances/containers
|
|
17
|
-
* - Rate limiting may be bypassed in ECS/multi-container deployments
|
|
18
|
-
* - NOT suitable for production clusters with multiple containers
|
|
19
|
-
*
|
|
20
|
-
* RECOMMENDATIONS:
|
|
21
|
-
* - Single ECS task: Acceptable (data lost on restart)
|
|
22
|
-
* - Multi-task/container ECS: Use Redis adapter (coming soon)
|
|
23
|
-
* - Production: Plan to implement Redis-backed storage adapter
|
|
24
|
-
*
|
|
25
|
-
* CURRENT BEHAVIOR:
|
|
26
|
-
* Rate limiting works per-container, not globally across containers
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```typescript
|
|
30
|
-
* const storage = new MemoryStorageAdapter();
|
|
31
|
-
* await storage.initialize();
|
|
32
|
-
* await storage.set('key', 'value', 60); // Set with 60 second TTL
|
|
33
|
-
* const value = await storage.get('key');
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
36
|
-
export class MemoryStorageAdapter implements StorageAdapter {
|
|
37
|
-
// Main key-value store with expiration support
|
|
38
|
-
private store: Map<string, StoredValue> = new Map();
|
|
39
|
-
|
|
40
|
-
// Hash storage for complex data structures (similar to Redis hashes)
|
|
41
|
-
private hashes: Map<string, Map<string, string>> = new Map();
|
|
42
|
-
|
|
43
|
-
// List storage for ordered collections (similar to Redis lists)
|
|
44
|
-
private lists: Map<string, string[]> = new Map();
|
|
45
|
-
|
|
46
|
-
// Interval timer for automatic cleanup of expired keys
|
|
47
|
-
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Initialize the storage adapter
|
|
51
|
-
* Starts a background cleanup job to remove expired keys
|
|
52
|
-
*/
|
|
53
|
-
async initialize(): Promise<void> {
|
|
54
|
-
// Start cleanup interval to remove expired keys every minute
|
|
55
|
-
this.cleanupInterval = setInterval(() => {
|
|
56
|
-
this.cleanupExpired();
|
|
57
|
-
}, 60000); // 60,000ms = 1 minute
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Check if the storage adapter is healthy and operational
|
|
62
|
-
* @returns Always returns true for in-memory storage
|
|
63
|
-
*/
|
|
64
|
-
async isHealthy(): Promise<boolean> {
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ============================================================================
|
|
69
|
-
// Basic Key-Value Operations
|
|
70
|
-
// ============================================================================
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Get a value by key
|
|
74
|
-
* Automatically removes and returns null if the key has expired
|
|
75
|
-
*
|
|
76
|
-
* @param key - The key to retrieve
|
|
77
|
-
* @returns The stored value or null if not found/expired
|
|
78
|
-
*/
|
|
79
|
-
async get(key: string): Promise<string | null> {
|
|
80
|
-
const stored = this.store.get(key);
|
|
81
|
-
|
|
82
|
-
// Key doesn't exist
|
|
83
|
-
if (!stored) return null;
|
|
84
|
-
|
|
85
|
-
// Check if key has expired
|
|
86
|
-
if (stored.expiresAt && stored.expiresAt < Date.now()) {
|
|
87
|
-
this.store.delete(key); // Clean up expired key
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return stored.value;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Set a key-value pair with optional TTL (time to live)
|
|
96
|
-
*
|
|
97
|
-
* @param key - The key to store
|
|
98
|
-
* @param value - The value to store
|
|
99
|
-
* @param ttl - Time to live in seconds (optional)
|
|
100
|
-
*/
|
|
101
|
-
async set(key: string, value: string, ttlSeconds?: number, options?: { nx?: boolean }): Promise<string | null> {
|
|
102
|
-
// For NX option, check if key exists and is not expired
|
|
103
|
-
if (options?.nx) {
|
|
104
|
-
const existing = this.store.get(key);
|
|
105
|
-
if (existing) {
|
|
106
|
-
// Check if existing key is expired
|
|
107
|
-
if (existing.expiresAt && existing.expiresAt < Date.now()) {
|
|
108
|
-
// Key exists but is expired - treat as non-existent and allow set
|
|
109
|
-
this.store.delete(key);
|
|
110
|
-
} else {
|
|
111
|
-
// Key exists and is not expired - NX failed
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const stored: StoredValue = { value };
|
|
118
|
-
|
|
119
|
-
// If TTL is provided, calculate expiration timestamp
|
|
120
|
-
if (ttlSeconds) {
|
|
121
|
-
stored.expiresAt = Date.now() + ttlSeconds * 1000; // Convert seconds to milliseconds
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
this.store.set(key, stored);
|
|
125
|
-
return value;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Delete a key from storage
|
|
130
|
-
* @param key - The key to delete
|
|
131
|
-
*/
|
|
132
|
-
async del(key: string): Promise<void> {
|
|
133
|
-
this.store.delete(key);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Check if a key exists and is not expired
|
|
138
|
-
* @param key - The key to check
|
|
139
|
-
* @returns True if key exists and is valid, false otherwise
|
|
140
|
-
*/
|
|
141
|
-
async exists(key: string): Promise<boolean> {
|
|
142
|
-
const value = await this.get(key);
|
|
143
|
-
return value !== null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ============================================================================
|
|
147
|
-
// Atomic Operations (for counters and rate limiting)
|
|
148
|
-
// ============================================================================
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Increment a counter stored at key
|
|
152
|
-
* If the key doesn't exist, it's initialized to 0 before incrementing
|
|
153
|
-
* Preserves TTL if the key already exists with an expiration
|
|
154
|
-
*
|
|
155
|
-
* @param key - The key to increment
|
|
156
|
-
* @param ttlSeconds - Optional TTL in seconds to set when creating a new key (only applied if key doesn't exist)
|
|
157
|
-
* @returns The new value after incrementing
|
|
158
|
-
*/
|
|
159
|
-
async incr(key: string, ttlSeconds?: number): Promise<number> {
|
|
160
|
-
const stored = this.store.get(key);
|
|
161
|
-
|
|
162
|
-
// Check if key exists and is not expired
|
|
163
|
-
let currentValue = '0';
|
|
164
|
-
let existingExpiry: number | undefined;
|
|
165
|
-
const wasNewKey = !stored || (stored.expiresAt && stored.expiresAt < Date.now());
|
|
166
|
-
|
|
167
|
-
if (stored) {
|
|
168
|
-
if (stored.expiresAt && stored.expiresAt < Date.now()) {
|
|
169
|
-
// Key expired - treat as non-existent
|
|
170
|
-
this.store.delete(key);
|
|
171
|
-
} else {
|
|
172
|
-
// Key exists and is valid - preserve expiry
|
|
173
|
-
currentValue = stored.value;
|
|
174
|
-
existingExpiry = stored.expiresAt;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const newValue = (parseInt(currentValue || '0', 10) + 1).toString();
|
|
179
|
-
const newStored: StoredValue = { value: newValue };
|
|
180
|
-
|
|
181
|
-
// Use provided TTL for new keys, otherwise preserve existing expiry
|
|
182
|
-
if (wasNewKey && ttlSeconds !== undefined) {
|
|
183
|
-
newStored.expiresAt = Date.now() + ttlSeconds * 1000;
|
|
184
|
-
} else if (existingExpiry) {
|
|
185
|
-
newStored.expiresAt = existingExpiry;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
this.store.set(key, newStored);
|
|
189
|
-
return parseInt(newValue, 10);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Decrement a counter stored at key
|
|
194
|
-
* If the key doesn't exist, it's initialized to 0 before decrementing
|
|
195
|
-
*
|
|
196
|
-
* @param key - The key to decrement
|
|
197
|
-
* @returns The new value after decrementing
|
|
198
|
-
*/
|
|
199
|
-
async decr(key: string): Promise<number> {
|
|
200
|
-
const current = await this.get(key);
|
|
201
|
-
const newValue = (parseInt(current || '0', 10) - 1).toString();
|
|
202
|
-
await this.set(key, newValue);
|
|
203
|
-
return parseInt(newValue, 10);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Set expiration time on an existing key
|
|
208
|
-
* @param key - The key to set expiration on
|
|
209
|
-
* @param ttl - Time to live in seconds
|
|
210
|
-
*/
|
|
211
|
-
async expire(key: string, ttl: number): Promise<void> {
|
|
212
|
-
const stored = this.store.get(key);
|
|
213
|
-
if (stored) {
|
|
214
|
-
stored.expiresAt = Date.now() + ttl * 1000; // Convert to milliseconds
|
|
215
|
-
this.store.set(key, stored);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Get the time to live (TTL) for a key
|
|
221
|
-
* @param key - The key to check
|
|
222
|
-
* @returns Seconds until expiration, -1 if no expiration, -2 if key doesn't exist
|
|
223
|
-
*/
|
|
224
|
-
async ttl(key: string): Promise<number> {
|
|
225
|
-
const stored = this.store.get(key);
|
|
226
|
-
|
|
227
|
-
// Key doesn't exist
|
|
228
|
-
if (!stored) return -2;
|
|
229
|
-
|
|
230
|
-
// Key exists but has no expiration
|
|
231
|
-
if (!stored.expiresAt) return -1;
|
|
232
|
-
|
|
233
|
-
// Calculate remaining seconds
|
|
234
|
-
const remaining = Math.floor((stored.expiresAt - Date.now()) / 1000);
|
|
235
|
-
return remaining > 0 ? remaining : -2; // Return -2 if already expired
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ============================================================================
|
|
239
|
-
// Hash Operations (for complex data structures)
|
|
240
|
-
// ============================================================================
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Get a field value from a hash
|
|
244
|
-
* @param key - Hash key
|
|
245
|
-
* @param field - Field name
|
|
246
|
-
* @returns Field value or null if not found
|
|
247
|
-
*/
|
|
248
|
-
async hget(key: string, field: string): Promise<string | null> {
|
|
249
|
-
const hash = this.hashes.get(key);
|
|
250
|
-
return hash?.get(field) ?? null;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Set a field value in a hash
|
|
255
|
-
* @param key - Hash key
|
|
256
|
-
* @param field - Field name
|
|
257
|
-
* @param value - Field value
|
|
258
|
-
*/
|
|
259
|
-
async hset(key: string, field: string, value: string): Promise<void> {
|
|
260
|
-
let hash = this.hashes.get(key);
|
|
261
|
-
if (!hash) {
|
|
262
|
-
hash = new Map();
|
|
263
|
-
this.hashes.set(key, hash);
|
|
264
|
-
}
|
|
265
|
-
hash.set(field, value);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Get all fields and values from a hash
|
|
270
|
-
* @param key - Hash key
|
|
271
|
-
* @returns Object with all field-value pairs
|
|
272
|
-
*/
|
|
273
|
-
async hgetall(key: string): Promise<Record<string, string>> {
|
|
274
|
-
const hash = this.hashes.get(key);
|
|
275
|
-
if (!hash) return {};
|
|
276
|
-
|
|
277
|
-
return Object.fromEntries(hash.entries());
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Delete one or more fields from a hash
|
|
282
|
-
* @param key - Hash key
|
|
283
|
-
* @param fields - Field names to delete
|
|
284
|
-
* @returns Number of fields deleted
|
|
285
|
-
*/
|
|
286
|
-
async hdel(key: string, ...fields: string[]): Promise<number> {
|
|
287
|
-
const hash = this.hashes.get(key);
|
|
288
|
-
if (!hash) return 0;
|
|
289
|
-
|
|
290
|
-
let deleted = 0;
|
|
291
|
-
for (const field of fields) {
|
|
292
|
-
if (hash.delete(field)) {
|
|
293
|
-
deleted++;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Clean up empty hash
|
|
298
|
-
if (hash.size === 0) {
|
|
299
|
-
this.hashes.delete(key);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return deleted;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// ============================================================================
|
|
306
|
-
// List Operations (for ordered collections)
|
|
307
|
-
// ============================================================================
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Push value to the left (beginning) of a list
|
|
311
|
-
* @param key - List key
|
|
312
|
-
* @param value - Value to push
|
|
313
|
-
*/
|
|
314
|
-
async lpush(key: string, value: string): Promise<void> {
|
|
315
|
-
let list = this.lists.get(key);
|
|
316
|
-
if (!list) {
|
|
317
|
-
list = [];
|
|
318
|
-
this.lists.set(key, list);
|
|
319
|
-
}
|
|
320
|
-
list.unshift(value); // Add to beginning
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Get a range of elements from a list
|
|
325
|
-
* @param key - List key
|
|
326
|
-
* @param start - Start index (0-based)
|
|
327
|
-
* @param stop - Stop index (-1 for end of list)
|
|
328
|
-
* @returns Array of values in range
|
|
329
|
-
*/
|
|
330
|
-
async lrange(key: string, start: number, stop: number): Promise<string[]> {
|
|
331
|
-
const list = this.lists.get(key);
|
|
332
|
-
if (!list) return [];
|
|
333
|
-
|
|
334
|
-
const end = stop === -1 ? list.length : stop + 1;
|
|
335
|
-
return list.slice(start, end);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Get the length of a list
|
|
340
|
-
* @param key - List key
|
|
341
|
-
* @returns List length
|
|
342
|
-
*/
|
|
343
|
-
async llen(key: string): Promise<number> {
|
|
344
|
-
const list = this.lists.get(key);
|
|
345
|
-
return list?.length ?? 0;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// ============================================================================
|
|
349
|
-
// Pattern Operations (for bulk operations)
|
|
350
|
-
// ============================================================================
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Find all keys matching a pattern
|
|
354
|
-
* @param pattern - Glob pattern (* and ? wildcards supported)
|
|
355
|
-
* @returns Array of matching keys
|
|
356
|
-
*/
|
|
357
|
-
async keys(pattern: string): Promise<string[]> {
|
|
358
|
-
const regex = this.patternToRegex(pattern);
|
|
359
|
-
return Array.from(this.store.keys()).filter((key) => regex.test(key));
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Iterate over keys matching a pattern (cursor-based)
|
|
364
|
-
* @param cursor - Cursor position (0 to start)
|
|
365
|
-
* @param pattern - Glob pattern
|
|
366
|
-
* @param count - Number of keys to return
|
|
367
|
-
* @returns Tuple of [new cursor, keys array]
|
|
368
|
-
*/
|
|
369
|
-
async scan(cursor: number, pattern: string, count: number): Promise<[number, string[]]> {
|
|
370
|
-
const allKeys = await this.keys(pattern);
|
|
371
|
-
const start = cursor;
|
|
372
|
-
const end = Math.min(cursor + count, allKeys.length);
|
|
373
|
-
const keys = allKeys.slice(start, end);
|
|
374
|
-
const newCursor = end >= allKeys.length ? 0 : end;
|
|
375
|
-
return [newCursor, keys];
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// ============================================================================
|
|
379
|
-
// Cleanup & Lifecycle
|
|
380
|
-
// ============================================================================
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Run cleanup of expired keys
|
|
384
|
-
*/
|
|
385
|
-
async cleanup(): Promise<void> {
|
|
386
|
-
this.cleanupExpired();
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Disconnect and cleanup all resources
|
|
391
|
-
*/
|
|
392
|
-
async disconnect(): Promise<void> {
|
|
393
|
-
// Stop cleanup interval
|
|
394
|
-
if (this.cleanupInterval) {
|
|
395
|
-
clearInterval(this.cleanupInterval);
|
|
396
|
-
this.cleanupInterval = null;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Clear all storage
|
|
400
|
-
this.store.clear();
|
|
401
|
-
this.hashes.clear();
|
|
402
|
-
this.lists.clear();
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// ============================================================================
|
|
406
|
-
// Private Helper Methods
|
|
407
|
-
// ============================================================================
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Remove all expired keys from storage
|
|
411
|
-
*/
|
|
412
|
-
private cleanupExpired(): void {
|
|
413
|
-
const now = Date.now();
|
|
414
|
-
const keysToDelete: string[] = [];
|
|
415
|
-
|
|
416
|
-
// Find all expired keys
|
|
417
|
-
for (const [key, stored] of this.store.entries()) {
|
|
418
|
-
if (stored.expiresAt && stored.expiresAt < now) {
|
|
419
|
-
keysToDelete.push(key);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Delete expired keys
|
|
424
|
-
for (const key of keysToDelete) {
|
|
425
|
-
this.store.delete(key);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Convert a glob pattern to a regular expression
|
|
431
|
-
* @param pattern - Glob pattern (* and ? wildcards)
|
|
432
|
-
* @returns RegExp for pattern matching
|
|
433
|
-
*/
|
|
434
|
-
private patternToRegex(pattern: string): RegExp {
|
|
435
|
-
// Escape regex special characters except * and ?
|
|
436
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
437
|
-
|
|
438
|
-
// Convert glob wildcards to regex
|
|
439
|
-
const regex = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
|
|
440
|
-
|
|
441
|
-
return new RegExp(`^${regex}$`);
|
|
442
|
-
}
|
|
443
|
-
}
|