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

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 +304 -12
  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 +41 -11
  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,406 @@
1
+ import { TSchema } from '@sinclair/typebox';
2
+
3
+ /**
4
+ * Event System Types
5
+ */
6
+
7
+ /**
8
+ * Pub/Sub capable cache interface for multi-instance events
9
+ */
10
+ interface PubSubCache {
11
+ /**
12
+ * Publish a message to a channel
13
+ */
14
+ publish(channel: string, message: unknown): Promise<void>;
15
+ /**
16
+ * Subscribe to a channel
17
+ */
18
+ subscribe(channel: string, handler: (message: unknown) => void | Promise<void>): Promise<void>;
19
+ }
20
+ /**
21
+ * Event handler function type
22
+ */
23
+ type EventHandler<TPayload> = (payload: TPayload) => void | Promise<void>;
24
+ /**
25
+ * Options for `EventDef.subscribe`.
26
+ */
27
+ interface EventSubscribeOptions {
28
+ /**
29
+ * Run only for an `emit` made in this process, never for a delivery the
30
+ * cache transport received from another pod. For a handler whose work must
31
+ * happen once per emit, not once per pod (it would otherwise run on every
32
+ * pod the event is cached to). Without a cache it runs on every `emit`.
33
+ */
34
+ localOnly?: boolean;
35
+ }
36
+ /**
37
+ * Job queue sender function type (used by job module)
38
+ */
39
+ type JobQueueSender = (queueName: string, payload: unknown) => Promise<void>;
40
+ /**
41
+ * What a contracted event promises to clients compiled separately from the
42
+ * server, written into `contracts/current.json` by `@spfn/core:contract`.
43
+ *
44
+ * The same version markers a route's `.contract()` carries; the payload shape
45
+ * is the event's own schema, so it is not repeated here.
46
+ */
47
+ interface EventContract {
48
+ /** Contract version the event first appeared in. */
49
+ since: string;
50
+ /** Contract version the event was announced for removal in. */
51
+ deprecatedIn?: string;
52
+ /** Contract version the event was removed in. */
53
+ removedIn?: string;
54
+ }
55
+ /**
56
+ * What the stream request asked for, copied once at connect.
57
+ *
58
+ * A plain snapshot, never the live Hono context, so a frame filter can read it
59
+ * on every frame long after the request object is gone. Header names are
60
+ * lowercased. It may hold `cookie` and `authorization` values — never log it.
61
+ */
62
+ interface SubscribeRequest {
63
+ /** Query parameters of the stream URL (`events`, `token` and any the app adds). */
64
+ query: Record<string, string>;
65
+ /** Request headers, names lowercased. */
66
+ headers: Record<string, string>;
67
+ }
68
+ /**
69
+ * What `.canSubscribe()` decides on, once per connection per requested event.
70
+ *
71
+ * `P` is whatever the SSE auth config's `resolvePrincipal` returns — core never
72
+ * knows what a principal is.
73
+ */
74
+ interface SubscribeContext<P = unknown> {
75
+ /** The subject the one-time token was issued to. */
76
+ subject: string;
77
+ /** The value `resolvePrincipal` returned for this connection, `undefined` without one. */
78
+ principal: P | undefined;
79
+ /** The connect request snapshot. */
80
+ request: SubscribeRequest;
81
+ }
82
+ /**
83
+ * Decides at connect whether a subject may receive an event. `false` or a
84
+ * throw denies the event on that connection; the connection stays open.
85
+ */
86
+ type SubscribePredicate<P = unknown> = (ctx: SubscribeContext<P>) => boolean | Promise<boolean>;
87
+ /**
88
+ * The frames a connection receives right after `connected`, for its own
89
+ * subject and query. Asked once per connection, after `canSubscribe` allowed
90
+ * the event; a throw or a rejection sends nothing for the event.
91
+ */
92
+ type InitialProducer<TPayload, P = unknown> = (ctx: SubscribeContext<P>) => TPayload[] | Promise<TPayload[]>;
93
+ /**
94
+ * What a per-event `.filter()` sees on every frame.
95
+ */
96
+ interface FrameContext {
97
+ /** The connection's subject, `undefined` when the stream runs without auth. */
98
+ subject: string | undefined;
99
+ /** The connect request snapshot. */
100
+ request: SubscribeRequest;
101
+ }
102
+ /**
103
+ * Decides per frame whether one connection receives one payload. Sync: it runs
104
+ * for every connection on every emit, so it matches the payload against what
105
+ * the connection asked for and never reads storage.
106
+ */
107
+ type FramePredicate<TPayload> = (payload: TPayload, ctx: FrameContext) => boolean;
108
+ /**
109
+ * Which of a subject's connections a `grant` / `revoke` reaches: those whose
110
+ * connect query carries every named field with exactly that value. A
111
+ * connection that declared no such field never matches. Plain JSON, so it
112
+ * travels to other pods unchanged.
113
+ */
114
+ interface SubscriptionMatch {
115
+ query: Record<string, string>;
116
+ }
117
+ /**
118
+ * How an SSE handler lets `grant` / `revoke` reach its open connections.
119
+ * Each handler registers one per router key of the event.
120
+ */
121
+ interface SubscriptionControl {
122
+ /** Activate the event on every open connection of `subject` (matching `match`, when given) where it is denied. */
123
+ grant: (subject: string, match?: SubscriptionMatch) => void;
124
+ /** Deactivate the event on every open connection of `subject` (matching `match`, when given) where it is allowed. */
125
+ revoke: (subject: string, match?: SubscriptionMatch) => void;
126
+ }
127
+ /**
128
+ * Event definition interface
129
+ */
130
+ interface EventDef<TPayload = void> {
131
+ /**
132
+ * Unique event name
133
+ */
134
+ readonly name: string;
135
+ /**
136
+ * TypeBox payload schema (optional)
137
+ */
138
+ readonly schema?: TSchema;
139
+ /**
140
+ * The contract declared with `.contract()`, absent when the event is not
141
+ * part of the published contract.
142
+ *
143
+ * Named with an underscore because `.contract()` holds the plain name, as
144
+ * a router's `_contractVersion` sits beside `.contractVersion()`.
145
+ */
146
+ readonly _contract?: EventContract;
147
+ /**
148
+ * Publish this event in `contracts/current.json`.
149
+ *
150
+ * Sets the contract on this definition and returns it. An event is defined
151
+ * once and shared, so a second call throws instead of one of the two
152
+ * declarations silently winning.
153
+ */
154
+ contract: (contract: EventContract) => EventDef<TPayload>;
155
+ /**
156
+ * The predicate declared with `.canSubscribe()`, absent when any subject
157
+ * that passes the global `authorize` may subscribe.
158
+ */
159
+ readonly _canSubscribe?: SubscribePredicate<any>;
160
+ /**
161
+ * Declare who may subscribe to this event over SSE.
162
+ *
163
+ * Runs once per connection, after the global `authorize`, only when the
164
+ * connection has a subject. `P` is the type of the principal the SSE auth
165
+ * config's `resolvePrincipal` returns. Set once: a second call throws.
166
+ *
167
+ * @example
168
+ * ```typescript
169
+ * export const auditLogged = defineEvent('audit.logged', payload)
170
+ * .canSubscribe<AuthPrincipal>(({ principal }) => principal?.role === 'admin');
171
+ * ```
172
+ */
173
+ canSubscribe: <P = unknown>(predicate: SubscribePredicate<P>) => EventDef<TPayload>;
174
+ /**
175
+ * The per-frame predicate declared with `.filter()`.
176
+ */
177
+ readonly _filter?: FramePredicate<TPayload>;
178
+ /**
179
+ * Declare which frames of this event a connection receives.
180
+ *
181
+ * Runs per frame on every connection subscribed to the event, with or
182
+ * without a subject, beside the global `filter` map (both must pass). Sync.
183
+ * Set once: a second call throws.
184
+ *
185
+ * @example
186
+ * ```typescript
187
+ * export const runtimeChanged = defineEvent('runtime.changed', payload)
188
+ * .filter((payload, { request }) => payload.workspaceId === request.query.workspaceId);
189
+ * ```
190
+ */
191
+ filter: (predicate: FramePredicate<TPayload>) => EventDef<TPayload>;
192
+ /**
193
+ * The producer declared with `.initial()`, absent when a connection
194
+ * receives only the frames emitted after it opened.
195
+ */
196
+ readonly _initial?: InitialProducer<TPayload, any>;
197
+ /**
198
+ * Declare the frames a connection receives right after `connected`, before
199
+ * any live frame — the event's current value for that connection.
200
+ *
201
+ * Runs once per connection with a subject, for an event `canSubscribe`
202
+ * allowed, with the same context `canSubscribe` receives. Its frames skip
203
+ * the frame filters: they were produced for this connection. A throw sends
204
+ * nothing for the event and leaves it out of `connected.initial`. Never on
205
+ * a `grant`. Set once: a second call throws.
206
+ *
207
+ * @example
208
+ * ```typescript
209
+ * export const runtimeChanged = defineEvent('runtime.changed', payload)
210
+ * .initial(async ({ request }) => [await readRuntime(request.query.workspaceId)]);
211
+ * ```
212
+ */
213
+ initial: <P = unknown>(producer: InitialProducer<TPayload, P>) => EventDef<TPayload>;
214
+ /**
215
+ * Subscribe to this event (in-memory handler). With the cache transport a
216
+ * handler runs on every pod for every emit, unless `{ localOnly: true }`
217
+ * keeps it to emits made in this process.
218
+ */
219
+ subscribe: (handler: EventHandler<TPayload>, options?: EventSubscribeOptions) => () => void;
220
+ /**
221
+ * Unsubscribe all handlers, `localOnly` ones included
222
+ */
223
+ unsubscribeAll: () => void;
224
+ /**
225
+ * Emit the event (triggers all subscribers and queued jobs)
226
+ */
227
+ emit: TPayload extends void ? () => Promise<void> : (payload: TPayload) => Promise<void>;
228
+ /**
229
+ * Activate this event on every open SSE connection of `subject` where it is
230
+ * denied — by `canSubscribe` or `authorize` at connect, or by a `revoke`.
231
+ *
232
+ * Reaches every pod when the cache transport is attached. Never re-runs
233
+ * `canSubscribe`: it is an operator's statement about an open connection.
234
+ * An event the connection never requested cannot be granted.
235
+ *
236
+ * With `match`, only the connections whose connect query carries every
237
+ * field of `match.query` with that value are activated.
238
+ */
239
+ grant: (subject: string, match?: SubscriptionMatch) => Promise<void>;
240
+ /**
241
+ * Deactivate this event on every open SSE connection of `subject` where it
242
+ * is allowed; the client receives a `subscription.revoked` frame.
243
+ *
244
+ * Reaches every pod when the cache transport is attached. Affects open
245
+ * connections only — the next connect asks `canSubscribe` afresh.
246
+ *
247
+ * With `match`, only the connections whose connect query carries every
248
+ * field of `match.query` with that value are deactivated.
249
+ */
250
+ revoke: (subject: string, match?: SubscriptionMatch) => Promise<void>;
251
+ /**
252
+ * Enable cache-based pub/sub for multi-instance support
253
+ * Must await before emitting events to ensure subscription is ready
254
+ */
255
+ useCache: (cache: PubSubCache) => Promise<EventDef<TPayload>>;
256
+ /**
257
+ * Internal: Register a job queue to receive this event
258
+ * Called by job registration system
259
+ */
260
+ _registerJobQueue: (queueName: string, sender: JobQueueSender) => void;
261
+ /**
262
+ * Internal: Let an SSE handler's connection registry receive `grant` /
263
+ * `revoke`. Called by `createSSEHandler` once per router key; every
264
+ * registered control receives every call. Returns the unregister function.
265
+ */
266
+ _registerSubscriptionControl: (control: SubscriptionControl) => () => void;
267
+ /**
268
+ * Internal: Drop the cache binding (cache + subscribed flag).
269
+ * Called by the event cache transport on shutdown so a later useCache can
270
+ * rebind a fresh cache in the same process.
271
+ */
272
+ _resetCache: () => void;
273
+ /**
274
+ * Type inference helper
275
+ */
276
+ _payload: TPayload;
277
+ }
278
+ /**
279
+ * Infer payload type from EventDef
280
+ */
281
+ type InferEventPayload$1<TEvent> = TEvent extends EventDef<infer TPayload> ? TPayload : never;
282
+
283
+ /**
284
+ * Event Router
285
+ *
286
+ * Type-safe event router for SSE subscription
287
+ *
288
+ * @example
289
+ * ```typescript
290
+ * import { defineEvent, defineEventRouter } from '@spfn/core/event';
291
+ * import { Type } from '@sinclair/typebox';
292
+ *
293
+ * const userCreated = defineEvent('user.created', Type.Object({
294
+ * userId: Type.String(),
295
+ * }));
296
+ *
297
+ * const orderPlaced = defineEvent('order.placed', Type.Object({
298
+ * orderId: Type.String(),
299
+ * amount: Type.Number(),
300
+ * }));
301
+ *
302
+ * export const eventRouter = defineEventRouter({
303
+ * userCreated,
304
+ * orderPlaced,
305
+ * });
306
+ *
307
+ * export type EventRouter = typeof eventRouter;
308
+ * ```
309
+ */
310
+
311
+ /**
312
+ * How a client reaches the contracted events of a router.
313
+ *
314
+ * Declared on the router because it is the stream's promise, not one event's:
315
+ * `createServer` checks it against `.events()` at boot, so the document cannot
316
+ * describe a path or an auth mode the server does not serve.
317
+ */
318
+ interface EventRouterContract {
319
+ /**
320
+ * `'tokenExchange'` when `.events()` enables `auth` — the client first posts
321
+ * to the token path and opens the stream with `?token=`. `'none'` otherwise.
322
+ */
323
+ auth: 'none' | 'tokenExchange';
324
+ /**
325
+ * The stream path `.events()` registers.
326
+ * @default '/events/stream'
327
+ */
328
+ streamPath?: string;
329
+ }
330
+ /**
331
+ * What every router of events carries — the SSE event router and the
332
+ * WebSocket router alike.
333
+ *
334
+ * The WebSocket router builds on this rather than on `EventRouterDef`, because
335
+ * only an SSE event router can be contracted.
336
+ */
337
+ interface EventRouterBase<TEvents extends Record<string, EventDef<any>>> {
338
+ /**
339
+ * Event definitions
340
+ */
341
+ readonly events: TEvents;
342
+ /**
343
+ * Event names as array
344
+ */
345
+ readonly eventNames: (keyof TEvents)[];
346
+ /**
347
+ * Type inference helper - payload types by event name
348
+ */
349
+ readonly _types: {
350
+ [K in keyof TEvents]: TEvents[K]['_payload'];
351
+ };
352
+ }
353
+ /**
354
+ * Event Router Definition
355
+ */
356
+ interface EventRouterDef<TEvents extends Record<string, EventDef<any>>> extends EventRouterBase<TEvents> {
357
+ /**
358
+ * The contract declared with `.contract()`, absent when the router declares
359
+ * none. Underscored for the same reason as `EventDef._contract`.
360
+ */
361
+ readonly _contract?: EventRouterContract;
362
+ /**
363
+ * Declare how a client reaches this router's contracted events.
364
+ *
365
+ * Sets the contract on this router and returns it. A `streamPath` that does
366
+ * not start with `/` throws here, where the typo is.
367
+ */
368
+ contract: (contract: EventRouterContract) => EventRouterDef<TEvents>;
369
+ }
370
+ /**
371
+ * Infer event names from EventRouter
372
+ */
373
+ type InferEventNames<T> = T extends EventRouterBase<infer E> ? keyof E & string : never;
374
+ /**
375
+ * Infer payload type for specific event
376
+ */
377
+ type InferEventPayload<T extends EventRouterBase<any>, K extends InferEventNames<T>> = T['_types'][K];
378
+ /**
379
+ * Infer all event payloads map
380
+ */
381
+ type InferEventPayloads<T extends EventRouterBase<any>> = T['_types'];
382
+ /**
383
+ * Define an event router for SSE subscription
384
+ *
385
+ * `.contract({ auth })` on the router, together with `.contract({ since })` on
386
+ * its events, publishes those events in `contracts/current.json` — see the
387
+ * contract README.
388
+ *
389
+ * @example
390
+ * ```typescript
391
+ * export const eventRouter = defineEventRouter({
392
+ * userCreated,
393
+ * orderPlaced,
394
+ * });
395
+ *
396
+ * // Type inference
397
+ * type Names = InferEventNames<typeof eventRouter>;
398
+ * // 'userCreated' | 'orderPlaced'
399
+ *
400
+ * type Payload = InferEventPayload<typeof eventRouter, 'userCreated'>;
401
+ * // { userId: string }
402
+ * ```
403
+ */
404
+ declare function defineEventRouter<TEvents extends Record<string, EventDef<any>>>(events: TEvents): EventRouterDef<TEvents>;
405
+
406
+ export { type EventRouterDef as E, type FrameContext as F, type InferEventNames as I, type JobQueueSender as J, type PubSubCache as P, type SubscribeContext as S, type EventDef as a, type EventContract as b, type EventHandler as c, type EventRouterContract as d, type EventSubscribeOptions as e, type FramePredicate as f, type InferEventPayload$1 as g, type InferEventPayloads as h, type InferEventPayload as i, type InitialProducer as j, type SubscribePredicate as k, type SubscribeRequest as l, type SubscriptionMatch as m, defineEventRouter as n, type EventRouterBase as o };
@@ -5,13 +5,20 @@ import { serve } from '@hono/node-server';
5
5
  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
