@pikku/inspector 0.12.50 → 0.12.52

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 (99) hide show
  1. package/CHANGELOG.md +441 -0
  2. package/OPTIMIZATION-PLAN.md +9 -3
  3. package/dist/add/add-auth.d.ts +1 -1
  4. package/dist/add/add-auth.js +1 -1
  5. package/dist/add/add-cli.js +7 -0
  6. package/dist/add/add-credential.js +1 -1
  7. package/dist/add/add-functions.js +156 -7
  8. package/dist/add/add-http-route.js +16 -6
  9. package/dist/add/add-middleware.js +46 -3
  10. package/dist/add/add-personas.d.ts +13 -0
  11. package/dist/add/add-personas.js +210 -0
  12. package/dist/add/add-scope.js +1 -1
  13. package/dist/add/add-secret.js +1 -1
  14. package/dist/add/add-system-role.d.ts +2 -0
  15. package/dist/add/add-system-role.js +102 -0
  16. package/dist/add/add-variable.js +1 -1
  17. package/dist/add/add-wire-addon.js +31 -0
  18. package/dist/error-codes.d.ts +10 -1
  19. package/dist/error-codes.js +11 -0
  20. package/dist/index.d.ts +1 -1
  21. package/dist/inspector.js +33 -1
  22. package/dist/types.d.ts +46 -2
  23. package/dist/utils/annotate-http-route-auth.d.ts +22 -0
  24. package/dist/utils/annotate-http-route-auth.js +48 -0
  25. package/dist/utils/extract-function-name.js +13 -4
  26. package/dist/utils/extract-function-node.js +18 -1
  27. package/dist/utils/extract-secret-usage.d.ts +9 -0
  28. package/dist/utils/extract-secret-usage.js +61 -0
  29. package/dist/utils/literal-properties.d.ts +22 -0
  30. package/dist/utils/literal-properties.js +69 -0
  31. package/dist/utils/load-addon-functions-meta.js +26 -1
  32. package/dist/utils/permissions.js +8 -0
  33. package/dist/utils/post-process.d.ts +68 -3
  34. package/dist/utils/post-process.js +243 -13
  35. package/dist/utils/secret-alias-services.d.ts +8 -0
  36. package/dist/utils/secret-alias-services.js +45 -0
  37. package/dist/utils/secret-brokers.d.ts +2 -0
  38. package/dist/utils/secret-brokers.js +9 -0
  39. package/dist/utils/serialize-inspector-state.d.ts +10 -0
  40. package/dist/utils/serialize-inspector-state.js +18 -0
  41. package/dist/utils/serialize-middleware-groups-meta.js +3 -0
  42. package/dist/utils/validate-exposed-functions-gated.d.ts +14 -0
  43. package/dist/utils/validate-exposed-functions-gated.js +85 -0
  44. package/dist/utils/validate-tags-resolve-to-middleware.d.ts +20 -0
  45. package/dist/utils/validate-tags-resolve-to-middleware.js +41 -0
  46. package/dist/visit.js +4 -0
  47. package/package.json +3 -3
  48. package/src/add/add-auth.test.ts +5 -3
  49. package/src/add/add-auth.ts +1 -1
  50. package/src/add/add-cli.ts +8 -0
  51. package/src/add/add-credential.ts +1 -1
  52. package/src/add/add-functions.test.ts +263 -1
  53. package/src/add/add-functions.ts +219 -9
  54. package/src/add/add-http-route.ts +20 -7
  55. package/src/add/add-middleware.test.ts +196 -0
  56. package/src/add/add-middleware.ts +64 -4
  57. package/src/add/add-personas.test.ts +268 -0
  58. package/src/add/add-personas.ts +276 -0
  59. package/src/add/add-scenario-step.test.ts +49 -3
  60. package/src/add/add-scope.test.ts +30 -30
  61. package/src/add/add-scope.ts +1 -1
  62. package/src/add/add-secret.ts +1 -1
  63. package/src/add/add-subject-scenario-steps.test.ts +161 -0
  64. package/src/add/add-system-role.test.ts +214 -0
  65. package/src/add/add-system-role.ts +164 -0
  66. package/src/add/add-variable.ts +1 -1
  67. package/src/add/add-wire-addon.test.ts +111 -0
  68. package/src/add/add-wire-addon.ts +29 -0
  69. package/src/add/addon-bans.test.ts +4 -4
  70. package/src/add/config-value-metadata.test.ts +11 -11
  71. package/src/add/wire-name-literal.test.ts +2 -2
  72. package/src/error-codes.ts +13 -0
  73. package/src/index.ts +4 -1
  74. package/src/inspector.ts +39 -0
  75. package/src/types.ts +47 -2
  76. package/src/utils/annotate-http-route-auth.test.ts +163 -0
  77. package/src/utils/annotate-http-route-auth.ts +52 -0
  78. package/src/utils/extract-function-name.ts +14 -4
  79. package/src/utils/extract-function-node.ts +18 -6
  80. package/src/utils/extract-secret-usage.test.ts +81 -0
  81. package/src/utils/extract-secret-usage.ts +73 -0
  82. package/src/utils/literal-properties.ts +97 -0
  83. package/src/utils/load-addon-functions-meta.ts +32 -2
  84. package/src/utils/permissions-shorthand.test.ts +79 -0
  85. package/src/utils/permissions.ts +7 -0
  86. package/src/utils/post-process.test.ts +133 -0
  87. package/src/utils/post-process.ts +310 -12
  88. package/src/utils/secret-alias-services.test.ts +146 -0
  89. package/src/utils/secret-alias-services.ts +58 -0
  90. package/src/utils/secret-brokers.test.ts +53 -0
  91. package/src/utils/secret-brokers.ts +11 -0
  92. package/src/utils/serialize-inspector-state.ts +28 -0
  93. package/src/utils/serialize-middleware-groups-meta.ts +3 -0
  94. package/src/utils/validate-exposed-functions-gated.test.ts +230 -0
  95. package/src/utils/validate-exposed-functions-gated.ts +94 -0
  96. package/src/utils/validate-tags-resolve-to-middleware.test.ts +105 -0
  97. package/src/utils/validate-tags-resolve-to-middleware.ts +48 -0
  98. package/src/visit.ts +4 -0
  99. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,164 @@
