@spfn/core 0.3.0-beta.2 → 0.3.0-beta.21

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 (94) hide show
  1. package/README.md +343 -16
  2. package/dist/app-config/index.d.ts +134 -0
  3. package/dist/app-config/index.js +58 -0
  4. package/dist/app-config/index.js.map +1 -0
  5. package/dist/authz/index.js +1 -381
  6. package/dist/authz/index.js.map +1 -1
  7. package/dist/{boss-D16fO2oG.d.ts → boss-BiDCbbAQ.d.ts} +60 -7
  8. package/dist/codegen/index.d.ts +93 -15
  9. package/dist/codegen/index.js +626 -165
  10. package/dist/codegen/index.js.map +1 -1
  11. package/dist/config/index.d.ts +0 -63
  12. package/dist/config/index.js +15 -15
  13. package/dist/config/index.js.map +1 -1
  14. package/dist/contract/index.d.ts +40 -17
  15. package/dist/contract/index.js +147 -23
  16. package/dist/contract/index.js.map +1 -1
  17. package/dist/db/index.d.ts +201 -34
  18. package/dist/db/index.js +294 -158
  19. package/dist/db/index.js.map +1 -1
  20. package/dist/{define-middleware-DfDP39Nq.d.ts → define-middleware-CVKgqo8S.d.ts} +1 -1
  21. package/dist/env/index.d.ts +6 -442
  22. package/dist/env/index.js +13 -16
  23. package/dist/env/index.js.map +1 -1
  24. package/dist/env/loader.js +24 -1
  25. package/dist/env/loader.js.map +1 -1
  26. package/dist/error-TsUIjp_x.d.ts +157 -0
  27. package/dist/errors/index.js +1 -381
  28. package/dist/errors/index.js.map +1 -1
  29. package/dist/event/index.d.ts +5 -5
  30. package/dist/event/index.js +32 -1
  31. package/dist/event/index.js.map +1 -1
  32. package/dist/event/sse/client.d.ts +18 -4
  33. package/dist/event/sse/client.js +38 -2
  34. package/dist/event/sse/client.js.map +1 -1
  35. package/dist/event/sse/index.d.ts +5 -4
  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.d.ts +6 -2
  44. package/dist/middleware/index.js +7 -388
  45. package/dist/middleware/index.js.map +1 -1
  46. package/dist/nextjs/index.d.ts +88 -8
  47. package/dist/nextjs/index.js +138 -62
  48. package/dist/nextjs/index.js.map +1 -1
  49. package/dist/nextjs/server.d.ts +45 -7
  50. package/dist/nextjs/server.js +14 -0
  51. package/dist/nextjs/server.js.map +1 -1
  52. package/dist/ops/index.d.ts +62 -43
  53. package/dist/ops/index.js +638 -97
  54. package/dist/ops/index.js.map +1 -1
  55. package/dist/registry-C2KQCh0m.d.ts +473 -0
  56. package/dist/route/index.d.ts +6 -3
  57. package/dist/route/index.js +97 -28
  58. package/dist/route/index.js.map +1 -1
  59. package/dist/route/types.d.ts +7 -1
  60. package/dist/{router-Qbssr11H.d.ts → route-builder-DRW-MSCJ.d.ts} +52 -127
  61. package/dist/router-4YoIy5fN.d.ts +172 -0
  62. package/dist/{token-manager-BT5EnUAR.d.ts → router-CysBmgv3.d.ts} +81 -127
  63. package/dist/server/index.d.ts +185 -14
  64. package/dist/server/index.js +883 -78
  65. package/dist/server/index.js.map +1 -1
  66. package/dist/surface/index.d.ts +115 -0
  67. package/dist/surface/index.js +1003 -0
  68. package/dist/surface/index.js.map +1 -0
  69. package/dist/token-manager-Ckjt2-qq.d.ts +122 -0
  70. package/dist/{types-D1c57Ko-.d.ts → types-5Rl11_Nb.d.ts} +43 -4
  71. package/dist/{types-ZQODsBft.d.ts → types-B8zAxFsE.d.ts} +2 -1
  72. package/dist/types-CxhPH4s_.d.ts +375 -0
  73. package/dist/{types-2AbaW4Ie.d.ts → types-W4u_S0lF.d.ts} +4 -3
  74. package/docs/file-upload.md +195 -333
  75. package/package.json +20 -9
  76. package/src/cache/README.md +330 -0
  77. package/src/codegen/README.md +634 -0
  78. package/src/config/README.md +333 -0
  79. package/src/contract/README.md +440 -0
  80. package/src/db/README.md +589 -0
  81. package/src/db/manager/README.md +500 -0
  82. package/src/db/schema/README.md +354 -0
  83. package/src/db/transaction/README.md +822 -0
  84. package/src/env/README.md +787 -0
  85. package/src/errors/README.md +429 -0
  86. package/src/event/README.md +789 -0
  87. package/src/job/README.md +613 -0
  88. package/src/logger/README.md +321 -0
  89. package/src/middleware/README.md +634 -0
  90. package/src/nextjs/README.md +688 -0
  91. package/src/route/README.md +763 -0
  92. package/src/security/README.md +100 -0
  93. package/src/server/README.md +704 -0
  94. package/dist/types-9oszaJqp.d.ts +0 -250
