@pikku/inspector 0.12.60 → 0.12.61

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 (136) hide show
  1. package/CHANGELOG.md +301 -0
  2. package/OPTIMIZATION-PLAN.md +19 -19
  3. package/dist/add/add-auth.js +1 -1
  4. package/dist/add/add-channel.d.ts +1 -1
  5. package/dist/add/add-cli.js +5 -0
  6. package/dist/add/add-functions.js +10 -1
  7. package/dist/add/add-http-route.js +18 -18
  8. package/dist/add/add-keyed-wiring.d.ts +2 -0
  9. package/dist/add/add-keyed-wiring.js +3 -1
  10. package/dist/add/add-personas.js +4 -1
  11. package/dist/add/add-secret.js +1 -0
  12. package/dist/add/add-variable.js +1 -0
  13. package/dist/add/add-wire-remote-addon.js +2 -1
  14. package/dist/add/add-workflow-graph.js +63 -6
  15. package/dist/add/add-workflow.d.ts +1 -1
  16. package/dist/error-codes.d.ts +3 -1
  17. package/dist/error-codes.js +3 -1
  18. package/dist/index.d.ts +3 -1
  19. package/dist/index.js +1 -0
  20. package/dist/inspector.js +12 -2
  21. package/dist/types.d.ts +25 -21
  22. package/dist/utils/annotate-http-route-auth.js +2 -1
  23. package/dist/utils/check-secret-sinks.js +2 -1
  24. package/dist/utils/compute-required-schemas.d.ts +1 -1
  25. package/dist/utils/contract-hashes.d.ts +2 -2
  26. package/dist/utils/contract-hashes.js +1 -1
  27. package/dist/utils/extract-function-name.js +1 -1
  28. package/dist/utils/extract-services.d.ts +1 -1
  29. package/dist/utils/extract-surface-usage.d.ts +29 -0
  30. package/dist/utils/extract-surface-usage.js +97 -0
  31. package/dist/utils/filter-inspector-state.js +53 -16
  32. package/dist/utils/filter-utils.d.ts +1 -1
  33. package/dist/utils/get-property-value.js +42 -3
  34. package/dist/utils/load-addon-functions-meta.js +31 -0
  35. package/dist/utils/middleware.d.ts +1 -1
  36. package/dist/utils/permissions.d.ts +1 -1
  37. package/dist/utils/post-process.d.ts +27 -7
  38. package/dist/utils/post-process.js +79 -14
  39. package/dist/utils/resolve-deploy-target.d.ts +1 -1
  40. package/dist/utils/resolve-function-meta.d.ts +1 -1
  41. package/dist/utils/resolve-function-types.d.ts +1 -1
  42. package/dist/utils/resolve-versions.js +1 -1
  43. package/dist/utils/schema-generator.d.ts +1 -1
  44. package/dist/utils/serialize-inspector-state.d.ts +2 -1
  45. package/dist/utils/serialize-inspector-state.js +2 -0
  46. package/dist/utils/serialize-openapi-json.d.ts +2 -2
  47. package/dist/utils/validate-auth-sessionless.js +2 -1
  48. package/dist/utils/workflow/derive-workflow-plan.d.ts +1 -1
  49. package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +1 -1
  50. package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +1 -1
  51. package/dist/utils/workflow/graph/finalize-workflows.js +1 -1
  52. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +6 -1
  53. package/dist/utils/workflow/graph/serialize-workflow-graph.js +26 -2
  54. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +11 -2
  55. package/package.json +10 -10
  56. package/run-tests.sh +0 -0
  57. package/src/add/add-agent.ts +2 -4
  58. package/src/add/add-auth.ts +2 -2
  59. package/src/add/add-channel.ts +1 -4
  60. package/src/add/add-cli.ts +7 -1
  61. package/src/add/add-credential.ts +3 -6
  62. package/src/add/add-file-with-factory.ts +2 -6
  63. package/src/add/add-functions.ts +24 -31
  64. package/src/add/add-gateway.ts +1 -1
  65. package/src/add/add-http-route-ref.test.ts +149 -0
  66. package/src/add/add-http-route.ts +26 -29
  67. package/src/add/add-keyed-wiring.ts +8 -7
  68. package/src/add/add-permission.ts +1 -1
  69. package/src/add/add-personas.test.ts +26 -2
  70. package/src/add/add-personas.ts +4 -3
  71. package/src/add/add-scenario-step.test.ts +37 -2
  72. package/src/add/add-scope.test.ts +15 -15
  73. package/src/add/add-scope.ts +5 -9
  74. package/src/add/add-secret.ts +1 -0
  75. package/src/add/add-system-role.test.ts +4 -4
  76. package/src/add/add-system-role.ts +2 -4
  77. package/src/add/add-variable.ts +1 -0
  78. package/src/add/add-wire-remote-addon.ts +4 -1
  79. package/src/add/add-workflow-graph-foreach.test.ts +174 -0
  80. package/src/add/add-workflow-graph-input.test.ts +1 -1
  81. package/src/add/add-workflow-graph.ts +78 -6
  82. package/src/add/add-workflow-sleep-expression.test.ts +3 -4
  83. package/src/add/add-workflow.ts +1 -4
  84. package/src/add/addon-contracts.test.ts +1 -1
  85. package/src/add/config-value-metadata.test.ts +94 -0
  86. package/src/add/secret-sink-check.test.ts +4 -1
  87. package/src/error-codes.ts +3 -1
  88. package/src/index.ts +3 -4
  89. package/src/inspector.ts +19 -2
  90. package/src/types.ts +25 -28
  91. package/src/utils/annotate-http-route-auth.test.ts +5 -7
  92. package/src/utils/annotate-http-route-auth.ts +2 -1
  93. package/src/utils/check-secret-sinks.ts +4 -1
  94. package/src/utils/compute-required-schemas.ts +1 -1
  95. package/src/utils/contract-hashes.test.ts +1 -1
  96. package/src/utils/contract-hashes.ts +3 -3
  97. package/src/utils/extract-function-name.ts +1 -1
  98. package/src/utils/extract-services.ts +1 -2
  99. package/src/utils/extract-surface-usage.test.ts +147 -0
  100. package/src/utils/extract-surface-usage.ts +121 -0
  101. package/src/utils/filter-inspector-state.test.ts +138 -12
  102. package/src/utils/filter-inspector-state.ts +82 -21
  103. package/src/utils/filter-utils.ts +1 -1
  104. package/src/utils/get-property-value.ts +43 -5
  105. package/src/utils/load-addon-functions-meta.ts +40 -0
  106. package/src/utils/middleware.ts +1 -1
  107. package/src/utils/permissions.ts +1 -1
  108. package/src/utils/post-process.test.ts +59 -2
  109. package/src/utils/post-process.ts +88 -25
  110. package/src/utils/resolve-deploy-target.ts +1 -1
  111. package/src/utils/resolve-function-meta.ts +1 -4
  112. package/src/utils/resolve-function-types.ts +1 -1
  113. package/src/utils/resolve-versions.ts +2 -5
  114. package/src/utils/schema-generator.ts +3 -3
  115. package/src/utils/serialize-inspector-state.ts +4 -1
  116. package/src/utils/serialize-mcp-json.ts +1 -1
  117. package/src/utils/serialize-openapi-json.ts +2 -2
  118. package/src/utils/validate-auth-sessionless.ts +2 -1
  119. package/src/utils/validate-tags-resolve-to-middleware.test.ts +4 -1
  120. package/src/utils/workflow/derive-workflow-plan.test.ts +1 -1
  121. package/src/utils/workflow/derive-workflow-plan.ts +1 -1
  122. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +1 -1
  123. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +2 -2
  124. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +1 -1
  125. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +1 -4
  126. package/src/utils/workflow/graph/finalize-workflow-wires.ts +1 -1
  127. package/src/utils/workflow/graph/finalize-workflows.ts +2 -2
  128. package/src/utils/workflow/graph/serialize-workflow-graph.ts +34 -3
  129. package/src/utils/workflow/graph/workflow-graph.types.ts +12 -2
  130. package/tsconfig.tsbuildinfo +1 -1
  131. package/dist/add/add-ai-agent.d.ts +0 -4
  132. package/dist/add/add-ai-agent.js +0 -442
  133. package/dist/add/add-ai-scorer.d.ts +0 -2
  134. package/dist/add/add-ai-scorer.js +0 -61
  135. package/dist/utils/secret-brokers.d.ts +0 -2
  136. package/dist/utils/secret-brokers.js +0 -9
