@spfn/core 0.2.0-beta.12 → 0.2.0-beta.13
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/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 +3 -2
- package/dist/server/index.js +151 -15
- package/dist/server/index.js.map +1 -1
- package/dist/types-B-lVqv6b.d.ts +298 -0
- 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
|
@@ -8,6 +8,7 @@ import { registerRoutes } from '@spfn/core/route';
|
|
|
8
8
|
import { ErrorHandler, RequestLogger } from '@spfn/core/middleware';
|
|
9
9
|
import { streamSSE } from 'hono/streaming';
|
|
10
10
|
import { logger } from '@spfn/core/logger';
|
|
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';
|
|
@@ -333,17 +334,26 @@ function loadEnvFiles() {
|
|
|
333
334
|
}
|
|
334
335
|
}
|
|
335
336
|
var sseLogger = logger.child("@spfn/core:sse");
|
|
336
|
-
function createSSEHandler(router, config2 = {}) {
|
|
337
|
+
function createSSEHandler(router, config2 = {}, tokenManager) {
|
|
337
338
|
const {
|
|
338
|
-
pingInterval = 3e4
|
|
339
|
-
|
|
339
|
+
pingInterval = 3e4,
|
|
340
|
+
auth: authConfig
|
|
340
341
|
} = config2;
|
|
341
342
|
return async (c) => {
|
|
342
|
-
const
|
|
343
|
-
if (
|
|
343
|
+
const subject = await authenticateToken(c, tokenManager);
|
|
344
|
+
if (subject === false) {
|
|
345
|
+
return c.json({ error: "Missing token parameter" }, 401);
|
|
346
|
+
}
|
|
347
|
+
if (subject === null) {
|
|
348
|
+
return c.json({ error: "Invalid or expired token" }, 401);
|
|
349
|
+
}
|
|
350
|
+
if (subject) {
|
|
351
|
+
c.set("sseSubject", subject);
|
|
352
|
+
}
|
|
353
|
+
const requestedEvents = parseRequestedEvents(c);
|
|
354
|
+
if (!requestedEvents) {
|
|
344
355
|
return c.json({ error: "Missing events parameter" }, 400);
|
|
345
356
|
}
|
|
346
|
-
const requestedEvents = eventsParam.split(",").map((e) => e.trim());
|
|
347
357
|
const validEventNames = router.eventNames;
|
|
348
358
|
const invalidEvents = requestedEvents.filter((e) => !validEventNames.includes(e));
|
|
349
359
|
if (invalidEvents.length > 0) {
|
|
@@ -353,19 +363,29 @@ function createSSEHandler(router, config2 = {}) {
|
|
|
353
363
|
validEvents: validEventNames
|
|
354
364
|
}, 400);
|
|
355
365
|
}
|
|
366
|
+
const allowedEvents = await authorizeEvents(subject, requestedEvents, authConfig);
|
|
367
|
+
if (allowedEvents === null) {
|
|
368
|
+
return c.json({ error: "Not authorized for any requested events" }, 403);
|
|
369
|
+
}
|
|
356
370
|
sseLogger.debug("SSE connection requested", {
|
|
357
|
-
events:
|
|
371
|
+
events: allowedEvents,
|
|
372
|
+
subject: subject || void 0,
|
|
358
373
|
clientIp: c.req.header("x-forwarded-for") || c.req.header("x-real-ip")
|
|
359
374
|
});
|
|
360
375
|
return streamSSE(c, async (stream) => {
|
|
361
376
|
const unsubscribes = [];
|
|
362
377
|
let messageId = 0;
|
|
363
|
-
for (const eventName of
|
|
378
|
+
for (const eventName of allowedEvents) {
|
|
364
379
|
const eventDef = router.events[eventName];
|
|
365
380
|
if (!eventDef) {
|
|
366
381
|
continue;
|
|
367
382
|
}
|
|
368
383
|
const unsubscribe = eventDef.subscribe((payload) => {
|
|
384
|
+
if (subject && authConfig?.filter?.[eventName]) {
|
|
385
|
+
if (!authConfig.filter[eventName](subject, payload)) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
369
389
|
messageId++;
|
|
370
390
|
const message = {
|
|
371
391
|
event: eventName,
|
|
@@ -384,13 +404,13 @@ function createSSEHandler(router, config2 = {}) {
|
|
|
384
404
|
unsubscribes.push(unsubscribe);
|
|
385
405
|
}
|
|
386
406
|
sseLogger.info("SSE connection established", {
|
|
387
|
-
events:
|
|
407
|
+
events: allowedEvents,
|
|
388
408
|
subscriptionCount: unsubscribes.length
|
|
389
409
|
});
|
|
390
410
|
await stream.writeSSE({
|
|
391
411
|
event: "connected",
|
|
392
412
|
data: JSON.stringify({
|
|
393
|
-
subscribedEvents:
|
|
413
|
+
subscribedEvents: allowedEvents,
|
|
394
414
|
timestamp: Date.now()
|
|
395
415
|
})
|
|
396
416
|
});
|
|
@@ -407,7 +427,7 @@ function createSSEHandler(router, config2 = {}) {
|
|
|
407
427
|
clearInterval(pingTimer);
|
|
408
428
|
unsubscribes.forEach((fn) => fn());
|
|
409
429
|
sseLogger.info("SSE connection closed", {
|
|
410
|
-
events:
|
|
430
|
+
events: allowedEvents
|
|
411
431
|
});
|
|
412
432
|
}, async (err) => {
|
|
413
433
|
sseLogger.error("SSE stream error", {
|
|
@@ -416,6 +436,99 @@ function createSSEHandler(router, config2 = {}) {
|
|
|
416
436
|
});
|
|
417
437
|
};
|
|
418
438
|
}
|
|
439
|
+
async function authenticateToken(c, tokenManager) {
|
|
440
|
+
if (!tokenManager) {
|
|
441
|
+
return void 0;
|
|
442
|
+
}
|
|
443
|
+
const token = c.req.query("token");
|
|
444
|
+
if (!token) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
return await tokenManager.verify(token);
|
|
448
|
+
}
|
|
449
|
+
function parseRequestedEvents(c) {
|
|
450
|
+
const eventsParam = c.req.query("events");
|
|
451
|
+
if (!eventsParam) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
return eventsParam.split(",").map((e) => e.trim());
|
|
455
|
+
}
|
|
456
|
+
async function authorizeEvents(subject, requestedEvents, authConfig) {
|
|
457
|
+
if (!subject || !authConfig?.authorize) {
|
|
458
|
+
return requestedEvents;
|
|
459
|
+
}
|
|
460
|
+
const allowed = await authConfig.authorize(subject, requestedEvents);
|
|
461
|
+
if (allowed.length === 0) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
return allowed;
|
|
465
|
+
}
|
|
466
|
+
var InMemoryTokenStore = class {
|
|
467
|
+
tokens = /* @__PURE__ */ new Map();
|
|
468
|
+
async set(token, data) {
|
|
469
|
+
this.tokens.set(token, data);
|
|
470
|
+
}
|
|
471
|
+
async consume(token) {
|
|
472
|
+
const data = this.tokens.get(token);
|
|
473
|
+
if (!data) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
this.tokens.delete(token);
|
|
477
|
+
return data;
|
|
478
|
+
}
|
|
479
|
+
async cleanup() {
|
|
480
|
+
const now = Date.now();
|
|
481
|
+
for (const [token, data] of this.tokens) {
|
|
482
|
+
if (data.expiresAt <= now) {
|
|
483
|
+
this.tokens.delete(token);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
var SSETokenManager = class {
|
|
489
|
+
store;
|
|
490
|
+
ttl;
|
|
491
|
+
cleanupTimer = null;
|
|
492
|
+
constructor(config2) {
|
|
493
|
+
this.ttl = config2?.ttl ?? 3e4;
|
|
494
|
+
this.store = config2?.store ?? new InMemoryTokenStore();
|
|
495
|
+
const cleanupInterval = config2?.cleanupInterval ?? 6e4;
|
|
496
|
+
this.cleanupTimer = setInterval(() => void this.store.cleanup(), cleanupInterval);
|
|
497
|
+
this.cleanupTimer.unref();
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Issue a new one-time-use token for the given subject
|
|
501
|
+
*/
|
|
502
|
+
async issue(subject) {
|
|
503
|
+
const token = randomBytes(32).toString("hex");
|
|
504
|
+
await this.store.set(token, {
|
|
505
|
+
token,
|
|
506
|
+
subject,
|
|
507
|
+
expiresAt: Date.now() + this.ttl
|
|
508
|
+
});
|
|
509
|
+
return token;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Verify and consume a token
|
|
513
|
+
* @returns subject string if valid, null if invalid/expired/already consumed
|
|
514
|
+
*/
|
|
515
|
+
async verify(token) {
|
|
516
|
+
const data = await this.store.consume(token);
|
|
517
|
+
if (!data || data.expiresAt <= Date.now()) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
return data.subject;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Cleanup timer and resources
|
|
524
|
+
*/
|
|
525
|
+
destroy() {
|
|
526
|
+
if (this.cleanupTimer) {
|
|
527
|
+
clearInterval(this.cleanupTimer);
|
|
528
|
+
this.cleanupTimer = null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
};
|
|
419
532
|
function createHealthCheckHandler(detailed) {
|
|
420
533
|
return async (c) => {
|
|
421
534
|
const response = {
|
|
@@ -641,13 +754,36 @@ function registerSSEEndpoint(app, config2) {
|
|
|
641
754
|
return;
|
|
642
755
|
}
|
|
643
756
|
const eventsConfig = config2.eventsConfig ?? {};
|
|
644
|
-
const
|
|
757
|
+
const streamPath = eventsConfig.path ?? "/events/stream";
|
|
758
|
+
const authConfig = eventsConfig.auth;
|
|
645
759
|
const debug = isDebugMode(config2);
|
|
646
|
-
|
|
760
|
+
let tokenManager;
|
|
761
|
+
if (authConfig?.enabled) {
|
|
762
|
+
tokenManager = new SSETokenManager({
|
|
763
|
+
ttl: authConfig.tokenTtl,
|
|
764
|
+
store: authConfig.store
|
|
765
|
+
});
|
|
766
|
+
const tokenPath = streamPath.replace(/\/[^/]+$/, "/token");
|
|
767
|
+
const mwHandlers = (config2.middlewares ?? []).map((mw) => mw.handler);
|
|
768
|
+
const getSubject = authConfig.getSubject ?? ((c) => c.get("auth")?.userId ?? null);
|
|
769
|
+
app.post(tokenPath, ...mwHandlers, async (c) => {
|
|
770
|
+
const subject = getSubject(c);
|
|
771
|
+
if (!subject) {
|
|
772
|
+
return c.json({ error: "Unable to identify subject" }, 401);
|
|
773
|
+
}
|
|
774
|
+
const token = await tokenManager.issue(subject);
|
|
775
|
+
return c.json({ token });
|
|
776
|
+
});
|
|
777
|
+
if (debug) {
|
|
778
|
+
serverLogger.info(`\u2713 SSE token endpoint registered at POST ${tokenPath}`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
app.get(streamPath, createSSEHandler(config2.events, eventsConfig, tokenManager));
|
|
647
782
|
if (debug) {
|
|
648
783
|
const eventNames = config2.events.eventNames;
|
|
649
|
-
serverLogger.info(`\u2713 SSE endpoint registered at ${
|
|
650
|
-
events: eventNames
|
|
784
|
+
serverLogger.info(`\u2713 SSE endpoint registered at ${streamPath}`, {
|
|
785
|
+
events: eventNames,
|
|
786
|
+
auth: !!authConfig?.enabled
|
|
651
787
|
});
|
|
652
788
|
}
|
|
653
789
|
}
|