@spfn/core 0.2.0-beta.52 → 0.2.0-beta.54
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/{boss-Cxqc-Oiw.d.ts → boss-CEik0yq-.d.ts} +7 -0
- package/dist/cache/index.js +10 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/config/index.d.ts +285 -0
- package/dist/config/index.js +60 -1
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +57 -0
- package/dist/db/index.js +55 -8
- package/dist/db/index.js.map +1 -1
- package/dist/define-middleware-DuXD8Hvu.d.ts +167 -0
- package/dist/env/index.js +4 -3
- package/dist/env/index.js.map +1 -1
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.js +20 -2
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +3 -3
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +2 -2
- package/dist/event/sse/index.d.ts +4 -4
- package/dist/event/sse/index.js +41 -16
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +2 -2
- package/dist/event/ws/index.d.ts +3 -3
- package/dist/event/ws/index.js +75 -16
- package/dist/event/ws/index.js.map +1 -1
- package/dist/job/index.d.ts +2 -2
- package/dist/job/index.js +4 -4
- package/dist/job/index.js.map +1 -1
- package/dist/middleware/index.d.ts +171 -2
- package/dist/middleware/index.js +1186 -10
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +10 -0
- package/dist/nextjs/server.js +115 -1
- package/dist/nextjs/server.js.map +1 -1
- package/dist/route/index.d.ts +3 -165
- package/dist/server/index.d.ts +31 -5
- package/dist/server/index.js +172 -36
- package/dist/server/index.js.map +1 -1
- package/dist/{token-manager-C2Ag5-s8.d.ts → token-manager-jKD_EsSE.d.ts} +0 -1
- package/dist/{types-VpVQIsyB.d.ts → types-BFB72jbM.d.ts} +1 -1
- package/dist/{types-CKsmzaB8.d.ts → types-DVjf37yO.d.ts} +37 -1
- package/package.json +6 -6
package/dist/route/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import * as _sinclair_typebox from '@sinclair/typebox';
|
|
|
2
2
|
import { TSchema, Static, Kind } from '@sinclair/typebox';
|
|
3
3
|
import { Context, MiddlewareHandler, Hono } from 'hono';
|
|
4
4
|
import { ContentfulStatusCode, RedirectStatusCode } from 'hono/utils/http-status';
|
|
5
|
+
import { N as NamedMiddleware } from '../define-middleware-DuXD8Hvu.js';
|
|
6
|
+
export { E as ExtractMiddlewareNames, b as NamedMiddlewareFactory, d as defineMiddleware, a as defineMiddlewareFactory } from '../define-middleware-DuXD8Hvu.js';
|
|
5
7
|
import { HttpMethod } from './types.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -209,170 +211,6 @@ type RouteBuilderContext<TInput extends RouteInput = RouteInput, TInterceptor ex
|
|
|
209
211
|
raw: Context;
|
|
210
212
|
};
|
|
211
213
|
|
|
212
|
-
/**
|
|
213
|
-
* Middleware Definition Helper
|
|
214
|
-
*
|
|
215
|
-
* Provides type-safe middleware definition with name inference
|
|
216
|
-
*/
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Named middleware with type inference
|
|
220
|
-
*
|
|
221
|
-
* @example
|
|
222
|
-
* ```ts
|
|
223
|
-
* export const authMiddleware = defineMiddleware('auth', async (c, next) => {
|
|
224
|
-
* // auth logic
|
|
225
|
-
* c.set('user', { id: 1 });
|
|
226
|
-
* await next();
|
|
227
|
-
* });
|
|
228
|
-
*
|
|
229
|
-
* export const rateLimitMiddleware = defineMiddleware('rateLimit', async (c, next) => {
|
|
230
|
-
* // rate limit logic
|
|
231
|
-
* await next();
|
|
232
|
-
* });
|
|
233
|
-
* ```
|
|
234
|
-
*/
|
|
235
|
-
type NamedMiddleware<TName extends string = string> = {
|
|
236
|
-
name: TName;
|
|
237
|
-
handler: MiddlewareHandler;
|
|
238
|
-
_name: TName;
|
|
239
|
-
skips?: string[];
|
|
240
|
-
};
|
|
241
|
-
/**
|
|
242
|
-
* Named middleware factory with type inference
|
|
243
|
-
*
|
|
244
|
-
* Factory function that creates middleware instances with parameters
|
|
245
|
-
*
|
|
246
|
-
* @example
|
|
247
|
-
* ```ts
|
|
248
|
-
* export const requirePermissions = defineMiddleware('permission',
|
|
249
|
-
* (...permissions: string[]) => async (c, next) => {
|
|
250
|
-
* // permission check logic
|
|
251
|
-
* await next();
|
|
252
|
-
* }
|
|
253
|
-
* );
|
|
254
|
-
* ```
|
|
255
|
-
*/
|
|
256
|
-
type NamedMiddlewareFactory<TName extends string = string, TArgs extends any[] = any[]> = {
|
|
257
|
-
name: TName;
|
|
258
|
-
_name: TName;
|
|
259
|
-
} & ((...args: TArgs) => MiddlewareHandler);
|
|
260
|
-
/**
|
|
261
|
-
* Define a named middleware
|
|
262
|
-
*
|
|
263
|
-
* Creates a middleware with a unique name that can be referenced
|
|
264
|
-
* in route-level skip() calls with full type safety.
|
|
265
|
-
*
|
|
266
|
-
* Supports two patterns:
|
|
267
|
-
* 1. Regular middleware: Direct middleware handler
|
|
268
|
-
* 2. Factory middleware: Function that creates middleware with parameters
|
|
269
|
-
*
|
|
270
|
-
* @param name - Unique middleware name
|
|
271
|
-
* @param handler - Middleware handler function
|
|
272
|
-
* @returns Named middleware with type inference
|
|
273
|
-
*
|
|
274
|
-
* @example
|
|
275
|
-
* ```ts
|
|
276
|
-
* // Regular middleware
|
|
277
|
-
* export const authMiddleware = defineMiddleware('auth', async (c, next) => {
|
|
278
|
-
* const token = c.req.header('authorization');
|
|
279
|
-
* if (!token) {
|
|
280
|
-
* return c.json({ error: 'Unauthorized' }, 401);
|
|
281
|
-
* }
|
|
282
|
-
* c.set('user', await verifyToken(token));
|
|
283
|
-
* await next();
|
|
284
|
-
* });
|
|
285
|
-
*
|
|
286
|
-
* // Factory middleware
|
|
287
|
-
* export const requirePermissions = defineMiddleware('permission',
|
|
288
|
-
* (...permissions: string[]) => async (c, next) => {
|
|
289
|
-
* const user = c.get('user');
|
|
290
|
-
* if (!hasPermissions(user, permissions)) {
|
|
291
|
-
* return c.json({ error: 'Forbidden' }, 403);
|
|
292
|
-
* }
|
|
293
|
-
* await next();
|
|
294
|
-
* }
|
|
295
|
-
* );
|
|
296
|
-
*
|
|
297
|
-
* // server.config.ts
|
|
298
|
-
* export default defineServerConfig()
|
|
299
|
-
* .middlewares([authMiddleware])
|
|
300
|
-
* .routes(appRouter)
|
|
301
|
-
* .build();
|
|
302
|
-
*
|
|
303
|
-
* // routes.ts - skip by name
|
|
304
|
-
* export const publicRoute = route.get('/health')
|
|
305
|
-
* .skip(['auth']) // ✅ Type-safe! Autocomplete!
|
|
306
|
-
* .handler(async (c) => c.success({ status: 'ok' }));
|
|
307
|
-
*
|
|
308
|
-
* // Use factory middleware inline
|
|
309
|
-
* export const protectedRoute = route.get('/admin')
|
|
310
|
-
* .use([requirePermissions('admin:write')])
|
|
311
|
-
* .handler(async (c) => { ... });
|
|
312
|
-
* ```
|
|
313
|
-
*/
|
|
314
|
-
/**
|
|
315
|
-
* Options for defineMiddleware
|
|
316
|
-
*/
|
|
317
|
-
interface DefineMiddlewareOptions {
|
|
318
|
-
/**
|
|
319
|
-
* Server-level middleware names to auto-skip when this middleware is used at route level.
|
|
320
|
-
*
|
|
321
|
-
* @example
|
|
322
|
-
* ```ts
|
|
323
|
-
* // optionalAuth auto-skips the global 'auth' middleware
|
|
324
|
-
* export const optionalAuth = defineMiddleware('optionalAuth', handler, {
|
|
325
|
-
* skips: ['auth']
|
|
326
|
-
* });
|
|
327
|
-
*
|
|
328
|
-
* // Usage: .use([optionalAuth]) — no need for .skip(['auth'])
|
|
329
|
-
* ```
|
|
330
|
-
*/
|
|
331
|
-
skips?: string[];
|
|
332
|
-
}
|
|
333
|
-
declare function defineMiddleware<TName extends string>(name: TName, handler: MiddlewareHandler, options?: DefineMiddlewareOptions): NamedMiddleware<TName>;
|
|
334
|
-
declare function defineMiddleware<TName extends string, TArgs extends any[]>(name: TName, factory: (...args: TArgs) => MiddlewareHandler, options?: DefineMiddlewareOptions): NamedMiddlewareFactory<TName, TArgs>;
|
|
335
|
-
/**
|
|
336
|
-
* Define a middleware factory explicitly
|
|
337
|
-
*
|
|
338
|
-
* Use this when your factory function has exactly 2 parameters,
|
|
339
|
-
* which would be incorrectly detected as a regular middleware handler.
|
|
340
|
-
*
|
|
341
|
-
* @param name - Unique middleware name
|
|
342
|
-
* @param factory - Factory function that returns a middleware handler
|
|
343
|
-
* @returns Named middleware factory with type inference
|
|
344
|
-
*
|
|
345
|
-
* @example
|
|
346
|
-
* ```ts
|
|
347
|
-
* // Factory with 2 params (would be misdetected by defineMiddleware)
|
|
348
|
-
* export const rateLimiter = defineMiddlewareFactory('rateLimit',
|
|
349
|
-
* (limit: number, window: number) => async (c, next) => {
|
|
350
|
-
* // rate limit logic using limit and window
|
|
351
|
-
* await next();
|
|
352
|
-
* }
|
|
353
|
-
* );
|
|
354
|
-
*
|
|
355
|
-
* // Usage
|
|
356
|
-
* route.get('/api')
|
|
357
|
-
* .use([rateLimiter(100, 60000)]) // 100 requests per minute
|
|
358
|
-
* .handler(...)
|
|
359
|
-
* ```
|
|
360
|
-
*/
|
|
361
|
-
declare function defineMiddlewareFactory<TName extends string, TArgs extends unknown[]>(name: TName, factory: (...args: TArgs) => MiddlewareHandler): NamedMiddlewareFactory<TName, TArgs>;
|
|
362
|
-
/**
|
|
363
|
-
* Extract middleware names from an array of named middlewares
|
|
364
|
-
*
|
|
365
|
-
* @example
|
|
366
|
-
* ```ts
|
|
367
|
-
* type Middlewares = [
|
|
368
|
-
* NamedMiddleware<'auth'>,
|
|
369
|
-
* NamedMiddleware<'rateLimit'>
|
|
370
|
-
* ];
|
|
371
|
-
* type Names = ExtractMiddlewareNames<Middlewares>; // 'auth' | 'rateLimit'
|
|
372
|
-
* ```
|
|
373
|
-
*/
|
|
374
|
-
type ExtractMiddlewareNames<T extends readonly NamedMiddleware<string>[]> = T[number]['_name'];
|
|
375
|
-
|
|
376
214
|
/**
|
|
377
215
|
* Route Builder
|
|
378
216
|
*
|
|
@@ -911,4 +749,4 @@ declare function getFileOptions(schema: TSchema): FileSchemaOptions | FileArrayS
|
|
|
911
749
|
*/
|
|
912
750
|
declare function formatFileSize(bytes: number): string;
|
|
913
751
|
|
|
914
|
-
export {
|
|
752
|
+
export { FileArraySchema, type FileArraySchemaOptions, type FileArraySchemaType, FileSchema, type FileSchemaOptions, type FileSchemaType, HttpMethod, type MergedInput, NamedMiddleware, Nullable, OptionalFileSchema, OptionalNullable, type PaginatedResult, type RegisteredRoute, type RouteBuilderContext, type RouteDef, type RouteHandlerFn, type RouteInput, type Router, defineRouter, formatFileSize, getFileOptions, isFileArraySchema, isFileSchema, isHttpMethod, registerRoutes, route };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -3,11 +3,11 @@ import { MiddlewareHandler, Hono } from 'hono';
|
|
|
3
3
|
import { cors } from 'hono/cors';
|
|
4
4
|
import { serve } from '@hono/node-server';
|
|
5
5
|
import { NamedMiddleware, Router } from '@spfn/core/route';
|
|
6
|
-
import { OnErrorContext } from '@spfn/core/middleware';
|
|
7
|
-
import { J as JobRouter, B as BossOptions } from '../boss-
|
|
8
|
-
import { E as EventRouterDef } from '../token-manager-
|
|
9
|
-
import { S as SSEHandlerConfig, a as SSEAuthConfig } from '../types-
|
|
10
|
-
import { W as WSRouterDef, a as WSHandlerConfig, b as WSMessageHandlers, c as WSAuthConfig } from '../types-
|
|
6
|
+
import { OnErrorContext, ProxyGuardConfig } from '@spfn/core/middleware';
|
|
7
|
+
import { J as JobRouter, B as BossOptions } from '../boss-CEik0yq-.js';
|
|
8
|
+
import { E as EventRouterDef } from '../token-manager-jKD_EsSE.js';
|
|
9
|
+
import { S as SSEHandlerConfig, a as SSEAuthConfig } from '../types-BFB72jbM.js';
|
|
10
|
+
import { W as WSRouterDef, a as WSHandlerConfig, b as WSMessageHandlers, c as WSAuthConfig } from '../types-DVjf37yO.js';
|
|
11
11
|
import '@sinclair/typebox';
|
|
12
12
|
import 'pg-boss';
|
|
13
13
|
|
|
@@ -96,6 +96,28 @@ interface ServerConfig {
|
|
|
96
96
|
* Additional custom middleware
|
|
97
97
|
*/
|
|
98
98
|
use?: MiddlewareHandler[];
|
|
99
|
+
/**
|
|
100
|
+
* Proxy-guard: verify requests came through the trusted Next.js RPC proxy
|
|
101
|
+
* (HMAC signature) and/or an allowed browser origin, then tag `clientType`.
|
|
102
|
+
* Lets the backend reject direct-to-backend calls that bypass the proxy.
|
|
103
|
+
*
|
|
104
|
+
* Disabled by default (`mode: 'off'`). Requires the same `SPFN_PROXY_SECRET`
|
|
105
|
+
* on the proxy and the backend. See PROXY-BACKEND-AUTH-SPEC.md.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* .proxyGuard({ mode: 'strict', allowedOrigins: ['https://app.example.com'] })
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
proxyGuard?: Omit<ProxyGuardConfig, 'nonceStore'> & {
|
|
113
|
+
/**
|
|
114
|
+
* Enable hard replay rejection via a Redis nonce store. Evaluated in BOTH modes
|
|
115
|
+
* (tag observes replays, strict rejects). Requires a cache (CACHE_URL); without
|
|
116
|
+
* one, falls back to the timestamp window. Degrades to the window if the store
|
|
117
|
+
* is briefly unavailable. @default false
|
|
118
|
+
*/
|
|
119
|
+
nonce?: boolean;
|
|
120
|
+
};
|
|
99
121
|
/**
|
|
100
122
|
* Global middlewares with names for route-level skip control
|
|
101
123
|
* Use defineMiddleware() for type-safe middleware definitions
|
|
@@ -788,6 +810,10 @@ declare class ServerConfigBuilder {
|
|
|
788
810
|
* Add named middlewares for route-level skip control
|
|
789
811
|
*/
|
|
790
812
|
middlewares(middlewares: ServerConfig['middlewares']): this;
|
|
813
|
+
/**
|
|
814
|
+
* Configure proxy-guard (verify trusted-proxy signature + origin → clientType)
|
|
815
|
+
*/
|
|
816
|
+
proxyGuard(proxyGuard: ServerConfig['proxyGuard']): this;
|
|
791
817
|
/**
|
|
792
818
|
* Register define-route based router
|
|
793
819
|
*
|
package/dist/server/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { cors } from 'hono/cors';
|
|
|
8
8
|
import { registerRoutes } from '@spfn/core/route';
|
|
9
9
|
import { ErrorHandler, RequestLogger } from '@spfn/core/middleware';
|
|
10
10
|
import { streamSSE } from 'hono/streaming';
|
|
11
|
-
import { randomBytes } from 'crypto';
|
|
11
|
+
import { randomBytes, createHash } from 'crypto';
|
|
12
12
|
import { Agent, setGlobalDispatcher } from 'undici';
|
|
13
13
|
import { initDatabase, getDatabase, closeDatabase } from '@spfn/core/db';
|
|
14
14
|
import { initCache, getCache, closeCache } from '@spfn/core/cache';
|
|
@@ -445,6 +445,23 @@ function createBoundedWriter(stream, maxQueue, onClose) {
|
|
|
445
445
|
|
|
446
446
|
// src/event/sse/handler.ts
|
|
447
447
|
var sseLogger = logger.child("@spfn/core:sse");
|
|
448
|
+
var frameCache = /* @__PURE__ */ new WeakMap();
|
|
449
|
+
function serializeFrame(eventName, payload) {
|
|
450
|
+
if (payload === null || typeof payload !== "object") {
|
|
451
|
+
return JSON.stringify({ event: eventName, data: payload });
|
|
452
|
+
}
|
|
453
|
+
let byEvent = frameCache.get(payload);
|
|
454
|
+
if (!byEvent) {
|
|
455
|
+
byEvent = /* @__PURE__ */ new Map();
|
|
456
|
+
frameCache.set(payload, byEvent);
|
|
457
|
+
}
|
|
458
|
+
let serialized = byEvent.get(eventName);
|
|
459
|
+
if (serialized === void 0) {
|
|
460
|
+
serialized = JSON.stringify({ event: eventName, data: payload });
|
|
461
|
+
byEvent.set(eventName, serialized);
|
|
462
|
+
}
|
|
463
|
+
return serialized;
|
|
464
|
+
}
|
|
448
465
|
var DEFAULT_MAX_QUEUE = 1e3;
|
|
449
466
|
function createSSEHandler(router, config = {}, tokenManager) {
|
|
450
467
|
const {
|
|
@@ -495,12 +512,14 @@ function createSSEHandler(router, config = {}, tokenManager) {
|
|
|
495
512
|
let connectionDead = false;
|
|
496
513
|
let pingTimer;
|
|
497
514
|
let writer;
|
|
515
|
+
let onClosed;
|
|
498
516
|
const cleanup = (reason) => {
|
|
499
517
|
if (connectionDead) return;
|
|
500
518
|
connectionDead = true;
|
|
501
519
|
clearInterval(pingTimer);
|
|
502
520
|
unsubscribes.forEach((fn) => fn());
|
|
503
521
|
writer?.close();
|
|
522
|
+
onClosed?.();
|
|
504
523
|
sseLogger.info("SSE dead connection cleaned up", {
|
|
505
524
|
events: allowedEvents,
|
|
506
525
|
reason
|
|
@@ -530,10 +549,6 @@ function createSSEHandler(router, config = {}, tokenManager) {
|
|
|
530
549
|
}
|
|
531
550
|
}
|
|
532
551
|
messageId++;
|
|
533
|
-
const message = {
|
|
534
|
-
event: eventName,
|
|
535
|
-
data: payload
|
|
536
|
-
};
|
|
537
552
|
sseLogger.debug("SSE sending event", {
|
|
538
553
|
event: eventName,
|
|
539
554
|
messageId
|
|
@@ -541,7 +556,7 @@ function createSSEHandler(router, config = {}, tokenManager) {
|
|
|
541
556
|
writer.enqueue({
|
|
542
557
|
id: String(messageId),
|
|
543
558
|
event: eventName,
|
|
544
|
-
data:
|
|
559
|
+
data: serializeFrame(eventName, payload)
|
|
545
560
|
});
|
|
546
561
|
});
|
|
547
562
|
unsubscribes.push(unsubscribe);
|
|
@@ -558,8 +573,15 @@ function createSSEHandler(router, config = {}, tokenManager) {
|
|
|
558
573
|
});
|
|
559
574
|
}, pingInterval);
|
|
560
575
|
const abortSignal = c.req.raw.signal;
|
|
561
|
-
|
|
562
|
-
await
|
|
576
|
+
if (!abortSignal.aborted && !connectionDead) {
|
|
577
|
+
await new Promise((resolve2) => {
|
|
578
|
+
const onAbort = () => resolve2();
|
|
579
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
580
|
+
onClosed = () => {
|
|
581
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
582
|
+
resolve2();
|
|
583
|
+
};
|
|
584
|
+
});
|
|
563
585
|
}
|
|
564
586
|
cleanup();
|
|
565
587
|
}, async (err) => {
|
|
@@ -596,24 +618,28 @@ async function authorizeEvents(subject, requestedEvents, authConfig) {
|
|
|
596
618
|
}
|
|
597
619
|
return allowed;
|
|
598
620
|
}
|
|
621
|
+
function hashToken(token) {
|
|
622
|
+
return createHash("sha256").update(token).digest("hex");
|
|
623
|
+
}
|
|
599
624
|
var InMemoryTokenStore = class {
|
|
600
625
|
tokens = /* @__PURE__ */ new Map();
|
|
601
626
|
async set(token, data) {
|
|
602
|
-
this.tokens.set(token, data);
|
|
627
|
+
this.tokens.set(hashToken(token), data);
|
|
603
628
|
}
|
|
604
629
|
async consume(token) {
|
|
605
|
-
const
|
|
630
|
+
const key = hashToken(token);
|
|
631
|
+
const data = this.tokens.get(key);
|
|
606
632
|
if (!data) {
|
|
607
633
|
return null;
|
|
608
634
|
}
|
|
609
|
-
this.tokens.delete(
|
|
635
|
+
this.tokens.delete(key);
|
|
610
636
|
return data;
|
|
611
637
|
}
|
|
612
638
|
async cleanup() {
|
|
613
639
|
const now = Date.now();
|
|
614
|
-
for (const [
|
|
640
|
+
for (const [key, data] of this.tokens) {
|
|
615
641
|
if (data.expiresAt <= now) {
|
|
616
|
-
this.tokens.delete(
|
|
642
|
+
this.tokens.delete(key);
|
|
617
643
|
}
|
|
618
644
|
}
|
|
619
645
|
}
|
|
@@ -626,14 +652,14 @@ var CacheTokenStore = class {
|
|
|
626
652
|
async set(token, data) {
|
|
627
653
|
const ttlSeconds = Math.max(1, Math.ceil((data.expiresAt - Date.now()) / 1e3));
|
|
628
654
|
await this.cache.set(
|
|
629
|
-
this.prefix + token,
|
|
655
|
+
this.prefix + hashToken(token),
|
|
630
656
|
JSON.stringify(data),
|
|
631
657
|
"EX",
|
|
632
658
|
ttlSeconds
|
|
633
659
|
);
|
|
634
660
|
}
|
|
635
661
|
async consume(token) {
|
|
636
|
-
const key = this.prefix + token;
|
|
662
|
+
const key = this.prefix + hashToken(token);
|
|
637
663
|
let raw = null;
|
|
638
664
|
if (this.cache.getdel) {
|
|
639
665
|
raw = await this.cache.getdel(key);
|
|
@@ -668,7 +694,6 @@ var SSETokenManager = class {
|
|
|
668
694
|
async issue(subject) {
|
|
669
695
|
const token = randomBytes(32).toString("hex");
|
|
670
696
|
await this.store.set(token, {
|
|
671
|
-
token,
|
|
672
697
|
subject,
|
|
673
698
|
expiresAt: Date.now() + this.ttl
|
|
674
699
|
});
|
|
@@ -1343,6 +1368,7 @@ async function createAutoConfiguredApp(config) {
|
|
|
1343
1368
|
});
|
|
1344
1369
|
}
|
|
1345
1370
|
applyDefaultMiddleware(app, config, enableLogger, enableCors);
|
|
1371
|
+
await applyProxyGuard(app, config);
|
|
1346
1372
|
if (Array.isArray(config?.use)) {
|
|
1347
1373
|
config.use.forEach((mw) => app.use("*", mw));
|
|
1348
1374
|
}
|
|
@@ -1365,6 +1391,51 @@ function applyDefaultMiddleware(app, config, enableLogger, enableCors) {
|
|
|
1365
1391
|
app.use("*", cors(corsOptions));
|
|
1366
1392
|
}
|
|
1367
1393
|
}
|
|
1394
|
+
async function applyProxyGuard(app, config) {
|
|
1395
|
+
const proxyGuardConfig = config?.proxyGuard;
|
|
1396
|
+
const mode = proxyGuardConfig?.mode ?? "off";
|
|
1397
|
+
if (mode === "off") {
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
const { createProxyGuard, createCacheNonceStore, createInMemoryNonceStore } = await import('@spfn/core/middleware');
|
|
1401
|
+
let nonceStore;
|
|
1402
|
+
if (proxyGuardConfig?.nonce) {
|
|
1403
|
+
try {
|
|
1404
|
+
const { getCache: getCache2 } = await import('@spfn/core/cache');
|
|
1405
|
+
const cache = getCache2();
|
|
1406
|
+
if (cache) {
|
|
1407
|
+
nonceStore = createCacheNonceStore(cache);
|
|
1408
|
+
serverLogger.info("Proxy-guard nonce replay rejection: cache (Redis/Valkey)");
|
|
1409
|
+
} else {
|
|
1410
|
+
nonceStore = createInMemoryNonceStore();
|
|
1411
|
+
serverLogger.info("Proxy-guard nonce replay rejection: in-memory (single instance only \u2014 use a cache for multi-instance)");
|
|
1412
|
+
}
|
|
1413
|
+
} catch {
|
|
1414
|
+
nonceStore = createInMemoryNonceStore();
|
|
1415
|
+
serverLogger.warn("Proxy-guard nonce: cache module unavailable \u2014 using in-memory store (single instance only)");
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
const autoSkip = [config?.healthCheck?.path ?? "/health"];
|
|
1419
|
+
if (config?.events) {
|
|
1420
|
+
autoSkip.push(config.eventsConfig?.path ?? "/events/stream");
|
|
1421
|
+
}
|
|
1422
|
+
if (config?.websockets) {
|
|
1423
|
+
autoSkip.push(config.websocketsConfig?.path ?? "/ws");
|
|
1424
|
+
}
|
|
1425
|
+
const skipPaths = [...proxyGuardConfig?.skipPaths ?? [], ...autoSkip];
|
|
1426
|
+
app.use("*", createProxyGuard({
|
|
1427
|
+
mode,
|
|
1428
|
+
secret: proxyGuardConfig?.secret,
|
|
1429
|
+
previousSecrets: proxyGuardConfig?.previousSecrets,
|
|
1430
|
+
windowMs: proxyGuardConfig?.windowMs,
|
|
1431
|
+
allowedOrigins: proxyGuardConfig?.allowedOrigins,
|
|
1432
|
+
maxBodyBytes: proxyGuardConfig?.maxBodyBytes,
|
|
1433
|
+
nonceStore,
|
|
1434
|
+
nonceFailClosed: proxyGuardConfig?.nonceFailClosed,
|
|
1435
|
+
skipPaths
|
|
1436
|
+
}));
|
|
1437
|
+
serverLogger.info(`\u2713 Proxy-guard enabled (mode: ${mode})`);
|
|
1438
|
+
}
|
|
1368
1439
|
function registerHealthCheckEndpoint(app, config) {
|
|
1369
1440
|
const healthCheckConfig = config?.healthCheck ?? {};
|
|
1370
1441
|
const healthCheckEnabled = healthCheckConfig.enabled !== false;
|
|
@@ -1606,9 +1677,7 @@ async function registerJobs(router) {
|
|
|
1606
1677
|
}
|
|
1607
1678
|
jobLogger2.info("Existing jobs cleared");
|
|
1608
1679
|
}
|
|
1609
|
-
|
|
1610
|
-
await registerJob(job2);
|
|
1611
|
-
}
|
|
1680
|
+
await Promise.all(jobs.map((job2) => registerJob(job2)));
|
|
1612
1681
|
jobLogger2.info("All jobs registered successfully");
|
|
1613
1682
|
}
|
|
1614
1683
|
async function ensureQueue(boss, queueName) {
|
|
@@ -1641,9 +1710,10 @@ async function executeJobHandler(job2, pgBossJob) {
|
|
|
1641
1710
|
async function registerWorker(boss, job2, queueName) {
|
|
1642
1711
|
await ensureQueue(boss, queueName);
|
|
1643
1712
|
const batchSize = job2.options?.batchSize ?? 1;
|
|
1713
|
+
const pollingIntervalSeconds = job2.options?.pollingIntervalSeconds ?? env.JOB_POLLING_INTERVAL_SECONDS;
|
|
1644
1714
|
await boss.work(
|
|
1645
1715
|
queueName,
|
|
1646
|
-
{ batchSize },
|
|
1716
|
+
{ batchSize, pollingIntervalSeconds },
|
|
1647
1717
|
async (pgBossJobs) => {
|
|
1648
1718
|
if (batchSize <= 1) {
|
|
1649
1719
|
await executeJobHandler(job2, pgBossJobs[0]);
|
|
@@ -1721,24 +1791,54 @@ async function registerJob(job2) {
|
|
|
1721
1791
|
jobLogger2.debug(`Job registered: ${job2.name}`);
|
|
1722
1792
|
}
|
|
1723
1793
|
var wsLogger = logger.child("@spfn/core:ws");
|
|
1794
|
+
function isOriginAllowed(origin, allowList) {
|
|
1795
|
+
if (!allowList) {
|
|
1796
|
+
return true;
|
|
1797
|
+
}
|
|
1798
|
+
if (!origin) {
|
|
1799
|
+
return true;
|
|
1800
|
+
}
|
|
1801
|
+
return allowList.has(origin);
|
|
1802
|
+
}
|
|
1724
1803
|
async function attachWSHandler(server, router, config = {}, tokenManager) {
|
|
1725
1804
|
const WebSocketServer = await loadWSServer();
|
|
1726
1805
|
const {
|
|
1727
1806
|
pingInterval = 3e4,
|
|
1728
1807
|
path = "/ws",
|
|
1808
|
+
maxPayload = 1048576,
|
|
1809
|
+
maxBufferedBytes = 1048576,
|
|
1810
|
+
maxConnections = 1e4,
|
|
1811
|
+
maxConnectionsPerSubject = 0,
|
|
1812
|
+
allowedOrigins,
|
|
1729
1813
|
auth: authConfig
|
|
1730
1814
|
} = config;
|
|
1815
|
+
const originAllowList = allowedOrigins && allowedOrigins.length > 0 ? new Set(allowedOrigins) : null;
|
|
1731
1816
|
if (authConfig?.enabled && !tokenManager) {
|
|
1732
1817
|
throw new Error(
|
|
1733
1818
|
"WebSocket auth.enabled=true requires a tokenManager. Pass tokenManager or use .websockets(router, { auth: { enabled: true } }) via startServer."
|
|
1734
1819
|
);
|
|
1735
1820
|
}
|
|
1736
|
-
const wss = new WebSocketServer({ server, path });
|
|
1821
|
+
const wss = new WebSocketServer({ server, path, maxPayload });
|
|
1737
1822
|
const clients = /* @__PURE__ */ new Set();
|
|
1823
|
+
const subjectCounts = /* @__PURE__ */ new Map();
|
|
1738
1824
|
wss.on("connection", (ws, req) => {
|
|
1825
|
+
if (!isOriginAllowed(req.headers?.origin, originAllowList)) {
|
|
1826
|
+
wsLogger.warn("WebSocket rejected: disallowed origin", { origin: req.headers?.origin });
|
|
1827
|
+
ws.close(1008, "Origin not allowed");
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
if (clients.size >= maxConnections) {
|
|
1831
|
+
ws.close(1013, "Server at capacity");
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1739
1834
|
clients.add(ws);
|
|
1740
1835
|
ws.on("close", () => clients.delete(ws));
|
|
1741
|
-
handleConnection(ws, req, router, authConfig, tokenManager,
|
|
1836
|
+
handleConnection(ws, req, router, authConfig, tokenManager, {
|
|
1837
|
+
pingInterval,
|
|
1838
|
+
maxBufferedBytes,
|
|
1839
|
+
maxConnectionsPerSubject,
|
|
1840
|
+
subjectCounts
|
|
1841
|
+
}).catch((err) => {
|
|
1742
1842
|
wsLogger.error("WebSocket connection handler error", err);
|
|
1743
1843
|
if (ws.readyState === 1) ws.close(1011, "Internal server error");
|
|
1744
1844
|
});
|
|
@@ -1761,7 +1861,8 @@ async function attachWSHandler(server, router, config = {}, tokenManager) {
|
|
|
1761
1861
|
});
|
|
1762
1862
|
});
|
|
1763
1863
|
}
|
|
1764
|
-
async function handleConnection(ws, req, router, authConfig, tokenManager,
|
|
1864
|
+
async function handleConnection(ws, req, router, authConfig, tokenManager, opts) {
|
|
1865
|
+
const { pingInterval, maxBufferedBytes, maxConnectionsPerSubject, subjectCounts } = opts;
|
|
1765
1866
|
let pingTimer;
|
|
1766
1867
|
let connectionUnsubscribes = [];
|
|
1767
1868
|
let subscribedEvents = [];
|
|
@@ -1795,13 +1896,26 @@ async function handleConnection(ws, req, router, authConfig, tokenManager, pingI
|
|
|
1795
1896
|
ws.close(4003, "Not authorized for any requested events");
|
|
1796
1897
|
return;
|
|
1797
1898
|
}
|
|
1899
|
+
if (maxConnectionsPerSubject > 0 && typeof subject === "string") {
|
|
1900
|
+
const current = subjectCounts.get(subject) ?? 0;
|
|
1901
|
+
if (current >= maxConnectionsPerSubject) {
|
|
1902
|
+
ws.close(1013, "Too many connections for this subject");
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
subjectCounts.set(subject, current + 1);
|
|
1906
|
+
ws.on("close", () => {
|
|
1907
|
+
const remaining = (subjectCounts.get(subject) ?? 1) - 1;
|
|
1908
|
+
if (remaining <= 0) subjectCounts.delete(subject);
|
|
1909
|
+
else subjectCounts.set(subject, remaining);
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1798
1912
|
subscribedEvents = allowedEvents;
|
|
1799
1913
|
wsLogger.info("WebSocket connection established", {
|
|
1800
1914
|
events: allowedEvents,
|
|
1801
1915
|
subject: subject ?? void 0
|
|
1802
1916
|
});
|
|
1803
|
-
const connection = createConnection(ws);
|
|
1804
|
-
connectionUnsubscribes = subscribeEvents(ws, router, allowedEvents, subject, authConfig);
|
|
1917
|
+
const connection = createConnection(ws, maxBufferedBytes);
|
|
1918
|
+
connectionUnsubscribes = subscribeEvents(ws, router, allowedEvents, subject, authConfig, maxBufferedBytes);
|
|
1805
1919
|
if (ws.readyState !== 1) {
|
|
1806
1920
|
connectionUnsubscribes.forEach((fn) => fn());
|
|
1807
1921
|
connectionUnsubscribes = [];
|
|
@@ -1811,8 +1925,18 @@ async function handleConnection(ws, req, router, authConfig, tokenManager, pingI
|
|
|
1811
1925
|
onClientMessage(data, router, connection, subject).catch((err) => wsLogger.error("Unhandled message error", err));
|
|
1812
1926
|
});
|
|
1813
1927
|
if (pingInterval > 0) {
|
|
1928
|
+
ws.isAlive = true;
|
|
1929
|
+
ws.on("pong", () => {
|
|
1930
|
+
ws.isAlive = true;
|
|
1931
|
+
});
|
|
1814
1932
|
pingTimer = setInterval(() => {
|
|
1815
|
-
if (ws.readyState
|
|
1933
|
+
if (ws.readyState !== 1) return;
|
|
1934
|
+
if (ws.isAlive === false) {
|
|
1935
|
+
ws.terminate();
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
ws.isAlive = false;
|
|
1939
|
+
ws.ping();
|
|
1816
1940
|
}, pingInterval);
|
|
1817
1941
|
}
|
|
1818
1942
|
connection.send("__connected", {
|
|
@@ -1851,16 +1975,24 @@ async function resolveAllowedEvents(subject, requestedEvents, authConfig) {
|
|
|
1851
1975
|
const allowed = await authConfig.authorize(subject, requestedEvents);
|
|
1852
1976
|
return allowed.length === 0 ? null : allowed;
|
|
1853
1977
|
}
|
|
1854
|
-
function
|
|
1978
|
+
function safeSend(ws, frame, maxBufferedBytes) {
|
|
1979
|
+
if (ws.readyState !== 1) return;
|
|
1980
|
+
if (ws.bufferedAmount > maxBufferedBytes) {
|
|
1981
|
+
ws.close(1013, "Send buffer overflow");
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
try {
|
|
1985
|
+
ws.send(JSON.stringify(frame));
|
|
1986
|
+
} catch {
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
function createConnection(ws, maxBufferedBytes) {
|
|
1855
1990
|
return {
|
|
1856
|
-
send: (type, payload) => {
|
|
1857
|
-
if (ws.readyState !== 1) return;
|
|
1858
|
-
ws.send(JSON.stringify({ type, data: payload }));
|
|
1859
|
-
},
|
|
1991
|
+
send: (type, payload) => safeSend(ws, { type, data: payload }, maxBufferedBytes),
|
|
1860
1992
|
close: (code, reason) => ws.close(code, reason)
|
|
1861
1993
|
};
|
|
1862
1994
|
}
|
|
1863
|
-
function subscribeEvents(ws, router, allowedEvents, subject, authConfig) {
|
|
1995
|
+
function subscribeEvents(ws, router, allowedEvents, subject, authConfig, maxBufferedBytes) {
|
|
1864
1996
|
const unsubscribes = [];
|
|
1865
1997
|
for (const eventName of allowedEvents) {
|
|
1866
1998
|
const eventDef = router.events[eventName];
|
|
@@ -1870,10 +2002,7 @@ function subscribeEvents(ws, router, allowedEvents, subject, authConfig) {
|
|
|
1870
2002
|
if (subject && authConfig?.filter?.[eventName]) {
|
|
1871
2003
|
if (!authConfig.filter[eventName](subject, payload)) return;
|
|
1872
2004
|
}
|
|
1873
|
-
|
|
1874
|
-
ws.send(JSON.stringify({ type: eventName, data: payload }));
|
|
1875
|
-
} catch {
|
|
1876
|
-
}
|
|
2005
|
+
safeSend(ws, { type: eventName, data: payload }, maxBufferedBytes);
|
|
1877
2006
|
});
|
|
1878
2007
|
unsubscribes.push(unsubscribe);
|
|
1879
2008
|
}
|
|
@@ -2493,6 +2622,13 @@ var ServerConfigBuilder = class {
|
|
|
2493
2622
|
this.config.middlewares = middlewares;
|
|
2494
2623
|
return this;
|
|
2495
2624
|
}
|
|
2625
|
+
/**
|
|
2626
|
+
* Configure proxy-guard (verify trusted-proxy signature + origin → clientType)
|
|
2627
|
+
*/
|
|
2628
|
+
proxyGuard(proxyGuard) {
|
|
2629
|
+
this.config.proxyGuard = proxyGuard;
|
|
2630
|
+
return this;
|
|
2631
|
+
}
|
|
2496
2632
|
/**
|
|
2497
2633
|
* Register define-route based router
|
|
2498
2634
|
*
|