- import { c as JobRouter, e as BossOptions } from '../boss-D16fO2oG.js';
9
- import { b as EventRouterDef, E as EventDef } from '../token-manager-BT5EnUAR.js';
10
- import { d as SSEHandlerConfig, e as SSEAuthConfig } from '../types-ZQODsBft.js';
11
- import { W as WSRouterDef, f as WSHandlerConfig, e as WSMessageHandlers, g as WSAuthConfig } from '../types-2AbaW4Ie.js';
8
+ import { c as JobRouter, f as BossOptions } from '../boss-BiDCbbAQ.js';
9
+ import { E as EventRouterDef, a as EventDef } from '../router-CX5uVjmU.js';
10
+ import { d as SSEHandlerConfig, e as SSEAuthConfig } from '../types-CurMab2Q.js';
11
+ import { W as WSRouterDef, f as WSHandlerConfig, e as WSMessageHandlers, g as WSAuthConfig } from '../types-DncqXfWN.js';
12
12
  import { DatabaseProvider, MigrationStatus, MigrationStatusDb } from '@spfn/core/db';
13
- import '@sinclair/typebox';
13
+ import * as _sinclair_typebox from '@sinclair/typebox';
14
+ import { Static } from '@sinclair/typebox';
15
+ import { R as RouteDef } from '../route-builder-DRW-MSCJ.js';
16
+ import { a as EnvRegistry } from '../registry-C2KQCh0m.js';
14
17
  import 'pg-boss';
