@spfn/core 0.2.0-beta.12 → 0.2.0-beta.14
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/config/index.d.ts +6 -6
- package/dist/config/index.js +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/env/loader.d.ts +27 -19
- package/dist/env/loader.js +33 -25
- package/dist/env/loader.js.map +1 -1
- package/dist/event/sse/client.d.ts +50 -1
- package/dist/event/sse/client.js +57 -22
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +10 -4
- package/dist/event/sse/index.js +125 -12
- package/dist/event/sse/index.js.map +1 -1
- package/dist/server/index.d.ts +9 -11
- package/dist/server/index.js +335 -148
- package/dist/server/index.js.map +1 -1
- package/dist/types-B-lVqv6b.d.ts +298 -0
- package/docs/env.md +43 -21
- package/docs/event.md +88 -0
- package/package.json +1 -1
- package/dist/types-B-e_f2dQ.d.ts +0 -121
package/dist/server/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { env } from '@spfn/core/config';
|
|
2
|
-
import {
|
|
3
|
-
import { existsSync } from 'fs';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
4
3
|
import { resolve, join } from 'path';
|
|
4
|
+
import { parse } from 'dotenv';
|
|
5
|
+
import { logger } from '@spfn/core/logger';
|
|
5
6
|
import { Hono } from 'hono';
|
|
6
7
|
import { cors } from 'hono/cors';
|
|
7
8
|
import { registerRoutes } from '@spfn/core/route';
|
|
8
9
|
import { ErrorHandler, RequestLogger } from '@spfn/core/middleware';
|
|
9
10
|
import { streamSSE } from 'hono/streaming';
|
|
10
|
-
import {
|
|
11
|
+
import { randomBytes } from 'crypto';
|
|
11
12
|
import { initDatabase, getDatabase, closeDatabase } from '@spfn/core/db';
|
|
12
13
|
import { initCache, getCache, closeCache } from '@spfn/core/cache';
|
|
13
14
|
import { serve } from '@hono/node-server';
|
|
@@ -314,36 +315,96 @@ var init_formatters = __esm({
|
|
|
314
315
|
};
|
|
315
316
|
}
|
|
316
317
|
});
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
318
|
+
var envLogger = logger.child("@spfn/core:env-loader");
|
|
319
|
+
function getEnvFiles(nodeEnv, server) {
|
|
320
|
+
const files = [
|
|
321
|
+
".env",
|
|
322
|
+
`.env.${nodeEnv}`
|
|
323
|
+
];
|
|
324
|
+
if (nodeEnv !== "test") {
|
|
325
|
+
files.push(".env.local");
|
|
326
|
+
}
|
|
327
|
+
files.push(`.env.${nodeEnv}.local`);
|
|
328
|
+
if (server) {
|
|
329
|
+
files.push(".env.server");
|
|
330
|
+
files.push(".env.server.local");
|
|
331
|
+
}
|
|
332
|
+
return files;
|
|
333
|
+
}
|
|
334
|
+
function parseEnvFile(filePath) {
|
|
335
|
+
if (!existsSync(filePath)) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
return parse(readFileSync(filePath, "utf-8"));
|
|
339
|
+
}
|
|
340
|
+
function loadEnv(options = {}) {
|
|
341
|
+
const {
|
|
342
|
+
cwd = process.cwd(),
|
|
343
|
+
nodeEnv = process.env.NODE_ENV || "local",
|
|
344
|
+
server = true,
|
|
345
|
+
debug = false,
|
|
346
|
+
override = false
|
|
347
|
+
} = options;
|
|
348
|
+
const envFiles = getEnvFiles(nodeEnv, server);
|
|
349
|
+
const loadedFiles = [];
|
|
350
|
+
const existingKeys = new Set(Object.keys(process.env));
|
|
351
|
+
const merged = {};
|
|
352
|
+
for (const fileName of envFiles) {
|
|
353
|
+
const filePath = resolve(cwd, fileName);
|
|
354
|
+
const parsed = parseEnvFile(filePath);
|
|
355
|
+
if (parsed === null) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
loadedFiles.push(fileName);
|
|
359
|
+
Object.assign(merged, parsed);
|
|
360
|
+
}
|
|
361
|
+
const loadedKeys = [];
|
|
362
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
363
|
+
if (!override && existingKeys.has(key)) {
|
|
364
|
+
continue;
|
|
332
365
|
}
|
|
366
|
+
process.env[key] = value;
|
|
367
|
+
loadedKeys.push(key);
|
|
368
|
+
}
|
|
369
|
+
if (debug && loadedFiles.length > 0) {
|
|
370
|
+
envLogger.debug(`Loaded env files: ${loadedFiles.join(", ")}`);
|
|
371
|
+
envLogger.debug(`Loaded ${loadedKeys.length} environment variables`);
|
|
372
|
+
}
|
|
373
|
+
return { loadedFiles, loadedKeys };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/server/dotenv-loader.ts
|
|
377
|
+
var warned = false;
|
|
378
|
+
function loadEnvFiles() {
|
|
379
|
+
if (!warned) {
|
|
380
|
+
warned = true;
|
|
381
|
+
console.warn(
|
|
382
|
+
'[SPFN] loadEnvFiles() is deprecated. Use loadEnv() from "@spfn/core/env/loader" instead.'
|
|
383
|
+
);
|
|
333
384
|
}
|
|
385
|
+
loadEnv();
|
|
334
386
|
}
|
|
335
387
|
var sseLogger = logger.child("@spfn/core:sse");
|
|
336
|
-
function createSSEHandler(router,
|
|
388
|
+
function createSSEHandler(router, config = {}, tokenManager) {
|
|
337
389
|
const {
|
|
338
|
-
pingInterval = 3e4
|
|
339
|
-
|
|
340
|
-
} =
|
|
390
|
+
pingInterval = 3e4,
|
|
391
|
+
auth: authConfig
|
|
392
|
+
} = config;
|
|
341
393
|
return async (c) => {
|
|
342
|
-
const
|
|
343
|
-
if (
|
|
394
|
+
const subject = await authenticateToken(c, tokenManager);
|
|
395
|
+
if (subject === false) {
|
|
396
|
+
return c.json({ error: "Missing token parameter" }, 401);
|
|
397
|
+
}
|
|
398
|
+
if (subject === null) {
|
|
399
|
+
return c.json({ error: "Invalid or expired token" }, 401);
|
|
400
|
+
}
|
|
401
|
+
if (subject) {
|
|
402
|
+
c.set("sseSubject", subject);
|
|
403
|
+
}
|
|
404
|
+
const requestedEvents = parseRequestedEvents(c);
|
|
405
|
+
if (!requestedEvents) {
|
|
344
406
|
return c.json({ error: "Missing events parameter" }, 400);
|
|
345
407
|
}
|
|
346
|
-
const requestedEvents = eventsParam.split(",").map((e) => e.trim());
|
|
347
408
|
const validEventNames = router.eventNames;
|
|
348
409
|
const invalidEvents = requestedEvents.filter((e) => !validEventNames.includes(e));
|
|
349
410
|
if (invalidEvents.length > 0) {
|
|
@@ -353,19 +414,29 @@ function createSSEHandler(router, config2 = {}) {
|
|
|
353
414
|
validEvents: validEventNames
|
|
354
415
|
}, 400);
|
|
355
416
|
}
|
|
417
|
+
const allowedEvents = await authorizeEvents(subject, requestedEvents, authConfig);
|
|
418
|
+
if (allowedEvents === null) {
|
|
419
|
+
return c.json({ error: "Not authorized for any requested events" }, 403);
|
|
420
|
+
}
|
|
356
421
|
sseLogger.debug("SSE connection requested", {
|
|
357
|
-
events:
|
|
422
|
+
events: allowedEvents,
|
|
423
|
+
subject: subject || void 0,
|
|
358
424
|
clientIp: c.req.header("x-forwarded-for") || c.req.header("x-real-ip")
|
|
359
425
|
});
|
|
360
426
|
return streamSSE(c, async (stream) => {
|
|
361
427
|
const unsubscribes = [];
|
|
362
428
|
let messageId = 0;
|
|
363
|
-
for (const eventName of
|
|
429
|
+
for (const eventName of allowedEvents) {
|
|
364
430
|
const eventDef = router.events[eventName];
|
|
365
431
|
if (!eventDef) {
|
|
366
432
|
continue;
|
|
367
433
|
}
|
|
368
434
|
const unsubscribe = eventDef.subscribe((payload) => {
|
|
435
|
+
if (subject && authConfig?.filter?.[eventName]) {
|
|
436
|
+
if (!authConfig.filter[eventName](subject, payload)) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
369
440
|
messageId++;
|
|
370
441
|
const message = {
|
|
371
442
|
event: eventName,
|
|
@@ -384,13 +455,13 @@ function createSSEHandler(router, config2 = {}) {
|
|
|
384
455
|
unsubscribes.push(unsubscribe);
|
|
385
456
|
}
|
|
386
457
|
sseLogger.info("SSE connection established", {
|
|
387
|
-
events:
|
|
458
|
+
events: allowedEvents,
|
|
388
459
|
subscriptionCount: unsubscribes.length
|
|
389
460
|
});
|
|
390
461
|
await stream.writeSSE({
|
|
391
462
|
event: "connected",
|
|
392
463
|
data: JSON.stringify({
|
|
393
|
-
subscribedEvents:
|
|
464
|
+
subscribedEvents: allowedEvents,
|
|
394
465
|
timestamp: Date.now()
|
|
395
466
|
})
|
|
396
467
|
});
|
|
@@ -407,7 +478,7 @@ function createSSEHandler(router, config2 = {}) {
|
|
|
407
478
|
clearInterval(pingTimer);
|
|
408
479
|
unsubscribes.forEach((fn) => fn());
|
|
409
480
|
sseLogger.info("SSE connection closed", {
|
|
410
|
-
events:
|
|
481
|
+
events: allowedEvents
|
|
411
482
|
});
|
|
412
483
|
}, async (err) => {
|
|
413
484
|
sseLogger.error("SSE stream error", {
|
|
@@ -416,6 +487,99 @@ function createSSEHandler(router, config2 = {}) {
|
|
|
416
487
|
});
|
|
417
488
|
};
|
|
418
489
|
}
|
|
490
|
+
async function authenticateToken(c, tokenManager) {
|
|
491
|
+
if (!tokenManager) {
|
|
492
|
+
return void 0;
|
|
493
|
+
}
|
|
494
|
+
const token = c.req.query("token");
|
|
495
|
+
if (!token) {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
return await tokenManager.verify(token);
|
|
499
|
+
}
|
|
500
|
+
function parseRequestedEvents(c) {
|
|
501
|
+
const eventsParam = c.req.query("events");
|
|
502
|
+
if (!eventsParam) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
return eventsParam.split(",").map((e) => e.trim());
|
|
506
|
+
}
|
|
507
|
+
async function authorizeEvents(subject, requestedEvents, authConfig) {
|
|
508
|
+
if (!subject || !authConfig?.authorize) {
|
|
509
|
+
return requestedEvents;
|
|
510
|
+
}
|
|
511
|
+
const allowed = await authConfig.authorize(subject, requestedEvents);
|
|
512
|
+
if (allowed.length === 0) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
return allowed;
|
|
516
|
+
}
|
|
517
|
+
var InMemoryTokenStore = class {
|
|
518
|
+
tokens = /* @__PURE__ */ new Map();
|
|
519
|
+
async set(token, data) {
|
|
520
|
+
this.tokens.set(token, data);
|
|
521
|
+
}
|
|
522
|
+
async consume(token) {
|
|
523
|
+
const data = this.tokens.get(token);
|
|
524
|
+
if (!data) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
this.tokens.delete(token);
|
|
528
|
+
return data;
|
|
529
|
+
}
|
|
530
|
+
async cleanup() {
|
|
531
|
+
const now = Date.now();
|
|
532
|
+
for (const [token, data] of this.tokens) {
|
|
533
|
+
if (data.expiresAt <= now) {
|
|
534
|
+
this.tokens.delete(token);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
var SSETokenManager = class {
|
|
540
|
+
store;
|
|
541
|
+
ttl;
|
|
542
|
+
cleanupTimer = null;
|
|
543
|
+
constructor(config) {
|
|
544
|
+
this.ttl = config?.ttl ?? 3e4;
|
|
545
|
+
this.store = config?.store ?? new InMemoryTokenStore();
|
|
546
|
+
const cleanupInterval = config?.cleanupInterval ?? 6e4;
|
|
547
|
+
this.cleanupTimer = setInterval(() => void this.store.cleanup(), cleanupInterval);
|
|
548
|
+
this.cleanupTimer.unref();
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Issue a new one-time-use token for the given subject
|
|
552
|
+
*/
|
|
553
|
+
async issue(subject) {
|
|
554
|
+
const token = randomBytes(32).toString("hex");
|
|
555
|
+
await this.store.set(token, {
|
|
556
|
+
token,
|
|
557
|
+
subject,
|
|
558
|
+
expiresAt: Date.now() + this.ttl
|
|
559
|
+
});
|
|
560
|
+
return token;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Verify and consume a token
|
|
564
|
+
* @returns subject string if valid, null if invalid/expired/already consumed
|
|
565
|
+
*/
|
|
566
|
+
async verify(token) {
|
|
567
|
+
const data = await this.store.consume(token);
|
|
568
|
+
if (!data || data.expiresAt <= Date.now()) {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
return data.subject;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Cleanup timer and resources
|
|
575
|
+
*/
|
|
576
|
+
destroy() {
|
|
577
|
+
if (this.cleanupTimer) {
|
|
578
|
+
clearInterval(this.cleanupTimer);
|
|
579
|
+
this.cleanupTimer = null;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
};
|
|
419
583
|
function createHealthCheckHandler(detailed) {
|
|
420
584
|
return async (c) => {
|
|
421
585
|
const response = {
|
|
@@ -474,34 +638,34 @@ function applyServerTimeouts(server, timeouts) {
|
|
|
474
638
|
server.headersTimeout = timeouts.headers;
|
|
475
639
|
}
|
|
476
640
|
}
|
|
477
|
-
function getTimeoutConfig(
|
|
641
|
+
function getTimeoutConfig(config) {
|
|
478
642
|
return {
|
|
479
|
-
request:
|
|
480
|
-
keepAlive:
|
|
481
|
-
headers:
|
|
643
|
+
request: config?.request ?? env.SERVER_TIMEOUT,
|
|
644
|
+
keepAlive: config?.keepAlive ?? env.SERVER_KEEPALIVE_TIMEOUT,
|
|
645
|
+
headers: config?.headers ?? env.SERVER_HEADERS_TIMEOUT
|
|
482
646
|
};
|
|
483
647
|
}
|
|
484
|
-
function getShutdownTimeout(
|
|
485
|
-
return
|
|
648
|
+
function getShutdownTimeout(config) {
|
|
649
|
+
return config?.timeout ?? env.SHUTDOWN_TIMEOUT;
|
|
486
650
|
}
|
|
487
|
-
function buildMiddlewareOrder(
|
|
651
|
+
function buildMiddlewareOrder(config) {
|
|
488
652
|
const order = [];
|
|
489
|
-
const middlewareConfig =
|
|
653
|
+
const middlewareConfig = config.middleware ?? {};
|
|
490
654
|
const enableLogger = middlewareConfig.logger !== false;
|
|
491
655
|
const enableCors = middlewareConfig.cors !== false;
|
|
492
656
|
const enableErrorHandler = middlewareConfig.errorHandler !== false;
|
|
493
657
|
if (enableLogger) order.push("RequestLogger");
|
|
494
658
|
if (enableCors) order.push("CORS");
|
|
495
|
-
|
|
496
|
-
if (
|
|
659
|
+
config.use?.forEach((_, i) => order.push(`Custom[${i}]`));
|
|
660
|
+
if (config.beforeRoutes) order.push("beforeRoutes hook");
|
|
497
661
|
order.push("Routes");
|
|
498
|
-
if (
|
|
662
|
+
if (config.afterRoutes) order.push("afterRoutes hook");
|
|
499
663
|
if (enableErrorHandler) order.push("ErrorHandler");
|
|
500
664
|
return order;
|
|
501
665
|
}
|
|
502
|
-
function buildStartupConfig(
|
|
503
|
-
const middlewareConfig =
|
|
504
|
-
const healthCheckConfig =
|
|
666
|
+
function buildStartupConfig(config, timeouts) {
|
|
667
|
+
const middlewareConfig = config.middleware ?? {};
|
|
668
|
+
const healthCheckConfig = config.healthCheck ?? {};
|
|
505
669
|
const healthCheckEnabled = healthCheckConfig.enabled !== false;
|
|
506
670
|
const healthCheckPath = healthCheckConfig.path ?? "/health";
|
|
507
671
|
const healthCheckDetailed = healthCheckConfig.detailed ?? env.NODE_ENV === "development";
|
|
@@ -510,7 +674,7 @@ function buildStartupConfig(config2, timeouts) {
|
|
|
510
674
|
logger: middlewareConfig.logger !== false,
|
|
511
675
|
cors: middlewareConfig.cors !== false,
|
|
512
676
|
errorHandler: middlewareConfig.errorHandler !== false,
|
|
513
|
-
custom:
|
|
677
|
+
custom: config.use?.length ?? 0
|
|
514
678
|
},
|
|
515
679
|
healthCheck: healthCheckEnabled ? {
|
|
516
680
|
enabled: true,
|
|
@@ -518,8 +682,8 @@ function buildStartupConfig(config2, timeouts) {
|
|
|
518
682
|
detailed: healthCheckDetailed
|
|
519
683
|
} : { enabled: false },
|
|
520
684
|
hooks: {
|
|
521
|
-
beforeRoutes: !!
|
|
522
|
-
afterRoutes: !!
|
|
685
|
+
beforeRoutes: !!config.beforeRoutes,
|
|
686
|
+
afterRoutes: !!config.afterRoutes
|
|
523
687
|
},
|
|
524
688
|
timeout: {
|
|
525
689
|
request: `${timeouts.request}ms`,
|
|
@@ -527,23 +691,23 @@ function buildStartupConfig(config2, timeouts) {
|
|
|
527
691
|
headers: `${timeouts.headers}ms`
|
|
528
692
|
},
|
|
529
693
|
shutdown: {
|
|
530
|
-
timeout: `${
|
|
694
|
+
timeout: `${config.shutdown?.timeout ?? env.SHUTDOWN_TIMEOUT}ms`
|
|
531
695
|
}
|
|
532
696
|
};
|
|
533
697
|
}
|
|
534
698
|
var serverLogger = logger.child("@spfn/core:server");
|
|
535
699
|
|
|
536
700
|
// src/server/create-server.ts
|
|
537
|
-
async function createServer(
|
|
701
|
+
async function createServer(config) {
|
|
538
702
|
const cwd = process.cwd();
|
|
539
703
|
const appPath = join(cwd, "src", "server", "app.ts");
|
|
540
704
|
const appJsPath = join(cwd, "src", "server", "app");
|
|
541
705
|
if (existsSync(appPath) || existsSync(appJsPath)) {
|
|
542
|
-
return await loadCustomApp(appPath, appJsPath,
|
|
706
|
+
return await loadCustomApp(appPath, appJsPath, config);
|
|
543
707
|
}
|
|
544
|
-
return await createAutoConfiguredApp(
|
|
708
|
+
return await createAutoConfiguredApp(config);
|
|
545
709
|
}
|
|
546
|
-
async function loadCustomApp(appPath, appJsPath,
|
|
710
|
+
async function loadCustomApp(appPath, appJsPath, config) {
|
|
547
711
|
const actualPath = existsSync(appPath) ? appPath : appJsPath;
|
|
548
712
|
const appModule = await import(actualPath);
|
|
549
713
|
const appFactory = appModule.default;
|
|
@@ -551,15 +715,15 @@ async function loadCustomApp(appPath, appJsPath, config2) {
|
|
|
551
715
|
throw new Error("app.ts must export a default function that returns a Hono app");
|
|
552
716
|
}
|
|
553
717
|
const app = await appFactory();
|
|
554
|
-
if (
|
|
555
|
-
const routes = registerRoutes(app,
|
|
556
|
-
logRegisteredRoutes(routes,
|
|
718
|
+
if (config?.routes) {
|
|
719
|
+
const routes = registerRoutes(app, config.routes, config.middlewares);
|
|
720
|
+
logRegisteredRoutes(routes, config?.debug ?? false);
|
|
557
721
|
}
|
|
558
722
|
return app;
|
|
559
723
|
}
|
|
560
|
-
async function createAutoConfiguredApp(
|
|
724
|
+
async function createAutoConfiguredApp(config) {
|
|
561
725
|
const app = new Hono();
|
|
562
|
-
const middlewareConfig =
|
|
726
|
+
const middlewareConfig = config?.middleware ?? {};
|
|
563
727
|
const enableLogger = middlewareConfig.logger !== false;
|
|
564
728
|
const enableCors = middlewareConfig.cors !== false;
|
|
565
729
|
const enableErrorHandler = middlewareConfig.errorHandler !== false;
|
|
@@ -569,31 +733,31 @@ async function createAutoConfiguredApp(config2) {
|
|
|
569
733
|
await next();
|
|
570
734
|
});
|
|
571
735
|
}
|
|
572
|
-
applyDefaultMiddleware(app,
|
|
573
|
-
if (Array.isArray(
|
|
574
|
-
|
|
736
|
+
applyDefaultMiddleware(app, config, enableLogger, enableCors);
|
|
737
|
+
if (Array.isArray(config?.use)) {
|
|
738
|
+
config.use.forEach((mw) => app.use("*", mw));
|
|
575
739
|
}
|
|
576
|
-
registerHealthCheckEndpoint(app,
|
|
577
|
-
await executeBeforeRoutesHook(app,
|
|
578
|
-
await loadAppRoutes(app,
|
|
579
|
-
registerSSEEndpoint(app,
|
|
580
|
-
await executeAfterRoutesHook(app,
|
|
740
|
+
registerHealthCheckEndpoint(app, config);
|
|
741
|
+
await executeBeforeRoutesHook(app, config);
|
|
742
|
+
await loadAppRoutes(app, config);
|
|
743
|
+
registerSSEEndpoint(app, config);
|
|
744
|
+
await executeAfterRoutesHook(app, config);
|
|
581
745
|
if (enableErrorHandler) {
|
|
582
746
|
app.onError(ErrorHandler());
|
|
583
747
|
}
|
|
584
748
|
return app;
|
|
585
749
|
}
|
|
586
|
-
function applyDefaultMiddleware(app,
|
|
750
|
+
function applyDefaultMiddleware(app, config, enableLogger, enableCors) {
|
|
587
751
|
if (enableLogger) {
|
|
588
752
|
app.use("*", RequestLogger());
|
|
589
753
|
}
|
|
590
754
|
if (enableCors) {
|
|
591
|
-
const corsOptions =
|
|
755
|
+
const corsOptions = config?.cors !== false ? config?.cors : void 0;
|
|
592
756
|
app.use("*", cors(corsOptions));
|
|
593
757
|
}
|
|
594
758
|
}
|
|
595
|
-
function registerHealthCheckEndpoint(app,
|
|
596
|
-
const healthCheckConfig =
|
|
759
|
+
function registerHealthCheckEndpoint(app, config) {
|
|
760
|
+
const healthCheckConfig = config?.healthCheck ?? {};
|
|
597
761
|
const healthCheckEnabled = healthCheckConfig.enabled !== false;
|
|
598
762
|
const healthCheckPath = healthCheckConfig.path ?? "/health";
|
|
599
763
|
const healthCheckDetailed = healthCheckConfig.detailed ?? process.env.NODE_ENV === "development";
|
|
@@ -602,15 +766,15 @@ function registerHealthCheckEndpoint(app, config2) {
|
|
|
602
766
|
serverLogger.debug(`Health check endpoint enabled at ${healthCheckPath}`);
|
|
603
767
|
}
|
|
604
768
|
}
|
|
605
|
-
async function executeBeforeRoutesHook(app,
|
|
606
|
-
if (
|
|
607
|
-
await
|
|
769
|
+
async function executeBeforeRoutesHook(app, config) {
|
|
770
|
+
if (config?.lifecycle?.beforeRoutes) {
|
|
771
|
+
await config.lifecycle.beforeRoutes(app);
|
|
608
772
|
}
|
|
609
773
|
}
|
|
610
|
-
async function loadAppRoutes(app,
|
|
611
|
-
const debug = isDebugMode(
|
|
612
|
-
if (
|
|
613
|
-
const routes = registerRoutes(app,
|
|
774
|
+
async function loadAppRoutes(app, config) {
|
|
775
|
+
const debug = isDebugMode(config);
|
|
776
|
+
if (config?.routes) {
|
|
777
|
+
const routes = registerRoutes(app, config.routes, config.middlewares);
|
|
614
778
|
logRegisteredRoutes(routes, debug);
|
|
615
779
|
} else if (debug) {
|
|
616
780
|
serverLogger.warn("\u26A0\uFE0F No routes configured. Use defineServerConfig().routes() to register routes.");
|
|
@@ -631,28 +795,51 @@ function logRegisteredRoutes(routes, debug) {
|
|
|
631
795
|
serverLogger.info(`\u2713 Routes registered (${routes.length}):
|
|
632
796
|
${routeLines}`);
|
|
633
797
|
}
|
|
634
|
-
async function executeAfterRoutesHook(app,
|
|
635
|
-
if (
|
|
636
|
-
await
|
|
798
|
+
async function executeAfterRoutesHook(app, config) {
|
|
799
|
+
if (config?.lifecycle?.afterRoutes) {
|
|
800
|
+
await config.lifecycle.afterRoutes(app);
|
|
637
801
|
}
|
|
638
802
|
}
|
|
639
|
-
function registerSSEEndpoint(app,
|
|
640
|
-
if (!
|
|
803
|
+
function registerSSEEndpoint(app, config) {
|
|
804
|
+
if (!config?.events) {
|
|
641
805
|
return;
|
|
642
806
|
}
|
|
643
|
-
const eventsConfig =
|
|
644
|
-
const
|
|
645
|
-
const
|
|
646
|
-
|
|
807
|
+
const eventsConfig = config.eventsConfig ?? {};
|
|
808
|
+
const streamPath = eventsConfig.path ?? "/events/stream";
|
|
809
|
+
const authConfig = eventsConfig.auth;
|
|
810
|
+
const debug = isDebugMode(config);
|
|
811
|
+
let tokenManager;
|
|
812
|
+
if (authConfig?.enabled) {
|
|
813
|
+
tokenManager = new SSETokenManager({
|
|
814
|
+
ttl: authConfig.tokenTtl,
|
|
815
|
+
store: authConfig.store
|
|
816
|
+
});
|
|
817
|
+
const tokenPath = streamPath.replace(/\/[^/]+$/, "/token");
|
|
818
|
+
const mwHandlers = (config.middlewares ?? []).map((mw) => mw.handler);
|
|
819
|
+
const getSubject = authConfig.getSubject ?? ((c) => c.get("auth")?.userId ?? null);
|
|
820
|
+
app.post(tokenPath, ...mwHandlers, async (c) => {
|
|
821
|
+
const subject = getSubject(c);
|
|
822
|
+
if (!subject) {
|
|
823
|
+
return c.json({ error: "Unable to identify subject" }, 401);
|
|
824
|
+
}
|
|
825
|
+
const token = await tokenManager.issue(subject);
|
|
826
|
+
return c.json({ token });
|
|
827
|
+
});
|
|
828
|
+
if (debug) {
|
|
829
|
+
serverLogger.info(`\u2713 SSE token endpoint registered at POST ${tokenPath}`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
app.get(streamPath, createSSEHandler(config.events, eventsConfig, tokenManager));
|
|
647
833
|
if (debug) {
|
|
648
|
-
const eventNames =
|
|
649
|
-
serverLogger.info(`\u2713 SSE endpoint registered at ${
|
|
650
|
-
events: eventNames
|
|
834
|
+
const eventNames = config.events.eventNames;
|
|
835
|
+
serverLogger.info(`\u2713 SSE endpoint registered at ${streamPath}`, {
|
|
836
|
+
events: eventNames,
|
|
837
|
+
auth: !!authConfig?.enabled
|
|
651
838
|
});
|
|
652
839
|
}
|
|
653
840
|
}
|
|
654
|
-
function isDebugMode(
|
|
655
|
-
return
|
|
841
|
+
function isDebugMode(config) {
|
|
842
|
+
return config?.debug ?? process.env.NODE_ENV === "development";
|
|
656
843
|
}
|
|
657
844
|
var jobLogger = logger.child("@spfn/core:job");
|
|
658
845
|
var bossInstance = null;
|
|
@@ -888,16 +1075,16 @@ function printBanner(options) {
|
|
|
888
1075
|
}
|
|
889
1076
|
|
|
890
1077
|
// src/server/validation.ts
|
|
891
|
-
function validateServerConfig(
|
|
892
|
-
if (
|
|
893
|
-
if (!Number.isInteger(
|
|
1078
|
+
function validateServerConfig(config) {
|
|
1079
|
+
if (config.port !== void 0) {
|
|
1080
|
+
if (!Number.isInteger(config.port) || config.port < 0 || config.port > 65535) {
|
|
894
1081
|
throw new Error(
|
|
895
|
-
`Invalid port: ${
|
|
1082
|
+
`Invalid port: ${config.port}. Port must be an integer between 0 and 65535.`
|
|
896
1083
|
);
|
|
897
1084
|
}
|
|
898
1085
|
}
|
|
899
|
-
if (
|
|
900
|
-
const { request, keepAlive, headers } =
|
|
1086
|
+
if (config.timeout) {
|
|
1087
|
+
const { request, keepAlive, headers } = config.timeout;
|
|
901
1088
|
if (request !== void 0 && (request < 0 || !Number.isFinite(request))) {
|
|
902
1089
|
throw new Error(`Invalid timeout.request: ${request}. Must be a positive number.`);
|
|
903
1090
|
}
|
|
@@ -913,16 +1100,16 @@ function validateServerConfig(config2) {
|
|
|
913
1100
|
);
|
|
914
1101
|
}
|
|
915
1102
|
}
|
|
916
|
-
if (
|
|
917
|
-
const timeout =
|
|
1103
|
+
if (config.shutdown?.timeout !== void 0) {
|
|
1104
|
+
const timeout = config.shutdown.timeout;
|
|
918
1105
|
if (timeout < 0 || !Number.isFinite(timeout)) {
|
|
919
1106
|
throw new Error(`Invalid shutdown.timeout: ${timeout}. Must be a positive number.`);
|
|
920
1107
|
}
|
|
921
1108
|
}
|
|
922
|
-
if (
|
|
923
|
-
if (!
|
|
1109
|
+
if (config.healthCheck?.path) {
|
|
1110
|
+
if (!config.healthCheck.path.startsWith("/")) {
|
|
924
1111
|
throw new Error(
|
|
925
|
-
`Invalid healthCheck.path: "${
|
|
1112
|
+
`Invalid healthCheck.path: "${config.healthCheck.path}". Must start with "/".`
|
|
926
1113
|
);
|
|
927
1114
|
}
|
|
928
1115
|
}
|
|
@@ -939,9 +1126,9 @@ var CONFIG_FILE_PATHS = [
|
|
|
939
1126
|
"src/server/server.config.ts"
|
|
940
1127
|
];
|
|
941
1128
|
var processHandlersRegistered = false;
|
|
942
|
-
async function startServer(
|
|
943
|
-
|
|
944
|
-
const finalConfig = await loadAndMergeConfig(
|
|
1129
|
+
async function startServer(config) {
|
|
1130
|
+
loadEnv();
|
|
1131
|
+
const finalConfig = await loadAndMergeConfig(config);
|
|
945
1132
|
const { host, port, debug } = finalConfig;
|
|
946
1133
|
validateServerConfig(finalConfig);
|
|
947
1134
|
if (!host || !port) {
|
|
@@ -999,7 +1186,7 @@ async function startServer(config2) {
|
|
|
999
1186
|
throw error;
|
|
1000
1187
|
}
|
|
1001
1188
|
}
|
|
1002
|
-
async function loadAndMergeConfig(
|
|
1189
|
+
async function loadAndMergeConfig(config) {
|
|
1003
1190
|
const cwd = process.cwd();
|
|
1004
1191
|
let fileConfig = {};
|
|
1005
1192
|
let loadedConfigPath = null;
|
|
@@ -1023,26 +1210,26 @@ async function loadAndMergeConfig(config2) {
|
|
|
1023
1210
|
}
|
|
1024
1211
|
return {
|
|
1025
1212
|
...fileConfig,
|
|
1026
|
-
...
|
|
1027
|
-
port:
|
|
1028
|
-
host:
|
|
1213
|
+
...config,
|
|
1214
|
+
port: config?.port ?? fileConfig?.port ?? env.PORT,
|
|
1215
|
+
host: config?.host ?? fileConfig?.host ?? env.HOST
|
|
1029
1216
|
};
|
|
1030
1217
|
}
|
|
1031
|
-
function getInfrastructureConfig(
|
|
1218
|
+
function getInfrastructureConfig(config) {
|
|
1032
1219
|
return {
|
|
1033
|
-
database:
|
|
1034
|
-
redis:
|
|
1220
|
+
database: config.infrastructure?.database !== false,
|
|
1221
|
+
redis: config.infrastructure?.redis !== false
|
|
1035
1222
|
};
|
|
1036
1223
|
}
|
|
1037
|
-
async function initializeInfrastructure(
|
|
1038
|
-
if (
|
|
1224
|
+
async function initializeInfrastructure(config) {
|
|
1225
|
+
if (config.lifecycle?.beforeInfrastructure) {
|
|
1039
1226
|
serverLogger.debug("Executing beforeInfrastructure hook...");
|
|
1040
|
-
await
|
|
1227
|
+
await config.lifecycle.beforeInfrastructure(config);
|
|
1041
1228
|
}
|
|
1042
|
-
const infraConfig = getInfrastructureConfig(
|
|
1229
|
+
const infraConfig = getInfrastructureConfig(config);
|
|
1043
1230
|
if (infraConfig.database) {
|
|
1044
1231
|
serverLogger.debug("Initializing database...");
|
|
1045
|
-
await initDatabase(
|
|
1232
|
+
await initDatabase(config.database);
|
|
1046
1233
|
} else {
|
|
1047
1234
|
serverLogger.debug("Database initialization disabled");
|
|
1048
1235
|
}
|
|
@@ -1052,11 +1239,11 @@ async function initializeInfrastructure(config2) {
|
|
|
1052
1239
|
} else {
|
|
1053
1240
|
serverLogger.debug("Redis initialization disabled");
|
|
1054
1241
|
}
|
|
1055
|
-
if (
|
|
1242
|
+
if (config.lifecycle?.afterInfrastructure) {
|
|
1056
1243
|
serverLogger.debug("Executing afterInfrastructure hook...");
|
|
1057
|
-
await
|
|
1244
|
+
await config.lifecycle.afterInfrastructure();
|
|
1058
1245
|
}
|
|
1059
|
-
if (
|
|
1246
|
+
if (config.jobs) {
|
|
1060
1247
|
const dbUrl = env.DATABASE_URL;
|
|
1061
1248
|
if (!dbUrl) {
|
|
1062
1249
|
throw new Error(
|
|
@@ -1066,22 +1253,22 @@ async function initializeInfrastructure(config2) {
|
|
|
1066
1253
|
serverLogger.debug("Initializing pg-boss...");
|
|
1067
1254
|
await initBoss({
|
|
1068
1255
|
connectionString: dbUrl,
|
|
1069
|
-
...
|
|
1256
|
+
...config.jobsConfig
|
|
1070
1257
|
});
|
|
1071
1258
|
serverLogger.debug("Registering jobs...");
|
|
1072
|
-
await registerJobs(
|
|
1259
|
+
await registerJobs(config.jobs);
|
|
1073
1260
|
}
|
|
1074
|
-
if (
|
|
1075
|
-
const infraConfig2 = getInfrastructureConfig(
|
|
1261
|
+
if (config.workflows) {
|
|
1262
|
+
const infraConfig2 = getInfrastructureConfig(config);
|
|
1076
1263
|
if (!infraConfig2.database) {
|
|
1077
1264
|
throw new Error(
|
|
1078
1265
|
"Workflows require database connection. Ensure database is enabled in infrastructure config."
|
|
1079
1266
|
);
|
|
1080
1267
|
}
|
|
1081
1268
|
serverLogger.debug("Initializing workflow engine...");
|
|
1082
|
-
|
|
1269
|
+
config.workflows._init(
|
|
1083
1270
|
getDatabase(),
|
|
1084
|
-
|
|
1271
|
+
config.workflowsConfig
|
|
1085
1272
|
);
|
|
1086
1273
|
serverLogger.info("Workflow engine initialized");
|
|
1087
1274
|
}
|
|
@@ -1094,8 +1281,8 @@ function startHttpServer(app, host, port) {
|
|
|
1094
1281
|
hostname: host
|
|
1095
1282
|
});
|
|
1096
1283
|
}
|
|
1097
|
-
function logMiddlewareOrder(
|
|
1098
|
-
const middlewareOrder = buildMiddlewareOrder(
|
|
1284
|
+
function logMiddlewareOrder(config) {
|
|
1285
|
+
const middlewareOrder = buildMiddlewareOrder(config);
|
|
1099
1286
|
serverLogger.debug("Middleware execution order", {
|
|
1100
1287
|
order: middlewareOrder
|
|
1101
1288
|
});
|
|
@@ -1107,8 +1294,8 @@ function logServerTimeouts(timeouts) {
|
|
|
1107
1294
|
headers: `${timeouts.headers}ms`
|
|
1108
1295
|
});
|
|
1109
1296
|
}
|
|
1110
|
-
function logServerStarted(debug, host, port,
|
|
1111
|
-
const startupConfig = buildStartupConfig(
|
|
1297
|
+
function logServerStarted(debug, host, port, config, timeouts) {
|
|
1298
|
+
const startupConfig = buildStartupConfig(config, timeouts);
|
|
1112
1299
|
serverLogger.info("Server started successfully", {
|
|
1113
1300
|
mode: debug ? "development" : "production",
|
|
1114
1301
|
host,
|
|
@@ -1116,7 +1303,7 @@ function logServerStarted(debug, host, port, config2, timeouts) {
|
|
|
1116
1303
|
config: startupConfig
|
|
1117
1304
|
});
|
|
1118
1305
|
}
|
|
1119
|
-
function createShutdownHandler(server,
|
|
1306
|
+
function createShutdownHandler(server, config, shutdownState) {
|
|
1120
1307
|
return async () => {
|
|
1121
1308
|
if (shutdownState.isShuttingDown) {
|
|
1122
1309
|
serverLogger.debug("Shutdown already in progress for this instance, skipping");
|
|
@@ -1147,7 +1334,7 @@ function createShutdownHandler(server, config2, shutdownState) {
|
|
|
1147
1334
|
if (timeoutId) clearTimeout(timeoutId);
|
|
1148
1335
|
serverLogger.warn("HTTP server close timeout, forcing shutdown", error);
|
|
1149
1336
|
});
|
|
1150
|
-
if (
|
|
1337
|
+
if (config.jobs) {
|
|
1151
1338
|
serverLogger.debug("Stopping pg-boss...");
|
|
1152
1339
|
try {
|
|
1153
1340
|
await stopBoss();
|
|
@@ -1155,15 +1342,15 @@ function createShutdownHandler(server, config2, shutdownState) {
|
|
|
1155
1342
|
serverLogger.error("pg-boss stop failed", error);
|
|
1156
1343
|
}
|
|
1157
1344
|
}
|
|
1158
|
-
if (
|
|
1345
|
+
if (config.lifecycle?.beforeShutdown) {
|
|
1159
1346
|
serverLogger.debug("Executing beforeShutdown hook...");
|
|
1160
1347
|
try {
|
|
1161
|
-
await
|
|
1348
|
+
await config.lifecycle.beforeShutdown();
|
|
1162
1349
|
} catch (error) {
|
|
1163
1350
|
serverLogger.error("beforeShutdown hook failed", error);
|
|
1164
1351
|
}
|
|
1165
1352
|
}
|
|
1166
|
-
const infraConfig = getInfrastructureConfig(
|
|
1353
|
+
const infraConfig = getInfrastructureConfig(config);
|
|
1167
1354
|
if (infraConfig.database) {
|
|
1168
1355
|
serverLogger.debug("Closing database connections...");
|
|
1169
1356
|
await closeInfrastructure(closeDatabase, "Database", TIMEOUTS.DATABASE_CLOSE);
|
|
@@ -1194,14 +1381,14 @@ async function closeInfrastructure(closeFn, name, timeout) {
|
|
|
1194
1381
|
serverLogger.error(`${name} close failed or timed out`, error);
|
|
1195
1382
|
}
|
|
1196
1383
|
}
|
|
1197
|
-
function createGracefulShutdown(shutdownServer,
|
|
1384
|
+
function createGracefulShutdown(shutdownServer, config, shutdownState) {
|
|
1198
1385
|
return async (signal) => {
|
|
1199
1386
|
if (shutdownState.isShuttingDown) {
|
|
1200
1387
|
serverLogger.warn(`${signal} received but shutdown already in progress, ignoring`);
|
|
1201
1388
|
return;
|
|
1202
1389
|
}
|
|
1203
1390
|
serverLogger.info(`${signal} received, starting graceful shutdown...`);
|
|
1204
|
-
const shutdownTimeout = getShutdownTimeout(
|
|
1391
|
+
const shutdownTimeout = getShutdownTimeout(config.shutdown);
|
|
1205
1392
|
let timeoutId;
|
|
1206
1393
|
try {
|
|
1207
1394
|
await Promise.race([
|
|
@@ -1287,10 +1474,10 @@ function registerProcessHandlers(shutdown) {
|
|
|
1287
1474
|
});
|
|
1288
1475
|
serverLogger.debug("Process-level shutdown handlers registered successfully");
|
|
1289
1476
|
}
|
|
1290
|
-
async function cleanupOnFailure(
|
|
1477
|
+
async function cleanupOnFailure(config) {
|
|
1291
1478
|
try {
|
|
1292
1479
|
serverLogger.debug("Cleaning up after initialization failure...");
|
|
1293
|
-
const infraConfig = getInfrastructureConfig(
|
|
1480
|
+
const infraConfig = getInfrastructureConfig(config);
|
|
1294
1481
|
if (infraConfig.database) {
|
|
1295
1482
|
await closeInfrastructure(closeDatabase, "Database", TIMEOUTS.DATABASE_CLOSE);
|
|
1296
1483
|
}
|
|
@@ -1422,10 +1609,10 @@ var ServerConfigBuilder = class {
|
|
|
1422
1609
|
* .build();
|
|
1423
1610
|
* ```
|
|
1424
1611
|
*/
|
|
1425
|
-
jobs(router,
|
|
1612
|
+
jobs(router, config) {
|
|
1426
1613
|
this.config.jobs = router;
|
|
1427
|
-
if (
|
|
1428
|
-
this.config.jobsConfig =
|
|
1614
|
+
if (config) {
|
|
1615
|
+
this.config.jobsConfig = config;
|
|
1429
1616
|
}
|
|
1430
1617
|
return this;
|
|
1431
1618
|
}
|
|
@@ -1457,10 +1644,10 @@ var ServerConfigBuilder = class {
|
|
|
1457
1644
|
* .events(eventRouter, { path: '/sse' })
|
|
1458
1645
|
* ```
|
|
1459
1646
|
*/
|
|
1460
|
-
events(router,
|
|
1647
|
+
events(router, config) {
|
|
1461
1648
|
this.config.events = router;
|
|
1462
|
-
if (
|
|
1463
|
-
this.config.eventsConfig =
|
|
1649
|
+
if (config) {
|
|
1650
|
+
this.config.eventsConfig = config;
|
|
1464
1651
|
}
|
|
1465
1652
|
return this;
|
|
1466
1653
|
}
|
|
@@ -1526,10 +1713,10 @@ var ServerConfigBuilder = class {
|
|
|
1526
1713
|
* .build();
|
|
1527
1714
|
* ```
|
|
1528
1715
|
*/
|
|
1529
|
-
workflows(router,
|
|
1716
|
+
workflows(router, config) {
|
|
1530
1717
|
this.config.workflows = router;
|
|
1531
|
-
if (
|
|
1532
|
-
this.config.workflowsConfig =
|
|
1718
|
+
if (config) {
|
|
1719
|
+
this.config.workflowsConfig = config;
|
|
1533
1720
|
}
|
|
1534
1721
|
return this;
|
|
1535
1722
|
}
|
|
@@ -1580,6 +1767,6 @@ function defineServerConfig() {
|
|
|
1580
1767
|
return new ServerConfigBuilder();
|
|
1581
1768
|
}
|
|
1582
1769
|
|
|
1583
|
-
export { createServer, defineServerConfig, loadEnvFiles, startServer };
|
|
1770
|
+
export { createServer, defineServerConfig, loadEnv, loadEnvFiles, startServer };
|
|
1584
1771
|
//# sourceMappingURL=index.js.map
|
|
1585
1772
|
//# sourceMappingURL=index.js.map
|