dms-middleware-auth 1.1.1 → 1.1.2
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/__tests__/auth.middleware.spec.ts +43 -4
- package/dist/auth.middleware.d.ts +2 -0
- package/dist/auth.middleware.js +41 -12
- package/package.json +2 -2
- package/repro_bug.ts +37 -0
- package/src/auth.middleware.ts +46 -14
- package/verify_fix_logic.ts +84 -0
|
@@ -87,7 +87,7 @@ describe('AuthMiddleware licensing', () => {
|
|
|
87
87
|
const result = await (middleware as any).checkLicenceAndValidate('my-realm', 'PUBLIC_KEY');
|
|
88
88
|
|
|
89
89
|
expect(result).toBe(true);
|
|
90
|
-
expect(cache.set).toHaveBeenCalledWith('client_Licence_token', 'server-token',
|
|
90
|
+
expect(cache.set).toHaveBeenCalledWith('client_Licence_token', 'server-token', expect.any(Number));
|
|
91
91
|
});
|
|
92
92
|
|
|
93
93
|
it('schedules licence expiration correctly but DOES NOT shutdown immediately', async () => {
|
|
@@ -103,14 +103,49 @@ describe('AuthMiddleware licensing', () => {
|
|
|
103
103
|
|
|
104
104
|
await (middleware as any).checkLicenceAndValidate('my-realm', 'PUBLIC_KEY');
|
|
105
105
|
|
|
106
|
-
// Fast forward
|
|
107
|
-
jest.advanceTimersByTime(1001);
|
|
106
|
+
// Fast forward past expiry + tolerance
|
|
107
|
+
jest.advanceTimersByTime(1001 + 60000);
|
|
108
108
|
expect((AuthMiddleware as any).licenceExpired).toBe(true);
|
|
109
109
|
|
|
110
110
|
// Should NOT have initiated shutdown yet (no request made after expiry)
|
|
111
111
|
expect((AuthMiddleware as any).shutdownInitiated).toBe(false);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
it('allows a 60-second clock tolerance for expiration', async () => {
|
|
115
|
+
jest.useFakeTimers();
|
|
116
|
+
cache.get.mockResolvedValueOnce(null);
|
|
117
|
+
jest.spyOn(middleware as any, 'getLicencingDetails').mockResolvedValue({
|
|
118
|
+
code: HttpStatusCode.Ok,
|
|
119
|
+
data: 'server-token',
|
|
120
|
+
});
|
|
121
|
+
jest.spyOn(middleware as any, 'verifyJwt').mockReturnValue({
|
|
122
|
+
lic_end: Date.now() - 30000, // 30 seconds ago
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const result = await (middleware as any).checkLicenceAndValidate('my-realm', 'PUBLIC_KEY');
|
|
126
|
+
|
|
127
|
+
// Even though it's 30s in the past, tolerance (60s) should keep it valid.
|
|
128
|
+
expect(result).toBe(true);
|
|
129
|
+
expect((AuthMiddleware as any).licenceExpired).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('fails after the 60-second grace period', async () => {
|
|
133
|
+
jest.useFakeTimers();
|
|
134
|
+
cache.get.mockResolvedValueOnce(null);
|
|
135
|
+
jest.spyOn(middleware as any, 'getLicencingDetails').mockResolvedValue({
|
|
136
|
+
code: HttpStatusCode.Ok,
|
|
137
|
+
data: 'server-token',
|
|
138
|
+
});
|
|
139
|
+
jest.spyOn(middleware as any, 'verifyJwt').mockReturnValue({
|
|
140
|
+
lic_end: Date.now() - 61000, // 61 seconds ago
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const result = await (middleware as any).checkLicenceAndValidate('my-realm', 'PUBLIC_KEY');
|
|
144
|
+
|
|
145
|
+
expect(result).toBe(false);
|
|
146
|
+
expect((AuthMiddleware as any).licenceExpired).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
114
149
|
it('shuts down on the next request after licence expiry', async () => {
|
|
115
150
|
jest.useFakeTimers();
|
|
116
151
|
(AuthMiddleware as any).licenceExpired = true;
|
|
@@ -123,7 +158,11 @@ describe('AuthMiddleware licensing', () => {
|
|
|
123
158
|
await middleware.use(req, res as any, next);
|
|
124
159
|
|
|
125
160
|
expect(res.status).toHaveBeenCalledWith(HttpStatusCode.Forbidden);
|
|
126
|
-
|
|
161
|
+
const responseJson = res.json.mock.calls[0][0];
|
|
162
|
+
expect(responseJson.message).toBe((AuthMiddleware as any).licenceExpiredMessage);
|
|
163
|
+
expect(responseJson).toHaveProperty('server_time');
|
|
164
|
+
expect(responseJson).toHaveProperty('licence_expiry');
|
|
165
|
+
expect(responseJson).toHaveProperty('drift_ms');
|
|
127
166
|
expect(stopSpy).toHaveBeenCalled();
|
|
128
167
|
});
|
|
129
168
|
|
|
@@ -20,6 +20,8 @@ export declare class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
20
20
|
private static licenceValidationPromise;
|
|
21
21
|
private static shutdownInitiated;
|
|
22
22
|
private static readonly licenceExpiredMessage;
|
|
23
|
+
private static readonly CLOCK_TOLERANCE_MS;
|
|
24
|
+
private readonly logger;
|
|
23
25
|
constructor(cacheManager: Cache, options: AuthMiddlewareOptions);
|
|
24
26
|
onModuleInit(): Promise<void>;
|
|
25
27
|
use(req: any, res: Response, next: NextFunction): Promise<void | Response<any, Record<string, any>>>;
|
package/dist/auth.middleware.js
CHANGED
|
@@ -56,6 +56,7 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
56
56
|
constructor(cacheManager, options) {
|
|
57
57
|
this.cacheManager = cacheManager;
|
|
58
58
|
this.options = options;
|
|
59
|
+
this.logger = new common_1.Logger('AuthMiddleware');
|
|
59
60
|
}
|
|
60
61
|
async onModuleInit() {
|
|
61
62
|
const { publicKey, realm } = this.options;
|
|
@@ -66,7 +67,12 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
66
67
|
try {
|
|
67
68
|
if (AuthMiddleware_1.licenceExpired) {
|
|
68
69
|
this.stopServer();
|
|
69
|
-
return res.status(axios_1.HttpStatusCode.Forbidden).json({
|
|
70
|
+
return res.status(axios_1.HttpStatusCode.Forbidden).json({
|
|
71
|
+
message: AuthMiddleware_1.licenceExpiredMessage,
|
|
72
|
+
server_time: new Date().toISOString(),
|
|
73
|
+
licence_expiry: AuthMiddleware_1.licenceValidatedUntilMs ? new Date(AuthMiddleware_1.licenceValidatedUntilMs).toISOString() : 'Unknown',
|
|
74
|
+
drift_ms: AuthMiddleware_1.licenceValidatedUntilMs ? (AuthMiddleware_1.licenceValidatedUntilMs - Date.now()) : 0
|
|
75
|
+
});
|
|
70
76
|
}
|
|
71
77
|
if (!AuthMiddleware_1.licenceValidatedUntilMs) {
|
|
72
78
|
return res.status(axios_1.HttpStatusCode.Forbidden).json({ message: AuthMiddleware_1.licenceExpiredMessage });
|
|
@@ -226,8 +232,10 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
226
232
|
try {
|
|
227
233
|
const token = await this.verifyJwt(lic_data, publicKey);
|
|
228
234
|
const licEndMs = this.normalizeEpochMs(token?.lic_end);
|
|
229
|
-
// Reject when licence end (epoch) is missing or already in the past.
|
|
230
|
-
|
|
235
|
+
// Reject when licence end (epoch) is missing or already in the past (with tolerance).
|
|
236
|
+
const now = this.getUtcNowMs();
|
|
237
|
+
if (!licEndMs || (licEndMs + AuthMiddleware_1.CLOCK_TOLERANCE_MS) <= now) {
|
|
238
|
+
this.logger.error(`Licence expired. Expiry: ${new Date(licEndMs || 0).toISOString()}, Server Time: ${new Date(now).toISOString()}`);
|
|
231
239
|
this.markLicenceExpired();
|
|
232
240
|
return { status: false };
|
|
233
241
|
}
|
|
@@ -256,18 +264,31 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
256
264
|
if (!licEndMs) {
|
|
257
265
|
return;
|
|
258
266
|
}
|
|
259
|
-
const
|
|
267
|
+
const now = this.getUtcNowMs();
|
|
268
|
+
const delay = (licEndMs + AuthMiddleware_1.CLOCK_TOLERANCE_MS) - now;
|
|
260
269
|
if (delay <= 0) {
|
|
270
|
+
this.logger.warn('Licence expired immediately (including tolerance).');
|
|
261
271
|
this.markLicenceExpired();
|
|
262
272
|
return;
|
|
263
273
|
}
|
|
264
274
|
if (AuthMiddleware_1.licenceExpiryTimer) {
|
|
265
275
|
clearTimeout(AuthMiddleware_1.licenceExpiryTimer);
|
|
266
276
|
}
|
|
267
|
-
//
|
|
277
|
+
// setTimeout uses a 32-bit signed integer. Max value is 2147483647 ms (approx 24.8 days).
|
|
278
|
+
// If the delay is larger than this, setTimeout will fire immediately (overflow).
|
|
279
|
+
// so we cap the delay and re-check when it fires.
|
|
280
|
+
const MAX_TIMEOUT_VAL = 2147483647;
|
|
281
|
+
const safeDelay = Math.min(delay, MAX_TIMEOUT_VAL);
|
|
268
282
|
AuthMiddleware_1.licenceExpiryTimer = setTimeout(() => {
|
|
269
|
-
this.
|
|
270
|
-
|
|
283
|
+
const remaining = (licEndMs + AuthMiddleware_1.CLOCK_TOLERANCE_MS) - this.getUtcNowMs();
|
|
284
|
+
if (remaining <= 0) {
|
|
285
|
+
this.markLicenceExpired();
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
// Reschedule if we still have time left
|
|
289
|
+
this.scheduleLicenceShutdown(licEndMs);
|
|
290
|
+
}
|
|
291
|
+
}, safeDelay);
|
|
271
292
|
}
|
|
272
293
|
stopServer() {
|
|
273
294
|
if (AuthMiddleware_1.shutdownInitiated) {
|
|
@@ -289,20 +310,27 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
289
310
|
}, 0);
|
|
290
311
|
}
|
|
291
312
|
normalizeEpochMs(epoch) {
|
|
292
|
-
|
|
313
|
+
const epochNum = Number(epoch);
|
|
314
|
+
if (!epochNum || Number.isNaN(epochNum)) {
|
|
293
315
|
return null;
|
|
294
316
|
}
|
|
295
|
-
if (
|
|
296
|
-
return
|
|
317
|
+
if (epochNum < 1_000_000_000_000) {
|
|
318
|
+
return epochNum * 1000;
|
|
297
319
|
}
|
|
298
|
-
return
|
|
320
|
+
return epochNum;
|
|
299
321
|
}
|
|
300
322
|
getUtcNowMs() {
|
|
301
323
|
return Date.now();
|
|
302
324
|
}
|
|
303
325
|
verifyJwt(token, publicKey) {
|
|
304
326
|
const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
|
|
305
|
-
|
|
327
|
+
// We ignore the standard 'exp' claim and rely on our custom 'lic_end' field.
|
|
328
|
+
// Also include a small clock tolerance for the library itself.
|
|
329
|
+
return jwt.verify(token, publicKeys, {
|
|
330
|
+
algorithms: ['RS256'],
|
|
331
|
+
ignoreExpiration: true,
|
|
332
|
+
clockTolerance: 60
|
|
333
|
+
});
|
|
306
334
|
}
|
|
307
335
|
decodeAccessToken(authHeader, publicKey) {
|
|
308
336
|
const token = this.extractBearerToken(authHeader);
|
|
@@ -320,6 +348,7 @@ AuthMiddleware.licenceValidatedUntilMs = null;
|
|
|
320
348
|
AuthMiddleware.licenceValidationPromise = null;
|
|
321
349
|
AuthMiddleware.shutdownInitiated = false;
|
|
322
350
|
AuthMiddleware.licenceExpiredMessage = "server Licence is expired!. please renew";
|
|
351
|
+
AuthMiddleware.CLOCK_TOLERANCE_MS = 60000; // 1 minute grace period
|
|
323
352
|
exports.AuthMiddleware = AuthMiddleware = AuthMiddleware_1 = __decorate([
|
|
324
353
|
(0, common_1.Injectable)(),
|
|
325
354
|
__param(0, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dms-middleware-auth",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
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",
|
|
@@ -29,4 +29,4 @@
|
|
|
29
29
|
"ts-jest": "^29.3.4",
|
|
30
30
|
"typescript": "^5.7.2"
|
|
31
31
|
}
|
|
32
|
-
}
|
|
32
|
+
}
|
package/repro_bug.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
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/src/auth.middleware.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Inject, Injectable, NestMiddleware, OnModuleInit } from '@nestjs/common';
|
|
1
|
+
import { Inject, Injectable, Logger, NestMiddleware, OnModuleInit } from '@nestjs/common';
|
|
2
2
|
import { Response, NextFunction } from 'express';
|
|
3
3
|
import * as jwt from 'jsonwebtoken';
|
|
4
4
|
import axios, { HttpStatusCode } from 'axios';
|
|
@@ -25,6 +25,8 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
25
25
|
private static licenceValidationPromise: Promise<boolean> | null = null;
|
|
26
26
|
private static shutdownInitiated = false;
|
|
27
27
|
private static readonly licenceExpiredMessage = "server Licence is expired!. please renew";
|
|
28
|
+
private static readonly CLOCK_TOLERANCE_MS = 60000; // 1 minute grace period
|
|
29
|
+
private readonly logger = new Logger('AuthMiddleware');
|
|
28
30
|
|
|
29
31
|
constructor(
|
|
30
32
|
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
|
@@ -42,7 +44,12 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
42
44
|
try {
|
|
43
45
|
if (AuthMiddleware.licenceExpired) {
|
|
44
46
|
this.stopServer();
|
|
45
|
-
return res.status(HttpStatusCode.Forbidden).json({
|
|
47
|
+
return res.status(HttpStatusCode.Forbidden).json({
|
|
48
|
+
message: AuthMiddleware.licenceExpiredMessage,
|
|
49
|
+
server_time: new Date().toISOString(),
|
|
50
|
+
licence_expiry: AuthMiddleware.licenceValidatedUntilMs ? new Date(AuthMiddleware.licenceValidatedUntilMs).toISOString() : 'Unknown',
|
|
51
|
+
drift_ms: AuthMiddleware.licenceValidatedUntilMs ? (AuthMiddleware.licenceValidatedUntilMs - Date.now()) : 0
|
|
52
|
+
});
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
if (!AuthMiddleware.licenceValidatedUntilMs) {
|
|
@@ -223,8 +230,10 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
223
230
|
const token: any = await this.verifyJwt(lic_data, publicKey);
|
|
224
231
|
const licEndMs = this.normalizeEpochMs(token?.lic_end);
|
|
225
232
|
|
|
226
|
-
// Reject when licence end (epoch) is missing or already in the past.
|
|
227
|
-
|
|
233
|
+
// Reject when licence end (epoch) is missing or already in the past (with tolerance).
|
|
234
|
+
const now = this.getUtcNowMs();
|
|
235
|
+
if (!licEndMs || (licEndMs + AuthMiddleware.CLOCK_TOLERANCE_MS) <= now) {
|
|
236
|
+
this.logger.error(`Licence expired. Expiry: ${new Date(licEndMs || 0).toISOString()}, Server Time: ${new Date(now).toISOString()}`);
|
|
228
237
|
this.markLicenceExpired();
|
|
229
238
|
return { status: false };
|
|
230
239
|
}
|
|
@@ -258,18 +267,34 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
258
267
|
if (!licEndMs) {
|
|
259
268
|
return;
|
|
260
269
|
}
|
|
261
|
-
const
|
|
270
|
+
const now = this.getUtcNowMs();
|
|
271
|
+
const delay = (licEndMs + AuthMiddleware.CLOCK_TOLERANCE_MS) - now;
|
|
272
|
+
|
|
262
273
|
if (delay <= 0) {
|
|
274
|
+
this.logger.warn('Licence expired immediately (including tolerance).');
|
|
263
275
|
this.markLicenceExpired();
|
|
264
276
|
return;
|
|
265
277
|
}
|
|
278
|
+
|
|
266
279
|
if (AuthMiddleware.licenceExpiryTimer) {
|
|
267
280
|
clearTimeout(AuthMiddleware.licenceExpiryTimer);
|
|
268
281
|
}
|
|
269
|
-
|
|
282
|
+
|
|
283
|
+
// setTimeout uses a 32-bit signed integer. Max value is 2147483647 ms (approx 24.8 days).
|
|
284
|
+
// If the delay is larger than this, setTimeout will fire immediately (overflow).
|
|
285
|
+
// so we cap the delay and re-check when it fires.
|
|
286
|
+
const MAX_TIMEOUT_VAL = 2147483647;
|
|
287
|
+
const safeDelay = Math.min(delay, MAX_TIMEOUT_VAL);
|
|
288
|
+
|
|
270
289
|
AuthMiddleware.licenceExpiryTimer = setTimeout(() => {
|
|
271
|
-
this.
|
|
272
|
-
|
|
290
|
+
const remaining = (licEndMs + AuthMiddleware.CLOCK_TOLERANCE_MS) - this.getUtcNowMs();
|
|
291
|
+
if (remaining <= 0) {
|
|
292
|
+
this.markLicenceExpired();
|
|
293
|
+
} else {
|
|
294
|
+
// Reschedule if we still have time left
|
|
295
|
+
this.scheduleLicenceShutdown(licEndMs);
|
|
296
|
+
}
|
|
297
|
+
}, safeDelay);
|
|
273
298
|
}
|
|
274
299
|
|
|
275
300
|
private stopServer() {
|
|
@@ -291,14 +316,15 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
291
316
|
}, 0);
|
|
292
317
|
}
|
|
293
318
|
|
|
294
|
-
private normalizeEpochMs(epoch: number): number | null {
|
|
295
|
-
|
|
319
|
+
private normalizeEpochMs(epoch: string | number): number | null {
|
|
320
|
+
const epochNum = Number(epoch);
|
|
321
|
+
if (!epochNum || Number.isNaN(epochNum)) {
|
|
296
322
|
return null;
|
|
297
323
|
}
|
|
298
|
-
if (
|
|
299
|
-
return
|
|
324
|
+
if (epochNum < 1_000_000_000_000) {
|
|
325
|
+
return epochNum * 1000;
|
|
300
326
|
}
|
|
301
|
-
return
|
|
327
|
+
return epochNum;
|
|
302
328
|
}
|
|
303
329
|
|
|
304
330
|
private getUtcNowMs() {
|
|
@@ -307,7 +333,13 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
307
333
|
|
|
308
334
|
private verifyJwt(token: string, publicKey: string) {
|
|
309
335
|
const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
|
|
310
|
-
|
|
336
|
+
// We ignore the standard 'exp' claim and rely on our custom 'lic_end' field.
|
|
337
|
+
// Also include a small clock tolerance for the library itself.
|
|
338
|
+
return jwt.verify(token, publicKeys, {
|
|
339
|
+
algorithms: ['RS256'],
|
|
340
|
+
ignoreExpiration: true,
|
|
341
|
+
clockTolerance: 60
|
|
342
|
+
});
|
|
311
343
|
}
|
|
312
344
|
|
|
313
345
|
private decodeAccessToken(authHeader: string, publicKey: string) {
|
|
@@ -0,0 +1,84 @@
|
|
|
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);
|