@spfn/core 0.3.0-beta.3 → 0.3.0-beta.30

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.
Files changed (104) hide show
  1. package/README.md +301 -10
  2. package/dist/app-config/index.d.ts +39 -1
  3. package/dist/app-config/index.js.map +1 -1
  4. package/dist/authz/index.js +47 -391
  5. package/dist/authz/index.js.map +1 -1
  6. package/dist/{boss-D16fO2oG.d.ts → boss-BiDCbbAQ.d.ts} +60 -7
  7. package/dist/codegen/index.d.ts +157 -16
  8. package/dist/codegen/index.js +772 -166
  9. package/dist/codegen/index.js.map +1 -1
  10. package/dist/config/index.js +4 -4
  11. package/dist/config/index.js.map +1 -1
  12. package/dist/contract/index.d.ts +39 -16
  13. package/dist/contract/index.js +147 -23
  14. package/dist/contract/index.js.map +1 -1
  15. package/dist/db/index.d.ts +230 -35
  16. package/dist/db/index.js +316 -160
  17. package/dist/db/index.js.map +1 -1
  18. package/dist/env/index.d.ts +6 -442
  19. package/dist/env/index.js +13 -16
  20. package/dist/env/index.js.map +1 -1
  21. package/dist/env/loader.js +24 -1
  22. package/dist/env/loader.js.map +1 -1
  23. package/dist/error-CIZlYJlL.d.ts +157 -0
  24. package/dist/errors/index.d.ts +18 -1
  25. package/dist/errors/index.js +47 -391
  26. package/dist/errors/index.js.map +1 -1
  27. package/dist/event/index.d.ts +5 -5
  28. package/dist/event/index.js +202 -3
  29. package/dist/event/index.js.map +1 -1
  30. package/dist/event/sse/client.d.ts +18 -4
  31. package/dist/event/sse/client.js +94 -18
  32. package/dist/event/sse/client.js.map +1 -1
  33. package/dist/event/sse/index.d.ts +18 -5
  34. package/dist/event/sse/index.js +452 -114
  35. package/dist/event/sse/index.js.map +1 -1
  36. package/dist/event/ws/client.d.ts +3 -2
  37. package/dist/event/ws/index.d.ts +4 -3
  38. package/dist/job/index.d.ts +48 -3
  39. package/dist/job/index.js +131 -52
  40. package/dist/job/index.js.map +1 -1
  41. package/dist/logger/index.js +0 -12
  42. package/dist/logger/index.js.map +1 -1
  43. package/dist/middleware/index.js +52 -397
  44. package/dist/middleware/index.js.map +1 -1
  45. package/dist/nextjs/index.d.ts +75 -14
  46. package/dist/nextjs/index.js +200 -84
  47. package/dist/nextjs/index.js.map +1 -1
  48. package/dist/nextjs/server.d.ts +46 -8
  49. package/dist/nextjs/server.js +15 -1
  50. package/dist/nextjs/server.js.map +1 -1
  51. package/dist/ops/index.d.ts +61 -42
  52. package/dist/ops/index.js +648 -97
  53. package/dist/ops/index.js.map +1 -1
  54. package/dist/registry-C2KQCh0m.d.ts +473 -0
  55. package/dist/route/index.d.ts +5 -2
  56. package/dist/route/index.js +107 -28
  57. package/dist/route/index.js.map +1 -1
  58. package/dist/route/types.d.ts +7 -1
  59. package/dist/{router-Cy7rAfmj.d.ts → route-builder-DRW-MSCJ.d.ts} +51 -126
  60. package/dist/router-C57y-vz1.d.ts +184 -0
  61. package/dist/router-CX5uVjmU.d.ts +406 -0
  62. package/dist/server/index.d.ts +130 -7
  63. package/dist/server/index.js +1174 -164
  64. package/dist/server/index.js.map +1 -1
  65. package/dist/state/client.d.ts +206 -0
  66. package/dist/state/client.js +678 -0
  67. package/dist/state/client.js.map +1 -0
  68. package/dist/state/index.d.ts +443 -0
  69. package/dist/state/index.js +644 -0
  70. package/dist/state/index.js.map +1 -0
  71. package/dist/state/react.d.ts +49 -0
  72. package/dist/state/react.js +50 -0
  73. package/dist/state/react.js.map +1 -0
  74. package/dist/surface/index.d.ts +115 -0
  75. package/dist/surface/index.js +1013 -0
  76. package/dist/surface/index.js.map +1 -0
  77. package/dist/token-manager-Ckjt2-qq.d.ts +122 -0
  78. package/dist/{types-CfYVhIQ9.d.ts → types-5Rl11_Nb.d.ts} +43 -4
  79. package/dist/types-CKH0D3Dy.d.ts +441 -0
  80. package/dist/{types-ZQODsBft.d.ts → types-CurMab2Q.d.ts} +52 -2
  81. package/dist/{types-2AbaW4Ie.d.ts → types-DncqXfWN.d.ts} +4 -3
  82. package/docs/file-upload.md +195 -333
  83. package/package.json +39 -9
  84. package/src/cache/README.md +330 -0
  85. package/src/codegen/README.md +721 -0
  86. package/src/config/README.md +333 -0
  87. package/src/contract/README.md +442 -0
  88. package/src/db/README.md +589 -0
  89. package/src/db/manager/README.md +500 -0
  90. package/src/db/schema/README.md +354 -0
  91. package/src/db/transaction/README.md +845 -0
  92. package/src/env/README.md +787 -0
  93. package/src/errors/README.md +445 -0
  94. package/src/event/README.md +994 -0
  95. package/src/job/README.md +613 -0
  96. package/src/logger/README.md +321 -0
  97. package/src/middleware/README.md +634 -0
  98. package/src/nextjs/README.md +756 -0
  99. package/src/route/README.md +763 -0
  100. package/src/security/README.md +100 -0
  101. package/src/server/README.md +704 -0
  102. package/src/state/README.md +806 -0
  103. package/dist/token-manager-BT5EnUAR.d.ts +0 -278
  104. package/dist/types-9oszaJqp.d.ts +0 -250
