@pikku/inspector 0.12.75 → 0.12.81

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 (219) hide show
  1. package/CHANGELOG.md +330 -0
  2. package/dist/add/add-analytics.d.ts +9 -0
  3. package/dist/add/add-analytics.js +178 -0
  4. package/dist/add/add-auth.js +0 -54
  5. package/dist/add/add-feature-flag.d.ts +2 -0
  6. package/dist/add/add-feature-flag.js +162 -0
  7. package/dist/add/add-wire-addon.js +1 -0
  8. package/dist/add/add-wire-remote-addon.js +1 -0
  9. package/dist/index.d.ts +3 -1
  10. package/dist/index.js +3 -1
  11. package/dist/inspector.js +4 -0
  12. package/dist/surface/collect-surface.d.ts +58 -0
  13. package/dist/surface/collect-surface.js +613 -0
  14. package/dist/types.d.ts +20 -11
  15. package/dist/utils/filter-inspector-state.js +14 -0
  16. package/dist/utils/post-process.js +15 -0
  17. package/dist/utils/read-module-specifiers.d.ts +8 -0
  18. package/dist/utils/read-module-specifiers.js +23 -0
  19. package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
  20. package/dist/utils/read-tsconfig-out-dir.js +12 -0
  21. package/dist/utils/resolve-deploy-target.d.ts +7 -0
  22. package/dist/utils/resolve-deploy-target.js +15 -7
  23. package/dist/utils/serialize-inspector-state.d.ts +5 -0
  24. package/dist/utils/serialize-inspector-state.js +8 -0
  25. package/dist/visit.js +4 -0
  26. package/package.json +16 -6
  27. package/run-tests.sh +21 -6
  28. package/src/add/add-addon-bans.ts +0 -85
  29. package/src/add/add-agent-scorer.test.ts +0 -197
  30. package/src/add/add-agent-scorer.ts +0 -83
  31. package/src/add/add-agent.test.ts +0 -87
  32. package/src/add/add-agent.ts +0 -638
  33. package/src/add/add-approval-description.ts +0 -76
  34. package/src/add/add-auth.test.ts +0 -791
  35. package/src/add/add-auth.ts +0 -424
  36. package/src/add/add-channel.ts +0 -778
  37. package/src/add/add-cli-renderers.test.ts +0 -74
  38. package/src/add/add-cli.ts +0 -1006
  39. package/src/add/add-credential.ts +0 -186
  40. package/src/add/add-feature.test.ts +0 -306
  41. package/src/add/add-feature.ts +0 -253
  42. package/src/add/add-file-extends-core-type.ts +0 -66
  43. package/src/add/add-file-with-config.ts +0 -45
  44. package/src/add/add-file-with-factory.test.ts +0 -134
  45. package/src/add/add-file-with-factory.ts +0 -288
  46. package/src/add/add-functions.test.ts +0 -743
  47. package/src/add/add-functions.ts +0 -1548
  48. package/src/add/add-gateway.ts +0 -143
  49. package/src/add/add-http-route-ref.test.ts +0 -149
  50. package/src/add/add-http-route.ts +0 -530
  51. package/src/add/add-http-routes.ts +0 -471
  52. package/src/add/add-keyed-wiring.ts +0 -180
  53. package/src/add/add-mcp-prompt.ts +0 -157
  54. package/src/add/add-mcp-resource.ts +0 -175
  55. package/src/add/add-middleware.test.ts +0 -196
  56. package/src/add/add-middleware.ts +0 -879
  57. package/src/add/add-permission-auth.test.ts +0 -59
  58. package/src/add/add-permission.ts +0 -311
  59. package/src/add/add-personas.test.ts +0 -406
  60. package/src/add/add-personas.ts +0 -287
  61. package/src/add/add-queue-worker.ts +0 -117
  62. package/src/add/add-rpc-invocations.test.ts +0 -287
  63. package/src/add/add-rpc-invocations.ts +0 -221
  64. package/src/add/add-scenario-step.test.ts +0 -509
  65. package/src/add/add-schedule.ts +0 -120
  66. package/src/add/add-scope.test.ts +0 -419
  67. package/src/add/add-scope.ts +0 -221
  68. package/src/add/add-secret.ts +0 -10
  69. package/src/add/add-subject-scenario-steps.test.ts +0 -161
  70. package/src/add/add-system-role.test.ts +0 -297
  71. package/src/add/add-system-role.ts +0 -162
  72. package/src/add/add-trigger.ts +0 -165
  73. package/src/add/add-variable.ts +0 -10
  74. package/src/add/add-wire-addon.test.ts +0 -185
  75. package/src/add/add-wire-addon.ts +0 -146
  76. package/src/add/add-wire-remote-addon.ts +0 -80
  77. package/src/add/add-workflow-edge-cases.test.ts +0 -201
  78. package/src/add/add-workflow-fanout.test.ts +0 -166
  79. package/src/add/add-workflow-graph-foreach.test.ts +0 -174
  80. package/src/add/add-workflow-graph-input.test.ts +0 -94
  81. package/src/add/add-workflow-graph-notes.test.ts +0 -74
  82. package/src/add/add-workflow-graph.ts +0 -644
  83. package/src/add/add-workflow-on-error.test.ts +0 -118
  84. package/src/add/add-workflow-return-switch.test.ts +0 -121
  85. package/src/add/add-workflow-silent-drops.test.ts +0 -261
  86. package/src/add/add-workflow-sleep-expression.test.ts +0 -96
  87. package/src/add/add-workflow.test.ts +0 -155
  88. package/src/add/add-workflow.ts +0 -482
  89. package/src/add/addon-bans.test.ts +0 -121
  90. package/src/add/addon-contracts.test.ts +0 -221
  91. package/src/add/allow-escape-hatches.test.ts +0 -144
  92. package/src/add/audit-meta.test.ts +0 -75
  93. package/src/add/classified-sink-check.test.ts +0 -347
  94. package/src/add/config-value-metadata.test.ts +0 -263
  95. package/src/add/dynamic-import-check.test.ts +0 -119
  96. package/src/add/expect-eventually-scenario-only.test.ts +0 -108
  97. package/src/add/function-body-span.test.ts +0 -96
  98. package/src/add/inline-wiring-function.test.ts +0 -104
  99. package/src/add/pii-check.test.ts +0 -258
  100. package/src/add/rpc-type-cast.test.ts +0 -123
  101. package/src/add/scenario-instrumentation.test.ts +0 -89
  102. package/src/add/scenario-instrumentation.ts +0 -42
  103. package/src/add/scenario-marker.test.ts +0 -89
  104. package/src/add/scenario-not-extractable.test.ts +0 -159
  105. package/src/add/scenario-services.test.ts +0 -103
  106. package/src/add/scenario-skip.test.ts +0 -83
  107. package/src/add/validate-workflow-graph-addons.test.ts +0 -102
  108. package/src/add/wire-name-literal.test.ts +0 -117
  109. package/src/error-codes-emitted.test.ts +0 -76
  110. package/src/error-codes.ts +0 -145
  111. package/src/index.ts +0 -66
  112. package/src/inspector.ts +0 -575
  113. package/src/schema-generator.ts +0 -1
  114. package/src/source-file-cache.ts +0 -73
  115. package/src/tsconfig-paths.test.ts +0 -90
  116. package/src/types-map.ts +0 -141
  117. package/src/types.ts +0 -775
  118. package/src/utils/annotate-http-route-auth.test.ts +0 -161
  119. package/src/utils/annotate-http-route-auth.ts +0 -53
  120. package/src/utils/check-classified-sinks.ts +0 -215
  121. package/src/utils/check-pii-output.ts +0 -120
  122. package/src/utils/compute-diagnostics.test.ts +0 -106
  123. package/src/utils/compute-required-schemas.ts +0 -48
  124. package/src/utils/contract-hashes.test.ts +0 -553
  125. package/src/utils/contract-hashes.ts +0 -387
  126. package/src/utils/custom-types-generator.test.ts +0 -124
  127. package/src/utils/custom-types-generator.ts +0 -150
  128. package/src/utils/detect-schema-vendor.test.ts +0 -123
  129. package/src/utils/detect-schema-vendor.ts +0 -120
  130. package/src/utils/does-type-extend-core-type.ts +0 -53
  131. package/src/utils/ensure-function-metadata.ts +0 -385
  132. package/src/utils/extract-function-name.ts +0 -612
  133. package/src/utils/extract-function-node.ts +0 -70
  134. package/src/utils/extract-node-value.test.ts +0 -117
  135. package/src/utils/extract-node-value.ts +0 -172
  136. package/src/utils/extract-secret-usage.test.ts +0 -81
  137. package/src/utils/extract-secret-usage.ts +0 -73
  138. package/src/utils/extract-services.ts +0 -70
  139. package/src/utils/extract-surface-usage.test.ts +0 -147
  140. package/src/utils/extract-surface-usage.ts +0 -121
  141. package/src/utils/filter-inspector-state.test.ts +0 -2232
  142. package/src/utils/filter-inspector-state.ts +0 -1298
  143. package/src/utils/filter-utils.test.ts +0 -843
  144. package/src/utils/filter-utils.ts +0 -161
  145. package/src/utils/find-root-dir.ts +0 -68
  146. package/src/utils/find-zod-transform.test.ts +0 -70
  147. package/src/utils/find-zod-transform.ts +0 -103
  148. package/src/utils/get-exported-variable-name.ts +0 -48
  149. package/src/utils/get-files-and-methods.ts +0 -157
  150. package/src/utils/get-property-value.test.ts +0 -141
  151. package/src/utils/get-property-value.ts +0 -331
  152. package/src/utils/hash.ts +0 -26
  153. package/src/utils/literal-properties.ts +0 -97
  154. package/src/utils/load-addon-functions-meta.test.ts +0 -157
  155. package/src/utils/load-addon-functions-meta.ts +0 -508
  156. package/src/utils/load-addon-scopes.test.ts +0 -138
  157. package/src/utils/middleware.test.ts +0 -204
  158. package/src/utils/middleware.ts +0 -303
  159. package/src/utils/nested-project-filter.test.ts +0 -53
  160. package/src/utils/nested-project-filter.ts +0 -28
  161. package/src/utils/permissions-shorthand.test.ts +0 -79
  162. package/src/utils/permissions.test.ts +0 -125
  163. package/src/utils/permissions.ts +0 -129
  164. package/src/utils/post-process.test.ts +0 -1094
  165. package/src/utils/post-process.ts +0 -1604
  166. package/src/utils/resolve-addon-package.ts +0 -106
  167. package/src/utils/resolve-core-type.test.ts +0 -142
  168. package/src/utils/resolve-core-type.ts +0 -35
  169. package/src/utils/resolve-deploy-target.test.ts +0 -135
  170. package/src/utils/resolve-deploy-target.ts +0 -65
  171. package/src/utils/resolve-function-meta.ts +0 -25
  172. package/src/utils/resolve-function-types.ts +0 -42
  173. package/src/utils/resolve-identifier.ts +0 -46
  174. package/src/utils/resolve-ref-contract.ts +0 -71
  175. package/src/utils/resolve-versions.test.ts +0 -499
  176. package/src/utils/resolve-versions.ts +0 -228
  177. package/src/utils/schema-cache.test.ts +0 -124
  178. package/src/utils/schema-generator.test.ts +0 -272
  179. package/src/utils/schema-generator.ts +0 -780
  180. package/src/utils/secret-alias-services.test.ts +0 -146
  181. package/src/utils/secret-alias-services.ts +0 -58
  182. package/src/utils/serialize-inspector-state.ts +0 -742
  183. package/src/utils/serialize-mcp-json.ts +0 -150
  184. package/src/utils/serialize-middleware-groups-meta.ts +0 -36
  185. package/src/utils/serialize-openapi-json.ts +0 -277
  186. package/src/utils/serialize-permissions-groups-meta.ts +0 -7
  187. package/src/utils/single-declaration.ts +0 -57
  188. package/src/utils/type-utils.ts +0 -145
  189. package/src/utils/validate-auth-sessionless.ts +0 -30
  190. package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
  191. package/src/utils/validate-exposed-functions-gated.ts +0 -103
  192. package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
  193. package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
  194. package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
  195. package/src/utils/workflow/derive-workflow-plan.ts +0 -117
  196. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
  197. package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
  198. package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
  199. package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
  200. package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
  201. package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
  202. package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
  203. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
  204. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
  205. package/src/utils/workflow/dsl/index.ts +0 -11
  206. package/src/utils/workflow/dsl/patterns.ts +0 -440
  207. package/src/utils/workflow/dsl/validation.ts +0 -203
  208. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
  209. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
  210. package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
  211. package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
  212. package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
  213. package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
  214. package/src/utils/workflow/graph/index.ts +0 -28
  215. package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
  216. package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
  217. package/src/visit.ts +0 -181
  218. package/tsconfig.json +0 -19
  219. package/tsconfig.tsbuildinfo +0 -1