@@ -1,4 +1,4 @@
1
- import { isRef } from '@pikku/core/ecosystem/workflow';
1
+ import { isRef, template } from '@pikku/core/workflow';
2
2
  /**
3
3
  * Convert a RefValue (from runtime) to DataRef (serialized)
4
4
  */
@@ -23,6 +23,22 @@ function serializeInputMapping(input) {
23
23
  }
24
24
  return result;
25
25
  }
26
+ /**
27
+ * Convert a `forEach` source — a bare node id, a ref, or a `(ref) => ref(...)`
28
+ * callback — to the serialized DataRef form
29
+ */
30
+ function serializeForEach(forEach, createRef) {
31
+ if (forEach === undefined)
32
+ return undefined;
33
+ if (typeof forEach === 'string')
34
+ return { $ref: forEach };
35
+ if (typeof forEach === 'function') {
36
+ return serializeForEach(forEach(createRef), createRef);
37
+ }
38
+ if (isRef(forEach))
39
+ return convertRef(forEach);
40
+ return undefined;
41
+ }
26
42
  /**
27
43
  * Convert next config from runtime format to serialized format
28
44
  * Runtime uses Record<string, string | string[]> for branching with graph.branch()
@@ -59,6 +75,7 @@ export function serializeWorkflowGraph(definition, options) {
59
75
  nodeId,
60
76
  path,
61
77
  });
78
+ const createItemRef = (path) => createRef('$item', path);
62
79
  // Track which nodes have incoming edges
63
80
  const hasIncomingEdge = new Set();
64
81
  // First pass: identify nodes with incoming edges
@@ -88,7 +105,7 @@ export function serializeWorkflowGraph(definition, options) {
88
105
  // Evaluate input callback to get the mapping
89
106
  let input = {};
90
107
  if (node.input) {
91
- const rawInput = node.input(createRef);
108
+ const rawInput = node.input(createRef, template, createItemRef);
92
109
  input = serializeInputMapping(rawInput);
93
110
  }
94
111
  // Get RPC name from func
@@ -100,6 +117,13 @@ export function serializeWorkflowGraph(definition, options) {
100
117
  next: serializeNext(node.next),
101
118
  onError: node.onError,
102
119
  };
120
+ const forEach = serializeForEach(node.forEach, createRef);
121
+ if (forEach) {
122
+ funcNode.forEach = forEach;
123
+ if (node.mode !== undefined) {
124
+ funcNode.mode = node.mode;
125
+ }
126
+ }
103
127
  if (node.notes !== undefined) {
104
128
  funcNode.notes = node.notes;
105
129
  }
@@ -67,8 +67,8 @@ export interface NodeOptions {
67
67
  * Flow node types for control flow (no RPC call)
68
68
  */
