@pikku/inspector 0.11.2 → 0.12.1

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 (211) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/OPTIMIZATION-PLAN.md +195 -0
  3. package/dist/add/add-ai-agent.d.ts +2 -0
  4. package/dist/add/add-ai-agent.js +314 -0
  5. package/dist/add/add-channel.js +81 -61
  6. package/dist/add/add-cli.d.ts +1 -1
  7. package/dist/add/add-cli.js +42 -19
  8. package/dist/add/add-file-extends-core-type.d.ts +1 -1
  9. package/dist/add/add-file-with-config.d.ts +1 -1
  10. package/dist/add/add-file-with-factory.d.ts +1 -1
  11. package/dist/add/add-file-with-factory.js +2 -0
  12. package/dist/add/add-functions.d.ts +1 -1
  13. package/dist/add/add-functions.js +256 -82
  14. package/dist/add/add-http-route.d.ts +20 -10
  15. package/dist/add/add-http-route.js +156 -66
  16. package/dist/add/add-http-routes.d.ts +5 -0
  17. package/dist/add/add-http-routes.js +160 -0
  18. package/dist/add/add-keyed-wiring.d.ts +12 -0
  19. package/dist/add/add-keyed-wiring.js +97 -0
  20. package/dist/add/add-mcp-prompt.d.ts +1 -1
  21. package/dist/add/add-mcp-prompt.js +14 -9
  22. package/dist/add/add-mcp-resource.d.ts +1 -1
  23. package/dist/add/add-mcp-resource.js +14 -9
  24. package/dist/add/add-middleware.d.ts +1 -4
  25. package/dist/add/add-middleware.js +364 -79
  26. package/dist/add/add-permission.d.ts +1 -1
  27. package/dist/add/add-permission.js +152 -40
  28. package/dist/add/add-queue-worker.d.ts +1 -1
  29. package/dist/add/add-queue-worker.js +18 -12
  30. package/dist/add/add-rpc-invocations.d.ts +3 -3
  31. package/dist/add/add-rpc-invocations.js +24 -10
  32. package/dist/add/add-schedule.d.ts +1 -1
  33. package/dist/add/add-schedule.js +11 -5
  34. package/dist/add/add-secret.d.ts +3 -0
  35. package/dist/add/add-secret.js +82 -0
  36. package/dist/add/add-trigger.d.ts +2 -0
  37. package/dist/add/add-trigger.js +87 -0
  38. package/dist/add/add-variable.d.ts +1 -0
  39. package/dist/add/add-variable.js +8 -0
  40. package/dist/add/add-wire-addon.d.ts +7 -0
  41. package/dist/add/add-wire-addon.js +70 -0
  42. package/dist/add/add-workflow-graph.d.ts +3 -2
  43. package/dist/add/add-workflow-graph.js +143 -406
  44. package/dist/add/add-workflow.d.ts +1 -1
  45. package/dist/add/add-workflow.js +6 -4
  46. package/dist/error-codes.d.ts +15 -1
  47. package/dist/error-codes.js +20 -1
  48. package/dist/index.d.ts +9 -8
  49. package/dist/index.js +5 -4
  50. package/dist/inspector.d.ts +2 -2
  51. package/dist/inspector.js +95 -15
  52. package/dist/schema-generator.d.ts +1 -0
  53. package/dist/schema-generator.js +1 -0
  54. package/dist/types-map.js +10 -1
  55. package/dist/types.d.ts +180 -50
  56. package/dist/utils/compute-required-schemas.d.ts +4 -0
  57. package/dist/utils/compute-required-schemas.js +40 -0
  58. package/dist/utils/contract-hashes.d.ts +52 -0
  59. package/dist/utils/contract-hashes.js +269 -0
  60. package/dist/utils/custom-types-generator.d.ts +9 -0
  61. package/dist/utils/custom-types-generator.js +71 -0
  62. package/dist/utils/detect-schema-vendor.d.ts +22 -0
  63. package/dist/utils/detect-schema-vendor.js +76 -0
  64. package/dist/utils/does-type-extend-core-type.d.ts +1 -1
  65. package/dist/utils/ensure-function-metadata.d.ts +6 -3
  66. package/dist/utils/ensure-function-metadata.js +220 -6
  67. package/dist/utils/extract-function-name.d.ts +5 -16
  68. package/dist/utils/extract-function-name.js +86 -291
  69. package/dist/utils/extract-services.d.ts +2 -1
  70. package/dist/utils/extract-services.js +25 -1
  71. package/dist/utils/filter-inspector-state.d.ts +1 -1
  72. package/dist/utils/filter-inspector-state.js +107 -23
  73. package/dist/utils/filter-utils.d.ts +2 -2
  74. package/dist/utils/get-files-and-methods.d.ts +1 -1
  75. package/dist/utils/get-property-value.d.ts +6 -1
  76. package/dist/utils/get-property-value.js +28 -3
  77. package/dist/utils/hash.d.ts +2 -0
  78. package/dist/utils/hash.js +23 -0
  79. package/dist/utils/middleware.d.ts +9 -32
  80. package/dist/utils/middleware.js +80 -66
  81. package/dist/utils/permissions.d.ts +4 -4
  82. package/dist/utils/permissions.js +10 -10
  83. package/dist/utils/post-process.d.ts +11 -11
  84. package/dist/utils/post-process.js +247 -24
  85. package/dist/utils/resolve-addon-package.d.ts +16 -0
  86. package/dist/utils/resolve-addon-package.js +34 -0
  87. package/dist/utils/resolve-function-types.d.ts +6 -0
  88. package/dist/utils/resolve-function-types.js +29 -0
  89. package/dist/utils/resolve-identifier.d.ts +10 -0
  90. package/dist/utils/resolve-identifier.js +36 -0
  91. package/dist/utils/resolve-versions.d.ts +2 -0
  92. package/dist/utils/resolve-versions.js +78 -0
  93. package/dist/utils/schema-generator.d.ts +9 -0
  94. package/dist/utils/schema-generator.js +209 -0
  95. package/dist/utils/serialize-inspector-state.d.ts +70 -23
  96. package/dist/utils/serialize-inspector-state.js +98 -22
  97. package/dist/utils/serialize-mcp-json.d.ts +2 -0
  98. package/dist/utils/serialize-mcp-json.js +99 -0
  99. package/dist/utils/serialize-middleware-groups-meta.d.ts +12 -0
  100. package/dist/utils/serialize-middleware-groups-meta.js +28 -0
  101. package/dist/utils/serialize-openapi-json.d.ts +85 -0
  102. package/dist/utils/serialize-openapi-json.js +151 -0
  103. package/dist/utils/serialize-permissions-groups-meta.d.ts +6 -0
  104. package/dist/utils/serialize-permissions-groups-meta.js +31 -0
  105. package/dist/utils/validate-auth-sessionless.d.ts +3 -0
  106. package/dist/utils/validate-auth-sessionless.js +14 -0
  107. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +34 -102
  108. package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +1 -1
  109. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +23 -4
  110. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +12 -10
  111. package/dist/utils/workflow/graph/finalize-workflow-wires.d.ts +3 -0
  112. package/dist/utils/workflow/graph/finalize-workflow-wires.js +276 -0
  113. package/dist/utils/workflow/graph/finalize-workflows.d.ts +2 -0
  114. package/dist/utils/workflow/graph/finalize-workflows.js +75 -0
  115. package/dist/utils/workflow/graph/index.d.ts +2 -0
  116. package/dist/utils/workflow/graph/index.js +2 -0
  117. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +0 -8
  118. package/dist/utils/workflow/graph/serialize-workflow-graph.js +1 -3
  119. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +53 -79
  120. package/dist/utils/workflow/graph/workflow-graph.types.js +1 -1
  121. package/dist/visit.d.ts +1 -1
  122. package/dist/visit.js +13 -6
  123. package/package.json +14 -4
  124. package/src/add/add-ai-agent.ts +468 -0
  125. package/src/add/add-channel.ts +103 -79
  126. package/src/add/add-cli.ts +68 -24
  127. package/src/add/add-file-extends-core-type.ts +1 -1
  128. package/src/add/add-file-with-config.ts +1 -1
  129. package/src/add/add-file-with-factory.ts +3 -1
  130. package/src/add/add-functions.ts +349 -103
  131. package/src/add/add-http-route.ts +263 -89
  132. package/src/add/add-http-routes.ts +229 -0
  133. package/src/add/add-keyed-wiring.ts +151 -0
  134. package/src/add/add-mcp-prompt.ts +27 -16
  135. package/src/add/add-mcp-resource.ts +28 -16
  136. package/src/add/add-middleware.ts +482 -80
  137. package/src/add/add-permission.ts +199 -40
  138. package/src/add/add-queue-worker.ts +25 -20
  139. package/src/add/add-rpc-invocations.ts +28 -11
  140. package/src/add/add-schedule.ts +17 -12
  141. package/src/add/add-secret.ts +140 -0
  142. package/src/add/add-trigger.ts +154 -0
  143. package/src/add/add-variable.ts +9 -0
  144. package/src/add/add-wire-addon.ts +80 -0
  145. package/src/add/add-workflow-graph.ts +180 -522
  146. package/src/add/add-workflow.ts +7 -6
  147. package/src/error-codes.ts +25 -1
  148. package/src/index.ts +23 -13
  149. package/src/inspector.ts +139 -19
  150. package/src/schema-generator.ts +1 -0
  151. package/src/types-map.ts +12 -1
  152. package/src/types.ts +199 -69
  153. package/src/utils/compute-required-schemas.ts +48 -0
  154. package/src/utils/contract-hashes.test.ts +553 -0
  155. package/src/utils/contract-hashes.ts +386 -0
  156. package/src/utils/custom-types-generator.ts +88 -0
  157. package/src/utils/detect-schema-vendor.ts +90 -0
  158. package/src/utils/does-type-extend-core-type.ts +1 -1
  159. package/src/utils/ensure-function-metadata.ts +325 -8
  160. package/src/utils/extract-function-name.ts +101 -351
  161. package/src/utils/extract-services.ts +35 -2
  162. package/src/utils/filter-inspector-state.test.ts +37 -25
  163. package/src/utils/filter-inspector-state.ts +146 -32
  164. package/src/utils/filter-utils.test.ts +1 -1
  165. package/src/utils/filter-utils.ts +2 -2
  166. package/src/utils/get-files-and-methods.ts +1 -1
  167. package/src/utils/get-property-value.ts +42 -4
  168. package/src/utils/hash.ts +26 -0
  169. package/src/utils/middleware.test.ts +204 -0
  170. package/src/utils/middleware.ts +131 -69
  171. package/src/utils/permissions.test.ts +35 -12
  172. package/src/utils/permissions.ts +12 -12
  173. package/src/utils/post-process.ts +306 -44
  174. package/src/utils/resolve-addon-package.ts +49 -0
  175. package/src/utils/resolve-function-types.ts +42 -0
  176. package/src/utils/resolve-identifier.ts +46 -0
  177. package/src/utils/resolve-versions.test.ts +249 -0
  178. package/src/utils/resolve-versions.ts +105 -0
  179. package/src/utils/schema-generator.ts +329 -0
  180. package/src/utils/serialize-inspector-state.ts +184 -43
  181. package/src/utils/serialize-mcp-json.ts +145 -0
  182. package/src/utils/serialize-middleware-groups-meta.ts +33 -0
  183. package/src/utils/serialize-openapi-json.ts +277 -0
  184. package/src/utils/serialize-permissions-groups-meta.ts +35 -0
  185. package/src/utils/test-data/inspector-state.json +69 -66
  186. package/src/utils/validate-auth-sessionless.ts +29 -0
  187. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +43 -119
  188. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +26 -6
  189. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +17 -10
  190. package/src/utils/workflow/graph/finalize-workflow-wires.ts +310 -0
  191. package/src/utils/workflow/graph/finalize-workflows.ts +100 -0
  192. package/src/utils/workflow/graph/index.ts +5 -0
  193. package/src/utils/workflow/graph/serialize-workflow-graph.ts +1 -8
  194. package/src/utils/workflow/graph/workflow-graph.types.ts +29 -78
  195. package/src/visit.ts +19 -7
  196. package/tsconfig.tsbuildinfo +1 -1
  197. package/dist/add/add-forge-credential.d.ts +0 -8
  198. package/dist/add/add-forge-credential.js +0 -77
  199. package/dist/add/add-forge-node.d.ts +0 -7
  200. package/dist/add/add-forge-node.js +0 -77
  201. package/dist/add/add-mcp-tool.d.ts +0 -2
  202. package/dist/add/add-mcp-tool.js +0 -81
  203. package/dist/utils/extract-service-metadata.d.ts +0 -19
  204. package/dist/utils/extract-service-metadata.js +0 -244
  205. package/dist/utils/write-service-metadata.d.ts +0 -13
  206. package/dist/utils/write-service-metadata.js +0 -37
  207. package/src/add/add-forge-credential.ts +0 -119
  208. package/src/add/add-forge-node.ts +0 -132
  209. package/src/add/add-mcp-tool.ts +0 -141
  210. package/src/utils/extract-service-metadata.ts +0 -353
  211. package/src/utils/write-service-metadata.ts +0 -51