@@ -1,8 +1,8 @@
1
- import { a as NamedMiddleware } from './define-middleware-DfDP39Nq.js';
2
1
  import { Context, MiddlewareHandler } from 'hono';
2
+ import { a as NamedMiddleware } from './define-middleware-CVKgqo8S.js';
3
3
  import { TSchema, Static } from '@sinclair/typebox';
4
4
  import { ContentfulStatusCode, RedirectStatusCode } from 'hono/utils/http-status';
5
- import { HttpMethod } from './route/types.js';
5
+ import { HttpMethod, RouteEffect } from './route/types.js';
6
6
 
7
7
  /**
8
8
  * Route Input Types
@@ -296,6 +296,13 @@ type RouteDef<TInput extends RouteInput = RouteInput, TInterceptor extends Route
296
296
  * the compatibility gate read it.
297
297
  */
298
298
  contract?: RouteContract;
299
+ /**
300
+ * What invoking this route does. Read by a command surface, which
301
+ * announces it in its manifest; codegen and the contract ignore it.
302
+ */
303
+ effect?: RouteEffect;
304
+ /** One line a command surface announces beside the route; codegen and the contract ignore it. */
305
+ summary?: string;
299
306
  handler: RouteHandlerFn<TInput, TInterceptor, TResponse>;
300
307
  _input: TInput;
301
308
  _interceptor: TInterceptor;
@@ -312,6 +319,8 @@ declare class RouteBuilder<TInput extends RouteInput = {}, TInterceptor extends
312
319
  _middlewares?: (MiddlewareHandler | NamedMiddleware<string>)[];
313
320
  _skipMiddlewares?: string[] | '*';
314
321
  _contract?: RouteContract;
322
+ _effect?: RouteEffect;
323
+ _summary?: string;
315
324
  /**
316
325
  * Create a new RouteBuilder with copied properties and optional overrides
317
326
  */
@@ -425,7 +434,7 @@ declare class RouteBuilder<TInput extends RouteInput = {}, TInterceptor extends
425
434
  * @example
426
435
  * ```ts
427
436
  * // Skip specific middlewares
428
- * route.get('/health')
437
+ * route.get('/status')
429
438
  * .skip(['auth', 'rateLimit'])
430
439
  * .handler(async (c) => c.json({ status: 'ok' }));
431
440
  *
@@ -472,6 +481,45 @@ declare class RouteBuilder<TInput extends RouteInput = {}, TInterceptor extends
472
481
  * ```
473
482
  */
474
483
  contract(contract: RouteContract): RouteBuilder<TInput, TInterceptor, TResponse>;
