@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
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
import { extractTypeKeys } from './type-utils.js';
|
|
2
4
|
import { ErrorCode } from '../error-codes.js';
|
|
3
5
|
import { AUTH_HANDLER_FUNC_ID } from '../add/add-auth.js';
|
|
6
|
+
import { flattenScopeDefinitions } from '@pikku/core/scope';
|
|
4
7
|
/**
|
|
5
8
|
* Stamp the inspected authorize/callbacks service set onto the generated auth
|
|
6
9
|
* handler's function meta.
|
|
@@ -39,28 +42,22 @@ export function extractWireNames(list) {
|
|
|
39
42
|
.map((item) => item.name);
|
|
40
43
|
}
|
|
41
44
|
/**
|
|
42
|
-
* Helper to expand middleware
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
+
* Helper to expand middleware groups into individual names and add their
|
|
46
|
+
* services to the aggregation. Handles tag-based and HTTP-pattern-based
|
|
47
|
+
* middleware groups. Permissions are function-scoped only and carry no groups.
|
|
45
48
|
*/
|
|
46
|
-
function expandAndAddGroupServices(list, state, addServices
|
|
49
|
+
function expandAndAddGroupServices(list, state, addServices) {
|
|
47
50
|
if (!list)
|
|
48
51
|
return;
|
|
49
52
|
for (const item of list) {
|
|
50
53
|
if (item.type === 'tag') {
|
|
51
|
-
|
|
52
|
-
const groupMeta = isMiddleware
|
|
53
|
-
? state.middleware.tagMiddleware.get(item.tag)
|
|
54
|
-
: state.permissions.tagPermissions.get(item.tag);
|
|
54
|
+
const groupMeta = state.middleware.tagMiddleware.get(item.tag);
|
|
55
55
|
if (groupMeta?.services) {
|
|
56
56
|
addServices(groupMeta.services);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
else if (item.type === 'http' && 'route' in item) {
|
|
60
|
-
|
|
61
|
-
const groupMeta = isMiddleware
|
|
62
|
-
? state.http.routeMiddleware.get(item.route)
|
|
63
|
-
: state.http.routePermissions.get(item.route);
|
|
60
|
+
const groupMeta = state.http.routeMiddleware.get(item.route);
|
|
64
61
|
if (groupMeta?.services) {
|
|
65
62
|
addServices(groupMeta.services);
|
|
66
63
|
}
|
|
@@ -149,34 +146,27 @@ export function aggregateRequiredServices(state) {
|
|
|
149
146
|
'options',
|
|
150
147
|
]) {
|
|
151
148
|
for (const routeMeta of Object.values(state.http.meta[method])) {
|
|
152
|
-
expandAndAddGroupServices(routeMeta.middleware, state, addServices
|
|
153
|
-
expandAndAddGroupServices(routeMeta.permissions, state, addServices, false);
|
|
149
|
+
expandAndAddGroupServices(routeMeta.middleware, state, addServices);
|
|
154
150
|
}
|
|
155
151
|
}
|
|
156
152
|
// Also check other wiring types (channels, queues, schedulers, MCP)
|
|
157
153
|
for (const channelMeta of Object.values(state.channels.meta)) {
|
|
158
|
-
expandAndAddGroupServices(channelMeta.middleware, state, addServices
|
|
159
|
-
expandAndAddGroupServices(channelMeta.permissions, state, addServices, false);
|
|
154
|
+
expandAndAddGroupServices(channelMeta.middleware, state, addServices);
|
|
160
155
|
}
|
|
161
156
|
for (const queueMeta of Object.values(state.queueWorkers.meta)) {
|
|
162
|
-
expandAndAddGroupServices(queueMeta.middleware, state, addServices
|
|
163
|
-
// expandAndAddGroupServices(queueMeta.permissions, state, addServices, false)
|
|
157
|
+
expandAndAddGroupServices(queueMeta.middleware, state, addServices);
|
|
164
158
|
}
|
|
165
159
|
for (const scheduleMeta of Object.values(state.scheduledTasks.meta)) {
|
|
166
|
-
expandAndAddGroupServices(scheduleMeta.middleware, state, addServices
|
|
167
|
-
// expandAndAddGroupServices(scheduleMeta.permissions, state, addServices, false)
|
|
160
|
+
expandAndAddGroupServices(scheduleMeta.middleware, state, addServices);
|
|
168
161
|
}
|
|
169
162
|
for (const toolMeta of Object.values(state.mcpEndpoints.toolsMeta)) {
|
|
170
|
-
expandAndAddGroupServices(toolMeta.middleware, state, addServices
|
|
171
|
-
expandAndAddGroupServices(toolMeta.permissions, state, addServices, false);
|
|
163
|
+
expandAndAddGroupServices(toolMeta.middleware, state, addServices);
|
|
172
164
|
}
|
|
173
165
|
for (const promptMeta of Object.values(state.mcpEndpoints.promptsMeta)) {
|
|
174
|
-
expandAndAddGroupServices(promptMeta.middleware, state, addServices
|
|
175
|
-
expandAndAddGroupServices(promptMeta.permissions, state, addServices, false);
|
|
166
|
+
expandAndAddGroupServices(promptMeta.middleware, state, addServices);
|
|
176
167
|
}
|
|
177
168
|
for (const resourceMeta of Object.values(state.mcpEndpoints.resourcesMeta)) {
|
|
178
|
-
expandAndAddGroupServices(resourceMeta.middleware, state, addServices
|
|
179
|
-
expandAndAddGroupServices(resourceMeta.permissions, state, addServices, false);
|
|
169
|
+
expandAndAddGroupServices(resourceMeta.middleware, state, addServices);
|
|
180
170
|
}
|
|
181
171
|
// 5. Services from session service factories
|
|
182
172
|
for (const singletonServices of state.wireServicesMeta.values()) {
|
|
@@ -299,14 +289,17 @@ export function validateSecretOverrides(logger, state) {
|
|
|
299
289
|
const { wireAddonDeclarations } = state.rpc;
|
|
300
290
|
if (!wireAddonDeclarations || wireAddonDeclarations.size === 0)
|
|
301
291
|
return;
|
|
302
|
-
|
|
292
|
+
// secretOverrides key on (and resolve to) SECRET IDs — the string the addon
|
|
293
|
+
// passes to getSecret — so validate against secretId, falling back to name for
|
|
294
|
+
// older meta without a secretId field.
|
|
295
|
+
const secretIds = new Set(state.secrets.definitions.map((d) => d.secretId ?? d.name));
|
|
303
296
|
for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
|
|
304
297
|
if (!addonDecl.secretOverrides)
|
|
305
298
|
continue;
|
|
306
|
-
for (const
|
|
307
|
-
if (!
|
|
308
|
-
const availableSecrets = Array.from(
|
|
309
|
-
logger.critical(ErrorCode.INVALID_VALUE, `Secret override '${
|
|
299
|
+
for (const [logicalName, resolvedName] of Object.entries(addonDecl.secretOverrides)) {
|
|
300
|
+
if (!secretIds.has(resolvedName)) {
|
|
301
|
+
const availableSecrets = Array.from(secretIds);
|
|
302
|
+
logger.critical(ErrorCode.INVALID_VALUE, `Secret override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a secret that does not exist. Available secrets: ${availableSecrets.join(', ') || 'none'}`);
|
|
310
303
|
}
|
|
311
304
|
}
|
|
312
305
|
}
|
|
@@ -319,10 +312,10 @@ export function validateCredentialOverrides(logger, state) {
|
|
|
319
312
|
for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
|
|
320
313
|
if (!addonDecl.credentialOverrides)
|
|
321
314
|
continue;
|
|
322
|
-
for (const
|
|
323
|
-
if (!credentialNames.has(
|
|
315
|
+
for (const [logicalName, resolvedName] of Object.entries(addonDecl.credentialOverrides)) {
|
|
316
|
+
if (!credentialNames.has(resolvedName)) {
|
|
324
317
|
const availableCredentials = Array.from(credentialNames);
|
|
325
|
-
logger.critical(ErrorCode.INVALID_VALUE, `Credential override '${
|
|
318
|
+
logger.critical(ErrorCode.INVALID_VALUE, `Credential override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a credential that does not exist. Available credentials: ${availableCredentials.join(', ') || 'none'}`);
|
|
326
319
|
}
|
|
327
320
|
}
|
|
328
321
|
}
|
|
@@ -331,18 +324,82 @@ export function validateVariableOverrides(logger, state) {
|
|
|
331
324
|
const { wireAddonDeclarations } = state.rpc;
|
|
332
325
|
if (!wireAddonDeclarations || wireAddonDeclarations.size === 0)
|
|
333
326
|
return;
|
|
334
|
-
|
|
327
|
+
// variableOverrides key on (and resolve to) VARIABLE IDs, so validate against
|
|
328
|
+
// variableId, falling back to name for older meta without a variableId field.
|
|
329
|
+
const variableIds = new Set(state.variables.definitions.map((d) => d.variableId ?? d.name));
|
|
335
330
|
for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
|
|
336
331
|
if (!addonDecl.variableOverrides)
|
|
337
332
|
continue;
|
|
338
|
-
for (const
|
|
339
|
-
if (!
|
|
340
|
-
const availableVariables = Array.from(
|
|
341
|
-
logger.critical(ErrorCode.INVALID_VALUE, `Variable override '${
|
|
333
|
+
for (const [logicalName, resolvedName] of Object.entries(addonDecl.variableOverrides)) {
|
|
334
|
+
if (!variableIds.has(resolvedName)) {
|
|
335
|
+
const availableVariables = Array.from(variableIds);
|
|
336
|
+
logger.critical(ErrorCode.INVALID_VALUE, `Variable override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a variable that does not exist. Available variables: ${availableVariables.join(', ') || 'none'}`);
|
|
342
337
|
}
|
|
343
338
|
}
|
|
344
339
|
}
|
|
345
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* A `wireRemoteAddon` package ships types only — its handlers run on the host —
|
|
343
|
+
* so it MUST be a devDependency, not a production dependency (a prod dep would
|
|
344
|
+
* drag in the runtime deps remote consumption exists to avoid). This is the
|
|
345
|
+
* mirror image of `wireAddon`, which requires a production dependency.
|
|
346
|
+
*/
|
|
347
|
+
export function validateRemoteAddonDependencies(logger, state) {
|
|
348
|
+
const { wireAddonDeclarations } = state.rpc;
|
|
349
|
+
if (!wireAddonDeclarations || wireAddonDeclarations.size === 0)
|
|
350
|
+
return;
|
|
351
|
+
const hasRemote = Array.from(wireAddonDeclarations.values()).some((d) => d.remote);
|
|
352
|
+
if (!hasRemote)
|
|
353
|
+
return;
|
|
354
|
+
const pkgJsonPath = join(state.rootDir, 'package.json');
|
|
355
|
+
if (!existsSync(pkgJsonPath))
|
|
356
|
+
return; // no manifest to check (e.g. some tests)
|
|
357
|
+
let pkgJson;
|
|
358
|
+
try {
|
|
359
|
+
pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
360
|
+
}
|
|
361
|
+
catch (e) {
|
|
362
|
+
logger.warn(`Could not read ${pkgJsonPath} to verify remote addon dependencies: ${e?.message ?? e}`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const prodDeps = pkgJson.dependencies ?? {};
|
|
366
|
+
const devDeps = pkgJson.devDependencies ?? {};
|
|
367
|
+
for (const [namespace, decl] of wireAddonDeclarations.entries()) {
|
|
368
|
+
if (!decl.remote)
|
|
369
|
+
continue;
|
|
370
|
+
if (decl.package in devDeps)
|
|
371
|
+
continue; // correct
|
|
372
|
+
if (decl.package in prodDeps) {
|
|
373
|
+
logger.critical(ErrorCode.REMOTE_ADDON_NOT_DEV_DEPENDENCY, `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".`);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
logger.critical(ErrorCode.REMOTE_ADDON_NOT_DEV_DEPENDENCY, `Remote addon '${namespace}' ('${decl.package}') is wired with wireRemoteAddon but is not in "devDependencies". Add '${decl.package}' to "devDependencies" (types only).`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* The auth a `wireRemoteAddon` consumer binds must reference a slot the consumer
|
|
382
|
+
* actually declares: a `credentialId` must be a wired credential, a `secretId`
|
|
383
|
+
* a wired secret. A custom `resolve()` and a public (omitted) surface are not
|
|
384
|
+
* statically checkable and are left to runtime.
|
|
385
|
+
*/
|
|
386
|
+
export function validateRemoteAddonAuth(logger, state) {
|
|
387
|
+
const { wireAddonDeclarations } = state.rpc;
|
|
388
|
+
if (!wireAddonDeclarations || wireAddonDeclarations.size === 0)
|
|
389
|
+
return;
|
|
390
|
+
const credentialNames = new Set(state.credentials?.definitions.map((d) => d.name) ?? []);
|
|
391
|
+
const secretNames = new Set(state.secrets.definitions.map((d) => d.name));
|
|
392
|
+
for (const [namespace, decl] of wireAddonDeclarations.entries()) {
|
|
393
|
+
if (!decl.remote)
|
|
394
|
+
continue;
|
|
395
|
+
if (decl.authCredentialId && !credentialNames.has(decl.authCredentialId)) {
|
|
396
|
+
logger.critical(ErrorCode.REMOTE_ADDON_AUTH_UNRESOLVED, `Remote addon '${namespace}' binds auth.credentialId '${decl.authCredentialId}', but no such credential is wired. Available credentials: ${Array.from(credentialNames).join(', ') || 'none'}`);
|
|
397
|
+
}
|
|
398
|
+
if (decl.authSecretId && !secretNames.has(decl.authSecretId)) {
|
|
399
|
+
logger.critical(ErrorCode.REMOTE_ADDON_AUTH_UNRESOLVED, `Remote addon '${namespace}' binds auth.secretId '${decl.authSecretId}', but no such secret is wired. Available secrets: ${Array.from(secretNames).join(', ') || 'none'}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
346
403
|
export function computeResolvedIOTypes(state) {
|
|
347
404
|
const { functions } = state;
|
|
348
405
|
for (const [pikkuFuncId, meta] of Object.entries(functions.meta)) {
|
|
@@ -424,34 +481,8 @@ export function computeMiddlewareGroupsMeta(state) {
|
|
|
424
481
|
};
|
|
425
482
|
}
|
|
426
483
|
export function computePermissionsGroupsMeta(state) {
|
|
427
|
-
const httpGroups = {};
|
|
428
|
-
for (const [pattern, meta] of state.http.routePermissions.entries()) {
|
|
429
|
-
httpGroups[pattern] = {
|
|
430
|
-
exportName: meta.exportName,
|
|
431
|
-
sourceFile: meta.sourceFile,
|
|
432
|
-
position: meta.position,
|
|
433
|
-
services: meta.services,
|
|
434
|
-
count: meta.count,
|
|
435
|
-
instanceIds: meta.instanceIds,
|
|
436
|
-
isFactory: meta.isFactory,
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
const tagGroups = {};
|
|
440
|
-
for (const [tag, meta] of state.permissions.tagPermissions.entries()) {
|
|
441
|
-
tagGroups[tag] = {
|
|
442
|
-
exportName: meta.exportName,
|
|
443
|
-
sourceFile: meta.sourceFile,
|
|
444
|
-
position: meta.position,
|
|
445
|
-
services: meta.services,
|
|
446
|
-
count: meta.count,
|
|
447
|
-
instanceIds: meta.instanceIds,
|
|
448
|
-
isFactory: meta.isFactory,
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
484
|
state.permissionsGroupsMeta = {
|
|
452
485
|
definitions: state.permissions.definitions,
|
|
453
|
-
httpGroups,
|
|
454
|
-
tagGroups,
|
|
455
486
|
};
|
|
456
487
|
}
|
|
457
488
|
const PRIMITIVE_TYPES = new Set([
|
|
@@ -508,6 +539,34 @@ export function validateAgentModels(logger, state) {
|
|
|
508
539
|
}
|
|
509
540
|
}
|
|
510
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* A `pikkuWorkflowGraph` node that references the `graph:` namespace (e.g.
|
|
544
|
+
* `graph:editFields`) needs @pikku/addon-graph wired — otherwise the RPC never
|
|
545
|
+
* registers and codegen fails deep in type-checking with an opaque error. Fail
|
|
546
|
+
* early with an actionable message pointing at `scaffold: { graph: true }`.
|
|
547
|
+
*/
|
|
548
|
+
export function validateWorkflowGraphAddons(logger, state) {
|
|
549
|
+
const addonGraphWired = Array.from(state.rpc.wireAddonDeclarations.values()).some((decl) => decl.package === '@pikku/addon-graph');
|
|
550
|
+
if (addonGraphWired) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
for (const [name, graph] of Object.entries(state.workflows.graphMeta)) {
|
|
554
|
+
for (const node of Object.values(graph.nodes)) {
|
|
555
|
+
if (!('rpcName' in node) || typeof node.rpcName !== 'string') {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (!node.rpcName.startsWith('graph:')) {
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (state.functions.meta[node.rpcName]) {
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
logger.critical(ErrorCode.WORKFLOW_GRAPH_ADDON_NOT_WIRED, `Workflow graph '${name}' references '${node.rpcName}' but @pikku/addon-graph is not wired. ` +
|
|
565
|
+
`Enable "scaffold": { "graph": true } in pikku.config.json (and install @pikku/addon-graph), ` +
|
|
566
|
+
`or wire it manually with wireAddon({ name: 'graph', package: '@pikku/addon-graph' }).`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
511
570
|
/**
|
|
512
571
|
* Validates that Zod schemas and wiring side-effects (wireHTTPRoutes,
|
|
513
572
|
* addPermission, addHTTPMiddleware, etc.) do not coexist in the same file.
|
|
@@ -528,13 +587,6 @@ export function validateSchemaWiringSeparation(logger, state) {
|
|
|
528
587
|
for (const file of state.http.files) {
|
|
529
588
|
wiringFiles.add(file);
|
|
530
589
|
}
|
|
531
|
-
// Permission wirings (addPermission calls)
|
|
532
|
-
for (const meta of state.permissions.tagPermissions.values()) {
|
|
533
|
-
wiringFiles.add(meta.sourceFile);
|
|
534
|
-
}
|
|
535
|
-
for (const meta of state.http.routePermissions.values()) {
|
|
536
|
-
wiringFiles.add(meta.sourceFile);
|
|
537
|
-
}
|
|
538
590
|
// Middleware wirings (addHTTPMiddleware calls)
|
|
539
591
|
for (const meta of state.http.routeMiddleware.values()) {
|
|
540
592
|
wiringFiles.add(meta.sourceFile);
|
|
@@ -628,3 +680,29 @@ export function computeDiagnostics(state) {
|
|
|
628
680
|
}
|
|
629
681
|
state.diagnostics = diagnostics;
|
|
630
682
|
}
|
|
683
|
+
/**
|
|
684
|
+
* Validates that every scope referenced by a function is declared via
|
|
685
|
+
* `wireScope`. Runs after all visitors, so declaration order does not matter.
|
|
686
|
+
*
|
|
687
|
+
* A `*` suffix is a wildcard requirement: `admin:*` requires the `admin` scope
|
|
688
|
+
* to be declared, and grants its whole subtree.
|
|
689
|
+
*/
|
|
690
|
+
export function validateScopeReferences(logger, state) {
|
|
691
|
+
const declared = new Set(flattenScopeDefinitions(state.scopes.definitions).map((s) => s.id));
|
|
692
|
+
for (const [funcName, meta] of Object.entries(state.functions.meta)) {
|
|
693
|
+
if (!meta.scopes?.length)
|
|
694
|
+
continue;
|
|
695
|
+
for (const scope of meta.scopes) {
|
|
696
|
+
// A trailing wildcard grants a subtree; the node it hangs off must exist.
|
|
697
|
+
const declaredForm = scope.endsWith(':*') ? scope.slice(0, -2) : scope;
|
|
698
|
+
if (scope === '*') {
|
|
699
|
+
logger.critical(ErrorCode.INVALID_VALUE, `Function '${funcName}' requires the bare wildcard scope '*'. A function must require a specific scope — '*' is only meaningful as a grant.`);
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (!declared.has(declaredForm)) {
|
|
703
|
+
const available = Array.from(declared);
|
|
704
|
+
logger.critical(ErrorCode.INVALID_VALUE, `Function '${funcName}' requires scope '${scope}' which is not declared. Declare it with wireScope. Available scopes: ${available.join(', ') || 'none'}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
@@ -127,10 +127,6 @@ export interface SerializableInspectorState {
|
|
|
127
127
|
string,
|
|
128
128
|
InspectorState['http']['routeMiddleware'] extends Map<string, infer V> ? V : never
|
|
129
129
|
]>;
|
|
130
|
-
routePermissions: Array<[
|
|
131
|
-
string,
|
|
132
|
-
InspectorState['http']['routePermissions'] extends Map<string, infer V> ? V : never
|
|
133
|
-
]>;
|
|
134
130
|
};
|
|
135
131
|
channels: {
|
|
136
132
|
files: string[];
|
|
@@ -177,6 +173,11 @@ export interface SerializableInspectorState {
|
|
|
177
173
|
path: string;
|
|
178
174
|
exportedName: string;
|
|
179
175
|
}]>;
|
|
176
|
+
remoteMeta: InspectorState['rpc']['remoteMeta'];
|
|
177
|
+
remoteFiles: Array<[string, {
|
|
178
|
+
path: string;
|
|
179
|
+
exportedName: string;
|
|
180
|
+
}]>;
|
|
180
181
|
invokedFunctions: string[];
|
|
181
182
|
invokedFunctionsByFile?: Array<[string, string[]]>;
|
|
182
183
|
usedAddons: string[];
|
|
@@ -185,6 +186,10 @@ export interface SerializableInspectorState {
|
|
|
185
186
|
{
|
|
186
187
|
package: string;
|
|
187
188
|
rpcEndpoint?: string;
|
|
189
|
+
remote?: boolean;
|
|
190
|
+
hasAuth?: boolean;
|
|
191
|
+
authCredentialId?: string;
|
|
192
|
+
authSecretId?: string;
|
|
188
193
|
secretOverrides?: Record<string, string>;
|
|
189
194
|
variableOverrides?: Record<string, string>;
|
|
190
195
|
credentialOverrides?: Record<string, string>;
|
|
@@ -227,6 +232,10 @@ export interface SerializableInspectorState {
|
|
|
227
232
|
definitions: InspectorState['credentials']['definitions'];
|
|
228
233
|
files: string[];
|
|
229
234
|
};
|
|
235
|
+
scopes: {
|
|
236
|
+
definitions: InspectorState['scopes']['definitions'];
|
|
237
|
+
files: string[];
|
|
238
|
+
};
|
|
230
239
|
variables: {
|
|
231
240
|
definitions: InspectorState['variables']['definitions'];
|
|
232
241
|
files: string[];
|
|
@@ -254,10 +263,6 @@ export interface SerializableInspectorState {
|
|
|
254
263
|
permissions: {
|
|
255
264
|
definitions: InspectorState['permissions']['definitions'];
|
|
256
265
|
instances: InspectorState['permissions']['instances'];
|
|
257
|
-
tagPermissions: Array<[
|
|
258
|
-
string,
|
|
259
|
-
InspectorState['permissions']['tagPermissions'] extends Map<string, infer V> ? V : never
|
|
260
|
-
]>;
|
|
261
266
|
};
|
|
262
267
|
serviceAggregation: {
|
|
263
268
|
requiredServices: string[];
|
|
@@ -42,7 +42,6 @@ export function serializeInspectorState(state) {
|
|
|
42
42
|
meta: state.http.meta,
|
|
43
43
|
files: Array.from(state.http.files),
|
|
44
44
|
routeMiddleware: Array.from(state.http.routeMiddleware.entries()),
|
|
45
|
-
routePermissions: Array.from(state.http.routePermissions.entries()),
|
|
46
45
|
},
|
|
47
46
|
channels: {
|
|
48
47
|
files: Array.from(state.channels.files),
|
|
@@ -77,6 +76,8 @@ export function serializeInspectorState(state) {
|
|
|
77
76
|
internalFiles: Array.from(state.rpc.internalFiles.entries()),
|
|
78
77
|
exposedMeta: state.rpc.exposedMeta,
|
|
79
78
|
exposedFiles: Array.from(state.rpc.exposedFiles.entries()),
|
|
79
|
+
remoteMeta: state.rpc.remoteMeta,
|
|
80
|
+
remoteFiles: Array.from(state.rpc.remoteFiles.entries()),
|
|
80
81
|
invokedFunctions: Array.from(state.rpc.invokedFunctions),
|
|
81
82
|
invokedFunctionsByFile: Array.from((state.rpc.invokedFunctionsByFile ?? new Map()).entries()).map(([file, fns]) => [file, Array.from(fns)]),
|
|
82
83
|
usedAddons: Array.from(state.rpc.usedAddons),
|
|
@@ -115,6 +116,10 @@ export function serializeInspectorState(state) {
|
|
|
115
116
|
definitions: state.credentials.definitions,
|
|
116
117
|
files: Array.from(state.credentials.files),
|
|
117
118
|
},
|
|
119
|
+
scopes: {
|
|
120
|
+
definitions: state.scopes.definitions,
|
|
121
|
+
files: Array.from(state.scopes.files),
|
|
122
|
+
},
|
|
118
123
|
variables: {
|
|
119
124
|
definitions: state.variables.definitions,
|
|
120
125
|
files: Array.from(state.variables.files),
|
|
@@ -136,7 +141,6 @@ export function serializeInspectorState(state) {
|
|
|
136
141
|
permissions: {
|
|
137
142
|
definitions: state.permissions.definitions,
|
|
138
143
|
instances: state.permissions.instances,
|
|
139
|
-
tagPermissions: Array.from(state.permissions.tagPermissions.entries()),
|
|
140
144
|
},
|
|
141
145
|
serviceAggregation: {
|
|
142
146
|
requiredServices: Array.from(state.serviceAggregation.requiredServices),
|
|
@@ -201,7 +205,6 @@ export function deserializeInspectorState(data) {
|
|
|
201
205
|
meta: data.http.meta,
|
|
202
206
|
files: new Set(data.http.files),
|
|
203
207
|
routeMiddleware: new Map(data.http.routeMiddleware),
|
|
204
|
-
routePermissions: new Map(data.http.routePermissions),
|
|
205
208
|
},
|
|
206
209
|
channels: {
|
|
207
210
|
files: new Set(data.channels.files),
|
|
@@ -236,6 +239,8 @@ export function deserializeInspectorState(data) {
|
|
|
236
239
|
internalFiles: new Map(data.rpc.internalFiles),
|
|
237
240
|
exposedMeta: data.rpc.exposedMeta,
|
|
238
241
|
exposedFiles: new Map(data.rpc.exposedFiles),
|
|
242
|
+
remoteMeta: data.rpc.remoteMeta ?? {},
|
|
243
|
+
remoteFiles: new Map(data.rpc.remoteFiles ?? []),
|
|
239
244
|
invokedFunctions: new Set(data.rpc.invokedFunctions),
|
|
240
245
|
invokedFunctionsByFile: new Map((data.rpc.invokedFunctionsByFile || []).map(([file, fns]) => [file, new Set(fns)])),
|
|
241
246
|
usedAddons: new Set(data.rpc.usedAddons || []),
|
|
@@ -274,6 +279,10 @@ export function deserializeInspectorState(data) {
|
|
|
274
279
|
definitions: data.credentials?.definitions || [],
|
|
275
280
|
files: new Set(data.credentials?.files || []),
|
|
276
281
|
},
|
|
282
|
+
scopes: {
|
|
283
|
+
definitions: data.scopes?.definitions || [],
|
|
284
|
+
files: new Set(data.scopes?.files || []),
|
|
285
|
+
},
|
|
277
286
|
variables: {
|
|
278
287
|
definitions: data.variables?.definitions || [],
|
|
279
288
|
files: new Set(data.variables?.files || []),
|
|
@@ -295,7 +304,6 @@ export function deserializeInspectorState(data) {
|
|
|
295
304
|
permissions: {
|
|
296
305
|
definitions: data.permissions.definitions,
|
|
297
306
|
instances: data.permissions.instances || {},
|
|
298
|
-
tagPermissions: new Map(data.permissions.tagPermissions),
|
|
299
307
|
},
|
|
300
308
|
serviceAggregation: {
|
|
301
309
|
requiredServices: new Set(data.serviceAggregation.requiredServices),
|
|
@@ -315,8 +323,6 @@ export function deserializeInspectorState(data) {
|
|
|
315
323
|
},
|
|
316
324
|
permissionsGroupsMeta: data.permissionsGroupsMeta || {
|
|
317
325
|
definitions: {},
|
|
318
|
-
httpGroups: {},
|
|
319
|
-
tagGroups: {},
|
|
320
326
|
},
|
|
321
327
|
requiredSchemas: new Set(data.requiredSchemas || []),
|
|
322
328
|
openAPISpec: data.openAPISpec || null,
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { InspectorState } from '../types.js';
|
|
2
2
|
export declare const serializePermissionsGroupsMeta: (state: InspectorState) => {
|
|
3
3
|
definitions: Record<string, import("../types.js").InspectorPermissionDefinition>;
|
|
4
|
-
httpGroups: Record<string, any>;
|
|
5
|
-
tagGroups: Record<string, any>;
|
|
6
4
|
};
|
|
@@ -1,31 +1,5 @@
|
|
|
1
1
|
export const serializePermissionsGroupsMeta = (state) => {
|
|
2
|
-
const httpGroups = {};
|
|
3
|
-
for (const [pattern, meta] of state.http.routePermissions.entries()) {
|
|
4
|
-
httpGroups[pattern] = {
|
|
5
|
-
exportName: meta.exportName,
|
|
6
|
-
sourceFile: meta.sourceFile,
|
|
7
|
-
position: meta.position,
|
|
8
|
-
services: meta.services,
|
|
9
|
-
count: meta.count,
|
|
10
|
-
instanceIds: meta.instanceIds,
|
|
11
|
-
isFactory: meta.isFactory,
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
const tagGroups = {};
|
|
15
|
-
for (const [tag, meta] of state.permissions.tagPermissions.entries()) {
|
|
16
|
-
tagGroups[tag] = {
|
|
17
|
-
exportName: meta.exportName,
|
|
18
|
-
sourceFile: meta.sourceFile,
|
|
19
|
-
position: meta.position,
|
|
20
|
-
services: meta.services,
|
|
21
|
-
count: meta.count,
|
|
22
|
-
instanceIds: meta.instanceIds,
|
|
23
|
-
isFactory: meta.isFactory,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
2
|
return {
|
|
27
3
|
definitions: state.permissions.definitions,
|
|
28
|
-
httpGroups,
|
|
29
|
-
tagGroups,
|
|
30
4
|
};
|
|
31
5
|
};
|
|
@@ -48,9 +48,7 @@ function containsConditional(steps) {
|
|
|
48
48
|
* e.g. "awaiting_approval" → "Awaiting approval", "building-image" → "Building image"
|
|
49
49
|
*/
|
|
50
50
|
function reasonToLabel(reason) {
|
|
51
|
-
return reason
|
|
52
|
-
.replace(/[-_]/g, ' ')
|
|
53
|
-
.replace(/^(.)/, (c) => c.toUpperCase());
|
|
51
|
+
return reason.replace(/[-_]/g, ' ').replace(/^(.)/, (c) => c.toUpperCase());
|
|
54
52
|
}
|
|
55
53
|
/** Flatten named steps (rpc/inline/sleep/parallel children) in source order. */
|
|
56
54
|
function collectNamedSteps(steps) {
|
|
@@ -72,6 +70,14 @@ function collectNamedSteps(steps) {
|
|
|
72
70
|
displayName: reasonToLabel(step.reason),
|
|
73
71
|
});
|
|
74
72
|
break;
|
|
73
|
+
case 'approval':
|
|
74
|
+
// Same as suspend, but namespaced `__workflow_approval:` to match the
|
|
75
|
+
// runtime's separate step key.
|
|
76
|
+
planned.push({
|
|
77
|
+
stepName: `__workflow_approval:${step.reason}`,
|
|
78
|
+
displayName: reasonToLabel(step.reason),
|
|
79
|
+
});
|
|
80
|
+
break;
|
|
75
81
|
case 'parallel':
|
|
76
82
|
for (const child of step.children) {
|
|
77
83
|
planned.push({ stepName: child.stepName });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { isWorkflowDoCall, isWorkflowExpectEventuallyCall, extractActorFromOptions, isWorkflowSleepCall, isWorkflowSuspendCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
|
|
2
|
+
import { isWorkflowDoCall, isWorkflowExpectEventuallyCall, extractActorFromOptions, isWorkflowSleepCall, isWorkflowSuspendCall, isWorkflowApprovalCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
|
|
3
3
|
import { validateNoDisallowedPatterns, validateAwaitedCalls, formatValidationErrors, } from './validation.js';
|
|
4
4
|
import { extractStringLiteral, extractNumberLiteral, } from '../../extract-node-value.js';
|
|
5
5
|
/**
|
|
@@ -363,6 +363,19 @@ function extractExpressionStatement(statement, context) {
|
|
|
363
363
|
if (isWorkflowSuspendCall(call, context.checker)) {
|
|
364
364
|
return extractSuspendStep(call, context);
|
|
365
365
|
}
|
|
366
|
+
if (isWorkflowApprovalCall(call, context.checker)) {
|
|
367
|
+
const step = extractApprovalStep(call, context, outputVar);
|
|
368
|
+
// Unlike suspend, an approval yields a value, so a downstream step can
|
|
369
|
+
// reference it — track the binding the same way workflow.do() does.
|
|
370
|
+
if (outputVar && step) {
|
|
371
|
+
const type = context.checker.getTypeAtLocation(expr);
|
|
372
|
+
context.outputVars.set(outputVar, { type, node: expr });
|
|
373
|
+
if (isArrayType(type, context.checker)) {
|
|
374
|
+
context.arrayVars.add(outputVar);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return step;
|
|
378
|
+
}
|
|
366
379
|
// Check for parallel group or fanout
|
|
367
380
|
if (isParallelFanout(call)) {
|
|
368
381
|
return extractParallelFanout(call, context);
|
|
@@ -543,6 +556,48 @@ function extractSuspendStep(call, context) {
|
|
|
543
556
|
return null;
|
|
544
557
|
}
|
|
545
558
|
}
|
|
559
|
+
/**
|
|
560
|
+
* Extract approval step from workflow.approval() call
|
|
561
|
+
*/
|
|
562
|
+
function extractApprovalStep(call, context, outputVar) {
|
|
563
|
+
const args = call.arguments;
|
|
564
|
+
if (args.length < 1)
|
|
565
|
+
return null;
|
|
566
|
+
try {
|
|
567
|
+
const reason = extractStringLiteral(args[0], context.checker);
|
|
568
|
+
const step = {
|
|
569
|
+
type: 'approval',
|
|
570
|
+
reason,
|
|
571
|
+
};
|
|
572
|
+
if (outputVar) {
|
|
573
|
+
step.outputVar = outputVar;
|
|
574
|
+
}
|
|
575
|
+
// The `schema` option is a runtime value validated inside the workflow body,
|
|
576
|
+
// so it is deliberately not serialized here — only `expiry`, which the graph
|
|
577
|
+
// and planned-step ladder need in order to describe the gate.
|
|
578
|
+
const options = args[1];
|
|
579
|
+
if (options && ts.isObjectLiteralExpression(options)) {
|
|
580
|
+
for (const prop of options.properties) {
|
|
581
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
582
|
+
prop.name.getText() === 'expiry' &&
|
|
583
|
+
(ts.isStringLiteral(prop.initializer) ||
|
|
584
|
+
ts.isNumericLiteral(prop.initializer))) {
|
|
585
|
+
step.expiry = ts.isNumericLiteral(prop.initializer)
|
|
586
|
+
? Number(prop.initializer.text)
|
|
587
|
+
: prop.initializer.text;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return step;
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
context.errors.push({
|
|
595
|
+
message: `Failed to extract approval step: ${error instanceof Error ? error.message : String(error)}`,
|
|
596
|
+
node: call,
|
|
597
|
+
});
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
546
601
|
/**
|
|
547
602
|
* Extract cancel step from throw WorkflowCancelledException statement
|
|
548
603
|
*/
|
|
@@ -2,6 +2,12 @@ import * as ts from 'typescript';
|
|
|
2
2
|
/**
|
|
3
3
|
* Pattern detection helpers for DSL workflow extraction
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* The wire object handed to a DSL func is named `workflow` in a workflow and
|
|
7
|
+
* `scenario` in a scenario — both carry the same `do`/`sleep`/`suspend` DSL, so
|
|
8
|
+
* step extraction accepts either identifier.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isWorkflowWireIdentifier(expr: ts.Expression): boolean;
|
|
5
11
|
/**
|
|
6
12
|
* Check if a call expression is workflow.do() or workflow.expectEventually()
|
|
7
13
|
* (both are RPC steps; expectEventually is the polling variant used by scenarios)
|
|
@@ -25,6 +31,10 @@ export declare function isWorkflowSleepCall(node: ts.CallExpression, checker: ts
|
|
|
25
31
|
* Check if a call expression is workflow.suspend()
|
|
26
32
|
*/
|
|
27
33
|
export declare function isWorkflowSuspendCall(node: ts.CallExpression, _checker: ts.TypeChecker): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Check if a call expression is workflow.approval()
|
|
36
|
+
*/
|
|
37
|
+
export declare function isWorkflowApprovalCall(node: ts.CallExpression, _checker: ts.TypeChecker): boolean;
|
|
28
38
|
/**
|
|
29
39
|
* Check if a throw statement throws WorkflowCancelledException
|
|
30
40
|
* Matches: throw new WorkflowCancelledException(...) or throw WorkflowCancelledException(...)
|