@@ -4,21 +4,113 @@ import {
4
4
  getCommonWireMetaData,
5
5
  } from '../utils/get-property-value.js'
6
6
  import { pathToRegexp } from 'path-to-regexp'
7
- import { HTTPMethod } from '@pikku/core/http'
8
- import { extractFunctionName } from '../utils/extract-function-name.js'
9
- import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
10
- import { AddWiring } from '../types.js'
7
+ import type { HTTPMethod } from '@pikku/core/http'
8
+ import {
9
+ extractFunctionName,
10
+ makeContextBasedId,
11
+ } from '../utils/extract-function-name.js'
12
+ import {
13
+ getPropertyAssignmentInitializer,
14
+ extractTypeKeys,
15
+ } from '../utils/type-utils.js'
16
+ import type { AddWiring, InspectorState } from '../types.js'
11
17
  import { resolveHTTPMiddlewareFromObject } from '../utils/middleware.js'
12
18
  import { resolveHTTPPermissionsFromObject } from '../utils/permissions.js'
13
19
  import { extractWireNames } from '../utils/post-process.js'
14
20
  import { ensureFunctionMetadata } from '../utils/ensure-function-metadata.js'
15
21
  import { ErrorCode } from '../error-codes.js'
22
+ import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js'
23
+ import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js'
24
+
25
+ import type { InspectorLogger } from '../types.js'
26
+
27
+ /**
28
+ * Parameters for registering an HTTP route
29
+ */
30
+ export interface RegisterHTTPRouteParams {
31
+ obj: ts.ObjectLiteralExpression
32
+ state: InspectorState
33
+ checker: ts.TypeChecker
34
+ logger: InspectorLogger
35
+ sourceFile: ts.SourceFile
36
+ basePath?: string
37
+ inheritedTags?: string[]
38
+ inheritedAuth?: boolean
39
+ }
40
+
41
+ /**
42
+ * Extract header schema reference from headers property
43
+ */
44
+ const extractHeadersSchema = (
45
+ obj: ts.ObjectLiteralExpression,
46
+ routeName: string,
47
+ method: string,
48
+ state: InspectorState,
49
+ checker: ts.TypeChecker,
50
+ logger: InspectorLogger
51
+ ): string | undefined => {
52
+ const headersNode = getPropertyAssignmentInitializer(
53
+ obj,
54
+ 'headers',
55
+ true,
56
+ checker
57
+ )
58
+ if (!headersNode || !ts.isIdentifier(headersNode)) return undefined
59
+
60
+ // Resolve the schema reference
61
+ const symbol = checker.getSymbolAtLocation(headersNode)
62
+ if (!symbol) return undefined
63
+
64
+ const decl = symbol.valueDeclaration || symbol.declarations?.[0]
65
+ if (!decl) return undefined
66
+
67
+ let sourceFile: string
68
+ if (ts.isImportSpecifier(decl)) {
69
+ const aliasedSymbol = checker.getAliasedSymbol(symbol)
70
+ if (aliasedSymbol) {
71
+ const aliasedDecl =
72
+ aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0]
73
+ if (aliasedDecl) {
74
+ sourceFile = aliasedDecl.getSourceFile().fileName
75
+ } else {
76
+ return undefined
77
+ }
78
+ } else {
79
+ return undefined
80
+ }
81
+ } else {
82
+ sourceFile = decl.getSourceFile().fileName
83
+ }
84
+
85
+ const vendor = detectSchemaVendorOrError(
86
+ headersNode,
87
+ checker,
88
+ logger,
89
+ `Route '${routeName}' header`,
90
+ sourceFile
91
+ )
92
+ if (!vendor) return undefined
93
+
94
+ // Create a sanitized schema name from route and method to avoid collisions
95
+ const sanitizedRoute = routeName
96
+ .replace(/[^a-zA-Z0-9]/g, '_')
97
+ .replace(/^_+|_+$/g, '')
98
+ const schemaName = `${method.toUpperCase()}_${sanitizedRoute}_Headers`
99
+
100
+ state.schemaLookup.set(schemaName, {
101
+ variableName: headersNode.text,
102
+ sourceFile,
103
+ vendor,
104
+ })
105
+
106
+ return schemaName
107
+ }
16
108
 