484
+ /**
485
+ * Declare what invoking this route does
486
+ *
487
+ * Read by a command surface (`@spfn/core/surface`, `@spfn/core/ops`),
488
+ * which announces it in its manifest so a machine caller can tell a read
489
+ * from a write before it calls — `spfn ops call` asks for `--yes` before a
490
+ * `destructive` command. A surface built with `defineSurface` refuses an
491
+ * app command that does not declare one; on `/_ops` it is optional.
492
+ *
493
+ * Nothing else reads it: codegen, the route map, and the contract are
494
+ * unaffected, and a route outside any surface simply carries it.
495
+ *
496
+ * @example
497
+ * ```ts
498
+ * export const pageAdd = app.route.post('/pages')
499
+ * .input({ body: PageAddBody })
500
+ * .effect('write')
501
+ * .summary('Add a page to the blueprint')
502
+ * .handler(async (c) => { ... });
503
+ * ```
504
+ */
505
+ effect(effect: RouteEffect): RouteBuilder<TInput, TInterceptor, TResponse>;
506
+ /**
507
+ * Describe this route in one line for a command surface's manifest
508
+ *
509
+ * A surface announces it beside the command in `spfn ops list` and
510
+ * `--describe`, and refuses one that is empty or longer than 500
511
+ * characters. Codegen, the route map, and the contract ignore it. On a
512
+ * module command the module's own summary wins.
513
+ *
514
+ * @example
515
+ * ```ts
516
+ * export const pageAdd = app.route.post('/pages')
517
+ * .effect('write')
518
+ * .summary('Add a page to the blueprint')
519
+ * .handler(async (c) => { ... });
520
+ * ```
521
+ */
522
+ summary(summary: string): RouteBuilder<TInput, TInterceptor, TResponse>;
475
523
  /**
476
524
  * Define handler function
477
525
  *
@@ -550,127 +598,4 @@ declare const route: {
550
598
  delete: (path: string) => RouteBuilder;
551
599
  };
552
600
 
553
- /**
554
- * Router Definition
555
- *
556
- * Provides router composition and middleware management
557
- */
558
-
559
- /**
560
- * Router definition - holds all routes
561
- */
562
- interface Router<TRoutes extends Record<string, RouteDef<any, any, any> | Router<any>>> {
563
- routes: TRoutes;
564
- _routes: TRoutes;
565
- _packageRouters: Router<any>[];
566
- _globalMiddlewares: NamedMiddleware<string>[];
567
- /** The contract version these routes publish, or null when uncontracted. */
568
- _contractVersion: string | null;
569
- /**
570
- * Register package routers (type-hidden)
571
- *
572
- * Package routes are:
573
- * - Recognized by RPC proxy and backend
574
- * - NOT exposed in client types (use package's own API like authApi, cmsApi)
575
- *
576
- * @example
577
- * ```ts
578
- * import { authRouter } from '@spfn/auth/server';
579
- * import { cmsAppRouter } from '@spfn/cms/server';
580
- *
581
- * export const appRouter = defineRouter({
582
- * getRoot,
583
- * getHealth,
584
- * })
585
- * .packages([authRouter, cmsAppRouter]);
586
- *
587
- * // Client usage:
588
- * // api.getRoot.call({}) - app routes
589
- * // authApi.login.call({}) - package API
590
- * ```
591
- */
592
- packages(routers: Router<any>[]): Router<TRoutes>;
593
- /**
594
- * Register global middlewares
595
- *
596
- * Applied to all routes unless explicitly skipped via .skip()
597
- *
598
- * @example
599
- * ```ts
600
- * import { authMiddleware, loggingMiddleware } from './middlewares';
601
- *
602
- * export const appRouter = defineRouter({
603
- * getRoot,
604
- * getHealth,
605
- * })
606
- * .packages([authRouter])
607
- * .use([authMiddleware, loggingMiddleware]);
608
- * ```
609
- */
610
- use(middlewares: NamedMiddleware<string>[]): Router<TRoutes>;
611
- /**
612
- * Declare the contract version these routes publish.
613
- *
614
- * A client compiled against this server — a mobile app in a store — is
615
- * generated from one version of the contract and cannot be updated when the
616
- * server changes. The server announces this version on every response so
617
- * that client can tell whether the two ends still agree.
618
- *
619
- * This is the version's source. A released snapshot is written to
620
- * `contracts/released/<version>.json` from what is declared here, so the
621
- * filename follows the code rather than the code having to be told what the
622
- * filename said.
623
- *
624
- * Only a server with contracted routes needs it. Without it the contract
625
- * generator still writes `current.json` and still runs the compatibility
626
- * gate; what it cannot do is cut a release or announce a version.
627
- *
628
- * @example
629
- * ```ts
630
- * export const appRouter = defineRouter({ getRoot, listItems })
631
- * .contractVersion('1.2.0')
632
- * .packages([authRouter]);
633
- * ```
634
- */
635
- contractVersion(version: string): Router<TRoutes>;
636
- }
637
- /**
638
- * Define a router with multiple routes (tRPC-style)
639
- *
640
- * Supports chainable API for packages and middlewares:
641
- *
642
- * @example
643
- * ```ts
644
- * // Basic usage
645
- * export const appRouter = defineRouter({
646
- * getRoot,
647
- * getHealth,
648
- * listExamples,
649
- * });
650
- *
651
- * // With package routers (type-hidden)
652
- * export const appRouter = defineRouter({
653
- * getRoot,
654
- * getHealth,
655
- * })
656
- * .packages([authRouter, cmsAppRouter]);
657
- *
658
- * // With global middlewares
659
- * export const appRouter = defineRouter({
660
- * getRoot,
661
- * getHealth,
662
- * })
663
- * .packages([authRouter])
664
- * .use([authMiddleware, loggingMiddleware]);
665
- *
666
- * export type AppRouter = typeof appRouter;
667
- * ```
668
- *
669
- * Package routes:
670
- * - Recognized by RPC proxy and backend for routing
671
- * - NOT included in AppRouter type (use authApi, cmsApi instead)
672
- * - Prevents confusion between app API and package APIs
673
- */
674
- declare function defineRouter<TRoutes extends Record<string, RouteDef<any, any, any> | Router<any>>>(routes: TRoutes): Router<TRoutes>;
675
-
676
- export { type MergedInput as M, type PaginatedResult as P, type Router as R, type RouteDef as a, RouteBuilder as b, type RouteAuthProfile as c, type RouteBuilderContext as d, type RouteContract as e, type RouteHandlerFn as f, type RouteInput as g, defineRouter as h, route as r };
601
+ export { type MergedInput as M, type PaginatedResult as P, type RouteDef as R, type RouteAuthProfile as a, type RouteBuilderContext as b, type RouteContract as c, type RouteHandlerFn as d, type RouteInput as e, RouteBuilder as f, route as r };
@@ -0,0 +1,172 @@
1
+ import { a as NamedMiddleware } from './define-middleware-CVKgqo8S.js';
2
+ import { R as RouteDef } from './route-builder-DRW-MSCJ.js';
3
+
4
+ /**
5
+ * Router Definition
6
+ *
7
+ * Provides router composition and middleware management
8
+ */
9
+
10
+ /**
11
+ * Router definition - holds all routes
12
+ */
13
+ interface Router<TRoutes extends Record<string, RouteDef<any, any, any> | Router<any>>> {
14
+ routes: TRoutes;
15
+ _routes: TRoutes;
16
+ _packageRouters: Router<any>[];
17
+ _globalMiddlewares: NamedMiddleware<string>[];
18
+ /** The contract version these routes publish, or null when uncontracted. */
19
+ _contractVersion: string | null;
20
+ /**
21
+ * Whether a client ever addresses these routes by name.
22
+ *
23
+ * True for an ordinary router. A mounted package router that is true here
24
+ * has its routes written into the app's generated route map, which is what
25
+ * lets the package's own client (`authApi.login`) resolve a name through the
26
+ * app's RPC proxy. It is also what makes an app route sharing a name with a
27
+ * package route a defect worth refusing a build over: the map holds one
28
+ * entry per name, so one of the two paths would become unreachable while the
29
+ * generated types still describe the app's route.
30
+ *
31
+ * False says no client names these routes: they are reached by URL, by
32
+ * something that was handed the URL, so nothing of them belongs in the map
33
+ * and a name they share with an app route is only a shared name.
34
+ * `createOpsRouter` sets it — `spfn ops` invokes an ops command over the URL
35
+ * the manifest gave it.
36
+ */
37
+ _publishesRouteMap: boolean;
38
+ /**
39
+ * The namespace of the command surface that built this router, absent on
40
+ * any other. Set by `defineSurface` (and so by `createOpsRouter`) as a
41
+ * non-enumerable property, so `.packages()` can refuse two surfaces on one
42
+ * namespace.
43
+ */
44
+ readonly _surfaceNamespace?: string;
45
+ /**
46
+ * Register package routers (type-hidden)
47
+ *
48
+ * Package routes are:
49
+ * - Recognized by RPC proxy and backend
50
+ * - NOT exposed in client types (use package's own API like authApi, cmsApi)
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * import { authRouter } from '@spfn/auth/server';
55
+ * import { cmsAppRouter } from '@spfn/cms/server';
56
+ *
57
+ * export const appRouter = defineRouter({
58
+ * getRoot,
59
+ * getStatus,
60
+ * })
61
+ * .packages([authRouter, cmsAppRouter]);
62
+ *
63
+ * // Client usage:
64
+ * // api.getRoot.call({}) - app routes
65
+ * // authApi.login.call({}) - package API
66
+ * ```
67
+ */
68
+ packages(routers: Router<any>[]): Router<TRoutes>;
69
+ /**
70
+ * Register global middlewares
71
+ *
72
+ * Applied to all routes unless explicitly skipped via .skip()
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * import { authMiddleware, loggingMiddleware } from './middlewares';
77
+ *
78
+ * export const appRouter = defineRouter({
79
+ * getRoot,
80
+ * getStatus,
81
+ * })
82
+ * .packages([authRouter])
83
+ * .use([authMiddleware, loggingMiddleware]);
84
+ * ```
85
+ */
86
+ use(middlewares: NamedMiddleware<string>[]): Router<TRoutes>;
87
+ /**
88
+ * Declare the contract version these routes publish.
89
+ *
90
+ * A client compiled against this server — a mobile app in a store — is
91
+ * generated from one version of the contract and cannot be updated when the
92
+ * server changes. The server announces this version on every response so
93
+ * that client can tell whether the two ends still agree.
94
+ *
95
+ * This is the version's source. A released snapshot is written to
96
+ * `contracts/released/<version>.json` from what is declared here, so the
97
+ * filename follows the code rather than the code having to be told what the
98
+ * filename said.
99
+ *
100
+ * Only a server with contracted routes needs it. Without it the contract
101
+ * generator still writes `current.json` and still runs the compatibility
102
+ * gate; what it cannot do is cut a release or announce a version.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * export const appRouter = defineRouter({ getRoot, listItems })
107
+ * .contractVersion('1.2.0')
108
+ * .packages([authRouter]);
109
+ * ```
110
+ */
111
+ contractVersion(version: string): Router<TRoutes>;
112
+ }
113
+ /**
114
+ * Define a router with multiple routes (tRPC-style)
115
+ *
116
+ * Supports chainable API for packages and middlewares:
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * // Basic usage
121
+ * export const appRouter = defineRouter({
122
+ * getRoot,
123
+ * getStatus,
124
+ * listExamples,
125
+ * });
126
+ *
127
+ * // With package routers (type-hidden)
128
+ * export const appRouter = defineRouter({
129
+ * getRoot,
130
+ * getStatus,
131
+ * })
132
+ * .packages([authRouter, cmsAppRouter]);
133
+ *
134
+ * // With global middlewares
135
+ * export const appRouter = defineRouter({
136
+ * getRoot,
137
+ * getStatus,
138
+ * })
139
+ * .packages([authRouter])
140
+ * .use([authMiddleware, loggingMiddleware]);
141
+ *
142
+ * export type AppRouter = typeof appRouter;
143
+ * ```
144
+ *
145
+ * Package routes:
146
+ * - Recognized by RPC proxy and backend for routing
147
+ * - NOT included in AppRouter type (use authApi, cmsApi instead)
148
+ * - Prevents confusion between app API and package APIs
149
+ */
150
+ declare function defineRouter<TRoutes extends Record<string, RouteDef<any, any, any> | Router<any>>>(routes: TRoutes): Router<TRoutes>;
151
+ /**
152
+ * A router no client addresses by name.
153
+ *
154
+ * The app's generated route map carries the routes of every package router the
155
+ * app mounts with `.packages()`, so that a package client calling its own route
156
+ * by name resolves through the app's RPC proxy. A router built here is left out
157
+ * of that map: its routes are reached by URL, by a tool that was told the URL,
158
+ * so no name of theirs has to resolve anywhere and a name shared with an app
159
+ * route is only a shared name.
160
+ *
161
+ * Declared by the package that ships the router, because only it knows whether
162
+ * any client names these routes: `createOpsRouter` and the tracking routes of
163
+ * `@spfn/notification` do it, and any package whose surface is reached by URL
164
+ * may. The obligation is the whole of it — a package whose client *does* name
165
+ * its routes must use `defineRouter`, or those names land in nobody's map and
166
+ * resolve nowhere (and a collision that really would take an app route's entry
167
+ * generates silently). `defineRouter` is the default for exactly that reason: a
168
+ * package that says nothing is carried and checked.
169
+ */
170
+ declare function defineUnmappedRouter<TRoutes extends Record<string, RouteDef<any, any, any> | Router<any>>>(routes: TRoutes): Router<TRoutes>;
171
+
172
+ export { type Router as R, defineUnmappedRouter as a, defineRouter as d };
@@ -25,6 +25,21 @@ type EventHandler<TPayload> = (payload: TPayload) => void | Promise<void>;
25
25
  * Job queue sender function type (used by job module)
