@pikku/inspector 0.12.52 → 0.12.55

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 (40) hide show
  1. package/CHANGELOG.md +341 -0
  2. package/dist/add/add-functions.js +46 -4
  3. package/dist/add/add-keyed-wiring.js +3 -1
  4. package/dist/add/add-personas.js +7 -1
  5. package/dist/add/add-workflow.js +12 -1
  6. package/dist/error-codes.d.ts +8 -1
  7. package/dist/error-codes.js +12 -0
  8. package/dist/types.d.ts +9 -0
  9. package/dist/utils/check-secret-sinks.d.ts +31 -0
  10. package/dist/utils/check-secret-sinks.js +117 -0
  11. package/dist/utils/post-process.js +4 -1
  12. package/dist/utils/single-declaration.d.ts +32 -0
  13. package/dist/utils/single-declaration.js +43 -0
  14. package/dist/utils/validate-exposed-functions-gated.js +15 -7
  15. package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +6 -0
  16. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +48 -1
  17. package/dist/utils/workflow/dsl/patterns.d.ts +9 -4
  18. package/dist/utils/workflow/dsl/patterns.js +26 -4
  19. package/package.json +3 -3
  20. package/src/add/add-functions.ts +57 -5
  21. package/src/add/add-keyed-wiring.ts +3 -0
  22. package/src/add/add-personas.test.ts +115 -1
  23. package/src/add/add-personas.ts +18 -9
  24. package/src/add/add-scenario-step.test.ts +38 -2
  25. package/src/add/add-scope.test.ts +73 -0
  26. package/src/add/add-system-role.test.ts +83 -0
  27. package/src/add/add-workflow.ts +21 -1
  28. package/src/add/allow-escape-hatches.test.ts +144 -0
  29. package/src/add/audit-meta.test.ts +75 -0
  30. package/src/add/secret-sink-check.test.ts +211 -0
  31. package/src/error-codes.ts +13 -0
  32. package/src/types.ts +9 -0
  33. package/src/utils/check-secret-sinks.ts +145 -0
  34. package/src/utils/post-process.ts +4 -1
  35. package/src/utils/single-declaration.ts +57 -0
  36. package/src/utils/validate-exposed-functions-gated.test.ts +2 -2
  37. package/src/utils/validate-exposed-functions-gated.ts +16 -7
  38. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +57 -0
  39. package/src/utils/workflow/dsl/patterns.ts +33 -4
  40. package/tsconfig.tsbuildinfo +1 -1