1
+ import * as ts from 'typescript'
2
+ import { getPropertyValue } from '../utils/get-property-value.js'
3
+ import type { AddWiring } from '../types.js'
4
+ import { ErrorCode } from '../error-codes.js'
5
+
6
+ const SEPARATOR = ':'
7
+
8
+ /**
9
+ * Unwraps `x as const` / `x satisfies T` so a cast declaration is still
10
+ * extracted rather than silently skipped.
11
+ */
12
+ const unwrapAs = (node: ts.Expression): ts.Expression =>
13
+ ts.isAsExpression(node) || ts.isSatisfiesExpression(node)
14
+ ? unwrapAs(node.expression)
15
+ : node
16
+
17
+ /**
18
+ * Reads a `scopes: [...]` array of string literals.
19
+ *
20
+ * Returns `undefined` rather than an empty array when the property is absent
21
+ * or unreadable, so the caller can tell "declared no scopes" from "we could
22
+ * not see the scopes" — the first is a legitimate placeholder role, the second
23
+ * would silently produce one.
24
+ */
25
+ const extractScopes = (
26
+ obj: ts.ObjectLiteralExpression,
27
+ roleName: string,
28
+ logger: Parameters<AddWiring>[0]
29
+ ): string[] | undefined => {
30
+ const prop = obj.properties.find(
31
+ (p) =>
32
+ ts.isPropertyAssignment(p) &&
33
+ (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
34
+ p.name.text === 'scopes'
35
+ )
36
+
37
+ if (!prop) {
38
+ logger.critical(
39
+ ErrorCode.INVALID_VALUE,
40
+ `System role '${roleName}' has no 'scopes'. A role that grants nothing should say so with 'scopes: []'.`
41
+ )
42
+ return undefined
43
+ }
44
+
45
+ if (
46
+ !ts.isPropertyAssignment(prop) ||
47
+ !ts.isArrayLiteralExpression(prop.initializer)
48
+ ) {
49
+ logger.critical(
50
+ ErrorCode.INVALID_VALUE,
51
+ `System role '${roleName}' must declare 'scopes' as an array literal.`
52
+ )
53
+ return undefined
54
+ }
55
+
56
+ const scopes: string[] = []
57
+ for (const element of prop.initializer.elements) {
58
+ const unwrapped = unwrapAs(element)
59
+ if (!ts.isStringLiteral(unwrapped)) {
60
+ logger.critical(
61
+ ErrorCode.INVALID_VALUE,
62
+ `System role '${roleName}' lists a scope that is not a string literal. ` +
63
+ `Scopes are extracted by AST, so a computed value cannot be checked against the declared set.`
64
+ )
65
+ continue
66
+ }
67
+ scopes.push(unwrapped.text)
68
+ }
69
+ return scopes
70
+ }
71
+
72
+ export const addSystemRole: AddWiring = (
73
+ logger,
74
+ node,
75
+ _checker,
76
+ state,
77
+ _options
78
+ ) => {
79
+ if (!ts.isCallExpression(node)) {
80
+ return
81
+ }
82
+
83
+ const expression = node.expression
84
+ if (!ts.isIdentifier(expression) || expression.text !== 'defineSystemRole') {
85
+ return
86
+ }
87
+
88
+ const firstArg = node.arguments[0]
89
+ if (!firstArg) {
90
+ return
91
+ }
92
+
93
+ const unwrapped = unwrapAs(firstArg)
94
+ if (!ts.isObjectLiteralExpression(unwrapped)) {
95
+ return
96
+ }
97
+
98
+ const sourceFile = node.getSourceFile().fileName
99
+
100
+ for (const prop of unwrapped.properties) {
101
+ if (!ts.isPropertyAssignment(prop)) {
102
+ continue
103
+ }
104
+
105
+ let name: string | undefined
106
+ if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) {
107
+ name = prop.name.text
108
+ }
109
+
110
+ if (name === undefined) {
111
+ logger.critical(
112
+ ErrorCode.NON_LITERAL_WIRE_NAME,
113
+ 'A system role is declared with a key that is not a literal.'
114
+ )
115
+ continue
116
+ }
117
+
118
+ if (name.length === 0) {
119
+ logger.critical(
120
+ ErrorCode.INVALID_VALUE,
121
+ 'A system role is declared with an empty name.'
122
+ )
123
+ continue
124
+ }
125
+
126
+ if (name.includes(SEPARATOR)) {
127
+ logger.critical(
128
+ ErrorCode.INVALID_VALUE,
129
+ `System role '${name}' contains the '${SEPARATOR}' separator. ` +
130
+ `'${SEPARATOR}' delimits scope ids; a role named like a scope reads as one.`
131
+ )
132
+ continue
133
+ }
134
+
135
+ if (!ts.isObjectLiteralExpression(prop.initializer)) {
136
+ logger.critical(
137
+ ErrorCode.INVALID_VALUE,
138
+ `System role '${name}' must be an object literal.`
139
+ )
140
+ continue
141
+ }
142
+
143
+ const scopes = extractScopes(prop.initializer, name, logger)
144
+ if (scopes === undefined) {
145
+ continue
146
+ }
147
+
148
+ const displayName = getPropertyValue(prop.initializer, 'displayName') as
149
+ | string
150
+ | null
151
+ const description = getPropertyValue(prop.initializer, 'description') as
152
+ | string
153
+ | null
154
+
155
+ state.systemRoles.files.add(sourceFile)
156
+ state.systemRoles.definitions.push({
157
+ name,
158
+ displayName: displayName || undefined,
159
+ description: description || undefined,
160
+ scopes,
161
+ sourceFile,
162
+ })
163
+ }
164
+ }
@@ -1,7 +1,7 @@
1
1
  import { createAddKeyedWiring } from './add-keyed-wiring.js'
