dms-middleware-auth 1.1.8 → 1.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/auth.middleware.js +5 -4
- package/package.json +1 -2
- package/src/auth.middleware.ts +71 -70
- package/repro_bug.ts +0 -37
- package/test_specific_date.ts +0 -76
- package/test_ttl_overflow.ts +0 -31
- package/verify_fix_logic.ts +0 -84
package/dist/auth.middleware.js
CHANGED
|
@@ -365,10 +365,11 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
365
365
|
// -------------------------
|
|
366
366
|
async cacheSetSeconds(key, value, _ttlSeconds) {
|
|
367
367
|
try {
|
|
368
|
-
// Force a consistent one-day TTL in seconds to avoid
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
this.
|
|
368
|
+
// Force a consistent one-day TTL in seconds; clamp to avoid 32-bit timer overflow even if a store multiplies by 1000
|
|
369
|
+
// Use a simple, safe 24-hour TTL; 86,400s (86,400,000ms if misread as ms) is far below the Node timer limit.
|
|
370
|
+
const ttlSeconds = AuthMiddleware_1.LIC_CACHE_TTL_SECONDS; // 24 * 60 * 60
|
|
371
|
+
await this.cacheManager.set(key, value, ttlSeconds);
|
|
372
|
+
this.logger.debug(`Cache set: ${key} (TTL: ${ttlSeconds}s)`);
|
|
372
373
|
}
|
|
373
374
|
catch (e) {
|
|
374
375
|
this.logger.error(`Cache set failed for key=${key}: ${e?.message || e}`, e?.stack);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dms-middleware-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Reusable middleware for authentication and authorization in NestJS applications.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
"@types/express": "^5.0.0",
|
|
26
26
|
"@types/jest": "^29.5.14",
|
|
27
27
|
"@types/jsonwebtoken": "^9.0.7",
|
|
28
|
-
"@types/node-forge": "^1.3.14",
|
|
29
28
|
"jest": "^29.7.0",
|
|
30
29
|
"ts-jest": "^29.3.4",
|
|
31
30
|
"typescript": "^5.7.2"
|
package/src/auth.middleware.ts
CHANGED
|
@@ -47,11 +47,11 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
47
47
|
|
|
48
48
|
// Cache TTLs (safe by design; prevents timer overflow)
|
|
49
49
|
private static readonly LIC_CACHE_TTL_SECONDS = 24 * 60 * 60; // 24 hours TTL for licence
|
|
50
|
-
private static readonly CLIENT_TOKEN_TTL_MAX_SECONDS = 60 * 60; // 1 hour cap
|
|
51
|
-
private static readonly CLIENT_TOKEN_TTL_SAFETY_SKEW_SECONDS = 30; // Refresh a bit early
|
|
52
|
-
private static readonly ROLE_ATTRIBUTES_TTL_SECONDS = 24 * 60 * 60; // 24 hours TTL for role attributes
|
|
53
|
-
|
|
54
|
-
private static readonly ONE_DAY_SECONDS = 24 * 60 * 60;
|
|
50
|
+
private static readonly CLIENT_TOKEN_TTL_MAX_SECONDS = 60 * 60; // 1 hour cap
|
|
51
|
+
private static readonly CLIENT_TOKEN_TTL_SAFETY_SKEW_SECONDS = 30; // Refresh a bit early
|
|
52
|
+
private static readonly ROLE_ATTRIBUTES_TTL_SECONDS = 24 * 60 * 60; // 24 hours TTL for role attributes
|
|
53
|
+
|
|
54
|
+
private static readonly ONE_DAY_SECONDS = 24 * 60 * 60;
|
|
55
55
|
|
|
56
56
|
constructor(
|
|
57
57
|
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
|
|
@@ -182,38 +182,38 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
182
182
|
// -------------------------
|
|
183
183
|
// Graceful shutdown for EKS
|
|
184
184
|
// -------------------------
|
|
185
|
-
private stopServer() {
|
|
186
|
-
if (AuthMiddleware.shutdownInitiated) return;
|
|
187
|
-
AuthMiddleware.shutdownInitiated = true;
|
|
188
|
-
|
|
189
|
-
this.logger.warn('Initiating graceful shutdown due to licence expiry');
|
|
190
|
-
|
|
191
|
-
// Give the response a moment to flush, then SIGTERM, then force exit after 5s
|
|
192
|
-
setTimeout(() => {
|
|
193
|
-
try {
|
|
194
|
-
process.kill(process.pid, 'SIGTERM');
|
|
195
|
-
} catch {
|
|
196
|
-
process.exit(1);
|
|
197
|
-
}
|
|
185
|
+
private stopServer() {
|
|
186
|
+
if (AuthMiddleware.shutdownInitiated) return;
|
|
187
|
+
AuthMiddleware.shutdownInitiated = true;
|
|
188
|
+
|
|
189
|
+
this.logger.warn('Initiating graceful shutdown due to licence expiry');
|
|
190
|
+
|
|
191
|
+
// Give the response a moment to flush, then SIGTERM, then force exit after 5s
|
|
192
|
+
setTimeout(() => {
|
|
193
|
+
try {
|
|
194
|
+
process.kill(process.pid, 'SIGTERM');
|
|
195
|
+
} catch {
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
198
|
|
|
199
199
|
if (!AuthMiddleware.shutdownTimer) {
|
|
200
200
|
AuthMiddleware.shutdownTimer = setTimeout(() => {
|
|
201
201
|
this.logger.error('Force exit after SIGTERM timeout');
|
|
202
202
|
process.exit(1);
|
|
203
203
|
}, 5000);
|
|
204
|
-
}
|
|
205
|
-
}, 500);
|
|
206
|
-
}
|
|
204
|
+
}
|
|
205
|
+
}, 500);
|
|
206
|
+
}
|
|
207
207
|
|
|
208
208
|
// -------------------------
|
|
209
209
|
// Licence validate flow (called at boot / can be called again if needed)
|
|
210
210
|
// -------------------------
|
|
211
|
-
private async checkLicenceAndValidate(realm: string, publicKey: string): Promise<boolean> {
|
|
212
|
-
try {
|
|
213
|
-
if (AuthMiddleware.licenceExpired) return false;
|
|
214
|
-
|
|
215
|
-
// If we already have an expiry and it's still valid, no need to fetch again
|
|
216
|
-
if (
|
|
211
|
+
private async checkLicenceAndValidate(realm: string, publicKey: string): Promise<boolean> {
|
|
212
|
+
try {
|
|
213
|
+
if (AuthMiddleware.licenceExpired) return false;
|
|
214
|
+
|
|
215
|
+
// If we already have an expiry and it's still valid, no need to fetch again
|
|
216
|
+
if (
|
|
217
217
|
AuthMiddleware.licenceValidatedUntilMs &&
|
|
218
218
|
Date.now() < AuthMiddleware.licenceValidatedUntilMs + AuthMiddleware.CLOCK_TOLERANCE_MS
|
|
219
219
|
) {
|
|
@@ -229,21 +229,21 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
229
229
|
// 1) Try cache first (short TTL)
|
|
230
230
|
const cachedToken = (await this.cacheManager.get('client_Licence_token')) as string | undefined;
|
|
231
231
|
if (cachedToken) {
|
|
232
|
-
const ok = await this.validateLicence(cachedToken, publicKey, realm, this.options.keycloakUrl, false);
|
|
233
|
-
if (ok) {
|
|
234
|
-
this.logger.log('Licence validated from cache');
|
|
235
|
-
return true;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
232
|
+
const ok = await this.validateLicence(cachedToken, publicKey, realm, this.options.keycloakUrl, false);
|
|
233
|
+
if (ok) {
|
|
234
|
+
this.logger.log('Licence validated from cache');
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
238
|
|
|
239
239
|
// 2) Fetch from licensing service
|
|
240
240
|
this.logger.log('Fetching licence from service');
|
|
241
241
|
const token = await this.getLicencingTokenFromService(realm);
|
|
242
|
-
const ok = await this.validateLicence(token, publicKey, realm, this.options.keycloakUrl, true);
|
|
243
|
-
|
|
244
|
-
if (ok) {
|
|
245
|
-
this.logger.log('Licence validated from service');
|
|
246
|
-
}
|
|
242
|
+
const ok = await this.validateLicence(token, publicKey, realm, this.options.keycloakUrl, true);
|
|
243
|
+
|
|
244
|
+
if (ok) {
|
|
245
|
+
this.logger.log('Licence validated from service');
|
|
246
|
+
}
|
|
247
247
|
|
|
248
248
|
return ok;
|
|
249
249
|
})();
|
|
@@ -259,19 +259,19 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
private async validateLicence(
|
|
263
|
-
tokenJwt: string,
|
|
264
|
-
publicKey: string,
|
|
265
|
-
realm: string,
|
|
266
|
-
keycloakUrl: string,
|
|
267
|
-
updateCache: boolean
|
|
268
|
-
): Promise<boolean> {
|
|
269
|
-
try {
|
|
270
|
-
const payload: any = this.verifyLicenceJwt(tokenJwt, publicKey, realm, keycloakUrl);
|
|
271
|
-
|
|
272
|
-
// Your custom claim: lic_end
|
|
273
|
-
const licEndMs = this.normalizeEpochMs(payload?.lic_end);
|
|
274
|
-
const now = Date.now();
|
|
262
|
+
private async validateLicence(
|
|
263
|
+
tokenJwt: string,
|
|
264
|
+
publicKey: string,
|
|
265
|
+
realm: string,
|
|
266
|
+
keycloakUrl: string,
|
|
267
|
+
updateCache: boolean
|
|
268
|
+
): Promise<boolean> {
|
|
269
|
+
try {
|
|
270
|
+
const payload: any = this.verifyLicenceJwt(tokenJwt, publicKey, realm, keycloakUrl);
|
|
271
|
+
|
|
272
|
+
// Your custom claim: lic_end
|
|
273
|
+
const licEndMs = this.normalizeEpochMs(payload?.lic_end);
|
|
274
|
+
const now = Date.now();
|
|
275
275
|
|
|
276
276
|
if (!licEndMs) {
|
|
277
277
|
this.logger.error('Licence token missing lic_end');
|
|
@@ -398,17 +398,17 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
398
398
|
// -------------------------
|
|
399
399
|
// JWT helpers
|
|
400
400
|
// -------------------------
|
|
401
|
-
private verifyLicenceJwt(token: string, publicKey: string, realm: string, keycloakUrl: string) {
|
|
402
|
-
const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
|
|
403
|
-
|
|
404
|
-
const issuer = `${keycloakUrl}/realms/${realm}`;
|
|
405
|
-
|
|
406
|
-
return jwt.verify(token, publicKeys, {
|
|
407
|
-
algorithms: ['RS256'],
|
|
408
|
-
issuer,
|
|
409
|
-
clockTolerance: 60,
|
|
410
|
-
});
|
|
411
|
-
}
|
|
401
|
+
private verifyLicenceJwt(token: string, publicKey: string, realm: string, keycloakUrl: string) {
|
|
402
|
+
const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
|
|
403
|
+
|
|
404
|
+
const issuer = `${keycloakUrl}/realms/${realm}`;
|
|
405
|
+
|
|
406
|
+
return jwt.verify(token, publicKeys, {
|
|
407
|
+
algorithms: ['RS256'],
|
|
408
|
+
issuer,
|
|
409
|
+
clockTolerance: 60,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
412
|
|
|
413
413
|
private decodeAccessToken(authHeader: string, publicKey: string) {
|
|
414
414
|
const token = this.extractBearerToken(authHeader);
|
|
@@ -438,14 +438,15 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
438
438
|
}
|
|
439
439
|
|
|
440
440
|
// -------------------------
|
|
441
|
-
// Cache helper with overflow protection
|
|
442
|
-
// -------------------------
|
|
443
|
-
private async cacheSetSeconds(key: string, value: any, _ttlSeconds: number) {
|
|
444
|
-
try {
|
|
445
|
-
// Force a consistent one-day TTL in seconds to avoid
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
this.
|
|
441
|
+
// Cache helper with overflow protection
|
|
442
|
+
// -------------------------
|
|
443
|
+
private async cacheSetSeconds(key: string, value: any, _ttlSeconds: number) {
|
|
444
|
+
try {
|
|
445
|
+
// Force a consistent one-day TTL in seconds; clamp to avoid 32-bit timer overflow even if a store multiplies by 1000
|
|
446
|
+
// Use a simple, safe 24-hour TTL; 86,400s (86,400,000ms if misread as ms) is far below the Node timer limit.
|
|
447
|
+
const ttlSeconds = AuthMiddleware.LIC_CACHE_TTL_SECONDS; // 24 * 60 * 60
|
|
448
|
+
await this.cacheManager.set(key, value, ttlSeconds);
|
|
449
|
+
this.logger.debug(`Cache set: ${key} (TTL: ${ttlSeconds}s)`);
|
|
449
450
|
} catch (e: any) {
|
|
450
451
|
this.logger.error(`Cache set failed for key=${key}: ${e?.message || e}`, e?.stack);
|
|
451
452
|
}
|
package/repro_bug.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
const licEndString = "1774981740000";
|
|
3
|
-
const CLOCK_TOLERANCE_MS = 60000;
|
|
4
|
-
|
|
5
|
-
function normalizeEpochMs(epoch: any): number | null {
|
|
6
|
-
if (!epoch || (typeof epoch === 'number' && Number.isNaN(epoch))) {
|
|
7
|
-
return null;
|
|
8
|
-
}
|
|
9
|
-
// Buggy logic: relying on implicit conversion but returning original type
|
|
10
|
-
if (epoch < 1_000_000_000_000) {
|
|
11
|
-
return epoch * 1000;
|
|
12
|
-
}
|
|
13
|
-
return epoch;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const now = Date.now();
|
|
17
|
-
const licEndMs = normalizeEpochMs(licEndString);
|
|
18
|
-
console.log(`licEndMs Type: ${typeof licEndMs}, Value: ${licEndMs}`);
|
|
19
|
-
|
|
20
|
-
// Simulate the logic in auth.middleware.ts
|
|
21
|
-
const delay = ((licEndMs as any) + CLOCK_TOLERANCE_MS) - now;
|
|
22
|
-
console.log(`Delay: ${delay}`);
|
|
23
|
-
|
|
24
|
-
if (delay > 2147483647) {
|
|
25
|
-
console.log("Delay exceeds 32-bit signed integer limit! This often causes setTimeout to fire immediately (overflow to 1).");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
console.log("Setting timeout...");
|
|
29
|
-
const timer = setTimeout(() => {
|
|
30
|
-
console.log("BUG REPRODUCED: Timeout fired immediately despite huge delay!");
|
|
31
|
-
}, delay);
|
|
32
|
-
|
|
33
|
-
// Keep alive for a bit to see if it fires immediately
|
|
34
|
-
setTimeout(() => {
|
|
35
|
-
console.log("Finished waiting 2s");
|
|
36
|
-
clearTimeout(timer);
|
|
37
|
-
}, 2000);
|
package/test_specific_date.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { AuthMiddleware } from './src/auth.middleware';
|
|
3
|
-
import * as jwt from 'jsonwebtoken';
|
|
4
|
-
import * as crypto from 'crypto';
|
|
5
|
-
|
|
6
|
-
async function runTest() {
|
|
7
|
-
console.log("--- Starting Thorough Test for Date 1774981740000 ---");
|
|
8
|
-
|
|
9
|
-
// 1. Setup Mocks
|
|
10
|
-
const mockCache = {
|
|
11
|
-
get: async (key: string) => undefined,
|
|
12
|
-
set: async (key: string, val: any, ttl: any) => {
|
|
13
|
-
console.log(`[MockCache] set key=${key}, ttl=${ttl}`);
|
|
14
|
-
},
|
|
15
|
-
} as any;
|
|
16
|
-
|
|
17
|
-
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
|
|
18
|
-
modulusLength: 2048,
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
let publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }) as string;
|
|
22
|
-
// Strip headers as AuthMiddleware adds them
|
|
23
|
-
const publicKeyBody = publicKeyPem
|
|
24
|
-
.replace('-----BEGIN PUBLIC KEY-----\n', '')
|
|
25
|
-
.replace('\n-----END PUBLIC KEY-----\n', '');
|
|
26
|
-
|
|
27
|
-
const mockOptions = {
|
|
28
|
-
publicKey: publicKeyBody,
|
|
29
|
-
keycloakUrl: 'http://localhost:8080',
|
|
30
|
-
realm: 'test-realm',
|
|
31
|
-
clientId: 'test-client',
|
|
32
|
-
clientSecret: 'secret',
|
|
33
|
-
clientUuid: 'uuid',
|
|
34
|
-
bypassURL: '/health',
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const authMiddleware = new AuthMiddleware(mockCache, mockOptions);
|
|
38
|
-
|
|
39
|
-
// 2. Create Token with specific date
|
|
40
|
-
const licEnd = 1774981740000;
|
|
41
|
-
const payload = {
|
|
42
|
-
lic_end: licEnd
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }) as string;
|
|
46
|
-
const token = jwt.sign(payload, privateKeyPem, { algorithm: 'RS256', noTimestamp: true });
|
|
47
|
-
|
|
48
|
-
console.log(`Test Token: ${token}`);
|
|
49
|
-
console.log(`Current Time (Date.now()): ${Date.now()}`);
|
|
50
|
-
console.log(`User's Date to test: ${licEnd}`);
|
|
51
|
-
|
|
52
|
-
// 3. Run Validation
|
|
53
|
-
console.log("\n--- Calling validateLicence ---");
|
|
54
|
-
// We can access private methods using any cast for testing
|
|
55
|
-
const result = await (authMiddleware as any).validateLicence(token, publicKeyBody, true);
|
|
56
|
-
|
|
57
|
-
console.log(`\nResult Status: ${result.status}`);
|
|
58
|
-
|
|
59
|
-
if (result.status) {
|
|
60
|
-
console.log("SUCCESS: Middleware accepted the future date.");
|
|
61
|
-
} else {
|
|
62
|
-
console.error("FAILURE: Middleware rejected the future date.");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// 4. Check if expired flag was set
|
|
66
|
-
const isExpired = (AuthMiddleware as any).licenceExpired;
|
|
67
|
-
console.log(`AuthMiddleware.licenceExpired: ${isExpired}`);
|
|
68
|
-
|
|
69
|
-
// 5. Cleanup Timer
|
|
70
|
-
if ((AuthMiddleware as any).licenceExpiryTimer) {
|
|
71
|
-
console.log("Clearing expiry timer...");
|
|
72
|
-
clearTimeout((AuthMiddleware as any).licenceExpiryTimer);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
runTest().catch(console.error);
|
package/test_ttl_overflow.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { caching } from 'cache-manager';
|
|
3
|
-
|
|
4
|
-
async function testTTL() {
|
|
5
|
-
console.log("--- Testing Cache TTL Overflow ---");
|
|
6
|
-
const cache = await caching('memory');
|
|
7
|
-
|
|
8
|
-
const largeTTL = 4351255821; // ~50 days in ms
|
|
9
|
-
console.log(`Setting key with TTL: ${largeTTL}ms`);
|
|
10
|
-
|
|
11
|
-
await cache.set('test_key', 'test_value', largeTTL);
|
|
12
|
-
|
|
13
|
-
const val = await cache.get('test_key');
|
|
14
|
-
console.log(`Immediate get: ${val}`);
|
|
15
|
-
|
|
16
|
-
if (!val) {
|
|
17
|
-
console.error("FAIL: Value expired immediately!");
|
|
18
|
-
} else {
|
|
19
|
-
console.log("Value still present. Waiting 2s to check again...");
|
|
20
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
21
|
-
const val2 = await cache.get('test_key');
|
|
22
|
-
console.log(`After 2s get: ${val2}`);
|
|
23
|
-
if (!val2) {
|
|
24
|
-
console.error("FAIL: Value expired after 2s (likely overflow)!");
|
|
25
|
-
} else {
|
|
26
|
-
console.log("SUCCESS: Cache handles large TTL.");
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
testTTL().catch(console.error);
|
package/verify_fix_logic.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
const CLOCK_TOLERANCE_MS = 60000;
|
|
3
|
-
const MAX_TIMEOUT_VAL = 2147483647;
|
|
4
|
-
|
|
5
|
-
let licenceExpiryTimer: NodeJS.Timeout | null = null;
|
|
6
|
-
let licenceExpired = false;
|
|
7
|
-
|
|
8
|
-
function getUtcNowMs() {
|
|
9
|
-
return Date.now();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function normalizeEpochMs(epoch: number | string): number | null {
|
|
13
|
-
const epochNum = Number(epoch);
|
|
14
|
-
if (!epochNum || Number.isNaN(epochNum)) {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
if (epochNum < 1_000_000_000_000) {
|
|
18
|
-
return epochNum * 1000;
|
|
19
|
-
}
|
|
20
|
-
return epochNum;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function markLicenceExpired() {
|
|
24
|
-
licenceExpired = true;
|
|
25
|
-
console.log("Licence marked as expired!");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function scheduleLicenceShutdown(licEndMs: number) {
|
|
29
|
-
if (!licEndMs) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const now = getUtcNowMs();
|
|
33
|
-
const delay = (licEndMs + CLOCK_TOLERANCE_MS) - now;
|
|
34
|
-
|
|
35
|
-
console.log(`Scheduling shutdown. LicEnd: ${licEndMs}, Now: ${now}, Full Delay: ${delay}`);
|
|
36
|
-
|
|
37
|
-
if (delay <= 0) {
|
|
38
|
-
console.log('Licence expired immediately (including tolerance).');
|
|
39
|
-
markLicenceExpired();
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (licenceExpiryTimer) {
|
|
44
|
-
clearTimeout(licenceExpiryTimer);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Check if delay fits in 32-bit int
|
|
48
|
-
const safeDelay = Math.min(delay, MAX_TIMEOUT_VAL);
|
|
49
|
-
|
|
50
|
-
console.log(`Setting timeout for ${safeDelay}ms`);
|
|
51
|
-
|
|
52
|
-
// In a real test we can't wait 24 days, so we'll just assert the logic is sound.
|
|
53
|
-
// If safeDelay < delay, it means we need to reschedule.
|
|
54
|
-
|
|
55
|
-
if (safeDelay < delay) {
|
|
56
|
-
console.log("Delay exceeds max timeout, scheduling recursive check...");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
licenceExpiryTimer = setTimeout(() => {
|
|
60
|
-
const remaining = (licEndMs + CLOCK_TOLERANCE_MS) - getUtcNowMs();
|
|
61
|
-
console.log(`Timeout fired. Remaining: ${remaining}`);
|
|
62
|
-
if (remaining <= 0) {
|
|
63
|
-
markLicenceExpired();
|
|
64
|
-
} else {
|
|
65
|
-
scheduleLicenceShutdown(licEndMs);
|
|
66
|
-
}
|
|
67
|
-
}, Math.min(safeDelay, 1000)); // Override delay to 1s for test execution
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Test Case
|
|
71
|
-
const licEndString = "1774981740000"; // Future date (approx 50 days from 2026-02-09)
|
|
72
|
-
const licEndMs = normalizeEpochMs(licEndString);
|
|
73
|
-
|
|
74
|
-
if (licEndMs) {
|
|
75
|
-
console.log(`Normalized LicEnd: ${licEndMs}`);
|
|
76
|
-
scheduleLicenceShutdown(licEndMs);
|
|
77
|
-
} else {
|
|
78
|
-
console.error("Failed to normalize epoch");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Wait for the test simulation
|
|
82
|
-
setTimeout(() => {
|
|
83
|
-
console.log("Test finished.");
|
|
84
|
-
}, 2000);
|