@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.
- package/CHANGELOG.md +32 -0
- package/dist/add/add-cli.js +10 -3
- package/dist/add/add-credential.js +2 -1
- package/dist/add/add-functions.js +48 -1
- package/dist/add/add-http-route.js +24 -5
- package/dist/add/add-keyed-wiring.js +3 -1
- package/dist/add/add-middleware.js +33 -4
- package/dist/add/add-permission.js +7 -7
- package/dist/add/add-workflow-graph.js +20 -1
- package/dist/error-codes.d.ts +3 -1
- package/dist/error-codes.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/inspector.js +2 -5
- package/dist/types.d.ts +10 -19
- package/dist/utils/check-pii-output.d.ts +14 -0
- package/dist/utils/check-pii-output.js +63 -0
- package/dist/utils/extract-function-name.js +6 -0
- package/dist/utils/filter-inspector-state.js +187 -59
- package/dist/utils/filter-utils.js +13 -5
- package/dist/utils/get-property-value.d.ts +10 -0
- package/dist/utils/get-property-value.js +30 -0
- package/dist/utils/post-process.d.ts +2 -3
- package/dist/utils/post-process.js +3 -23
- package/dist/utils/resolve-addon-package.d.ts +4 -5
- package/dist/utils/resolve-addon-package.js +64 -16
- package/dist/utils/resolve-deploy-target.d.ts +28 -0
- package/dist/utils/resolve-deploy-target.js +56 -0
- package/dist/utils/resolve-versions.js +79 -0
- package/dist/utils/schema-generator.js +31 -12
- package/package.json +2 -2
- package/src/add/add-cli.ts +10 -3
- package/src/add/add-credential.ts +3 -0
- package/src/add/add-functions.test.ts +149 -0
- package/src/add/add-functions.ts +61 -1
- package/src/add/add-gateway.ts +5 -1
- package/src/add/add-http-route.ts +26 -6
- package/src/add/add-keyed-wiring.ts +7 -1
- package/src/add/add-mcp-prompt.ts +5 -1
- package/src/add/add-mcp-resource.ts +5 -1
- package/src/add/add-middleware.ts +42 -4
- package/src/add/add-permission.ts +7 -7
- package/src/add/add-schedule.ts +5 -1
- package/src/add/add-workflow-graph.ts +19 -1
- package/src/add/pii-check.test.ts +197 -0
- package/src/add/wire-name-literal.test.ts +114 -0
- package/src/error-codes.ts +4 -0
- package/src/index.ts +1 -0
- package/src/inspector.ts +1 -5
- package/src/types.ts +19 -15
- package/src/utils/check-pii-output.ts +76 -0
- package/src/utils/extract-function-name.ts +8 -0
- package/src/utils/filter-inspector-state.test.ts +168 -64
- package/src/utils/filter-inspector-state.ts +290 -64
- package/src/utils/filter-utils.test.ts +30 -15
- package/src/utils/filter-utils.ts +14 -5
- package/src/utils/get-property-value.ts +40 -0
- package/src/utils/post-process.ts +3 -38
- package/src/utils/resolve-addon-package.ts +65 -14
- package/src/utils/resolve-deploy-target.test.ts +105 -0
- package/src/utils/resolve-deploy-target.ts +63 -0
- package/src/utils/resolve-versions.test.ts +108 -0
- package/src/utils/resolve-versions.ts +86 -0
- package/src/utils/schema-generator.ts +37 -13
- 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
|
+
})
|
package/src/error-codes.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|