@gagandeep023/api-gateway 0.3.0 → 0.4.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/backend/index.d.mts +51 -3
- package/dist/backend/index.d.ts +51 -3
- package/dist/backend/index.js +329 -9
- package/dist/backend/index.js.map +1 -1
- package/dist/backend/index.mjs +321 -8
- package/dist/backend/index.mjs.map +1 -1
- package/dist/frontend/index.js +36 -16
- package/dist/frontend/index.js.map +1 -1
- package/dist/frontend/index.mjs +37 -17
- package/dist/frontend/index.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +368 -28
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +361 -28
- package/dist/index.mjs.map +1 -1
- package/dist/types/index.d.mts +19 -1
- package/dist/types/index.d.ts +19 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
package/dist/backend/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
|
-
import { RateLimitConfig, RequestLog, GatewayAnalytics, GatewayMiddlewareConfig, ApiKeysConfig, IpRules } from '../types/index.mjs';
|
|
2
|
+
import { RateLimitConfig, RequestLog, GatewayAnalytics, DeviceEntry, GatewayMiddlewareConfig, ApiKeysConfig, IpRules } from '../types/index.mjs';
|
|
3
3
|
|
|
4
4
|
declare class RateLimiterService {
|
|
5
5
|
private tokenBucket;
|
|
@@ -29,9 +29,43 @@ declare class AnalyticsService {
|
|
|
29
29
|
private getOrderedLogs;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
declare class DeviceRegistryService {
|
|
33
|
+
private devices;
|
|
34
|
+
private filePath;
|
|
35
|
+
private writeTimeout;
|
|
36
|
+
private cleanupInterval;
|
|
37
|
+
private registrationAttempts;
|
|
38
|
+
constructor(filePath: string);
|
|
39
|
+
private loadFromDisk;
|
|
40
|
+
private saveToDiskSync;
|
|
41
|
+
private debouncedSave;
|
|
42
|
+
private cleanupExpired;
|
|
43
|
+
private getActiveCountByIp;
|
|
44
|
+
private checkRateLimit;
|
|
45
|
+
private recordAttempt;
|
|
46
|
+
registerDevice(browserId: string, ip: string, userAgent: string): {
|
|
47
|
+
success: true;
|
|
48
|
+
device: DeviceEntry;
|
|
49
|
+
} | {
|
|
50
|
+
success: false;
|
|
51
|
+
error: string;
|
|
52
|
+
status: number;
|
|
53
|
+
};
|
|
54
|
+
getDevice(browserId: string): DeviceEntry | null;
|
|
55
|
+
updateLastSeen(browserId: string, ip: string): void;
|
|
56
|
+
revokeDevice(browserId: string): boolean;
|
|
57
|
+
getStats(): {
|
|
58
|
+
total: number;
|
|
59
|
+
active: number;
|
|
60
|
+
expired: number;
|
|
61
|
+
};
|
|
62
|
+
destroy(): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
32
65
|
interface GatewayInstances {
|
|
33
66
|
rateLimiterService: RateLimiterService;
|
|
34
67
|
analyticsService: AnalyticsService;
|
|
68
|
+
deviceRegistry?: DeviceRegistryService;
|
|
35
69
|
middleware: Router;
|
|
36
70
|
config: Required<GatewayMiddlewareConfig>;
|
|
37
71
|
}
|
|
@@ -44,7 +78,12 @@ interface GatewayRoutesOptions {
|
|
|
44
78
|
}
|
|
45
79
|
declare function createGatewayRoutes(options: GatewayRoutesOptions): Router;
|
|
46
80
|
|
|
47
|
-
|
|
81
|
+
interface DeviceAuthRoutesOptions {
|
|
82
|
+
deviceRegistry: DeviceRegistryService;
|
|
83
|
+
}
|
|
84
|
+
declare function createDeviceAuthRoutes(options: DeviceAuthRoutesOptions): Router;
|
|
85
|
+
|
|
86
|
+
declare function createApiKeyAuth(getKeys: () => ApiKeysConfig, deviceRegistry?: DeviceRegistryService): (req: Request, res: Response, next: NextFunction) => void;
|
|
48
87
|
|
|
49
88
|
declare function createIpFilter(getRules: () => IpRules): (req: Request, res: Response, next: NextFunction) => void;
|
|
50
89
|
|
|
@@ -52,4 +91,13 @@ declare function createRateLimiter(service: RateLimiterService): (req: Request,
|
|
|
52
91
|
|
|
53
92
|
declare function createRequestLogger(analytics: AnalyticsService): (req: Request, res: Response, next: NextFunction) => void;
|
|
54
93
|
|
|
55
|
-
|
|
94
|
+
declare function generateTOTP(browserId: string, secret: string, timeOffset?: number): string;
|
|
95
|
+
declare function validateTOTP(browserId: string, secret: string, providedCode: string): boolean;
|
|
96
|
+
declare function formatKey(browserId: string, code: string): string;
|
|
97
|
+
declare function parseKey(key: string): {
|
|
98
|
+
browserId: string;
|
|
99
|
+
code: string;
|
|
100
|
+
} | null;
|
|
101
|
+
declare function generateSecret(): string;
|
|
102
|
+
|
|
103
|
+
export { AnalyticsService, type DeviceAuthRoutesOptions, DeviceRegistryService, type GatewayInstances, type GatewayRoutesOptions, RateLimiterService, createApiKeyAuth, createDeviceAuthRoutes, createGatewayMiddleware, createGatewayRoutes, createIpFilter, createRateLimiter, createRequestLogger, formatKey, generateSecret, generateTOTP, parseKey, validateTOTP };
|
package/dist/backend/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
|
-
import { RateLimitConfig, RequestLog, GatewayAnalytics, GatewayMiddlewareConfig, ApiKeysConfig, IpRules } from '../types/index.js';
|
|
2
|
+
import { RateLimitConfig, RequestLog, GatewayAnalytics, DeviceEntry, GatewayMiddlewareConfig, ApiKeysConfig, IpRules } from '../types/index.js';
|
|
3
3
|
|
|
4
4
|
declare class RateLimiterService {
|
|
5
5
|
private tokenBucket;
|
|
@@ -29,9 +29,43 @@ declare class AnalyticsService {
|
|
|
29
29
|
private getOrderedLogs;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
declare class DeviceRegistryService {
|
|
33
|
+
private devices;
|
|
34
|
+
private filePath;
|
|
35
|
+
private writeTimeout;
|
|
36
|
+
private cleanupInterval;
|
|
37
|
+
private registrationAttempts;
|
|
38
|
+
constructor(filePath: string);
|
|
39
|
+
private loadFromDisk;
|
|
40
|
+
private saveToDiskSync;
|
|
41
|
+
private debouncedSave;
|
|
42
|
+
private cleanupExpired;
|
|
43
|
+
private getActiveCountByIp;
|
|
44
|
+
private checkRateLimit;
|
|
45
|
+
private recordAttempt;
|
|
46
|
+
registerDevice(browserId: string, ip: string, userAgent: string): {
|
|
47
|
+
success: true;
|
|
48
|
+
device: DeviceEntry;
|
|
49
|
+
} | {
|
|
50
|
+
success: false;
|
|
51
|
+
error: string;
|
|
52
|
+
status: number;
|
|
53
|
+
};
|
|
54
|
+
getDevice(browserId: string): DeviceEntry | null;
|
|
55
|
+
updateLastSeen(browserId: string, ip: string): void;
|
|
56
|
+
revokeDevice(browserId: string): boolean;
|
|
57
|
+
getStats(): {
|
|
58
|
+
total: number;
|
|
59
|
+
active: number;
|
|
60
|
+
expired: number;
|
|
61
|
+
};
|
|
62
|
+
destroy(): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
32
65
|
interface GatewayInstances {
|
|
33
66
|
rateLimiterService: RateLimiterService;
|
|
34
67
|
analyticsService: AnalyticsService;
|
|
68
|
+
deviceRegistry?: DeviceRegistryService;
|
|
35
69
|
middleware: Router;
|
|
36
70
|
config: Required<GatewayMiddlewareConfig>;
|
|
37
71
|
}
|
|
@@ -44,7 +78,12 @@ interface GatewayRoutesOptions {
|
|
|
44
78
|
}
|
|
45
79
|
declare function createGatewayRoutes(options: GatewayRoutesOptions): Router;
|
|
46
80
|
|
|
47
|
-
|
|
81
|
+
interface DeviceAuthRoutesOptions {
|
|
82
|
+
deviceRegistry: DeviceRegistryService;
|
|
83
|
+
}
|
|
84
|
+
declare function createDeviceAuthRoutes(options: DeviceAuthRoutesOptions): Router;
|
|
85
|
+
|
|
86
|
+
declare function createApiKeyAuth(getKeys: () => ApiKeysConfig, deviceRegistry?: DeviceRegistryService): (req: Request, res: Response, next: NextFunction) => void;
|
|
48
87
|
|
|
49
88
|
declare function createIpFilter(getRules: () => IpRules): (req: Request, res: Response, next: NextFunction) => void;
|
|
50
89
|
|
|
@@ -52,4 +91,13 @@ declare function createRateLimiter(service: RateLimiterService): (req: Request,
|
|
|
52
91
|
|
|
53
92
|
declare function createRequestLogger(analytics: AnalyticsService): (req: Request, res: Response, next: NextFunction) => void;
|
|
54
93
|
|
|
55
|
-
|
|
94
|
+
declare function generateTOTP(browserId: string, secret: string, timeOffset?: number): string;
|
|
95
|
+
declare function validateTOTP(browserId: string, secret: string, providedCode: string): boolean;
|
|
96
|
+
declare function formatKey(browserId: string, code: string): string;
|
|
97
|
+
declare function parseKey(key: string): {
|
|
98
|
+
browserId: string;
|
|
99
|
+
code: string;
|
|
100
|
+
} | null;
|
|
101
|
+
declare function generateSecret(): string;
|
|
102
|
+
|
|
103
|
+
export { AnalyticsService, type DeviceAuthRoutesOptions, DeviceRegistryService, type GatewayInstances, type GatewayRoutesOptions, RateLimiterService, createApiKeyAuth, createDeviceAuthRoutes, createGatewayMiddleware, createGatewayRoutes, createIpFilter, createRateLimiter, createRequestLogger, formatKey, generateSecret, generateTOTP, parseKey, validateTOTP };
|
package/dist/backend/index.js
CHANGED
|
@@ -31,13 +31,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var backend_exports = {};
|
|
32
32
|
__export(backend_exports, {
|
|
33
33
|
AnalyticsService: () => AnalyticsService,
|
|
34
|
+
DeviceRegistryService: () => DeviceRegistryService,
|
|
34
35
|
RateLimiterService: () => RateLimiterService,
|
|
35
36
|
createApiKeyAuth: () => createApiKeyAuth,
|
|
37
|
+
createDeviceAuthRoutes: () => createDeviceAuthRoutes,
|
|
36
38
|
createGatewayMiddleware: () => createGatewayMiddleware,
|
|
37
39
|
createGatewayRoutes: () => createGatewayRoutes,
|
|
38
40
|
createIpFilter: () => createIpFilter,
|
|
39
41
|
createRateLimiter: () => createRateLimiter,
|
|
40
|
-
createRequestLogger: () => createRequestLogger
|
|
42
|
+
createRequestLogger: () => createRequestLogger,
|
|
43
|
+
formatKey: () => formatKey,
|
|
44
|
+
generateSecret: () => generateSecret,
|
|
45
|
+
generateTOTP: () => generateTOTP,
|
|
46
|
+
parseKey: () => parseKey,
|
|
47
|
+
validateTOTP: () => validateTOTP
|
|
41
48
|
});
|
|
42
49
|
module.exports = __toCommonJS(backend_exports);
|
|
43
50
|
|
|
@@ -201,7 +208,7 @@ var AnalyticsService = class {
|
|
|
201
208
|
const current = endpointCounts.get(log.path) || 0;
|
|
202
209
|
endpointCounts.set(log.path, current + 1);
|
|
203
210
|
}
|
|
204
|
-
const topEndpoints = Array.from(endpointCounts.entries()).map(([
|
|
211
|
+
const topEndpoints = Array.from(endpointCounts.entries()).map(([path2, count]) => ({ path: path2, count })).sort((a, b) => b.count - a.count).slice(0, 5);
|
|
205
212
|
const errorCount = ordered.filter((l) => l.statusCode >= 400).length;
|
|
206
213
|
const errorRate = this.count > 0 ? errorCount / this.count * 100 : 0;
|
|
207
214
|
const totalResponseTime = ordered.reduce((sum, l) => sum + l.responseTime, 0);
|
|
@@ -235,6 +242,226 @@ var AnalyticsService = class {
|
|
|
235
242
|
}
|
|
236
243
|
};
|
|
237
244
|
|
|
245
|
+
// src/backend/services/DeviceRegistryService.ts
|
|
246
|
+
var import_fs = __toESM(require("fs"));
|
|
247
|
+
var import_path = __toESM(require("path"));
|
|
248
|
+
|
|
249
|
+
// src/backend/utils/totp.ts
|
|
250
|
+
var import_crypto = __toESM(require("crypto"));
|
|
251
|
+
var TIME_WINDOW_MS = 3600 * 1e3;
|
|
252
|
+
var CODE_LENGTH = 16;
|
|
253
|
+
function generateTOTP(browserId, secret, timeOffset = 0) {
|
|
254
|
+
const timeWindow = Math.floor(Date.now() / TIME_WINDOW_MS) + timeOffset;
|
|
255
|
+
const message = `${browserId}:${timeWindow}`;
|
|
256
|
+
const hmac = import_crypto.default.createHmac("sha256", secret).update(message).digest("hex");
|
|
257
|
+
return hmac.substring(0, CODE_LENGTH);
|
|
258
|
+
}
|
|
259
|
+
function validateTOTP(browserId, secret, providedCode) {
|
|
260
|
+
const currentCode = generateTOTP(browserId, secret, 0);
|
|
261
|
+
const previousCode = generateTOTP(browserId, secret, -1);
|
|
262
|
+
return timingSafeEqual(providedCode, currentCode) || timingSafeEqual(providedCode, previousCode);
|
|
263
|
+
}
|
|
264
|
+
function timingSafeEqual(a, b) {
|
|
265
|
+
if (a.length !== b.length) return false;
|
|
266
|
+
const bufA = Buffer.from(a);
|
|
267
|
+
const bufB = Buffer.from(b);
|
|
268
|
+
return import_crypto.default.timingSafeEqual(bufA, bufB);
|
|
269
|
+
}
|
|
270
|
+
function formatKey(browserId, code) {
|
|
271
|
+
return `totp_${browserId}_${code}`;
|
|
272
|
+
}
|
|
273
|
+
function parseKey(key) {
|
|
274
|
+
if (!key.startsWith("totp_")) return null;
|
|
275
|
+
const parts = key.slice(5).split("_");
|
|
276
|
+
if (parts.length < 2) return null;
|
|
277
|
+
const code = parts[parts.length - 1];
|
|
278
|
+
const browserId = parts.slice(0, -1).join("_");
|
|
279
|
+
if (!browserId || !code) return null;
|
|
280
|
+
return { browserId, code };
|
|
281
|
+
}
|
|
282
|
+
function generateSecret() {
|
|
283
|
+
return import_crypto.default.randomBytes(32).toString("hex");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/backend/services/DeviceRegistryService.ts
|
|
287
|
+
var ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
288
|
+
var CLEANUP_INTERVAL_MS = 60 * 60 * 1e3;
|
|
289
|
+
var DEBOUNCE_WRITE_MS = 2e3;
|
|
290
|
+
var MAX_REGISTRATIONS_PER_IP_PER_MIN = 10;
|
|
291
|
+
var MAX_REGISTRATIONS_PER_IP_TOTAL = 30;
|
|
292
|
+
var RATE_WINDOW_MS = 60 * 1e3;
|
|
293
|
+
var DeviceRegistryService = class {
|
|
294
|
+
devices = /* @__PURE__ */ new Map();
|
|
295
|
+
filePath;
|
|
296
|
+
writeTimeout = null;
|
|
297
|
+
cleanupInterval = null;
|
|
298
|
+
// In-memory rate tracking: ip -> timestamps of recent registration attempts
|
|
299
|
+
registrationAttempts = /* @__PURE__ */ new Map();
|
|
300
|
+
constructor(filePath) {
|
|
301
|
+
this.filePath = filePath;
|
|
302
|
+
this.loadFromDisk();
|
|
303
|
+
this.cleanupExpired();
|
|
304
|
+
this.cleanupInterval = setInterval(() => this.cleanupExpired(), CLEANUP_INTERVAL_MS);
|
|
305
|
+
}
|
|
306
|
+
loadFromDisk() {
|
|
307
|
+
try {
|
|
308
|
+
if (import_fs.default.existsSync(this.filePath)) {
|
|
309
|
+
const raw = import_fs.default.readFileSync(this.filePath, "utf-8");
|
|
310
|
+
const data = JSON.parse(raw);
|
|
311
|
+
for (const device of data.devices) {
|
|
312
|
+
this.devices.set(device.browserId, device);
|
|
313
|
+
}
|
|
314
|
+
console.log(`[DeviceRegistry] Loaded ${this.devices.size} devices from ${this.filePath}`);
|
|
315
|
+
} else {
|
|
316
|
+
const dir = import_path.default.dirname(this.filePath);
|
|
317
|
+
if (!import_fs.default.existsSync(dir)) {
|
|
318
|
+
import_fs.default.mkdirSync(dir, { recursive: true });
|
|
319
|
+
}
|
|
320
|
+
this.saveToDiskSync();
|
|
321
|
+
console.log(`[DeviceRegistry] Created new devices file at ${this.filePath}`);
|
|
322
|
+
}
|
|
323
|
+
} catch (err) {
|
|
324
|
+
console.error("[DeviceRegistry] Failed to load devices file:", err);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
saveToDiskSync() {
|
|
328
|
+
const data = {
|
|
329
|
+
devices: Array.from(this.devices.values())
|
|
330
|
+
};
|
|
331
|
+
import_fs.default.writeFileSync(this.filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
332
|
+
}
|
|
333
|
+
debouncedSave() {
|
|
334
|
+
if (this.writeTimeout) clearTimeout(this.writeTimeout);
|
|
335
|
+
this.writeTimeout = setTimeout(() => {
|
|
336
|
+
this.saveToDiskSync();
|
|
337
|
+
}, DEBOUNCE_WRITE_MS);
|
|
338
|
+
}
|
|
339
|
+
cleanupExpired() {
|
|
340
|
+
const now = Date.now();
|
|
341
|
+
let removed = 0;
|
|
342
|
+
for (const [id, device] of this.devices) {
|
|
343
|
+
if (new Date(device.expiresAt).getTime() <= now) {
|
|
344
|
+
this.devices.delete(id);
|
|
345
|
+
removed++;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (removed > 0) {
|
|
349
|
+
console.log(`[DeviceRegistry] Cleaned up ${removed} expired devices`);
|
|
350
|
+
this.debouncedSave();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
getActiveCountByIp(ip) {
|
|
354
|
+
const now = Date.now();
|
|
355
|
+
let count = 0;
|
|
356
|
+
for (const device of this.devices.values()) {
|
|
357
|
+
if (device.ip === ip && device.active && new Date(device.expiresAt).getTime() > now) {
|
|
358
|
+
count++;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return count;
|
|
362
|
+
}
|
|
363
|
+
checkRateLimit(ip) {
|
|
364
|
+
const now = Date.now();
|
|
365
|
+
const attempts = this.registrationAttempts.get(ip) || [];
|
|
366
|
+
const recent = attempts.filter((t) => now - t < RATE_WINDOW_MS);
|
|
367
|
+
this.registrationAttempts.set(ip, recent);
|
|
368
|
+
return recent.length < MAX_REGISTRATIONS_PER_IP_PER_MIN;
|
|
369
|
+
}
|
|
370
|
+
recordAttempt(ip) {
|
|
371
|
+
const attempts = this.registrationAttempts.get(ip) || [];
|
|
372
|
+
attempts.push(Date.now());
|
|
373
|
+
this.registrationAttempts.set(ip, attempts);
|
|
374
|
+
}
|
|
375
|
+
registerDevice(browserId, ip, userAgent) {
|
|
376
|
+
if (!this.checkRateLimit(ip)) {
|
|
377
|
+
return {
|
|
378
|
+
success: false,
|
|
379
|
+
error: "Registration rate limit exceeded. Max 10 per minute per IP.",
|
|
380
|
+
status: 429
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
this.recordAttempt(ip);
|
|
384
|
+
if (this.getActiveCountByIp(ip) >= MAX_REGISTRATIONS_PER_IP_TOTAL) {
|
|
385
|
+
return {
|
|
386
|
+
success: false,
|
|
387
|
+
error: "Maximum device registrations reached for this IP. Max 30 per IP.",
|
|
388
|
+
status: 403
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const existing = this.devices.get(browserId);
|
|
392
|
+
if (existing && existing.active && new Date(existing.expiresAt).getTime() > Date.now()) {
|
|
393
|
+
existing.expiresAt = new Date(Date.now() + ONE_WEEK_MS).toISOString();
|
|
394
|
+
existing.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
395
|
+
existing.lastIp = ip;
|
|
396
|
+
this.debouncedSave();
|
|
397
|
+
return { success: true, device: existing };
|
|
398
|
+
}
|
|
399
|
+
const now = /* @__PURE__ */ new Date();
|
|
400
|
+
const device = {
|
|
401
|
+
browserId,
|
|
402
|
+
sharedSecret: generateSecret(),
|
|
403
|
+
ip,
|
|
404
|
+
userAgent,
|
|
405
|
+
registeredAt: now.toISOString(),
|
|
406
|
+
expiresAt: new Date(now.getTime() + ONE_WEEK_MS).toISOString(),
|
|
407
|
+
lastSeen: now.toISOString(),
|
|
408
|
+
lastIp: ip,
|
|
409
|
+
active: true
|
|
410
|
+
};
|
|
411
|
+
this.devices.set(browserId, device);
|
|
412
|
+
this.debouncedSave();
|
|
413
|
+
return { success: true, device };
|
|
414
|
+
}
|
|
415
|
+
getDevice(browserId) {
|
|
416
|
+
const device = this.devices.get(browserId) || null;
|
|
417
|
+
if (!device) return null;
|
|
418
|
+
if (new Date(device.expiresAt).getTime() <= Date.now()) {
|
|
419
|
+
this.devices.delete(browserId);
|
|
420
|
+
this.debouncedSave();
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
if (!device.active) return null;
|
|
424
|
+
return device;
|
|
425
|
+
}
|
|
426
|
+
updateLastSeen(browserId, ip) {
|
|
427
|
+
const device = this.devices.get(browserId);
|
|
428
|
+
if (!device) return;
|
|
429
|
+
device.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
430
|
+
if (device.lastIp !== ip) {
|
|
431
|
+
console.log(`[DeviceRegistry] IP change for ${browserId}: ${device.lastIp} -> ${ip}`);
|
|
432
|
+
device.lastIp = ip;
|
|
433
|
+
}
|
|
434
|
+
this.debouncedSave();
|
|
435
|
+
}
|
|
436
|
+
revokeDevice(browserId) {
|
|
437
|
+
const device = this.devices.get(browserId);
|
|
438
|
+
if (!device) return false;
|
|
439
|
+
device.active = false;
|
|
440
|
+
this.debouncedSave();
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
getStats() {
|
|
444
|
+
const now = Date.now();
|
|
445
|
+
let active = 0;
|
|
446
|
+
let expired = 0;
|
|
447
|
+
for (const device of this.devices.values()) {
|
|
448
|
+
if (!device.active || new Date(device.expiresAt).getTime() <= now) {
|
|
449
|
+
expired++;
|
|
450
|
+
} else {
|
|
451
|
+
active++;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return { total: this.devices.size, active, expired };
|
|
455
|
+
}
|
|
456
|
+
destroy() {
|
|
457
|
+
if (this.cleanupInterval) clearInterval(this.cleanupInterval);
|
|
458
|
+
if (this.writeTimeout) {
|
|
459
|
+
clearTimeout(this.writeTimeout);
|
|
460
|
+
this.saveToDiskSync();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
238
465
|
// src/config/defaults.ts
|
|
239
466
|
var DEFAULT_RATE_LIMIT_CONFIG = {
|
|
240
467
|
tiers: {
|
|
@@ -255,7 +482,7 @@ var DEFAULT_API_KEYS = {
|
|
|
255
482
|
};
|
|
256
483
|
|
|
257
484
|
// src/backend/middleware/apiKeyAuth.ts
|
|
258
|
-
function createApiKeyAuth(getKeys) {
|
|
485
|
+
function createApiKeyAuth(getKeys, deviceRegistry) {
|
|
259
486
|
return function apiKeyAuth(req, res, next) {
|
|
260
487
|
const apiKey = req.header("X-API-Key") || req.query.apiKey;
|
|
261
488
|
if (!apiKey) {
|
|
@@ -264,6 +491,29 @@ function createApiKeyAuth(getKeys) {
|
|
|
264
491
|
next();
|
|
265
492
|
return;
|
|
266
493
|
}
|
|
494
|
+
if (apiKey.startsWith("totp_") && deviceRegistry) {
|
|
495
|
+
const parsed = parseKey(apiKey);
|
|
496
|
+
if (!parsed) {
|
|
497
|
+
res.status(401).json({ error: "Malformed TOTP key" });
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const device = deviceRegistry.getDevice(parsed.browserId);
|
|
501
|
+
if (!device) {
|
|
502
|
+
res.status(401).json({ error: "Device not registered or expired" });
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (!validateTOTP(parsed.browserId, device.sharedSecret, parsed.code)) {
|
|
506
|
+
res.status(401).json({ error: "Invalid or expired TOTP code" });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const ip = req.ip || req.socket.remoteAddress || "unknown";
|
|
510
|
+
deviceRegistry.updateLastSeen(parsed.browserId, ip);
|
|
511
|
+
req.clientId = parsed.browserId;
|
|
512
|
+
req.tier = "free";
|
|
513
|
+
req.apiKeyValue = apiKey;
|
|
514
|
+
next();
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
267
517
|
const config = getKeys();
|
|
268
518
|
const keyEntry = config.keys.find((k) => k.key === apiKey && k.active);
|
|
269
519
|
if (!keyEntry) {
|
|
@@ -348,18 +598,24 @@ function createGatewayMiddleware(userConfig) {
|
|
|
348
598
|
const config = {
|
|
349
599
|
rateLimits: userConfig?.rateLimits ?? DEFAULT_RATE_LIMIT_CONFIG,
|
|
350
600
|
ipRules: userConfig?.ipRules ?? DEFAULT_IP_RULES,
|
|
351
|
-
apiKeys: userConfig?.apiKeys ?? DEFAULT_API_KEYS
|
|
601
|
+
apiKeys: userConfig?.apiKeys ?? DEFAULT_API_KEYS,
|
|
602
|
+
deviceRegistryPath: userConfig?.deviceRegistryPath ?? ""
|
|
352
603
|
};
|
|
353
604
|
const rateLimiterService = new RateLimiterService(config.rateLimits);
|
|
354
605
|
const analyticsService = new AnalyticsService();
|
|
606
|
+
let deviceRegistry;
|
|
607
|
+
if (config.deviceRegistryPath) {
|
|
608
|
+
deviceRegistry = new DeviceRegistryService(config.deviceRegistryPath);
|
|
609
|
+
}
|
|
355
610
|
const router = (0, import_express.Router)();
|
|
356
611
|
router.use(createRequestLogger(analyticsService));
|
|
357
|
-
router.use(createApiKeyAuth(() => config.apiKeys));
|
|
612
|
+
router.use(createApiKeyAuth(() => config.apiKeys, deviceRegistry));
|
|
358
613
|
router.use(createIpFilter(() => config.ipRules));
|
|
359
614
|
router.use(createRateLimiter(rateLimiterService));
|
|
360
615
|
return {
|
|
361
616
|
rateLimiterService,
|
|
362
617
|
analyticsService,
|
|
618
|
+
deviceRegistry,
|
|
363
619
|
middleware: router,
|
|
364
620
|
config
|
|
365
621
|
};
|
|
@@ -367,7 +623,7 @@ function createGatewayMiddleware(userConfig) {
|
|
|
367
623
|
|
|
368
624
|
// src/backend/routes/gateway.ts
|
|
369
625
|
var import_express2 = require("express");
|
|
370
|
-
var
|
|
626
|
+
var import_crypto2 = __toESM(require("crypto"));
|
|
371
627
|
function createGatewayRoutes(options) {
|
|
372
628
|
const { rateLimiterService, analyticsService, config } = options;
|
|
373
629
|
const router = (0, import_express2.Router)();
|
|
@@ -380,7 +636,6 @@ function createGatewayRoutes(options) {
|
|
|
380
636
|
res.setHeader("Cache-Control", "no-cache");
|
|
381
637
|
res.setHeader("Connection", "keep-alive");
|
|
382
638
|
res.setHeader("X-Accel-Buffering", "no");
|
|
383
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
384
639
|
res.flushHeaders();
|
|
385
640
|
const send = () => {
|
|
386
641
|
const analytics = analyticsService.getAnalytics(rateLimiterService.rateLimitHits);
|
|
@@ -411,7 +666,7 @@ function createGatewayRoutes(options) {
|
|
|
411
666
|
}
|
|
412
667
|
const newKey = {
|
|
413
668
|
id: `key_${String(config.apiKeys.keys.length + 1).padStart(3, "0")}`,
|
|
414
|
-
key: `gw_live_${
|
|
669
|
+
key: `gw_live_${import_crypto2.default.randomBytes(16).toString("hex")}`,
|
|
415
670
|
name,
|
|
416
671
|
tier: tier || "free",
|
|
417
672
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -438,15 +693,80 @@ function createGatewayRoutes(options) {
|
|
|
438
693
|
});
|
|
439
694
|
return router;
|
|
440
695
|
}
|
|
696
|
+
|
|
697
|
+
// src/backend/routes/deviceAuth.ts
|
|
698
|
+
var import_express3 = require("express");
|
|
699
|
+
function createDeviceAuthRoutes(options) {
|
|
700
|
+
const { deviceRegistry } = options;
|
|
701
|
+
const router = (0, import_express3.Router)();
|
|
702
|
+
router.post("/register", (req, res) => {
|
|
703
|
+
const { browserId } = req.body;
|
|
704
|
+
if (!browserId || typeof browserId !== "string") {
|
|
705
|
+
res.status(400).json({ error: "browserId is required" });
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
709
|
+
if (!uuidRegex.test(browserId)) {
|
|
710
|
+
res.status(400).json({ error: "browserId must be a valid UUID" });
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const ip = req.ip || req.socket.remoteAddress || "unknown";
|
|
714
|
+
const userAgent = req.headers["user-agent"] || "unknown";
|
|
715
|
+
const result = deviceRegistry.registerDevice(browserId, ip, userAgent);
|
|
716
|
+
if (!result.success) {
|
|
717
|
+
res.status(result.status).json({ error: result.error });
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
res.status(201).json({
|
|
721
|
+
browserId: result.device.browserId,
|
|
722
|
+
sharedSecret: result.device.sharedSecret,
|
|
723
|
+
expiresAt: result.device.expiresAt
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
router.get("/status/:browserId", (req, res) => {
|
|
727
|
+
const browserId = req.params.browserId;
|
|
728
|
+
const device = deviceRegistry.getDevice(browserId);
|
|
729
|
+
if (!device) {
|
|
730
|
+
res.status(404).json({ registered: false });
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
res.json({
|
|
734
|
+
registered: true,
|
|
735
|
+
browserId: device.browserId,
|
|
736
|
+
expiresAt: device.expiresAt,
|
|
737
|
+
registeredAt: device.registeredAt
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
router.delete("/:browserId", (req, res) => {
|
|
741
|
+
const browserId = req.params.browserId;
|
|
742
|
+
const revoked = deviceRegistry.revokeDevice(browserId);
|
|
743
|
+
if (!revoked) {
|
|
744
|
+
res.status(404).json({ error: "Device not found" });
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
res.json({ message: "Device revoked", browserId });
|
|
748
|
+
});
|
|
749
|
+
router.get("/stats", (_req, res) => {
|
|
750
|
+
res.json(deviceRegistry.getStats());
|
|
751
|
+
});
|
|
752
|
+
return router;
|
|
753
|
+
}
|
|
441
754
|
// Annotate the CommonJS export names for ESM import in node:
|
|
442
755
|
0 && (module.exports = {
|
|
443
756
|
AnalyticsService,
|
|
757
|
+
DeviceRegistryService,
|
|
444
758
|
RateLimiterService,
|
|
445
759
|
createApiKeyAuth,
|
|
760
|
+
createDeviceAuthRoutes,
|
|
446
761
|
createGatewayMiddleware,
|
|
447
762
|
createGatewayRoutes,
|
|
448
763
|
createIpFilter,
|
|
449
764
|
createRateLimiter,
|
|
450
|
-
createRequestLogger
|
|
765
|
+
createRequestLogger,
|
|
766
|
+
formatKey,
|
|
767
|
+
generateSecret,
|
|
768
|
+
generateTOTP,
|
|
769
|
+
parseKey,
|
|
770
|
+
validateTOTP
|
|
451
771
|
});
|
|
452
772
|
//# sourceMappingURL=index.js.map
|