@pikku/inspector 0.12.75 → 0.12.82

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 (223) hide show
  1. package/CHANGELOG.md +357 -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-middleware.js +66 -42
  8. package/dist/add/add-wire-addon.js +7 -4
  9. package/dist/add/add-wire-remote-addon.js +1 -0
  10. package/dist/error-codes.d.ts +1 -0
  11. package/dist/error-codes.js +1 -0
  12. package/dist/index.d.ts +3 -1
  13. package/dist/index.js +3 -1
  14. package/dist/inspector.js +5 -0
  15. package/dist/surface/collect-surface.d.ts +58 -0
  16. package/dist/surface/collect-surface.js +613 -0
  17. package/dist/types.d.ts +36 -12
  18. package/dist/utils/filter-inspector-state.js +14 -0
  19. package/dist/utils/load-addon-functions-meta.js +24 -11
  20. package/dist/utils/post-process.js +15 -0
  21. package/dist/utils/read-module-specifiers.d.ts +8 -0
  22. package/dist/utils/read-module-specifiers.js +23 -0
  23. package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
  24. package/dist/utils/read-tsconfig-out-dir.js +12 -0
  25. package/dist/utils/resolve-deploy-target.d.ts +7 -0
  26. package/dist/utils/resolve-deploy-target.js +15 -7
  27. package/dist/utils/serialize-inspector-state.d.ts +6 -0
  28. package/dist/utils/serialize-inspector-state.js +10 -0
  29. package/dist/visit.js +4 -0
  30. package/package.json +16 -6
  31. package/run-tests.sh +21 -6
  32. package/src/add/add-addon-bans.ts +0 -85
  33. package/src/add/add-agent-scorer.test.ts +0 -197
  34. package/src/add/add-agent-scorer.ts +0 -83
  35. package/src/add/add-agent.test.ts +0 -87
  36. package/src/add/add-agent.ts +0 -638
  37. package/src/add/add-approval-description.ts +0 -76
  38. package/src/add/add-auth.test.ts +0 -791
  39. package/src/add/add-auth.ts +0 -424
  40. package/src/add/add-channel.ts +0 -778
  41. package/src/add/add-cli-renderers.test.ts +0 -74
  42. package/src/add/add-cli.ts +0 -1006
  43. package/src/add/add-credential.ts +0 -186
  44. package/src/add/add-feature.test.ts +0 -306
  45. package/src/add/add-feature.ts +0 -253
  46. package/src/add/add-file-extends-core-type.ts +0 -66
  47. package/src/add/add-file-with-config.ts +0 -45
  48. package/src/add/add-file-with-factory.test.ts +0 -134
  49. package/src/add/add-file-with-factory.ts +0 -288
  50. package/src/add/add-functions.test.ts +0 -743
  51. package/src/add/add-functions.ts +0 -1548
  52. package/src/add/add-gateway.ts +0 -143
  53. package/src/add/add-http-route-ref.test.ts +0 -149
  54. package/src/add/add-http-route.ts +0 -530
  55. package/src/add/add-http-routes.ts +0 -471
  56. package/src/add/add-keyed-wiring.ts +0 -180
  57. package/src/add/add-mcp-prompt.ts +0 -157
  58. package/src/add/add-mcp-resource.ts +0 -175
  59. package/src/add/add-middleware.test.ts +0 -196
  60. package/src/add/add-middleware.ts +0 -879
  61. package/src/add/add-permission-auth.test.ts +0 -59
  62. package/src/add/add-permission.ts +0 -311
  63. package/src/add/add-personas.test.ts +0 -406
  64. package/src/add/add-personas.ts +0 -287
  65. package/src/add/add-queue-worker.ts +0 -117
  66. package/src/add/add-rpc-invocations.test.ts +0 -287
  67. package/src/add/add-rpc-invocations.ts +0 -221
  68. package/src/add/add-scenario-step.test.ts +0 -509
  69. package/src/add/add-schedule.ts +0 -120
  70. package/src/add/add-scope.test.ts +0 -419
  71. package/src/add/add-scope.ts +0 -221
  72. package/src/add/add-secret.ts +0 -10
  73. package/src/add/add-subject-scenario-steps.test.ts +0 -161
  74. package/src/add/add-system-role.test.ts +0 -297
  75. package/src/add/add-system-role.ts +0 -162
  76. package/src/add/add-trigger.ts +0 -165
  77. package/src/add/add-variable.ts +0 -10
  78. package/src/add/add-wire-addon.test.ts +0 -185
  79. package/src/add/add-wire-addon.ts +0 -146
  80. package/src/add/add-wire-remote-addon.ts +0 -80
  81. package/src/add/add-workflow-edge-cases.test.ts +0 -201
  82. package/src/add/add-workflow-fanout.test.ts +0 -166
  83. package/src/add/add-workflow-graph-foreach.test.ts +0 -174
  84. package/src/add/add-workflow-graph-input.test.ts +0 -94
  85. package/src/add/add-workflow-graph-notes.test.ts +0 -74
  86. package/src/add/add-workflow-graph.ts +0 -644
  87. package/src/add/add-workflow-on-error.test.ts +0 -118
  88. package/src/add/add-workflow-return-switch.test.ts +0 -121
  89. package/src/add/add-workflow-silent-drops.test.ts +0 -261
  90. package/src/add/add-workflow-sleep-expression.test.ts +0 -96
  91. package/src/add/add-workflow.test.ts +0 -155
  92. package/src/add/add-workflow.ts +0 -482
  93. package/src/add/addon-bans.test.ts +0 -121
  94. package/src/add/addon-contracts.test.ts +0 -221
  95. package/src/add/allow-escape-hatches.test.ts +0 -144
  96. package/src/add/audit-meta.test.ts +0 -75
  97. package/src/add/classified-sink-check.test.ts +0 -347
  98. package/src/add/config-value-metadata.test.ts +0 -263
  99. package/src/add/dynamic-import-check.test.ts +0 -119
  100. package/src/add/expect-eventually-scenario-only.test.ts +0 -108
  101. package/src/add/function-body-span.test.ts +0 -96
  102. package/src/add/inline-wiring-function.test.ts +0 -104
  103. package/src/add/pii-check.test.ts +0 -258
  104. package/src/add/rpc-type-cast.test.ts +0 -123
  105. package/src/add/scenario-instrumentation.test.ts +0 -89
  106. package/src/add/scenario-instrumentation.ts +0 -42
  107. package/src/add/scenario-marker.test.ts +0 -89
  108. package/src/add/scenario-not-extractable.test.ts +0 -159
  109. package/src/add/scenario-services.test.ts +0 -103
  110. package/src/add/scenario-skip.test.ts +0 -83
  111. package/src/add/validate-workflow-graph-addons.test.ts +0 -102
  112. package/src/add/wire-name-literal.test.ts +0 -117
  113. package/src/error-codes-emitted.test.ts +0 -76
  114. package/src/error-codes.ts +0 -145
  115. package/src/index.ts +0 -66
  116. package/src/inspector.ts +0 -575
  117. package/src/schema-generator.ts +0 -1
  118. package/src/source-file-cache.ts +0 -73
  119. package/src/tsconfig-paths.test.ts +0 -90
  120. package/src/types-map.ts +0 -141
  121. package/src/types.ts +0 -775
  122. package/src/utils/annotate-http-route-auth.test.ts +0 -161
  123. package/src/utils/annotate-http-route-auth.ts +0 -53
  124. package/src/utils/check-classified-sinks.ts +0 -215
  125. package/src/utils/check-pii-output.ts +0 -120
  126. package/src/utils/compute-diagnostics.test.ts +0 -106
  127. package/src/utils/compute-required-schemas.ts +0 -48
  128. package/src/utils/contract-hashes.test.ts +0 -553
  129. package/src/utils/contract-hashes.ts +0 -387
  130. package/src/utils/custom-types-generator.test.ts +0 -124
  131. package/src/utils/custom-types-generator.ts +0 -150
  132. package/src/utils/detect-schema-vendor.test.ts +0 -123
  133. package/src/utils/detect-schema-vendor.ts +0 -120
  134. package/src/utils/does-type-extend-core-type.ts +0 -53
  135. package/src/utils/ensure-function-metadata.ts +0 -385
  136. package/src/utils/extract-function-name.ts +0 -612
  137. package/src/utils/extract-function-node.ts +0 -70
  138. package/src/utils/extract-node-value.test.ts +0 -117
  139. package/src/utils/extract-node-value.ts +0 -172
  140. package/src/utils/extract-secret-usage.test.ts +0 -81
  141. package/src/utils/extract-secret-usage.ts +0 -73
  142. package/src/utils/extract-services.ts +0 -70
  143. package/src/utils/extract-surface-usage.test.ts +0 -147
  144. package/src/utils/extract-surface-usage.ts +0 -121
  145. package/src/utils/filter-inspector-state.test.ts +0 -2232
  146. package/src/utils/filter-inspector-state.ts +0 -1298
  147. package/src/utils/filter-utils.test.ts +0 -843
  148. package/src/utils/filter-utils.ts +0 -161
  149. package/src/utils/find-root-dir.ts +0 -68
  150. package/src/utils/find-zod-transform.test.ts +0 -70
  151. package/src/utils/find-zod-transform.ts +0 -103
  152. package/src/utils/get-exported-variable-name.ts +0 -48
  153. package/src/utils/get-files-and-methods.ts +0 -157
  154. package/src/utils/get-property-value.test.ts +0 -141
  155. package/src/utils/get-property-value.ts +0 -331
  156. package/src/utils/hash.ts +0 -26
  157. package/src/utils/literal-properties.ts +0 -97
  158. package/src/utils/load-addon-functions-meta.test.ts +0 -157
  159. package/src/utils/load-addon-functions-meta.ts +0 -508
  160. package/src/utils/load-addon-scopes.test.ts +0 -138
  161. package/src/utils/middleware.test.ts +0 -204
  162. package/src/utils/middleware.ts +0 -303
  163. package/src/utils/nested-project-filter.test.ts +0 -53
  164. package/src/utils/nested-project-filter.ts +0 -28
  165. package/src/utils/permissions-shorthand.test.ts +0 -79
  166. package/src/utils/permissions.test.ts +0 -125
  167. package/src/utils/permissions.ts +0 -129
  168. package/src/utils/post-process.test.ts +0 -1094
  169. package/src/utils/post-process.ts +0 -1604
  170. package/src/utils/resolve-addon-package.ts +0 -106
  171. package/src/utils/resolve-core-type.test.ts +0 -142
  172. package/src/utils/resolve-core-type.ts +0 -35
  173. package/src/utils/resolve-deploy-target.test.ts +0 -135
  174. package/src/utils/resolve-deploy-target.ts +0 -65
  175. package/src/utils/resolve-function-meta.ts +0 -25
  176. package/src/utils/resolve-function-types.ts +0 -42
  177. package/src/utils/resolve-identifier.ts +0 -46
  178. package/src/utils/resolve-ref-contract.ts +0 -71
  179. package/src/utils/resolve-versions.test.ts +0 -499
  180. package/src/utils/resolve-versions.ts +0 -228
  181. package/src/utils/schema-cache.test.ts +0 -124
  182. package/src/utils/schema-generator.test.ts +0 -272
  183. package/src/utils/schema-generator.ts +0 -780
  184. package/src/utils/secret-alias-services.test.ts +0 -146
  185. package/src/utils/secret-alias-services.ts +0 -58
  186. package/src/utils/serialize-inspector-state.ts +0 -742
  187. package/src/utils/serialize-mcp-json.ts +0 -150
  188. package/src/utils/serialize-middleware-groups-meta.ts +0 -36
  189. package/src/utils/serialize-openapi-json.ts +0 -277
  190. package/src/utils/serialize-permissions-groups-meta.ts +0 -7
  191. package/src/utils/single-declaration.ts +0 -57
  192. package/src/utils/type-utils.ts +0 -145
  193. package/src/utils/validate-auth-sessionless.ts +0 -30
  194. package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
  195. package/src/utils/validate-exposed-functions-gated.ts +0 -103
  196. package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
  197. package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
  198. package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
  199. package/src/utils/workflow/derive-workflow-plan.ts +0 -117
  200. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
  201. package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
  202. package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
  203. package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
  204. package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
  205. package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
  206. package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
  207. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
  208. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
  209. package/src/utils/workflow/dsl/index.ts +0 -11
  210. package/src/utils/workflow/dsl/patterns.ts +0 -440
  211. package/src/utils/workflow/dsl/validation.ts +0 -203
  212. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
  213. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
  214. package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
  215. package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
  216. package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
  217. package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
  218. package/src/utils/workflow/graph/index.ts +0 -28
  219. package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
  220. package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
  221. package/src/visit.ts +0 -181
  222. package/tsconfig.json +0 -19
  223. 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
- }