@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.
- package/CHANGELOG.md +441 -0
- package/OPTIMIZATION-PLAN.md +9 -3
- package/dist/add/add-auth.d.ts +1 -1
- package/dist/add/add-auth.js +1 -1
- package/dist/add/add-cli.js +7 -0
- package/dist/add/add-credential.js +1 -1
- package/dist/add/add-functions.js +156 -7
- package/dist/add/add-http-route.js +16 -6
- package/dist/add/add-middleware.js +46 -3
- package/dist/add/add-personas.d.ts +13 -0
- package/dist/add/add-personas.js +210 -0
- package/dist/add/add-scope.js +1 -1
- package/dist/add/add-secret.js +1 -1
- package/dist/add/add-system-role.d.ts +2 -0
- package/dist/add/add-system-role.js +102 -0
- package/dist/add/add-variable.js +1 -1
- package/dist/add/add-wire-addon.js +31 -0
- package/dist/error-codes.d.ts +10 -1
- package/dist/error-codes.js +11 -0
- package/dist/index.d.ts +1 -1
- package/dist/inspector.js +33 -1
- package/dist/types.d.ts +46 -2
- package/dist/utils/annotate-http-route-auth.d.ts +22 -0
- package/dist/utils/annotate-http-route-auth.js +48 -0
- package/dist/utils/extract-function-name.js +13 -4
- package/dist/utils/extract-function-node.js +18 -1
- package/dist/utils/extract-secret-usage.d.ts +9 -0
- package/dist/utils/extract-secret-usage.js +61 -0
- package/dist/utils/literal-properties.d.ts +22 -0
- package/dist/utils/literal-properties.js +69 -0
- package/dist/utils/load-addon-functions-meta.js +26 -1
- package/dist/utils/permissions.js +8 -0
- package/dist/utils/post-process.d.ts +68 -3
- package/dist/utils/post-process.js +243 -13
- package/dist/utils/secret-alias-services.d.ts +8 -0
- package/dist/utils/secret-alias-services.js +45 -0
- package/dist/utils/secret-brokers.d.ts +2 -0
- package/dist/utils/secret-brokers.js +9 -0
- package/dist/utils/serialize-inspector-state.d.ts +10 -0
- package/dist/utils/serialize-inspector-state.js +18 -0
- package/dist/utils/serialize-middleware-groups-meta.js +3 -0
- package/dist/utils/validate-exposed-functions-gated.d.ts +14 -0
- package/dist/utils/validate-exposed-functions-gated.js +85 -0
- package/dist/utils/validate-tags-resolve-to-middleware.d.ts +20 -0
- package/dist/utils/validate-tags-resolve-to-middleware.js +41 -0
- package/dist/visit.js +4 -0
- package/package.json +3 -3
- package/src/add/add-auth.test.ts +5 -3
- package/src/add/add-auth.ts +1 -1
- package/src/add/add-cli.ts +8 -0
- package/src/add/add-credential.ts +1 -1
- package/src/add/add-functions.test.ts +263 -1
- package/src/add/add-functions.ts +219 -9
- package/src/add/add-http-route.ts +20 -7
- package/src/add/add-middleware.test.ts +196 -0
- package/src/add/add-middleware.ts +64 -4
- package/src/add/add-personas.test.ts +268 -0
- package/src/add/add-personas.ts +276 -0
- package/src/add/add-scenario-step.test.ts +49 -3
- package/src/add/add-scope.test.ts +30 -30
- package/src/add/add-scope.ts +1 -1
- package/src/add/add-secret.ts +1 -1
- package/src/add/add-subject-scenario-steps.test.ts +161 -0
- package/src/add/add-system-role.test.ts +214 -0
- package/src/add/add-system-role.ts +164 -0
- package/src/add/add-variable.ts +1 -1
- package/src/add/add-wire-addon.test.ts +111 -0
- package/src/add/add-wire-addon.ts +29 -0
- package/src/add/addon-bans.test.ts +4 -4
- package/src/add/config-value-metadata.test.ts +11 -11
- package/src/add/wire-name-literal.test.ts +2 -2
- package/src/error-codes.ts +13 -0
- package/src/index.ts +4 -1
- package/src/inspector.ts +39 -0
- package/src/types.ts +47 -2
- package/src/utils/annotate-http-route-auth.test.ts +163 -0
- package/src/utils/annotate-http-route-auth.ts +52 -0
- package/src/utils/extract-function-name.ts +14 -4
- package/src/utils/extract-function-node.ts +18 -6
- package/src/utils/extract-secret-usage.test.ts +81 -0
- package/src/utils/extract-secret-usage.ts +73 -0
- package/src/utils/literal-properties.ts +97 -0
- package/src/utils/load-addon-functions-meta.ts +32 -2
- package/src/utils/permissions-shorthand.test.ts +79 -0
- package/src/utils/permissions.ts +7 -0
- package/src/utils/post-process.test.ts +133 -0
- package/src/utils/post-process.ts +310 -12
- package/src/utils/secret-alias-services.test.ts +146 -0
- package/src/utils/secret-alias-services.ts +58 -0
- package/src/utils/secret-brokers.test.ts +53 -0
- package/src/utils/secret-brokers.ts +11 -0
- package/src/utils/serialize-inspector-state.ts +28 -0
- package/src/utils/serialize-middleware-groups-meta.ts +3 -0
- package/src/utils/validate-exposed-functions-gated.test.ts +230 -0
- package/src/utils/validate-exposed-functions-gated.ts +94 -0
- package/src/utils/validate-tags-resolve-to-middleware.test.ts +105 -0
- package/src/utils/validate-tags-resolve-to-middleware.ts +48 -0
- package/src/visit.ts +4 -0
- 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
|
+
}
|
package/src/add/add-variable.ts
CHANGED
|
@@ -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('
|
|
107
|
+
test('defineSecret inside an addon is allowed', async () => {
|
|
108
108
|
const source = [
|
|
109
|
-
"import {
|
|
110
|
-
"
|
|
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
|
|
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
|
|
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 {
|
|
46
|
+
"import { defineSecret } from '@pikku/core'",
|
|
47
47
|
"import { z } from 'zod'",
|
|
48
48
|
'export const StripeKeySchema = z.string()',
|
|
49
|
-
'
|
|
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 {
|
|
79
|
+
"import { defineSecret } from '@pikku/core'",
|
|
80
80
|
"import { z } from 'zod'",
|
|
81
81
|
'export const ApiKeySchema = z.string()',
|
|
82
|
-
'
|
|
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
|
|
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 {
|
|
111
|
+
"import { defineVariable } from '@pikku/core'",
|
|
112
112
|
"import { z } from 'zod'",
|
|
113
113
|
'export const ConsoleUrlSchema = z.string()',
|
|
114
|
-
'
|
|
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
|
|
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 {
|
|
144
|
+
"import { defineCredential } from '@pikku/core'",
|
|
145
145
|
"import { z } from 'zod'",
|
|
146
146
|
'export const TokenSchema = z.object({ token: z.string() })',
|
|
147
|
-
'
|
|
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 {
|
|
65
|
+
"import { defineSecret } from '@pikku/core'",
|
|
66
66
|
'const SECRET_ID = "STRIPE_SECRET_KEY"',
|
|
67
|
-
'
|
|
67
|
+
'defineSecret({',
|
|
68
68
|
' secretId: SECRET_ID,',
|
|
69
69
|
" name: 'Stripe secret key',",
|
|
70
70
|
" displayName: 'Stripe secret key',",
|
package/src/error-codes.ts
CHANGED
|
@@ -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 {
|
|
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
|