69
69
  export type FlowType = 'sleep' | 'suspend' | 'approval' | 'branch' | 'parallel' | 'fanout' | 'inline' | 'switch' | 'filter' | 'arrayPredicate' | 'return' | 'cancel' | 'set';
70
- import type { ContextVariable, WorkflowContext, WorkflowPlannedStep } from '@pikku/core/ecosystem/workflow';
71
- import type { ScenarioStepPhase } from '@pikku/core/ecosystem/scenario';
70
+ import type { ContextVariable, WorkflowContext, WorkflowPlannedStep } from '@pikku/core/workflow';
71
+ import type { ScenarioStepPhase } from '@pikku/core/scenario';
72
72
  export type { ContextVariable, WorkflowContext };
73
73
  /**
74
74
  * Base node properties shared by all node types
@@ -90,11 +90,20 @@ interface BaseNode {
90
90
  /**
91
91
  * Function node - calls an RPC
92
92
  */
93
+ /** How the per-item instances of a `forEach` node run */
94
+ export type ForEachMode = 'parallel' | 'sequential';
93
95
  export interface FunctionNode extends BaseNode {
94
96
  /** RPC function name */
95
97
  rpcName: string;
96
98
  /** Input mapping - values can be literals or DataRefs */
97
99
  input?: Record<string, unknown | DataRef>;
100
+ /**
101
+ * Run this node once per element of the referenced array. The node's own
102
+ * result becomes the ordered array of per-item results.
103
+ */
104
+ forEach?: DataRef;
105
+ /** How the per-item instances run. Defaults to 'parallel'. */
106
+ mode?: ForEachMode;
98
107
  /** Output variable name for storing result */
99
108
  outputVar?: string;
100
109
  /** Hash of nodeId + RPC input/output schemas for version detection */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.60",
3
+ "version": "0.12.61",
4
4
  "description": "TypeScript inspector for Pikku — discovers functions and wirings to drive code generation",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "license": "BUSL-1.1",
@@ -35,23 +35,23 @@
35
35
  "prepublishOnly": "yarn build"
36
36
  },
37
37
  "dependencies": {
38
- "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
38
+ "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.4",
39
39
  "openapi-types": "^12.1.3",
40
- "path-to-regexp": "^8.3.0",
41
- "ts-json-schema-generator": "2.9.1-next.16",
42
- "tsx": "^4.21.0",
40
+ "path-to-regexp": "^8.4.2",
41
+ "ts-json-schema-generator": "2.9.1-next.18",
42
+ "tsx": "^4.23.12",
43
43
  "typescript": "^6.0.3",
44
- "zod": "^4.3.6",
44
+ "zod": "^4.4.3",
45
45
  "zod-to-ts": "^2.1.0"
46
46
  },
47
47
  "peerDependencies": {
48
- "@pikku/core": "^0.12.84"
48
+ "@pikku/core": "^0.12.85"
49
49
  },
50
50
  "devDependencies": {
51
- "@pikku/core": "^0.12.84",
52
- "@types/node": "^24.11.0"
51
+ "@pikku/core": "^0.12.85",
52
+ "@types/node": "^24.13.3"
53
53
  },
54
54
  "engines": {
55
55
  "node": ">=24"
56
56
  }
