ts-procedures 5.9.0 → 5.10.0

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 (80) hide show
  1. package/README.md +1 -1
  2. package/agent_config/claude-code/agents/ts-procedures-architect.md +46 -101
  3. package/agent_config/claude-code/skills/guide/SKILL.md +49 -34
  4. package/agent_config/claude-code/skills/guide/anti-patterns.md +6 -5
  5. package/agent_config/claude-code/skills/guide/api-reference.md +60 -49
  6. package/agent_config/claude-code/skills/review/SKILL.md +12 -17
  7. package/agent_config/claude-code/skills/scaffold/SKILL.md +18 -23
  8. package/agent_config/claude-code/skills/scaffold/templates/client.md +115 -0
  9. package/agent_config/lib/install-claude.mjs +22 -22
  10. package/docs/core.md +5 -9
  11. package/docs/streaming.md +9 -9
  12. package/package.json +3 -14
  13. package/src/client/call.test.ts +162 -0
  14. package/src/client/errors.test.ts +43 -0
  15. package/src/client/fetch-adapter.test.ts +340 -0
  16. package/src/client/hooks.test.ts +191 -0
  17. package/src/client/index.test.ts +290 -0
  18. package/src/client/request-builder.test.ts +184 -0
  19. package/src/client/stream.test.ts +331 -0
  20. package/src/codegen/bin/cli.test.ts +260 -0
  21. package/src/codegen/bin/cli.ts +282 -0
  22. package/src/codegen/constants.ts +1 -0
  23. package/src/codegen/e2e.test.ts +565 -0
  24. package/src/codegen/emit-client-runtime.test.ts +93 -0
  25. package/src/codegen/emit-client-runtime.ts +114 -0
  26. package/src/codegen/emit-client-types.test.ts +39 -0
  27. package/src/codegen/emit-client-types.ts +27 -0
  28. package/src/codegen/emit-errors.test.ts +202 -0
  29. package/src/codegen/emit-errors.ts +80 -0
  30. package/src/codegen/emit-index.test.ts +127 -0
  31. package/src/codegen/emit-index.ts +58 -0
  32. package/src/codegen/emit-scope.test.ts +624 -0
  33. package/src/codegen/emit-scope.ts +389 -0
  34. package/src/codegen/emit-types.test.ts +205 -0
  35. package/src/codegen/emit-types.ts +158 -0
  36. package/src/codegen/group-routes.test.ts +159 -0
  37. package/src/codegen/group-routes.ts +61 -0
  38. package/src/codegen/index.ts +30 -0
  39. package/src/codegen/naming.test.ts +50 -0
  40. package/src/codegen/naming.ts +25 -0
  41. package/src/codegen/pipeline.test.ts +316 -0
  42. package/src/codegen/pipeline.ts +108 -0
  43. package/src/codegen/resolve-envelope.test.ts +76 -0
  44. package/src/codegen/resolve-envelope.ts +61 -0
  45. package/src/errors.test.ts +163 -0
  46. package/src/errors.ts +107 -0
  47. package/src/exports.ts +7 -0
  48. package/src/implementations/http/doc-registry.test.ts +415 -0
  49. package/src/implementations/http/doc-registry.ts +143 -0
  50. package/src/implementations/http/express-rpc/README.md +6 -6
  51. package/src/implementations/http/express-rpc/index.test.ts +957 -0
  52. package/src/implementations/http/express-rpc/index.ts +266 -0
  53. package/src/implementations/http/express-rpc/types.ts +16 -0
  54. package/src/implementations/http/hono-api/index.test.ts +1341 -0
  55. package/src/implementations/http/hono-api/index.ts +463 -0
  56. package/src/implementations/http/hono-api/types.ts +16 -0
  57. package/src/implementations/http/hono-rpc/README.md +6 -6
  58. package/src/implementations/http/hono-rpc/index.test.ts +1075 -0
  59. package/src/implementations/http/hono-rpc/index.ts +238 -0
  60. package/src/implementations/http/hono-rpc/types.ts +16 -0
  61. package/src/implementations/http/hono-stream/README.md +12 -12
  62. package/src/implementations/http/hono-stream/index.test.ts +1768 -0
  63. package/src/implementations/http/hono-stream/index.ts +456 -0
  64. package/src/implementations/http/hono-stream/types.ts +20 -0
  65. package/src/implementations/types.ts +174 -0
  66. package/src/index.test.ts +1185 -0
  67. package/src/index.ts +522 -0
  68. package/src/schema/compute-schema.test.ts +128 -0
  69. package/src/schema/compute-schema.ts +88 -0
  70. package/src/schema/extract-json-schema.test.ts +25 -0
  71. package/src/schema/extract-json-schema.ts +15 -0
  72. package/src/schema/parser.test.ts +182 -0
  73. package/src/schema/parser.ts +215 -0
  74. package/src/schema/resolve-schema-lib.test.ts +19 -0
  75. package/src/schema/resolve-schema-lib.ts +29 -0
  76. package/src/schema/types.ts +20 -0
  77. package/src/stack-utils.test.ts +94 -0
  78. package/src/stack-utils.ts +129 -0
  79. package/docs/superpowers/plans/2026-03-30-client-codegen.md +0 -2833
  80. package/docs/superpowers/specs/2026-03-30-client-codegen-design.md +0 -632
