@spfn/core 0.2.0-beta.9 → 0.3.0-beta.2
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/LICENSE +1 -1
- package/README.md +467 -305
- package/dist/authz/index.d.ts +34 -0
- package/dist/authz/index.js +810 -0
- package/dist/authz/index.js.map +1 -0
- package/dist/{boss-DI1r4kTS.d.ts → boss-D16fO2oG.d.ts} +41 -1
- package/dist/cache/index.js +42 -30
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +121 -13
- package/dist/codegen/index.js +212 -15
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +615 -6
- package/dist/config/index.js +124 -5
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +220 -0
- package/dist/contract/index.js +558 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/db/index.d.ts +528 -85
- package/dist/db/index.js +831 -122
- package/dist/db/index.js.map +1 -1
- package/dist/define-middleware-DfDP39Nq.d.ts +167 -0
- package/dist/env/index.d.ts +26 -2
- package/dist/env/index.js +15 -5
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +26 -19
- package/dist/env/loader.js +32 -25
- package/dist/env/loader.js.map +1 -1
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.js +418 -5
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +33 -3
- package/dist/event/index.js +24 -3
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +42 -3
- package/dist/event/sse/client.js +128 -45
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +12 -5
- package/dist/event/sse/index.js +280 -32
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +59 -0
- package/dist/event/ws/client.js +273 -0
- package/dist/event/ws/client.js.map +1 -0
- package/dist/event/ws/index.d.ts +94 -0
- package/dist/event/ws/index.js +272 -0
- package/dist/event/ws/index.js.map +1 -0
- package/dist/job/index.d.ts +2 -2
- package/dist/job/index.js +155 -42
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.d.ts +5 -0
- package/dist/logger/index.js +14 -0
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +347 -9
- package/dist/middleware/index.js +1462 -15
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +2 -2
- package/dist/nextjs/index.js +42 -28
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +35 -51
- package/dist/nextjs/server.js +126 -60
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +152 -0
- package/dist/ops/index.js +500 -0
- package/dist/ops/index.js.map +1 -0
- package/dist/route/index.d.ts +8 -694
- package/dist/route/index.js +111 -22
- package/dist/route/index.js.map +1 -1
- package/dist/router-Qbssr11H.d.ts +676 -0
- package/dist/security/index.d.ts +83 -0
- package/dist/security/index.js +173 -0
- package/dist/security/index.js.map +1 -0
- package/dist/server/index.d.ts +491 -22
- package/dist/server/index.js +1887 -308
- package/dist/server/index.js.map +1 -1
- package/dist/token-manager-BT5EnUAR.d.ts +278 -0
- package/dist/types-2AbaW4Ie.d.ts +205 -0
- package/dist/{types-BOPTApC2.d.ts → types-9oszaJqp.d.ts} +7 -2
- package/dist/types-D1c57Ko-.d.ts +115 -0
- package/dist/types-ZQODsBft.d.ts +282 -0
- package/package.json +244 -208
- package/dist/router-Di7ENoah.d.ts +0 -151
- package/dist/types-B-e_f2dQ.d.ts +0 -121
- package/docs/cache.md +0 -133
- package/docs/codegen.md +0 -74
- package/docs/database.md +0 -346
- package/docs/entity.md +0 -539
- package/docs/env.md +0 -477
- package/docs/errors.md +0 -319
- package/docs/event.md +0 -116
- package/docs/job.md +0 -131
- package/docs/logger.md +0 -108
- package/docs/middleware.md +0 -337
- package/docs/nextjs.md +0 -241
- package/docs/repository.md +0 -496
- package/docs/route.md +0 -497
- package/docs/server.md +0 -307
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { TSchema } from '@sinclair/typebox';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Event System Types
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pub/Sub capable cache interface for multi-instance events
|
|
9
|
+
*/
|
|
10
|
+
interface PubSubCache {
|
|
11
|
+
/**
|
|
12
|
+
* Publish a message to a channel
|
|
13
|
+
*/
|
|
14
|
+
publish(channel: string, message: unknown): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Subscribe to a channel
|
|
17
|
+
*/
|
|
18
|
+
subscribe(channel: string, handler: (message: unknown) => void | Promise<void>): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Event handler function type
|
|
22
|
+
*/
|
|
23
|
+
type EventHandler<TPayload> = (payload: TPayload) => void | Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Job queue sender function type (used by job module)
|
|
26
|
+
*/
|
|
27
|
+
type JobQueueSender = (queueName: string, payload: unknown) => Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Event definition interface
|
|
30
|
+
*/
|
|
31
|
+
interface EventDef<TPayload = void> {
|
|
32
|
+
/**
|
|
33
|
+
* Unique event name
|
|
34
|
+
*/
|
|
35
|
+
readonly name: string;
|
|
36
|
+
/**
|
|
37
|
+
* TypeBox payload schema (optional)
|
|
38
|
+
*/
|
|
39
|
+
readonly schema?: TSchema;
|
|
40
|
+
/**
|
|
41
|
+
* Subscribe to this event (in-memory handler)
|
|
42
|
+
*/
|
|
43
|
+
subscribe: (handler: EventHandler<TPayload>) => () => void;
|
|
44
|
+
/**
|
|
45
|
+
* Unsubscribe all handlers
|
|
46
|
+
*/
|
|
47
|
+
unsubscribeAll: () => void;
|
|
48
|
+
/**
|
|
49
|
+
* Emit the event (triggers all subscribers and queued jobs)
|
|
50
|
+
*/
|
|
51
|
+
emit: TPayload extends void ? () => Promise<void> : (payload: TPayload) => Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Enable cache-based pub/sub for multi-instance support
|
|
54
|
+
* Must await before emitting events to ensure subscription is ready
|
|
55
|
+
*/
|
|
56
|
+
useCache: (cache: PubSubCache) => Promise<EventDef<TPayload>>;
|
|
57
|
+
/**
|
|
58
|
+
* Internal: Register a job queue to receive this event
|
|
59
|
+
* Called by job registration system
|
|
60
|
+
*/
|
|
61
|
+
_registerJobQueue: (queueName: string, sender: JobQueueSender) => void;
|
|
62
|
+
/**
|
|
63
|
+
* Internal: Drop the cache binding (cache + subscribed flag).
|
|
64
|
+
* Called by the event cache transport on shutdown so a later useCache can
|
|
65
|
+
* rebind a fresh cache in the same process.
|
|
66
|
+
*/
|
|
67
|
+
_resetCache: () => void;
|
|
68
|
+
/**
|
|
69
|
+
* Type inference helper
|
|
70
|
+
*/
|
|
71
|
+
_payload: TPayload;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Infer payload type from EventDef
|
|
75
|
+
*/
|
|
76
|
+
type InferEventPayload$1<TEvent> = TEvent extends EventDef<infer TPayload> ? TPayload : never;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Event Router
|
|
80
|
+
*
|
|
81
|
+
* Type-safe event router for SSE subscription
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* import { defineEvent, defineEventRouter } from '@spfn/core/event';
|
|
86
|
+
* import { Type } from '@sinclair/typebox';
|
|
87
|
+
*
|
|
88
|
+
* const userCreated = defineEvent('user.created', Type.Object({
|
|
89
|
+
* userId: Type.String(),
|
|
90
|
+
* }));
|
|
91
|
+
*
|
|
92
|
+
* const orderPlaced = defineEvent('order.placed', Type.Object({
|
|
93
|
+
* orderId: Type.String(),
|
|
94
|
+
* amount: Type.Number(),
|
|
95
|
+
* }));
|
|
96
|
+
*
|
|
97
|
+
* export const eventRouter = defineEventRouter({
|
|
98
|
+
* userCreated,
|
|
99
|
+
* orderPlaced,
|
|
100
|
+
* });
|
|
101
|
+
*
|
|
102
|
+
* export type EventRouter = typeof eventRouter;
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Event Router Definition
|
|
108
|
+
*/
|
|
109
|
+
interface EventRouterDef<TEvents extends Record<string, EventDef<any>>> {
|
|
110
|
+
/**
|
|
111
|
+
* Event definitions
|
|
112
|
+
*/
|
|
113
|
+
readonly events: TEvents;
|
|
114
|
+
/**
|
|
115
|
+
* Event names as array
|
|
116
|
+
*/
|
|
117
|
+
readonly eventNames: (keyof TEvents)[];
|
|
118
|
+
/**
|
|
119
|
+
* Type inference helper - payload types by event name
|
|
120
|
+
*/
|
|
121
|
+
readonly _types: {
|
|
122
|
+
[K in keyof TEvents]: TEvents[K]['_payload'];
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Infer event names from EventRouter
|
|
127
|
+
*/
|
|
128
|
+
type InferEventNames<T> = T extends EventRouterDef<infer E> ? keyof E & string : never;
|
|
129
|
+
/**
|
|
130
|
+
* Infer payload type for specific event
|
|
131
|
+
*/
|
|
132
|
+
type InferEventPayload<T extends EventRouterDef<any>, K extends InferEventNames<T>> = T['_types'][K];
|
|
133
|
+
/**
|
|
134
|
+
* Infer all event payloads map
|
|
135
|
+
*/
|
|
136
|
+
type InferEventPayloads<T extends EventRouterDef<any>> = T['_types'];
|
|
137
|
+
/**
|
|
138
|
+
* Define an event router for SSE subscription
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* export const eventRouter = defineEventRouter({
|
|
143
|
+
* userCreated,
|
|
144
|
+
* orderPlaced,
|
|
145
|
+
* });
|
|
146
|
+
*
|
|
147
|
+
* // Type inference
|
|
148
|
+
* type Names = InferEventNames<typeof eventRouter>;
|
|
149
|
+
* // 'userCreated' | 'orderPlaced'
|
|
150
|
+
*
|
|
151
|
+
* type Payload = InferEventPayload<typeof eventRouter, 'userCreated'>;
|
|
152
|
+
* // { userId: string }
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
declare function defineEventRouter<TEvents extends Record<string, EventDef<any>>>(events: TEvents): EventRouterDef<TEvents>;
|
|
156
|
+
|
|
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 };
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
import { E as EventDef, b as EventRouterDef, I as InferEventNames, e as InferEventPayload, i as SSETokenStore, S as SSETokenManager } from './token-manager-BT5EnUAR.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* WebSocket Types
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* WebSocket Router Definition
|
|
10
|
+
*
|
|
11
|
+
* Extends EventRouterDef with client→server message handlers.
|
|
12
|
+
*/
|
|
13
|
+
interface WSRouterDef<TEvents extends Record<string, EventDef<any>>, TMessages extends WSMessageHandlers = WSMessageHandlers> extends EventRouterDef<TEvents> {
|
|
14
|
+
messages: TMessages;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Low-level WS connection handle passed to message handlers
|
|
18
|
+
*/
|
|
19
|
+
interface WSRawConnection {
|
|
20
|
+
send(type: string, payload: unknown): void;
|
|
21
|
+
close(code?: number, reason?: string): void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Context passed to each client→server message handler
|
|
25
|
+
*/
|
|
26
|
+
interface WSMessageContext<TPayload = unknown> {
|
|
27
|
+
payload: TPayload;
|
|
28
|
+
subject?: string;
|
|
29
|
+
ws: WSRawConnection;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Single message handler function
|
|
33
|
+
*/
|
|
34
|
+
type WSMessageHandlerFn<TPayload = unknown> = (ctx: WSMessageContext<TPayload>) => void | Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Map of message type name → handler
|
|
37
|
+
*/
|
|
38
|
+
type WSMessageHandlers = Record<string, WSMessageHandlerFn<any>>;
|
|
39
|
+
/**
|
|
40
|
+
* WebSocket auth configuration (internal, non-generic)
|
|
41
|
+
*/
|
|
42
|
+
interface WSHandlerAuthConfig {
|
|
43
|
+
enabled?: boolean;
|
|
44
|
+
tokenTtl?: number;
|
|
45
|
+
store?: SSETokenStore;
|
|
46
|
+
tokenManager?: SSETokenManager | (() => SSETokenManager);
|
|
47
|
+
/**
|
|
48
|
+
* Extract subject from Hono context (used on token-issue endpoint)
|
|
49
|
+
* @default (c) => c.get('auth')?.userId ?? null
|
|
50
|
+
*/
|
|
51
|
+
getSubject?: (c: Context) => string | null;
|
|
52
|
+
/**
|
|
53
|
+
* Authorize event subscriptions on connect
|
|
54
|
+
* Return allowed events subset. Empty array = 403 rejection.
|
|
55
|
+
*/
|
|
56
|
+
authorize?: (subject: string, events: string[]) => Promise<string[]> | string[];
|
|
57
|
+
/**
|
|
58
|
+
* Per-event payload filter (called on every emission)
|
|
59
|
+
* Return false to skip sending the event to this client.
|
|
60
|
+
*/
|
|
61
|
+
filter?: Record<string, (subject: string, payload: unknown) => boolean>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* WebSocket auth configuration (user-facing, generic)
|
|
65
|
+
*/
|
|
66
|
+
interface WSAuthConfig<TRouter extends WSRouterDef<any, any>> {
|
|
67
|
+
enabled?: boolean;
|
|
68
|
+
tokenTtl?: number;
|
|
69
|
+
store?: SSETokenStore;
|
|
70
|
+
tokenManager?: SSETokenManager | (() => SSETokenManager);
|
|
71
|
+
getSubject?: (c: Context) => string | null;
|
|
72
|
+
authorize?: (subject: string, events: InferEventNames<TRouter>[]) => Promise<InferEventNames<TRouter>[]> | InferEventNames<TRouter>[];
|
|
73
|
+
filter?: {
|
|
74
|
+
[K in InferEventNames<TRouter>]?: (subject: string, payload: InferEventPayload<TRouter, K>) => boolean;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Configuration for the WebSocket server handler
|
|
79
|
+
*/
|
|
80
|
+
interface WSHandlerConfig {
|
|
81
|
+
/**
|
|
82
|
+
* Keep-alive ping interval in ms
|
|
83
|
+
* @default 30000
|
|
84
|
+
*/
|
|
85
|
+
pingInterval?: number;
|
|
86
|
+
/**
|
|
87
|
+
* Cross-pod event broadcast via the cache (Redis/Valkey) pub/sub.
|
|
88
|
+
*
|
|
89
|
+
* When `true` (default) and a cache is configured (`CACHE_URL`), each event
|
|
90
|
+
* is auto-wired so an `emit` on one pod reaches subscribers on every pod.
|
|
91
|
+
* Without a cache this is a no-op (events stay in-process). Set `false` to
|
|
92
|
+
* force in-process even when a cache is present.
|
|
93
|
+
*
|
|
94
|
+
* @default true
|
|
95
|
+
*/
|
|
96
|
+
multiInstance?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Pub/sub channel prefix for cross-pod broadcast.
|
|
99
|
+
*
|
|
100
|
+
* Defaults to env `SPFN_SSE_CHANNEL_PREFIX`, else `spfn:sse:`. Use distinct
|
|
101
|
+
* prefixes to isolate apps/tenants that share one Redis instance.
|
|
102
|
+
*/
|
|
103
|
+
channelPrefix?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Maximum inbound message size in bytes. Frames larger than this are
|
|
106
|
+
* rejected by the ws library before they are buffered or parsed.
|
|
107
|
+
* @default 1048576 (1 MiB)
|
|
108
|
+
*/
|
|
109
|
+
maxPayload?: number;
|
|
110
|
+
/**
|
|
111
|
+
* Backpressure cap: if a connection's outbound buffer (`bufferedAmount`)
|
|
112
|
+
* exceeds this many bytes, the connection is closed (1013) instead of
|
|
113
|
+
* buffering more — a slow consumer cannot drive the process to OOM.
|
|
114
|
+
* @default 1048576 (1 MiB)
|
|
115
|
+
*/
|
|
116
|
+
maxBufferedBytes?: number;
|
|
117
|
+
/**
|
|
118
|
+
* Maximum number of concurrent connections accepted by this server. New
|
|
119
|
+
* connections beyond the cap are rejected with close code 1013.
|
|
120
|
+
* @default 10000
|
|
121
|
+
*/
|
|
122
|
+
maxConnections?: number;
|
|
123
|
+
/**
|
|
124
|
+
* Maximum concurrent connections per authenticated subject (0 = unlimited).
|
|
125
|
+
* @default 0
|
|
126
|
+
*/
|
|
127
|
+
maxConnectionsPerSubject?: number;
|
|
128
|
+
/**
|
|
129
|
+
* Allow-list of `Origin` header values permitted to open a WebSocket. A
|
|
130
|
+
* browser sends Origin on the upgrade, so this blocks cross-site scripts from
|
|
131
|
+
* connecting with the user's ambient cookies (WebSocket has no same-origin
|
|
132
|
+
* policy / CORS of its own). Connections whose Origin isn't listed are closed
|
|
133
|
+
* with 1008. Undefined/empty = no Origin check (default; non-browser clients
|
|
134
|
+
* such as native apps send no Origin and are always allowed).
|
|
135
|
+
*
|
|
136
|
+
* @example ['https://app.example.com', 'https://admin.example.com']
|
|
137
|
+
* @default undefined (disabled)
|
|
138
|
+
*/
|
|
139
|
+
allowedOrigins?: string[];
|
|
140
|
+
/**
|
|
141
|
+
* Authentication and authorization configuration
|
|
142
|
+
*/
|
|
143
|
+
auth?: WSHandlerAuthConfig;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* WebSocket client configuration
|
|
147
|
+
*/
|
|
148
|
+
interface WSClientConfig {
|
|
149
|
+
/**
|
|
150
|
+
* Backend API host URL (ws:// or wss://)
|
|
151
|
+
* @default derived from NEXT_PUBLIC_SPFN_API_URL
|
|
152
|
+
*/
|
|
153
|
+
host?: string;
|
|
154
|
+
/**
|
|
155
|
+
* WS endpoint pathname
|
|
156
|
+
* @default '/ws'
|
|
157
|
+
*/
|
|
158
|
+
pathname?: string;
|
|
159
|
+
/**
|
|
160
|
+
* Auto reconnect on disconnect
|
|
161
|
+
* @default true
|
|
162
|
+
*/
|
|
163
|
+
reconnect?: boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Reconnect delay in ms
|
|
166
|
+
* @default 3000
|
|
167
|
+
*/
|
|
168
|
+
reconnectDelay?: number;
|
|
169
|
+
/**
|
|
170
|
+
* Maximum reconnect attempts (0 = infinite)
|
|
171
|
+
* @default 0
|
|
172
|
+
*/
|
|
173
|
+
maxReconnectAttempts?: number;
|
|
174
|
+
/**
|
|
175
|
+
* Acquire a one-time token before connecting
|
|
176
|
+
*/
|
|
177
|
+
acquireToken?: () => Promise<string>;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* WebSocket connection state
|
|
181
|
+
*/
|
|
182
|
+
type WSConnectionState = 'connecting' | 'open' | 'closed' | 'error';
|
|
183
|
+
/**
|
|
184
|
+
* Event handlers map for WSRouterDef
|
|
185
|
+
*/
|
|
186
|
+
type WSEventHandlers<TRouter extends WSRouterDef<any, any>> = {
|
|
187
|
+
[K in InferEventNames<TRouter>]?: (payload: InferEventPayload<TRouter, K>) => void;
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Subscribe options
|
|
191
|
+
*/
|
|
192
|
+
interface WSSubscribeOptions<TRouter extends WSRouterDef<any, any>> {
|
|
193
|
+
events: InferEventNames<TRouter>[];
|
|
194
|
+
handlers: WSEventHandlers<TRouter>;
|
|
195
|
+
onOpen?: () => void;
|
|
196
|
+
onError?: (error: Event) => void;
|
|
197
|
+
onClose?: () => void;
|
|
198
|
+
onReconnect?: (attempt: number) => void;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Unsubscribe function
|
|
202
|
+
*/
|
|
203
|
+
type WSUnsubscribe = () => void;
|
|
204
|
+
|
|
205
|
+
export type { WSRouterDef as W, WSSubscribeOptions as a, WSUnsubscribe as b, WSConnectionState as c, WSClientConfig as d, WSMessageHandlers as e, WSHandlerConfig as f, WSAuthConfig as g, WSEventHandlers as h, WSHandlerAuthConfig as i, WSMessageContext as j, WSMessageHandlerFn as k, WSRawConnection as l };
|
|
@@ -159,7 +159,7 @@ interface ApiConfig {
|
|
|
159
159
|
/**
|
|
160
160
|
* Request timeout in milliseconds
|
|
161
161
|
*
|
|
162
|
-
* @default
|
|
162
|
+
* @default env.SERVER_TIMEOUT (120000)
|
|
163
163
|
*/
|
|
164
164
|
timeout?: number;
|
|
165
165
|
/**
|
|
@@ -202,6 +202,11 @@ interface ApiConfig {
|
|
|
202
202
|
* Per-call options
|
|
203
203
|
*/
|
|
204
204
|
interface CallOptions {
|
|
205
|
+
/**
|
|
206
|
+
* Request timeout in milliseconds
|
|
207
|
+
* Overrides the global timeout set in ApiConfig
|
|
208
|
+
*/
|
|
209
|
+
timeout?: number;
|
|
205
210
|
/**
|
|
206
211
|
* Additional headers for this request
|
|
207
212
|
*/
|
|
@@ -242,4 +247,4 @@ interface CallOptions {
|
|
|
242
247
|
};
|
|
243
248
|
}
|
|
244
249
|
|
|
245
|
-
export type { ApiConfig as A, CallOptions as C, InferRouteInput as I, RequestInterceptor as R,
|
|
250
|
+
export type { ApiConfig as A, CallOptions as C, InferRouteInput as I, RequestInterceptor as R, SetCookie as S, ResponseInterceptor as a, InferRouteOutput as b, CookieOptions as c, RouterInput as d, RouterOutput as e, StructuredInput as f };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { c as RouteAuthProfile } from './router-Qbssr11H.js';
|
|
2
|
+
import { HttpMethod } from './route/types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Contract Document Types
|
|
6
|
+
*
|
|
7
|
+
* The shape of `contracts/current.json` and of a released snapshot under
|
|
8
|
+
* `contracts/released/<version>.json`.
|
|
9
|
+
*
|
|
10
|
+
* An operation is identified by its **name** — the key it holds in the router —
|
|
11
|
+
* not by method and path. That is what lets a changed path be reported as a
|
|
12
|
+
* broken promise instead of read as one operation disappearing and another
|
|
13
|
+
* appearing.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** A JSON Schema object as TypeBox serializes it. */
|
|
17
|
+
type JsonSchema = Record<string, unknown>;
|
|
18
|
+
/** Request schemas, one per part of the request. */
|
|
19
|
+
interface ContractRequest {
|
|
20
|
+
params?: JsonSchema;
|
|
21
|
+
query?: JsonSchema;
|
|
22
|
+
body?: JsonSchema;
|
|
23
|
+
/**
|
|
24
|
+
* Never produced: a contracted route carrying multipart is refused at
|
|
25
|
+
* collection. Kept because a snapshot released before that rule may still
|
|
26
|
+
* have one, and the compatibility comparison has to read it.
|
|
27
|
+
*/
|
|
28
|
+
formData?: JsonSchema;
|
|
29
|
+
headers?: JsonSchema;
|
|
30
|
+
cookies?: JsonSchema;
|
|
31
|
+
}
|
|
32
|
+
/** One contracted operation. */
|
|
33
|
+
interface ContractOperation {
|
|
34
|
+
/** Router key. The operation's identity across versions. */
|
|
35
|
+
name: string;
|
|
36
|
+
method: HttpMethod;
|
|
37
|
+
path: string;
|
|
38
|
+
/** Contract version the operation first appeared in. */
|
|
39
|
+
since: string;
|
|
40
|
+
auth: RouteAuthProfile;
|
|
41
|
+
requiresSession: boolean;
|
|
42
|
+
/** Present only when the operation is announced for removal. */
|
|
43
|
+
deprecatedIn?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Present only when the operation is gone.
|
|
46
|
+
*
|
|
47
|
+
* A client generated before this version may still call it, so the record
|
|
48
|
+
* outlives the route: without it a caller sees an operation that simply
|
|
49
|
+
* stopped existing, with nothing saying when or that it was announced.
|
|
50
|
+
*/
|
|
51
|
+
removedIn?: string;
|
|
52
|
+
/** What the client sends. */
|
|
53
|
+
request: ContractRequest;
|
|
54
|
+
/**
|
|
55
|
+
* What middleware injects into the request before the handler sees it.
|
|
56
|
+
*
|
|
57
|
+
* A web client never sends these — an interceptor fills them in. A client
|
|
58
|
+
* that talks to the route directly does send them, so they are part of the
|
|
59
|
+
* published request shape and are compared under the request rules.
|
|
60
|
+
*/
|
|
61
|
+
interceptor: ContractRequest;
|
|
62
|
+
/** What the client reads. */
|
|
63
|
+
response: JsonSchema;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* How a client's version is judged against the server's.
|
|
67
|
+
*
|
|
68
|
+
* - `allOrNothing` — one contract version is the whole surface's pass or
|
|
69
|
+
* refusal. Right for an auth primitive, where admitting a client that agrees
|
|
70
|
+
* about part of the admission sequence and not the rest is not a safe middle.
|
|
71
|
+
* - `perOperation` — availability is recorded per operation, so the verdict
|
|
72
|
+
* narrows to the operations a client actually calls. Deleting a response field
|
|
73
|
+
* from one route then stops blocking a client that never calls it.
|
|
74
|
+
*
|
|
75
|
+
* Stated rather than inferred: an app contract and @spfn/auth's mobile contract
|
|
76
|
+
* share this format under different rules, and a consumer must not have to guess
|
|
77
|
+
* which one it is holding.
|
|
78
|
+
*/
|
|
79
|
+
type CompatibilityPolicy = 'allOrNothing' | 'perOperation';
|
|
80
|
+
/** The generated contract. */
|
|
81
|
+
interface ContractDocument {
|
|
82
|
+
/** Shape version of this document, not of the API it describes. */
|
|
83
|
+
documentVersion: 1;
|
|
84
|
+
/**
|
|
85
|
+
* The version this document publishes, from `.contractVersion()` on the
|
|
86
|
+
* router. Absent when the router declares none — the gate still runs, but
|
|
87
|
+
* nothing can be released or announced.
|
|
88
|
+
*/
|
|
89
|
+
contractVersion?: string;
|
|
90
|
+
/** Always `perOperation` for an app contract. See the type. */
|
|
91
|
+
compatibilityPolicy: CompatibilityPolicy;
|
|
92
|
+
/** Sorted by name, so the file does not churn on router reordering. */
|
|
93
|
+
operations: ContractOperation[];
|
|
94
|
+
}
|
|
95
|
+
/** A released snapshot: the document plus the digest that pins it. */
|
|
96
|
+
interface ContractSnapshot {
|
|
97
|
+
version: string;
|
|
98
|
+
/** SHA-256 over the canonical encoding of `document`. */
|
|
99
|
+
sha256: string;
|
|
100
|
+
document: ContractDocument;
|
|
101
|
+
}
|
|
102
|
+
/** What a gate violation is about. */
|
|
103
|
+
type ContractViolationKind = 'operation.removed' | 'operation.path-changed' | 'operation.method-changed' | 'request.required-field-added' | 'request.field-became-required' | 'request.type-changed' | 'response.field-removed' | 'response.field-became-optional' | 'response.type-changed' | 'usage.undecidable' | 'usage.still-called' | 'snapshot.digest-mismatch';
|
|
104
|
+
/** One reason the build refuses. */
|
|
105
|
+
interface ContractViolation {
|
|
106
|
+
kind: ContractViolationKind;
|
|
107
|
+
/** Operation name, when the violation belongs to one. */
|
|
108
|
+
operation?: string;
|
|
109
|
+
/** Where inside the operation, e.g. `request.body.email`. */
|
|
110
|
+
location?: string;
|
|
111
|
+
/** What went wrong, in one line. */
|
|
112
|
+
detail: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type { ContractDocument as C, JsonSchema as J, ContractViolation as a, ContractOperation as b, ContractSnapshot as c, CompatibilityPolicy as d, ContractRequest as e, ContractViolationKind as f };
|