57
- }
57
+ }
package/run-tests.sh CHANGED
File without changes
@@ -395,14 +395,12 @@ export const addAgent: AddWiring = (logger, node, checker, state, options) => {
395
395
 
396
396
  const roleValue = getPropertyValue(obj, 'role') as string | null
397
397
  const personalityValue = getPropertyValue(obj, 'personality') as
398
- | string
399
- | null
398
+ string | null
400
399
  const goalValue = getPropertyValue(obj, 'goal') as string | null
401
400
 
402
401
  const maxStepsValue = getPropertyValue(obj, 'maxSteps') as number | null
403
402
  const temperatureValue = getPropertyValue(obj, 'temperature') as
404
- | number
405
- | null
403
+ number | null
406
404
  const toolChoiceValue = getPropertyValue(obj, 'toolChoice') as string | null
407
405
  const scorersValue = resolveScorerNames(obj, nameValue || '', logger)
408
406
  const toolsValue = resolveToolReferences(
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript'
2
- import type { FunctionServicesMeta } from '@pikku/core/ecosystem/types'
2
+ import type { FunctionServicesMeta } from '@pikku/core/function'
3
3
  import type { AddWiring } from '../types.js'
4
4
  import { ErrorCode } from '../error-codes.js'
5
5
  import { extractServicesFromFunction } from '../utils/extract-services.js'
@@ -89,7 +89,7 @@ const readArrayProp = (
89
89
 
90
90
  /**
91
91
  * Read the callee name of a `plugins: [...]` entry. better-auth plugins are
92
- * factory calls (`bearer()`, `twoFactor({ ... })`, `admin()`); the entry's id
92
+ * factory calls (`bearer()`, `twoFactor({ ... })`, `ban()`); the entry's id
93
93
  * is the called function's name. Member-expression callees (`foo.bar()`) and
94
94
  * non-call entries are ignored.
95
95
  */
@@ -10,10 +10,7 @@ import {
10
10
  makeContextBasedId,
11
11
  } from '../utils/extract-function-name.js'
12
12
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
13
- import type {
14
- ChannelMessageMeta,
15
- ChannelMeta,
16
- } from '@pikku/core/ecosystem/channel'
13
+ import type { ChannelMessageMeta, ChannelMeta } from '@pikku/core/channel'
17
14
  import type { InspectorState, AddWiring } from '../types.js'
18
15
  import {
19
16
  resolveMiddleware,
@@ -6,7 +6,7 @@ import type {
6
6
  InspectorOptions,
7
7
  InspectorState,
8
8
  } from '../types.js'
9
- import type { CLIProgramMeta, CLICommandMeta } from '@pikku/core/ecosystem/cli'
9
+ import type { CLIProgramMeta, CLICommandMeta } from '@pikku/core/cli'
10
10
  import {
11
11
  extractFunctionName,
12
12
  makeContextBasedId,
@@ -630,6 +630,12 @@ function processOptions(
630
630
  }
631
631
  break
632
632
 
633
+ case 'type':
634
+ if (ts.isStringLiteral(optProp.initializer)) {
635
+ option.type = optProp.initializer.text
636
+ }
637
+ break
638
+
633
639
  case 'default':
634
640
  // Extract default value from expression
635
641
  if (ts.isStringLiteral(optProp.initializer)) {
@@ -39,11 +39,9 @@ export const addCredential: AddWiring = (
39
39
 
40
40
  const nameValue = getPropertyValue(obj, 'name') as string | null
41
41
  const displayNameValue = getPropertyValue(obj, 'displayName') as
42
- | string
43
- | null
42
+ string | null
44
43
  const descriptionValue = getPropertyValue(obj, 'description') as
45
- | string
46
- | null
44
+ string | null
47
45
  const typeValue = getPropertyValue(obj, 'type') as string | null
48
46
  const docsUrlValue = getPropertyValue(obj, 'docsUrl') as string | null
49
47
 
@@ -126,8 +124,7 @@ export const addCredential: AddWiring = (
126
124
  'appCredentialSecretId'
127
125
  ) as string | null
128
126
  const tokenSecretId = getPropertyValue(oauth2Obj, 'tokenSecretId') as
129
- | string
130
- | null
127
+ string | null
131
128
  const authorizationUrl = getPropertyValue(
132
129
  oauth2Obj,
133
130
  'authorizationUrl'
@@ -61,9 +61,7 @@ export const addFileWithFactory = (
61
61
  if (state && callExpression.arguments.length > 0) {
62
62
  const firstArg = callExpression.arguments[0]
63
63
  let functionNode:
64
- | ts.ArrowFunction
65
- | ts.FunctionExpression
66
- | undefined
64
+ ts.ArrowFunction | ts.FunctionExpression | undefined
67
65
 
68
66
  if (ts.isArrowFunction(firstArg)) {
69
67
  functionNode = firstArg
@@ -179,9 +177,7 @@ export const addFileWithFactory = (
179
177
  node.initializer
180
178
  ) {
181
179
  let functionNode:
182
- | ts.ArrowFunction
183
- | ts.FunctionExpression
184
- | undefined
180
+ ts.ArrowFunction | ts.FunctionExpression | undefined
185
181
  if (ts.isArrowFunction(node.initializer)) {
186
182
  functionNode = node.initializer
187
183
  } else if (ts.isFunctionExpression(node.initializer)) {
@@ -8,14 +8,10 @@ import {
8
8
  } from '../utils/extract-function-name.js'
9
9
  import { extractFunctionNode } from '../utils/extract-function-node.js'
10
10
  import { extractUsedWires } from '../utils/extract-services.js'
11
- import type { AuditDurability } from '@pikku/core/ecosystem/services'
12
- import type { FunctionServicesMeta } from '@pikku/core/ecosystem/types'
13
- import type { ScenarioSurface } from '@pikku/core/scenario'
14
- import type { ScenarioStepKind } from '@pikku/core/ecosystem/scenario'
15
- import {
16
- formatVersionedId,
17
- parseVersionedId,
18
- } from '@pikku/core/ecosystem/types'
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'
19
15
 
20
16
  /** Binding keys a `pikkuScenarioStep` may declare, in report order. */
21
17
  const SCENARIO_SURFACE_KEYS: readonly ScenarioSurface[] = [
@@ -49,7 +45,7 @@ import { ErrorCode } from '../error-codes.js'
49
45
  import { findPiiPaths } from '../utils/check-pii-output.js'
50
46
  import { findRevealedSecretSinks } from '../utils/check-secret-sinks.js'
51
47
  import { isScenarioInstrumentationFunction } from './scenario-instrumentation.js'
52
- import type { NodeType } from '@pikku/core/ecosystem/node'
48
+ import type { NodeType } from '@pikku/core/node'
53
49
 
54
50
  const isValidVariableName = (name: string) => {
55
51
  const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
@@ -597,6 +593,7 @@ export const addFunctions: AddWiring = (
597
593
  let workflowRetries: number | undefined
598
594
  let workflowTimeout: string | undefined
599
595
  let scenarioStepSurfaces: ScenarioSurface[] | undefined
596
+ let scenarioStepRequiresActor: boolean | undefined
600
597
  let scenarioStepAddon: string | undefined
601
598
  let scenarioStepTemplate: string | undefined
602
599
  let scopes: string[] | undefined
@@ -688,8 +685,7 @@ export const addFunctions: AddWiring = (
688
685
  mcp = getPropertyValue(firstArg, 'mcp') as boolean | undefined
689
686
  auth = getPropertyValue(firstArg, 'auth') as boolean | undefined
690
687
  permissionsInBody = getPropertyValue(firstArg, 'permissionsInBody') as
691
- | boolean
692
- | undefined
688
+ boolean | undefined
693
689
  if (permissionsInBody && !options.allow?.permissionsInBody) {
694
690
  logger.critical(
695
691
  ErrorCode.PERMISSIONS_IN_BODY_NOT_ALLOWED,
@@ -726,22 +722,15 @@ export const addFunctions: AddWiring = (
726
722
  }
727
723
  readonly_ = getPropertyValue(firstArg, 'readonly') as boolean | undefined
728
724
  deploy = getPropertyValue(firstArg, 'deploy') as
729
- | 'serverless'
730
- | 'server'
731
- | 'auto'
732
- | undefined
725
+ 'serverless' | 'server' | 'auto' | undefined
733
726
  approvalRequired = getPropertyValue(firstArg, 'approvalRequired') as
734
- | boolean
735
- | undefined
727
+ boolean | undefined
736
728
  workflowQueued = getPropertyValue(firstArg, 'workflowQueued') as
737
- | boolean
738
- | undefined
729
+ boolean | undefined
739
730
  workflowRetries = getPropertyValue(firstArg, 'workflowRetries') as
740
- | number
741
- | undefined
731
+ number | undefined
742
732
  workflowTimeout = getPropertyValue(firstArg, 'workflowTimeout') as
743
- | string
744
- | undefined
733
+ string | undefined
745
734
  // A step declares one implementation per surface it supports; the binding
746
735
  // keys that are present ARE the declaration, so there is nothing to read a
747
736
  // value from — only names to collect.
@@ -772,8 +761,7 @@ export const addFunctions: AddWiring = (
772
761
  scenarioStepSurfaces = ['default']
773
762
  if (scenarioStepKind === 'addon') {
774
763
  scenarioStepAddon = getPropertyValue(firstArg, 'addon') as
775
- | string
776
- | undefined
764
+ string | undefined
777
765
  if (!scenarioStepAddon) {
778
766
  logger.error(
779
767
  `• pikkuAddonScenarioStep('${name}') needs a string literal \`addon\` naming the addon whose system acts — the same name its wireAddon declares.`
@@ -785,10 +773,16 @@ export const addFunctions: AddWiring = (
785
773
  scenarioStepSurfaces = declaredSurfaces.length
786
774
  ? declaredSurfaces
787
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
788
783
  }
789
784
  scenarioStepTemplate = getPropertyValue(firstArg, 'template') as
790
- | string
791
- | undefined
785
+ string | undefined
792
786
  scopes = getArrayPropertyValue(firstArg, 'scopes') ?? undefined
793
787
 
794
788
  // Extract approvalDescription identifier reference
@@ -832,13 +826,11 @@ export const addFunctions: AddWiring = (
832
826
  ) {
833
827
  const nodeObj = prop.initializer
834
828
  nodeDisplayName = getPropertyValue(nodeObj, 'displayName') as
835
- | string
836
- | null
829
+ string | null
837
830
  nodeCategory = getPropertyValue(nodeObj, 'category') as string | null
838
831
  nodeType = getPropertyValue(nodeObj, 'type') as NodeType | null
839
832
  nodeErrorOutput = getPropertyValue(nodeObj, 'errorOutput') as
840
- | boolean
841
- | null
833
+ boolean | null
842
834
 
843
835
  if (!nodeDisplayName) {
844
836
  logger.critical(
@@ -1381,6 +1373,7 @@ export const addFunctions: AddWiring = (
1381
1373
  workflowRetries: workflowRetries ?? undefined,
1382
1374
  workflowTimeout: workflowTimeout ?? undefined,
1383
1375
  scenarioStepSurfaces,
1376
+ scenarioStepRequiresActor,
1384
1377
  // `persona` is the default reading of an unmarked step, so it is left off
1385
1378
  // rather than written into every step meta a project already has.
1386
1379
  scenarioStepKind:
@@ -13,7 +13,7 @@ import { ensureInlineWiringFunction } from '../utils/ensure-function-metadata.js
13
13
  import { resolveMiddleware } from '../utils/middleware.js'
14
14
  import { extractWireNames } from '../utils/post-process.js'
15
15
  import { resolveAddonName } from '../utils/resolve-addon-package.js'
16
- import type { GatewayTransportType } from '@pikku/core/ecosystem/gateway'
16
+ import type { GatewayTransportType } from '@pikku/core/gateway'
17
17
 
18
18
  import { ErrorCode } from '../error-codes.js'
19
19
 
@@ -0,0 +1,149 @@
1
+ import { strict as assert } from 'assert'
2
+ import { describe, test, before, after } from 'node:test'
3
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
4
+ import { tmpdir } from 'os'
5
+ import { join } from 'path'
6
+ import { inspect } from '../inspector.js'
7
+ import { filterInspectorState } from '../utils/filter-inspector-state.js'
8
+ import type { InspectorLogger, InspectorState } from '../types.js'
9
+
10
+ const ADDON_PACKAGE = '@addon/greeter'
11
+ const NAMESPACE = 'ext'
12
+ const TARGET = `${NAMESPACE}:greet`
13
+
14
+ const logger: InspectorLogger = {
15
+ debug: () => {},
16
+ info: () => {},
17
+ warn: () => {},
18
+ error: () => {},
19
+ diagnostic: () => {},
20
+ critical: () => {},
21
+ hasCriticalErrors: () => false,
22
+ } as unknown as InspectorLogger
23
+
24
+ const writeAddonFixture = (rootDir: string) => {
25
+ writeFileSync(
26
+ join(rootDir, 'package.json'),
27
+ JSON.stringify({ name: 'consumer' })
28
+ )
29
+ const addonDir = join(rootDir, 'node_modules', ADDON_PACKAGE)
30
+ const pikku = join(addonDir, '.pikku')
31
+ mkdirSync(join(pikku, 'function'), { recursive: true })
32
+ mkdirSync(join(pikku, 'schemas', 'schemas'), { recursive: true })
33
+ writeFileSync(
34
+ join(addonDir, 'package.json'),
35
+ JSON.stringify({ name: ADDON_PACKAGE })
36
+ )
37
+ writeFileSync(
38
+ join(pikku, 'function', 'pikku-functions-meta.gen.json'),
39
+ JSON.stringify({
40
+ greet: {
41
+ pikkuFuncId: 'greet',
42
+ name: 'greet',
43
+ sessionless: true,
44
+ inputSchemaName: 'GreetInput',
45
+ outputSchemaName: 'GreetOutput',
46
+ inputs: ['GreetInput'],
47
+ outputs: ['GreetOutput'],
48
+ services: { optimized: true, services: ['logger'] },
49
+ },
50
+ })
51
+ )
52
+ writeFileSync(
53
+ join(pikku, 'schemas', 'schemas', 'GreetInput.schema.json'),
54
+ JSON.stringify({
55
+ type: 'object',
56
+ properties: {
57
+ id: { type: 'string' },
58
+ loud: { type: 'boolean' },
59
+ name: { type: 'string' },
60
+ },
61
+ })
62
+ )
63
+ }
64
+
65
+ const writeWiring = (rootDir: string): string => {
66
+ const file = join(rootDir, 'greet.wiring.ts')
67
+ writeFileSync(
68
+ file,
69
+ [
70
+ `import { ref, wireAddon } from '@pikku/core'`,
71
+ `import { wireHTTP } from '@pikku/core/http'`,
72
+ ``,
73
+ `wireAddon({ name: '${NAMESPACE}', package: '${ADDON_PACKAGE}' })`,
74
+ ``,
75
+ `wireHTTP({`,
76
+ ` auth: false,`,
77
+ ` method: 'get',`,
78
+ ` route: '/greet/:id',`,
79
+ ` func: ref('${TARGET}'),`,
80
+ `})`,
81
+ ``,
82
+ `wireHTTP({`,
83
+ ` auth: false,`,
84
+ ` method: 'post',`,
85
+ ` route: '/greet',`,
86
+ ` query: ['loud'],`,
87
+ ` func: ref('${TARGET}'),`,
88
+ `})`,
89
+ ].join('\n')
90
+ )
91
+ return file
92
+ }
93
+
94
+ describe('an HTTP route wired with ref() to an addon function', () => {
95
+ let rootDir: string
96
+ let state: InspectorState
97
+
98
+ before(async () => {
99
+ rootDir = mkdtempSync(join(tmpdir(), 'pikku-http-ref-'))
100
+ writeAddonFixture(rootDir)
101
+ const file = writeWiring(rootDir)
102
+ state = await inspect(logger, [file], { rootDir })
103
+ })
104
+
105
+ after(() => {
106
+ rmSync(rootDir, { recursive: true, force: true })
107
+ })
108
+
109
+ test('records the ref target as the route function id', () => {
110
+ assert.equal(state.http.meta.get['/greet/:id']?.pikkuFuncId, TARGET)
111
+ assert.equal(state.http.meta.post['/greet']?.pikkuFuncId, TARGET)
112
+ })
113
+
114
+ // packageName names the package a wire runs in — its middleware, its
115
+ // services, its scoped credentials. The route belongs to the consuming app
116
+ // however its function is reached, so tagging it with the addon's package
117
+ // would silently swap the app's global middleware for the addon's.
118
+ test("leaves the route in the consuming app's package", () => {
119
+ assert.equal(state.http.meta.get['/greet/:id']?.packageName, undefined)
120
+ assert.equal(state.http.meta.post['/greet']?.packageName, undefined)
121
+ })
122
+
123
+ test('mints no wrapper function metadata for the route', () => {
124
+ const wrapperIds = Object.keys(state.functions.meta).filter((id) =>
125
+ id.startsWith('http:')
126
+ )
127
+ assert.deepEqual(wrapperIds, [])
128
+ assert.equal(state.functions.meta[TARGET], undefined)
129
+ })
130
+
131
+ test('keeps each route its own params and query', () => {
132
+ assert.deepEqual(state.http.meta.get['/greet/:id']?.params, ['id'])
133
+ assert.equal(state.http.meta.get['/greet/:id']?.query, undefined)
134
+ assert.equal(state.http.meta.post['/greet']?.params, undefined)
135
+ assert.deepEqual(state.http.meta.post['/greet']?.query, ['loud'])
136
+ })
137
+
138
+ test('marks the addon target as used so tree-shaking keeps it', () => {
139
+ assert.ok(state.serviceAggregation.usedFunctions.has(TARGET))
140
+ })
141
+
142
+ test('survives a filtered bundle together with its addon declaration', () => {
143
+ const filtered = filterInspectorState(state, { names: [TARGET] }, logger)
144
+ assert.equal(filtered.http.meta.get['/greet/:id']?.pikkuFuncId, TARGET)
145
+ assert.equal(filtered.http.meta.post['/greet']?.pikkuFuncId, TARGET)
146
+ assert.ok(filtered.rpc.wireAddonDeclarations.has(NAMESPACE))
147
+ assert.ok(filtered.serviceAggregation.usedFunctions.has(TARGET))
148
+ })
149
+ })
@@ -4,7 +4,7 @@ import {
4
4
  getCommonWireMetaData,
5
5
  } from '../utils/get-property-value.js'
6
6
  import { pathToRegexp } from 'path-to-regexp'
7
- import type { HTTPMethod } from '@pikku/core/ecosystem/http'
7
+ import type { HTTPMethod } from '@pikku/core/http'
8
8
  import {
9
9
  extractFunctionName,
10
10
  makeContextBasedId,
@@ -243,6 +243,17 @@ export function registerHTTPRoute({
243
243
  )
244
244
  : null
245
245
 
246
+ // A ref()-wired route points straight at the addon function, the same way the
247
+ // CLI and channel adders record one. The addon's own metadata is the contract
248
+ // — and it is authoritative where the checker is not: `ref('ns:fn')` only
249
+ // types precisely once the consumer's own rpc map already lists the addon's
250
+ // functions, so a cold run inferred the whole `FlattenedRPCMap` and a warm one
251
+ // inferred the real input. The addon's metadata says which it is either way.
252
+ //
253
+ // The route stays the consuming app's all the same, so it records no
254
+ // packageName: that names the package a wire *runs in*, and running this one
255
+ // inside the addon would hand it the addon's middleware and services instead
256
+ // of the app's. `ref` reaches the addon over RPC, which enters it properly.
246
257
  if (refAddonTarget) {
247
258
  const targetMeta = resolveFunctionMeta(state, refAddonTarget)
248
259
  if (!targetMeta) {
@@ -251,25 +262,16 @@ export function registerHTTPRoute({
251
262
  )
252
263
  return
253
264
  }
254
- }
255
-
256
- ensureFunctionMetadata(
257
- state,
258
- funcName,
259
- fullRoute,
260
- funcInitializer,
261
- checker,
262
- extracted.isHelper
263
- )
264
-
265
- // Propagate sessionless from the addon target so the runtime can correctly
266
- // determine whether a session is required for ref()-based routes.
267
- if (refAddonTarget) {
268
- const targetMeta = resolveFunctionMeta(state, refAddonTarget)
269
- const inlineMeta = state.functions.meta[funcName]
270
- if (targetMeta && inlineMeta && targetMeta.sessionless !== undefined) {
271
- inlineMeta.sessionless = targetMeta.sessionless
272
- }
265
+ funcName = refAddonTarget
266
+ } else {
267
+ ensureFunctionMetadata(
268
+ state,
269
+ funcName,
270
+ fullRoute,
271
+ funcInitializer,
272
+ checker,
273
+ extracted.isHelper
274
+ )
273
275
  }
274
276
 
275
277
  // The route's own `auth`, falling back to the group's. Recorded on the meta
@@ -322,13 +324,12 @@ export function registerHTTPRoute({
322
324
  const input = fnMeta.inputs?.[0] || null
323
325
 
324
326
  const getRouteInputKeys = (): string[] | null => {
325
- const targetFuncName = refAddonTarget ?? funcName
326
- const inputTypes = state.typesLookup.get(targetFuncName)
327
+ const inputTypes = state.typesLookup.get(funcName)
327
328
  if (inputTypes && inputTypes.length > 0) {
328
329
  return extractTypeKeys(inputTypes[0])
329
330
  }
330
331
 
331
- const targetMeta = resolveFunctionMeta(state, targetFuncName)
332
+ const targetMeta = resolveFunctionMeta(state, funcName)
332
333
  if (targetMeta?.inputSchemaName) {
333
334
  const schema = state.schemas[targetMeta.inputSchemaName] as any
334
335
  const properties = schema?.properties
@@ -354,7 +355,7 @@ export function registerHTTPRoute({
354
355
  logger.critical(
355
356
  ErrorCode.ROUTE_PARAM_MISMATCH,
356
357
  `Route '${fullRoute}' has path parameter(s) [${missingParams.join(', ')}] ` +
357
- `not found in function '${refAddonTarget ?? funcName}' input type. ` +
358
+ `not found in function '${funcName}' input type. ` +
358
359
  `Input type has: [${inputKeys.join(', ')}]`
359
360
  )
360
361
  return
@@ -368,7 +369,7 @@ export function registerHTTPRoute({
368
369
  logger.critical(
369
370
  ErrorCode.ROUTE_QUERY_MISMATCH,
370
371
  `Route '${fullRoute}' has query parameter(s) [${missingQuery.join(', ')}] ` +
371
- `not found in function '${refAddonTarget ?? funcName}' input type. ` +
372
+ `not found in function '${funcName}' input type. ` +
372
373
  `Input type has: [${inputKeys.join(', ')}]`
373
374
  )
374
375
  return
@@ -393,9 +394,6 @@ export function registerHTTPRoute({
393
394
  const permissions = resolvePermissions(state, obj, tags, checker)
394
395
 
395
396
  state.serviceAggregation.usedFunctions.add(funcName)
396
- if (refAddonTarget) {
397
- state.serviceAggregation.usedFunctions.add(refAddonTarget)
398
- }
399
397
  extractWireNames(middleware).forEach((name) =>
400
398
  state.serviceAggregation.usedMiddleware.add(name)
401
399
  )
@@ -420,7 +418,6 @@ export function registerHTTPRoute({
420
418
  state.http.files.add(sourceFile.fileName)
421
419
  state.http.meta[method][fullRoute] = {
422
420
  pikkuFuncId: funcName,
423
- ...(refAddonTarget && { refTarget: refAddonTarget }),
424
421
  ...(packageName && { packageName }),
425
422
  route: fullRoute,
426
423
  sourceFile: sourceFile.fileName,