@@ -0,0 +1,456 @@
1
+ import { Hono, Context } from 'hono'
2
+ import { streamSSE, streamText } from 'hono/streaming'
3
+ import { kebabCase } from 'es-toolkit/string'
4
+ import { castArray } from 'es-toolkit/compat'
5
+ import { TStreamProcedureRegistration } from '../../../index.js'
6
+ import { ExtractConfig, ExtractContext, ProceduresFactory, RPCConfig } from '../../types.js'
7
+ import { HonoStreamFactoryItem, StreamHttpRouteDoc, StreamMode } from './types.js'
8
+ import { ProcedureValidationError } from '../../../errors.js'
9
+
10
+ export type { StreamHttpRouteDoc, StreamMode }
11
+
12
+ export type SSEOptions = {
13
+ event?: string
14
+ id?: string
15
+ retry?: number
16
+ }
17
+
18
+ const sseMetadata = new WeakMap<object, SSEOptions>()
19
+
20
+ export function sse<T extends object>(data: T, options?: SSEOptions): T {
21
+ sseMetadata.set(data, options ?? {})
22
+ return data
23
+ }
24
+
25
+ function getSSEMeta(value: unknown): SSEOptions | undefined {
26
+ if (typeof value === 'object' && value !== null) {
27
+ return sseMetadata.get(value)
28
+ }
29
+ return undefined
30
+ }
31
+
32
+ /**
33
+ * Result from onMidStreamError callback.
34
+ * @property data - The data to write as the SSE `data:` field content (should match yieldType schema)
35
+ * @property closeStream - Whether to close the stream after writing (defaults to true)
36
+ */
37
+ export type MidStreamErrorResult<TErrorData = unknown> = {
38
+ data: TErrorData
39
+ closeStream?: boolean
40
+ }
41
+
42
+ export type HonoStreamAppBuilderConfig<TErrorData = unknown> = {
43
+ /**
44
+ * An existing Hono application instance to use.
45
+ * If not provided, a new instance will be created.
46
+ */
47
+ app?: Hono
48
+ /** Optional path prefix for all stream routes. */
49
+ pathPrefix?: string
50
+ /** Default stream mode for all routes. Defaults to 'sse'. */
51
+ defaultStreamMode?: StreamMode
52
+ onRequestStart?: (c: Context) => void
53
+ onRequestEnd?: (c: Context) => void
54
+ onStreamStart?: (procedure: TStreamProcedureRegistration, c: Context, streamMode: StreamMode) => void
55
+ onStreamEnd?: (procedure: TStreamProcedureRegistration, c: Context, streamMode: StreamMode) => void
56
+ /**
57
+ * Called for errors BEFORE streaming starts (validation, auth, context resolution).
58
+ * Return value IS used as the HTTP response.
59
+ */
60
+ onPreStreamError?: (
61
+ procedure: TStreamProcedureRegistration,
62
+ c: Context,
63
+ error: ProcedureValidationError | Error
64
+ ) => Response | Promise<Response>
65
+ /**
66
+ * Called for errors DURING streaming (generator throws).
67
+ * Return value is written to the stream as a yield.
68
+ * Should return a value matching your yieldType schema (e.g., error variant of a union).
69
+ * Return undefined to use default behavior (writes { error: message }).
70
+ *
71
+ * Use sse() to attach SSE metadata (event, id, retry) to the error data object.
72
+ *
73
+ * @returns { data, closeStream? } - data to yield, whether to close after (default true)
74
+ */
75
+ onMidStreamError?: (
76
+ procedure: TStreamProcedureRegistration,
77
+ c: Context,
78
+ error: Error
79
+ ) => MidStreamErrorResult<TErrorData> | undefined
80
+ }
81
+
82
+ /**
83
+ * Builder class for creating a Hono application with streaming RPC routes.
84
+ *
85
+ * Usage:
86
+ * const StreamRPC = Procedures<StreamContext, RPCConfig>()
87
+ *
88
+ * const streamApp = new HonoStreamAppBuilder()
89
+ * .register(StreamRPC, (c): Promise<StreamContext> => { /* context resolution logic * / })
90
+ * .build();
91
+ *
92
+ * const app = streamApp.app; // Hono application
93
+ * const docs = streamApp.docs; // Stream route documentation
94
+ */
95
+ export class HonoStreamAppBuilder<TErrorData = unknown> {
96
+ /**
97
+ * Constructor for HonoStreamAppBuilder.
98
+ */
99
+ constructor(readonly config?: HonoStreamAppBuilderConfig<TErrorData>) {
100
+ if (config?.app) {
101
+ this._app = config.app
102
+ }
103
+
104
+ if (config?.onRequestStart) {
105
+ this._app.use('*', async (c, next) => {
106
+ config.onRequestStart!(c)
107
+ await next()
108
+ })
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Generates the stream route path based on the RPC configuration.
114
+ */
115
+ static makeStreamHttpRoutePath({
116
+ name,
117
+ config,
118
+ prefix,
119
+ }: {
120
+ name: string
121
+ prefix?: string
122
+ config: RPCConfig
123
+ }) {
124
+ const normalizedPrefix = prefix ? (prefix.startsWith('/') ? prefix : `/${prefix}`) : ''
125
+
126
+ return `${normalizedPrefix}/${castArray(config.scope).map(kebabCase).join('/')}/${kebabCase(name)}/${String(config.version).trim()}`
127
+ }
128
+
129
+ /**
130
+ * Instance method wrapper for makeStreamHttpRoutePath that uses the builder's pathPrefix.
131
+ */
132
+ makeStreamHttpRoutePath(name: string, config: RPCConfig): string {
133
+ return HonoStreamAppBuilder.makeStreamHttpRoutePath({
134
+ name,
135
+ config,
136
+ prefix: this.config?.pathPrefix,
137
+ })
138
+ }
139
+
140
+ private factories: HonoStreamFactoryItem<any>[] = []
141
+
142
+ private _app: Hono = new Hono()
143
+ private _docs: (StreamHttpRouteDoc & object)[] = []
144
+
145
+ get app(): Hono {
146
+ return this._app
147
+ }
148
+
149
+ get docs(): StreamHttpRouteDoc[] {
150
+ return this._docs
151
+ }
152
+
153
+ /**
154
+ * Registers a procedure factory with its context.
155
+ * Only streaming procedures (created with CreateStream) will be registered.
156
+ */
157
+ register<TFactory extends ProceduresFactory>(
158
+ factory: TFactory,
159
+ factoryContext:
160
+ | ExtractContext<TFactory>
161
+ | ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>),
162
+ options?: {
163
+ streamMode?: StreamMode
164
+ extendProcedureDoc?: (params: {
165
+ base: StreamHttpRouteDoc
166
+ procedure: TStreamProcedureRegistration<any, ExtractConfig<TFactory>>
167
+ }) => Record<string, any>
168
+ }
169
+ ): this {
170
+ this.factories.push({
171
+ factory,
172
+ factoryContext,
173
+ streamMode: options?.streamMode,
174
+ extendProcedureDoc: options?.extendProcedureDoc,
175
+ } as HonoStreamFactoryItem<any>)
176
+ return this
177
+ }
178
+
179
+ /**
180
+ * Creates a route handler for streaming procedures.
181
+ */
182
+ private createStreamHandler(
183
+ procedure: TStreamProcedureRegistration,
184
+ factoryContext: HonoStreamFactoryItem['factoryContext'],
185
+ streamMode: StreamMode
186
+ ) {
187
+ return async (c: Context) => {
188
+ try {
189
+ const context =
190
+ typeof factoryContext === 'function' ? await factoryContext(c) : factoryContext
191
+
192
+ // GET: query params, POST: JSON body
193
+ const params =
194
+ c.req.method === 'GET'
195
+ ? Object.fromEntries(new URL(c.req.url).searchParams)
196
+ : await c.req.json().catch(() => ({}))
197
+
198
+ // Validate params BEFORE starting the stream
199
+ if (procedure.config.validation?.params) {
200
+ const { errors } = procedure.config.validation.params(params)
201
+ if (errors) {
202
+ const error = new ProcedureValidationError(
203
+ procedure.name,
204
+ `Validation error for ${procedure.name}`,
205
+ errors
206
+ )
207
+ // Use onPreStreamError if provided
208
+ if (this.config?.onPreStreamError) {
209
+ return this.config.onPreStreamError(procedure, c, error)
210
+ }
211
+ return c.json({ error: error.message }, 400)
212
+ }
213
+ }
214
+
215
+ if (this.config?.onStreamStart) {
216
+ this.config.onStreamStart(procedure, c, streamMode)
217
+ }
218
+
219
+ if (streamMode === 'sse') {
220
+ return this.handleSSEStream(procedure, context, params, c)
221
+ } else {
222
+ return this.handleTextStream(procedure, context, params, c)
223
+ }
224
+ } catch (error) {
225
+ // Use onPreStreamError for context resolution errors
226
+ if (this.config?.onPreStreamError) {
227
+ return this.config.onPreStreamError(procedure, c, error as Error)
228
+ }
229
+ return c.json({ error: (error as Error).message }, 500)
230
+ }
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Handles SSE streaming mode.
236
+ */
237
+ private handleSSEStream(
238
+ procedure: TStreamProcedureRegistration,
239
+ context: any,
240
+ params: any,
241
+ c: Context
242
+ ) {
243
+ return streamSSE(c, async (stream) => {
244
+ // Pass isPrevalidated: true since we already validated params in createStreamHandler
245
+ const generator = procedure.handler({ ...context, signal: c.req.raw.signal, isPrevalidated: true }, params)
246
+
247
+ stream.onAbort(async () => {
248
+ await generator.return(undefined)
249
+ })
250
+
251
+ let eventId = 0
252
+ try {
253
+ const iterator = generator[Symbol.asyncIterator]()
254
+ let iterResult = await iterator.next()
255
+
256
+ while (!iterResult.done) {
257
+ const value = iterResult.value
258
+ const currentId = eventId++
259
+ const meta = getSSEMeta(value)
260
+
261
+ const data =
262
+ typeof value === 'string'
263
+ ? value
264
+ : value != null
265
+ ? JSON.stringify(value)
266
+ : ''
267
+
268
+ await stream.writeSSE({
269
+ data,
270
+ event: meta?.event ?? procedure.name,
271
+ id: meta?.id ?? String(currentId),
272
+ ...(meta?.retry !== undefined && { retry: meta.retry }),
273
+ })
274
+
275
+ iterResult = await iterator.next()
276
+ }
277
+
278
+ // Send return value as a special 'return' event (if present)
279
+ if (iterResult.value !== undefined) {
280
+ const returnData =
281
+ typeof iterResult.value === 'string'
282
+ ? iterResult.value
283
+ : JSON.stringify(iterResult.value)
284
+
285
+ await stream.writeSSE({
286
+ data: returnData,
287
+ event: 'return',
288
+ id: String(eventId++),
289
+ })
290
+ }
291
+ } catch (error) {
292
+ // Get error yield value from callback (onMidStreamError)
293
+ let errorResult: MidStreamErrorResult<TErrorData> | undefined
294
+
295
+ if (this.config?.onMidStreamError) {
296
+ errorResult = this.config.onMidStreamError(procedure, c, error as Error)
297
+ }
298
+
299
+ // Write error value to stream
300
+ const errorData = errorResult?.data ?? { error: (error as Error).message }
301
+ const sseMeta = getSSEMeta(errorData)
302
+
303
+ await stream.writeSSE({
304
+ data: typeof errorData === 'string' ? errorData : JSON.stringify(errorData),
305
+ event: sseMeta?.event ?? (errorResult?.data !== undefined ? procedure.name : 'error'),
306
+ id: sseMeta?.id ?? String(eventId++),
307
+ ...(sseMeta?.retry !== undefined && { retry: sseMeta.retry }),
308
+ })
309
+
310
+ // closeStream defaults to true if not specified
311
+ // (stream closes naturally after this handler completes)
312
+ } finally {
313
+ if (this.config?.onStreamEnd) {
314
+ this.config.onStreamEnd(procedure, c, 'sse')
315
+ }
316
+ if (this.config?.onRequestEnd) {
317
+ this.config.onRequestEnd(c)
318
+ }
319
+ }
320
+ })
321
+ }
322
+
323
+ /**
324
+ * Handles text streaming mode.
325
+ */
326
+ private handleTextStream(
327
+ procedure: TStreamProcedureRegistration,
328
+ context: any,
329
+ params: any,
330
+ c: Context
331
+ ) {
332
+ return streamText(c, async (stream) => {
333
+ // Pass isPrevalidated: true since we already validated params in createStreamHandler
334
+ const generator = procedure.handler({ ...context, signal: c.req.raw.signal, isPrevalidated: true }, params)
335
+
336
+ stream.onAbort(async () => {
337
+ await generator.return(undefined)
338
+ })
339
+
340
+ try {
341
+ for await (const value of generator) {
342
+ await stream.writeln(JSON.stringify(value))
343
+ }
344
+ } catch (error) {
345
+ // Get error yield value from callback (onMidStreamError)
346
+ let errorResult: MidStreamErrorResult<TErrorData> | undefined
347
+
348
+ if (this.config?.onMidStreamError) {
349
+ errorResult = this.config.onMidStreamError(procedure, c, error as Error)
350
+ }
351
+
352
+ // Write error value to stream
353
+ const errorData = errorResult?.data ?? { error: (error as Error).message }
354
+ await stream.writeln(JSON.stringify(errorData))
355
+ } finally {
356
+ if (this.config?.onStreamEnd) {
357
+ this.config.onStreamEnd(procedure, c, 'text')
358
+ }
359
+ if (this.config?.onRequestEnd) {
360
+ this.config.onRequestEnd(c)
361
+ }
362
+ }
363
+ })
364
+ }
365
+
366
+ /**
367
+ * Builds and returns the Hono application with registered streaming routes.
368
+ */
369
+ build(): Hono {
370
+ this.factories.forEach(({ factory, factoryContext, streamMode, extendProcedureDoc }) => {
371
+ const mode = streamMode ?? this.config?.defaultStreamMode ?? 'sse'
372
+
373
+ factory
374
+ .getProcedures()
375
+ .filter(
376
+ (p: { isStream?: boolean }): p is TStreamProcedureRegistration => p.isStream === true
377
+ )
378
+ .forEach((procedure: TStreamProcedureRegistration<any, RPCConfig>) => {
379
+ const route = this.buildStreamHttpRouteDoc(procedure, mode, extendProcedureDoc)
380
+
381
+ this._docs.push(route)
382
+
383
+ const handler = this.createStreamHandler(procedure, factoryContext, mode)
384
+
385
+ // Register both GET and POST handlers
386
+ this._app.get(route.path, handler)
387
+ this._app.post(route.path, handler)
388
+ })
389
+ })
390
+
391
+ return this._app
392
+ }
393
+
394
+ /**
395
+ * Generates the Stream HTTP route documentation for the given procedure.
396
+ */
397
+ private buildStreamHttpRouteDoc(
398
+ procedure: TStreamProcedureRegistration<any, RPCConfig>,
399
+ streamMode: StreamMode,
400
+ extendProcedureDoc?: HonoStreamFactoryItem['extendProcedureDoc']
401
+ ): StreamHttpRouteDoc {
402
+ const { config } = procedure
403
+ const path = HonoStreamAppBuilder.makeStreamHttpRoutePath({
404
+ name: procedure.name,
405
+ config,
406
+ prefix: this.config?.pathPrefix,
407
+ })
408
+ const methods = ['get', 'post'] as const
409
+ const jsonSchema: { params?: Record<string, unknown>; yieldType?: Record<string, unknown>; returnType?: Record<string, unknown> } = {}
410
+
411
+ if (config.schema?.params) {
412
+ jsonSchema.params = config.schema.params
413
+ }
414
+ if (streamMode === 'sse') {
415
+ jsonSchema.yieldType = {
416
+ type: 'object',
417
+ description: 'SSE message envelope. The data field contains the procedure yield value.',
418
+ required: ['data', 'event', 'id'],
419
+ properties: {
420
+ data: config.schema?.yieldType ?? {},
421
+ event: { type: 'string' },
422
+ id: { type: 'string' },
423
+ retry: { type: 'number' },
424
+ },
425
+ }
426
+ } else if (config.schema?.yieldType) {
427
+ // Text mode: pass through as-is
428
+ jsonSchema.yieldType = config.schema.yieldType
429
+ }
430
+ if (config.schema?.returnType) {
431
+ jsonSchema.returnType = config.schema.returnType
432
+ }
433
+
434
+ const base: StreamHttpRouteDoc = {
435
+ kind: 'stream',
436
+ name: procedure.name,
437
+ version: config.version,
438
+ scope: config.scope,
439
+ path,
440
+ methods: [...methods],
441
+ streamMode,
442
+ jsonSchema,
443
+ }
444
+
445
+ let extendedDoc: object = {}
446
+
447
+ if (extendProcedureDoc) {
448
+ extendedDoc = extendProcedureDoc({ base, procedure })
449
+ }
450
+
451
+ return {
452
+ ...extendedDoc,
453
+ ...base,
454
+ }
455
+ }
456
+ }
@@ -0,0 +1,20 @@
1
+ import { Context } from 'hono'
2
+ import { TStreamProcedureRegistration } from '../../../index.js'
3
+ import { ExtractConfig, ExtractContext } from '../../types.js'
4
+
5
+ export type { StreamHttpRouteDoc } from '../../types.js'
6
+ export type { StreamMode } from '../../types.js'
7
+
8
+ import type { StreamHttpRouteDoc, StreamMode } from '../../types.js'
9
+
10
+ export type HonoStreamFactoryItem<TFactory = any> = {
11
+ factory: TFactory
12
+ factoryContext:
13
+ | ExtractContext<TFactory>
14
+ | ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>)
15
+ streamMode?: StreamMode
16
+ extendProcedureDoc?: (params: {
17
+ base: StreamHttpRouteDoc
18
+ procedure: TStreamProcedureRegistration<any, ExtractConfig<TFactory>>
19
+ }) => Record<string, any>
20
+ }
@@ -0,0 +1,174 @@
1
+ import { Procedures } from '../index.js'
2
+
3
+ export interface RPCConfig {
4
+ // Scope or scopes (scope segments) required to access the RPC
5
+ scope: string | string[]
6
+ version: number
7
+ }
8
+
9
+ export type FactoryItem<C> = {
10
+ factory: ReturnType<typeof Procedures<C, RPCConfig>>
11
+ factoryContext: (req: Request) => C
12
+ }
13
+
14
+ export interface RPCHttpRouteDoc extends RPCConfig {
15
+ kind: 'rpc'
16
+ name: string // procedure name
17
+ path: string
18
+ method: 'post'
19
+ jsonSchema: {
20
+ body?: Record<string, unknown>
21
+ response?: Record<string, unknown>
22
+ }
23
+ }
24
+
25
+ export type StreamMode = 'sse' | 'text'
26
+
27
+ // ================
28
+ // API (REST-style) types
29
+ // ================
30
+
31
+ export type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'
32
+
33
+ export interface APIConfig {
34
+ /** HTTP route path (supports Hono path params, e.g., '/users/:id') */
35
+ path: string
36
+ /** HTTP method for this endpoint */
37
+ method: HttpMethod
38
+ /** HTTP status code on success. Defaults: POST→201, DELETE→204, others→200 */
39
+ successStatus?: number
40
+ /** Optional scope for grouping API routes in generated client files */
41
+ scope?: string
42
+ }
43
+
44
+ export interface APIHttpRouteDoc extends APIConfig {
45
+ kind: 'api'
46
+ name: string
47
+ /** Full resolved path including pathPrefix */
48
+ fullPath: string
49
+ jsonSchema: {
50
+ pathParams?: Record<string, unknown>
51
+ query?: Record<string, unknown>
52
+ body?: Record<string, unknown>
53
+ headers?: Record<string, unknown>
54
+ response?: Record<string, unknown>
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Constrains schema.input channel names to valid HTTP input sources.
60
+ * Use with `satisfies` or as a type annotation to catch typos at compile time:
61
+ *
62
+ * @example
63
+ * schema: {
64
+ * input: {
65
+ * pathParams: Type.Object({ id: Type.String() }),
66
+ * qurey: Type.Object({ ... }), // TS error: 'qurey' not in APIInput
67
+ * } satisfies APIInput
68
+ * }
69
+ */
70
+ export type APIInput<T extends {
71
+ pathParams?: unknown
72
+ query?: unknown
73
+ body?: unknown
74
+ headers?: unknown
75
+ } = {
76
+ pathParams?: unknown
77
+ query?: unknown
78
+ body?: unknown
79
+ headers?: unknown
80
+ }> = T
81
+
82
+ export interface StreamHttpRouteDoc extends RPCConfig {
83
+ kind: 'stream'
84
+ name: string // procedure name
85
+ path: string
86
+ methods: ('get' | 'post')[]
87
+ streamMode: StreamMode
88
+ jsonSchema: {
89
+ params?: Record<string, unknown> // Query params (GET) or body (POST)
90
+ yieldType?: Record<string, unknown> // Schema for each streamed value
91
+ returnType?: Record<string, unknown> // Final return (optional)
92
+ }
93
+ }
94
+
95
+ // ================
96
+ // Utility types
97
+ // ================
98
+
99
+ /**
100
+ * Extracts the TContext type from a Procedures factory return type.
101
+ * Uses the first parameter of the handler function to infer the context type.
102
+ */
103
+ export type ExtractContext<TFactory> = TFactory extends {
104
+ getProcedures: () => Array<{ handler: (ctx: infer TContext, ...args: any[]) => any }>
105
+ }
106
+ ? TContext
107
+ : never
108
+
109
+ /**
110
+ * Extracts the TConfig type from a Procedures factory return type.
111
+ * Uses the config property of the procedure registration to infer the config type.
112
+ */
113
+ export type ExtractConfig<TFactory> = TFactory extends {
114
+ getProcedures: () => Array<{ config: infer TConfig }>
115
+ }
116
+ ? TConfig
117
+ : never
118
+
119
+ /**
120
+ * Minimal structural type for a Procedures factory.
121
+ * Uses explicit `any` types to avoid variance issues with generic constraints.
122
+ */
123
+ export type ProceduresFactory = {
124
+ getProcedures: () => Array<{
125
+ name: string
126
+ isStream?: boolean
127
+ config: any
128
+ handler: (ctx: any, params?: any) => Promise<any> | AsyncGenerator<any, any, unknown>
129
+ }>
130
+ Create: (...args: any[]) => any
131
+ CreateStream?: (...args: any[]) => any
132
+ }
133
+
134
+ // ================
135
+ // DocRegistry types
136
+ // ================
137
+
138
+ export type AnyHttpRouteDoc = RPCHttpRouteDoc | APIHttpRouteDoc | StreamHttpRouteDoc
139
+
140
+ export interface DocSource<T = AnyHttpRouteDoc> {
141
+ readonly docs: T[]
142
+ }
143
+
144
+ export interface HeaderDoc {
145
+ name: string
146
+ description?: string
147
+ required?: boolean
148
+ example?: string
149
+ }
150
+
151
+ export interface ErrorDoc {
152
+ name: string
153
+ statusCode: number
154
+ description: string
155
+ schema?: Record<string, unknown>
156
+ }
157
+
158
+ export interface DocRegistryConfig {
159
+ basePath?: string
160
+ headers?: HeaderDoc[]
161
+ errors?: ErrorDoc[]
162
+ }
163
+
164
+ export interface DocRegistryOutputOptions<TEnvelope = DocEnvelope> {
165
+ filter?: (route: AnyHttpRouteDoc) => boolean
166
+ transform?: (envelope: DocEnvelope) => TEnvelope
167
+ }
168
+
169
+ export interface DocEnvelope {
170
+ basePath: string
171
+ headers: HeaderDoc[]
172
+ errors: ErrorDoc[]
173
+ routes: AnyHttpRouteDoc[]
174
+ }