@@ -0,0 +1,441 @@
1
+ import { TSchema, Static } from '@sinclair/typebox';
2
+ import { ErrorRegistry, ErrorRegistryInput } from '@spfn/core/errors';
3
+ import { RouteDef, RouteInput } from '@spfn/core/route';
4
+
5
+ /**
6
+ * Typed client error
7
+ *
8
+ * `errorType` tells apart why a call failed:
9
+ * - `'http'`: the server answered with a non-2xx status
10
+ * - `'network'`: the fetch or the body read failed (status 0)
11
+ * - `'timeout'`: the call's timeout elapsed before response headers arrived (status 408)
12
+ * - `'aborted'`: the caller's own signal aborted the call (status 0); `cause` is the signal's reason
13
+ */
14
+ declare class ApiError extends Error {
15
+ readonly status: number;
16
+ readonly url: string;
17
+ readonly response?: unknown | undefined;
18
+ readonly errorType?: "http" | "network" | "timeout" | "aborted" | undefined;
19
+ constructor(message: string, status: number, url: string, response?: unknown | undefined, errorType?: "http" | "network" | "timeout" | "aborted" | undefined, options?: {
20
+ cause?: unknown;
21
+ });
22
+ }
23
+
24
+ /**
25
+ * Convert File types in schema to actual File for client usage
26
+ *
27
+ * TypeBox File schemas become actual File objects on the client side.
28
+ */
29
+ type ConvertFileTypes<T> = T extends File ? File : T extends File[] ? File[] : T;
30
+ /**
31
+ * Extract form data input type with File support
32
+ *
33
+ * Maps schema types to runtime types, converting FileSchema to File.
34
+ */
35
+ type FormDataInput<T> = {
36
+ [K in keyof T]: ConvertFileTypes<T[K]>;
37
+ };
38
+ /**
39
+ * Extract structured input from RouteInput
40
+ *
41
+ * Converts TypeBox schemas to their static types for each input field.
42
+ */
43
+ type StructuredInput<TInput extends RouteInput> = {
44
+ params: TInput['params'] extends TSchema ? Static<TInput['params']> : {};
45
+ query: TInput['query'] extends TSchema ? Static<TInput['query']> : {};
46
+ body: TInput['body'] extends TSchema ? Static<TInput['body']> : {};
47
+ formData: TInput['formData'] extends TSchema ? FormDataInput<Static<TInput['formData']>> : {};
48
+ headers: TInput['headers'] extends TSchema ? Static<TInput['headers']> : {};
49
+ cookies: TInput['cookies'] extends TSchema ? Static<TInput['cookies']> : {};
50
+ };
51
+ /**
52
+ * Infer route input type from RouteDef
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * // Server route definition
57
+ * const getUser = route.get('/users/:id')
58
+ * .input({ params: Type.Object({ id: Type.String() }) })
59
+ * .handler(...);
60
+ *
61
+ * // Client: extract input type
62
+ * type Input = InferRouteInput<typeof getUser>;
63
+ * // { params: { id: string }, query: {}, body: {}, ... }
64
+ * ```
65
+ */
66
+ type InferRouteInput<TRoute> = TRoute extends RouteDef<infer TInput, any, any> ? StructuredInput<TInput> : never;
67
+ /**
68
+ * Infer route output type from RouteDef
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * // Server route definition
73
+ * const getUser = route.get('/users/:id')
74
+ * .handler(async (c) => {
75
+ * return { id: '1', name: 'John' };
76
+ * });
77
+ *
78
+ * // Client: extract output type
79
+ * type Output = InferRouteOutput<typeof getUser>;
80
+ * // { id: string, name: string }
81
+ * ```
82
+ */
83
+ type InferRouteOutput<TRoute> = TRoute extends RouteDef<any, any, infer TResponse> ? TResponse : never;
84
+ /**
85
+ * True only for `any`.
86
+ *
87
+ * `any` satisfies both branches of a conditional type, so a router whose routes are
88
+ * unresolved (`Router<any>`) would otherwise flatten into "every name exists" and
89
+ * accept a misspelled route name. A route record that is not known contributes no
90
+ * names instead.
91
+ */
92
+ type IsAny<T> = 0 extends (1 & T) ? true : false;
93
+ /**
94
+ * True when `T` is a union of two or more members.
95
+ */
96
+ type IsUnion<T, TCopy = T> = T extends unknown ? ([TCopy] extends [T] ? false : true) : false;
97
+ /**
98
+ * One leaf of a router tree: the name a route is registered under, its definition, and
99
+ * the path it was reached by.
100
+ *
101
+ * A route is identified by its own key whatever depth it sits at — `registerRoutes`
102
+ * mounts a nested route under that key, and the contract collector reports a collision
103
+ * on it. Nesting groups the source; it does not qualify the name. The trail is carried
104
+ * so that one name reached through two branches stays two leaves.
105
+ */
106
+ interface RouteLeaf<TName extends PropertyKey, TDef, TTrail extends PropertyKey[]> {
107
+ name: TName;
108
+ def: TDef;
109
+ trail: TTrail;
110
+ }
111
+ /** Decrements the nesting budget; `PrevDepth[0]` is `never`, which ends the walk. */
112
+ type PrevDepth = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
113
+ /**
114
+ * How many levels of nesting are followed.
115
+ *
116
+ * A budget rather than open recursion: a router type that refers to itself would
117
+ * otherwise never terminate, and TypeScript would report an instantiation-depth error
118
+ * in whichever component happened to read a route type. A route below the budget is not
119
+ * visible to these types; ten levels is far past any hand-written tree.
120
+ */
121
+ type MaxNesting = 10;
122
+ /**
123
+ * Collect every route in a route record as a leaf, descending into nested routers
124
+ * exactly as `registerRoutes` does.
125
+ */
126
+ type RouteLeaves<TRoutes, TTrail extends PropertyKey[] = [], TDepth extends number = MaxNesting> = [
127
+ TDepth
128
+ ] extends [never] ? never : IsAny<TRoutes> extends true ? never : {
129
+ [K in keyof TRoutes]-?: RouteLeafOf<K, Exclude<TRoutes[K], undefined>, [...TTrail, K], TDepth>;
130
+ }[keyof TRoutes];
131
+ /**
132
+ * A nested router contributes its own leaves; anything else is a leaf.
133
+ *
134
+ * `{ _routes: ... }` is the runtime's own test for a router (`isRouter`), and a
135
+ * `RouteDef` carries `handler`, `_input` and `_response` but never `_routes`, so a
136
+ * route cannot be mistaken for a router.
137
+ */
138
+ type RouteLeafOf<TName extends PropertyKey, TValue, TTrail extends PropertyKey[], TDepth extends number> = TValue extends {
139
+ _routes: infer TNested;
140
+ } ? RouteLeaves<TNested, TTrail, PrevDepth[TDepth]> : RouteLeaf<TName, TValue, TTrail>;
141
+ /** Every name the tree declares. */
142
+ type LeafNames<TLeaves> = Extract<TLeaves extends RouteLeaf<infer TName, any, any> ? TName : never, PropertyKey>;
143
+ /**
144
+ * The leaves declaring exactly this name.
145
+ *
146
+ * Exactly, not assignably: a record typed `Record<string, RouteDef>` contributes the
147
+ * name `string`, which every literal name is assignable to, and a loose sibling must
148
+ * not make each of its siblings look doubly declared.
149
+ */
150
+ type LeavesNamed<TLeaves, TName extends PropertyKey> = TLeaves extends RouteLeaf<infer TLeafName, any, any> ? [TLeafName] extends [TName] ? [TName] extends [TLeafName] ? TLeaves : never : never : never;
151
+ /** The definition a leaf carries. */
152
+ type LeafDef<TLeaves> = TLeaves extends RouteLeaf<any, infer TDef, any> ? TDef : never;
153
+ /** Where a leaf was found — one distinct trail per place the name is declared. */
154
+ type LeafTrail<TLeaves> = TLeaves extends RouteLeaf<any, any, infer TTrail> ? TTrail : never;
155
+ /**
156
+ * Flatten the leaves into one namespace of name → definition.
157
+ *
158
+ * A name that two branches both declare is left out rather than resolved to one of
159
+ * them: the tree gives it no single meaning, and picking a winner silently would hand
160
+ * a component another route's types. Naming it then fails the `K` constraint on
161
+ * `RouterOutput`/`RouterInput`, on the line that named it. Two places is counted by
162
+ * trail, so one slot holding a union of route definitions stays one route and still
163
+ * infers the union.
164
+ *
165
+ * The result is a fresh mapped type, so no key carries an optional or readonly modifier
166
+ * out of the source record — a route key that turned optional would change what
167
+ * `InferRouteInput` returns for it.
168
+ */
169
+ type FlattenLeaves<TLeaves> = {
170
+ [TName in LeafNames<TLeaves> as UnambiguousName<TLeaves, TName>]: LeafDef<LeavesNamed<TLeaves, TName>>;
171
+ };
172
+ /** The name itself when one place declares it, and nothing when two do. */
173
+ type UnambiguousName<TLeaves, TName extends PropertyKey> = IsUnion<LeafTrail<LeavesNamed<TLeaves, TName>>> extends true ? never : TName;
174
+ /**
175
+ * Extract routes from Router type
176
+ *
177
+ * `Router<TRoutes>` holds its routes in `_routes`, and a value in that record is either
178
+ * a route or another router. Nested routers are flattened away, so every route appears
179
+ * here under its own name at one level — the same set of names the server registers.
180
+ *
181
+ * Exported so that the typed client (`Client`, in `./builder`) names a route the same
182
+ * way `RouterOutput` and `RouterInput` do; the two must not diverge, or a route would
183
+ * type in one place and not the other. It stays internal to the package: the
184
+ * `@spfn/core/nextjs` entry point exports a fixed list of type names, and this is not
185
+ * one of them.
186
+ */
187
+ type ExtractRoutes<TRouter> = TRouter extends {
188
+ _routes: infer TRoutes;
189
+ } ? FlattenLeaves<RouteLeaves<TRoutes>> : FlattenLeaves<RouteLeaves<TRouter>>;
190
+ /**
191
+ * Extract output type for a specific route from router
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * import type { RouterOutput } from '@spfn/core/nextjs';
196
+ * import type { AppRouter } from '@/server/router';
197
+ *
198
+ * // Get output type for a specific route
199
+ * type ListData = RouterOutput<AppRouter, 'listExamples'>;
200
+ *
201
+ * // Use in props
202
+ * interface Props {
203
+ * data: RouterOutput<AppRouter, 'listExamples'>;
204
+ * }
205
+ *
206
+ * // Extract item type from paginated response
207
+ * type Example = RouterOutput<AppRouter, 'listExamples'>['items'][number];
208
+ * ```
209
+ *
210
+ * A route declared inside a nested router is named the same way. The server registers
211
+ * a nested route under its own key, so `K` is the flat set of route names in the tree
212
+ * and the depth the route was declared at never appears here:
213
+ *
214
+ * ```typescript
215
+ * const appRouter = defineRouter({
216
+ * getRoot,
217
+ * examples: defineRouter({ listExamples, createExample }),
218
+ * });
219
+ *
220
+ * type ListData = RouterOutput<typeof appRouter, 'listExamples'>;
221
+ * ```
222
+ *
223
+ * Two branches declaring the same name make that name ambiguous, and it is left out of
224
+ * `K` rather than resolved to one of them — naming it is a compile error here instead
225
+ * of the wrong route's types reaching a component.
226
+ */
227
+ type RouterOutput<TRouter, K extends keyof ExtractRoutes<TRouter>> = InferRouteOutput<ExtractRoutes<TRouter>[K]>;
228
+ /**
229
+ * Extract input type for a specific route from router
230
+ *
231
+ * @example
232
+ * ```typescript
233
+ * import type { RouterInput } from '@spfn/core/nextjs';
234
+ * import type { AppRouter } from '@/server/router';
235
+ *
236
+ * // Get input type for a specific route
237
+ * type CreateInput = RouterInput<AppRouter, 'createExample'>;
238
+ *
239
+ * // Use in function parameter
240
+ * function submitForm(data: RouterInput<AppRouter, 'createExample'>['body']) {
241
+ * // ...
242
+ * }
243
+ * ```
244
+ *
245
+ * `K` is the flat set of route names, so a route declared inside a nested router is
246
+ * named by its own key here too — see {@link RouterOutput}.
247
+ */
248
+ type RouterInput<TRouter, K extends keyof ExtractRoutes<TRouter>> = InferRouteInput<ExtractRoutes<TRouter>[K]>;
249
+ /**
250
+ * Cookie options for setCookie
251
+ */
252
+ interface CookieOptions {
253
+ httpOnly?: boolean;
254
+ secure?: boolean;
255
+ sameSite?: 'strict' | 'lax' | 'none';
256
+ maxAge?: number;
257
+ path?: string;
258
+ domain?: string;
259
+ }
260
+ /**
261
+ * Cookie to set in response
262
+ */
263
+ interface SetCookie {
264
+ name: string;
265
+ value: string;
266
+ options?: CookieOptions;
267
+ }
268
+ /**
269
+ * Request interceptor - called before fetch
270
+ */
271
+ type RequestInterceptor = (url: string, init: RequestInit) => Promise<RequestInit> | RequestInit;
272
+ /**
273
+ * Response interceptor - called after fetch
274
+ *
275
+ * It runs outside the client's transport error handling, so throwing from it is a
276
+ * supported way to end the call: the error reaches the caller unchanged, which is what
277
+ * lets `redirect()` and `notFound()` navigate from one (issue #104). Returning
278
+ * `{ response, body }` replaces both for everything after it, error-status handling
279
+ * included.
280
+ */
281
+ type ResponseInterceptor = (response: Response, body: any) => Promise<{
282
+ response: Response;
283
+ body: any;
284
+ }> | {
285
+ response: Response;
286
+ body: any;
287
+ };
288
+ /**
289
+ * How one call ended, as `ApiConfig.onOutcome` is told it
290
+ *
291
+ * - `ok: true`: the server answered 2xx; `response` and `body` are what the response
292
+ * interceptors left, and `body` is what the call returns
293
+ * - `'http'`: the server answered with an error status; `error` is the object the call
294
+ * throws — the registered class a `__type` body names (`NotFoundError`,
295
+ * `UnauthorizedError`, …) or else an `ApiError`, hence `Error`
296
+ * - `'network'` / `'timeout'` / `'aborted'`: no answer; `error` is the `ApiError` the call
297
+ * throws, `status` its status (0, 408, 0)
298
+ */
299
+ type ApiOutcome = {
300
+ ok: true;
301
+ response: Response;
302
+ body: unknown;
303
+ } | {
304
+ ok: false;
305
+ errorType: 'http';
306
+ status: number;
307
+ error: Error;
308
+ } | {
309
+ ok: false;
310
+ errorType: 'network' | 'timeout' | 'aborted';
311
+ status: number;
312
+ error: ApiError;
313
+ };
314
+ /**
315
+ * Client configuration
316
+ */
317
+ interface ApiConfig {
318
+ /**
319
+ * Base URL for RPC endpoint
320
+ *
321
+ * @default '/api/rpc'
322
+ * @example '/api/rpc', 'http://localhost:3000/api/rpc'
323
+ */
324
+ baseUrl?: string;
325
+ /**
326
+ * Default headers for all requests
327
+ */
328
+ headers?: Record<string, string>;
329
+ /**
330
+ * Request timeout in milliseconds
331
+ *
332
+ * @default env.SERVER_TIMEOUT (120000)
333
+ */
334
+ timeout?: number;
335
+ /**
336
+ * Custom fetch implementation
337
+ */
338
+ fetch?: typeof fetch;
339
+ /**
340
+ * Global request interceptor
341
+ */
342
+ onRequest?: RequestInterceptor;
343
+ /**
344
+ * Global response interceptor
345
+ */
346
+ onResponse?: ResponseInterceptor;
347
+ /**
348
+ * Told once how each call that reached fetch ended — after the response interceptors
349
+ * ran, before the call returns or throws
350
+ *
351
+ * Observed: a transport failure (`'network'`, `'timeout'`, `'aborted'`) and every
352
+ * answer, error statuses included; `error` is the very object the call then throws.
353
+ * Not observed: a throw from an `onRequest` or `onResponse` interceptor, or from
354
+ * building the request — those are the app's own and reach the caller unchanged.
355
+ *
356
+ * Called synchronously and never awaited. A throw from it, or a rejection of the
357
+ * promise it returns, is logged and never changes what the call returns or throws.
358
+ * Config only: there is no per-call observer.
359
+ *
360
+ * @example
361
+ * ```typescript
362
+ * const api = createApi<AppRouter>({
363
+ * onOutcome: (outcome) => setServerStatus(outcome.ok ? 'up' : outcome.errorType),
364
+ * });
365
+ * ```
366
+ */
367
+ onOutcome?: (outcome: ApiOutcome) => void;
368
+ /**
369
+ * Custom error registry for deserialization
370
+ *
371
+ * Core HTTP errors are automatically registered. Use this to add your custom application errors.
372
+ *
373
+ * @example
374
+ * ```typescript
375
+ * import { errorRegistry } from '@spfn/core/errors';
376
+ * import { authErrorRegistry } from '@myapp/auth/errors';
377
+ * import { PaymentFailedError } from '@/server/errors';
378
+ *
379
+ * const api = createApi<AppRouter>({
380
+ * errorRegistry: [errorRegistry, authErrorRegistry, PaymentFailedError]
381
+ * });
382
+ * ```
383
+ */
384
+ errorRegistry?: ErrorRegistry | ErrorRegistryInput[];
385
+ /**
386
+ * Enable debug logging
387
+ *
388
+ * @default false
389
+ */
390
+ debug?: boolean;
391
+ }
392
+ /**
393
+ * Per-call options
394
+ */
395
+ interface CallOptions {
396
+ /**
397
+ * Request timeout in milliseconds, covering the wait for response headers
398
+ * Overrides the global timeout set in ApiConfig; set it with `.timeout(ms)`
399
+ */
400
+ timeout?: number;
401
+ /**
402
+ * Additional headers for this request
403
+ */
404
+ headers?: Record<string, string>;
405
+ /**
406
+ * Override cookies for this request
407
+ *
408
+ * Note: Cookies are automatically forwarded by the proxy.
409
+ * Use this only when you need to override them.
410
+ */
411
+ cookies?: Record<string, string>;
412
+ /**
413
+ * Request-specific interceptor
414
+ */
415
+ onRequest?: RequestInterceptor;
416
+ /**
417
+ * Response-specific interceptor
418
+ */
419
+ onResponse?: ResponseInterceptor;
420
+ /**
421
+ * Next.js-specific fetch options
422
+ *
423
+ * @example
424
+ * // Time-based revalidation
425
+ * { next: { revalidate: 60 } }
426
+ *
427
+ * // Disable cache
428
+ * { cache: 'no-store' }
429
+ *
430
+ * // Tag-based revalidation
431
+ * { next: { tags: ['users'] } }
432
+ */
433
+ fetchOptions?: RequestInit & {
434
+ next?: {
435
+ revalidate?: number | false;
436
+ tags?: string[];
437
+ };
438
+ };
439
+ }
440
+
441
+ export { type ApiConfig as A, type CallOptions as C, type ExtractRoutes as E, type InferRouteInput as I, type RequestInterceptor as R, type SetCookie as S, type ResponseInterceptor as a, type InferRouteOutput as b, ApiError as c, type ApiOutcome as d, type CookieOptions as e, type RouterInput as f, type RouterOutput as g, type StructuredInput as h };
@@ -1,5 +1,6 @@
1
1
  import { Context } from 'hono';
