@pikku/inspector 0.12.11 → 0.12.13

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 (65) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/add/add-cli.js +10 -3
  3. package/dist/add/add-credential.js +2 -1
  4. package/dist/add/add-functions.js +48 -1
  5. package/dist/add/add-http-route.js +24 -5
  6. package/dist/add/add-keyed-wiring.js +3 -1
  7. package/dist/add/add-middleware.js +33 -4
  8. package/dist/add/add-permission.js +7 -7
  9. package/dist/add/add-workflow-graph.js +20 -1
  10. package/dist/error-codes.d.ts +3 -1
  11. package/dist/error-codes.js +3 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +1 -0
  14. package/dist/inspector.js +2 -5
  15. package/dist/types.d.ts +10 -19
  16. package/dist/utils/check-pii-output.d.ts +14 -0
  17. package/dist/utils/check-pii-output.js +63 -0
  18. package/dist/utils/extract-function-name.js +6 -0
  19. package/dist/utils/filter-inspector-state.js +187 -59
  20. package/dist/utils/filter-utils.js +13 -5
  21. package/dist/utils/get-property-value.d.ts +10 -0
  22. package/dist/utils/get-property-value.js +30 -0
  23. package/dist/utils/post-process.d.ts +2 -3
  24. package/dist/utils/post-process.js +3 -23
  25. package/dist/utils/resolve-addon-package.d.ts +4 -5
  26. package/dist/utils/resolve-addon-package.js +64 -16
  27. package/dist/utils/resolve-deploy-target.d.ts +28 -0
  28. package/dist/utils/resolve-deploy-target.js +56 -0
  29. package/dist/utils/resolve-versions.js +79 -0
  30. package/dist/utils/schema-generator.js +31 -12
  31. package/package.json +2 -2
  32. package/src/add/add-cli.ts +10 -3
  33. package/src/add/add-credential.ts +3 -0
  34. package/src/add/add-functions.test.ts +149 -0
  35. package/src/add/add-functions.ts +61 -1
  36. package/src/add/add-gateway.ts +5 -1
  37. package/src/add/add-http-route.ts +26 -6
  38. package/src/add/add-keyed-wiring.ts +7 -1
  39. package/src/add/add-mcp-prompt.ts +5 -1
  40. package/src/add/add-mcp-resource.ts +5 -1
  41. package/src/add/add-middleware.ts +42 -4
  42. package/src/add/add-permission.ts +7 -7
  43. package/src/add/add-schedule.ts +5 -1
  44. package/src/add/add-workflow-graph.ts +19 -1
  45. package/src/add/pii-check.test.ts +197 -0
  46. package/src/add/wire-name-literal.test.ts +114 -0
  47. package/src/error-codes.ts +4 -0
  48. package/src/index.ts +1 -0
  49. package/src/inspector.ts +1 -5
  50. package/src/types.ts +19 -15
  51. package/src/utils/check-pii-output.ts +76 -0
  52. package/src/utils/extract-function-name.ts +8 -0
  53. package/src/utils/filter-inspector-state.test.ts +168 -64
  54. package/src/utils/filter-inspector-state.ts +290 -64
  55. package/src/utils/filter-utils.test.ts +30 -15
  56. package/src/utils/filter-utils.ts +14 -5
  57. package/src/utils/get-property-value.ts +40 -0
  58. package/src/utils/post-process.ts +3 -38
  59. package/src/utils/resolve-addon-package.ts +65 -14
  60. package/src/utils/resolve-deploy-target.test.ts +105 -0
  61. package/src/utils/resolve-deploy-target.ts +63 -0
  62. package/src/utils/resolve-versions.test.ts +108 -0
  63. package/src/utils/resolve-versions.ts +86 -0
  64. package/src/utils/schema-generator.ts +37 -13
  65. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,114 @@
