@pikku/inspector 0.12.14 → 0.12.16

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 (41) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/add/add-ai-agent.js +1 -1
  3. package/dist/add/add-channel.js +25 -7
  4. package/dist/add/add-functions.js +28 -13
  5. package/dist/add/add-gateway.js +1 -1
  6. package/dist/add/add-http-route.js +23 -1
  7. package/dist/add/add-mcp-prompt.js +1 -1
  8. package/dist/add/add-mcp-resource.js +1 -1
  9. package/dist/add/add-queue-worker.js +1 -1
  10. package/dist/add/add-schedule.js +1 -1
  11. package/dist/add/add-trigger.js +1 -1
  12. package/dist/add/add-workflow.js +1 -1
  13. package/dist/utils/check-pii-output.d.ts +9 -4
  14. package/dist/utils/check-pii-output.js +17 -7
  15. package/dist/utils/ensure-function-metadata.js +1 -1
  16. package/dist/utils/extract-node-value.d.ts +1 -1
  17. package/dist/utils/extract-node-value.js +10 -1
  18. package/dist/utils/get-property-value.d.ts +1 -1
  19. package/dist/utils/get-property-value.js +35 -9
  20. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +20 -9
  21. package/package.json +1 -1
  22. package/src/add/add-ai-agent.ts +1 -1
  23. package/src/add/add-channel.ts +37 -7
  24. package/src/add/add-functions.ts +44 -13
  25. package/src/add/add-gateway.ts +1 -1
  26. package/src/add/add-http-route.ts +26 -1
  27. package/src/add/add-mcp-prompt.ts +1 -1
  28. package/src/add/add-mcp-resource.ts +1 -1
  29. package/src/add/add-queue-worker.ts +1 -1
  30. package/src/add/add-schedule.ts +1 -1
  31. package/src/add/add-trigger.ts +1 -1
  32. package/src/add/add-workflow.test.ts +152 -0
  33. package/src/add/add-workflow.ts +2 -1
  34. package/src/add/pii-check.test.ts +70 -28
  35. package/src/utils/check-pii-output.ts +27 -11
  36. package/src/utils/ensure-function-metadata.ts +3 -1
  37. package/src/utils/extract-node-value.test.ts +12 -10
  38. package/src/utils/extract-node-value.ts +15 -1
  39. package/src/utils/get-property-value.ts +33 -13
  40. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +22 -9
  41. package/tsconfig.tsbuildinfo +1 -1
@@ -1,16 +1,22 @@
1
1
  import * as ts from 'typescript'
2
2
 
3
+ export type ClassifiedField = {
4
+ path: string
5
+ classification: 'private' | 'pii' | 'secret' | string
6
+ }
7
+
3
8
  /**
4
9
  * Recursively walks a resolved TypeScript type looking for `__classification__` brands —
5
- * the structural marker emitted by `Private<T>` and `Secret<T>`.
10
+ * the structural marker emitted by `Private<T>`, `Pii<T>`, and `Secret<T>`.
6
11
  *
7
12
  * `Private<T> = T & { readonly __classification__: 'private' }` shows up in the TS type
8
13
  * system as an intersection whose constituents include a type with a `__classification__`
9
14
  * property. We detect that by checking whether any constituent of an
10
15
  * intersection exposes a property named `__classification__`.
11
16
  *
12
- * Returns the list of dotted field paths where a brand was found
13
- * (e.g. `['email', 'address.phone']`). An empty array means clean.
17
+ * Returns the list of classified fields found, each with its dotted path and
18
+ * classification level (e.g. `[{ path: 'email', classification: 'private' }]`).
19
+ * An empty array means clean.
14
20
  */