2
- import { b as EventRouterDef, I as InferEventNames, e as InferEventPayload, i as SSETokenStore, S as SSETokenManager } from './token-manager-BT5EnUAR.js';
2
+ import { E as EventRouterDef, I as InferEventNames, i as InferEventPayload, l as SubscribeRequest } from './router-CX5uVjmU.js';
3
+ import { c as SSETokenStore, S as SSETokenManager } from './token-manager-Ckjt2-qq.js';
3
4
 
4
5
  /**
5
6
  * SSE Types
@@ -78,6 +79,17 @@ interface SSEHandlerAuthConfig {
78
79
  * Return false to skip sending the event to this user.
79
80
  */
80
81
  filter?: Record<string, (subject: string, payload: unknown) => boolean>;
82
+ /**
83
+ * Resolve the principal every `.canSubscribe()` of a connection receives
84
+ * (called once per connection, after the subject is known).
85
+ *
86
+ * The stream request carries no `AuthContext`: the subject comes from the
87
+ * one-time token, and the stream is mounted without the app's middleware.
88
+ * So an app that decides on roles or memberships loads them here, once,
89
+ * instead of in every predicate. Absent → predicates see
90
+ * `principal: undefined`. A throw refuses the connection with 403.
91
+ */
92
+ resolvePrincipal?: (subject: string, request: SubscribeRequest) => unknown | Promise<unknown>;
81
93
  }
