@spfn/core 0.3.0-beta.18 → 0.3.0-beta.19
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/codegen/index.d.ts +20 -0
- package/dist/codegen/index.js +68 -10
- package/dist/codegen/index.js.map +1 -1
- package/dist/contract/index.d.ts +37 -15
- package/dist/contract/index.js +147 -23
- package/dist/contract/index.js.map +1 -1
- package/dist/event/index.d.ts +4 -3
- package/dist/event/index.js +32 -1
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +3 -2
- package/dist/event/sse/index.d.ts +5 -4
- package/dist/event/ws/client.d.ts +3 -2
- package/dist/event/ws/index.d.ts +4 -3
- package/dist/ops/index.d.ts +1 -1
- package/dist/{token-manager-BT5EnUAR.d.ts → router-CysBmgv3.d.ts} +81 -127
- package/dist/server/index.d.ts +4 -3
- package/dist/server/index.js +26 -2
- package/dist/server/index.js.map +1 -1
- package/dist/token-manager-Ckjt2-qq.d.ts +122 -0
- package/dist/{types-ClQVomgV.d.ts → types-B4DuGCyM.d.ts} +42 -3
- package/dist/{types-ZQODsBft.d.ts → types-B8zAxFsE.d.ts} +2 -1
- package/dist/{types-2AbaW4Ie.d.ts → types-W4u_S0lF.d.ts} +4 -3
- package/package.json +1 -1
- package/src/codegen/README.md +11 -2
- package/src/contract/README.md +113 -5
- package/src/event/README.md +48 -0
package/dist/event/ws/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { e as WSMessageHandlers, W as WSRouterDef, f as WSHandlerConfig } from '../../types-
|
|
3
|
-
export { g as WSAuthConfig, d as WSClientConfig, c as WSConnectionState, h as WSEventHandlers, i as WSHandlerAuthConfig, j as WSMessageContext, k as WSMessageHandlerFn, l as WSRawConnection, a as WSSubscribeOptions, b as WSUnsubscribe } from '../../types-
|
|
1
|
+
import { a as EventDef } from '../../router-CysBmgv3.js';
|
|
2
|
+
import { e as WSMessageHandlers, W as WSRouterDef, f as WSHandlerConfig } from '../../types-W4u_S0lF.js';
|
|
3
|
+
export { g as WSAuthConfig, d as WSClientConfig, c as WSConnectionState, h as WSEventHandlers, i as WSHandlerAuthConfig, j as WSMessageContext, k as WSMessageHandlerFn, l as WSRawConnection, a as WSSubscribeOptions, b as WSUnsubscribe } from '../../types-W4u_S0lF.js';
|
|
4
4
|
import { Server } from 'node:http';
|
|
5
|
+
import { S as SSETokenManager } from '../../token-manager-Ckjt2-qq.js';
|
|
5
6
|
import '@sinclair/typebox';
|
|
6
7
|
import 'hono';
|
|
7
8
|
|
package/dist/ops/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { MiddlewareHandler } from 'hono';
|
|
|
2
2
|
import { a as NamedMiddleware } from '../define-middleware-CVKgqo8S.js';
|
|
3
3
|
import { R as RouteDef, a as RouteBuilder } from '../route-builder-2ani2jEI.js';
|
|
4
4
|
import { R as Router } from '../router-BqyEARoY.js';
|
|
5
|
-
import { J as JsonSchema } from '../types-
|
|
5
|
+
import { J as JsonSchema } from '../types-B4DuGCyM.js';
|
|
6
6
|
import { HttpMethod } from '../route/types.js';
|
|
7
7
|
import '@sinclair/typebox';
|
|
8
8
|
import 'hono/utils/http-status';
|
|
@@ -25,6 +25,21 @@ type EventHandler<TPayload> = (payload: TPayload) => void | Promise<void>;
|
|
|
25
25
|
* Job queue sender function type (used by job module)
|
|
26
26
|
*/
|
|
27
27
|
type JobQueueSender = (queueName: string, payload: unknown) => Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* What a contracted event promises to clients compiled separately from the
|
|
30
|
+
* server, written into `contracts/current.json` by `@spfn/core:contract`.
|
|
31
|
+
*
|
|
32
|
+
* The same version markers a route's `.contract()` carries; the payload shape
|
|
33
|
+
* is the event's own schema, so it is not repeated here.
|
|
34
|
+
*/
|
|
35
|
+
interface EventContract {
|
|
36
|
+
/** Contract version the event first appeared in. */
|
|
37
|
+
since: string;
|
|
38
|
+
/** Contract version the event was announced for removal in. */
|
|
39
|
+
deprecatedIn?: string;
|
|
40
|
+
/** Contract version the event was removed in. */
|
|
41
|
+
removedIn?: string;
|
|
42
|
+
}
|
|
28
43
|
/**
|
|
29
44
|
* Event definition interface
|
|
30
45
|
*/
|
|
@@ -37,6 +52,22 @@ interface EventDef<TPayload = void> {
|
|
|
37
52
|
* TypeBox payload schema (optional)
|
|
38
53
|
*/
|
|
39
54
|
readonly schema?: TSchema;
|
|
55
|
+
/**
|
|
56
|
+
* The contract declared with `.contract()`, absent when the event is not
|
|
57
|
+
* part of the published contract.
|
|
58
|
+
*
|
|
59
|
+
* Named with an underscore because `.contract()` holds the plain name, as
|
|
60
|
+
* a router's `_contractVersion` sits beside `.contractVersion()`.
|
|
61
|
+
*/
|
|
62
|
+
readonly _contract?: EventContract;
|
|
63
|
+
/**
|
|
64
|
+
* Publish this event in `contracts/current.json`.
|
|
65
|
+
*
|
|
66
|
+
* Sets the contract on this definition and returns it. An event is defined
|
|
67
|
+
* once and shared, so a second call throws instead of one of the two
|
|
68
|
+
* declarations silently winning.
|
|
69
|
+
*/
|
|
70
|
+
contract: (contract: EventContract) => EventDef<TPayload>;
|
|
40
71
|
/**
|
|
41
72
|
* Subscribe to this event (in-memory handler)
|
|
42
73
|
*/
|
|
@@ -104,9 +135,32 @@ type InferEventPayload$1<TEvent> = TEvent extends EventDef<infer TPayload> ? TPa
|
|
|
104
135
|
*/
|
|
105
136
|
|
|
106
137
|
/**
|
|
107
|
-
*
|
|
138
|
+
* How a client reaches the contracted events of a router.
|
|
139
|
+
*
|
|
140
|
+
* Declared on the router because it is the stream's promise, not one event's:
|
|
141
|
+
* `createServer` checks it against `.events()` at boot, so the document cannot
|
|
142
|
+
* describe a path or an auth mode the server does not serve.
|
|
108
143
|
*/
|
|
109
|
-
interface
|
|
144
|
+
interface EventRouterContract {
|
|
145
|
+
/**
|
|
146
|
+
* `'tokenExchange'` when `.events()` enables `auth` — the client first posts
|
|
147
|
+
* to the token path and opens the stream with `?token=`. `'none'` otherwise.
|
|
148
|
+
*/
|
|
149
|
+
auth: 'none' | 'tokenExchange';
|
|
150
|
+
/**
|
|
151
|
+
* The stream path `.events()` registers.
|
|
152
|
+
* @default '/events/stream'
|
|
153
|
+
*/
|
|
154
|
+
streamPath?: string;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* What every router of events carries — the SSE event router and the
|
|
158
|
+
* WebSocket router alike.
|
|
159
|
+
*
|
|
160
|
+
* The WebSocket router builds on this rather than on `EventRouterDef`, because
|
|
161
|
+
* only an SSE event router can be contracted.
|
|
162
|
+
*/
|
|
163
|
+
interface EventRouterBase<TEvents extends Record<string, EventDef<any>>> {
|
|
110
164
|
/**
|
|
111
165
|
* Event definitions
|
|
112
166
|
*/
|
|
@@ -122,21 +176,42 @@ interface EventRouterDef<TEvents extends Record<string, EventDef<any>>> {
|
|
|
122
176
|
[K in keyof TEvents]: TEvents[K]['_payload'];
|
|
123
177
|
};
|
|
124
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Event Router Definition
|
|
181
|
+
*/
|
|
182
|
+
interface EventRouterDef<TEvents extends Record<string, EventDef<any>>> extends EventRouterBase<TEvents> {
|
|
183
|
+
/**
|
|
184
|
+
* The contract declared with `.contract()`, absent when the router declares
|
|
185
|
+
* none. Underscored for the same reason as `EventDef._contract`.
|
|
186
|
+
*/
|
|
187
|
+
readonly _contract?: EventRouterContract;
|
|
188
|
+
/**
|
|
189
|
+
* Declare how a client reaches this router's contracted events.
|
|
190
|
+
*
|
|
191
|
+
* Sets the contract on this router and returns it. A `streamPath` that does
|
|
192
|
+
* not start with `/` throws here, where the typo is.
|
|
193
|
+
*/
|
|
194
|
+
contract: (contract: EventRouterContract) => EventRouterDef<TEvents>;
|
|
195
|
+
}
|
|
125
196
|
/**
|
|
126
197
|
* Infer event names from EventRouter
|
|
127
198
|
*/
|
|
128
|
-
type InferEventNames<T> = T extends
|
|
199
|
+
type InferEventNames<T> = T extends EventRouterBase<infer E> ? keyof E & string : never;
|
|
129
200
|
/**
|
|
130
201
|
* Infer payload type for specific event
|
|
131
202
|
*/
|
|
132
|
-
type InferEventPayload<T extends
|
|
203
|
+
type InferEventPayload<T extends EventRouterBase<any>, K extends InferEventNames<T>> = T['_types'][K];
|
|
133
204
|
/**
|
|
134
205
|
* Infer all event payloads map
|
|
135
206
|
*/
|
|
136
|
-
type InferEventPayloads<T extends
|
|
207
|
+
type InferEventPayloads<T extends EventRouterBase<any>> = T['_types'];
|
|
137
208
|
/**
|
|
138
209
|
* Define an event router for SSE subscription
|
|
139
210
|
*
|
|
211
|
+
* `.contract({ auth })` on the router, together with `.contract({ since })` on
|
|
212
|
+
* its events, publishes those events in `contracts/current.json` — see the
|
|
213
|
+
* contract README.
|
|
214
|
+
*
|
|
140
215
|
* @example
|
|
141
216
|
* ```typescript
|
|
142
217
|
* export const eventRouter = defineEventRouter({
|
|
@@ -154,125 +229,4 @@ type InferEventPayloads<T extends EventRouterDef<any>> = T['_types'];
|
|
|
154
229
|
*/
|
|
155
230
|
declare function defineEventRouter<TEvents extends Record<string, EventDef<any>>>(events: TEvents): EventRouterDef<TEvents>;
|
|
156
231
|
|
|
157
|
-
|
|
158
|
-
* SSE Token Manager
|
|
159
|
-
*
|
|
160
|
-
* Auth-agnostic token issuance and verification for SSE connections.
|
|
161
|
-
* Issues one-time-use tokens with TTL for Token Exchange pattern.
|
|
162
|
-
*
|
|
163
|
-
* @example
|
|
164
|
-
* ```typescript
|
|
165
|
-
* const manager = new SSETokenManager({ ttl: 30000 });
|
|
166
|
-
*
|
|
167
|
-
* // Issue token for authenticated user
|
|
168
|
-
* const token = await manager.issue('user-123');
|
|
169
|
-
*
|
|
170
|
-
* // Verify and consume token (one-time use)
|
|
171
|
-
* const subject = await manager.verify(token); // 'user-123'
|
|
172
|
-
* const again = await manager.verify(token); // null (already consumed)
|
|
173
|
-
*
|
|
174
|
-
* // Cleanup on shutdown
|
|
175
|
-
* manager.destroy();
|
|
176
|
-
* ```
|
|
177
|
-
*/
|
|
178
|
-
/**
|
|
179
|
-
* Minimal cache client interface (compatible with ioredis Redis | Cluster)
|
|
180
|
-
*/
|
|
181
|
-
type CacheClient = {
|
|
182
|
-
set(key: string, value: string, ...args: any[]): Promise<any>;
|
|
183
|
-
getdel?(key: string): Promise<string | null>;
|
|
184
|
-
get(key: string): Promise<string | null>;
|
|
185
|
-
del(...keys: string[]): Promise<number>;
|
|
186
|
-
};
|
|
187
|
-
/**
|
|
188
|
-
* Stored SSE token data
|
|
189
|
-
*/
|
|
190
|
-
interface SSEToken {
|
|
191
|
-
subject: string;
|
|
192
|
-
expiresAt: number;
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Token storage interface
|
|
196
|
-
*
|
|
197
|
-
* Implement this for custom storage backends (e.g., Redis for multi-instance).
|
|
198
|
-
*/
|
|
199
|
-
interface SSETokenStore {
|
|
200
|
-
/** Store a token */
|
|
201
|
-
set(token: string, data: SSEToken): Promise<void>;
|
|
202
|
-
/** Get and delete a token (one-time use) */
|
|
203
|
-
consume(token: string): Promise<SSEToken | null>;
|
|
204
|
-
/** Remove expired tokens */
|
|
205
|
-
cleanup(): Promise<void>;
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* SSETokenManager configuration
|
|
209
|
-
*/
|
|
210
|
-
interface SSETokenManagerConfig {
|
|
211
|
-
/**
|
|
212
|
-
* Token time-to-live in milliseconds
|
|
213
|
-
* @default 30000
|
|
214
|
-
*/
|
|
215
|
-
ttl?: number;
|
|
216
|
-
/**
|
|
217
|
-
* Custom token store (default: in-memory Map)
|
|
218
|
-
*/
|
|
219
|
-
store?: SSETokenStore;
|
|
220
|
-
/**
|
|
221
|
-
* Cleanup interval in milliseconds
|
|
222
|
-
* @default 60000
|
|
223
|
-
*/
|
|
224
|
-
cleanupInterval?: number;
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Redis/Valkey-backed token store for multi-instance deployments.
|
|
228
|
-
*
|
|
229
|
-
* Uses SET EX for automatic TTL expiry and GETDEL for atomic one-time consumption.
|
|
230
|
-
* No cleanup needed — Redis handles expiration automatically.
|
|
231
|
-
*
|
|
232
|
-
* @example
|
|
233
|
-
* ```typescript
|
|
234
|
-
* import { getCache } from '@spfn/core/cache';
|
|
235
|
-
*
|
|
236
|
-
* const cache = getCache();
|
|
237
|
-
* if (cache) {
|
|
238
|
-
* const store = new CacheTokenStore(cache);
|
|
239
|
-
* const manager = new SSETokenManager({ store });
|
|
240
|
-
* }
|
|
241
|
-
* ```
|
|
242
|
-
*/
|
|
243
|
-
declare class CacheTokenStore implements SSETokenStore {
|
|
244
|
-
private cache;
|
|
245
|
-
private prefix;
|
|
246
|
-
constructor(cache: CacheClient);
|
|
247
|
-
set(token: string, data: SSEToken): Promise<void>;
|
|
248
|
-
consume(token: string): Promise<SSEToken | null>;
|
|
249
|
-
cleanup(): Promise<void>;
|
|
250
|
-
}
|
|
251
|
-
declare class SSETokenManager {
|
|
252
|
-
private store;
|
|
253
|
-
private ttl;
|
|
254
|
-
private cleanupTimer;
|
|
255
|
-
constructor(config?: SSETokenManagerConfig);
|
|
256
|
-
/**
|
|
257
|
-
* How long an issued token stays valid, in milliseconds.
|
|
258
|
-
*
|
|
259
|
-
* Exposed so a caller that has to report an expiry to its client computes the same
|
|
260
|
-
* number this manager stamps on the token, instead of assuming the default.
|
|
261
|
-
*/
|
|
262
|
-
get ttlMs(): number;
|
|
263
|
-
/**
|
|
264
|
-
* Issue a new one-time-use token for the given subject
|
|
265
|
-
*/
|
|
266
|
-
issue(subject: string): Promise<string>;
|
|
267
|
-
/**
|
|
268
|
-
* Verify and consume a token
|
|
269
|
-
* @returns subject string if valid, null if invalid/expired/already consumed
|
|
270
|
-
*/
|
|
271
|
-
verify(token: string): Promise<string | null>;
|
|
272
|
-
/**
|
|
273
|
-
* Cleanup timer and resources
|
|
274
|
-
*/
|
|
275
|
-
destroy(): void;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export { CacheTokenStore as C, type EventDef as E, type InferEventNames as I, type JobQueueSender as J, type PubSubCache as P, SSETokenManager as S, type EventHandler as a, type EventRouterDef as b, type InferEventPayload$1 as c, type InferEventPayloads as d, type InferEventPayload as e, defineEventRouter as f, type SSEToken as g, type SSETokenManagerConfig as h, type SSETokenStore as i };
|
|
232
|
+
export { type EventRouterDef as E, type InferEventNames as I, type JobQueueSender as J, type PubSubCache as P, type EventDef as a, type EventContract as b, type EventHandler as c, type EventRouterContract as d, type InferEventPayload$1 as e, type InferEventPayloads as f, type InferEventPayload as g, defineEventRouter as h, type EventRouterBase as i };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -6,15 +6,16 @@ import { NamedMiddleware, Router } from '@spfn/core/route';
|
|
|
6
6
|
import { OnErrorContext, ProxyGuardConfig, RateLimitOptions } from '@spfn/core/middleware';
|
|
7
7
|
import { SafeFetchPolicy } from '@spfn/core/security';
|
|
8
8
|
import { c as JobRouter, f as BossOptions } from '../boss-BiDCbbAQ.js';
|
|
9
|
-
import {
|
|
10
|
-
import { d as SSEHandlerConfig, e as SSEAuthConfig } from '../types-
|
|
11
|
-
import { W as WSRouterDef, f as WSHandlerConfig, e as WSMessageHandlers, g as WSAuthConfig } from '../types-
|
|
9
|
+
import { E as EventRouterDef, a as EventDef } from '../router-CysBmgv3.js';
|
|
10
|
+
import { d as SSEHandlerConfig, e as SSEAuthConfig } from '../types-B8zAxFsE.js';
|
|
11
|
+
import { W as WSRouterDef, f as WSHandlerConfig, e as WSMessageHandlers, g as WSAuthConfig } from '../types-W4u_S0lF.js';
|
|
12
12
|
import { DatabaseProvider, MigrationStatus, MigrationStatusDb } from '@spfn/core/db';
|
|
13
13
|
import * as _sinclair_typebox from '@sinclair/typebox';
|
|
14
14
|
import { Static } from '@sinclair/typebox';
|
|
15
15
|
import { R as RouteDef } from '../route-builder-2ani2jEI.js';
|
|
16
16
|
import { a as EnvRegistry } from '../registry-C2KQCh0m.js';
|
|
17
17
|
import 'pg-boss';
|
|
18
|
+
import '../token-manager-Ckjt2-qq.js';
|
|
18
19
|
import '../define-middleware-CVKgqo8S.js';
|
|
19
20
|
import 'hono/utils/http-status';
|
|
20
21
|
import '../route/types.js';
|
package/dist/server/index.js
CHANGED
|
@@ -644,6 +644,12 @@ async function authorizeEvents(subject, requestedEvents, authConfig) {
|
|
|
644
644
|
}
|
|
645
645
|
return allowed;
|
|
646
646
|
}
|
|
647
|
+
|
|
648
|
+
// src/event/sse/paths.ts
|
|
649
|
+
var DEFAULT_SSE_STREAM_PATH = "/events/stream";
|
|
650
|
+
function sseTokenPath(streamPath) {
|
|
651
|
+
return streamPath.replace(/\/[^/]+$/, "/token");
|
|
652
|
+
}
|
|
647
653
|
function hashToken(token) {
|
|
648
654
|
return createHash("sha256").update(token).digest("hex");
|
|
649
655
|
}
|
|
@@ -2126,8 +2132,9 @@ async function registerSSEEndpoint(app, config) {
|
|
|
2126
2132
|
return;
|
|
2127
2133
|
}
|
|
2128
2134
|
const eventsConfig = config.eventsConfig ?? {};
|
|
2129
|
-
const streamPath = eventsConfig.path ??
|
|
2135
|
+
const streamPath = eventsConfig.path ?? DEFAULT_SSE_STREAM_PATH;
|
|
2130
2136
|
const authConfig = eventsConfig.auth;
|
|
2137
|
+
assertEventContractServed(config.events._contract, streamPath, !!authConfig?.enabled);
|
|
2131
2138
|
const debug = isDebugMode(config);
|
|
2132
2139
|
let tokenManager;
|
|
2133
2140
|
if (authConfig?.enabled) {
|
|
@@ -2150,7 +2157,7 @@ async function registerSSEEndpoint(app, config) {
|
|
|
2150
2157
|
ttl: authConfig.tokenTtl,
|
|
2151
2158
|
store
|
|
2152
2159
|
});
|
|
2153
|
-
const tokenPath = streamPath
|
|
2160
|
+
const tokenPath = sseTokenPath(streamPath);
|
|
2154
2161
|
const mwHandlers = resolveEndpointMiddlewares(config).map((mw) => mw.handler);
|
|
2155
2162
|
const getSubject = authConfig.getSubject ?? ((c) => c.get("auth")?.userId ?? null);
|
|
2156
2163
|
app.on(["POST"], [tokenPath], ...mwHandlers, async (c) => {
|
|
@@ -2179,6 +2186,23 @@ async function registerSSEEndpoint(app, config) {
|
|
|
2179
2186
|
});
|
|
2180
2187
|
}
|
|
2181
2188
|
}
|
|
2189
|
+
function assertEventContractServed(contract, streamPath, authEnabled) {
|
|
2190
|
+
if (!contract) {
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
const contractPath = contract.streamPath ?? DEFAULT_SSE_STREAM_PATH;
|
|
2194
|
+
if (contractPath !== streamPath) {
|
|
2195
|
+
throw new Error(
|
|
2196
|
+
`SSE event router contract declares streamPath ${contractPath} but .events() registers ${streamPath}; the contract would describe a path the server does not serve`
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
const servedAuth = authEnabled ? "tokenExchange" : "none";
|
|
2200
|
+
if (contract.auth !== servedAuth) {
|
|
2201
|
+
throw new Error(
|
|
2202
|
+
`SSE event router contract declares auth ${contract.auth} but .events() serves auth ${servedAuth}; the contract would describe an auth mode the server does not serve`
|
|
2203
|
+
);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2182
2206
|
function isDebugMode(config) {
|
|
2183
2207
|
return config?.debug ?? process.env.NODE_ENV === "development";
|
|
2184
2208
|
}
|