@spfn/core 0.2.0-beta.45 → 0.2.0-beta.47
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/index.d.ts +6 -2
- package/dist/event/index.js +12 -1
- 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 -3
- 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 +213 -0
- package/dist/event/ws/index.js.map +1 -0
- package/dist/server/index.d.ts +58 -2
- package/dist/server/index.js +285 -2
- package/dist/server/index.js.map +1 -1
- package/dist/{router-Di7ENoah.d.ts → token-manager-DSwIDD-_.d.ts} +116 -1
- package/dist/{types-DKQ90YL7.d.ts → types-BOOUBu9l.d.ts} +2 -117
- package/dist/types-FuJb3yrP.d.ts +151 -0
- package/package.json +14 -2
|
@@ -148,4 +148,119 @@ type InferEventPayloads<T extends EventRouterDef<any>> = T['_types'];
|
|
|
148
148
|
*/
|
|
149
149
|
declare function defineEventRouter<TEvents extends Record<string, EventDef<any>>>(events: TEvents): EventRouterDef<TEvents>;
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
/**
|
|
152
|
+
* SSE Token Manager
|
|
153
|
+
*
|
|
154
|
+
* Auth-agnostic token issuance and verification for SSE connections.
|
|
155
|
+
* Issues one-time-use tokens with TTL for Token Exchange pattern.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```typescript
|
|
159
|
+
* const manager = new SSETokenManager({ ttl: 30000 });
|
|
160
|
+
*
|
|
161
|
+
* // Issue token for authenticated user
|
|
162
|
+
* const token = await manager.issue('user-123');
|
|
163
|
+
*
|
|
164
|
+
* // Verify and consume token (one-time use)
|
|
165
|
+
* const subject = await manager.verify(token); // 'user-123'
|
|
166
|
+
* const again = await manager.verify(token); // null (already consumed)
|
|
167
|
+
*
|
|
168
|
+
* // Cleanup on shutdown
|
|
169
|
+
* manager.destroy();
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
/**
|
|
173
|
+
* Minimal cache client interface (compatible with ioredis Redis | Cluster)
|
|
174
|
+
*/
|
|
175
|
+
type CacheClient = {
|
|
176
|
+
set(key: string, value: string, ...args: any[]): Promise<any>;
|
|
177
|
+
getdel?(key: string): Promise<string | null>;
|
|
178
|
+
get(key: string): Promise<string | null>;
|
|
179
|
+
del(key: string | string[]): Promise<number>;
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Stored SSE token data
|
|
183
|
+
*/
|
|
184
|
+
interface SSEToken {
|
|
185
|
+
token: string;
|
|
186
|
+
subject: string;
|
|
187
|
+
expiresAt: number;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Token storage interface
|
|
191
|
+
*
|
|
192
|
+
* Implement this for custom storage backends (e.g., Redis for multi-instance).
|
|
193
|
+
*/
|
|
194
|
+
interface SSETokenStore {
|
|
195
|
+
/** Store a token */
|
|
196
|
+
set(token: string, data: SSEToken): Promise<void>;
|
|
197
|
+
/** Get and delete a token (one-time use) */
|
|
198
|
+
consume(token: string): Promise<SSEToken | null>;
|
|
199
|
+
/** Remove expired tokens */
|
|
200
|
+
cleanup(): Promise<void>;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* SSETokenManager configuration
|
|
204
|
+
*/
|
|
205
|
+
interface SSETokenManagerConfig {
|
|
206
|
+
/**
|
|
207
|
+
* Token time-to-live in milliseconds
|
|
208
|
+
* @default 30000
|
|
209
|
+
*/
|
|
210
|
+
ttl?: number;
|
|
211
|
+
/**
|
|
212
|
+
* Custom token store (default: in-memory Map)
|
|
213
|
+
*/
|
|
214
|
+
store?: SSETokenStore;
|
|
215
|
+
/**
|
|
216
|
+
* Cleanup interval in milliseconds
|
|
217
|
+
* @default 60000
|
|
218
|
+
*/
|
|
219
|
+
cleanupInterval?: number;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Redis/Valkey-backed token store for multi-instance deployments.
|
|
223
|
+
*
|
|
224
|
+
* Uses SET EX for automatic TTL expiry and GETDEL for atomic one-time consumption.
|
|
225
|
+
* No cleanup needed — Redis handles expiration automatically.
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```typescript
|
|
229
|
+
* import { getCache } from '@spfn/core/cache';
|
|
230
|
+
*
|
|
231
|
+
* const cache = getCache();
|
|
232
|
+
* if (cache) {
|
|
233
|
+
* const store = new CacheTokenStore(cache);
|
|
234
|
+
* const manager = new SSETokenManager({ store });
|
|
235
|
+
* }
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
declare class CacheTokenStore implements SSETokenStore {
|
|
239
|
+
private cache;
|
|
240
|
+
private prefix;
|
|
241
|
+
constructor(cache: CacheClient);
|
|
242
|
+
set(token: string, data: SSEToken): Promise<void>;
|
|
243
|
+
consume(token: string): Promise<SSEToken | null>;
|
|
244
|
+
cleanup(): Promise<void>;
|
|
245
|
+
}
|
|
246
|
+
declare class SSETokenManager {
|
|
247
|
+
private store;
|
|
248
|
+
private ttl;
|
|
249
|
+
private cleanupTimer;
|
|
250
|
+
constructor(config?: SSETokenManagerConfig);
|
|
251
|
+
/**
|
|
252
|
+
* Issue a new one-time-use token for the given subject
|
|
253
|
+
*/
|
|
254
|
+
issue(subject: string): Promise<string>;
|
|
255
|
+
/**
|
|
256
|
+
* Verify and consume a token
|
|
257
|
+
* @returns subject string if valid, null if invalid/expired/already consumed
|
|
258
|
+
*/
|
|
259
|
+
verify(token: string): Promise<string | null>;
|
|
260
|
+
/**
|
|
261
|
+
* Cleanup timer and resources
|
|
262
|
+
*/
|
|
263
|
+
destroy(): void;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export { CacheTokenStore as C, type EventRouterDef as E, type InferEventNames as I, type JobQueueSender as J, type PubSubCache as P, SSETokenManager as S, type EventDef as a, type InferEventPayload as b, type InferEventPayloads as c, defineEventRouter as d, type EventHandler as e, type InferEventPayload$1 as f, type SSEToken as g, type SSETokenStore as h, type SSETokenManagerConfig as i };
|
|
@@ -1,120 +1,5 @@
|
|
|
1
1
|
import { Context } from 'hono';
|
|
2
|
-
import { E as EventRouterDef, I as InferEventNames, b as InferEventPayload } from './
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* SSE Token Manager
|
|
6
|
-
*
|
|
7
|
-
* Auth-agnostic token issuance and verification for SSE connections.
|
|
8
|
-
* Issues one-time-use tokens with TTL for Token Exchange pattern.
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```typescript
|
|
12
|
-
* const manager = new SSETokenManager({ ttl: 30000 });
|
|
13
|
-
*
|
|
14
|
-
* // Issue token for authenticated user
|
|
15
|
-
* const token = await manager.issue('user-123');
|
|
16
|
-
*
|
|
17
|
-
* // Verify and consume token (one-time use)
|
|
18
|
-
* const subject = await manager.verify(token); // 'user-123'
|
|
19
|
-
* const again = await manager.verify(token); // null (already consumed)
|
|
20
|
-
*
|
|
21
|
-
* // Cleanup on shutdown
|
|
22
|
-
* manager.destroy();
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
/**
|
|
26
|
-
* Minimal cache client interface (compatible with ioredis Redis | Cluster)
|
|
27
|
-
*/
|
|
28
|
-
type CacheClient = {
|
|
29
|
-
set(key: string, value: string, ...args: any[]): Promise<any>;
|
|
30
|
-
getdel?(key: string): Promise<string | null>;
|
|
31
|
-
get(key: string): Promise<string | null>;
|
|
32
|
-
del(key: string | string[]): Promise<number>;
|
|
33
|
-
};
|
|
34
|
-
/**
|
|
35
|
-
* Stored SSE token data
|
|
36
|
-
*/
|
|
37
|
-
interface SSEToken {
|
|
38
|
-
token: string;
|
|
39
|
-
subject: string;
|
|
40
|
-
expiresAt: number;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Token storage interface
|
|
44
|
-
*
|
|
45
|
-
* Implement this for custom storage backends (e.g., Redis for multi-instance).
|
|
46
|
-
*/
|
|
47
|
-
interface SSETokenStore {
|
|
48
|
-
/** Store a token */
|
|
49
|
-
set(token: string, data: SSEToken): Promise<void>;
|
|
50
|
-
/** Get and delete a token (one-time use) */
|
|
51
|
-
consume(token: string): Promise<SSEToken | null>;
|
|
52
|
-
/** Remove expired tokens */
|
|
53
|
-
cleanup(): Promise<void>;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* SSETokenManager configuration
|
|
57
|
-
*/
|
|
58
|
-
interface SSETokenManagerConfig {
|
|
59
|
-
/**
|
|
60
|
-
* Token time-to-live in milliseconds
|
|
61
|
-
* @default 30000
|
|
62
|
-
*/
|
|
63
|
-
ttl?: number;
|
|
64
|
-
/**
|
|
65
|
-
* Custom token store (default: in-memory Map)
|
|
66
|
-
*/
|
|
67
|
-
store?: SSETokenStore;
|
|
68
|
-
/**
|
|
69
|
-
* Cleanup interval in milliseconds
|
|
70
|
-
* @default 60000
|
|
71
|
-
*/
|
|
72
|
-
cleanupInterval?: number;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Redis/Valkey-backed token store for multi-instance deployments.
|
|
76
|
-
*
|
|
77
|
-
* Uses SET EX for automatic TTL expiry and GETDEL for atomic one-time consumption.
|
|
78
|
-
* No cleanup needed — Redis handles expiration automatically.
|
|
79
|
-
*
|
|
80
|
-
* @example
|
|
81
|
-
* ```typescript
|
|
82
|
-
* import { getCache } from '@spfn/core/cache';
|
|
83
|
-
*
|
|
84
|
-
* const cache = getCache();
|
|
85
|
-
* if (cache) {
|
|
86
|
-
* const store = new CacheTokenStore(cache);
|
|
87
|
-
* const manager = new SSETokenManager({ store });
|
|
88
|
-
* }
|
|
89
|
-
* ```
|
|
90
|
-
*/
|
|
91
|
-
declare class CacheTokenStore implements SSETokenStore {
|
|
92
|
-
private cache;
|
|
93
|
-
private prefix;
|
|
94
|
-
constructor(cache: CacheClient);
|
|
95
|
-
set(token: string, data: SSEToken): Promise<void>;
|
|
96
|
-
consume(token: string): Promise<SSEToken | null>;
|
|
97
|
-
cleanup(): Promise<void>;
|
|
98
|
-
}
|
|
99
|
-
declare class SSETokenManager {
|
|
100
|
-
private store;
|
|
101
|
-
private ttl;
|
|
102
|
-
private cleanupTimer;
|
|
103
|
-
constructor(config?: SSETokenManagerConfig);
|
|
104
|
-
/**
|
|
105
|
-
* Issue a new one-time-use token for the given subject
|
|
106
|
-
*/
|
|
107
|
-
issue(subject: string): Promise<string>;
|
|
108
|
-
/**
|
|
109
|
-
* Verify and consume a token
|
|
110
|
-
* @returns subject string if valid, null if invalid/expired/already consumed
|
|
111
|
-
*/
|
|
112
|
-
verify(token: string): Promise<string | null>;
|
|
113
|
-
/**
|
|
114
|
-
* Cleanup timer and resources
|
|
115
|
-
*/
|
|
116
|
-
destroy(): void;
|
|
117
|
-
}
|
|
2
|
+
import { h as SSETokenStore, S as SSETokenManager, E as EventRouterDef, I as InferEventNames, b as InferEventPayload } from './token-manager-DSwIDD-_.js';
|
|
118
3
|
|
|
119
4
|
/**
|
|
120
5
|
* SSE Types
|
|
@@ -369,4 +254,4 @@ type SSEConnectionState = 'connecting' | 'open' | 'closed' | 'error';
|
|
|
369
254
|
*/
|
|
370
255
|
type SSEUnsubscribe = () => void;
|
|
371
256
|
|
|
372
|
-
export {
|
|
257
|
+
export type { SSEHandlerConfig as S, SSEAuthConfig as a, SSEMessage as b, SSEHandlerAuthConfig as c, SSEClientConfig as d, SSEEventHandler as e, SSEEventHandlers as f, SSESubscribeOptions as g, SSEConnectionState as h, SSEUnsubscribe as i };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
import { a as EventDef, E as EventRouterDef, h as SSETokenStore, S as SSETokenManager, I as InferEventNames, b as InferEventPayload } from './token-manager-DSwIDD-_.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
|
+
* Authentication and authorization configuration
|
|
88
|
+
*/
|
|
89
|
+
auth?: WSHandlerAuthConfig;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* WebSocket client configuration
|
|
93
|
+
*/
|
|
94
|
+
interface WSClientConfig {
|
|
95
|
+
/**
|
|
96
|
+
* Backend API host URL (ws:// or wss://)
|
|
97
|
+
* @default derived from NEXT_PUBLIC_SPFN_API_URL
|
|
98
|
+
*/
|
|
99
|
+
host?: string;
|
|
100
|
+
/**
|
|
101
|
+
* WS endpoint pathname
|
|
102
|
+
* @default '/ws'
|
|
103
|
+
*/
|
|
104
|
+
pathname?: string;
|
|
105
|
+
/**
|
|
106
|
+
* Auto reconnect on disconnect
|
|
107
|
+
* @default true
|
|
108
|
+
*/
|
|
109
|
+
reconnect?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Reconnect delay in ms
|
|
112
|
+
* @default 3000
|
|
113
|
+
*/
|
|
114
|
+
reconnectDelay?: number;
|
|
115
|
+
/**
|
|
116
|
+
* Maximum reconnect attempts (0 = infinite)
|
|
117
|
+
* @default 0
|
|
118
|
+
*/
|
|
119
|
+
maxReconnectAttempts?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Acquire a one-time token before connecting
|
|
122
|
+
*/
|
|
123
|
+
acquireToken?: () => Promise<string>;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* WebSocket connection state
|
|
127
|
+
*/
|
|
128
|
+
type WSConnectionState = 'connecting' | 'open' | 'closed' | 'error';
|
|
129
|
+
/**
|
|
130
|
+
* Event handlers map for WSRouterDef
|
|
131
|
+
*/
|
|
132
|
+
type WSEventHandlers<TRouter extends WSRouterDef<any, any>> = {
|
|
133
|
+
[K in InferEventNames<TRouter>]?: (payload: InferEventPayload<TRouter, K>) => void;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Subscribe options
|
|
137
|
+
*/
|
|
138
|
+
interface WSSubscribeOptions<TRouter extends WSRouterDef<any, any>> {
|
|
139
|
+
events: InferEventNames<TRouter>[];
|
|
140
|
+
handlers: WSEventHandlers<TRouter>;
|
|
141
|
+
onOpen?: () => void;
|
|
142
|
+
onError?: (error: Event) => void;
|
|
143
|
+
onClose?: () => void;
|
|
144
|
+
onReconnect?: (attempt: number) => void;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Unsubscribe function
|
|
148
|
+
*/
|
|
149
|
+
type WSUnsubscribe = () => void;
|
|
150
|
+
|
|
151
|
+
export type { WSRouterDef as W, WSHandlerConfig as a, WSMessageHandlers as b, WSAuthConfig as c, WSHandlerAuthConfig as d, WSMessageContext as e, WSMessageHandlerFn as f, WSRawConnection as g, WSClientConfig as h, WSConnectionState as i, WSEventHandlers as j, WSSubscribeOptions as k, WSUnsubscribe as l };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spfn/core",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.47",
|
|
4
4
|
"description": "SPFN Framework Core - File-based routing, transactions, repository pattern",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -98,6 +98,16 @@
|
|
|
98
98
|
"types": "./dist/event/sse/client.d.ts",
|
|
99
99
|
"import": "./dist/event/sse/client.js",
|
|
100
100
|
"require": "./dist/event/sse/client.js"
|
|
101
|
+
},
|
|
102
|
+
"./event/ws": {
|
|
103
|
+
"types": "./dist/event/ws/index.d.ts",
|
|
104
|
+
"import": "./dist/event/ws/index.js",
|
|
105
|
+
"require": "./dist/event/ws/index.js"
|
|
106
|
+
},
|
|
107
|
+
"./event/ws/client": {
|
|
108
|
+
"types": "./dist/event/ws/client.d.ts",
|
|
109
|
+
"import": "./dist/event/ws/client.js",
|
|
110
|
+
"require": "./dist/event/ws/client.js"
|
|
101
111
|
}
|
|
102
112
|
},
|
|
103
113
|
"keywords": [
|
|
@@ -156,11 +166,13 @@
|
|
|
156
166
|
"zod": "^4.1.11"
|
|
157
167
|
},
|
|
158
168
|
"optionalDependencies": {
|
|
159
|
-
"ioredis": "^5.4.1"
|
|
169
|
+
"ioredis": "^5.4.1",
|
|
170
|
+
"ws": "^8.0.0"
|
|
160
171
|
},
|
|
161
172
|
"devDependencies": {
|
|
162
173
|
"@types/micromatch": "^4.0.9",
|
|
163
174
|
"@types/node": "^20.11.0",
|
|
175
|
+
"@types/ws": "^8.18.1",
|
|
164
176
|
"@vitest/coverage-v8": "^4.0.6",
|
|
165
177
|
"drizzle-kit": "^0.31.6",
|
|
166
178
|
"madge": "^8.0.0",
|