@spfn/core 0.2.0-beta.67 → 0.2.0-beta.68

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 (35) hide show
  1. package/README.md +309 -116
  2. package/dist/authz/index.js +398 -3
  3. package/dist/authz/index.js.map +1 -1
  4. package/dist/codegen/index.d.ts +114 -8
  5. package/dist/codegen/index.js +162 -3
  6. package/dist/codegen/index.js.map +1 -1
  7. package/dist/config/index.js +1 -1
  8. package/dist/config/index.js.map +1 -1
  9. package/dist/contract/index.d.ts +288 -0
  10. package/dist/contract/index.js +534 -0
  11. package/dist/contract/index.js.map +1 -0
  12. package/dist/{define-middleware-DuXD8Hvu.d.ts → define-middleware-B9bFuXVU.d.ts} +1 -1
  13. package/dist/errors/index.js +398 -3
  14. package/dist/errors/index.js.map +1 -1
  15. package/dist/event/index.d.ts +3 -3
  16. package/dist/event/sse/client.d.ts +2 -2
  17. package/dist/event/sse/index.d.ts +4 -4
  18. package/dist/event/sse/index.js +9 -0
  19. package/dist/event/sse/index.js.map +1 -1
  20. package/dist/event/ws/client.d.ts +2 -2
  21. package/dist/event/ws/index.d.ts +3 -3
  22. package/dist/middleware/index.d.ts +108 -11
  23. package/dist/middleware/index.js +769 -632
  24. package/dist/middleware/index.js.map +1 -1
  25. package/dist/route/index.d.ts +8 -552
  26. package/dist/route/index.js +36 -0
  27. package/dist/route/index.js.map +1 -1
  28. package/dist/router-DhvbMhef.d.ts +641 -0
  29. package/dist/server/index.d.ts +3 -3
  30. package/dist/server/index.js +9 -0
  31. package/dist/server/index.js.map +1 -1
  32. package/dist/{token-manager-jKD_EsSE.d.ts → token-manager-vZeqBbtA.d.ts} +7 -0
  33. package/dist/{types-DVjf37yO.d.ts → types-CF-37KAG.d.ts} +1 -1
  34. package/dist/{types-BFB72jbM.d.ts → types-D9uMxeQS.d.ts} +1 -1
  35. package/package.json +11 -9