17
109
  /**
18
110
  * Populate metaInputTypes for a given route based on method, input type,
19
- * query and params. Returns undefined (we only mutate metaTypes).
111
+ * query and params.
20
112
  */
21
- export const getInputTypes = (
113
+ const computeInputTypes = (
22
114
  metaTypes: Map<
23
115
  string,
24
116
  { query?: string[]; params?: string[]; body?: string[] }
@@ -27,7 +119,7 @@ export const getInputTypes = (
27
119
  inputType: string | null,
28
120
  queryValues: string[],
29
121
  paramsValues: string[]
30
- ): undefined => {
122
+ ): void => {
31
123
  if (!inputType) return
32
124
  metaTypes.set(inputType, {
33
125
  query: queryValues,
@@ -36,72 +128,61 @@ export const getInputTypes = (
36
128
  ? [...new Set([...queryValues, ...paramsValues])]
37
129
  : [],
38
130
  })
39
- return
40
131
  }
41
132
 
42
133
  /**
43
- * Simplified wireHTTP: re-uses function metadata from state.functions.meta
44
- * instead of re-inferring types here.
134
+ * Shared function to register an HTTP route in the inspector state.
135
+ * Used by both wireHTTP and wireHTTPRoutes.
45
136
  */
46
- export const addHTTPRoute: AddWiring = (
47
- logger,
48
- node,
49
- checker,
137
+ export function registerHTTPRoute({
138
+ obj,
50
139
  state,
51
- options
52
- ) => {
53
- // only look at calls
54
- if (!ts.isCallExpression(node)) return
140
+ checker,
141
+ logger,
142
+ sourceFile,
143
+ basePath = '',
144
+ inheritedTags = [],
145
+ inheritedAuth,
146
+ }: RegisterHTTPRouteParams): void {
147
+ // Extract route path
148
+ const routePath = getPropertyValue(obj, 'route') as string | null
149
+ if (!routePath) return
55
150
 
56
- const { expression, arguments: args } = node
57
- if (!ts.isIdentifier(expression) || expression.text !== 'wireHTTP') return
151
+ const method = (
152
+ (getPropertyValue(obj, 'method') as string) || 'get'
153
+ ).toLowerCase()
154
+ const fullRoute = basePath + routePath
58
155
 
59
- // must pass an object literal
60
- const firstArg = args[0]
61
- if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) return
62
- const obj = firstArg
156
+ // Extract params from route path
157
+ let params: string[] = []
158
+ try {
159
+ const keys = pathToRegexp(fullRoute).keys
160
+ params = keys.filter((k) => k.type === 'param').map((k) => k.name)
161
+ } catch (e) {
162
+ logger.error(
163
+ `Failed to parse route '${fullRoute}': ${e instanceof Error ? e.message : e}`
164
+ )
165
+ return
166
+ }
63
167
 
64
- // --- extract HTTP metadata ---
65
- const route = getPropertyValue(obj, 'route') as string | null
66
- if (!route) return
168
+ // Get common metadata
169
+ const {
170
+ disabled,
171
+ title,
172
+ tags: routeTags,
173
+ summary,
174
+ description,
175
+ errors,
176
+ } = getCommonWireMetaData(obj, 'HTTP route', fullRoute, logger)
67
177
 
68
- const keys = pathToRegexp(route).keys
69
- const params = keys.filter((k) => k.type === 'param').map((k) => k.name)
178
+ if (disabled) return
70
179
 
71
- const method =
72
- (getPropertyValue(obj, 'method') as string)?.toLowerCase() || 'get'
73
- const { title, tags, summary, description, errors } = getCommonWireMetaData(
74
- obj,
75
- 'HTTP route',
76
- route,
77
- logger
78
- )
79
- const query = (getPropertyValue(obj, 'query') as string[]) || []
180
+ // Merge inherited tags with route tags
181
+ const tags = [...inheritedTags, ...(routeTags || [])]
80
182
 
81
- // Check if this is a workflow trigger (workflow: true)
82
- const isWorkflowTrigger = getPropertyValue(obj, 'workflow') === true
83
- if (isWorkflowTrigger) {
84
- // Workflow triggers don't need func - they're handled by workflow-utils
85
- // Just record the route for HTTP meta but skip function processing
86
- state.http.files.add(node.getSourceFile().fileName)
87
- state.http.meta[method][route] = {
88
- pikkuFuncName: '', // No function - workflow handles it
89
- route,
90
- method: method as HTTPMethod,
91
- params: params.length > 0 ? params : undefined,
92
- query: query.length > 0 ? query : undefined,
93
- inputTypes: undefined,
94
- title,
95
- summary,
96
- description,
97
- errors,
98
- tags,
99
- workflow: true,
100
- }
101
- return
102
- }
183
+ const query = (getPropertyValue(obj, 'query') as string[]) || []
103
184
 
104
- // --- find the referenced function name first for filtering ---
185
+ // Get function reference
105
186
  const funcInitializer = getPropertyAssignmentInitializer(
106
187
  obj,
107
188
  'func',
@@ -111,21 +192,27 @@ export const addHTTPRoute: AddWiring = (
111
192
  if (!funcInitializer) {
112
193
  logger.critical(
113
194
  ErrorCode.MISSING_FUNC,
114
- `No valid 'func' property for route '${route}'.`
195
+ `No valid 'func' property for route '${fullRoute}'.`
115
196
  )
116
197
  return
117
198
  }
118
199
 
119
- const funcName = extractFunctionName(
200
+ const extracted = extractFunctionName(funcInitializer, checker, state.rootDir)
201
+ let funcName = extracted.pikkuFuncId
202
+ if (funcName.startsWith('__temp_')) {
203
+ funcName = makeContextBasedId('http', method, fullRoute)
204
+ }
205
+
206
+ ensureFunctionMetadata(
207
+ state,
208
+ funcName,
209
+ fullRoute,
120
210
  funcInitializer,
121
211
  checker,
122
- state.rootDir
123
- ).pikkuFuncName
124
-
125
- // Ensure function metadata exists (creates stub for inline functions)
126
- ensureFunctionMetadata(state, funcName, route)
212
+ extracted.isHelper
213
+ )
127
214
 
128
- // lookup existing function metadata
215
+ // Lookup existing function metadata
129
216
  const fnMeta = state.functions.meta[funcName]
130
217
  if (!fnMeta) {
131
218
  logger.critical(
@@ -134,36 +221,80 @@ export const addHTTPRoute: AddWiring = (
134
221
  )
135
222
  return
136
223
  }
224
+
225
+ if (
226
+ !validateAuthSessionless(
227
+ logger,
228
+ obj,
229
+ state,
230
+ funcName,
231
+ `Route '${fullRoute}'`,
232
+ inheritedAuth
233
+ )
234
+ ) {
235
+ return
236
+ }
237
+
137
238
  const input = fnMeta.inputs?.[0] || null
138
239
 
139
- // --- compute inputTypes (body/query/params) ---
140
- const inputTypes = getInputTypes(
141
- state.http.metaInputTypes,
142
- method,
143
- input,
144
- query,
145
- params
146
- )
240
+ // Validate that route params and query params exist in function input type
241
+ if (params.length > 0 || query.length > 0) {
242
+ const inputTypes = state.typesLookup.get(funcName)
243
+ if (inputTypes && inputTypes.length > 0) {
244
+ const inputKeys = extractTypeKeys(inputTypes[0])
245
+
246
+ // Check path params
247
+ if (params.length > 0) {
248
+ const missingParams = params.filter((p) => !inputKeys.includes(p))
249
+ if (missingParams.length > 0) {
250
+ logger.critical(
251
+ ErrorCode.ROUTE_PARAM_MISMATCH,
252
+ `Route '${fullRoute}' has path parameter(s) [${missingParams.join(', ')}] ` +
253
+ `not found in function '${funcName}' input type. ` +
254
+ `Input type has: [${inputKeys.join(', ')}]`
255
+ )
256
+ return
257
+ }
258
+ }
259
+
260
+ // Check query params
261
+ if (query.length > 0) {
262
+ const missingQuery = query.filter((q) => !inputKeys.includes(q))
263
+ if (missingQuery.length > 0) {
264
+ logger.critical(
265
+ ErrorCode.ROUTE_QUERY_MISMATCH,
266
+ `Route '${fullRoute}' has query parameter(s) [${missingQuery.join(', ')}] ` +
267
+ `not found in function '${funcName}' input type. ` +
268
+ `Input type has: [${inputKeys.join(', ')}]`
269
+ )
270
+ return
271
+ }
272
+ }
273
+ }
274
+ }
147
275
 
148
- // --- resolve middleware ---
276
+ // Compute inputTypes (body/query/params)
277
+ computeInputTypes(state.http.metaInputTypes, method, input, query, params)
278
+
279
+ // Resolve middleware
149
280
  const middleware = resolveHTTPMiddlewareFromObject(
150
281
  state,
151
- route,
282
+ fullRoute,
152
283
  obj,
153
284
  tags,
154
285
  checker
155
286
  )
156
287
 
157
- // --- resolve permissions ---
288
+ // Resolve permissions
158
289
  const permissions = resolveHTTPPermissionsFromObject(
159
290
  state,
160
- route,
291
+ fullRoute,
161
292
  obj,
162
293
  tags,
163
294
  checker
164
295
  )
165
296
 
166
- // --- track used functions/middleware/permissions for service aggregation ---
297
+ // Track used functions/middleware/permissions for service aggregation
167
298
  state.serviceAggregation.usedFunctions.add(funcName)
168
299
  extractWireNames(middleware).forEach((name) =>
169
300
  state.serviceAggregation.usedMiddleware.add(name)
@@ -172,21 +303,64 @@ export const addHTTPRoute: AddWiring = (
172
303
  state.serviceAggregation.usedPermissions.add(name)
173
304
  )
174
305
 
175
- // --- record route ---
176
- state.http.files.add(node.getSourceFile().fileName)
177
- state.http.meta[method][route] = {
178
- pikkuFuncName: funcName,
179
- route,
306
+ // Check for SSE
307
+ const sse = getPropertyValue(obj, 'sse') === true
308
+
309
+ // Extract header schema
310
+ const headersSchemaName = extractHeadersSchema(
311
+ obj,
312
+ fullRoute,
313
+ method,
314
+ state,
315
+ checker,
316
+ logger
317
+ )
318
+
319
+ // Record route
320
+ state.http.files.add(sourceFile.fileName)
321
+ state.http.meta[method][fullRoute] = {
322
+ pikkuFuncId: funcName,
323
+ route: fullRoute,
180
324
  method: method as HTTPMethod,
181
325
  params: params.length > 0 ? params : undefined,
182
326
  query: query.length > 0 ? query : undefined,
183
- inputTypes,
327
+ inputTypes: undefined,
184
328
  title,
185
329
  summary,
186
330
  description,
187
331
  errors,
188
- tags,
332
+ tags: tags.length > 0 ? tags : undefined,
189
333
  middleware,
190
334
  permissions,
335
+ sse: sse ? true : undefined,
336
+ headersSchemaName,
337
+ groupBasePath: basePath || undefined,
191
338
  }
192
339
  }
340
+
341
+ /**
342
+ * Process wireHTTP calls
343
+ */
344
+ export const addHTTPRoute: AddWiring = (
345
+ logger,
346
+ node,
347
+ checker,
348
+ state,
349
+ _options
350
+ ) => {
351
+ if (!ts.isCallExpression(node)) return
352
+
353
+ const { expression, arguments: args } = node
354
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireHTTP') return
355
+
356
+ const firstArg = args[0]
357
+ if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) return
358
+
359
+ registerHTTPRoute({
360
+ obj: firstArg,
361
+ state,
362
+ checker,
363
+ logger,
364
+ sourceFile: node.getSourceFile(),
365
+ })
366
+ }
@@ -0,0 +1,229 @@
1
+ import * as ts from 'typescript'
2
+ import { getPropertyValue } from '../utils/get-property-value.js'
3
+ import type { AddWiring, InspectorState, InspectorLogger } from '../types.js'
4
+ import { registerHTTPRoute } from './add-http-route.js'
5
+ import { resolveIdentifier } from '../utils/resolve-identifier.js'
6
+
7
+ /**
8
+ * Group configuration extracted from wireHTTPRoutes or defineHTTPRoutes
9
+ */
10
+ interface GroupConfig {
11
+ basePath: string
12
+ tags: string[]
13
+ auth?: boolean
14
+ }
15
+
16
+ /**
17
+ * Process wireHTTPRoutes calls
18
+ */
19
+ export const addHTTPRoutes: AddWiring = (
20
+ logger,
21
+ node,
22
+ checker,
23
+ state,
24
+ _options
25
+ ) => {
26
+ if (!ts.isCallExpression(node)) return
27
+
28
+ const { expression, arguments: args } = node
29
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireHTTPRoutes')
30
+ return
31
+
32
+ const firstArg = args[0]
33
+ if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) return
34
+
35
+ // Extract group config
36
+ const groupConfig = extractGroupConfig(firstArg)
37
+
38
+ // Get routes property
39
+ const routesProp = getPropertyAssignment(firstArg, 'routes')
40
+ if (!routesProp) return
41
+
42
+ // Process routes recursively
43
+ processRoutes(
44
+ routesProp.initializer,
45
+ groupConfig,
46
+ state,
47
+ checker,
48
+ logger,
49
+ node.getSourceFile()
50
+ )
51
+ }
52
+
53
+ /**
54
+ * Get a property assignment from an object literal
55
+ */
56
+ function getPropertyAssignment(
57
+ obj: ts.ObjectLiteralExpression,
58
+ propName: string
59
+ ): ts.PropertyAssignment | undefined {
60
+ for (const prop of obj.properties) {
61
+ if (
62
+ ts.isPropertyAssignment(prop) &&
63
+ ts.isIdentifier(prop.name) &&
64
+ prop.name.text === propName
65
+ ) {
66
+ return prop
67
+ }
68
+ }
69
+ return undefined
70
+ }
71
+
72
+ /**
73
+ * Extract group configuration from an object literal
74
+ */
75
+ function extractGroupConfig(obj: ts.ObjectLiteralExpression): GroupConfig {
76
+ const basePath = (getPropertyValue(obj, 'basePath') as string) || ''
77
+ const tags = (getPropertyValue(obj, 'tags') as string[]) || []
78
+ const auth = getPropertyValue(obj, 'auth')
79
+
80
+ return {
81
+ basePath,
82
+ tags,
83
+ auth: auth === true ? true : auth === false ? false : undefined,
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Merge two group configs following cascading rules
89
+ */
90
+ function mergeConfigs(parent: GroupConfig, child: GroupConfig): GroupConfig {
91
+ return {
92
+ basePath: parent.basePath + child.basePath,
93
+ tags: [...parent.tags, ...child.tags],
94
+ auth: child.auth ?? parent.auth,
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Check if a value is a route config (has method, func, and route)
100
+ */
101
+ function isRouteConfig(obj: ts.ObjectLiteralExpression): boolean {
102
+ let hasMethod = false
103
+ let hasFunc = false
104
+ let hasRoute = false
105
+
106
+ for (const prop of obj.properties) {
107
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
108
+ if (prop.name.text === 'method') hasMethod = true
109
+ if (prop.name.text === 'func') hasFunc = true
110
+ if (prop.name.text === 'route') hasRoute = true
111
+ }
112
+ }
113
+
114
+ return hasMethod && hasFunc && hasRoute
115
+ }
116
+
117
+ /**
118
+ * Check if a value is a route contract (has routes property but no method/func)
119
+ */
120
+ function isRouteContract(obj: ts.ObjectLiteralExpression): boolean {
121
+ let hasRoutes = false
122
+ let hasMethod = false
123
+ let hasFunc = false
124
+
125
+ for (const prop of obj.properties) {
126
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
127
+ if (prop.name.text === 'routes') hasRoutes = true
128
+ if (prop.name.text === 'method') hasMethod = true
129
+ if (prop.name.text === 'func') hasFunc = true
130
+ }
131
+ }
132
+
133
+ return hasRoutes && !hasMethod && !hasFunc
134
+ }
135
+
136
+ /**
137
+ * Recursively process routes - handles nested maps, contracts, and identifiers
138
+ */
139
+ function processRoutes(
140
+ node: ts.Node,
141
+ parentConfig: GroupConfig,
142
+ state: InspectorState,
143
+ checker: ts.TypeChecker,
144
+ logger: InspectorLogger,
145
+ sourceFile: ts.SourceFile
146
+ ): void {
147
+ // Handle array of routes
148
+ if (ts.isArrayLiteralExpression(node)) {
149
+ for (const element of node.elements) {
150
+ if (ts.isObjectLiteralExpression(element) && isRouteConfig(element)) {
151
+ processRoute(element, parentConfig, state, checker, logger, sourceFile)
152
+ }
153
+ }
154
+ return
155
+ }
156
+
157
+ // Handle object literal
158
+ if (ts.isObjectLiteralExpression(node)) {
159
+ // Check if this is a route config
160
+ if (isRouteConfig(node)) {
161
+ processRoute(node, parentConfig, state, checker, logger, sourceFile)
162
+ return
163
+ }
164
+
165
+ // Check if this is a route contract
166
+ if (isRouteContract(node)) {
167
+ const contractConfig = extractGroupConfig(node)
168
+ const mergedConfig = mergeConfigs(parentConfig, contractConfig)
169
+ const routesProp = getPropertyAssignment(node, 'routes')
170
+ if (routesProp) {
171
+ processRoutes(
172
+ routesProp.initializer,
173
+ mergedConfig,
174
+ state,
175
+ checker,
176
+ logger,
177
+ sourceFile
178
+ )
179
+ }
180
+ return
181
+ }
182
+
183
+ // Otherwise it's a nested map - process each property
184
+ for (const prop of node.properties) {
185
+ if (ts.isPropertyAssignment(prop)) {
186
+ processRoutes(
187
+ prop.initializer,
188
+ parentConfig,
189
+ state,
190
+ checker,
191
+ logger,
192
+ sourceFile
193
+ )
194
+ }
195
+ }
196
+ return
197
+ }
198
+
199
+ // Handle identifier - resolve to its definition
200
+ if (ts.isIdentifier(node)) {
201
+ const resolved = resolveIdentifier(node, checker, ['defineHTTPRoutes'])
202
+ if (resolved) {
203
+ processRoutes(resolved, parentConfig, state, checker, logger, sourceFile)
204
+ }
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Register a single route using the shared registerHTTPRoute function
210
+ */
211
+ function processRoute(
212
+ obj: ts.ObjectLiteralExpression,
213
+ groupConfig: GroupConfig,
214
+ state: InspectorState,
215
+ checker: ts.TypeChecker,
216
+ logger: InspectorLogger,
217
+ sourceFile: ts.SourceFile
218
+ ): void {
219
+ registerHTTPRoute({
220
+ obj,
221
+ state,
222
+ checker,
223
+ logger,
224
+ sourceFile,
225
+ basePath: groupConfig.basePath,
226
+ inheritedTags: groupConfig.tags,
227
+ inheritedAuth: groupConfig.auth,
228
+ })
229
+ }