18
+ import '../token-manager-Ckjt2-qq.js';
19
+ import '../define-middleware-CVKgqo8S.js';
20
+ import 'hono/utils/http-status';
21
+ import '../route/types.js';
15
22
 
16
23
  /**
17
24
  * @deprecated Use `loadEnv` from '@spfn/core/env/loader' instead.
@@ -22,6 +29,33 @@ import 'pg-boss';
22
29
  */
23
30
  declare function loadEnvFiles(): void;
24
31
 
32
+ /** Stable operation identity used by separately deployed clients. */
33
+ declare const CORE_TIME_OPERATION_ID = "core.time";
34
+ /**
35
+ * Closed response shape for the server-time wire capability.
36
+ *
37
+ * Millisecond timestamps use integers throughout SPFN's external contracts.
38
+ */
39
+ declare const ServerTimeResponseSchema: _sinclair_typebox.TObject<{
40
+ serverTimeMillis: _sinclair_typebox.TInteger;
41
+ }>;
42
+ type ServerTimeResponse = Static<typeof ServerTimeResponseSchema>;
43
+ /** Injectable source of Unix epoch milliseconds. */
44
+ interface ServerClock {
45
+ now(): number;
46
+ }
47
+ /**
48
+ * Build the route from a clock so tests and alternate runtimes can supply the
49
+ * epoch source without replacing global time.
50
+ */
51
+ declare function createCoreTimeRoute(clock?: ServerClock): RouteDef<{}, {}, {
52
+ serverTimeMillis: number;
53
+ }>;
54
+ /** The production route and the public wire-contract source of truth. */
55
+ declare const CORE_TIME_ROUTE: RouteDef<{}, {}, {
56
+ serverTimeMillis: number;
57
+ }>;
58
+
25
59
  /**
26
60
  * Workflow router interface for @spfn/core integration
27
61
  *
@@ -202,6 +236,8 @@ interface ServerConfig {
202
236
  * Jobs defined with job()...handler() style
203
237
  * Uses pg-boss for PostgreSQL-based job queue
204
238
  *
239
+ * Merged across every `.jobs()` call, so this holds all of them.
240
+ *
205
241
  * @example
206
242
  * ```typescript
207
243
  * import { job, defineJobRouter } from '@spfn/core/job';
@@ -489,6 +525,15 @@ interface ServerConfig {
489
525
  */