82
94
  /**
83
95
  * SSE auth configuration (user-facing, generic)
@@ -113,6 +125,7 @@ interface SSEAuthConfig<TRouter extends EventRouterDef<any>> {
113
125
  filter?: {
114
126
  [K in InferEventNames<TRouter>]?: (subject: string, payload: InferEventPayload<TRouter, K>) => boolean;
115
127
  };
128
+ resolvePrincipal?: (subject: string, request: SubscribeRequest) => unknown | Promise<unknown>;
116
129
  }
117
130
  /**
118
131
  * SSE Handler configuration
@@ -248,6 +261,13 @@ interface SSESubscribeOptions<TRouter extends EventRouterDef<any>> {
248
261
  * Event handlers
249
262
  */
250
263
  handlers: SSEEventHandlers<TRouter>;
264
+ /**
265
+ * Extra connect-query fields, e.g. the key a state event is filtered on
266
+ * (`{ workspaceId: 'w1' }`). Appended to the stream URL after `events=`,
267
+ * both sides URI-encoded, and reused on every reconnect. `events` and
268
+ * `token` are reserved: subscribe throws naming them.
269
+ */
270
+ query?: Record<string, string>;
251
271
  /**
252
272
  * Called when connection opens
253
273
  */
@@ -269,6 +289,36 @@ interface SSESubscribeOptions<TRouter extends EventRouterDef<any>> {
269
289
  * Called when reconnecting
270
290
  */
271
291
  onReconnect?: (attempt: number) => void;
292
+ /**
293
+ * Called when the server grants or revokes one event on the open connection
294
+ * (`subscription.granted` / `subscription.revoked` frames). Only events in
295
+ * `events` can change: an event never requested cannot be granted.
296
+ */
297
+ onSubscriptionChange?: (change: SSESubscriptionChange<TRouter>) => void;
298
+ /**
299
+ * Called with the server's `connected` frame on every connection — the
300
+ * first open and each reconnect — after `onOpen`.
301
+ */
302
+ onConnected?: (payload: ConnectedPayload) => void;
303
+ }
304
+ /**
305
+ * The `connected` frame, the first a connection receives: which requested
306
+ * events it is subscribed to, which are denied, and which were served their
307
+ * initial frames (written right after this one). `initial` is absent from a
308
+ * server that predates it.
309
+ */
310
+ interface ConnectedPayload {
311
+ subscribedEvents: string[];
312
+ deniedEvents: string[];
313
+ initial?: string[];
314
+ timestamp: number;
315
+ }
316
+ /**
317
+ * A `grant` / `revoke` the server applied to the open connection.
318
+ */
319
+ interface SSESubscriptionChange<TRouter extends EventRouterDef<any>> {
320
+ event: InferEventNames<TRouter>;
321
+ status: 'granted' | 'revoked';
272
322
  }
