@spfn/core 0.3.0-beta.2 → 0.3.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -6
- package/dist/app-config/index.d.ts +96 -0
- package/dist/app-config/index.js +58 -0
- package/dist/app-config/index.js.map +1 -0
- package/dist/config/index.d.ts +0 -63
- package/dist/config/index.js +11 -11
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +4 -4
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/{define-middleware-DfDP39Nq.d.ts → define-middleware-CVKgqo8S.d.ts} +1 -1
- package/dist/middleware/index.d.ts +6 -2
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/ops/index.d.ts +3 -3
- package/dist/ops/index.js +1 -1
- package/dist/ops/index.js.map +1 -1
- package/dist/route/index.d.ts +3 -3
- package/dist/route/index.js +1 -1
- package/dist/route/index.js.map +1 -1
- package/dist/{router-Qbssr11H.d.ts → router-Cy7rAfmj.d.ts} +7 -7
- package/dist/server/index.d.ts +56 -8
- package/dist/server/index.js +173 -30
- package/dist/server/index.js.map +1 -1
- package/dist/{types-D1c57Ko-.d.ts → types-CfYVhIQ9.d.ts} +1 -1
- package/package.json +6 -1
package/dist/server/index.js
CHANGED
|
@@ -11,11 +11,12 @@ import { setDefaultSafeFetchPolicy } from '@spfn/core/security';
|
|
|
11
11
|
import { streamSSE } from 'hono/streaming';
|
|
12
12
|
import { randomBytes, createHash } from 'crypto';
|
|
13
13
|
import { Agent, setGlobalDispatcher } from 'undici';
|
|
14
|
-
import { initDatabase, getDatabase, hasMigrationTargets, collectMigrationStatus, countPendingMigrations, pendingMigrationTargets, formatPendingMigrations, pendingMigrationsSummary, RUN_MIGRATIONS_HINT,
|
|
14
|
+
import { initDatabase, getDatabase, hasMigrationTargets, collectMigrationStatus, countPendingMigrations, pendingMigrationTargets, formatPendingMigrations, pendingMigrationsSummary, RUN_MIGRATIONS_HINT, closeDatabase, migrationTargets } from '@spfn/core/db';
|
|
15
15
|
import { initCache, getCache, closeCache } from '@spfn/core/cache';
|
|
16
16
|
import { serve } from '@hono/node-server';
|
|
17
17
|
import PgBoss from 'pg-boss';
|
|
18
18
|
import { networkInterfaces } from 'os';
|
|
19
|
+
import { pathToFileURL } from 'url';
|
|
19
20
|
|
|
20
21
|
var __defProp = Object.defineProperty;
|
|
21
22
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -1076,6 +1077,11 @@ async function runMigrationBootGate(config, cwd = process.cwd()) {
|
|
|
1076
1077
|
);
|
|
1077
1078
|
}
|
|
1078
1079
|
|
|
1080
|
+
// src/server/namespace.ts
|
|
1081
|
+
var CORE_NAMESPACE = "/_core";
|
|
1082
|
+
var CORE_HEALTH_PATH = `${CORE_NAMESPACE}/health`;
|
|
1083
|
+
var LEGACY_HEALTH_PATH = "/health";
|
|
1084
|
+
|
|
1079
1085
|
// src/server/shutdown-manager.ts
|
|
1080
1086
|
var DEFAULT_HOOK_TIMEOUT = 1e4;
|
|
1081
1087
|
var DEFAULT_HOOK_ORDER = 100;
|
|
@@ -1308,7 +1314,7 @@ async function readMigrationHealth() {
|
|
|
1308
1314
|
targets: migrationTargets(snapshot.status)
|
|
1309
1315
|
};
|
|
1310
1316
|
}
|
|
1311
|
-
function createHealthCheckHandler(detailed) {
|
|
1317
|
+
function createHealthCheckHandler(detailed, infrastructure) {
|
|
1312
1318
|
return async (c) => {
|
|
1313
1319
|
const shutdownManager = getShutdownManager();
|
|
1314
1320
|
if (shutdownManager.isShuttingDown()) {
|
|
@@ -1322,23 +1328,29 @@ function createHealthCheckHandler(detailed) {
|
|
|
1322
1328
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1323
1329
|
};
|
|
1324
1330
|
if (detailed) {
|
|
1331
|
+
const databaseDisabled = infrastructure?.database === false;
|
|
1332
|
+
const redisDisabled = infrastructure?.redis === false;
|
|
1325
1333
|
let dbStatus = "unknown";
|
|
1326
1334
|
let dbError;
|
|
1327
|
-
|
|
1328
|
-
|
|
1335
|
+
if (databaseDisabled) {
|
|
1336
|
+
dbStatus = "disabled";
|
|
1337
|
+
} else {
|
|
1329
1338
|
try {
|
|
1330
|
-
|
|
1331
|
-
|
|
1339
|
+
const db = getDatabase();
|
|
1340
|
+
try {
|
|
1341
|
+
await db.execute("SELECT 1");
|
|
1342
|
+
dbStatus = "connected";
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
dbStatus = "error";
|
|
1345
|
+
dbError = error instanceof Error ? error.message : String(error);
|
|
1346
|
+
}
|
|
1332
1347
|
} catch (error) {
|
|
1333
|
-
dbStatus = "
|
|
1334
|
-
dbError =
|
|
1348
|
+
dbStatus = "not_initialized";
|
|
1349
|
+
dbError = "Database not available";
|
|
1335
1350
|
}
|
|
1336
|
-
} catch (error) {
|
|
1337
|
-
dbStatus = "not_initialized";
|
|
1338
|
-
dbError = "Database not available";
|
|
1339
1351
|
}
|
|
1340
|
-
const redis = getCache();
|
|
1341
|
-
let redisStatus = redis ? "unknown" : "not_initialized";
|
|
1352
|
+
const redis = redisDisabled ? null : getCache();
|
|
1353
|
+
let redisStatus = redisDisabled ? "disabled" : redis ? "unknown" : "not_initialized";
|
|
1342
1354
|
let redisError;
|
|
1343
1355
|
if (redis) {
|
|
1344
1356
|
try {
|
|
@@ -1438,7 +1450,7 @@ function buildStartupConfig(config, timeouts) {
|
|
|
1438
1450
|
const middlewareConfig = config.middleware ?? {};
|
|
1439
1451
|
const healthCheckConfig = config.healthCheck ?? {};
|
|
1440
1452
|
const healthCheckEnabled = healthCheckConfig.enabled !== false;
|
|
1441
|
-
const healthCheckPath = healthCheckConfig.path
|
|
1453
|
+
const healthCheckPath = healthCheckConfig.path;
|
|
1442
1454
|
const healthCheckDetailed = healthCheckConfig.detailed ?? env.NODE_ENV === "development";
|
|
1443
1455
|
return {
|
|
1444
1456
|
middleware: {
|
|
@@ -1449,6 +1461,7 @@ function buildStartupConfig(config, timeouts) {
|
|
|
1449
1461
|
},
|
|
1450
1462
|
healthCheck: healthCheckEnabled ? {
|
|
1451
1463
|
enabled: true,
|
|
1464
|
+
corePath: CORE_HEALTH_PATH,
|
|
1452
1465
|
path: healthCheckPath,
|
|
1453
1466
|
detailed: healthCheckDetailed
|
|
1454
1467
|
} : { enabled: false },
|
|
@@ -1512,9 +1525,12 @@ async function createAutoConfiguredApp(config) {
|
|
|
1512
1525
|
if (Array.isArray(config?.use)) {
|
|
1513
1526
|
config.use.forEach((mw) => app.use("*", mw));
|
|
1514
1527
|
}
|
|
1515
|
-
|
|
1528
|
+
registerCoreHealthEndpoint(app, config);
|
|
1516
1529
|
await executeBeforeRoutesHook(app, config);
|
|
1517
|
-
await loadAppRoutes(app, config);
|
|
1530
|
+
const appRoutes = await loadAppRoutes(app, config);
|
|
1531
|
+
registerMovedHealthSignpost(app, config, appRoutes);
|
|
1532
|
+
warnOnShadowedOptInPath(config, appRoutes);
|
|
1533
|
+
warnOnCoreNamespaceRoutes(appRoutes);
|
|
1518
1534
|
await registerSSEEndpoint(app, config);
|
|
1519
1535
|
await executeAfterRoutesHook(app, config);
|
|
1520
1536
|
if (enableErrorHandler) {
|
|
@@ -1555,7 +1571,10 @@ async function applyProxyGuard(app, config) {
|
|
|
1555
1571
|
serverLogger.warn("Proxy-guard nonce: cache module unavailable \u2014 using in-memory store (single instance only)");
|
|
1556
1572
|
}
|
|
1557
1573
|
}
|
|
1558
|
-
const autoSkip = [
|
|
1574
|
+
const autoSkip = [CORE_HEALTH_PATH, LEGACY_HEALTH_PATH];
|
|
1575
|
+
if (config?.healthCheck?.path) {
|
|
1576
|
+
autoSkip.push(config.healthCheck.path);
|
|
1577
|
+
}
|
|
1559
1578
|
if (config?.events) {
|
|
1560
1579
|
autoSkip.push(config.eventsConfig?.path ?? "/events/stream");
|
|
1561
1580
|
}
|
|
@@ -1598,15 +1617,75 @@ function applyOutboundFetch(config) {
|
|
|
1598
1617
|
const policy = config?.outboundFetch ?? { blockPrivateIps: env.SAFE_FETCH_BLOCK_PRIVATE_IPS };
|
|
1599
1618
|
setDefaultSafeFetchPolicy(policy);
|
|
1600
1619
|
}
|
|
1601
|
-
function
|
|
1620
|
+
function resolveHealthCheck(config) {
|
|
1602
1621
|
const healthCheckConfig = config?.healthCheck ?? {};
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1622
|
+
return {
|
|
1623
|
+
enabled: healthCheckConfig.enabled !== false,
|
|
1624
|
+
// No default. An unset `path` means the endpoint answers at
|
|
1625
|
+
// CORE_HEALTH_PATH and nowhere else — a default here is what used to put
|
|
1626
|
+
// it on /health without anyone asking.
|
|
1627
|
+
path: healthCheckConfig.path,
|
|
1628
|
+
detailed: healthCheckConfig.detailed ?? process.env.NODE_ENV === "development"
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
function registerCoreHealthEndpoint(app, config) {
|
|
1632
|
+
const { enabled, path, detailed } = resolveHealthCheck(config);
|
|
1633
|
+
if (!enabled) {
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
const handler = createHealthCheckHandler(detailed, config?.infrastructure);
|
|
1637
|
+
app.get(CORE_HEALTH_PATH, handler);
|
|
1638
|
+
serverLogger.debug(`Health check endpoint enabled at ${CORE_HEALTH_PATH}`);
|
|
1639
|
+
if (path && path !== CORE_HEALTH_PATH) {
|
|
1640
|
+
app.get(path, handler);
|
|
1641
|
+
serverLogger.debug(`Health check endpoint also answering at ${path}, as configured`);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
function registerMovedHealthSignpost(app, config, appRoutes) {
|
|
1645
|
+
const { enabled, path } = resolveHealthCheck(config);
|
|
1646
|
+
if (!enabled || path === LEGACY_HEALTH_PATH) {
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
if (appRoutes.some((r) => r.method === "GET" && r.path === LEGACY_HEALTH_PATH)) {
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
let warned2 = false;
|
|
1653
|
+
app.get(LEGACY_HEALTH_PATH, (c) => {
|
|
1654
|
+
if (!warned2) {
|
|
1655
|
+
warned2 = true;
|
|
1656
|
+
serverLogger.warn(
|
|
1657
|
+
`\u26A0\uFE0F GET ${LEGACY_HEALTH_PATH} is answering 410: @spfn/core no longer serves it. Point your readiness probe, Dockerfile HEALTHCHECK and load balancer at ${CORE_HEALTH_PATH}, or restore this path with healthCheck({ path: '${LEGACY_HEALTH_PATH}' }). This notice is removed in the next release.`
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
return c.json({
|
|
1661
|
+
error: "health endpoint moved",
|
|
1662
|
+
movedTo: CORE_HEALTH_PATH,
|
|
1663
|
+
detail: `@spfn/core no longer serves ${LEGACY_HEALTH_PATH}. Point your readiness probe, Dockerfile HEALTHCHECK and load balancer at ${CORE_HEALTH_PATH}, or restore this path with healthCheck({ path: '${LEGACY_HEALTH_PATH}' }).`
|
|
1664
|
+
}, 410);
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
function warnOnShadowedOptInPath(config, appRoutes) {
|
|
1668
|
+
const { enabled, path } = resolveHealthCheck(config);
|
|
1669
|
+
if (!enabled || !path || path === CORE_HEALTH_PATH) {
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
const shadowed = appRoutes.filter((r) => r.method === "GET" && r.path === path);
|
|
1673
|
+
if (shadowed.length === 0) {
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
serverLogger.warn(
|
|
1677
|
+
`\u26A0\uFE0F ${shadowed.map((r) => r.name).join(", ")} never runs: GET ${path} is served by the built-in health endpoint, which healthCheck({ path }) asked for and which is registered before app routes. Drop that option, or move the route to a path your app owns.`
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
function warnOnCoreNamespaceRoutes(appRoutes) {
|
|
1681
|
+
const inside = appRoutes.filter((r) => r.path === CORE_NAMESPACE || r.path.startsWith(`${CORE_NAMESPACE}/`));
|
|
1682
|
+
if (inside.length === 0) {
|
|
1683
|
+
return;
|
|
1609
1684
|
}
|
|
1685
|
+
const names = inside.map((r) => `${r.name} (${r.method} ${r.path})`).join(", ");
|
|
1686
|
+
serverLogger.warn(
|
|
1687
|
+
`\u26A0\uFE0F ${names} never runs: ${CORE_NAMESPACE}/ belongs to @spfn/core and its endpoints are registered before app routes. Move the route to a path your app owns.`
|
|
1688
|
+
);
|
|
1610
1689
|
}
|
|
1611
1690
|
async function executeBeforeRoutesHook(app, config) {
|
|
1612
1691
|
if (config?.lifecycle?.beforeRoutes) {
|
|
@@ -1618,9 +1697,12 @@ async function loadAppRoutes(app, config) {
|
|
|
1618
1697
|
if (config?.routes) {
|
|
1619
1698
|
const routes = registerRoutes(app, config.routes, config.middlewares);
|
|
1620
1699
|
logRegisteredRoutes(routes, debug);
|
|
1621
|
-
|
|
1700
|
+
return routes;
|
|
1701
|
+
}
|
|
1702
|
+
if (debug) {
|
|
1622
1703
|
serverLogger.warn("\u26A0\uFE0F No routes configured. Use defineServerConfig().routes() to register routes.");
|
|
1623
1704
|
}
|
|
1705
|
+
return [];
|
|
1624
1706
|
}
|
|
1625
1707
|
function logRegisteredRoutes(routes, debug) {
|
|
1626
1708
|
if (routes.length === 0) {
|
|
@@ -2285,6 +2367,45 @@ function printBanner(options) {
|
|
|
2285
2367
|
}
|
|
2286
2368
|
console.log("");
|
|
2287
2369
|
}
|
|
2370
|
+
var PORT_DEFAULTS = {
|
|
2371
|
+
server: 8790
|
|
2372
|
+
};
|
|
2373
|
+
var HOST_DEFAULT = "localhost";
|
|
2374
|
+
var CONFIG_FILE_NAMES = [
|
|
2375
|
+
"spfn.config.js",
|
|
2376
|
+
"spfn.config.mjs"
|
|
2377
|
+
];
|
|
2378
|
+
async function loadAppConfig(cwd = process.cwd()) {
|
|
2379
|
+
for (const fileName of CONFIG_FILE_NAMES) {
|
|
2380
|
+
const fullPath = join(cwd, fileName);
|
|
2381
|
+
if (!existsSync(fullPath)) {
|
|
2382
|
+
continue;
|
|
2383
|
+
}
|
|
2384
|
+
try {
|
|
2385
|
+
const module = await import(pathToFileURL(fullPath).href);
|
|
2386
|
+
return module.default ?? {};
|
|
2387
|
+
} catch (error) {
|
|
2388
|
+
console.warn(
|
|
2389
|
+
`\u26A0\uFE0F ${fileName} could not be imported \u2014 falling back to defaults for ports and host. ${error instanceof Error ? error.message : String(error)}`
|
|
2390
|
+
);
|
|
2391
|
+
return {};
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
return {};
|
|
2395
|
+
}
|
|
2396
|
+
function resolveServerAddress(config, deprecated = {}, env6 = process.env) {
|
|
2397
|
+
return {
|
|
2398
|
+
port: readPort(env6.SPFN_PORT) ?? config.ports?.server ?? deprecated.port ?? PORT_DEFAULTS.server,
|
|
2399
|
+
host: env6.SPFN_HOST || config.host || deprecated.host || HOST_DEFAULT
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
function readPort(value) {
|
|
2403
|
+
if (!value) {
|
|
2404
|
+
return void 0;
|
|
2405
|
+
}
|
|
2406
|
+
const port = Number(value);
|
|
2407
|
+
return Number.isInteger(port) && port > 0 && port < 65536 ? port : void 0;
|
|
2408
|
+
}
|
|
2288
2409
|
|
|
2289
2410
|
// src/server/validation.ts
|
|
2290
2411
|
function validateServerConfig(config) {
|
|
@@ -2333,10 +2454,12 @@ var TIMEOUTS = {
|
|
|
2333
2454
|
REDIS_CLOSE: 5e3};
|
|
2334
2455
|
var CONFIG_FILE_PATHS = [
|
|
2335
2456
|
".spfn/server/server.config.mjs",
|
|
2457
|
+
".spfn/server/server.config.js",
|
|
2336
2458
|
".spfn/server/server.config",
|
|
2337
2459
|
"src/server/server.config",
|
|
2338
2460
|
"src/server/server.config.ts"
|
|
2339
2461
|
];
|
|
2462
|
+
var AUTHORED_CONFIG_PATH = "src/server/server.config.ts";
|
|
2340
2463
|
var processHandlersRegistered = false;
|
|
2341
2464
|
async function startServer(config) {
|
|
2342
2465
|
loadEnv();
|
|
@@ -2400,8 +2523,7 @@ async function startServer(config) {
|
|
|
2400
2523
|
throw error;
|
|
2401
2524
|
}
|
|
2402
2525
|
}
|
|
2403
|
-
async function loadAndMergeConfig(config) {
|
|
2404
|
-
const cwd = process.cwd();
|
|
2526
|
+
async function loadAndMergeConfig(config, cwd = process.cwd()) {
|
|
2405
2527
|
let fileConfig = {};
|
|
2406
2528
|
let loadedConfigPath = null;
|
|
2407
2529
|
for (const configPath of CONFIG_FILE_PATHS) {
|
|
@@ -2419,16 +2541,37 @@ async function loadAndMergeConfig(config) {
|
|
|
2419
2541
|
}
|
|
2420
2542
|
if (loadedConfigPath) {
|
|
2421
2543
|
serverLogger.debug(`Loaded configuration from ${loadedConfigPath}`);
|
|
2544
|
+
} else if (existsSync(join(cwd, AUTHORED_CONFIG_PATH))) {
|
|
2545
|
+
serverLogger.warn(
|
|
2546
|
+
`\u26A0\uFE0F ${AUTHORED_CONFIG_PATH} exists but no configuration was loaded, so this server is running on defaults: no middlewares, no routes and no infrastructure settings from it. Run "spfn build" so the compiled config lands in .spfn/server/.`
|
|
2547
|
+
);
|
|
2422
2548
|
} else {
|
|
2423
2549
|
serverLogger.debug("No configuration file found, using defaults");
|
|
2424
2550
|
}
|
|
2551
|
+
const appConfig = await loadAppConfig(cwd);
|
|
2552
|
+
warnOnDeprecatedAddress(config, fileConfig);
|
|
2553
|
+
const address = resolveServerAddress(appConfig, {
|
|
2554
|
+
port: config?.port ?? fileConfig?.port,
|
|
2555
|
+
host: config?.host ?? fileConfig?.host
|
|
2556
|
+
});
|
|
2425
2557
|
return {
|
|
2426
2558
|
...fileConfig,
|
|
2427
2559
|
...config,
|
|
2428
|
-
port:
|
|
2429
|
-
host:
|
|
2560
|
+
port: address.port,
|
|
2561
|
+
host: address.host
|
|
2430
2562
|
};
|
|
2431
2563
|
}
|
|
2564
|
+
function warnOnDeprecatedAddress(config, fileConfig) {
|
|
2565
|
+
const used = [
|
|
2566
|
+
(config?.port ?? fileConfig?.port) !== void 0 ? ".port()" : null,
|
|
2567
|
+
(config?.host ?? fileConfig?.host) !== void 0 ? ".host()" : null
|
|
2568
|
+
].filter(Boolean);
|
|
2569
|
+
if (used.length > 0) {
|
|
2570
|
+
serverLogger.warn(
|
|
2571
|
+
`\u26A0\uFE0F ${used.join(" and ")} in server.config is deprecated and will be removed. Move the address to spfn.config.js \u2014 \`ports: { next, server }\` and \`host\` \u2014 so the Dockerfile, the compose file and next.config can read the same value.`
|
|
2572
|
+
);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2432
2575
|
function getInfrastructureConfig(config) {
|
|
2433
2576
|
return {
|
|
2434
2577
|
database: config.infrastructure?.database !== false,
|
|
@@ -3184,6 +3327,6 @@ function defineServerConfig() {
|
|
|
3184
3327
|
return new ServerConfigBuilder();
|
|
3185
3328
|
}
|
|
3186
3329
|
|
|
3187
|
-
export { PendingMigrationsError, createServer, createServerlessApp, defineServerConfig, getMigrationSnapshot, getShutdownManager, loadEnv, loadEnvFiles, provisionInfrastructure, resetMigrationSnapshot, resetServerlessApp, startServer };
|
|
3330
|
+
export { CORE_HEALTH_PATH, CORE_NAMESPACE, PendingMigrationsError, createServer, createServerlessApp, defineServerConfig, getMigrationSnapshot, getShutdownManager, loadEnv, loadEnvFiles, provisionInfrastructure, resetMigrationSnapshot, resetServerlessApp, startServer };
|
|
3188
3331
|
//# sourceMappingURL=index.js.map
|
|
3189
3332
|
//# sourceMappingURL=index.js.map
|