@@ -26,6 +26,7 @@ import {
26
26
  isWorkflowDoCall,
27
27
  isWorkflowExpectEventuallyCall,
28
28
  isScenarioStepCall,
29
+ isScenarioExpectationCall,
29
30
  getScenarioStepPhase,
30
31
  DYNAMIC_SCENARIO_STEP_TARGET,
31
32
  extractActorFromOptions,
@@ -82,9 +83,64 @@ export interface ExtractionResult {
82
83
  steps?: WorkflowStepMeta[]
83
84
  /** Workflow context (top-level variables) */
84
85
  context?: WorkflowContext
86
+ /**
87
+ * The flow asserts something through an expectation helper rather than a
88
+ * `then` step. Recorded separately because those helpers are inline steps and
89
+ * leave no phase behind, so PKU680 has no other way to see them.
90
+ */
91
+ asserts?: boolean
85
92
  reason?: string
86
93
  }
87
94
 
95
+ /**
96
+ * A function the walk must not step into, because nothing here says it runs.
97
+ *
98
+ * A callback handed straight to a call — a fanout's `map`, an inline
99
+ * `workflow.do` step — executes as part of the body and counts. A function that
100
+ * is merely *declared* or assigned to a name does not: an assertion parked in an
101
+ * unused helper asserts nothing, and counting it would let a scenario silence
102
+ * PKU680 with dead code.
103
+ */
104
+ function isUncalledFunction(node: ts.Node): boolean {
105
+ if (
106
+ !ts.isFunctionDeclaration(node) &&
107
+ !ts.isFunctionExpression(node) &&
108
+ !ts.isArrowFunction(node)
109
+ ) {
110
+ return false
111
+ }
112
+ if (ts.isFunctionDeclaration(node)) return true
113
+ const parent = node.parent
114
+ return !(
115
+ parent &&
116
+ ts.isCallExpression(parent) &&
117
+ parent.arguments.some((argument) => argument === node)
118
+ )
119
+ }
120
+
121
+ /**
122
+ * Whether the body calls one of the expectation helpers anywhere it runs.
123
+ *
124
+ * A whole-body walk rather than a check inside step extraction: the helpers
125
+ * produce no step of their own, so there is no extraction result to hang the
126
+ * answer off, and one nested inside a branch asserts just as much as one at the
127
+ * top level.
128
+ */
129
+ function hasScenarioExpectation(body: ts.Node): boolean {
130
+ let found = false
131
+ const visit = (node: ts.Node): void => {
132
+ if (found) return
133
+ if (ts.isCallExpression(node) && isScenarioExpectationCall(node)) {
134
+ found = true
135
+ return
136
+ }
137
+ if (node !== body && isUncalledFunction(node)) return
138
+ ts.forEachChild(node, visit)
139
+ }
140
+ visit(body)
141
+ return found
142
+ }
143
+
88
144
  /**
89
145
  * Extract DSL workflow metadata from a function declaration
90
146
  */
@@ -170,6 +226,7 @@ export function extractDSLWorkflow(
170
226
  steps,
171
227
  context:
172
228
  Object.keys(workflowContext).length > 0 ? workflowContext : undefined,
229
+ asserts: hasScenarioExpectation(arrowFunc.body) || undefined,
173
230
  }
174
231
  } catch (error) {
175
232
  return {
@@ -44,14 +44,14 @@ export function isWorkflowDoCall(
44
44
  */
45
45
  export const DYNAMIC_SCENARIO_STEP_TARGET = '<dynamic>'
46
46
 
47
- const SCENARIO_STEP_PHASES = ['step', 'given', 'when', 'then'] as const
47
+ const SCENARIO_STEP_PHASES = ['given', 'when', 'then'] as const
48
48
 
49
49
  export type ScenarioStepPhaseName = (typeof SCENARIO_STEP_PHASES)[number]
50
50
 
51
51
  /**
52
- * Check if a call expression is scenario.step()/given()/when()/then() — a
53
- * declared `pikkuScenarioStep` invoked by name. `given`/`when`/`then` are pure
54
- * sugar over `step`: the phase only changes the prose a reporter renders.
52
+ * Check if a call expression is scenario.given()/when()/then() — a declared
53
+ * `pikkuScenarioStep` invoked by name. `given`/`when` are pure sugar for each
54
+ * other; `then` additionally makes the step's bindings witnesses.
55
55
  */
56
56
  export function isScenarioStepCall(node: ts.CallExpression): boolean {
57
57
  return getScenarioStepPhase(node) !== undefined
@@ -74,6 +74,35 @@ export function getScenarioStepPhase(
74
74
  return SCENARIO_STEP_PHASES.find((phase) => phase === propAccess.name.text)
75
75
  }
76
76
 
77
+ /**
78
+ * The expectation helpers that assert without being a `then`.
79
+ *
80
+ * `expectService` and `expectError` are inline steps rather than scenario
81
+ * steps, so they carry no phase and never reach the step meta — but each one is
82
+ * a witness: a recorded service call, or a refusal. PKU680 has to see them, or
83
+ * a scenario whose only assertion is one of these reads as asserting nothing.
84
+ */
85
+ const SCENARIO_EXPECTATION_CALLS = [
86
+ 'expectService',
87
+ 'expectError',
88
+ 'expectEventually',
89
+ ] as const
90
+
91
+ /**
92
+ * Check if a call expression is one of the scenario expectation helpers —
93
+ * `scenario.expectService()`, `.expectError()` or `.expectEventually()`.
94
+ */
95
+ export function isScenarioExpectationCall(node: ts.CallExpression): boolean {
96
+ return (
97
+ ts.isPropertyAccessExpression(node.expression) &&
98
+ SCENARIO_EXPECTATION_CALLS.some(
99
+ (name) =>
100
+ name === (node.expression as ts.PropertyAccessExpression).name.text
101
+ ) &&
102
+ isWorkflowWireIdentifier(node.expression.expression)
103
+ )
104
+ }
105
+
77
106
  /**
78
107
  * Check if a call expression is workflow.expectEventually()
79
108
  */