@@ -0,0 +1,641 @@
1
+ import { b as NamedMiddleware } from './define-middleware-B9bFuXVU.js';
2
+ import { Context, MiddlewareHandler } from 'hono';
3
+ import { TSchema, Static } from '@sinclair/typebox';
4
+ import { ContentfulStatusCode, RedirectStatusCode } from 'hono/utils/http-status';
5
+ import { HttpMethod } from './route/types.js';
6
+
7
+ /**
8
+ * Route Input Types
9
+ *
10
+ * Defines the structure for route input validation schemas
11
+ */
12
+
13
+ /**
14
+ * Route input schemas
15
+ *
16
+ * Defines validation schemas for different parts of an HTTP request
17
+ */
18
+ type RouteInput = {
19
+ /** Path parameters (e.g., /users/:id) */
20
+ params?: TSchema;
21
+ /** Query string parameters (e.g., ?page=1&limit=20) */
22
+ query?: TSchema;
23
+ /** Request body (JSON) */
24
+ body?: TSchema;
25
+ /** Form data (multipart/form-data) for file uploads */
26
+ formData?: TSchema;
27
+ /** HTTP headers */
28
+ headers?: TSchema;
29
+ /** Cookies */
30
+ cookies?: TSchema;
31
+ };
32
+
33
+ /**
34
+ * Route Builder Context
35
+ *
36
+ * Provides structured input access and response helpers for route handlers
37
+ */
38
+
39
+ /**
40
+ * Paginated response structure
41
+ */
42
+ type PaginatedResult<T> = {
43
+ items: T[];
44
+ pagination: {
45
+ page: number;
46
+ limit: number;
47
+ total: number;
48
+ totalPages: number;
49
+ };
50
+ };
51
+ /**
52
+ * Merge input with interceptor-injected fields
53
+ * Server receives both client input and interceptor-injected fields
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * type ClientInput = { body: { email: string, password: string } };
58
+ * type InterceptorInput = { body: { publicKey: string, keyId: string } };
59
+ * // MergedInput = { body: { email: string, password: string, publicKey: string, keyId: string } }
60
+ * ```
61
+ */
62
+ type MergedInput<TInput extends RouteInput, TInterceptor extends RouteInput> = {
63
+ params: (TInput['params'] extends TSchema ? Static<TInput['params']> : {}) & (TInterceptor['params'] extends TSchema ? Static<TInterceptor['params']> : {});
64
+ query: (TInput['query'] extends TSchema ? Static<TInput['query']> : {}) & (TInterceptor['query'] extends TSchema ? Static<TInterceptor['query']> : {});
65
+ body: (TInput['body'] extends TSchema ? Static<TInput['body']> : {}) & (TInterceptor['body'] extends TSchema ? Static<TInterceptor['body']> : {});
66
+ formData: (TInput['formData'] extends TSchema ? Static<TInput['formData']> : {}) & (TInterceptor['formData'] extends TSchema ? Static<TInterceptor['formData']> : {});
67
+ headers: (TInput['headers'] extends TSchema ? Static<TInput['headers']> : {}) & (TInterceptor['headers'] extends TSchema ? Static<TInterceptor['headers']> : {});
68
+ cookies: (TInput['cookies'] extends TSchema ? Static<TInput['cookies']> : {}) & (TInterceptor['cookies'] extends TSchema ? Static<TInterceptor['cookies']> : {});
69
+ };
70
+ /**
71
+ * RouteBuilderContext - define-route dedicated context
72
+ *
73
+ * Provides structured input access through data() method
74
+ */
75
+ type RouteBuilderContext<TInput extends RouteInput = RouteInput, TInterceptor extends RouteInput = {}> = {
76
+ /**
77
+ * Get structured input data
78
+ *
79
+ * Returns an object with separate params, query, body, headers, cookies
80
+ * If interceptor fields are defined, they are merged with input fields
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * // GET /users/:id?page=1
85
+ * const { params, query } = await c.data();
86
+ * // params = { id: string }
87
+ * // query = { page: number }
88
+ *
89
+ * // POST /users with headers
90
+ * const { body, headers } = await c.data();
91
+ * // body = { name: string }
92
+ * // headers = { authorization: string }
93
+ *
94
+ * // With interceptor-injected fields
95
+ * const { body } = await c.data();
96
+ * // body = { email: string, password: string, publicKey: string, keyId: string }
97
+ * ```
98
+ */
99
+ data(): Promise<MergedInput<TInput, TInterceptor>>;
100
+ /**
101
+ * Return JSON response with custom status and headers
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * return c.json({ message: 'Custom response' }, 200);
106
+ * ```
107
+ */
108
+ json(data: unknown, status?: ContentfulStatusCode, headers?: Record<string, string | string[]>): Response;
109
+ /**
110
+ * Return 201 Created response with optional Location header
111
+ * Returns data directly for type inference
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * const user = await createUser(body);
116
+ * return c.created(user, `/users/${user.id}`);
117
+ * // Response: 201 Created
118
+ * // Header: Location: /users/123
119
+ * // Body: { id: '123', name: 'John' }
120
+ * // Type: User (inferred from data)
121
+ * ```
122
+ */
123
+ created<T>(data: T, location?: string): T;
124
+ /**
125
+ * Return 202 Accepted response
126
+ * Returns data directly for type inference
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * // With data
131
+ * return c.accepted({ jobId: '123' });
132
+ * // Response: 202 Accepted, Body: { jobId: '123' }
133
+ * // Type: { jobId: string }
134
+ *
135
+ * // Without data
136
+ * return c.accepted();
137
+ * // Response: 202 Accepted, Body: (empty)
138
+ * // Type: void
139
+ * ```
140
+ */
141
+ accepted(): void;
142
+ accepted<T>(data: T): T;
143
+ /**
144
+ * Return 204 No Content response (empty body)
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * await deleteUser(id);
149
+ * return c.noContent();
150
+ * // Response: 204 No Content, Body: (empty)
151
+ * // Type: void
152
+ * ```
153
+ */
154
+ noContent(): void;
155
+ /**
156
+ * Return 304 Not Modified response (empty body)
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * if (etag === requestEtag) {
161
+ * return c.notModified();
162
+ * }
163
+ * // Response: 304 Not Modified, Body: (empty)
164
+ * // Type: void
165
+ * ```
166
+ */
167
+ notModified(): void;
168
+ /**
169
+ * Return paginated response with metadata
170
+ * Returns `{ items: [...], pagination: {...} }` format with type inference
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * const users = await getUsers(page, limit);
175
+ * const total = await countUsers();
176
+ * return c.paginated(users, page, limit, total);
177
+ * // Response: {
178
+ * // items: [...],
179
+ * // pagination: {
180
+ * // page: 1,
181
+ * // limit: 20,
182
+ * // total: 100,
183
+ * // totalPages: 5
184
+ * // }
185
+ * // }
186
+ * // Type: PaginatedResult<User>
187
+ * ```
188
+ */
189
+ paginated<T>(data: T[], page: number, limit: number, total: number): PaginatedResult<T>;
190
+ /**
191
+ * Redirect to another URL
192
+ *
193
+ * @param url - Target URL to redirect to
194
+ * @param status - HTTP status code (301, 302, 303, 307, 308). Default: 302
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * // Temporary redirect (302)
199
+ * return c.redirect('/login');
200
+ *
201
+ * // Permanent redirect (301)
202
+ * return c.redirect('/new-path', 301);
203
+ *
204
+ * // See Other (303) - useful after POST
205
+ * return c.redirect('/success', 303);
206
+ * ```
207
+ */
208
+ redirect(url: string, status?: RedirectStatusCode): Response;
209
+ raw: Context;
210
+ };
211
+
212
+ /**
213
+ * Route Contract
214
+ *
215
+ * A contract marks a route as a versioned public promise to clients that are
216
+ * compiled and deployed separately from the server — a mobile app, an external
217
+ * API consumer. Those clients cannot be fixed by redeploying the server, so the
218
+ * shape they read has to survive server changes.
219
+ *
220
+ * A web client does not need this. `createApi<AppRouter>()` derives its types
221
+ * from the router in the same build, so a removed response field breaks the
222
+ * TypeScript compile instead of a running app.
223
+ *
224
+ * The response shape is declared here rather than inferred from the handler's
225
+ * return type: a declared schema exists at runtime, which is what the generator
226
+ * and the compatibility gate read. `_response` on RouteDef disappears after
227
+ * compilation.
228
+ */
229
+
230
+ /**
231
+ * Authentication profile a contracted operation is admitted under.
232
+ *
233
+ * - `none` — the operation is called before any key exists to sign with
234
+ * (enrollment, login), so it carries neither proof nor session headers.
235
+ * - `clientProofV1` — admitted by the @spfn/auth client-proof admission order.
236
+ *
237
+ * The union is deliberately closed. A profile name is part of what the contract
238
+ * publishes to external clients, so adding one is a change to this file rather
239
+ * than a string a route can invent.
240
+ */
241
+ type RouteAuthProfile = 'none' | 'clientProofV1';
242
+ /**
243
+ * The public promise a contracted route makes.
244
+ */
245
+ interface RouteContract {
246
+ /** Contract version this operation first appeared in (e.g. '1.2.0'). */
247
+ since: string;
248
+ /**
249
+ * Response shape. TypeBox schema, declared — not inferred.
250
+ *
251
+ * An operation that answers with no body declares `Type.Null()`.
252
+ */
253
+ response: TSchema;
254
+ /** Authentication profile. Defaults to `'none'`. */
255
+ auth?: RouteAuthProfile;
256
+ /** Whether the call carries a session. Defaults to `false`. */
257
+ requiresSession?: boolean;
258
+ /** Contract version this operation was announced for removal in. */
259
+ deprecatedIn?: string;
260
+ }
261
+
262
+ /**
263
+ * Route Builder
264
+ *
265
+ * Provides tRPC-style chainable API for route definition
266
+ */
267
+
268
+ /**
269
+ * Route handler function
270
+ */
271
+ type RouteHandlerFn<TInput extends RouteInput = RouteInput, TInterceptor extends RouteInput = {}, TResponse = unknown> = (c: RouteBuilderContext<TInput, TInterceptor>) => Response | Promise<Response> | TResponse | Promise<TResponse>;
272
+ /**
273
+ * Route definition result
274
+ *
275
+ * Contains all information needed for type inference and registration
276
+ */
277
+ type RouteDef<TInput extends RouteInput = RouteInput, TInterceptor extends RouteInput = {}, TResponse = unknown> = {
278
+ method?: HttpMethod;
279
+ path?: string;
280
+ input?: TInput;
281
+ interceptor?: TInterceptor;
282
+ middlewares?: (MiddlewareHandler | NamedMiddleware<string>)[];
283
+ skipMiddlewares?: string[] | '*';
284
+ /**
285
+ * Public promise this route makes to separately deployed clients.
286
+ *
287
+ * Present as a runtime value, unlike `_response`: the contract generator and
288
+ * the compatibility gate read it.
289
+ */
290
+ contract?: RouteContract;
291
+ handler: RouteHandlerFn<TInput, TInterceptor, TResponse>;
292
+ _input: TInput;
293
+ _interceptor: TInterceptor;
294
+ _response: TResponse;
295
+ };
296
+ /**
297
+ * Route builder with chainable API (tRPC-style)
298
+ */
299
+ declare class RouteBuilder<TInput extends RouteInput = {}, TInterceptor extends RouteInput = {}, TResponse = never> {
300
+ _method?: HttpMethod;
301
+ _path?: string;
302
+ _input?: TInput;
303
+ _interceptor?: TInterceptor;
304
+ _middlewares?: (MiddlewareHandler | NamedMiddleware<string>)[];
305
+ _skipMiddlewares?: string[] | '*';
306
+ _contract?: RouteContract;
307
+ /**
308
+ * Create a new RouteBuilder with copied properties and optional overrides
309
+ */
310
+ private clone;
311
+ /**
312
+ * Define input schemas
313
+ *
314
+ * @example
315
+ * ```ts
316
+ * route.get('/users/:id')
317
+ * .input({
318
+ * params: Type.Object({ id: Type.String() }),
319
+ * query: Type.Object({ page: Type.Number() }),
320
+ * headers: Type.Object({ authorization: Type.String() })
321
+ * })
322
+ * .handler(async (c) => {
323
+ * const { params, query, headers } = await c.data();
324
+ * // params = { id: string }
325
+ * // query = { page: number }
326
+ * // headers = { authorization: string }
327
+ * })
328
+ * ```
329
+ */
330
+ input<TNewInput extends RouteInput>(input: TNewInput): RouteBuilder<TNewInput, TInterceptor, TResponse>;
331
+ /**
332
+ * Define fields injected by interceptors
333
+ *
334
+ * These fields are:
335
+ * - Available in the handler (merged with input)
336
+ * - Excluded from client types (codegen uses only input)
337
+ * - Not validated by route input schema (injected by middleware)
338
+ *
339
+ * Use this when middleware/interceptors add fields to the request
340
+ * before it reaches the handler.
341
+ *
342
+ * @example
343
+ * ```ts
344
+ * // Auth interceptor injects crypto key fields
345
+ * route.post('/_auth/login')
346
+ * .input({
347
+ * body: Type.Object({
348
+ * email: Type.String(),
349
+ * password: Type.String()
350
+ * })
351
+ * })
352
+ * .interceptor({
353
+ * body: Type.Object({
354
+ * publicKey: Type.String(),
355
+ * keyId: Type.String(),
356
+ * fingerprint: Type.String()
357
+ * })
358
+ * })
359
+ * .handler(async (c) => {
360
+ * const { body } = await c.data();
361
+ * // body type: { email, password, publicKey, keyId, fingerprint }
362
+ * // Client only sees: { email, password }
363
+ * return loginService(body);
364
+ * });
365
+ * ```
366
+ */
367
+ interceptor<TNewInterceptor extends RouteInput>(interceptor: TNewInterceptor): RouteBuilder<TInput, TNewInterceptor, TResponse>;
368
+ /**
369
+ * Add middlewares to the route
370
+ *
371
+ * Accepts both regular middleware handlers and named middlewares (NamedMiddleware).
372
+ * Named middlewares that are already registered globally will be automatically
373
+ * deduplicated to prevent double execution.
374
+ *
375
+ * @example
376
+ * ```ts
377
+ * import { authenticate } from '@spfn/auth/server/middleware';
378
+ *
379
+ * // With NamedMiddleware (auto-deduped if registered globally)
380
+ * route.get('/users')
381
+ * .use([authenticate, RateLimitMiddleware()])
382
+ *
383
+ * // With regular middleware handlers
384
+ * route.get('/users')
385
+ * .use([AuthMiddleware(), RateLimitMiddleware()])
386
+ * ```
387
+ */
388
+ middleware(middlewares: (MiddlewareHandler | NamedMiddleware<string>)[]): RouteBuilder<TInput, TInterceptor, TResponse>;
389
+ /**
390
+ * Add middlewares to the route (alias for `.middleware()`)
391
+ *
392
+ * Accepts both regular middleware handlers and named middlewares (NamedMiddleware).
393
+ * Named middlewares that are already registered globally will be automatically
394
+ * deduplicated to prevent double execution.
395
+ *
396
+ * @example
397
+ * ```ts
398
+ * import { authenticate } from '@spfn/auth/server/middleware';
399
+ *
400
+ * // With NamedMiddleware (auto-deduped if registered globally)
401
+ * route.get('/users')
402
+ * .use([authenticate, RateLimitMiddleware()])
403
+ *
404
+ * // With regular middleware handlers
405
+ * route.get('/users')
406
+ * .use([AuthMiddleware(), RateLimitMiddleware()])
407
+ * ```
408
+ */
409
+ use(middlewares: (MiddlewareHandler | NamedMiddleware<string>)[]): RouteBuilder<TInput, TInterceptor, TResponse>;
410
+ /**
411
+ * Skip server-level named middlewares
412
+ *
413
+ * Useful for public endpoints that should bypass auth or rate limiting
414
+ *
415
+ * @param middlewareNames - Array of middleware names to skip, or '*' to skip all
416
+ *
417
+ * @example
418
+ * ```ts
419
+ * // Skip specific middlewares
420
+ * route.get('/health')
421
+ * .skip(['auth', 'rateLimit'])
422
+ * .handler(async (c) => c.json({ status: 'ok' }));
423
+ *
424
+ * // Skip only auth (still apply rate limiting)
425
+ * route.get('/public-data')
426
+ * .skip(['auth'])
427
+ * .handler(async (c) => { ... });
428
+ *
429
+ * // Skip all middlewares
430
+ * route.get('/public-health')
431
+ * .skip('*')
432
+ * .handler(async (c) => c.json({ status: 'ok' }));
433
+ * ```
434
+ */
435
+ skip(middlewareNames: string[] | '*'): RouteBuilder<TInput, TInterceptor, TResponse>;
436
+ /**
437
+ * Publish this route as a versioned contract operation
438
+ *
439
+ * Marks the route as a promise to clients that are compiled and deployed
440
+ * separately from the server — a mobile app, an external API consumer.
441
+ * The `@spfn/core:contract` generator writes every contracted route into
442
+ * `contracts/current.json`, and the build refuses a change that would break
443
+ * an already-released client.
444
+ *
445
+ * Routes without `.contract()` are unaffected: they simply do not appear in
446
+ * the contract. A web client needs nothing here — it derives its types from
447
+ * the router in the same build.
448
+ *
449
+ * @example
450
+ * ```ts
451
+ * export const getUser = route.get('/users/:id')
452
+ * .input({ params: Type.Object({ id: Type.String() }) })
453
+ * .contract({
454
+ * since: '1.2.0',
455
+ * auth: 'clientProofV1',
456
+ * requiresSession: true,
457
+ * response: Type.Object({
458
+ * id: Type.String(),
459
+ * name: Type.String(),
460
+ * email: Type.Optional(Type.String()),
461
+ * }),
462
+ * })
463
+ * .handler(async (c) => { ... });
464
+ * ```
465
+ */
466
+ contract(contract: RouteContract): RouteBuilder<TInput, TInterceptor, TResponse>;
467
+ /**
468
+ * Define handler function
469
+ *
470
+ * Response type is automatically inferred from the return value.
471
+ * Use helper methods like `c.created()`, `c.paginated()` for proper type inference.
472
+ *
473
+ * @example
474
+ * ```ts
475
+ * // Direct return - type inferred from data
476
+ * route.get('/users/:id')
477
+ * .input({ params: Type.Object({ id: Type.String() }) })
478
+ * .handler(async (c) => {
479
+ * const { params } = await c.data();
480
+ * return await getUser(params.id); // Type: User
481
+ * })
482
+ *
483
+ * // Using c.created() - returns data with 201 status, type preserved
484
+ * route.post('/users')
485
+ * .input({ body: Type.Object({ name: Type.String() }) })
486
+ * .handler(async (c) => {
487
+ * const { body } = await c.data();
488
+ * return c.created(await createUser(body)); // Type: User
489
+ * })
490
+ *
491
+ * // Using c.paginated() - returns PaginatedResult<T>
492
+ * route.get('/users')
493
+ * .handler(async (c) => {
494
+ * const users = await getUsers();
495
+ * return c.paginated(users, 1, 20, 100); // Type: PaginatedResult<User>
496
+ * })
497
+ *
498
+ * // Using c.noContent() - returns void
499
+ * route.delete('/users/:id')
500
+ * .handler(async (c) => {
501
+ * await deleteUser(params.id);
502
+ * return c.noContent(); // Type: void
503
+ * })
504
+ *
505
+ * // Using c.json() - returns Response (type inference lost)
506
+ * // Use only when you need custom status codes not covered by helpers
507
+ * route.get('/custom')
508
+ * .handler(async (c) => {
509
+ * return c.json({ data }, 418); // Type: Response
510
+ * })
511
+ * ```
512
+ */
513
+ handler<THandlerResponse>(fn: RouteHandlerFn<TInput, TInterceptor, THandlerResponse>): RouteDef<TInput, TInterceptor, THandlerResponse>;
514
+ }
515
+ /**
516
+ * Route builder entry point
517
+ *
518
+ * @example
519
+ * ```ts
520
+ * // GET request
521
+ * export const getUser = route.get('/users/:id')
522
+ * .input({ params: Type.Object({ id: Type.String() }) })
523
+ * .handler(async (c) => {
524
+ * const { params } = await c.data();
525
+ * return await db.user.findUnique({ where: { id: params.id } });
526
+ * });
527
+ *
528
+ * // POST request
529
+ * export const createUser = route.post('/users')
530
+ * .input({ body: Type.Object({ name: Type.String(), email: Type.String() }) })
531
+ * .handler(async (c) => {
532
+ * const { body } = await c.data();
533
+ * return c.created(await db.user.create({ data: body }));
534
+ * });
535
+ * ```
536
+ */
537
+ declare const route: {
538
+ get: (path: string) => RouteBuilder;
539
+ post: (path: string) => RouteBuilder;
540
+ put: (path: string) => RouteBuilder;
541
+ patch: (path: string) => RouteBuilder;
542
+ delete: (path: string) => RouteBuilder;
543
+ };
544
+
545
+ /**
546
+ * Router Definition
547
+ *
548
+ * Provides router composition and middleware management
549
+ */
550
+
551
+ /**
552
+ * Router definition - holds all routes
553
+ */
554
+ interface Router<TRoutes extends Record<string, RouteDef<any, any, any> | Router<any>>> {
555
+ routes: TRoutes;
556
+ _routes: TRoutes;
557
+ _packageRouters: Router<any>[];
558
+ _globalMiddlewares: NamedMiddleware<string>[];
559
+ /**
560
+ * Register package routers (type-hidden)
561
+ *
562
+ * Package routes are:
563
+ * - Recognized by RPC proxy and backend
564
+ * - NOT exposed in client types (use package's own API like authApi, cmsApi)
565
+ *
566
+ * @example
567
+ * ```ts
568
+ * import { authRouter } from '@spfn/auth/server';
569
+ * import { cmsAppRouter } from '@spfn/cms/server';
570
+ *
571
+ * export const appRouter = defineRouter({
572
+ * getRoot,
573
+ * getHealth,
574
+ * })
575
+ * .packages([authRouter, cmsAppRouter]);
576
+ *
577
+ * // Client usage:
578
+ * // api.getRoot.call({}) - app routes
579
+ * // authApi.login.call({}) - package API
580
+ * ```
581
+ */
582
+ packages(routers: Router<any>[]): Router<TRoutes>;
583
+ /**
584
+ * Register global middlewares
585
+ *
586
+ * Applied to all routes unless explicitly skipped via .skip()
587
+ *
588
+ * @example
589
+ * ```ts
590
+ * import { authMiddleware, loggingMiddleware } from './middlewares';
591
+ *
592
+ * export const appRouter = defineRouter({
593
+ * getRoot,
594
+ * getHealth,
595
+ * })
596
+ * .packages([authRouter])
597
+ * .use([authMiddleware, loggingMiddleware]);
598
+ * ```
599
+ */
600
+ use(middlewares: NamedMiddleware<string>[]): Router<TRoutes>;
601
+ }
602
+ /**
603
+ * Define a router with multiple routes (tRPC-style)
604
+ *
605
+ * Supports chainable API for packages and middlewares:
606
+ *
607
+ * @example
608
+ * ```ts
609
+ * // Basic usage
610
+ * export const appRouter = defineRouter({
611
+ * getRoot,
612
+ * getHealth,
613
+ * listExamples,
614
+ * });
615
+ *
616
+ * // With package routers (type-hidden)
617
+ * export const appRouter = defineRouter({
618
+ * getRoot,
619
+ * getHealth,
620
+ * })
621
+ * .packages([authRouter, cmsAppRouter]);
622
+ *
623
+ * // With global middlewares
624
+ * export const appRouter = defineRouter({
625
+ * getRoot,
626
+ * getHealth,
627
+ * })
628
+ * .packages([authRouter])
629
+ * .use([authMiddleware, loggingMiddleware]);
630
+ *
631
+ * export type AppRouter = typeof appRouter;
632
+ * ```
633
+ *
634
+ * Package routes:
635
+ * - Recognized by RPC proxy and backend for routing
636
+ * - NOT included in AppRouter type (use authApi, cmsApi instead)
637
+ * - Prevents confusion between app API and package APIs
638
+ */
639
+ declare function defineRouter<TRoutes extends Record<string, RouteDef<any, any, any> | Router<any>>>(routes: TRoutes): Router<TRoutes>;
640
+
641
+ export { type MergedInput as M, type PaginatedResult as P, type RouteDef as R, type Router as a, type RouteInput as b, type RouteBuilderContext as c, type RouteHandlerFn as d, type RouteContract as e, type RouteAuthProfile as f, defineRouter as g, route as r };
@@ -6,9 +6,9 @@ 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 { J as JobRouter, B as BossOptions } from '../boss-gXhgctn6.js';
9
- import { E as EventRouterDef, a as EventDef } from '../token-manager-jKD_EsSE.js';
10
- import { S as SSEHandlerConfig, a as SSEAuthConfig } from '../types-BFB72jbM.js';
11
- import { W as WSRouterDef, a as WSHandlerConfig, b as WSMessageHandlers, c as WSAuthConfig } from '../types-DVjf37yO.js';
9
+ import { E as EventRouterDef, a as EventDef } from '../token-manager-vZeqBbtA.js';
10
+ import { S as SSEHandlerConfig, a as SSEAuthConfig } from '../types-D9uMxeQS.js';
11
+ import { W as WSRouterDef, a as WSHandlerConfig, b as WSMessageHandlers, c as WSAuthConfig } from '../types-CF-37KAG.js';
12
12
  import { DatabaseProvider } from '@spfn/core/db';
13
13
  import '@sinclair/typebox';
14
14
  import 'pg-boss';
@@ -689,6 +689,15 @@ var SSETokenManager = class {
689
689
  this.cleanupTimer = setInterval(() => void this.store.cleanup(), cleanupInterval);
690
690
  this.cleanupTimer.unref();
691
691
  }
692
+ /**
693
+ * How long an issued token stays valid, in milliseconds.
694
+ *
695
+ * Exposed so a caller that has to report an expiry to its client computes the same
696
+ * number this manager stamps on the token, instead of assuming the default.
697
+ */
698
+ get ttlMs() {
699
+ return this.ttl;
700
+ }
692
701
  /**
693
702
  * Issue a new one-time-use token for the given subject
694
703
  */