15
21
  export function findPiiPaths(
16
22
  checker: ts.TypeChecker,
@@ -18,23 +24,33 @@ export function findPiiPaths(
18
24
  path = '',
19
25
  depth = 0,
20
26
  seen = new Set<ts.Type>()
21
- ): string[] {
27
+ ): ClassifiedField[] {
22
28
  if (depth > 8 || seen.has(type)) return []
23
29
  seen.add(type)
24
30
 
25
31
  // ── Is this type itself branded? ─────────────────────────────────────────
26
32
  // Private<T> = T & { readonly __classification__: 'private' } → isIntersection()
27
- // where one constituent has a `__classification__` property.
33
+ // where one constituent has a `__classification__` property whose type is a string literal.
28
34
  if (type.isIntersection()) {
29
- const branded = type.types.some((t) =>
30
- t.getProperties().some((p) => p.name === '__classification__')
31
- )
32
- if (branded) {
33
- return [path || '<return value>']
35
+ for (const t of type.types) {
36
+ const classificationProp = t
37
+ .getProperties()
38
+ .find((p) => p.name === '__classification__')
39
+ if (classificationProp) {
40
+ const decl =
41
+ classificationProp.valueDeclaration ??
42
+ classificationProp.declarations?.[0]
43
+ const classification = decl
44
+ ? ((
45
+ checker.getTypeOfSymbolAtLocation(classificationProp, decl) as any
46
+ )?.value ?? 'private')
47
+ : 'private'
48
+ return [{ path: path || '<return value>', classification }]
49
+ }
34
50
  }
35
51
  }
36
52
 
37
- const violations: string[] = []
53
+ const violations: ClassifiedField[] = []
38
54
 
39
55
  // ── Union: check every branch ─────────────────────────────────────────────
40
56
  if (type.isUnion()) {
@@ -280,7 +280,9 @@ export function ensureFunctionMetadata(
280
280
  const { tags } = getCommonWireMetaData(
281
281
  firstArg,
282
282
  'Function',
283
- fallbackName || pikkuFuncId
283
+ fallbackName || pikkuFuncId,
284
+ undefined,
285
+ checker
284
286
  )
285
287
  if (tags) {
286
288
  meta.tags = tags
@@ -46,18 +46,20 @@ describe('extractDescription', () => {
46
46
  assert.equal(extractDescription(obj, checker), 'my step')
47
47
  })
48
48
 
49
- test('returns null for non-literal description value without crashing', () => {
49
+ test('extracts concatenated string literals in description', () => {
50
50
  const { checker, sourceFile } = createChecker(
51
- `const name = 'test'; const data = { description: name + ' addon' }`
51
+ `const data = { description: 'line one ' + 'line two' }`
52
52
  )
53
- const objs: ts.ObjectLiteralExpression[] = []
54
- const visit = (node: ts.Node) => {
55
- if (ts.isObjectLiteralExpression(node)) objs.push(node)
56
- ts.forEachChild(node, visit)
57
- }
58
- ts.forEachChild(sourceFile, visit)
59
- const dataObj = objs[objs.length - 1]!
60
- assert.equal(extractDescription(dataObj, checker), null)
53
+ const obj = findObjectLiteral(sourceFile)!
54
+ assert.equal(extractDescription(obj, checker), 'line one line two')
55
+ })
56
+
57
+ test('extracts deeply nested concatenation in description', () => {
58
+ const { checker, sourceFile } = createChecker(
59
+ `const data = { description: 'a' + 'b' + 'c' }`
60
+ )
61
+ const obj = findObjectLiteral(sourceFile)!
62
+ assert.equal(extractDescription(obj, checker), 'abc')
61
63
  })
62
64
 
63
65
  test('returns null for non-object node', () => {
@@ -32,6 +32,16 @@ export function extractStringLiteral(
32
32
  return extractStringLiteral(node.expression, checker)
33
33
  }
34
34
 
35
+ if (
36
+ ts.isBinaryExpression(node) &&
37
+ node.operatorToken.kind === ts.SyntaxKind.PlusToken
38
+ ) {
39
+ return (
40
+ extractStringLiteral(node.left, checker) +
41
+ extractStringLiteral(node.right, checker)
42
+ )
43
+ }
44
+
35
45
  // Try to evaluate constant identifiers
36
46
  if (ts.isIdentifier(node)) {
37
47
  const symbol = checker.getSymbolAtLocation(node)
@@ -52,7 +62,7 @@ export function extractStringLiteral(
52
62
  /**
53
63
  * Check if node is string-like (string literal or template expression)
54
64
  */
55
- export function isStringLike(node: ts.Node, _checker: ts.TypeChecker): boolean {
65
+ export function isStringLike(node: ts.Node, checker: ts.TypeChecker): boolean {
56
66
  if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
57
67
  return true
58
68
  }
@@ -60,6 +70,10 @@ export function isStringLike(node: ts.Node, _checker: ts.TypeChecker): boolean {
60
70
  if (ts.isTemplateExpression(node)) {
61
71
  return true
62
72
  }
73
+ // Unwrap type assertions: `expr as Type` or `<Type>expr`
74
+ if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
75
+ return isStringLike(node.expression, checker)
76
+ }
63
77
  return false
64
78
  }
65
79
 
@@ -1,5 +1,6 @@
1
1
  import * as ts from 'typescript'
2
2
  import { ErrorCode } from '../error-codes.js'
3
+ import { extractStringLiteral } from './extract-node-value.js'
3
4
 
4
5
  /**
5
6
  * Extracts an array of strings from an object property.
@@ -137,7 +138,8 @@ export const getCommonWireMetaData = (
137
138
  obj: ts.ObjectLiteralExpression,
138
139
  wiringType: string,
139
140
  wiringName: string | null,
140
- logger?: { critical: (code: ErrorCode, message: string) => void }
141
+ logger?: { critical: (code: ErrorCode, message: string) => void },
142
+ checker?: ts.TypeChecker
141
143
  ): {
142
144
  disabled?: true
143
145
  title?: string
@@ -166,18 +168,36 @@ export const getCommonWireMetaData = (
166
168
  prop.initializer.kind === ts.SyntaxKind.TrueKeyword
167
169
  ) {
168
170
  metadata.disabled = true
169
- } else if (propName === 'title' && ts.isStringLiteral(prop.initializer)) {
170
- metadata.title = prop.initializer.text
171
- } else if (
172
- propName === 'summary' &&
173
- ts.isStringLiteral(prop.initializer)
174
- ) {
175
- metadata.summary = prop.initializer.text
176
- } else if (
177
- propName === 'description' &&
178
- ts.isStringLiteral(prop.initializer)
179
- ) {
180
- metadata.description = prop.initializer.text
171
+ } else if (propName === 'title') {
172
+ try {
173
+ metadata.title = checker
174
+ ? extractStringLiteral(prop.initializer, checker)
175
+ : ts.isStringLiteral(prop.initializer)
176
+ ? prop.initializer.text
177
+ : undefined
178
+ } catch {
179
+ // non-static title — skip
180
+ }
181
+ } else if (propName === 'summary') {
182
+ try {
183
+ metadata.summary = checker
184
+ ? extractStringLiteral(prop.initializer, checker)
185
+ : ts.isStringLiteral(prop.initializer)
186
+ ? prop.initializer.text
187
+ : undefined
188
+ } catch {
189
+ // non-static summary — skip
190
+ }
191
+ } else if (propName === 'description') {
192
+ try {
193
+ metadata.description = checker
194
+ ? extractStringLiteral(prop.initializer, checker)
195
+ : ts.isStringLiteral(prop.initializer)
196
+ ? prop.initializer.text
197
+ : undefined
198
+ } catch {
199
+ // non-static description — skip
200
+ }
181
201
  } else if (propName === 'tags') {
182
202
  if (ts.isArrayLiteralExpression(prop.initializer)) {
183
203
  metadata.tags = prop.initializer.elements
@@ -436,21 +436,34 @@ function extractExpressionStatement(
436
436
  outputVar = expr.left.text
437
437
 
438
438
  // Check if this is an assignment to a context variable (set step)
439
+ // But if the RHS is a workflow.do() call, fall through to RPC extraction —
440
+ // reassigning a pre-declared variable with a workflow step is valid and common.
439
441
  if (context.contextVars.has(outputVar)) {
440
- const literalValue = extractLiteralValue(expr.right)
441
- if (literalValue !== undefined) {
442
+ const rhs = expr.right
443
+ const rhsCall =
444
+ ts.isAwaitExpression(rhs) && ts.isCallExpression(rhs.expression)
445
+ ? rhs.expression
446
+ : null
447
+ const isWorkflowCall = rhsCall
448
+ ? isWorkflowDoCall(rhsCall, context.checker)
449
+ : false
450
+
451
+ if (!isWorkflowCall) {
452
+ const literalValue = extractLiteralValue(expr.right)
453
+ if (literalValue !== undefined) {
454
+ return {
455
+ type: 'set',
456
+ variable: outputVar,
457
+ value: literalValue,
458
+ } as SetStepMeta
459
+ }
460
+ // Non-literal assignment to context var - use expression as string
442
461
  return {
443
462
  type: 'set',
444
463
  variable: outputVar,
445
- value: literalValue,
464
+ value: getSourceText(expr.right),
446
465
  } as SetStepMeta
447
466
  }
448
- // Non-literal assignment to context var - use expression as string
449
- return {
450
- type: 'set',
451
- variable: outputVar,
452
- value: getSourceText(expr.right),
453
- } as SetStepMeta
454
467
  }
455
468
  }
456
469
  // Use right side as the expression to extract from