490
526
  detailed?: boolean;
491
527
  };
528
+ /**
529
+ * Server-time capability dependencies.
530
+ *
531
+ * The endpoint is always enabled at `GET /_core/time`. Supplying a clock is
532
+ * primarily a deterministic test seam; production defaults to `Date.now()`.
533
+ */
534
+ serverTime?: {
535
+ clock?: ServerClock;
536
+ };
492
537
  /**
493
538
  * Migration boot gate
494
539
  *
@@ -510,6 +555,35 @@ interface ServerConfig {
510
555
  */
511
556
  allowPending?: boolean;
512
557
  };
558
+ /**
559
+ * Environment boot check
560
+ *
561
+ * Before serving, the server validates every variable of `@spfn/core`'s
562
+ * registry and of the registries listed here: a required variable must be
563
+ * set, and a set variable must pass its `validator`. On failure it logs
564
+ * each key with the reason — never a value — and refuses to start.
565
+ * `SKIP_ENV_VALIDATION` does not skip this check.
566
+ *
567
+ * @example
568
+ * ```typescript
569
+ * // src/server/config/env.config.ts
570
+ * export const envSchema = defineEnvSchema({ ... });
571
+ * export const envRegistry = createEnvRegistry(envSchema);
572
+ * export const env = envRegistry.validate();
573
+ *
574
+ * // src/server/server.config.ts
575
+ * export default defineServerConfig()
576
+ * .env({ registries: [envRegistry] })
577
+ * .build();
578
+ * ```
579
+ */
580
+ env?: {
581
+ /**
582
+ * Registries checked alongside `@spfn/core`'s: the app's own, and any
583
+ * package registry the app wants held to the same gate.
584
+ */
585
+ registries?: EnvRegistry<any>[];
586
+ };
513
587
  /**
514
588
  * Infrastructure initialization control
515
589
  * Controls automatic initialization of database and Redis
@@ -757,6 +831,13 @@ declare const CORE_NAMESPACE = "/_core";
757
831
  * balancer console — and a version bump migrates none of them.
758
832
  */
