@pikku/inspector 0.12.41 → 0.12.43
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 +104 -0
- package/dist/add/add-addon-bans.js +1 -0
- package/dist/add/add-ai-agent.d.ts +3 -1
- package/dist/add/add-ai-agent.js +82 -0
- package/dist/add/add-credential.js +3 -1
- package/dist/add/add-functions.js +10 -9
- package/dist/add/add-gateway.js +3 -0
- package/dist/add/add-http-route.js +2 -2
- package/dist/add/add-mcp-prompt.js +0 -1
- package/dist/add/add-mcp-resource.js +0 -1
- package/dist/add/add-permission.d.ts +1 -1
- package/dist/add/add-permission.js +3 -177
- package/dist/add/add-queue-worker.js +3 -0
- package/dist/add/add-schedule.js +3 -0
- package/dist/add/add-scope.d.ts +2 -0
- package/dist/add/add-scope.js +146 -0
- package/dist/add/add-trigger.js +3 -0
- package/dist/add/add-wire-remote-addon.d.ts +10 -0
- package/dist/add/add-wire-remote-addon.js +66 -0
- package/dist/add/add-workflow-graph.js +32 -3
- package/dist/error-codes.d.ts +3 -0
- package/dist/error-codes.js +4 -0
- package/dist/inspector.js +19 -7
- package/dist/types.d.ts +19 -13
- package/dist/utils/custom-types-generator.js +2 -1
- package/dist/utils/ensure-function-metadata.d.ts +15 -0
- package/dist/utils/ensure-function-metadata.js +24 -0
- package/dist/utils/filter-inspector-state.js +9 -0
- package/dist/utils/get-property-value.d.ts +7 -2
- package/dist/utils/get-property-value.js +48 -1
- package/dist/utils/load-addon-functions-meta.js +81 -6
- package/dist/utils/nested-project-filter.d.ts +1 -0
- package/dist/utils/nested-project-filter.js +29 -0
- package/dist/utils/permissions.d.ts +1 -21
- package/dist/utils/permissions.js +17 -108
- package/dist/utils/post-process.d.ts +30 -1
- package/dist/utils/post-process.js +150 -72
- package/dist/utils/serialize-inspector-state.d.ts +13 -8
- package/dist/utils/serialize-inspector-state.js +12 -6
- package/dist/utils/serialize-permissions-groups-meta.d.ts +0 -2
- package/dist/utils/serialize-permissions-groups-meta.js +0 -26
- package/dist/utils/workflow/derive-workflow-plan.js +9 -3
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +56 -1
- package/dist/utils/workflow/dsl/patterns.d.ts +10 -0
- package/dist/utils/workflow/dsl/patterns.js +24 -8
- package/dist/utils/workflow/dsl/validation.js +3 -4
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +14 -0
- package/dist/utils/workflow/graph/finalize-workflows.d.ts +7 -0
- package/dist/utils/workflow/graph/finalize-workflows.js +11 -2
- package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +2 -0
- package/dist/utils/workflow/graph/serialize-workflow-graph.js +4 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +6 -2
- package/dist/visit.js +4 -0
- package/package.json +3 -3
- package/src/add/add-addon-bans.ts +1 -0
- package/src/add/add-ai-agent.test.ts +87 -0
- package/src/add/add-ai-agent.ts +122 -0
- package/src/add/add-credential.ts +6 -0
- package/src/add/add-functions.ts +10 -12
- package/src/add/add-gateway.ts +11 -0
- package/src/add/add-http-route.ts +2 -8
- package/src/add/add-mcp-prompt.ts +0 -1
- package/src/add/add-mcp-resource.ts +0 -1
- package/src/add/add-permission.ts +4 -242
- package/src/add/add-queue-worker.ts +11 -0
- package/src/add/add-schedule.ts +11 -0
- package/src/add/add-scope.test.ts +346 -0
- package/src/add/add-scope.ts +225 -0
- package/src/add/add-trigger.ts +11 -0
- package/src/add/add-wire-remote-addon.ts +77 -0
- package/src/add/add-workflow-graph-input.test.ts +94 -0
- package/src/add/add-workflow-graph-notes.test.ts +74 -0
- package/src/add/add-workflow-graph.ts +35 -3
- package/src/add/inline-wiring-function.test.ts +104 -0
- package/src/add/validate-workflow-graph-addons.test.ts +102 -0
- package/src/error-codes.ts +5 -0
- package/src/inspector.ts +22 -6
- package/src/types.ts +16 -17
- package/src/utils/custom-types-generator.test.ts +26 -1
- package/src/utils/custom-types-generator.ts +2 -1
- package/src/utils/ensure-function-metadata.ts +42 -0
- package/src/utils/filter-inspector-state.test.ts +0 -2
- package/src/utils/filter-inspector-state.ts +9 -0
- package/src/utils/get-property-value.test.ts +141 -0
- package/src/utils/get-property-value.ts +62 -1
- package/src/utils/load-addon-functions-meta.test.ts +157 -0
- package/src/utils/load-addon-functions-meta.ts +94 -6
- package/src/utils/load-addon-scopes.test.ts +138 -0
- package/src/utils/nested-project-filter.test.ts +53 -0
- package/src/utils/nested-project-filter.ts +28 -0
- package/src/utils/permissions.test.ts +7 -232
- package/src/utils/permissions.ts +20 -160
- package/src/utils/post-process.test.ts +269 -4
- package/src/utils/post-process.ts +216 -95
- package/src/utils/serialize-inspector-state.ts +22 -25
- package/src/utils/serialize-permissions-groups-meta.ts +0 -28
- package/src/utils/workflow/derive-workflow-plan.test.ts +41 -3
- package/src/utils/workflow/derive-workflow-plan.ts +11 -4
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +66 -0
- package/src/utils/workflow/dsl/patterns.ts +34 -8
- package/src/utils/workflow/dsl/validation.ts +3 -4
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +15 -0
- package/src/utils/workflow/graph/finalize-workflows.test.ts +50 -0
- package/src/utils/workflow/graph/finalize-workflows.ts +13 -2
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +6 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +8 -0
- package/src/visit.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -10,9 +10,12 @@ import type {
|
|
|
10
10
|
MiddlewareMetadata,
|
|
11
11
|
PermissionMetadata,
|
|
12
12
|
} from '@pikku/core'
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
14
|
+
import { join } from 'node:path'
|
|
13
15
|
import { extractTypeKeys } from './type-utils.js'
|
|
14
16
|
import { ErrorCode } from '../error-codes.js'
|
|
15
17
|
import { AUTH_HANDLER_FUNC_ID } from '../add/add-auth.js'
|
|
18
|
+
import { flattenScopeDefinitions } from '@pikku/core/scope'
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* Stamp the inspected authorize/callbacks service set onto the generated auth
|
|
@@ -46,7 +49,7 @@ export function stampAuthHandlerServices(
|
|
|
46
49
|
* Skips type:'http' and type:'tag' (reference groups, not individuals).
|
|
47
50
|
*/
|
|
48
51
|
export function extractWireNames(
|
|
49
|
-
list?: MiddlewareMetadata
|
|
52
|
+
list?: Array<MiddlewareMetadata | PermissionMetadata>
|
|
50
53
|
): string[] {
|
|
51
54
|
if (!list) return []
|
|
52
55
|
return list
|
|
@@ -57,34 +60,25 @@ export function extractWireNames(
|
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
/**
|
|
60
|
-
* Helper to expand middleware
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
+
* Helper to expand middleware groups into individual names and add their
|
|
64
|
+
* services to the aggregation. Handles tag-based and HTTP-pattern-based
|
|
65
|
+
* middleware groups. Permissions are function-scoped only and carry no groups.
|
|
63
66
|
*/
|
|
64
67
|
function expandAndAddGroupServices(
|
|
65
|
-
list: MiddlewareMetadata[] |
|
|
68
|
+
list: MiddlewareMetadata[] | undefined,
|
|
66
69
|
state: InspectorState | Omit<InspectorState, 'typesLookup'>,
|
|
67
|
-
addServices: (services: FunctionServicesMeta | undefined) => void
|
|
68
|
-
isMiddleware: boolean
|
|
70
|
+
addServices: (services: FunctionServicesMeta | undefined) => void
|
|
69
71
|
): void {
|
|
70
72
|
if (!list) return
|
|
71
73
|
|
|
72
74
|
for (const item of list) {
|
|
73
75
|
if (item.type === 'tag') {
|
|
74
|
-
|
|
75
|
-
const groupMeta = isMiddleware
|
|
76
|
-
? state.middleware.tagMiddleware.get(item.tag)
|
|
77
|
-
: state.permissions.tagPermissions.get(item.tag)
|
|
78
|
-
|
|
76
|
+
const groupMeta = state.middleware.tagMiddleware.get(item.tag)
|
|
79
77
|
if (groupMeta?.services) {
|
|
80
78
|
addServices(groupMeta.services)
|
|
81
79
|
}
|
|
82
80
|
} else if (item.type === 'http' && 'route' in item) {
|
|
83
|
-
|
|
84
|
-
const groupMeta = isMiddleware
|
|
85
|
-
? state.http.routeMiddleware.get(item.route)
|
|
86
|
-
: state.http.routePermissions.get(item.route)
|
|
87
|
-
|
|
81
|
+
const groupMeta = state.http.routeMiddleware.get(item.route)
|
|
88
82
|
if (groupMeta?.services) {
|
|
89
83
|
addServices(groupMeta.services)
|
|
90
84
|
}
|
|
@@ -188,55 +182,33 @@ export function aggregateRequiredServices(
|
|
|
188
182
|
'options',
|
|
189
183
|
] as const) {
|
|
190
184
|
for (const routeMeta of Object.values(state.http.meta[method])) {
|
|
191
|
-
expandAndAddGroupServices(routeMeta.middleware, state, addServices
|
|
192
|
-
expandAndAddGroupServices(
|
|
193
|
-
routeMeta.permissions,
|
|
194
|
-
state,
|
|
195
|
-
addServices,
|
|
196
|
-
false
|
|
197
|
-
)
|
|
185
|
+
expandAndAddGroupServices(routeMeta.middleware, state, addServices)
|
|
198
186
|
}
|
|
199
187
|
}
|
|
200
188
|
|
|
201
189
|
// Also check other wiring types (channels, queues, schedulers, MCP)
|
|
202
190
|
for (const channelMeta of Object.values(state.channels.meta)) {
|
|
203
|
-
expandAndAddGroupServices(channelMeta.middleware, state, addServices
|
|
204
|
-
expandAndAddGroupServices(
|
|
205
|
-
channelMeta.permissions,
|
|
206
|
-
state,
|
|
207
|
-
addServices,
|
|
208
|
-
false
|
|
209
|
-
)
|
|
191
|
+
expandAndAddGroupServices(channelMeta.middleware, state, addServices)
|
|
210
192
|
}
|
|
211
193
|
|
|
212
194
|
for (const queueMeta of Object.values(state.queueWorkers.meta)) {
|
|
213
|
-
expandAndAddGroupServices(queueMeta.middleware, state, addServices
|
|
214
|
-
// expandAndAddGroupServices(queueMeta.permissions, state, addServices, false)
|
|
195
|
+
expandAndAddGroupServices(queueMeta.middleware, state, addServices)
|
|
215
196
|
}
|
|
216
197
|
|
|
217
198
|
for (const scheduleMeta of Object.values(state.scheduledTasks.meta)) {
|
|
218
|
-
expandAndAddGroupServices(scheduleMeta.middleware, state, addServices
|
|
219
|
-
// expandAndAddGroupServices(scheduleMeta.permissions, state, addServices, false)
|
|
199
|
+
expandAndAddGroupServices(scheduleMeta.middleware, state, addServices)
|
|
220
200
|
}
|
|
221
201
|
|
|
222
202
|
for (const toolMeta of Object.values(state.mcpEndpoints.toolsMeta)) {
|
|
223
|
-
expandAndAddGroupServices(toolMeta.middleware, state, addServices
|
|
224
|
-
expandAndAddGroupServices(toolMeta.permissions, state, addServices, false)
|
|
203
|
+
expandAndAddGroupServices(toolMeta.middleware, state, addServices)
|
|
225
204
|
}
|
|
226
205
|
|
|
227
206
|
for (const promptMeta of Object.values(state.mcpEndpoints.promptsMeta)) {
|
|
228
|
-
expandAndAddGroupServices(promptMeta.middleware, state, addServices
|
|
229
|
-
expandAndAddGroupServices(promptMeta.permissions, state, addServices, false)
|
|
207
|
+
expandAndAddGroupServices(promptMeta.middleware, state, addServices)
|
|
230
208
|
}
|
|
231
209
|
|
|
232
210
|
for (const resourceMeta of Object.values(state.mcpEndpoints.resourcesMeta)) {
|
|
233
|
-
expandAndAddGroupServices(resourceMeta.middleware, state, addServices
|
|
234
|
-
expandAndAddGroupServices(
|
|
235
|
-
resourceMeta.permissions,
|
|
236
|
-
state,
|
|
237
|
-
addServices,
|
|
238
|
-
false
|
|
239
|
-
)
|
|
211
|
+
expandAndAddGroupServices(resourceMeta.middleware, state, addServices)
|
|
240
212
|
}
|
|
241
213
|
|
|
242
214
|
// 5. Services from session service factories
|
|
@@ -378,17 +350,24 @@ export function validateSecretOverrides(
|
|
|
378
350
|
const { wireAddonDeclarations } = state.rpc
|
|
379
351
|
if (!wireAddonDeclarations || wireAddonDeclarations.size === 0) return
|
|
380
352
|
|
|
381
|
-
|
|
353
|
+
// secretOverrides key on (and resolve to) SECRET IDs — the string the addon
|
|
354
|
+
// passes to getSecret — so validate against secretId, falling back to name for
|
|
355
|
+
// older meta without a secretId field.
|
|
356
|
+
const secretIds = new Set(
|
|
357
|
+
state.secrets.definitions.map((d: any) => d.secretId ?? d.name)
|
|
358
|
+
)
|
|
382
359
|
|
|
383
360
|
for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
|
|
384
361
|
if (!addonDecl.secretOverrides) continue
|
|
385
362
|
|
|
386
|
-
for (const
|
|
387
|
-
|
|
388
|
-
|
|
363
|
+
for (const [logicalName, resolvedName] of Object.entries(
|
|
364
|
+
addonDecl.secretOverrides
|
|
365
|
+
)) {
|
|
366
|
+
if (!secretIds.has(resolvedName)) {
|
|
367
|
+
const availableSecrets = Array.from(secretIds)
|
|
389
368
|
logger.critical(
|
|
390
369
|
ErrorCode.INVALID_VALUE,
|
|
391
|
-
`Secret override '${
|
|
370
|
+
`Secret override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a secret that does not exist. Available secrets: ${availableSecrets.join(', ') || 'none'}`
|
|
392
371
|
)
|
|
393
372
|
}
|
|
394
373
|
}
|
|
@@ -409,12 +388,14 @@ export function validateCredentialOverrides(
|
|
|
409
388
|
for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
|
|
410
389
|
if (!addonDecl.credentialOverrides) continue
|
|
411
390
|
|
|
412
|
-
for (const
|
|
413
|
-
|
|
391
|
+
for (const [logicalName, resolvedName] of Object.entries(
|
|
392
|
+
addonDecl.credentialOverrides
|
|
393
|
+
)) {
|
|
394
|
+
if (!credentialNames.has(resolvedName)) {
|
|
414
395
|
const availableCredentials = Array.from(credentialNames)
|
|
415
396
|
logger.critical(
|
|
416
397
|
ErrorCode.INVALID_VALUE,
|
|
417
|
-
`Credential override '${
|
|
398
|
+
`Credential override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a credential that does not exist. Available credentials: ${availableCredentials.join(', ') || 'none'}`
|
|
418
399
|
)
|
|
419
400
|
}
|
|
420
401
|
}
|
|
@@ -428,23 +409,120 @@ export function validateVariableOverrides(
|
|
|
428
409
|
const { wireAddonDeclarations } = state.rpc
|
|
429
410
|
if (!wireAddonDeclarations || wireAddonDeclarations.size === 0) return
|
|
430
411
|
|
|
431
|
-
|
|
412
|
+
// variableOverrides key on (and resolve to) VARIABLE IDs, so validate against
|
|
413
|
+
// variableId, falling back to name for older meta without a variableId field.
|
|
414
|
+
const variableIds = new Set(
|
|
415
|
+
state.variables.definitions.map((d: any) => d.variableId ?? d.name)
|
|
416
|
+
)
|
|
432
417
|
|
|
433
418
|
for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
|
|
434
419
|
if (!addonDecl.variableOverrides) continue
|
|
435
420
|
|
|
436
|
-
for (const
|
|
437
|
-
|
|
438
|
-
|
|
421
|
+
for (const [logicalName, resolvedName] of Object.entries(
|
|
422
|
+
addonDecl.variableOverrides
|
|
423
|
+
)) {
|
|
424
|
+
if (!variableIds.has(resolvedName)) {
|
|
425
|
+
const availableVariables = Array.from(variableIds)
|
|
439
426
|
logger.critical(
|
|
440
427
|
ErrorCode.INVALID_VALUE,
|
|
441
|
-
`Variable override '${
|
|
428
|
+
`Variable override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a variable that does not exist. Available variables: ${availableVariables.join(', ') || 'none'}`
|
|
442
429
|
)
|
|
443
430
|
}
|
|
444
431
|
}
|
|
445
432
|
}
|
|
446
433
|
}
|
|
447
434
|
|
|
435
|
+
/**
|
|
436
|
+
* A `wireRemoteAddon` package ships types only — its handlers run on the host —
|
|
437
|
+
* so it MUST be a devDependency, not a production dependency (a prod dep would
|
|
438
|
+
* drag in the runtime deps remote consumption exists to avoid). This is the
|
|
439
|
+
* mirror image of `wireAddon`, which requires a production dependency.
|
|
440
|
+
*/
|
|
441
|
+
export function validateRemoteAddonDependencies(
|
|
442
|
+
logger: InspectorLogger,
|
|
443
|
+
state: InspectorState | Omit<InspectorState, 'typesLookup'>
|
|
444
|
+
): void {
|
|
445
|
+
const { wireAddonDeclarations } = state.rpc
|
|
446
|
+
if (!wireAddonDeclarations || wireAddonDeclarations.size === 0) return
|
|
447
|
+
|
|
448
|
+
const hasRemote = Array.from(wireAddonDeclarations.values()).some(
|
|
449
|
+
(d) => d.remote
|
|
450
|
+
)
|
|
451
|
+
if (!hasRemote) return
|
|
452
|
+
|
|
453
|
+
const pkgJsonPath = join(state.rootDir, 'package.json')
|
|
454
|
+
if (!existsSync(pkgJsonPath)) return // no manifest to check (e.g. some tests)
|
|
455
|
+
|
|
456
|
+
let pkgJson: {
|
|
457
|
+
dependencies?: Record<string, string>
|
|
458
|
+
devDependencies?: Record<string, string>
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'))
|
|
462
|
+
} catch (e: any) {
|
|
463
|
+
logger.warn(
|
|
464
|
+
`Could not read ${pkgJsonPath} to verify remote addon dependencies: ${e?.message ?? e}`
|
|
465
|
+
)
|
|
466
|
+
return
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const prodDeps = pkgJson.dependencies ?? {}
|
|
470
|
+
const devDeps = pkgJson.devDependencies ?? {}
|
|
471
|
+
|
|
472
|
+
for (const [namespace, decl] of wireAddonDeclarations.entries()) {
|
|
473
|
+
if (!decl.remote) continue
|
|
474
|
+
if (decl.package in devDeps) continue // correct
|
|
475
|
+
|
|
476
|
+
if (decl.package in prodDeps) {
|
|
477
|
+
logger.critical(
|
|
478
|
+
ErrorCode.REMOTE_ADDON_NOT_DEV_DEPENDENCY,
|
|
479
|
+
`Remote addon '${namespace}' ('${decl.package}') is a production dependency, but wireRemoteAddon consumes it for types only — its handlers run on the host. Move '${decl.package}' from "dependencies" to "devDependencies".`
|
|
480
|
+
)
|
|
481
|
+
} else {
|
|
482
|
+
logger.critical(
|
|
483
|
+
ErrorCode.REMOTE_ADDON_NOT_DEV_DEPENDENCY,
|
|
484
|
+
`Remote addon '${namespace}' ('${decl.package}') is wired with wireRemoteAddon but is not in "devDependencies". Add '${decl.package}' to "devDependencies" (types only).`
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* The auth a `wireRemoteAddon` consumer binds must reference a slot the consumer
|
|
492
|
+
* actually declares: a `credentialId` must be a wired credential, a `secretId`
|
|
493
|
+
* a wired secret. A custom `resolve()` and a public (omitted) surface are not
|
|
494
|
+
* statically checkable and are left to runtime.
|
|
495
|
+
*/
|
|
496
|
+
export function validateRemoteAddonAuth(
|
|
497
|
+
logger: InspectorLogger,
|
|
498
|
+
state: InspectorState | Omit<InspectorState, 'typesLookup'>
|
|
499
|
+
): void {
|
|
500
|
+
const { wireAddonDeclarations } = state.rpc
|
|
501
|
+
if (!wireAddonDeclarations || wireAddonDeclarations.size === 0) return
|
|
502
|
+
|
|
503
|
+
const credentialNames = new Set(
|
|
504
|
+
state.credentials?.definitions.map((d) => d.name) ?? []
|
|
505
|
+
)
|
|
506
|
+
const secretNames = new Set(state.secrets.definitions.map((d) => d.name))
|
|
507
|
+
|
|
508
|
+
for (const [namespace, decl] of wireAddonDeclarations.entries()) {
|
|
509
|
+
if (!decl.remote) continue
|
|
510
|
+
|
|
511
|
+
if (decl.authCredentialId && !credentialNames.has(decl.authCredentialId)) {
|
|
512
|
+
logger.critical(
|
|
513
|
+
ErrorCode.REMOTE_ADDON_AUTH_UNRESOLVED,
|
|
514
|
+
`Remote addon '${namespace}' binds auth.credentialId '${decl.authCredentialId}', but no such credential is wired. Available credentials: ${Array.from(credentialNames).join(', ') || 'none'}`
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
if (decl.authSecretId && !secretNames.has(decl.authSecretId)) {
|
|
518
|
+
logger.critical(
|
|
519
|
+
ErrorCode.REMOTE_ADDON_AUTH_UNRESOLVED,
|
|
520
|
+
`Remote addon '${namespace}' binds auth.secretId '${decl.authSecretId}', but no such secret is wired. Available secrets: ${Array.from(secretNames).join(', ') || 'none'}`
|
|
521
|
+
)
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
448
526
|
export function computeResolvedIOTypes(state: InspectorState): void {
|
|
449
527
|
const { functions } = state
|
|
450
528
|
for (const [pikkuFuncId, meta] of Object.entries(functions.meta)) {
|
|
@@ -531,36 +609,8 @@ export function computeMiddlewareGroupsMeta(state: InspectorState): void {
|
|
|
531
609
|
}
|
|
532
610
|
|
|
533
611
|
export function computePermissionsGroupsMeta(state: InspectorState): void {
|
|
534
|
-
const httpGroups: Record<string, any> = {}
|
|
535
|
-
for (const [pattern, meta] of state.http.routePermissions.entries()) {
|
|
536
|
-
httpGroups[pattern] = {
|
|
537
|
-
exportName: meta.exportName,
|
|
538
|
-
sourceFile: meta.sourceFile,
|
|
539
|
-
position: meta.position,
|
|
540
|
-
services: meta.services,
|
|
541
|
-
count: meta.count,
|
|
542
|
-
instanceIds: meta.instanceIds,
|
|
543
|
-
isFactory: meta.isFactory,
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const tagGroups: Record<string, any> = {}
|
|
548
|
-
for (const [tag, meta] of state.permissions.tagPermissions.entries()) {
|
|
549
|
-
tagGroups[tag] = {
|
|
550
|
-
exportName: meta.exportName,
|
|
551
|
-
sourceFile: meta.sourceFile,
|
|
552
|
-
position: meta.position,
|
|
553
|
-
services: meta.services,
|
|
554
|
-
count: meta.count,
|
|
555
|
-
instanceIds: meta.instanceIds,
|
|
556
|
-
isFactory: meta.isFactory,
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
612
|
state.permissionsGroupsMeta = {
|
|
561
613
|
definitions: state.permissions.definitions,
|
|
562
|
-
httpGroups,
|
|
563
|
-
tagGroups,
|
|
564
614
|
}
|
|
565
615
|
}
|
|
566
616
|
|
|
@@ -632,6 +682,44 @@ export function validateAgentModels(
|
|
|
632
682
|
}
|
|
633
683
|
}
|
|
634
684
|
|
|
685
|
+
/**
|
|
686
|
+
* A `pikkuWorkflowGraph` node that references the `graph:` namespace (e.g.
|
|
687
|
+
* `graph:editFields`) needs @pikku/addon-graph wired — otherwise the RPC never
|
|
688
|
+
* registers and codegen fails deep in type-checking with an opaque error. Fail
|
|
689
|
+
* early with an actionable message pointing at `scaffold: { graph: true }`.
|
|
690
|
+
*/
|
|
691
|
+
export function validateWorkflowGraphAddons(
|
|
692
|
+
logger: InspectorLogger,
|
|
693
|
+
state: InspectorState
|
|
694
|
+
): void {
|
|
695
|
+
const addonGraphWired = Array.from(
|
|
696
|
+
state.rpc.wireAddonDeclarations.values()
|
|
697
|
+
).some((decl) => decl.package === '@pikku/addon-graph')
|
|
698
|
+
if (addonGraphWired) {
|
|
699
|
+
return
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
for (const [name, graph] of Object.entries(state.workflows.graphMeta)) {
|
|
703
|
+
for (const node of Object.values(graph.nodes)) {
|
|
704
|
+
if (!('rpcName' in node) || typeof node.rpcName !== 'string') {
|
|
705
|
+
continue
|
|
706
|
+
}
|
|
707
|
+
if (!node.rpcName.startsWith('graph:')) {
|
|
708
|
+
continue
|
|
709
|
+
}
|
|
710
|
+
if (state.functions.meta[node.rpcName]) {
|
|
711
|
+
continue
|
|
712
|
+
}
|
|
713
|
+
logger.critical(
|
|
714
|
+
ErrorCode.WORKFLOW_GRAPH_ADDON_NOT_WIRED,
|
|
715
|
+
`Workflow graph '${name}' references '${node.rpcName}' but @pikku/addon-graph is not wired. ` +
|
|
716
|
+
`Enable "scaffold": { "graph": true } in pikku.config.json (and install @pikku/addon-graph), ` +
|
|
717
|
+
`or wire it manually with wireAddon({ name: 'graph', package: '@pikku/addon-graph' }).`
|
|
718
|
+
)
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
635
723
|
/**
|
|
636
724
|
* Validates that Zod schemas and wiring side-effects (wireHTTPRoutes,
|
|
637
725
|
* addPermission, addHTTPMiddleware, etc.) do not coexist in the same file.
|
|
@@ -658,14 +746,6 @@ export function validateSchemaWiringSeparation(
|
|
|
658
746
|
wiringFiles.add(file)
|
|
659
747
|
}
|
|
660
748
|
|
|
661
|
-
// Permission wirings (addPermission calls)
|
|
662
|
-
for (const meta of state.permissions.tagPermissions.values()) {
|
|
663
|
-
wiringFiles.add(meta.sourceFile)
|
|
664
|
-
}
|
|
665
|
-
for (const meta of state.http.routePermissions.values()) {
|
|
666
|
-
wiringFiles.add(meta.sourceFile)
|
|
667
|
-
}
|
|
668
|
-
|
|
669
749
|
// Middleware wirings (addHTTPMiddleware calls)
|
|
670
750
|
for (const meta of state.http.routeMiddleware.values()) {
|
|
671
751
|
wiringFiles.add(meta.sourceFile)
|
|
@@ -769,3 +849,44 @@ export function computeDiagnostics(state: InspectorState): void {
|
|
|
769
849
|
|
|
770
850
|
state.diagnostics = diagnostics
|
|
771
851
|
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Validates that every scope referenced by a function is declared via
|
|
855
|
+
* `wireScope`. Runs after all visitors, so declaration order does not matter.
|
|
856
|
+
*
|
|
857
|
+
* A `*` suffix is a wildcard requirement: `admin:*` requires the `admin` scope
|
|
858
|
+
* to be declared, and grants its whole subtree.
|
|
859
|
+
*/
|
|
860
|
+
export function validateScopeReferences(
|
|
861
|
+
logger: InspectorLogger,
|
|
862
|
+
state: InspectorState | Omit<InspectorState, 'typesLookup'>
|
|
863
|
+
): void {
|
|
864
|
+
const declared = new Set(
|
|
865
|
+
flattenScopeDefinitions(state.scopes.definitions).map((s) => s.id)
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
for (const [funcName, meta] of Object.entries(state.functions.meta)) {
|
|
869
|
+
if (!meta.scopes?.length) continue
|
|
870
|
+
|
|
871
|
+
for (const scope of meta.scopes) {
|
|
872
|
+
// A trailing wildcard grants a subtree; the node it hangs off must exist.
|
|
873
|
+
const declaredForm = scope.endsWith(':*') ? scope.slice(0, -2) : scope
|
|
874
|
+
|
|
875
|
+
if (scope === '*') {
|
|
876
|
+
logger.critical(
|
|
877
|
+
ErrorCode.INVALID_VALUE,
|
|
878
|
+
`Function '${funcName}' requires the bare wildcard scope '*'. A function must require a specific scope — '*' is only meaningful as a grant.`
|
|
879
|
+
)
|
|
880
|
+
continue
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (!declared.has(declaredForm)) {
|
|
884
|
+
const available = Array.from(declared)
|
|
885
|
+
logger.critical(
|
|
886
|
+
ErrorCode.INVALID_VALUE,
|
|
887
|
+
`Function '${funcName}' requires scope '${scope}' which is not declared. Declare it with wireScope. Available scopes: ${available.join(', ') || 'none'}`
|
|
888
|
+
)
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
@@ -112,14 +112,6 @@ export interface SerializableInspectorState {
|
|
|
112
112
|
: never,
|
|
113
113
|
]
|
|
114
114
|
>
|
|
115
|
-
routePermissions: Array<
|
|
116
|
-
[
|
|
117
|
-
string,
|
|
118
|
-
InspectorState['http']['routePermissions'] extends Map<string, infer V>
|
|
119
|
-
? V
|
|
120
|
-
: never,
|
|
121
|
-
]
|
|
122
|
-
>
|
|
123
115
|
}
|
|
124
116
|
channels: {
|
|
125
117
|
files: string[]
|
|
@@ -154,6 +146,8 @@ export interface SerializableInspectorState {
|
|
|
154
146
|
internalFiles: Array<[string, { path: string; exportedName: string }]>
|
|
155
147
|
exposedMeta: InspectorState['rpc']['exposedMeta']
|
|
156
148
|
exposedFiles: Array<[string, { path: string; exportedName: string }]>
|
|
149
|
+
remoteMeta: InspectorState['rpc']['remoteMeta']
|
|
150
|
+
remoteFiles: Array<[string, { path: string; exportedName: string }]>
|
|
157
151
|
invokedFunctions: string[]
|
|
158
152
|
invokedFunctionsByFile?: Array<[string, string[]]>
|
|
159
153
|
usedAddons: string[]
|
|
@@ -163,6 +157,10 @@ export interface SerializableInspectorState {
|
|
|
163
157
|
{
|
|
164
158
|
package: string
|
|
165
159
|
rpcEndpoint?: string
|
|
160
|
+
remote?: boolean
|
|
161
|
+
hasAuth?: boolean
|
|
162
|
+
authCredentialId?: string
|
|
163
|
+
authSecretId?: string
|
|
166
164
|
secretOverrides?: Record<string, string>
|
|
167
165
|
variableOverrides?: Record<string, string>
|
|
168
166
|
credentialOverrides?: Record<string, string>
|
|
@@ -203,6 +201,10 @@ export interface SerializableInspectorState {
|
|
|
203
201
|
definitions: InspectorState['credentials']['definitions']
|
|
204
202
|
files: string[]
|
|
205
203
|
}
|
|
204
|
+
scopes: {
|
|
205
|
+
definitions: InspectorState['scopes']['definitions']
|
|
206
|
+
files: string[]
|
|
207
|
+
}
|
|
206
208
|
variables: {
|
|
207
209
|
definitions: InspectorState['variables']['definitions']
|
|
208
210
|
files: string[]
|
|
@@ -244,17 +246,6 @@ export interface SerializableInspectorState {
|
|
|
244
246
|
permissions: {
|
|
245
247
|
definitions: InspectorState['permissions']['definitions']
|
|
246
248
|
instances: InspectorState['permissions']['instances']
|
|
247
|
-
tagPermissions: Array<
|
|
248
|
-
[
|
|
249
|
-
string,
|
|
250
|
-
InspectorState['permissions']['tagPermissions'] extends Map<
|
|
251
|
-
string,
|
|
252
|
-
infer V
|
|
253
|
-
>
|
|
254
|
-
? V
|
|
255
|
-
: never,
|
|
256
|
-
]
|
|
257
|
-
>
|
|
258
249
|
}
|
|
259
250
|
serviceAggregation: {
|
|
260
251
|
requiredServices: string[]
|
|
@@ -344,7 +335,6 @@ export function serializeInspectorState(
|
|
|
344
335
|
meta: state.http.meta,
|
|
345
336
|
files: Array.from(state.http.files),
|
|
346
337
|
routeMiddleware: Array.from(state.http.routeMiddleware.entries()),
|
|
347
|
-
routePermissions: Array.from(state.http.routePermissions.entries()),
|
|
348
338
|
},
|
|
349
339
|
channels: {
|
|
350
340
|
files: Array.from(state.channels.files),
|
|
@@ -379,6 +369,8 @@ export function serializeInspectorState(
|
|
|
379
369
|
internalFiles: Array.from(state.rpc.internalFiles.entries()),
|
|
380
370
|
exposedMeta: state.rpc.exposedMeta,
|
|
381
371
|
exposedFiles: Array.from(state.rpc.exposedFiles.entries()),
|
|
372
|
+
remoteMeta: state.rpc.remoteMeta,
|
|
373
|
+
remoteFiles: Array.from(state.rpc.remoteFiles.entries()),
|
|
382
374
|
invokedFunctions: Array.from(state.rpc.invokedFunctions),
|
|
383
375
|
invokedFunctionsByFile: Array.from(
|
|
384
376
|
(
|
|
@@ -423,6 +415,10 @@ export function serializeInspectorState(
|
|
|
423
415
|
definitions: state.credentials.definitions,
|
|
424
416
|
files: Array.from(state.credentials.files),
|
|
425
417
|
},
|
|
418
|
+
scopes: {
|
|
419
|
+
definitions: state.scopes.definitions,
|
|
420
|
+
files: Array.from(state.scopes.files),
|
|
421
|
+
},
|
|
426
422
|
variables: {
|
|
427
423
|
definitions: state.variables.definitions,
|
|
428
424
|
files: Array.from(state.variables.files),
|
|
@@ -446,7 +442,6 @@ export function serializeInspectorState(
|
|
|
446
442
|
permissions: {
|
|
447
443
|
definitions: state.permissions.definitions,
|
|
448
444
|
instances: state.permissions.instances,
|
|
449
|
-
tagPermissions: Array.from(state.permissions.tagPermissions.entries()),
|
|
450
445
|
},
|
|
451
446
|
serviceAggregation: {
|
|
452
447
|
requiredServices: Array.from(state.serviceAggregation.requiredServices),
|
|
@@ -536,7 +531,6 @@ export function deserializeInspectorState(
|
|
|
536
531
|
meta: data.http.meta,
|
|
537
532
|
files: new Set(data.http.files),
|
|
538
533
|
routeMiddleware: new Map(data.http.routeMiddleware),
|
|
539
|
-
routePermissions: new Map(data.http.routePermissions),
|
|
540
534
|
},
|
|
541
535
|
channels: {
|
|
542
536
|
files: new Set(data.channels.files),
|
|
@@ -571,6 +565,8 @@ export function deserializeInspectorState(
|
|
|
571
565
|
internalFiles: new Map(data.rpc.internalFiles),
|
|
572
566
|
exposedMeta: data.rpc.exposedMeta,
|
|
573
567
|
exposedFiles: new Map(data.rpc.exposedFiles),
|
|
568
|
+
remoteMeta: data.rpc.remoteMeta ?? {},
|
|
569
|
+
remoteFiles: new Map(data.rpc.remoteFiles ?? []),
|
|
574
570
|
invokedFunctions: new Set(data.rpc.invokedFunctions),
|
|
575
571
|
invokedFunctionsByFile: new Map(
|
|
576
572
|
(data.rpc.invokedFunctionsByFile || []).map(
|
|
@@ -613,6 +609,10 @@ export function deserializeInspectorState(
|
|
|
613
609
|
definitions: data.credentials?.definitions || [],
|
|
614
610
|
files: new Set(data.credentials?.files || []),
|
|
615
611
|
},
|
|
612
|
+
scopes: {
|
|
613
|
+
definitions: data.scopes?.definitions || [],
|
|
614
|
+
files: new Set(data.scopes?.files || []),
|
|
615
|
+
},
|
|
616
616
|
variables: {
|
|
617
617
|
definitions: data.variables?.definitions || [],
|
|
618
618
|
files: new Set(data.variables?.files || []),
|
|
@@ -634,7 +634,6 @@ export function deserializeInspectorState(
|
|
|
634
634
|
permissions: {
|
|
635
635
|
definitions: data.permissions.definitions,
|
|
636
636
|
instances: data.permissions.instances || {},
|
|
637
|
-
tagPermissions: new Map(data.permissions.tagPermissions),
|
|
638
637
|
},
|
|
639
638
|
serviceAggregation: {
|
|
640
639
|
requiredServices: new Set(data.serviceAggregation.requiredServices),
|
|
@@ -654,8 +653,6 @@ export function deserializeInspectorState(
|
|
|
654
653
|
},
|
|
655
654
|
permissionsGroupsMeta: data.permissionsGroupsMeta || {
|
|
656
655
|
definitions: {},
|
|
657
|
-
httpGroups: {},
|
|
658
|
-
tagGroups: {},
|
|
659
656
|
},
|
|
660
657
|
requiredSchemas: new Set(data.requiredSchemas || []),
|
|
661
658
|
openAPISpec: data.openAPISpec || null,
|
|
@@ -1,35 +1,7 @@
|
|
|
1
1
|
import type { InspectorState } from '../types.js'
|
|
2
2
|
|
|
3
3
|
export const serializePermissionsGroupsMeta = (state: InspectorState) => {
|
|
4
|
-
const httpGroups: Record<string, any> = {}
|
|
5
|
-
for (const [pattern, meta] of state.http.routePermissions.entries()) {
|
|
6
|
-
httpGroups[pattern] = {
|
|
7
|
-
exportName: meta.exportName,
|
|
8
|
-
sourceFile: meta.sourceFile,
|
|
9
|
-
position: meta.position,
|
|
10
|
-
services: meta.services,
|
|
11
|
-
count: meta.count,
|
|
12
|
-
instanceIds: meta.instanceIds,
|
|
13
|
-
isFactory: meta.isFactory,
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const tagGroups: Record<string, any> = {}
|
|
18
|
-
for (const [tag, meta] of state.permissions.tagPermissions.entries()) {
|
|
19
|
-
tagGroups[tag] = {
|
|
20
|
-
exportName: meta.exportName,
|
|
21
|
-
sourceFile: meta.sourceFile,
|
|
22
|
-
position: meta.position,
|
|
23
|
-
services: meta.services,
|
|
24
|
-
count: meta.count,
|
|
25
|
-
instanceIds: meta.instanceIds,
|
|
26
|
-
isFactory: meta.isFactory,
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
4
|
return {
|
|
31
5
|
definitions: state.permissions.definitions,
|
|
32
|
-
httpGroups,
|
|
33
|
-
tagGroups,
|
|
34
6
|
}
|
|
35
7
|
}
|
|
@@ -84,7 +84,11 @@ describe('deriveWorkflowPlan', () => {
|
|
|
84
84
|
} as WorkflowStepMeta,
|
|
85
85
|
])
|
|
86
86
|
assert.equal(plan.deterministic, false)
|
|
87
|
-
assert.equal(
|
|
87
|
+
assert.equal(
|
|
88
|
+
plan.plannedSteps,
|
|
89
|
+
undefined,
|
|
90
|
+
'loop count is runtime-dependent'
|
|
91
|
+
)
|
|
88
92
|
})
|
|
89
93
|
|
|
90
94
|
test('a fanout nested inside a branch still suppresses the plan', () => {
|
|
@@ -133,8 +137,42 @@ describe('deriveWorkflowPlan', () => {
|
|
|
133
137
|
{ stepName: 'build' },
|
|
134
138
|
{ stepName: '__workflow_suspend:building', displayName: 'Building' },
|
|
135
139
|
{ stepName: 'publish' },
|
|
136
|
-
{
|
|
137
|
-
|
|
140
|
+
{
|
|
141
|
+
stepName: '__workflow_suspend:awaiting_approval',
|
|
142
|
+
displayName: 'Awaiting approval',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
stepName: '__workflow_suspend:building-image',
|
|
146
|
+
displayName: 'Building image',
|
|
147
|
+
},
|
|
148
|
+
])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('approval steps appear in the plan with __workflow_approval: key and sentence-case displayName', () => {
|
|
152
|
+
const plan = deriveWorkflowPlan([
|
|
153
|
+
rpc('build'),
|
|
154
|
+
{ type: 'approval', reason: 'awaiting_approval' } as WorkflowStepMeta,
|
|
155
|
+
rpc('publish'),
|
|
156
|
+
])
|
|
157
|
+
assert.equal(plan.deterministic, true)
|
|
158
|
+
assert.deepEqual(plan.plannedSteps, [
|
|
159
|
+
{ stepName: 'build' },
|
|
160
|
+
{
|
|
161
|
+
stepName: '__workflow_approval:awaiting_approval',
|
|
162
|
+
displayName: 'Awaiting approval',
|
|
163
|
+
},
|
|
164
|
+
{ stepName: 'publish' },
|
|
165
|
+
])
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('an approval is namespaced apart from a suspend sharing its reason', () => {
|
|
169
|
+
const plan = deriveWorkflowPlan([
|
|
170
|
+
{ type: 'suspend', reason: 'sign_off' } as WorkflowStepMeta,
|
|
171
|
+
{ type: 'approval', reason: 'sign_off' } as WorkflowStepMeta,
|
|
138
172
|
])
|
|
173
|
+
assert.deepEqual(
|
|
174
|
+
plan.plannedSteps.map((s) => s.stepName),
|
|
175
|
+
['__workflow_suspend:sign_off', '__workflow_approval:sign_off']
|
|
176
|
+
)
|
|
139
177
|
})
|
|
140
178
|
})
|