1
+ import { strict as assert } from 'assert'
2
+ import { describe, test } from 'node:test'
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { inspect } from '../inspector.js'
7
+ import { ErrorCode } from '../error-codes.js'
8
+ import type { InspectorLogger } from '../types.js'
9
+
10
+ const makeLogger = (criticals: Array<{ code: ErrorCode; message: string }>) =>
11
+ ({
12
+ debug: () => {},
13
+ info: () => {},
14
+ warn: () => {},
15
+ error: () => {},
16
+ critical: (code: ErrorCode, message: string) => {
17
+ criticals.push({ code, message })
18
+ },
19
+ hasCriticalErrors: () => criticals.length > 0,
20
+ }) satisfies InspectorLogger
21
+
22
+ describe('wiring name must be a string literal', () => {
23
+ test('logs a critical error when a queue worker name is a const reference', async () => {
24
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-nonliteral-name-'))
25
+ const file = join(rootDir, 'queue.ts')
26
+
27
+ await writeFile(
28
+ file,
29
+ [
30
+ "import { pikkuSessionlessFunc, wireQueueWorker } from '@pikku/core'",
31
+ 'const QUEUE_NAME = "stripe-webhook-event"',
32
+ 'export const handler = pikkuSessionlessFunc({',
33
+ ' func: async () => ({ ok: true })',
34
+ '})',
35
+ 'wireQueueWorker({',
36
+ ' name: QUEUE_NAME,',
37
+ ' func: handler,',
38
+ '})',
39
+ ].join('\n')
40
+ )
41
+
42
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
43
+ try {
44
+ await inspect(makeLogger(criticals), [file], { rootDir })
45
+ const hit = criticals.find(
46
+ (entry) => entry.code === ErrorCode.NON_LITERAL_WIRE_NAME
47
+ )
48
+ assert.ok(hit, 'expected NON_LITERAL_WIRE_NAME critical')
49
+ assert.match(hit!.message, /QUEUE_NAME/)
50
+ } finally {
51
+ await rm(rootDir, { recursive: true, force: true })
52
+ }
53
+ })
54
+
55
+ test('logs a critical error when a secret id is a const reference', async () => {
56
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-nonliteral-secret-'))
57
+ const file = join(rootDir, 'secret.ts')
58
+
59
+ await writeFile(
60
+ file,
61
+ [
62
+ "import { wireSecret } from '@pikku/core'",
63
+ 'const SECRET_ID = "STRIPE_SECRET_KEY"',
64
+ 'wireSecret({',
65
+ ' secretId: SECRET_ID,',
66
+ " name: 'Stripe secret key',",
67
+ " displayName: 'Stripe secret key',",
68
+ '})',
69
+ ].join('\n')
70
+ )
71
+
72
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
73
+ try {
74
+ await inspect(makeLogger(criticals), [file], { rootDir })
75
+ const hit = criticals.find(
76
+ (entry) => entry.code === ErrorCode.NON_LITERAL_WIRE_NAME
77
+ )
78
+ assert.ok(hit, 'expected NON_LITERAL_WIRE_NAME critical')
79
+ assert.match(hit!.message, /SECRET_ID/)
80
+ } finally {
81
+ await rm(rootDir, { recursive: true, force: true })
82
+ }
83
+ })
84
+
85
+ test('does not flag a queue worker whose name is an inline literal', async () => {
86
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-literal-name-'))
87
+ const file = join(rootDir, 'queue.ts')
88
+
89
+ await writeFile(
90
+ file,
91
+ [
92
+ "import { pikkuSessionlessFunc, wireQueueWorker } from '@pikku/core'",
93
+ 'export const handler = pikkuSessionlessFunc({',
94
+ ' func: async () => ({ ok: true })',
95
+ '})',
96
+ 'wireQueueWorker({',
97
+ " name: 'stripe-webhook-event',",
98
+ ' func: handler,',
99
+ '})',
100
+ ].join('\n')
101
+ )
102
+
103
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
104
+ try {
105
+ await inspect(makeLogger(criticals), [file], { rootDir })
106
+ const hit = criticals.find(
107
+ (entry) => entry.code === ErrorCode.NON_LITERAL_WIRE_NAME
108
+ )
109
+ assert.equal(hit, undefined)
110
+ } finally {
111
+ await rm(rootDir, { recursive: true, force: true })
112
+ }
113
+ })
114
+ })
@@ -10,6 +10,7 @@
10
10
  export enum ErrorCode {
11
11
  // Validation errors
12
12
  MISSING_NAME = 'PKU111',
13
+ NON_LITERAL_WIRE_NAME = 'PKU118',
13
14
  MISSING_DESCRIPTION = 'PKU123',
14
15
  INVALID_VALUE = 'PKU124',
15
16
  MISSING_URI = 'PKU220',
@@ -76,4 +77,7 @@ export enum ErrorCode {
76
77
 
77
78
  // Feature Flag
78
79
  WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED = 'PKU901',
80
+
81
+ // Data classification errors
82
+ PII_IN_OUTPUT = 'PKU910',
79
83
  }
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export {
9
9
  } from './utils/serialize-inspector-state.js'
10
10
  export type { SerializableInspectorState } from './utils/serialize-inspector-state.js'
11
11
  export { filterInspectorState } from './utils/filter-inspector-state.js'
12
+ export { resolveDeployTarget } from './utils/resolve-deploy-target.js'
12
13
  export {
13
14
  generateCustomTypes,
14
15
  sanitizeTypeName,
package/src/inspector.ts CHANGED
@@ -12,7 +12,6 @@ import { findCommonAncestor } from './utils/find-root-dir.js'
12
12
  import {
13
13
  aggregateRequiredServices,
14
14
  validateAgentModels,
15
- validateAgentOverrides,
16
15
  validateSecretOverrides,
17
16
  validateVariableOverrides,
18
17
  validateCredentialOverrides,
@@ -250,8 +249,6 @@ export const inspect = async (
250
249
  const rootDir = options.rootDir || findCommonAncestor(routeFiles)
251
250
 
252
251
  const startSourceFiles = performance.now()
253
- // Filter source files to only include files within the project rootDir
254
- // This prevents picking up types from external packages (including workspace symlinks)
255
252
  const sourceFiles = program
256
253
  .getSourceFiles()
257
254
  .filter((sf) => sf.fileName.startsWith(rootDir))
@@ -354,8 +351,7 @@ export const inspect = async (
354
351
  )
355
352
  }
356
353
 
357
- validateAgentModels(logger, state, options.modelConfig)
358
- validateAgentOverrides(logger, state, options.modelConfig)
354
+ validateAgentModels(logger, state)
359
355
  validateSecretOverrides(logger, state)
360
356
  validateVariableOverrides(logger, state)
361
357
  validateCredentialOverrides(logger, state)
package/src/types.ts CHANGED
@@ -179,10 +179,28 @@ export interface InspectorPermissionState {
179
179
  export type InspectorFilters = {
180
180
  names?: string[] // Wildcard support: "email-*" matches "email-worker", "email-sender"
181
181
  tags?: string[]
182
- types?: string[]
182
+ wires?: string[]
183
183
  directories?: string[]
184
184
  httpRoutes?: string[] // HTTP route patterns: "/api/*", "/webhooks/*"
185
185
  httpMethods?: string[] // HTTP methods: "GET", "POST", "DELETE", etc.
186
+
187
+ excludeNames?: string[]
188
+ excludeTags?: string[]
189
+ excludeWires?: string[]
190
+ excludeDirectories?: string[]
191
+ excludeHttpRoutes?: string[]
192
+ excludeHttpMethods?: string[]
193
+
194
+ // Keep only functions whose effective deploy target is in this list.
195
+ // A function's effective target is its explicit `deploy` field, or
196
+ // 'server' if any of its services are listed in `serverlessIncompatible`,
197
+ // otherwise 'serverless'.
198
+ target?: Array<'serverless' | 'server'>
199
+ excludeTarget?: Array<'serverless' | 'server'>
200
+ // Service names that, when consumed by a function, force its target
201
+ // to 'server'. Sourced from `pikku.config.json` →
202
+ // `deploy.serverlessIncompatible`. Used only when deploy filters are set.
203
+ serverlessIncompatible?: string[]
186
204
  }
187
205
 
188
206
  export type AddonConfig = {
@@ -192,19 +210,6 @@ export type AddonConfig = {
192
210
  forceInclude?: boolean
193
211
  }
194
212
 
195
- export type ModelConfigEntry =
196
- | string
197
- | { model: string; temperature?: number; maxSteps?: number }
198
-
199
- export type InspectorModelConfig = {
200
- models?: Record<string, ModelConfigEntry>
201
- agentDefaults?: { temperature?: number; maxSteps?: number }
202
- agentOverrides?: Record<
203
- string,
204
- { model?: string; temperature?: number; maxSteps?: number }
205
- >
206
- }
207
-
208
213
  export type InspectorOptions = Partial<{
209
214
  setupOnly: boolean
210
215
  rootDir: string
@@ -225,7 +230,6 @@ export type InspectorOptions = Partial<{
225
230
  }
226
231
  tags: string[]
227
232
  manifest: VersionManifest
228
- modelConfig: InspectorModelConfig
229
233
  oldProgram: ts.Program | undefined
230
234
  }>
231
235
 
@@ -0,0 +1,76 @@
1
+ import * as ts from 'typescript'
2
+
3
+ /**
4
+ * Recursively walks a resolved TypeScript type looking for `__pii__` brands —
5
+ * the structural marker emitted by `Private<T>` and `Secret<T>`.
6
+ *
7
+ * `Private<T> = T & { readonly __pii__: 'private' }` shows up in the TS type
8
+ * system as an intersection whose constituents include a type with a `__pii__`
9
+ * property. We detect that by checking whether any constituent of an
10
+ * intersection exposes a property named `__pii__`.
11
+ *
12
+ * Returns the list of dotted field paths where a brand was found
13
+ * (e.g. `['email', 'address.phone']`). An empty array means clean.
14
+ */
15
+ export function findPiiPaths(
16
+ checker: ts.TypeChecker,
17
+ type: ts.Type,
18
+ path = '',
19
+ depth = 0,
20
+ seen = new Set<ts.Type>()
21
+ ): string[] {
22
+ if (depth > 8 || seen.has(type)) return []
23
+ seen.add(type)
24
+
25
+ // ── Is this type itself branded? ─────────────────────────────────────────
26
+ // Private<T> = T & { readonly __pii__: 'private' } → isIntersection()
27
+ // where one constituent has a `__pii__` property.
28
+ if (type.isIntersection()) {
29
+ const branded = type.types.some((t) =>
30
+ t.getProperties().some((p) => p.name === '__pii__')
31
+ )
32
+ if (branded) {
33
+ return [path || '<return value>']
34
+ }
35
+ }
36
+
37
+ const violations: string[] = []
38
+
39
+ // ── Union: check every branch ─────────────────────────────────────────────
40
+ if (type.isUnion()) {
41
+ for (const branch of type.types) {
42
+ violations.push(...findPiiPaths(checker, branch, path, depth, seen))
43
+ }
44
+ return violations
45
+ }
46
+
47
+ // ── Object: recurse into named properties ─────────────────────────────────
48
+ if (type.flags & ts.TypeFlags.Object) {
49
+ const ref = type as ts.TypeReference
50
+ for (const arg of (ref as any).typeArguments ?? []) {
51
+ violations.push(...findPiiPaths(checker, arg, path, depth + 1, seen))
52
+ }
53
+
54
+ const numberIndex = checker.getIndexTypeOfType(type, ts.IndexKind.Number)
55
+ if (numberIndex) {
56
+ const idxPath = path ? `${path}[]` : '[]'
57
+ violations.push(...findPiiPaths(checker, numberIndex, idxPath, depth + 1, seen))
58
+ }
59
+ const stringIndex = checker.getIndexTypeOfType(type, ts.IndexKind.String)
60
+ if (stringIndex) {
61
+ const idxPath = path ? `${path}[*]` : '[*]'
62
+ violations.push(...findPiiPaths(checker, stringIndex, idxPath, depth + 1, seen))
63
+ }
64
+
65
+ for (const prop of type.getProperties()) {
66
+ if (prop.name.startsWith('__')) continue
67
+ const decl = prop.valueDeclaration ?? prop.declarations?.[0]
68
+ if (!decl) continue
69
+ const propType = checker.getTypeOfSymbolAtLocation(prop, decl)
70
+ const subPath = path ? `${path}.${prop.name}` : prop.name
71
+ violations.push(...findPiiPaths(checker, propType, subPath, depth + 1, seen))
72
+ }
73
+ }
74
+
75
+ return violations
76
+ }
@@ -444,6 +444,14 @@ export function extractFunctionName(
444
444
  }
445
445
 
446
446
  if (result.version !== null) {
447
+ // Strip trailing VN suffix if it matches the version (e.g. createCardV1 + version:1 → createCard@v1)
448
+ const vSuffix = `V${result.version}`
449
+ if (
450
+ result.pikkuFuncId.endsWith(vSuffix) &&
451
+ result.pikkuFuncId.length > vSuffix.length
452
+ ) {
453
+ result.pikkuFuncId = result.pikkuFuncId.slice(0, -vSuffix.length)
454
+ }
447
455
  result.pikkuFuncId = formatVersionedId(result.pikkuFuncId, result.version)
448
456
  }
449
457