2
2
 
3
3
  export const addVariable = createAddKeyedWiring({
4
- functionName: 'wireVariable',
4
+ functionName: 'defineVariable',
5
5
  idField: 'variableId',
6
6
  label: 'Variable',
7
7
  schemaPrefix: 'VariableSchema',
@@ -0,0 +1,111 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, test, beforeEach } from 'node:test'
3
+ import * as ts from 'typescript'
4
+ import { addWireAddon } from './add-wire-addon.js'
5
+
6
+ let state: any
7
+
8
+ const logger = {
9
+ debug: () => {},
10
+ info: () => {},
11
+ warn: () => {},
12
+ error: () => {},
13
+ } as any
14
+
15
+ /** Parses a source snippet and runs addWireAddon over every call expression. */
16
+ const inspect = (source: string) => {
17
+ const file = ts.createSourceFile(
18
+ 'wiring.ts',
19
+ source,
20
+ ts.ScriptTarget.Latest,
21
+ true
22
+ )
23
+ const visit = (node: ts.Node) => {
24
+ addWireAddon(node, state, logger)
25
+ ts.forEachChild(node, visit)
26
+ }
27
+ visit(file)
28
+ return state.rpc.wireAddonDeclarations
29
+ }
30
+
31
+ beforeEach(() => {
32
+ state = {
33
+ rpc: {
34
+ wireAddonDeclarations: new Map(),
35
+ usedAddons: new Set(),
36
+ wireAddonFiles: new Set(),
37
+ },
38
+ }
39
+ })
40
+
41
+ describe('addWireAddon', () => {
42
+ test('captures the addon gates alongside its plumbing', () => {
43
+ const declarations = inspect(`
44
+ wireAddon({
45
+ name: 'console',
46
+ package: '@pikku/addon-console',
47
+ rpcEndpoint: '/rpc',
48
+ auth: true,
49
+ tags: ['admin', 'internal'],
50
+ scopes: ['admin'],
51
+ })
52
+ `)
53
+
54
+ assert.deepEqual(declarations.get('console'), {
55
+ package: '@pikku/addon-console',
56
+ rpcEndpoint: '/rpc',
57
+ mcp: undefined,
58
+ auth: true,
59
+ tags: ['admin', 'internal'],
60
+ scopes: ['admin'],
61
+ secretOverrides: undefined,
62
+ variableOverrides: undefined,
63
+ credentialOverrides: undefined,
64
+ })
65
+ })
66
+
67
+ test('leaves the gates undefined when none are declared', () => {
68
+ const declarations = inspect(`
69
+ wireAddon({ name: 'console', package: '@pikku/addon-console' })
70
+ `)
71
+
72
+ const declaration = declarations.get('console')
73
+ assert.equal(declaration.auth, undefined)
74
+ assert.equal(declaration.tags, undefined)
75
+ assert.equal(declaration.scopes, undefined)
76
+ })
77
+
78
+ test('records auth: false rather than dropping it', () => {
79
+ const declarations = inspect(`
80
+ wireAddon({ name: 'console', package: '@x/y', auth: false })
81
+ `)
82
+
83
+ assert.equal(declarations.get('console').auth, false)
84
+ })
85
+
86
+ test('drops a scopes array that is not statically knowable', () => {
87
+ // A partial list would read as the complete set of gates, so an array with
88
+ // any non-literal entry is reported as unknown instead.
89
+ const declarations = inspect(`
90
+ wireAddon({ name: 'console', package: '@x/y', scopes: ['admin', ...EXTRA] })
91
+ `)
92
+
93
+ assert.equal(declarations.get('console').scopes, undefined)
94
+ })
95
+
96
+ test('drops a scopes value that is a reference rather than an array', () => {
97
+ const declarations = inspect(`
98
+ wireAddon({ name: 'console', package: '@x/y', scopes: ADMIN_SCOPES })
99
+ `)
100
+
101
+ assert.equal(declarations.get('console').scopes, undefined)
102
+ })
103
+
104
+ test('keeps an explicitly empty scopes array distinct from an absent one', () => {
105
+ const declarations = inspect(`
106
+ wireAddon({ name: 'console', package: '@x/y', scopes: [] })
107
+ `)
108
+
109
+ assert.deepEqual(declarations.get('console').scopes, [])
110
+ })
111
+ })
@@ -1,6 +1,19 @@
1
1
  import * as ts from 'typescript'
2
2
  import type { InspectorState, InspectorLogger } from '../types.js'
3
3
 
4
+ function parseStringArray(node: ts.Expression): string[] | undefined {
5
+ if (!ts.isArrayLiteralExpression(node)) return undefined
6
+ const values: string[] = []
7
+ for (const element of node.elements) {
8
+ // A non-literal entry (a spread, a const reference) is not statically
9
+ // knowable. Dropping the whole array rather than a silent partial keeps a
10
+ // consumer from reading a short list as the complete set of gates.
11
+ if (!ts.isStringLiteral(element)) return undefined
12
+ values.push(element.text)
13
+ }
14
+ return values
15
+ }
16
+
4
17
  function parseStringRecord(
5
18
  obj: ts.ObjectLiteralExpression
6
19
  ): Record<string, string> {
@@ -41,6 +54,9 @@ export function addWireAddon(
41
54
  let pkg: string | undefined
42
55
  let rpcEndpoint: string | undefined
43
56
  let mcp: boolean | undefined
57
+ let auth: boolean | undefined
58
+ let tags: string[] | undefined
59
+ let scopes: string[] | undefined
44
60
  let secretOverrides: Record<string, string> | undefined
45
61
  let variableOverrides: Record<string, string> | undefined
46
62
  let credentialOverrides: Record<string, string> | undefined
@@ -61,6 +77,16 @@ export function addWireAddon(
61
77
  prop.initializer.kind === ts.SyntaxKind.FalseKeyword)
62
78
  ) {
63
79
  mcp = prop.initializer.kind === ts.SyntaxKind.TrueKeyword
80
+ } else if (
81
+ key === 'auth' &&
82
+ (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
83
+ prop.initializer.kind === ts.SyntaxKind.FalseKeyword)
84
+ ) {
85
+ auth = prop.initializer.kind === ts.SyntaxKind.TrueKeyword
86
+ } else if (key === 'tags') {
87
+ tags = parseStringArray(prop.initializer)
88
+ } else if (key === 'scopes') {
89
+ scopes = parseStringArray(prop.initializer)
64
90
  } else if (
65
91
  key === 'secretOverrides' &&
66
92
  ts.isObjectLiteralExpression(prop.initializer)
@@ -86,6 +112,9 @@ export function addWireAddon(
86
112
  package: pkg,
87
113
  rpcEndpoint,
88
114
  mcp,
115
+ auth,
116
+ tags,
117
+ scopes,
89
118
  secretOverrides,
90
119
  variableOverrides,
91
120
  credentialOverrides,
@@ -104,17 +104,17 @@ describe('addon authoring bans', () => {
104
104
  })
105
105
  })
106
106
 
107
- test('wireSecret inside an addon is allowed', async () => {
107
+ test('defineSecret inside an addon is allowed', async () => {
108
108
  const source = [
109
- "import { wireSecret } from '@pikku/core'",
110
- "wireSecret({ name: 'example', secretId: 'EXAMPLE' } as any)",
109
+ "import { defineSecret } from '@pikku/core'",
110
+ "defineSecret({ name: 'example', secretId: 'EXAMPLE' } as any)",
111
111
  ].join('\n')
112
112
  await withTempApp(source, async (file, rootDir) => {
113
113
  const { logger, criticals } = recordingLogger()
114
114
  await inspect(logger, [file], { rootDir, isAddon: true })
115
115
  assert.ok(
116
116
  !criticals.some((c) => c.code === ErrorCode.ADDON_WIRING_NOT_ALLOWED),
117
- `expected wireSecret to be allowed, got ${JSON.stringify(criticals)}`
117
+ `expected defineSecret to be allowed, got ${JSON.stringify(criticals)}`
118
118
  )
119
119
  })
120
120
  })
@@ -36,17 +36,17 @@ const makeLogger = (criticals: Array<{ code: ErrorCode; message: string }>) =>
36
36
  * downstream, which is exactly how `displayName` went unused.
37
37
  */
38
38
  describe('config value metadata (docsUrl)', () => {
39
- test('carries docsUrl from wireSecret into the definition', async () => {
39
+ test('carries docsUrl from defineSecret into the definition', async () => {
40
40
  const rootDir = await mkdtemp(`${fixtureRoot}-secret-`)
41
41
  const file = join(rootDir, 'secret.ts')
42
42
 
43
43
  await writeFile(
44
44
  file,
45
45
  [
46
- "import { wireSecret } from '@pikku/core'",
46
+ "import { defineSecret } from '@pikku/core'",
47
47
  "import { z } from 'zod'",
48
48
  'export const StripeKeySchema = z.string()',
49
- 'wireSecret({',
49
+ 'defineSecret({',
50
50
  " name: 'stripeKey',",
51
51
  " displayName: 'Stripe Secret Key',",
52
52
  " secretId: 'STRIPE_SECRET_KEY',",
@@ -76,10 +76,10 @@ describe('config value metadata (docsUrl)', () => {
76
76
  await writeFile(
77
77
  file,
78
78
  [
79
- "import { wireSecret } from '@pikku/core'",
79
+ "import { defineSecret } from '@pikku/core'",
80
80
  "import { z } from 'zod'",
81
81
  'export const ApiKeySchema = z.string()',
82
- 'wireSecret({',
82
+ 'defineSecret({',
83
83
  " name: 'apiKey',",
84
84
  " displayName: 'API Key',",
85
85
  " secretId: 'API_KEY',",
@@ -101,17 +101,17 @@ describe('config value metadata (docsUrl)', () => {
101
101
  }
102
102
  })
103
103
 
104
- test('carries docsUrl from wireVariable into the definition', async () => {
104
+ test('carries docsUrl from defineVariable into the definition', async () => {
105
105
  const rootDir = await mkdtemp(`${fixtureRoot}-variable-`)
106
106
  const file = join(rootDir, 'variable.ts')
107
107
 
108
108
  await writeFile(
109
109
  file,
110
110
  [
111
- "import { wireVariable } from '@pikku/core'",
111
+ "import { defineVariable } from '@pikku/core'",
112
112
  "import { z } from 'zod'",
113
113
  'export const ConsoleUrlSchema = z.string()',
114
- 'wireVariable({',
114
+ 'defineVariable({',
115
115
  " name: 'consoleUrl',",
116
116
  " displayName: 'Console URL',",
117
117
  " variableId: 'CONSOLE_URL',",
@@ -134,17 +134,17 @@ describe('config value metadata (docsUrl)', () => {
134
134
  }
135
135
  })
136
136
 
137
- test('carries docsUrl from wireCredential into the definition', async () => {
137
+ test('carries docsUrl from defineCredential into the definition', async () => {
138
138
  const rootDir = await mkdtemp(`${fixtureRoot}-credential-`)
139
139
  const file = join(rootDir, 'credential.ts')
140
140
 
141
141
  await writeFile(
142
142
  file,
143
143
  [
144
- "import { wireCredential } from '@pikku/core'",
144
+ "import { defineCredential } from '@pikku/core'",
145
145
  "import { z } from 'zod'",
146
146
  'export const TokenSchema = z.object({ token: z.string() })',
147
- 'wireCredential({',
147
+ 'defineCredential({',
148
148
  " name: 'githubToken',",
149
149
  " displayName: 'GitHub Token',",
150
150
  " type: 'wire',",
@@ -62,9 +62,9 @@ describe('wiring name must be a string literal', () => {
62
62
  await writeFile(
63
63
  file,
64
64
  [
65
- "import { wireSecret } from '@pikku/core'",
65
+ "import { defineSecret } from '@pikku/core'",
66
66
  'const SECRET_ID = "STRIPE_SECRET_KEY"',
67
- 'wireSecret({',
67
+ 'defineSecret({',
68
68
  ' secretId: SECRET_ID,',
69
69
  " name: 'Stripe secret key',",
70
70
  " displayName: 'Stripe secret key',",
@@ -25,6 +25,7 @@ export enum ErrorCode {
25
25
  SCENARIO_BROWSER_STEP_NEEDS_ACTOR = 'PKU677',
26
26
  SCENARIO_STEP_TARGET_NOT_STATIC = 'PKU678',
27
27
  SCENARIO_NOT_EXTRACTABLE = 'PKU679',
28
+ SCENARIO_HAS_NO_ASSERTION = 'PKU680',
28
29
  WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = 'PKU600',
29
30
  INVALID_DSL_WORKFLOW = 'PKU641',
30
31
  WORKFLOW_GRAPH_ADDON_NOT_WIRED = 'PKU642',
@@ -53,6 +54,8 @@ export enum ErrorCode {
53
54
  ROUTE_PARAM_MISMATCH = 'PKU571',
54
55
  ROUTE_QUERY_MISMATCH = 'PKU572',
55
56
  AUTH_DISABLED_REQUIRES_SESSIONLESS = 'PKU573',
57
+ EXPOSED_FUNCTION_HAS_NO_GATE = 'PKU574',
58
+ TAG_RESOLVES_TO_NO_MIDDLEWARE = 'PKU575',
56
59
 
57
60
  // Versioning errors
58
61
  DUPLICATE_FUNCTION_VERSION = 'PKU850',
@@ -70,6 +73,11 @@ export enum ErrorCode {
70
73
  MISSING_MODEL = 'PKU145',
71
74
  INVALID_MODEL = 'PKU146',
72
75
 
76
+ // Agent tool reference errors
77
+ AGENT_TOOL_UNKNOWN_NAMESPACE = 'PKU152',
78
+ AGENT_TOOL_NOT_FOUND = 'PKU153',
79
+ AGENT_TOOL_MISSING_DESCRIPTION = 'PKU154',
80
+
73
81
  // File structure errors
74
82
  SCHEMA_AND_WIRING_COLOCATED = 'PKU490',
75
83
 
@@ -90,6 +98,11 @@ export enum ErrorCode {
90
98
  ADDON_CONTRACT_HANDLERS_NOT_ALLOWED = 'PKU921',
91
99
 
92
100
  RPC_INVOCATION_TYPE_CAST = 'PKU940',
101
+
102
+ // Secret boundary errors
103
+ SECRET_SERVICE_ALIASED = 'PKU950',
104
+ SECRET_NOT_DECLARED = 'PKU951',
105
+ SECRET_KEY_NOT_STATIC = 'PKU952',
93
106
  }
94
107
 
95
108
  /**
package/src/index.ts CHANGED
@@ -32,7 +32,10 @@ export type {
32
32
  VersionManifest,
33
33
  VersionManifestEntry,
34
34
  } from './utils/contract-hashes.js'
35
- export type { InspectorFeature, InspectorFeatureEntry } from './types.js'
35
+ export type {
36
+ InspectorFeature,
37
+ InspectorFeatureEntry,
38
+ } from './types.js'
36
39
  export { serializeMCPJson } from './utils/serialize-mcp-json.js'
37
40
  export type { OpenAPISpecInfo } from './utils/serialize-openapi-json.js'
38
41
  export {
package/src/inspector.ts CHANGED
@@ -15,22 +15,31 @@ import {
15
15
  aggregateRequiredServices,
16
16
  stampAuthHandlerServices,
17
17
  validateAgentModels,
18
+ validateAgentToolReferences,
18
19
  validateSecretOverrides,
19
20
  validateVariableOverrides,
20
21
  validateCredentialOverrides,
21
22
  validateRemoteAddonDependencies,
22
23
  validateRemoteAddonAuth,
23
24
  validateScopeReferences,
25
+ validateSystemRoleScopes,
26
+ validatePersonaRoles,
24
27
  computeResolvedIOTypes,
25
28
  computeMiddlewareGroupsMeta,
26
29
  computePermissionsGroupsMeta,
27
30
  computeRequiredSchemas,
31
+ computeSecretBrokers,
32
+ validateNoSecretAliasServices,
33
+ validateSecretUsage,
28
34
  computeDiagnostics,
29
35
  validateSchemaWiringSeparation,
30
36
  validateScenarioServices,
31
37
  validateScenarioSteps,
32
38
  validateWorkflowGraphAddons,
33
39
  } from './utils/post-process.js'
40
+ import { validateExposedFunctionsGated } from './utils/validate-exposed-functions-gated.js'
41
+ import { validateTagsResolveToMiddleware } from './utils/validate-tags-resolve-to-middleware.js'
42
+ import { annotateHttpRouteAuth } from './utils/annotate-http-route-auth.js'
34
43
  import { generateOpenAPISpec } from './utils/serialize-openapi-json.js'
35
44
  import { pikkuState } from '@pikku/core/internal'
36
45
  import { resolveLatestVersions } from './utils/resolve-versions.js'
@@ -40,6 +49,7 @@ import {
40
49
  finalizeWorkflowWires,
41
50
  } from './utils/workflow/graph/finalize-workflow-wires.js'
42
51
  import { generateAllSchemas } from './utils/schema-generator.js'
52
+ import { extractSecretUsage } from './utils/extract-secret-usage.js'
43
53
  import {
44
54
  loadAddonFunctionsMeta,
45
55
  loadAddonSchemas,
@@ -169,6 +179,7 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
169
179
  secrets: {
170
180
  definitions: [],
171
181
  files: new Set(),
182
+ usage: new Map(),
172
183
  },
173
184
  credentials: {
174
185
  definitions: [],
@@ -178,6 +189,14 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
178
189
  definitions: [],
179
190
  files: new Set(),
180
191
  },
192
+ systemRoles: {
193
+ definitions: [],
194
+ files: new Set(),
195
+ },
196
+ personas: {
197
+ definitions: [],
198
+ files: new Set(),
199
+ },
181
200
  variables: {
182
201
  definitions: [],
183
202
  files: new Set(),
@@ -354,6 +373,13 @@ export const inspect = async (
354
373
 
355
374
  const state = getInitialInspectorState(rootDir)
356
375
 
376
+ for (const sourceFile of sourceFiles) {
377
+ const usage = extractSecretUsage(sourceFile)
378
+ if (usage.keys.length > 0 || usage.dynamic.length > 0) {
379
+ state.secrets.usage.set(sourceFile.fileName, usage)
380
+ }
381
+ }
382
+
357
383
  // First sweep: add all functions
358
384
  const startSetup = performance.now()
359
385
  for (const sourceFile of sourceFiles) {
@@ -451,11 +477,21 @@ export const inspect = async (
451
477
  computeResolvedIOTypes(state)
452
478
  computeMiddlewareGroupsMeta(state)
453
479
  computePermissionsGroupsMeta(state)
480
+ computeSecretBrokers(state)
481
+ validateNoSecretAliasServices(logger, checker, state)
482
+ validateSecretUsage(logger, state)
454
483
  computeDiagnostics(state)
455
484
  validateSchemaWiringSeparation(logger, state)
456
485
  validateWorkflowGraphAddons(logger, state)
457
486
  validateScenarioServices(logger, state)
458
487
  validateScenarioSteps(logger, state)
488
+ // Needs every route, every function and every wireAddon declaration
489
+ // together, so it can only run once the whole program is inspected.
490
+ annotateHttpRouteAuth(state)
491
+ // Last of the validators: they read function meta and every wireAddon
492
+ // declaration together, so they need the whole program inspected.
493
+ validateExposedFunctionsGated(logger, state)
494
+ validateTagsResolveToMiddleware(logger, state)
459
495
 
460
496
  if (options.openAPI) {
461
497
  state.openAPISpec = await generateOpenAPISpec(
@@ -469,12 +505,15 @@ export const inspect = async (
469
505
  }
470
506
 
471
507
  validateAgentModels(logger, state)
508
+ validateAgentToolReferences(logger, state, options)
472
509
  validateSecretOverrides(logger, state)
473
510
  validateVariableOverrides(logger, state)
474
511
  validateCredentialOverrides(logger, state)
475
512
  validateRemoteAddonDependencies(logger, state)
476
513
  validateRemoteAddonAuth(logger, state)
477
514
  validateScopeReferences(logger, state)
515
+ validateSystemRoleScopes(logger, state)
516
+ validatePersonaRoles(logger, state)
478
517
  }
479
518
 
480
519
  state.program = program