@spfn/core 0.2.0-beta.70 → 0.2.0-beta.72
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 +76 -2
- package/dist/{boss-gXhgctn6.d.ts → boss-D16fO2oG.d.ts} +1 -1
- package/dist/config/index.d.ts +30 -0
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +2 -2
- package/dist/db/index.d.ts +133 -2
- package/dist/db/index.js +198 -4
- package/dist/db/index.js.map +1 -1
- package/dist/{define-middleware-B9bFuXVU.d.ts → define-middleware-DfDP39Nq.d.ts} +1 -1
- package/dist/event/index.d.ts +3 -3
- package/dist/event/sse/client.d.ts +2 -2
- package/dist/event/sse/index.d.ts +4 -4
- package/dist/event/ws/client.d.ts +2 -2
- package/dist/event/ws/index.d.ts +3 -3
- package/dist/job/index.d.ts +2 -2
- package/dist/middleware/index.d.ts +1 -1
- package/dist/nextjs/index.d.ts +2 -2
- package/dist/nextjs/server.d.ts +1 -1
- package/dist/route/index.d.ts +3 -3
- package/dist/route/index.js +16 -8
- package/dist/route/index.js.map +1 -1
- package/dist/{router-HDVa6gfc.d.ts → router-BgZUDHp5.d.ts} +2 -2
- package/dist/server/index.d.ts +110 -11
- package/dist/server/index.js +168 -36
- package/dist/server/index.js.map +1 -1
- package/dist/{token-manager-vZeqBbtA.d.ts → token-manager-BT5EnUAR.d.ts} +1 -1
- package/dist/{types-CF-37KAG.d.ts → types-2AbaW4Ie.d.ts} +2 -2
- package/dist/{types-7Mhoxnnt.d.ts → types-9oszaJqp.d.ts} +1 -1
- package/dist/{types-D9uMxeQS.d.ts → types-ZQODsBft.d.ts} +2 -2
- package/package.json +5 -5
package/dist/server/index.js
CHANGED
|
@@ -11,7 +11,7 @@ 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, closeDatabase } from '@spfn/core/db';
|
|
14
|
+
import { initDatabase, getDatabase, hasMigrationTargets, collectMigrationStatus, countPendingMigrations, pendingMigrationTargets, formatPendingMigrations, pendingMigrationsSummary, RUN_MIGRATIONS_HINT, migrationTargets, closeDatabase } 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';
|
|
@@ -646,8 +646,8 @@ var InMemoryTokenStore = class {
|
|
|
646
646
|
}
|
|
647
647
|
};
|
|
648
648
|
var CacheTokenStore = class {
|
|
649
|
-
constructor(
|
|
650
|
-
this.cache =
|
|
649
|
+
constructor(cache2) {
|
|
650
|
+
this.cache = cache2;
|
|
651
651
|
}
|
|
652
652
|
prefix = "sse:token:";
|
|
653
653
|
async set(token, data) {
|
|
@@ -989,6 +989,93 @@ async function closeEventTransport() {
|
|
|
989
989
|
}
|
|
990
990
|
var serverLogger = logger.child("@spfn/core:server");
|
|
991
991
|
|
|
992
|
+
// src/server/migration-gate.ts
|
|
993
|
+
var SNAPSHOT_TTL_MS = 3e4;
|
|
994
|
+
var cache = null;
|
|
995
|
+
var PendingMigrationsError = class extends Error {
|
|
996
|
+
targets;
|
|
997
|
+
constructor(message, targets) {
|
|
998
|
+
super(message);
|
|
999
|
+
this.name = "PendingMigrationsError";
|
|
1000
|
+
this.targets = targets;
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
function resetMigrationSnapshot() {
|
|
1004
|
+
cache = null;
|
|
1005
|
+
}
|
|
1006
|
+
async function inspect(cwd, db) {
|
|
1007
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1008
|
+
if (!hasMigrationTargets(cwd)) {
|
|
1009
|
+
return { state: "skipped", checkedAt, reason: "no function package or project migrations found" };
|
|
1010
|
+
}
|
|
1011
|
+
let database = db;
|
|
1012
|
+
if (!database) {
|
|
1013
|
+
try {
|
|
1014
|
+
database = getDatabase();
|
|
1015
|
+
} catch {
|
|
1016
|
+
return { state: "skipped", checkedAt, reason: "no database initialized" };
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
try {
|
|
1020
|
+
const status = await collectMigrationStatus(database, cwd);
|
|
1021
|
+
return { state: "ok", checkedAt, status, pending: countPendingMigrations(status) };
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
return {
|
|
1024
|
+
state: "unavailable",
|
|
1025
|
+
checkedAt,
|
|
1026
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
async function getMigrationSnapshot(options = {}) {
|
|
1031
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1032
|
+
const ttl = options.ttlMs ?? SNAPSHOT_TTL_MS;
|
|
1033
|
+
const now = Date.now();
|
|
1034
|
+
if (!options.force && cache && cache.cwd === cwd && now - cache.at < ttl) {
|
|
1035
|
+
return cache.value;
|
|
1036
|
+
}
|
|
1037
|
+
const value = await inspect(cwd, options.db);
|
|
1038
|
+
cache = { at: now, cwd, value };
|
|
1039
|
+
return value;
|
|
1040
|
+
}
|
|
1041
|
+
function pendingIsAllowed(config) {
|
|
1042
|
+
return config?.migrations?.allowPending ?? env.SPFN_ALLOW_PENDING_MIGRATIONS === true;
|
|
1043
|
+
}
|
|
1044
|
+
async function runMigrationBootGate(config, cwd = process.cwd()) {
|
|
1045
|
+
const snapshot = await getMigrationSnapshot({ cwd, force: true });
|
|
1046
|
+
if (snapshot.state === "skipped") {
|
|
1047
|
+
serverLogger.debug(`Migration gate skipped: ${snapshot.reason}`);
|
|
1048
|
+
return snapshot;
|
|
1049
|
+
}
|
|
1050
|
+
if (snapshot.state === "unavailable") {
|
|
1051
|
+
serverLogger.warn(
|
|
1052
|
+
`Could not verify migration status: ${snapshot.reason}. Starting anyway \u2014 run \`pnpm spfn db status\` to check.`
|
|
1053
|
+
);
|
|
1054
|
+
return snapshot;
|
|
1055
|
+
}
|
|
1056
|
+
const targets = pendingMigrationTargets(snapshot.status);
|
|
1057
|
+
if (targets.length === 0) {
|
|
1058
|
+
serverLogger.debug("Migration gate passed: database is up to date");
|
|
1059
|
+
return snapshot;
|
|
1060
|
+
}
|
|
1061
|
+
const lines = formatPendingMigrations(targets);
|
|
1062
|
+
const summary = pendingMigrationsSummary(targets);
|
|
1063
|
+
if (pendingIsAllowed(config)) {
|
|
1064
|
+
serverLogger.warn(`Starting with pending migrations \u2014 ${summary}`);
|
|
1065
|
+
lines.forEach((line) => serverLogger.warn(` ${line}`));
|
|
1066
|
+
serverLogger.warn(` Requests hitting the missing columns will fail. ${RUN_MIGRATIONS_HINT}`);
|
|
1067
|
+
return snapshot;
|
|
1068
|
+
}
|
|
1069
|
+
serverLogger.error(`Refusing to start: ${summary}`);
|
|
1070
|
+
lines.forEach((line) => serverLogger.error(` ${line}`));
|
|
1071
|
+
serverLogger.error(` ${RUN_MIGRATIONS_HINT}`);
|
|
1072
|
+
serverLogger.error(" To start anyway: SPFN_ALLOW_PENDING_MIGRATIONS=true (or `spfn dev --allow-pending-migrations`)");
|
|
1073
|
+
throw new PendingMigrationsError(
|
|
1074
|
+
`${summary}. ${RUN_MIGRATIONS_HINT}, or set SPFN_ALLOW_PENDING_MIGRATIONS=true to start anyway.`,
|
|
1075
|
+
targets.map((target) => target.name)
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
992
1079
|
// src/server/shutdown-manager.ts
|
|
993
1080
|
var DEFAULT_HOOK_TIMEOUT = 1e4;
|
|
994
1081
|
var DEFAULT_HOOK_ORDER = 100;
|
|
@@ -1203,6 +1290,24 @@ async function withTimeout2(promise, timeout, message) {
|
|
|
1203
1290
|
}
|
|
1204
1291
|
|
|
1205
1292
|
// src/server/helpers.ts
|
|
1293
|
+
async function readMigrationHealth() {
|
|
1294
|
+
const snapshot = await getMigrationSnapshot();
|
|
1295
|
+
if (snapshot.state !== "ok") {
|
|
1296
|
+
return {
|
|
1297
|
+
status: "unknown",
|
|
1298
|
+
pending: 0,
|
|
1299
|
+
checkedAt: snapshot.checkedAt,
|
|
1300
|
+
targets: [],
|
|
1301
|
+
reason: snapshot.reason
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
return {
|
|
1305
|
+
status: snapshot.pending > 0 ? "pending" : "up_to_date",
|
|
1306
|
+
pending: snapshot.pending,
|
|
1307
|
+
checkedAt: snapshot.checkedAt,
|
|
1308
|
+
targets: migrationTargets(snapshot.status)
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1206
1311
|
function createHealthCheckHandler(detailed) {
|
|
1207
1312
|
return async (c) => {
|
|
1208
1313
|
const shutdownManager = getShutdownManager();
|
|
@@ -1256,11 +1361,32 @@ function createHealthCheckHandler(detailed) {
|
|
|
1256
1361
|
};
|
|
1257
1362
|
const hasErrors = dbStatus === "error" || dbStatus === "not_initialized" || redisStatus === "error";
|
|
1258
1363
|
response.status = hasErrors ? "degraded" : "ok";
|
|
1364
|
+
response.migrations = await readMigrationHealth();
|
|
1259
1365
|
}
|
|
1260
1366
|
const statusCode = response.status === "ok" ? 200 : 503;
|
|
1261
1367
|
return c.json(response, statusCode);
|
|
1262
1368
|
};
|
|
1263
1369
|
}
|
|
1370
|
+
function resolveEndpointMiddlewares(config) {
|
|
1371
|
+
const candidates = [
|
|
1372
|
+
...config.middlewares ?? [],
|
|
1373
|
+
...config.routes?._globalMiddlewares ?? []
|
|
1374
|
+
];
|
|
1375
|
+
for (const pkgRouter of config.routes?._packageRouters ?? []) {
|
|
1376
|
+
candidates.push(...pkgRouter._globalMiddlewares ?? []);
|
|
1377
|
+
}
|
|
1378
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1379
|
+
return candidates.filter((mw) => {
|
|
1380
|
+
if (!mw.name) {
|
|
1381
|
+
return true;
|
|
1382
|
+
}
|
|
1383
|
+
if (seen.has(mw.name)) {
|
|
1384
|
+
return false;
|
|
1385
|
+
}
|
|
1386
|
+
seen.add(mw.name);
|
|
1387
|
+
return true;
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1264
1390
|
function applyServerTimeouts(server, timeouts) {
|
|
1265
1391
|
if ("timeout" in server) {
|
|
1266
1392
|
server.timeout = timeouts.request;
|
|
@@ -1416,9 +1542,9 @@ async function applyProxyGuard(app, config) {
|
|
|
1416
1542
|
if (proxyGuardConfig?.nonce) {
|
|
1417
1543
|
try {
|
|
1418
1544
|
const { getCache: getCache2 } = await import('@spfn/core/cache');
|
|
1419
|
-
const
|
|
1420
|
-
if (
|
|
1421
|
-
nonceStore = createCacheNonceStore(
|
|
1545
|
+
const cache2 = getCache2();
|
|
1546
|
+
if (cache2) {
|
|
1547
|
+
nonceStore = createCacheNonceStore(cache2);
|
|
1422
1548
|
serverLogger.info("Proxy-guard nonce replay rejection: cache (Redis/Valkey)");
|
|
1423
1549
|
} else {
|
|
1424
1550
|
nonceStore = createInMemoryNonceStore();
|
|
@@ -1530,9 +1656,9 @@ async function registerSSEEndpoint(app, config) {
|
|
|
1530
1656
|
if (!store) {
|
|
1531
1657
|
try {
|
|
1532
1658
|
const { getCache: getCache2 } = await import('@spfn/core/cache');
|
|
1533
|
-
const
|
|
1534
|
-
if (
|
|
1535
|
-
store = new CacheTokenStore(
|
|
1659
|
+
const cache2 = getCache2();
|
|
1660
|
+
if (cache2) {
|
|
1661
|
+
store = new CacheTokenStore(cache2);
|
|
1536
1662
|
if (debug) {
|
|
1537
1663
|
serverLogger.info("SSE token store: cache (Redis/Valkey)");
|
|
1538
1664
|
}
|
|
@@ -1546,7 +1672,7 @@ async function registerSSEEndpoint(app, config) {
|
|
|
1546
1672
|
store
|
|
1547
1673
|
});
|
|
1548
1674
|
const tokenPath = streamPath.replace(/\/[^/]+$/, "/token");
|
|
1549
|
-
const mwHandlers = (config
|
|
1675
|
+
const mwHandlers = resolveEndpointMiddlewares(config).map((mw) => mw.handler);
|
|
1550
1676
|
const getSubject = authConfig.getSubject ?? ((c) => c.get("auth")?.userId ?? null);
|
|
1551
1677
|
app.on(["POST"], [tokenPath], ...mwHandlers, async (c) => {
|
|
1552
1678
|
const subject = getSubject(c);
|
|
@@ -2228,6 +2354,7 @@ async function startServer(config) {
|
|
|
2228
2354
|
};
|
|
2229
2355
|
try {
|
|
2230
2356
|
await initializeInfrastructure(finalConfig);
|
|
2357
|
+
await runMigrationBootGate(finalConfig);
|
|
2231
2358
|
const app = await createServer(finalConfig);
|
|
2232
2359
|
const server = startHttpServer(app, host, port);
|
|
2233
2360
|
let wsCleanup;
|
|
@@ -2380,9 +2507,9 @@ async function initializeWebSocket(server, app, config) {
|
|
|
2380
2507
|
if (!store) {
|
|
2381
2508
|
try {
|
|
2382
2509
|
const { getCache: getCache2 } = await import('@spfn/core/cache');
|
|
2383
|
-
const
|
|
2384
|
-
if (
|
|
2385
|
-
store = new CacheTokenStore(
|
|
2510
|
+
const cache2 = getCache2();
|
|
2511
|
+
if (cache2) {
|
|
2512
|
+
store = new CacheTokenStore(cache2);
|
|
2386
2513
|
if (debug) serverLogger.info("WS token store: cache (Redis/Valkey)");
|
|
2387
2514
|
}
|
|
2388
2515
|
} catch {
|
|
@@ -2394,7 +2521,7 @@ async function initializeWebSocket(server, app, config) {
|
|
|
2394
2521
|
store
|
|
2395
2522
|
});
|
|
2396
2523
|
const tokenPath = wsPath.replace(/\/[^/]+$/, "/token");
|
|
2397
|
-
const mwHandlers = (config
|
|
2524
|
+
const mwHandlers = resolveEndpointMiddlewares(config).map((mw) => mw.handler);
|
|
2398
2525
|
const getSubject = authConfig.getSubject ?? ((c) => c.get("auth")?.userId ?? null);
|
|
2399
2526
|
app.on(["POST"], [tokenPath], ...mwHandlers, async (c) => {
|
|
2400
2527
|
const subject = getSubject(c);
|
|
@@ -2803,9 +2930,16 @@ var ServerConfigBuilder = class {
|
|
|
2803
2930
|
/**
|
|
2804
2931
|
* Register define-route based router
|
|
2805
2932
|
*
|
|
2806
|
-
*
|
|
2807
|
-
*
|
|
2808
|
-
*
|
|
2933
|
+
* Router-level middleware (`.use()`) and package routers (`.packages()`) travel
|
|
2934
|
+
* with the router itself and are applied by `registerRoutes` when the routes are
|
|
2935
|
+
* mounted — this method only records which router to mount.
|
|
2936
|
+
*
|
|
2937
|
+
* It deliberately does **not** copy `router._globalMiddlewares` into
|
|
2938
|
+
* `config.middlewares`: that copy used to make `registerRoutes` see the same
|
|
2939
|
+
* middleware twice (once from the config list, once from the router it was
|
|
2940
|
+
* handed) and attach both to every route. Middleware that verifies a JWT survives
|
|
2941
|
+
* running twice; middleware that consumes one-shot state — a nonce replay ledger —
|
|
2942
|
+
* rejects its own request the second time round.
|
|
2809
2943
|
*
|
|
2810
2944
|
* @example
|
|
2811
2945
|
* ```typescript
|
|
@@ -2816,29 +2950,12 @@ var ServerConfigBuilder = class {
|
|
|
2816
2950
|
* .use([authMiddleware]);
|
|
2817
2951
|
*
|
|
2818
2952
|
* export default defineServerConfig()
|
|
2819
|
-
* .routes(appRouter) //
|
|
2953
|
+
* .routes(appRouter) // .use() middleware applied once, at registration
|
|
2820
2954
|
* .build();
|
|
2821
2955
|
* ```
|
|
2822
2956
|
*/
|
|
2823
2957
|
routes(router) {
|
|
2824
2958
|
this.config.routes = router;
|
|
2825
|
-
const allGlobalMiddlewares = [];
|
|
2826
|
-
if (router._globalMiddlewares?.length > 0) {
|
|
2827
|
-
allGlobalMiddlewares.push(...router._globalMiddlewares);
|
|
2828
|
-
}
|
|
2829
|
-
if (router._packageRouters?.length > 0) {
|
|
2830
|
-
for (const pkgRouter of router._packageRouters) {
|
|
2831
|
-
if (pkgRouter._globalMiddlewares?.length > 0) {
|
|
2832
|
-
allGlobalMiddlewares.push(...pkgRouter._globalMiddlewares);
|
|
2833
|
-
}
|
|
2834
|
-
}
|
|
2835
|
-
}
|
|
2836
|
-
if (allGlobalMiddlewares.length > 0) {
|
|
2837
|
-
this.config.middlewares = [
|
|
2838
|
-
...this.config.middlewares || [],
|
|
2839
|
-
...allGlobalMiddlewares
|
|
2840
|
-
];
|
|
2841
|
-
}
|
|
2842
2959
|
return this;
|
|
2843
2960
|
}
|
|
2844
2961
|
/**
|
|
@@ -2978,6 +3095,21 @@ var ServerConfigBuilder = class {
|
|
|
2978
3095
|
this.config.infrastructure = infrastructure;
|
|
2979
3096
|
return this;
|
|
2980
3097
|
}
|
|
3098
|
+
/**
|
|
3099
|
+
* Configure the migration boot gate
|
|
3100
|
+
*
|
|
3101
|
+
* @example
|
|
3102
|
+
* ```typescript
|
|
3103
|
+
* // A harness that applies migrations itself, after the server is up
|
|
3104
|
+
* export default defineServerConfig()
|
|
3105
|
+
* .migrations({ allowPending: true })
|
|
3106
|
+
* .build();
|
|
3107
|
+
* ```
|
|
3108
|
+
*/
|
|
3109
|
+
migrations(migrations) {
|
|
3110
|
+
this.config.migrations = migrations;
|
|
3111
|
+
return this;
|
|
3112
|
+
}
|
|
2981
3113
|
/**
|
|
2982
3114
|
* Register workflow router for workflow orchestration
|
|
2983
3115
|
*
|
|
@@ -3052,6 +3184,6 @@ function defineServerConfig() {
|
|
|
3052
3184
|
return new ServerConfigBuilder();
|
|
3053
3185
|
}
|
|
3054
3186
|
|
|
3055
|
-
export { createServer, createServerlessApp, defineServerConfig, getShutdownManager, loadEnv, loadEnvFiles, provisionInfrastructure, resetServerlessApp, startServer };
|
|
3187
|
+
export { PendingMigrationsError, createServer, createServerlessApp, defineServerConfig, getMigrationSnapshot, getShutdownManager, loadEnv, loadEnvFiles, provisionInfrastructure, resetMigrationSnapshot, resetServerlessApp, startServer };
|
|
3056
3188
|
//# sourceMappingURL=index.js.map
|
|
3057
3189
|
//# sourceMappingURL=index.js.map
|