759
833
  declare const CORE_HEALTH_PATH = "/_core/health";
834
+ /**
835
+ * Where clients obtain the server's current Unix epoch in milliseconds.
836
+ *
837
+ * This endpoint is registered before application routes and application auth
838
+ * middleware so a client can call it before it has a proof or session.
839
+ */
840
+ declare const CORE_TIME_PATH = "/_core/time";
760
841
 
761
842
  /**
762
843
  * Start SPFN Server
@@ -768,6 +849,8 @@ declare const CORE_HEALTH_PATH = "/_core/health";
768
849
  * Start SPFN server
769
850
  *
770
851
  * Automatically loads server.config.ts if exists
852
+ * Validates the environment (core's registry + `config.env.registries`) and
853
+ * exits with code 1 when a variable is missing or invalid
771
854
  * Automatically initializes Database and Redis from environment
772
855
  * Sets up graceful shutdown handlers for SIGTERM and SIGINT
773
856
  *
@@ -911,6 +994,9 @@ declare function getShutdownManager(): ShutdownManager;
911
994
  * - does NOT run seed/RBAC provisioning per cold start — that moves to a
912
995
  * deploy-time step, see {@link provisionInfrastructure}.
913
996
  *
997
+ * Like {@link startServer}, it runs the environment boot check before building
998
+ * the app; a failure rejects the returned promise instead of exiting.
999
+ *
914
1000
  * `startServer()` / `spfn start` (the always-on path) is unchanged by this module.
915
1001
  */