26
26
  */
27
27
  type JobQueueSender = (queueName: string, payload: unknown) => Promise<void>;
28
+ /**
29
+ * What a contracted event promises to clients compiled separately from the
30
+ * server, written into `contracts/current.json` by `@spfn/core:contract`.
31
+ *
32
+ * The same version markers a route's `.contract()` carries; the payload shape
33
+ * is the event's own schema, so it is not repeated here.
34
+ */
35
+ interface EventContract {
36
+ /** Contract version the event first appeared in. */
37
+ since: string;
38
+ /** Contract version the event was announced for removal in. */
39
+ deprecatedIn?: string;
40
+ /** Contract version the event was removed in. */
41
+ removedIn?: string;
42
+ }
28
43
  /**
29
44
  * Event definition interface
30
45
  */
@@ -37,6 +52,22 @@ interface EventDef<TPayload = void> {
37
52
  * TypeBox payload schema (optional)
38
53
  */
39
54
  readonly schema?: TSchema;
55
+ /**
56
+ * The contract declared with `.contract()`, absent when the event is not
57
+ * part of the published contract.
58
+ *
59
+ * Named with an underscore because `.contract()` holds the plain name, as
60
+ * a router's `_contractVersion` sits beside `.contractVersion()`.
61
+ */
62
+ readonly _contract?: EventContract;
63
+ /**
64
+ * Publish this event in `contracts/current.json`.
65
+ *
66
+ * Sets the contract on this definition and returns it. An event is defined
67
+ * once and shared, so a second call throws instead of one of the two
68
+ * declarations silently winning.
69
+ */
70
+ contract: (contract: EventContract) => EventDef<TPayload>;
40
71
  /**
41
72
  * Subscribe to this event (in-memory handler)
42
73
  */
@@ -104,9 +135,32 @@ type InferEventPayload$1<TEvent> = TEvent extends EventDef<infer TPayload> ? TPa
104
135
  */
105
136
 
106
137
  /**
107
- * Event Router Definition
138
+ * How a client reaches the contracted events of a router.
139
+ *
140
+ * Declared on the router because it is the stream's promise, not one event's:
141
+ * `createServer` checks it against `.events()` at boot, so the document cannot
142
+ * describe a path or an auth mode the server does not serve.
108
143
  */
109
- interface EventRouterDef<TEvents extends Record<string, EventDef<any>>> {
144
+ interface EventRouterContract {
145
+ /**
146
+ * `'tokenExchange'` when `.events()` enables `auth` — the client first posts
147
+ * to the token path and opens the stream with `?token=`. `'none'` otherwise.
148
+ */
149
+ auth: 'none' | 'tokenExchange';
150
+ /**
151
+ * The stream path `.events()` registers.
152
+ * @default '/events/stream'
153
+ */
154
+ streamPath?: string;
155
+ }
156
+ /**
157
+ * What every router of events carries — the SSE event router and the
158
+ * WebSocket router alike.
159
+ *
160
+ * The WebSocket router builds on this rather than on `EventRouterDef`, because
161
+ * only an SSE event router can be contracted.
162
+ */
163
+ interface EventRouterBase<TEvents extends Record<string, EventDef<any>>> {
110
164
  /**
111
165
  * Event definitions
112
166
  */
@@ -122,21 +176,42 @@ interface EventRouterDef<TEvents extends Record<string, EventDef<any>>> {
122
176
  [K in keyof TEvents]: TEvents[K]['_payload'];
123
177
  };
124
178
  }
179
+ /**
180
+ * Event Router Definition
181
+ */
182
+ interface EventRouterDef<TEvents extends Record<string, EventDef<any>>> extends EventRouterBase<TEvents> {
183
+ /**
184
+ * The contract declared with `.contract()`, absent when the router declares
185
+ * none. Underscored for the same reason as `EventDef._contract`.
186
+ */
187
+ readonly _contract?: EventRouterContract;
188
+ /**
189
+ * Declare how a client reaches this router's contracted events.
190
+ *
191
+ * Sets the contract on this router and returns it. A `streamPath` that does
192
+ * not start with `/` throws here, where the typo is.
193
+ */
194
+ contract: (contract: EventRouterContract) => EventRouterDef<TEvents>;
195
+ }
125
196
  /**
126
197
  * Infer event names from EventRouter
127
198
  */
128
- type InferEventNames<T> = T extends EventRouterDef<infer E> ? keyof E & string : never;
199
+ type InferEventNames<T> = T extends EventRouterBase<infer E> ? keyof E & string : never;
129
200
  /**
130
201
  * Infer payload type for specific event
131
202
  */
132
- type InferEventPayload<T extends EventRouterDef<any>, K extends InferEventNames<T>> = T['_types'][K];
203
+ type InferEventPayload<T extends EventRouterBase<any>, K extends InferEventNames<T>> = T['_types'][K];
133
204
  /**
134
205
  * Infer all event payloads map
135
206
  */
136
- type InferEventPayloads<T extends EventRouterDef<any>> = T['_types'];
207
+ type InferEventPayloads<T extends EventRouterBase<any>> = T['_types'];
137
208
  /**
138
209
  * Define an event router for SSE subscription
139
210
  *
211
+ * `.contract({ auth })` on the router, together with `.contract({ since })` on
212
+ * its events, publishes those events in `contracts/current.json` — see the
213
+ * contract README.
214
+ *
140
215
  * @example
141
216
  * ```typescript
142
217
  * export const eventRouter = defineEventRouter({
@@ -154,125 +229,4 @@ type InferEventPayloads<T extends EventRouterDef<any>> = T['_types'];
154
229
  */
155
230
  declare function defineEventRouter<TEvents extends Record<string, EventDef<any>>>(events: TEvents): EventRouterDef<TEvents>;
156
231
 
157
- /**
158
- * SSE Token Manager
159
- *
160
- * Auth-agnostic token issuance and verification for SSE connections.
161
- * Issues one-time-use tokens with TTL for Token Exchange pattern.
162
- *
163
- * @example
164
- * ```typescript
165
- * const manager = new SSETokenManager({ ttl: 30000 });
166
- *
167
- * // Issue token for authenticated user
168
- * const token = await manager.issue('user-123');
169
- *
170
- * // Verify and consume token (one-time use)
171
- * const subject = await manager.verify(token); // 'user-123'
172
- * const again = await manager.verify(token); // null (already consumed)
173
- *
174
- * // Cleanup on shutdown
175
- * manager.destroy();
176
- * ```
177
- */
178
- /**
179
- * Minimal cache client interface (compatible with ioredis Redis | Cluster)
180
- */
181
- type CacheClient = {
182
- set(key: string, value: string, ...args: any[]): Promise<any>;
183
- getdel?(key: string): Promise<string | null>;
184
- get(key: string): Promise<string | null>;
185
- del(...keys: string[]): Promise<number>;
186
- };
187
- /**
188
- * Stored SSE token data
189
- */
190
- interface SSEToken {
191
- subject: string;
192
- expiresAt: number;
193
- }
194
- /**
195
- * Token storage interface
196
- *
197
- * Implement this for custom storage backends (e.g., Redis for multi-instance).
198
- */
199
- interface SSETokenStore {
200
- /** Store a token */
201
- set(token: string, data: SSEToken): Promise<void>;
202
- /** Get and delete a token (one-time use) */
203
- consume(token: string): Promise<SSEToken | null>;
204
- /** Remove expired tokens */
205
- cleanup(): Promise<void>;
206
- }
207
- /**
208
- * SSETokenManager configuration
209
- */
210
- interface SSETokenManagerConfig {
211
- /**
212
- * Token time-to-live in milliseconds
213
- * @default 30000
214
- */
215
- ttl?: number;
216
- /**
217
- * Custom token store (default: in-memory Map)
218
- */
219
- store?: SSETokenStore;
220
- /**
221
- * Cleanup interval in milliseconds
222
- * @default 60000
223
- */
224
- cleanupInterval?: number;
225
- }
226
- /**
227
- * Redis/Valkey-backed token store for multi-instance deployments.
228
- *
229
- * Uses SET EX for automatic TTL expiry and GETDEL for atomic one-time consumption.
230
- * No cleanup needed — Redis handles expiration automatically.
231
- *
232
- * @example
233
- * ```typescript
234
- * import { getCache } from '@spfn/core/cache';
235
- *
236
- * const cache = getCache();
237
- * if (cache) {
238
- * const store = new CacheTokenStore(cache);
239
- * const manager = new SSETokenManager({ store });
240
- * }
241
- * ```
242
- */
243
- declare class CacheTokenStore implements SSETokenStore {
244
- private cache;
245
- private prefix;
246
- constructor(cache: CacheClient);
247
- set(token: string, data: SSEToken): Promise<void>;
248
- consume(token: string): Promise<SSEToken | null>;
249
- cleanup(): Promise<void>;
250
- }
251
- declare class SSETokenManager {
252
- private store;
253
- private ttl;
254
- private cleanupTimer;
255
- constructor(config?: SSETokenManagerConfig);
256
- /**
257
- * How long an issued token stays valid, in milliseconds.
258
- *
259
- * Exposed so a caller that has to report an expiry to its client computes the same
260
- * number this manager stamps on the token, instead of assuming the default.
261
- */
262
- get ttlMs(): number;
263
- /**
264
- * Issue a new one-time-use token for the given subject
265
- */
266
- issue(subject: string): Promise<string>;
267
- /**
268
- * Verify and consume a token
269
- * @returns subject string if valid, null if invalid/expired/already consumed
270
- */
271
- verify(token: string): Promise<string | null>;
272
- /**
273
- * Cleanup timer and resources
274
- */
275
- destroy(): void;
276
- }
277
-
278
- export { CacheTokenStore as C, type EventDef as E, type InferEventNames as I, type JobQueueSender as J, type PubSubCache as P, SSETokenManager as S, type EventHandler as a, type EventRouterDef as b, type InferEventPayload$1 as c, type InferEventPayloads as d, type InferEventPayload as e, defineEventRouter as f, type SSEToken as g, type SSETokenManagerConfig as h, type SSETokenStore as i };
232
+ export { type EventRouterDef as E, type InferEventNames as I, type JobQueueSender as J, type PubSubCache as P, type EventDef as a, type EventContract as b, type EventHandler as c, type EventRouterContract as d, type InferEventPayload$1 as e, type InferEventPayloads as f, type InferEventPayload as g, defineEventRouter as h, type EventRouterBase as i };