273
323
  /**
274
324
  * SSE connection state
@@ -279,4 +329,4 @@ type SSEConnectionState = 'connecting' | 'open' | 'closed' | 'error';
279
329
  */
280
330
  type SSEUnsubscribe = () => void;
281
331
 
282
- export type { SSEClientConfig as S, SSESubscribeOptions as a, SSEUnsubscribe as b, SSEConnectionState as c, SSEHandlerConfig as d, SSEAuthConfig as e, SSEEventHandler as f, SSEEventHandlers as g, SSEHandlerAuthConfig as h, SSEMessage as i };
332
+ export type { ConnectedPayload as C, SSEClientConfig as S, SSESubscribeOptions as a, SSEUnsubscribe as b, SSEConnectionState as c, SSEHandlerConfig as d, SSEAuthConfig as e, SSEEventHandler as f, SSEEventHandlers as g, SSEHandlerAuthConfig as h, SSEMessage as i, SSESubscriptionChange as j };
@@ -1,5 +1,6 @@
1
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';
2
+ import { a as EventDef, o as EventRouterBase, I as InferEventNames, i as InferEventPayload } from './router-CX5uVjmU.js';
3
+ import { c as SSETokenStore, S as SSETokenManager } from './token-manager-Ckjt2-qq.js';
3
4
 
4
5
  /**
5
6
  * WebSocket Types
@@ -8,9 +9,9 @@ import { E as EventDef, b as EventRouterDef, I as InferEventNames, e as InferEve
8
9
  /**
9
10
  * WebSocket Router Definition
10
11
  *
11
- * Extends EventRouterDef with client→server message handlers.
12
+ * Extends the event router shape with client→server message handlers.
12
13
  */
13
- interface WSRouterDef<TEvents extends Record<string, EventDef<any>>, TMessages extends WSMessageHandlers = WSMessageHandlers> extends EventRouterDef<TEvents> {
14
+ interface WSRouterDef<TEvents extends Record<string, EventDef<any>>, TMessages extends WSMessageHandlers = WSMessageHandlers> extends EventRouterBase<TEvents> {
14
15
  messages: TMessages;
15
16
  }
16
17
  /**