916
1002
 
@@ -1097,6 +1183,20 @@ declare class ServerConfigBuilder {
1097
1183
  /**
1098
1184
  * Register background jobs router
1099
1185
  *
1186
+ * May be called once per domain router: repeated calls **merge** into one
1187
+ * router, the way `.routes()` composes. Calling it several times used to
1188
+ * keep only the last router and register nothing else, without warning.
1189
+ *
1190
+ * The pg-boss `config` is per server, so pass it on one call only.
1191
+ *
1192
+ * Two names can collide, and both fail loudly:
1193
+ * - a router **key** present in two calls throws here, at config time;
1194
+ * - two jobs sharing a **`job.name`** (the pg-boss queue name) throw in
1195
+ * `registerJobs()` at boot, before any queue or worker is created.
1196
+ *
1197
+ * @throws if a router key was already registered by an earlier call
1198
+ * @throws if `config` is given when an earlier call already gave one
1199
+ *
1100
1200
  * @example
1101
1201
  * ```typescript
1102
1202
  * import { job, defineJobRouter } from '@spfn/core/job';
@@ -1109,7 +1209,8 @@ declare class ServerConfigBuilder {
1109
1209
  *
1110
1210
  * export default defineServerConfig()
1111
1211
  * .routes(appRouter)
1112
- * .jobs(jobRouter)
1212
+ * .jobs(jobRouter, { clearOnStart: isDev }) // config on one call only
1213
+ * .jobs(billingJobRouter) // merged, not replaced
1113
1214
  * .build();
1114
1215
  * ```
1115
1216
  */
@@ -1197,6 +1298,12 @@ declare class ServerConfigBuilder {
1197
1298
  * Configure health check endpoint
1198
1299
  */
1199
1300
  healthCheck(healthCheck: ServerConfig['healthCheck']): this;
1301
+ /**
1302
+ * Supply the clock used by the built-in `GET /_core/time` capability.
1303
+ * Production servers normally keep the default `Date.now()` clock; this is
1304
+ * exposed so tests can assert an exact wire value without replacing globals.
1305
+ */
1306
+ serverTime(serverTime: ServerConfig['serverTime']): this;
1200
1307
  /**
1201
1308
  * Configure infrastructure initialization
1202
1309
  */
@@ -1213,6 +1320,22 @@ declare class ServerConfigBuilder {
1213
1320
  * ```
1214
1321
  */
1215
1322
  migrations(migrations: ServerConfig['migrations']): this;
1323
+ /**
1324
+ * Hand the app's env registries to the boot check
1325
+ *
1326
+ * `@spfn/core`'s own registry is always checked; the registries given here
1327
+ * are checked with it, before the server listens.
1328
+ *
1329
+ * @example
1330
+ * ```typescript
1331
+ * import { envRegistry } from '@/server/config/env.config';
1332
+ *
1333
+ * export default defineServerConfig()
1334
+ * .env({ registries: [envRegistry] })
1335
+ * .build();
1336
+ * ```
1337
+ */
1338
+ env(env: ServerConfig['env']): this;
1216
1339
  /**
1217
1340
  * Register workflow router for workflow orchestration
1218
1341
  *
@@ -1273,4 +1396,4 @@ declare class ServerConfigBuilder {
1273
1396
  */
1274
1397
  declare function defineServerConfig(): ServerConfigBuilder;
1275
1398
 
1276
- export { type AppFactory, CORE_HEALTH_PATH, CORE_NAMESPACE, type MigrationSnapshot, PendingMigrationsError, type ServerConfig, type ServerInstance, type ShutdownHookOptions, createServer, createServerlessApp, defineServerConfig, getMigrationSnapshot, getShutdownManager, loadEnvFiles, provisionInfrastructure, resetMigrationSnapshot, resetServerlessApp, startServer };
1399
+ export { type AppFactory, CORE_HEALTH_PATH, CORE_NAMESPACE, CORE_TIME_OPERATION_ID, CORE_TIME_PATH, CORE_TIME_ROUTE, type MigrationSnapshot, PendingMigrationsError, type ServerClock, type ServerConfig, type ServerInstance, type ServerTimeResponse, ServerTimeResponseSchema, type ShutdownHookOptions, createCoreTimeRoute, createServer, createServerlessApp, defineServerConfig, getMigrationSnapshot, getShutdownManager, loadEnvFiles, provisionInfrastructure, resetMigrationSnapshot, resetServerlessApp, startServer };