@@ -1,1548 +0,0 @@
1
- import * as ts from 'typescript'
2
- import type { AddWiring, InspectorState, SchemaRef } from '../types.js'
3
- import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js'
4
- import type { TypesMap } from '../types-map.js'
5
- import {
6
- extractFunctionName,
7
- funcIdToTypeName,
8
- } from '../utils/extract-function-name.js'
9
- import { extractFunctionNode } from '../utils/extract-function-node.js'
10
- import { extractUsedWires } from '../utils/extract-services.js'
11
- import type { AuditDurability } from '@pikku/core/services'
12
- import type { FunctionServicesMeta } from '@pikku/core/function'
13
- import type { ScenarioSurface, ScenarioStepKind } from '@pikku/core/scenario'
14
- import { formatVersionedId, parseVersionedId } from '@pikku/core/utils'
15
-
16
- /** Binding keys a `pikkuScenarioStep` may declare, in report order. */
17
- const SCENARIO_SURFACE_KEYS: readonly ScenarioSurface[] = [
18
- 'browser',
19
- 'cli',
20
- 'default',
21
- ]
22
-
23
- /**
24
- * The step wrappers, and who acts in each.
25
- *
26
- * Which function was called *is* the kind. That is the whole reason these are
27
- * three declarations rather than a `kind:` field on one: a string literal can be
28
- * copied into an addon package by a consumer, a called identifier cannot.
29
- */
30
- const SCENARIO_STEP_KINDS = new Map<string, ScenarioStepKind>([
31
- ['pikkuScenarioStep', 'persona'],
32
- ['pikkuPlatformScenarioStep', 'platform'],
33
- ['pikkuAddonScenarioStep', 'addon'],
34
- ])
35
- import {
36
- getPropertyValue,
37
- getArrayPropertyValue,
38
- getCommonWireMetaData,
39
- } from '../utils/get-property-value.js'
40
- import { canonicalJSON, hashString } from '../utils/hash.js'
41
- import { resolveMiddleware } from '../utils/middleware.js'
42
- import { resolvePermissions } from '../utils/permissions.js'
43
- import { extractWireNames } from '../utils/post-process.js'
44
- import { ErrorCode } from '../error-codes.js'
45
- import { findPiiPaths } from '../utils/check-pii-output.js'
46
- import { findClassifiedSinks } from '../utils/check-classified-sinks.js'
47
- import { isScenarioInstrumentationFunction } from './scenario-instrumentation.js'
48
- import type { NodeType } from '@pikku/core/node'
49
-
50
- const isValidVariableName = (name: string) => {
51
- const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
52
- return regex.test(name)
53
- }
54
-
55
- /**
56
- * True when the handler body contains a runtime dynamic `import(...)` call
57
- * anywhere (including nested callbacks). Type-only positions like
58
- * `import('x').Foo` are `ImportTypeNode`s, not `CallExpression`s with an
59
- * `ImportKeyword` callee, so they are intentionally not matched.
60
- */
61
- const handlerHasDynamicImport = (
62
- handler: ts.ArrowFunction | ts.FunctionExpression
63
- ): boolean => {
64
- let found = false
65
- const visit = (node: ts.Node) => {
66
- if (found) {
67
- return
68
- }
69
- if (
70
- ts.isCallExpression(node) &&
71
- node.expression.kind === ts.SyntaxKind.ImportKeyword
72
- ) {
73
- found = true
74
- return
75
- }
76
- ts.forEachChild(node, visit)
77
- }
78
- visit(handler.body)
79
- return found
80
- }
81
-
82
- const nullifyTypes = (type: string | null) => {
83
- if (
84
- type === 'void' ||
85
- type === 'undefined' ||
86
- type === 'unknown' ||
87
- type === 'any' ||
88
- type === 'null'
89
- ) {
90
- return null
91
- }
92
- return type
93
- }
94
-
95
- const resolveTypeImports = (
96
- type: ts.Type,
97
- resolvedTypes: TypesMap,
98
- isCustom: boolean,
99
- checker: ts.TypeChecker
100
- ): string[] => {
101
- const types: string[] = []
102
-
103
- const visitType = (currentType: ts.Type) => {
104
- const symbol = currentType.aliasSymbol || currentType.getSymbol()
105
-
106
- if (symbol) {
107
- const declarations = symbol.getDeclarations()
108
- const declaration = declarations?.[0]
109
- if (declaration) {
110
- const sourceFile = declaration.getSourceFile()
111
- const path = sourceFile.fileName
112
-
113
- // Skip built-in utility types or TypeScript lib types
114
- // Skip enum members (but not the enum type itself)
115
- const isEnumMember = declaration && ts.isEnumMember(declaration)
116
-
117
- if (
118
- !path.includes('node_modules/typescript') &&
119
- symbol.getName() !== '__type' &&
120
- !isPrimitiveType(currentType) &&
121
- !isEnumMember
122
- ) {
123
- const originalName = symbol.getName()
124
- // Check if the type is already in the map
125
- let uniqueName = resolvedTypes.exists(originalName, path)
126
- if (!uniqueName) {
127
- if (isCustom) {
128
- uniqueName = resolvedTypes.addUniqueType(originalName, path)
129
- } else {
130
- resolvedTypes.addType(originalName, path)
131
- uniqueName = originalName
132
- }
133
- }
134
- types.push(uniqueName)
135
- }
136
- }
137
- }
138
-
139
- if (isCustom) {
140
- // Handle nested utility types like Partial, Pick, etc.
141
- if (currentType.aliasTypeArguments) {
142
- currentType.aliasTypeArguments.forEach(visitType)
143
- }
144
-
145
- // Handle intersections and unions
146
- if (currentType.isUnionOrIntersection()) {
147
- currentType.types.forEach(visitType)
148
- }
149
-
150
- // Handle object types with type arguments
151
- if (
152
- currentType.flags & ts.TypeFlags.Object &&
153
- (currentType as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference
154
- ) {
155
- const typeRef = currentType as ts.TypeReference
156
- typeRef.typeArguments?.forEach(visitType)
157
- }
158
- }
159
-
160
- // Always traverse type arguments for thorough type collection
161
- if (currentType.aliasTypeArguments) {
162
- currentType.aliasTypeArguments.forEach(visitType)
163
- }
164
-
165
- // Always handle intersections and unions
166
- if (currentType.isUnionOrIntersection()) {
167
- currentType.types.forEach(visitType)
168
- }
169
-
170
- // Always handle object types with type arguments
171
- if (
172
- currentType.flags & ts.TypeFlags.Object &&
173
- (currentType as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference
174
- ) {
175
- const typeRef = currentType as ts.TypeReference
176
- typeRef.typeArguments?.forEach(visitType)
177
- }
178
-
179
- // Handle anonymous object types with enum properties (e.g., { userType: UserType })
180
- // Only traverse into enum property types to avoid over-importing other named types
181
- if (currentType.flags & ts.TypeFlags.Object) {
182
- const objectType = currentType as ts.ObjectType
183
- const typeSymbol = objectType.getSymbol()
184
-
185
- // Only traverse properties for anonymous object types (no symbol or __type symbol)
186
- // Skip named types, interfaces, and enums to avoid over-importing
187
- const isAnonymousObject = !typeSymbol || typeSymbol.getName() === '__type'
188
-
189
- if (isAnonymousObject) {
190
- const properties = objectType.getProperties()
191
- for (const prop of properties) {
192
- if (prop.valueDeclaration) {
193
- const propType = checker.getTypeOfSymbolAtLocation(
194
- prop,
195
- prop.valueDeclaration
196
- )
197
- if (propType) {
198
- visitType(propType)
199
- }
200
- }
201
- }
202
- }
203
- }
204
- }
205
-
206
- visitType(type)
207
- return types
208
- }
209
-
210
- const resolveUnionTypes = (
211
- checker: ts.TypeChecker,
212
- type: ts.Type
213
- ): { types: ts.Type[]; names: string[] } => {
214
- const types: ts.Type[] = []
215
- const names: string[] = []
216
-
217
- // Check if it's a union type AND not part of an intersection
218
- if (type.isUnion() && !(type.flags & ts.TypeFlags.Intersection)) {
219
- for (const t of type.types) {
220
- const name = nullifyTypes(
221
- checker.typeToString(t, undefined, ts.TypeFormatFlags.NoTruncation)
222
- )
223
- if (name) {
224
- types.push(t)
225
- names.push(name)
226
- }
227
- }
228
- } else {
229
- const name = nullifyTypes(
230
- checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation)
231
- )
232
- if (name) {
233
- types.push(type)
234
- names.push(name)
235
- }
236
- }
237
-
238
- return { types, names }
239
- }
240
-
241
- const getNamesAndTypes = (
242
- checker: ts.TypeChecker,
243
- typesMap: TypesMap,
244
- direction: 'Input' | 'Output',
245
- funcName: string,
246
- type?: ts.Type
247
- ) => {
248
- if (!type) {
249
- return { names: [], types: [] }
250
- }
251
-
252
- // 1) Handle an explicit void (or undefined or null) type up front
253
- if (type.flags & (ts.TypeFlags.VoidLike | ts.TypeFlags.Null)) {
254
- return {
255
- names: [],
256
- types: [],
257
- }
258
- }
259
-
260
- // 2) For unions, resolve all member names/types
261
- const { names: rawNames, types: rawTypes } = resolveUnionTypes(checker, type)
262
-
263
- // If the union is exactly [void], we'd have caught it above.
264
- // If it's e.g. [string, void], rawNames should already include 'void'.
265
-
266
- // 3) If multiple names or the single name isn't a valid identifier,
267
- // we emit an alias type.
268
- const firstName = rawNames[0]
269
- if (rawNames.length > 1 || (firstName && !isValidVariableName(firstName))) {
270
- const aliasType = rawNames.join(' | ')
271
- const aliasName = funcIdToTypeName(funcName) + direction
272
-
273
- // record the alias in your TypesMap
274
- const references = rawTypes.flatMap((t) =>
275
- resolveTypeImports(t, typesMap, true, checker)
276
- )
277
-
278
- typesMap.addCustomType(aliasName, aliasType, references)
279
-
280
- return {
281
- names: [aliasName],
282
- types: rawTypes,
283
- }
284
- }
285
-
286
- // 4) Single, valid name → inline it
287
- const uniqueNames = rawNames.flatMap((name, i) => {
288
- const t = rawTypes[i]
289
- if (!t) {
290
- throw new Error(`Expected type for name "${name}" in ${funcName}`)
291
- }
292
- if (isPrimitiveType(t)) {
293
- return name
294
- }
295
- // non-primitive: import/alias it inline
296
- return resolveTypeImports(t, typesMap, false, checker)
297
- })
298
-
299
- return {
300
- names: uniqueNames,
301
- types: rawTypes,
302
- }
303
- }
304
-
305
- const isPrimitiveType = (type: ts.Type): boolean => {
306
- const primitiveFlags =
307
- ts.TypeFlags.Number |
308
- ts.TypeFlags.String |
309
- ts.TypeFlags.Boolean |
310
- ts.TypeFlags.BigInt |
311
- ts.TypeFlags.ESSymbol |
312
- ts.TypeFlags.Void |
313
- ts.TypeFlags.Undefined |
314
- ts.TypeFlags.Null |
315
- ts.TypeFlags.Unknown |
316
- ts.TypeFlags.VoidLike
317
-
318
- return (type.flags & primitiveFlags) !== 0
319
- }
320
-
321
- /**
322
- * If `type` is a `Promise<T>`, return `T`, otherwise return `type` itself.
323
- */
324
- function unwrapPromise(checker: ts.TypeChecker, type: ts.Type): ts.Type {
325
- if (!type?.symbol) return type
326
-
327
- const isPromise =
328
- type.symbol.name === 'Promise' &&
329
- checker.getFullyQualifiedName(type.symbol).includes('Promise')
330
-
331
- // aliasTypeArguments covers most Promise<T> cases
332
- if (isPromise && type.aliasTypeArguments?.length === 1) {
333
- return type.aliasTypeArguments[0]!
334
- }
335
-
336
- // fallback for raw TypeReference
337
- if (isPromise && (type as ts.TypeReference).typeArguments?.length === 1) {
338
- return (type as ts.TypeReference).typeArguments![0]!
339
- }
340
-
341
- return type
342
- }
343
-
344
- const resolveExistingFunctionSource = (
345
- state: InspectorState,
346
- pikkuFuncId: string
347
- ): string | null => {
348
- return (
349
- state.functions.meta[pikkuFuncId]?.sourceFile ||
350
- state.rpc.internalFiles.get(pikkuFuncId)?.path ||
351
- null
352
- )
353
- }
354
-
355
- const areCompatibleFunctionIds = (
356
- existingId: string,
357
- incomingId: string
358
- ): boolean => {
359
- if (existingId === incomingId) {
360
- return true
361
- }
362
-
363
- const existingParsed = parseVersionedId(existingId)
364
- const incomingParsed = parseVersionedId(incomingId)
365
-
366
- return existingParsed.baseName === incomingParsed.baseName
367
- }
368
-
369
- function printNode(node: ts.Node): string {
370
- return ts
371
- .createPrinter({ removeComments: true })
372
- .printNode(ts.EmitHint.Unspecified, node, node.getSourceFile())
373
- }
374
-
375
- function computeImplementationHash(args: {
376
- wrapper: string
377
- handler: ts.ArrowFunction | ts.FunctionExpression
378
- objectNode?: ts.ObjectLiteralExpression
379
- isDirectFunction: boolean
380
- }): string {
381
- const { wrapper, handler, objectNode, isDirectFunction } = args
382
- return hashString(
383
- canonicalJSON({
384
- wrapper,
385
- isDirectFunction,
386
- handler: printNode(handler),
387
- config: objectNode ? printNode(objectNode) : null,
388
- })
389
- )
390
- }
391
-
392
- /**
393
- * Registers a `pikkuRemoteChannelFunc` declaration: a name, its schemas and its
394
- * description, with no body to inspect. The name has to reach `rpc.internalMeta`
395
- * for `channel.remote` to resolve the schemas it validates each call against.
396
- */
397
- const addRemoteChannelFunc = ({
398
- state,
399
- logger,
400
- node,
401
- pikkuFuncId,
402
- name,
403
- exportedName,
404
- inputSchemaRef,
405
- outputSchemaRef,
406
- meta,
407
- }: {
408
- state: InspectorState
409
- logger: Parameters<AddWiring>[0]
410
- node: ts.CallExpression
411
- pikkuFuncId: string
412
- name: string
413
- exportedName?: string | null
414
- inputSchemaRef: SchemaRef | null
415
- outputSchemaRef: SchemaRef | null
416
- meta: {
417
- version?: number
418
- title?: string
419
- tags?: string[]
420
- summary?: string
421
- description?: string
422
- errors?: string[]
423
- }
424
- }) => {
425
- const sourceFile = node.getSourceFile().fileName
426
-
427
- if (!exportedName) {
428
- logger.error(
429
- `• Remote channel function '${name}' is not exported, this is not allowed.`
430
- )
431
- return
432
- }
433
-
434
- // Two files declaring the same capability is the collision that matters:
435
- // both ids are the bare name, so the id-compatibility check below reads them
436
- // as the same function and lets the second overwrite the first.
437
- const existingFunction = state.functions.meta[pikkuFuncId]
438
- if (
439
- existingFunction &&
440
- existingFunction.sourceFile &&
441
- existingFunction.sourceFile !== sourceFile
442
- ) {
443
- logger.critical(
444
- ErrorCode.DUPLICATE_FUNCTION_NAME,
445
- `Function name '${name}' is not unique. ` +
446
- `'${pikkuFuncId}' is already defined in '${existingFunction.sourceFile}' and cannot be redefined in '${sourceFile}'.`
447
- )
448
- return
449
- }
450
-
451
- const existing = state.rpc.internalMeta[name]
452
- if (existing && !areCompatibleFunctionIds(existing, pikkuFuncId)) {
453
- logger.critical(
454
- ErrorCode.DUPLICATE_FUNCTION_NAME,
455
- `Function name '${name}' is not unique. ` +
456
- `It already points to '${existing}' in '${resolveExistingFunctionSource(state, existing) || 'unknown file'}', but '${pikkuFuncId}' in '${sourceFile}' tried to use the same name.`
457
- )
458
- return
459
- }
460
-
461
- const capitalizedName = funcIdToTypeName(name)
462
- const registerSchema = (
463
- ref: SchemaRef | null,
464
- suffix: 'Input' | 'Output'
465
- ) => {
466
- if (!ref) return null
467
- const schemaName = `${capitalizedName}${suffix}`
468
- state.schemaLookup.set(schemaName, ref)
469
- state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
470
- return schemaName
471
- }
472
- const inputSchemaName = registerSchema(inputSchemaRef, 'Input')
473
- const outputSchemaName = registerSchema(outputSchemaRef, 'Output')
474
-
475
- state.functions.meta[pikkuFuncId] = {
476
- pikkuFuncId,
477
- functionType: 'remote',
478
- funcWrapper: 'pikkuRemoteChannelFunc',
479
- sessionless: true,
480
- name,
481
- services: { optimized: true, services: [] },
482
- inputSchemaName,
483
- outputSchemaName,
484
- inputs: inputSchemaName ? [inputSchemaName] : [],
485
- outputs: outputSchemaName ? [outputSchemaName] : [],
486
- sourceFile,
487
- exportedName,
488
- ...meta,
489
- }
490
-
491
- state.functions.files.set(pikkuFuncId, { path: sourceFile, exportedName })
492
- state.rpc.internalMeta[name] = pikkuFuncId
493
- // Imported so a local call gets the factory's "this runs on the client"
494
- // error rather than a bare lookup miss.
495
- state.rpc.internalFiles.set(pikkuFuncId, { path: sourceFile, exportedName })
496
- }
497
-
498
- /**
499
- * Inspect pikkuFunc calls, extract input/output and first-arg destructuring,
500
- * then push into state.functions.meta.
501
- */
502
- export const addFunctions: AddWiring = (
503
- logger,
504
- node,
505
- checker,
506
- state,
507
- options
508
- ) => {
509
- if (!ts.isCallExpression(node)) return
510
-
511
- const { expression, arguments: args, typeArguments } = node
512
-
513
- // only handle calls like pikkuFunc(...)
514
- if (!ts.isIdentifier(expression)) {
515
- return
516
- }
517
-
518
- // Match identifiers that contain both "pikku" and "func" (case insensitive),
519
- // plus pikkuScenario and the three scenario step wrappers (workflow wrappers
520
- // without "func" in their names)
521
- const pikkuFuncPattern = /pikku.*func/i
522
- if (
523
- !pikkuFuncPattern.test(expression.text) &&
524
- expression.text !== 'pikkuScenario' &&
525
- !SCENARIO_STEP_KINDS.has(expression.text)
526
- ) {
527
- return
528
- }
529
-
530
- // A scenario step is bundled like any invoked function, but is deliberately
531
- // never RPC-registered: a step may drive a browser, so exposing it over the
532
- // network would put a test harness on the public API surface.
533
- const scenarioStepKind = SCENARIO_STEP_KINDS.get(expression.text)
534
- const isScenarioStep = scenarioStepKind !== undefined
535
-
536
- // A scenario's own body. Marked so the codegen partitions and the deploy
537
- // analyzer can tell it apart from an application workflow without having to
538
- // reach into the workflow graph.
539
- const isScenario = expression.text === 'pikkuScenario'
540
-
541
- // only handle calls like pikkuFunc(...)
542
- if (!ts.isIdentifier(expression) || !expression.text.startsWith('pikku')) {
543
- return
544
- }
545
-
546
- if (args.length === 0) return
547
-
548
- let { pikkuFuncId, name, explicitName, exportedName } = extractFunctionName(
549
- node,
550
- checker,
551
- state.rootDir
552
- )
553
-
554
- if (!pikkuFuncId || pikkuFuncId.startsWith('__temp_')) {
555
- return
556
- }
557
-
558
- // pikku's own scenario instrumentation, which older projects still carry as a
559
- // scaffolded file in their source. It is not application code and must not be
560
- // registered, metadata'd or deployed as if it were — `pikku dev` registers the
561
- // implementations itself, and that is the only server that should have them.
562
- if (
563
- isScenarioInstrumentationFunction(pikkuFuncId) ||
564
- isScenarioInstrumentationFunction(name)
565
- ) {
566
- return
567
- }
568
-
569
- let title: string | undefined
570
- let tags: string[] | undefined
571
- let summary: string | undefined
572
- let description: string | undefined
573
- let errors: string[] | undefined
574
- let expose: boolean | undefined
575
- let remote: boolean | undefined
576
- let mcp: boolean | undefined
577
- /**
578
- * A sessionless function's own `auth: true`. `sessionless` records the
579
- * baseline — a `pikkuFunc` always needs a session — while this records the
580
- * tightening a `pikkuSessionlessFunc` applies to itself, which is otherwise
581
- * invisible to anything reading meta.
582
- */
583
- let auth: boolean | undefined
584
- /** The author's claim that the body authorizes its own callers. */
585
- let permissionsInBody: boolean | undefined
586
- /** The function's `audit` config, resolved to its object form. */
587
- let audit: { durability: AuditDurability } | undefined
588
- let readonly_: boolean | undefined
589
- let deploy: 'serverless' | 'server' | 'auto' | undefined
590
- let approvalRequired: boolean | undefined
591
- let approvalDescription: string | undefined
592
- let workflowQueued: boolean | undefined
593
- let workflowRetries: number | undefined
594
- let workflowTimeout: string | undefined
595
- let scenarioStepSurfaces: ScenarioSurface[] | undefined
596
- let scenarioStepRequiresActor: boolean | undefined
597
- let scenarioStepAddon: string | undefined
598
- let scenarioStepTemplate: string | undefined
599
- let scopes: string[] | undefined
600
- let version: number | undefined
601
- let objectNode: ts.ObjectLiteralExpression | undefined
602
- let nodeDisplayName: string | null = null
603
- let nodeCategory: string | null = null
604
- let nodeType: NodeType | null = null
605
- let nodeErrorOutput: boolean | null = null
606
-
607
- // Extract the function node using shared utility
608
- const firstArg = args[0]!
609
- const {
610
- funcNode: handlerNode,
611
- resolvedFunc,
612
- isDirectFunction,
613
- } = extractFunctionNode(firstArg, checker)
614
-
615
- // Variables to hold schema references if provided
616
- let inputSchemaRef: SchemaRef | null = null
617
- let outputSchemaRef: SchemaRef | null = null
618
-
619
- // Helper to resolve schema identifier to its actual source file and detect vendor.
620
- // Logs a fatal error and returns null if vendor cannot be determined.
621
- const resolveSchemaRef = (
622
- identifier: ts.Identifier,
623
- context: string
624
- ): SchemaRef | null => {
625
- const symbol = checker.getSymbolAtLocation(identifier)
626
- if (!symbol) return null
627
-
628
- const decl = symbol.valueDeclaration || symbol.declarations?.[0]
629
- if (!decl) return null
630
-
631
- let sourceFile: string
632
-
633
- // If it's an import specifier, resolve the aliased symbol to get the actual source
634
- if (ts.isImportSpecifier(decl)) {
635
- const aliasedSymbol = checker.getAliasedSymbol(symbol)
636
- if (aliasedSymbol) {
637
- const aliasedDecl =
638
- aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0]
639
- if (aliasedDecl) {
640
- sourceFile = aliasedDecl.getSourceFile().fileName
641
- } else {
642
- return null
643
- }
644
- } else {
645
- return null
646
- }
647
- } else {
648
- sourceFile = decl.getSourceFile().fileName
649
- }
650
-
651
- const vendor = detectSchemaVendorOrError(
652
- identifier,
653
- checker,
654
- logger,
655
- context,
656
- sourceFile
657
- )
658
- if (!vendor) return null
659
-
660
- return {
661
- variableName: identifier.text,
662
- sourceFile,
663
- vendor,
664
- }
665
- }
666
-
667
- // Extract config properties if using object form
668
- if (ts.isObjectLiteralExpression(firstArg)) {
669
- objectNode = firstArg
670
- const metadata = getCommonWireMetaData(
671
- firstArg,
672
- 'Function',
673
- name,
674
- logger,
675
- checker
676
- )
677
- if (metadata.disabled) return
678
- title = metadata.title
679
- tags = metadata.tags
680
- summary = metadata.summary
681
- description = metadata.description
682
- errors = metadata.errors
683
- expose = getPropertyValue(firstArg, 'expose') as boolean | undefined
684
- remote = getPropertyValue(firstArg, 'remote') as boolean | undefined
685
- mcp = getPropertyValue(firstArg, 'mcp') as boolean | undefined
686
- auth = getPropertyValue(firstArg, 'auth') as boolean | undefined
687
- permissionsInBody = getPropertyValue(firstArg, 'permissionsInBody') as
688
- boolean | undefined
689
- if (permissionsInBody && !options.allow?.permissionsInBody) {
690
- logger.critical(
691
- ErrorCode.PERMISSIONS_IN_BODY_NOT_ALLOWED,
692
- `'${name}' declares \`permissionsInBody: true\`, which this project has not enabled. ` +
693
- `It silences the exposed-and-ungated audit without gating anything, so it is opt-in: ` +
694
- `set \`allow: { permissionsInBody: true }\` in pikku.config.json if the project accepts ` +
695
- `permission checks that only a reader can verify. Otherwise express the check as a ` +
696
- `\`permissions\` entry, or drop \`expose: true\` if the function was not meant to be ` +
697
- `callable from outside.`
698
- )
699
- return
700
- }
701
- // `audit` is either `true` or `{ durability }`, so it needs the nested read:
702
- // getPropertyValue stringifies an object initializer rather than parsing it.
703
- // Mirrors core's `resolveAuditConfig` — the two must agree, or meta would
704
- // claim a durability the runner does not use.
705
- const auditProp = firstArg.properties.find(
706
- (p) =>
707
- ts.isPropertyAssignment(p) &&
708
- ts.isIdentifier(p.name) &&
709
- p.name.text === 'audit'
710
- )
711
- if (auditProp && ts.isPropertyAssignment(auditProp)) {
712
- const auditInit = auditProp.initializer
713
- if (auditInit.kind === ts.SyntaxKind.TrueKeyword) {
714
- audit = { durability: 'best-effort' }
715
- } else if (ts.isObjectLiteralExpression(auditInit)) {
716
- const durability = getPropertyValue(
717
- auditInit,
718
- 'durability'
719
- ) as AuditDurability | null
720
- audit = { durability: durability ?? 'best-effort' }
721
- }
722
- }
723
- readonly_ = getPropertyValue(firstArg, 'readonly') as boolean | undefined
724
- deploy = getPropertyValue(firstArg, 'deploy') as
725
- 'serverless' | 'server' | 'auto' | undefined
726
- approvalRequired = getPropertyValue(firstArg, 'approvalRequired') as
727
- boolean | undefined
728
- workflowQueued = getPropertyValue(firstArg, 'workflowQueued') as
729
- boolean | undefined
730
- workflowRetries = getPropertyValue(firstArg, 'workflowRetries') as
731
- number | undefined
732
- workflowTimeout = getPropertyValue(firstArg, 'workflowTimeout') as
733
- string | undefined
734
- // A step declares one implementation per surface it supports; the binding
735
- // keys that are present ARE the declaration, so there is nothing to read a
736
- // value from — only names to collect.
737
- const declaredSurfaces = SCENARIO_SURFACE_KEYS.filter((surface) =>
738
- firstArg.properties.some(
739
- (prop) =>
740
- (ts.isPropertyAssignment(prop) ||
741
- ts.isMethodDeclaration(prop) ||
742
- ts.isShorthandPropertyAssignment(prop)) &&
743
- ts.isIdentifier(prop.name) &&
744
- prop.name.text === surface
745
- )
746
- )
747
- if (scenarioStepKind === 'platform' || scenarioStepKind === 'addon') {
748
- // Neither kind has surfaces. Nobody clicks "Stripe's webhook arrives", and
749
- // there is no human behind "the platform has expired the trial" — so both
750
- // take a single `func`, and a binding key here is a coded error rather
751
- // than a convention nobody reads.
752
- if (declaredSurfaces.length > 0) {
753
- logger.error(
754
- `• ${expression.text}('${name}') declares ${declaredSurfaces.join('/')} — a ${scenarioStepKind} step has one implementation, declared as \`func\`. Surfaces are how a person drives the system, and nobody drives this one.`
755
- )
756
- return
757
- }
758
- // The phase rule depends on it: an assertion runs every witness it has and
759
- // fails if they disagree, so a step with exactly one witness by
760
- // construction has to be unable to look like it has several.
761
- scenarioStepSurfaces = ['default']
762
- if (scenarioStepKind === 'addon') {
763
- scenarioStepAddon = getPropertyValue(firstArg, 'addon') as
764
- string | undefined
765
- if (!scenarioStepAddon) {
766
- logger.error(
767
- `• pikkuAddonScenarioStep('${name}') needs a string literal \`addon\` naming the addon whose system acts — the same name its wireAddon declares.`
768
- )
769
- return
770
- }
771
- }
772
- } else {
773
- scenarioStepSurfaces = declaredSurfaces.length
774
- ? declaredSurfaces
775
- : undefined
776
- // A browser binding cannot be provisioned without a window to open, so it
777
- // requires an actor whether or not the step said so.
778
- scenarioStepRequiresActor =
779
- getPropertyValue(firstArg, 'actor') === true ||
780
- declaredSurfaces.includes('browser')
781
- ? true
782
- : undefined
783
- }
784
- scenarioStepTemplate = getPropertyValue(firstArg, 'template') as
785
- string | undefined
786
- scopes = getArrayPropertyValue(firstArg, 'scopes') ?? undefined
787
-
788
- // Extract approvalDescription identifier reference
789
- for (const prop of firstArg.properties) {
790
- if (
791
- ts.isPropertyAssignment(prop) &&
792
- ts.isIdentifier(prop.name) &&
793
- prop.name.text === 'approvalDescription' &&
794
- ts.isIdentifier(prop.initializer)
795
- ) {
796
- const { pikkuFuncId: descId } = extractFunctionName(
797
- prop.initializer,
798
- checker,
799
- state.rootDir
800
- )
801
- if (descId && !descId.startsWith('__temp_')) {
802
- approvalDescription = descId
803
- } else {
804
- // Try resolving the identifier directly
805
- approvalDescription = prop.initializer.text
806
- }
807
- break
808
- }
809
- }
810
-
811
- const versionRaw = getPropertyValue(firstArg, 'version')
812
- if (versionRaw !== null && versionRaw !== undefined) {
813
- const parsed = Number(versionRaw)
814
- if (Number.isInteger(parsed) && parsed >= 1) {
815
- version = parsed
816
- }
817
- }
818
-
819
- // Extract node config from nested object
820
- for (const prop of firstArg.properties) {
821
- if (
822
- ts.isPropertyAssignment(prop) &&
823
- ts.isIdentifier(prop.name) &&
824
- prop.name.text === 'node' &&
825
- ts.isObjectLiteralExpression(prop.initializer)
826
- ) {
827
- const nodeObj = prop.initializer
828
- nodeDisplayName = getPropertyValue(nodeObj, 'displayName') as
829
- string | null
830
- nodeCategory = getPropertyValue(nodeObj, 'category') as string | null
831
- nodeType = getPropertyValue(nodeObj, 'type') as NodeType | null
832
- nodeErrorOutput = getPropertyValue(nodeObj, 'errorOutput') as
833
- boolean | null
834
-
835
- if (!nodeDisplayName) {
836
- logger.critical(
837
- ErrorCode.MISSING_NAME,
838
- `Function '${name}' node config is missing the required 'displayName' property.`
839
- )
840
- }
841
- if (!nodeCategory) {
842
- logger.critical(
843
- ErrorCode.MISSING_NAME,
844
- `Function '${name}' node config is missing the required 'category' property.`
845
- )
846
- }
847
- if (!nodeType) {
848
- logger.critical(
849
- ErrorCode.MISSING_NAME,
850
- `Function '${name}' node config is missing the required 'type' property.`
851
- )
852
- } else if (!['trigger', 'action', 'end'].includes(nodeType)) {
853
- logger.critical(
854
- ErrorCode.INVALID_VALUE,
855
- `Function '${name}' node config has invalid type '${nodeType}'. Must be 'trigger', 'action', or 'end'.`
856
- )
857
- }
858
- break
859
- }
860
- }
861
-
862
- // Extract schema variable names from input/output properties
863
- for (const prop of firstArg.properties) {
864
- if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
865
- const propName = prop.name.text
866
- if (propName === 'input' || propName === 'output') {
867
- if (ts.isIdentifier(prop.initializer)) {
868
- // Good - it's a variable reference, resolve its actual source file and vendor
869
- const context = `Function '${name}' ${propName}`
870
- const ref = resolveSchemaRef(prop.initializer, context)
871
- if (ref) {
872
- if (propName === 'input') {
873
- inputSchemaRef = ref
874
- } else {
875
- outputSchemaRef = ref
876
- }
877
- }
878
- } else if (ts.isCallExpression(prop.initializer)) {
879
- // Bad - it's an inline expression
880
- const schemaName = `${funcIdToTypeName(name)}${propName.charAt(0).toUpperCase() + propName.slice(1)}`
881
- logger.critical(
882
- ErrorCode.INLINE_SCHEMA,
883
- `Inline schemas are not supported for '${propName}' in '${name}'.\n` +
884
- ` Extract to an exported variable:\n` +
885
- ` export const ${schemaName} = ${prop.initializer.getText()}\n` +
886
- ` Then use: ${propName}: ${schemaName}`
887
- )
888
- }
889
- }
890
- }
891
- }
892
- }
893
-
894
- if (version !== undefined) {
895
- let baseName = explicitName || exportedName || pikkuFuncId
896
- // Strip trailing VN suffix if it matches the version (e.g. getDataV1 + version:1 → getData@v1)
897
- const vSuffix = `V${version}`
898
- if (baseName.endsWith(vSuffix) && baseName.length > vSuffix.length) {
899
- baseName = baseName.slice(0, -vSuffix.length)
900
- }
901
- pikkuFuncId = formatVersionedId(baseName, version)
902
- }
903
-
904
- // A remote channel capability is a contract with no body: the connected
905
- // client answers it. Everything past this point reads the handler, so it gets
906
- // its own path rather than a nullable handler threaded through all of it.
907
- if (expression.text === 'pikkuRemoteChannelFunc') {
908
- addRemoteChannelFunc({
909
- state,
910
- logger,
911
- node,
912
- pikkuFuncId,
913
- name,
914
- exportedName,
915
- inputSchemaRef,
916
- outputSchemaRef,
917
- meta: { version, title, tags, summary, description, errors },
918
- })
919
- return
920
- }
921
-
922
- const isMCPToolFunc = expression.text === 'pikkuMCPToolFunc'
923
- const isListFunc = expression.text === 'pikkuListFunc'
924
- const mcpEnabled = mcp || isMCPToolFunc
925
-
926
- // Pick the handler: use resolvedFunc when it exists and is a function, otherwise fall back to handlerNode
927
- const handler =
928
- resolvedFunc &&
929
- (ts.isArrowFunction(resolvedFunc) || ts.isFunctionExpression(resolvedFunc))
930
- ? resolvedFunc
931
- : handlerNode
932
-
933
- // Validate that we got a valid function
934
- if (!ts.isArrowFunction(handler) && !ts.isFunctionExpression(handler)) {
935
- logger.error(`• No valid 'func' property found for ${pikkuFuncId}.`)
936
- // Create stub metadata to prevent "function not found" errors in wirings
937
- state.functions.meta[pikkuFuncId] = {
938
- pikkuFuncId,
939
- functionType: 'user',
940
- name,
941
- services: { optimized: false, services: [] },
942
- inputSchemaName: null,
943
- outputSchemaName: null,
944
- inputs: [],
945
- outputs: [],
946
- middleware: undefined,
947
- }
948
- return
949
- }
950
-
951
- const services: FunctionServicesMeta = {
952
- optimized: true,
953
- services: [],
954
- }
955
-
956
- const firstParam = handler.parameters[0]
957
- if (firstParam) {
958
- if (ts.isObjectBindingPattern(firstParam.name)) {
959
- for (const elem of firstParam.name.elements) {
960
- const original =
961
- elem.propertyName && ts.isIdentifier(elem.propertyName)
962
- ? elem.propertyName.text
963
- : ts.isIdentifier(elem.name)
964
- ? elem.name.text
965
- : undefined
966
- if (original) {
967
- services.services.push(original)
968
- }
969
- }
970
- } else if (
971
- ts.isIdentifier(firstParam.name) &&
972
- !firstParam.name.text.startsWith('_')
973
- ) {
974
- services.optimized = false
975
- }
976
- }
977
-
978
- const wires = extractUsedWires(handler, 2)
979
-
980
- // --- Generics → ts.Type[], unwrapped from Promise ---
981
- const genericTypes: ts.Type[] = (typeArguments ?? [])
982
- .map((tn) => checker.getTypeFromTypeNode(tn))
983
- .map((t) => unwrapPromise(checker, t))
984
-
985
- // pikkuChannelConnectionFunc<Out> declares a single generic that is the
986
- // OUTPUT type — its input is always void (PikkuFunctionSessionless<void, Out>).
987
- // Every other wrapper reads generic[0] as INPUT, so without this guard the
988
- // connect handler's output generic is mis-recorded as inputSchemaName and the
989
- // empty WS handshake fails input validation at connect (1008/403).
990
- const isChannelConnectionFunc = /ChannelConnection/i.test(expression.text)
991
-
992
- const capitalizedName = funcIdToTypeName(name)
993
-
994
- // --- Input Extraction ---
995
- let inputNames: string[] = []
996
- let inputTypes: ts.Type[] = []
997
-
998
- if (inputSchemaRef) {
999
- const schemaName = `${capitalizedName}Input`
1000
- inputNames = [schemaName]
1001
- state.schemaLookup.set(schemaName, inputSchemaRef)
1002
- state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
1003
- } else if (isListFunc && genericTypes.length >= 1 && genericTypes[0]) {
1004
- const inputAliasName = `${capitalizedName}Input`
1005
- const filterType = genericTypes[0]
1006
- const filterTypeText = checker.typeToString(
1007
- filterType,
1008
- undefined,
1009
- ts.TypeFormatFlags.NoTruncation
1010
- )
1011
- const refs = resolveTypeImports(
1012
- filterType,
1013
- state.functions.typesMap,
1014
- true,
1015
- checker
1016
- )
1017
- state.functions.typesMap.addCustomType(
1018
- inputAliasName,
1019
- `{ cursor?: string; limit?: number; sort?: Array<{ column: string; direction: 'asc' | 'desc' }>; filter?: unknown; search?: string; } & ${filterTypeText}`,
1020
- [...new Set(refs)]
1021
- )
1022
- inputNames = [inputAliasName]
1023
- const secondParam = handler.parameters[1]
1024
- if (secondParam) {
1025
- inputTypes = [checker.getTypeAtLocation(secondParam)]
1026
- } else {
1027
- inputTypes = [filterType]
1028
- }
1029
- } else if (
1030
- !isChannelConnectionFunc &&
1031
- !isListFunc &&
1032
- genericTypes.length >= 1 &&
1033
- genericTypes[0]
1034
- ) {
1035
- // Fall back to extracting from generic type arguments
1036
- const result = getNamesAndTypes(
1037
- checker,
1038
- state.functions.typesMap,
1039
- 'Input',
1040
- name,
1041
- genericTypes[0]
1042
- )
1043
- inputNames = result.names
1044
- inputTypes = result.types
1045
- } else {
1046
- // Fall back to extracting from the function's second parameter type
1047
- const secondParam = handler.parameters[1]
1048
- if (secondParam) {
1049
- const paramType = checker.getTypeAtLocation(secondParam)
1050
- const result = getNamesAndTypes(
1051
- checker,
1052
- state.functions.typesMap,
1053
- 'Input',
1054
- pikkuFuncId,
1055
- paramType
1056
- )
1057
- inputNames = result.names
1058
- inputTypes = result.types
1059
- }
1060
- }
1061
-
1062
- // --- Output Extraction ---
1063
- let outputNames: string[] = []
1064
-
1065
- if (outputSchemaRef) {
1066
- const schemaName = `${capitalizedName}Output`
1067
- outputNames = [schemaName]
1068
- state.schemaLookup.set(schemaName, outputSchemaRef)
1069
- state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
1070
- } else if (isListFunc && genericTypes.length >= 2 && genericTypes[1]) {
1071
- const outputAliasName = `${capitalizedName}Output`
1072
- const rowType = genericTypes[1]
1073
- const rowTypeText = checker.typeToString(
1074
- rowType,
1075
- undefined,
1076
- ts.TypeFormatFlags.NoTruncation
1077
- )
1078
- const refs = resolveTypeImports(
1079
- rowType,
1080
- state.functions.typesMap,
1081
- true,
1082
- checker
1083
- )
1084
- state.functions.typesMap.addCustomType(
1085
- outputAliasName,
1086
- `{ rows: Array<${rowTypeText}>; nextCursor: string | null; totalCount?: number; }`,
1087
- [...new Set(refs)]
1088
- )
1089
- outputNames = [outputAliasName]
1090
- } else if (!isListFunc && genericTypes.length >= 2) {
1091
- outputNames = getNamesAndTypes(
1092
- checker,
1093
- state.functions.typesMap,
1094
- 'Output',
1095
- name,
1096
- genericTypes[1]
1097
- ).names
1098
- } else {
1099
- const sig = checker.getSignatureFromDeclaration(handler)
1100
- if (sig) {
1101
- const rawRet = checker.getReturnTypeOfSignature(sig)
1102
- const unwrapped = unwrapPromise(checker, rawRet)
1103
- outputNames = getNamesAndTypes(
1104
- checker,
1105
- state.functions.typesMap,
1106
- 'Output',
1107
- pikkuFuncId,
1108
- unwrapped
1109
- ).names
1110
- }
1111
- }
1112
-
1113
- const mcpOutputTypes: Record<string, string> = {
1114
- pikkuMCPResourceFunc: 'MCPResourceResponse',
1115
- pikkuMCPToolFunc: 'MCPToolResponse',
1116
- pikkuMCPPromptFunc: 'MCPPromptResponse',
1117
- }
1118
- const mcpOutputType = mcpOutputTypes[expression.text]
1119
- if (mcpOutputType && outputNames[0] !== mcpOutputType) {
1120
- let resolved = false
1121
- const rawSymbol = checker.getSymbolAtLocation(expression)
1122
- const funcSymbol =
1123
- rawSymbol && rawSymbol.flags & ts.SymbolFlags.Alias
1124
- ? checker.getAliasedSymbol(rawSymbol)
1125
- : rawSymbol
1126
- const funcDecls = funcSymbol?.getDeclarations() || []
1127
- for (const funcDecl of funcDecls) {
1128
- if (resolved) break
1129
- const mcpTypeSymbol = checker.resolveName(
1130
- mcpOutputType,
1131
- funcDecl,
1132
- ts.SymbolFlags.Type,
1133
- false
1134
- )
1135
- if (mcpTypeSymbol) {
1136
- const aliased =
1137
- mcpTypeSymbol.flags & ts.SymbolFlags.Alias
1138
- ? checker.getAliasedSymbol(mcpTypeSymbol)
1139
- : mcpTypeSymbol
1140
- const typeDecl = aliased?.getDeclarations()?.[0]
1141
- if (typeDecl) {
1142
- const path = typeDecl.getSourceFile().fileName
1143
- if (!state.functions.typesMap.exists(mcpOutputType, path)) {
1144
- state.functions.typesMap.addType(mcpOutputType, path)
1145
- }
1146
- resolved = true
1147
- }
1148
- }
1149
- }
1150
- if (!resolved) {
1151
- state.functions.typesMap.addCustomType(mcpOutputType, mcpOutputType, [])
1152
- }
1153
- outputNames = [mcpOutputType]
1154
- }
1155
-
1156
- if (inputNames.length > 1) {
1157
- logger.warn(
1158
- 'More than one input type detected, only the first one will be used as a schema.'
1159
- )
1160
- }
1161
-
1162
- // Store the input type for later use
1163
- if (inputTypes.length > 0) {
1164
- state.typesLookup.set(pikkuFuncId, inputTypes)
1165
- }
1166
-
1167
- const sourceFile = node.getSourceFile().fileName
1168
- const existingFunction = state.functions.meta[pikkuFuncId]
1169
- if (
1170
- existingFunction &&
1171
- existingFunction.sourceFile &&
1172
- existingFunction.sourceFile !== sourceFile
1173
- ) {
1174
- logger.critical(
1175
- ErrorCode.DUPLICATE_FUNCTION_NAME,
1176
- `Function name '${name}' is not unique. ` +
1177
- `'${pikkuFuncId}' is already defined in '${existingFunction.sourceFile}' and cannot be redefined in '${sourceFile}'.`
1178
- )
1179
- return
1180
- }
1181
-
1182
- if (exportedName || explicitName) {
1183
- const existingInternal = state.rpc.internalMeta[name]
1184
- if (
1185
- existingInternal &&
1186
- !areCompatibleFunctionIds(existingInternal, pikkuFuncId)
1187
- ) {
1188
- const existingSource =
1189
- resolveExistingFunctionSource(state, existingInternal) || 'unknown file'
1190
- logger.critical(
1191
- ErrorCode.DUPLICATE_FUNCTION_NAME,
1192
- `Function name '${name}' is not unique. ` +
1193
- `It already points to '${existingInternal}' in '${existingSource}', but '${pikkuFuncId}' in '${sourceFile}' tried to use the same name.`
1194
- )
1195
- return
1196
- }
1197
-
1198
- if (expose) {
1199
- const existingExposed = state.rpc.exposedMeta[name]
1200
- if (
1201
- existingExposed &&
1202
- !areCompatibleFunctionIds(existingExposed, pikkuFuncId)
1203
- ) {
1204
- const existingSource =
1205
- resolveExistingFunctionSource(state, existingExposed) ||
1206
- 'unknown file'
1207
- logger.critical(
1208
- ErrorCode.DUPLICATE_FUNCTION_NAME,
1209
- `Exposed function name '${name}' is not unique. ` +
1210
- `It already points to '${existingExposed}' in '${existingSource}', but '${pikkuFuncId}' in '${sourceFile}' tried to use the same name.`
1211
- )
1212
- return
1213
- }
1214
- }
1215
- }
1216
-
1217
- const sessionless = expression.text !== 'pikkuFunc'
1218
-
1219
- // ── Classification brand check ─────────────────────────────────────────────
1220
- // Walk the function body's ACTUAL inferred return type looking for classification
1221
- // brands (__classification__ property on Private<T>, Pii<T>, Secret<T>).
1222
- //
1223
- // Semantics:
1224
- // secret → never returned by any exposed function (sessioned or not)
1225
- // private → only visible to authenticated (sessioned) users; ok for pikkuFunc
1226
- // public → safe for sessionless functions
1227
- // Opt-in only: inferring every handler's return type (getReturnTypeOfSignature)
1228
- // is the single most expensive checker operation and dominates `pikku all`
1229
- // wall-clock. The classification leak scan is a security lint, not codegen, so
1230
- // it runs ONLY when explicitly requested (`pikku all --security`) — see the
1231
- // classificationCheck option. Default codegen skips it entirely.
1232
- if (options.classificationCheck) {
1233
- // A revealed vault secret — or a PII column — is invisible to the return-type
1234
- // scan below when it is written out mid-body rather than returned, so walk
1235
- // the sinks too.
1236
- for (const violation of findClassifiedSinks(checker, handler)) {
1237
- logger.diagnostic({
1238
- severity: 'error',
1239
- code:
1240
- violation.classification === 'secret'
1241
- ? ErrorCode.SECRET_REVEALED_INTO_SINK
1242
- : ErrorCode.PII_INTO_SINK,
1243
- message:
1244
- violation.classification === 'secret'
1245
- ? `Function '${name}' passes a revealed secret to '${violation.sink}' ` +
1246
- `at line ${violation.line} (path '${violation.path}').\n ` +
1247
- `A vault secret must not be written to a log, an audit, a queue, an email or a webhook. ` +
1248
- `Pass the value only to the client that needs it, and reveal it there.`
1249
- : `Function '${name}' passes personal data to '${violation.sink}' ` +
1250
- `at line ${violation.line} (path '${violation.path}').\n ` +
1251
- `A log, the console and a webhook all leave your control — they are shipped to third parties and retained ` +
1252
- `long past the request. Log an id instead, or narrow the value to a non-identifying field. ` +
1253
- `An email to the data subject, an audit in your own database and a queue on your own infrastructure are fine.`,
1254
- })
1255
- }
1256
-
1257
- const sig = checker.getSignatureFromDeclaration(handler)
1258
- if (sig) {
1259
- const rawRet = checker.getReturnTypeOfSignature(sig)
1260
- const unwrapped = unwrapPromise(checker, rawRet)
1261
- const classifiedFields = findPiiPaths(checker, unwrapped)
1262
-
1263
- const secretPaths = classifiedFields
1264
- .filter((f) => f.classification === 'secret')
1265
- .map((f) => f.path)
1266
-
1267
- const privatePaths = classifiedFields
1268
- .filter(
1269
- (f) => f.classification === 'private' || f.classification === 'pii'
1270
- )
1271
- .map((f) => f.path)
1272
-
1273
- if (secretPaths.length > 0) {
1274
- logger.diagnostic({
1275
- severity: 'error',
1276
- code: ErrorCode.PII_IN_OUTPUT,
1277
- message:
1278
- `Function '${name}' exposes secret-classified field(s) in its return type: ` +
1279
- secretPaths.map((p) => `'${p}'`).join(', ') +
1280
- `.\n Secret fields must never appear in function output. ` +
1281
- `Strip these fields before returning or change the column classification.`,
1282
- })
1283
- }
1284
-
1285
- if (sessionless && privatePaths.length > 0) {
1286
- logger.diagnostic({
1287
- severity: 'error',
1288
- code: ErrorCode.PII_IN_OUTPUT,
1289
- message:
1290
- `Sessionless function '${name}' exposes private-classified field(s) in its return type: ` +
1291
- privatePaths.map((p) => `'${p}'`).join(', ') +
1292
- `.\n Private fields are only safe to return from authenticated (sessioned) functions. ` +
1293
- `Either require a session (use pikkuFunc) or set the column's security to 'public' in db/annotations.ts if it is safe to expose publicly.`,
1294
- })
1295
- }
1296
- }
1297
- }
1298
-
1299
- // --- resolve middleware ---
1300
- let middleware = objectNode
1301
- ? resolveMiddleware(state, objectNode, tags, checker)
1302
- : undefined
1303
-
1304
- // --- resolve permissions ---
1305
- let permissions = objectNode
1306
- ? resolvePermissions(state, objectNode, tags, checker)
1307
- : undefined
1308
-
1309
- if (options.tags?.length) {
1310
- tags = [...new Set([...(tags || []), ...options.tags])]
1311
- const tagEntries = options.tags.map((tag) => ({
1312
- type: 'tag' as const,
1313
- tag,
1314
- }))
1315
- const existingMiddlewareTags = new Set(
1316
- (middleware || [])
1317
- .filter((m) => m.type === 'tag')
1318
- .map((m) => (m as any).tag)
1319
- )
1320
- const newMiddleware = tagEntries.filter(
1321
- (e) => !existingMiddlewareTags.has(e.tag)
1322
- )
1323
- if (newMiddleware.length > 0) {
1324
- middleware = [...(middleware || []), ...newMiddleware]
1325
- }
1326
- }
1327
-
1328
- const implementationHash = computeImplementationHash({
1329
- wrapper: expression.text,
1330
- handler,
1331
- objectNode,
1332
- isDirectFunction,
1333
- })
1334
-
1335
- const handlerSourceFile = handler.getSourceFile()
1336
- const bodySourceFile =
1337
- handlerSourceFile.fileName !== sourceFile
1338
- ? handlerSourceFile.fileName
1339
- : undefined
1340
- const lineOf = (pos: number) =>
1341
- handlerSourceFile.getLineAndCharacterOfPosition(pos).line + 1
1342
- const handlerBody = handler.body
1343
- const bodySpan =
1344
- ts.isBlock(handlerBody) && handlerBody.statements.length > 0
1345
- ? {
1346
- bodyStart: lineOf(
1347
- handlerBody.statements[0].getStart(handlerSourceFile)
1348
- ),
1349
- bodyEnd: lineOf(
1350
- handlerBody.statements[handlerBody.statements.length - 1].getEnd()
1351
- ),
1352
- }
1353
- : {
1354
- bodyStart: lineOf(handlerBody.getStart(handlerSourceFile)),
1355
- bodyEnd: lineOf(handlerBody.getEnd()),
1356
- }
1357
-
1358
- state.functions.meta[pikkuFuncId] = {
1359
- pikkuFuncId,
1360
- functionType: 'user',
1361
- funcWrapper: expression.text,
1362
- sessionless,
1363
- name,
1364
- services,
1365
- wires: wires.wires.length > 0 || !wires.optimized ? wires : undefined,
1366
- inputSchemaName: inputNames[0] ?? null,
1367
- outputSchemaName: outputNames[0] ?? null,
1368
- inputs: inputNames.filter((n) => n !== 'void') ?? null,
1369
- outputs: outputNames.filter((n) => n !== 'void') ?? null,
1370
- expose: expose || undefined,
1371
- auth: auth || undefined,
1372
- permissionsInBody: permissionsInBody || undefined,
1373
- audit,
1374
- remote: remote || undefined,
1375
- scenarioStep: isScenarioStep || undefined,
1376
- scenario: isScenario || undefined,
1377
- mcp: mcpEnabled || undefined,
1378
- readonly: readonly_ || undefined,
1379
- deploy: deploy || undefined,
1380
- approvalRequired: approvalRequired || undefined,
1381
- approvalDescription: approvalDescription || undefined,
1382
- workflowQueued: workflowQueued === true ? true : undefined,
1383
- workflowRetries: workflowRetries ?? undefined,
1384
- workflowTimeout: workflowTimeout ?? undefined,
1385
- scenarioStepSurfaces,
1386
- scenarioStepRequiresActor,
1387
- // `persona` is the default reading of an unmarked step, so it is left off
1388
- // rather than written into every step meta a project already has.
1389
- scenarioStepKind:
1390
- scenarioStepKind && scenarioStepKind !== 'persona'
1391
- ? scenarioStepKind
1392
- : undefined,
1393
- scenarioStepAddon,
1394
- scenarioStepTemplate: scenarioStepTemplate || undefined,
1395
- scopes: scopes ?? undefined,
1396
- implementationHash,
1397
- version,
1398
- title,
1399
- tags: tags || undefined,
1400
- summary,
1401
- description,
1402
- errors,
1403
- middleware,
1404
- permissions,
1405
- isDirectFunction,
1406
- sourceFile,
1407
- bodySourceFile,
1408
- exportedName: exportedName || undefined,
1409
- ...bodySpan,
1410
- }
1411
-
1412
- if (handlerHasDynamicImport(handler)) {
1413
- state.functions.dynamicImportIds.add(pikkuFuncId)
1414
- }
1415
-
1416
- // Populate node metadata if node config is present
1417
- if (nodeDisplayName && nodeCategory && nodeType) {
1418
- state.nodes.files.add(node.getSourceFile().fileName)
1419
- state.nodes.meta[pikkuFuncId] = {
1420
- name: pikkuFuncId,
1421
- displayName: nodeDisplayName,
1422
- category: nodeCategory,
1423
- type: nodeType,
1424
- rpc: pikkuFuncId,
1425
- description,
1426
- errorOutput: nodeErrorOutput ?? false,
1427
- inputSchemaName: inputNames[0] ?? null,
1428
- outputSchemaName: outputNames[0] ?? null,
1429
- tags,
1430
- }
1431
- }
1432
-
1433
- if (mcpEnabled) {
1434
- // `pikkuMCPToolFunc`'s config accepts `name`, and it is the name an AI client
1435
- // calls the tool by — `runMCPTool` looks the tool up in `toolsMeta` by exactly
1436
- // this key and then dispatches through `pikkuFuncId`, so it is a public label
1437
- // rather than an identity. It was never read here, so a declared `name` was
1438
- // silently dropped and the tool published under whatever the export happened to
1439
- // be called. It does NOT become the `pikkuFuncId`: renaming an export's rpc id
1440
- // is a different decision, and the scenario-step wrappers are the only place
1441
- // `name` carries that weight.
1442
- const declaredName = objectNode
1443
- ? getPropertyValue(objectNode, 'name')
1444
- : undefined
1445
- const toolName =
1446
- typeof declaredName === 'string' && declaredName ? declaredName : name
1447
- if (!description) {
1448
- logger.warn(`MCP tool '${toolName}' is missing a description.`)
1449
- }
1450
- state.mcpEndpoints.files.add(node.getSourceFile().fileName)
1451
- state.mcpEndpoints.toolsMeta[toolName] = {
1452
- pikkuFuncId,
1453
- name: toolName,
1454
- title: title || undefined,
1455
- description: description || undefined,
1456
- summary,
1457
- errors,
1458
- tags,
1459
- inputSchema: inputNames[0] ?? null,
1460
- outputSchema: outputNames[0] ?? null,
1461
- middleware,
1462
- }
1463
- state.serviceAggregation.usedFunctions.add(pikkuFuncId)
1464
- extractWireNames(middleware).forEach((n) =>
1465
- state.serviceAggregation.usedMiddleware.add(n)
1466
- )
1467
- extractWireNames(permissions).forEach((n) =>
1468
- state.serviceAggregation.usedPermissions.add(n)
1469
- )
1470
- }
1471
-
1472
- // Workflow functions don't get registered as RPC functions,
1473
- // they are their own type handled by add-workflow
1474
- if (expression.text.includes('Workflow')) {
1475
- return
1476
- }
1477
-
1478
- // Trigger and channel connect/disconnect functions are not callable via RPC
1479
- const nonRPCPatterns = [
1480
- /Trigger/i,
1481
- /ChannelConnection/i,
1482
- /ChannelDisconnection/i,
1483
- ]
1484
- if (nonRPCPatterns.some((pattern) => pattern.test(expression.text))) {
1485
- return
1486
- }
1487
-
1488
- // Store function file location for wiring generation
1489
- if (exportedName) {
1490
- state.functions.files.set(pikkuFuncId, {
1491
- path: node.getSourceFile().fileName,
1492
- exportedName,
1493
- })
1494
- }
1495
-
1496
- if (exportedName || explicitName) {
1497
- if (!exportedName) {
1498
- logger.error(
1499
- `• Function with explicit name '${name}' is not exported, this is not allowed.`
1500
- )
1501
- return
1502
- }
1503
-
1504
- if (remote && !isScenarioStep) {
1505
- state.rpc.invokedFunctions.add(pikkuFuncId)
1506
- // The consumer-facing surface a wireRemoteAddon imports (mirrors exposedMeta)
1507
- state.rpc.remoteMeta[name] = pikkuFuncId
1508
- state.rpc.remoteFiles.set(name, {
1509
- path: node.getSourceFile().fileName,
1510
- exportedName,
1511
- })
1512
- }
1513
-
1514
- if (expose && !isScenarioStep) {
1515
- state.rpc.exposedMeta[name] = pikkuFuncId
1516
- state.rpc.exposedFiles.set(name, {
1517
- path: node.getSourceFile().fileName,
1518
- exportedName,
1519
- })
1520
- // Track exposed RPC function for service aggregation
1521
- state.serviceAggregation.usedFunctions.add(pikkuFuncId)
1522
- }
1523
-
1524
- if (!isScenarioStep) {
1525
- // We add it to internal meta to allow autocomplete for everything
1526
- state.rpc.internalMeta[name] = pikkuFuncId
1527
-
1528
- if (version !== undefined) {
1529
- state.rpc.internalMeta[pikkuFuncId] = pikkuFuncId
1530
- state.rpc.invokedFunctions.add(pikkuFuncId)
1531
- }
1532
- }
1533
-
1534
- // But we only import the actual function if it's actually invoked to keep
1535
- // bundle size down
1536
- if (
1537
- state.rpc.invokedFunctions.has(pikkuFuncId) ||
1538
- expose ||
1539
- remote ||
1540
- mcpEnabled
1541
- ) {
1542
- state.rpc.internalFiles.set(pikkuFuncId, {
1543
- path: node.getSourceFile().fileName,
1544
- exportedName,
1545
- })
1546
- }
1547
- }
1548
- }