@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.
Files changed (108) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/add/add-addon-bans.js +1 -0
  3. package/dist/add/add-ai-agent.d.ts +3 -1
  4. package/dist/add/add-ai-agent.js +82 -0
  5. package/dist/add/add-credential.js +3 -1
  6. package/dist/add/add-functions.js +10 -9
  7. package/dist/add/add-gateway.js +3 -0
  8. package/dist/add/add-http-route.js +2 -2
  9. package/dist/add/add-mcp-prompt.js +0 -1
  10. package/dist/add/add-mcp-resource.js +0 -1
  11. package/dist/add/add-permission.d.ts +1 -1
  12. package/dist/add/add-permission.js +3 -177
  13. package/dist/add/add-queue-worker.js +3 -0
  14. package/dist/add/add-schedule.js +3 -0
  15. package/dist/add/add-scope.d.ts +2 -0
  16. package/dist/add/add-scope.js +146 -0
  17. package/dist/add/add-trigger.js +3 -0
  18. package/dist/add/add-wire-remote-addon.d.ts +10 -0
  19. package/dist/add/add-wire-remote-addon.js +66 -0
  20. package/dist/add/add-workflow-graph.js +32 -3
  21. package/dist/error-codes.d.ts +3 -0
  22. package/dist/error-codes.js +4 -0
  23. package/dist/inspector.js +19 -7
  24. package/dist/types.d.ts +19 -13
  25. package/dist/utils/custom-types-generator.js +2 -1
  26. package/dist/utils/ensure-function-metadata.d.ts +15 -0
  27. package/dist/utils/ensure-function-metadata.js +24 -0
  28. package/dist/utils/filter-inspector-state.js +9 -0
  29. package/dist/utils/get-property-value.d.ts +7 -2
  30. package/dist/utils/get-property-value.js +48 -1
  31. package/dist/utils/load-addon-functions-meta.js +81 -6
  32. package/dist/utils/nested-project-filter.d.ts +1 -0
  33. package/dist/utils/nested-project-filter.js +29 -0
  34. package/dist/utils/permissions.d.ts +1 -21
  35. package/dist/utils/permissions.js +17 -108
  36. package/dist/utils/post-process.d.ts +30 -1
  37. package/dist/utils/post-process.js +150 -72
  38. package/dist/utils/serialize-inspector-state.d.ts +13 -8
  39. package/dist/utils/serialize-inspector-state.js +12 -6
  40. package/dist/utils/serialize-permissions-groups-meta.d.ts +0 -2
  41. package/dist/utils/serialize-permissions-groups-meta.js +0 -26
  42. package/dist/utils/workflow/derive-workflow-plan.js +9 -3
  43. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +56 -1
  44. package/dist/utils/workflow/dsl/patterns.d.ts +10 -0
  45. package/dist/utils/workflow/dsl/patterns.js +24 -8
  46. package/dist/utils/workflow/dsl/validation.js +3 -4
  47. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +14 -0
  48. package/dist/utils/workflow/graph/finalize-workflows.d.ts +7 -0
  49. package/dist/utils/workflow/graph/finalize-workflows.js +11 -2
  50. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +2 -0
  51. package/dist/utils/workflow/graph/serialize-workflow-graph.js +4 -0
  52. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +6 -2
  53. package/dist/visit.js +4 -0
  54. package/package.json +3 -3
  55. package/src/add/add-addon-bans.ts +1 -0
  56. package/src/add/add-ai-agent.test.ts +87 -0
  57. package/src/add/add-ai-agent.ts +122 -0
  58. package/src/add/add-credential.ts +6 -0
  59. package/src/add/add-functions.ts +10 -12
  60. package/src/add/add-gateway.ts +11 -0
  61. package/src/add/add-http-route.ts +2 -8
  62. package/src/add/add-mcp-prompt.ts +0 -1
  63. package/src/add/add-mcp-resource.ts +0 -1
  64. package/src/add/add-permission.ts +4 -242
  65. package/src/add/add-queue-worker.ts +11 -0
  66. package/src/add/add-schedule.ts +11 -0
  67. package/src/add/add-scope.test.ts +346 -0
  68. package/src/add/add-scope.ts +225 -0
  69. package/src/add/add-trigger.ts +11 -0
  70. package/src/add/add-wire-remote-addon.ts +77 -0
  71. package/src/add/add-workflow-graph-input.test.ts +94 -0
  72. package/src/add/add-workflow-graph-notes.test.ts +74 -0
  73. package/src/add/add-workflow-graph.ts +35 -3
  74. package/src/add/inline-wiring-function.test.ts +104 -0
  75. package/src/add/validate-workflow-graph-addons.test.ts +102 -0
  76. package/src/error-codes.ts +5 -0
  77. package/src/inspector.ts +22 -6
  78. package/src/types.ts +16 -17
  79. package/src/utils/custom-types-generator.test.ts +26 -1
  80. package/src/utils/custom-types-generator.ts +2 -1
  81. package/src/utils/ensure-function-metadata.ts +42 -0
  82. package/src/utils/filter-inspector-state.test.ts +0 -2
  83. package/src/utils/filter-inspector-state.ts +9 -0
  84. package/src/utils/get-property-value.test.ts +141 -0
  85. package/src/utils/get-property-value.ts +62 -1
  86. package/src/utils/load-addon-functions-meta.test.ts +157 -0
  87. package/src/utils/load-addon-functions-meta.ts +94 -6
  88. package/src/utils/load-addon-scopes.test.ts +138 -0
  89. package/src/utils/nested-project-filter.test.ts +53 -0
  90. package/src/utils/nested-project-filter.ts +28 -0
  91. package/src/utils/permissions.test.ts +7 -232
  92. package/src/utils/permissions.ts +20 -160
  93. package/src/utils/post-process.test.ts +269 -4
  94. package/src/utils/post-process.ts +216 -95
  95. package/src/utils/serialize-inspector-state.ts +22 -25
  96. package/src/utils/serialize-permissions-groups-meta.ts +0 -28
  97. package/src/utils/workflow/derive-workflow-plan.test.ts +41 -3
  98. package/src/utils/workflow/derive-workflow-plan.ts +11 -4
  99. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +66 -0
  100. package/src/utils/workflow/dsl/patterns.ts +34 -8
  101. package/src/utils/workflow/dsl/validation.ts +3 -4
  102. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +15 -0
  103. package/src/utils/workflow/graph/finalize-workflows.test.ts +50 -0
  104. package/src/utils/workflow/graph/finalize-workflows.ts +13 -2
  105. package/src/utils/workflow/graph/serialize-workflow-graph.ts +6 -0
  106. package/src/utils/workflow/graph/workflow-graph.types.ts +8 -0
  107. package/src/visit.ts +4 -0
  108. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,146 @@
1
+ import * as ts from 'typescript';
2
+ import { getPropertyValue } from '../utils/get-property-value.js';
3
+ import { ErrorCode } from '../error-codes.js';
4
+ const SEPARATOR = ':';
5
+ const WILDCARD = '*';
6
+ /**
7
+ * Validates a single scope segment, reporting rather than throwing so the
8
+ * inspector can surface every problem in one pass.
9
+ *
10
+ * @returns true when the segment is usable.
11
+ */
12
+ const isValidSegment = (segment, scopeName, logger) => {
13
+ if (segment.length === 0) {
14
+ logger.critical(ErrorCode.INVALID_VALUE, `Scope '${scopeName}' contains an empty segment.`);
15
+ return false;
16
+ }
17
+ if (segment.includes(SEPARATOR)) {
18
+ logger.critical(ErrorCode.INVALID_VALUE, `Scope segment '${segment}' in '${scopeName}' contains the '${SEPARATOR}' separator. ` +
19
+ `Nest scopes with the 'scopes' property instead of embedding '${SEPARATOR}' in a name.`);
20
+ return false;
21
+ }
22
+ if (segment === WILDCARD) {
23
+ logger.critical(ErrorCode.INVALID_VALUE, `Scope segment '${segment}' in '${scopeName}' is the wildcard. ` +
24
+ `'${WILDCARD}' is reserved for granting a scope and its descendants, and cannot be declared.`);
25
+ return false;
26
+ }
27
+ return true;
28
+ };
29
+ /**
30
+ * Recursively extracts a `scopes: { ... }` object literal into nested metadata.
31
+ */
32
+ const extractScopeNodes = (obj, scopeName, logger) => {
33
+ const nodes = {};
34
+ for (const prop of obj.properties) {
35
+ if (!ts.isPropertyAssignment(prop)) {
36
+ continue;
37
+ }
38
+ let segment;
39
+ if (ts.isIdentifier(prop.name)) {
40
+ segment = prop.name.text;
41
+ }
42
+ else if (ts.isStringLiteral(prop.name)) {
43
+ segment = prop.name.text;
44
+ }
45
+ if (segment === undefined) {
46
+ logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, `Scope '${scopeName}' has a nested scope whose key is not a literal.`);
47
+ continue;
48
+ }
49
+ if (!isValidSegment(segment, scopeName, logger)) {
50
+ continue;
51
+ }
52
+ if (!ts.isObjectLiteralExpression(prop.initializer)) {
53
+ logger.critical(ErrorCode.INVALID_VALUE, `Nested scope '${segment}' in '${scopeName}' must be an object literal.`);
54
+ continue;
55
+ }
56
+ const node = {};
57
+ const displayName = getPropertyValue(prop.initializer, 'displayName');
58
+ if (displayName) {
59
+ node.displayName = displayName;
60
+ }
61
+ const description = getPropertyValue(prop.initializer, 'description');
62
+ if (description) {
63
+ node.description = description;
64
+ }
65
+ const nestedProp = prop.initializer.properties.find((p) => ts.isPropertyAssignment(p) &&
66
+ ts.isIdentifier(p.name) &&
67
+ p.name.text === 'scopes');
68
+ if (nestedProp &&
69
+ ts.isPropertyAssignment(nestedProp) &&
70
+ ts.isObjectLiteralExpression(nestedProp.initializer)) {
71
+ const nested = extractScopeNodes(nestedProp.initializer, scopeName, logger);
72
+ if (nested) {
73
+ node.scopes = nested;
74
+ }
75
+ }
76
+ nodes[segment] = node;
77
+ }
78
+ return Object.keys(nodes).length > 0 ? nodes : undefined;
79
+ };
80
+ /**
81
+ * Unwraps `x as const` / `x as any` so a cast declaration is still extracted
82
+ * rather than silently skipped.
83
+ */
84
+ const unwrapAs = (node) => ts.isAsExpression(node) || ts.isSatisfiesExpression(node)
85
+ ? unwrapAs(node.expression)
86
+ : node;
87
+ export const addScope = (logger, node, checker, state, _options) => {
88
+ if (!ts.isCallExpression(node)) {
89
+ return;
90
+ }
91
+ const expression = node.expression;
92
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireScope') {
93
+ return;
94
+ }
95
+ const firstArg = node.arguments[0];
96
+ if (!firstArg) {
97
+ return;
98
+ }
99
+ const unwrapped = unwrapAs(firstArg);
100
+ if (!ts.isObjectLiteralExpression(unwrapped)) {
101
+ return;
102
+ }
103
+ const sourceFile = node.getSourceFile().fileName;
104
+ // Roots are keyed exactly like the nodes beneath them, so each property of
105
+ // the call's single argument is one tree.
106
+ for (const prop of unwrapped.properties) {
107
+ if (!ts.isPropertyAssignment(prop)) {
108
+ continue;
109
+ }
110
+ let name;
111
+ if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) {
112
+ name = prop.name.text;
113
+ }
114
+ if (name === undefined) {
115
+ logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, 'A scope is declared with a key that is not a literal.');
116
+ continue;
117
+ }
118
+ if (!isValidSegment(name, name, logger)) {
119
+ continue;
120
+ }
121
+ if (!ts.isObjectLiteralExpression(prop.initializer)) {
122
+ logger.critical(ErrorCode.INVALID_VALUE, `Scope '${name}' must be an object literal.`);
123
+ continue;
124
+ }
125
+ const root = prop.initializer;
126
+ const displayNameValue = getPropertyValue(root, 'displayName');
127
+ const descriptionValue = getPropertyValue(root, 'description');
128
+ let scopes;
129
+ const scopesProp = root.properties.find((p) => ts.isPropertyAssignment(p) &&
130
+ ts.isIdentifier(p.name) &&
131
+ p.name.text === 'scopes');
132
+ if (scopesProp &&
133
+ ts.isPropertyAssignment(scopesProp) &&
134
+ ts.isObjectLiteralExpression(scopesProp.initializer)) {
135
+ scopes = extractScopeNodes(scopesProp.initializer, name, logger);
136
+ }
137
+ state.scopes.files.add(sourceFile);
138
+ state.scopes.definitions.push({
139
+ name,
140
+ displayName: displayNameValue || undefined,
141
+ description: descriptionValue || undefined,
142
+ scopes,
143
+ sourceFile,
144
+ });
145
+ }
146
+ };
@@ -2,6 +2,7 @@ import * as ts from 'typescript';
2
2
  import { getPropertyValue, getCommonWireMetaData, } from '../utils/get-property-value.js';
3
3
  import { extractFunctionName, makeContextBasedId, } from '../utils/extract-function-name.js';
4
4
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
5
+ import { ensureInlineWiringFunction } from '../utils/ensure-function-metadata.js';
5
6
  import { resolveMiddleware } from '../utils/middleware.js';
6
7
  import { extractWireNames } from '../utils/post-process.js';
7
8
  import { resolveAddonName } from '../utils/resolve-addon-package.js';
@@ -45,6 +46,8 @@ const addWireTrigger = (logger, node, checker, state, firstArg) => {
45
46
  if (!nameValue) {
46
47
  return;
47
48
  }
49
+ // Register metadata for a func inlined into the wiring (see helper).
50
+ ensureInlineWiringFunction(state, pikkuFuncId, nameValue, funcInitializer, checker, extracted.isHelper);
48
51
  // --- resolve middleware ---
49
52
  const middleware = resolveMiddleware(state, obj, tags, checker);
50
53
  // --- track used functions/middleware for service aggregation ---
@@ -0,0 +1,10 @@
1
+ import * as ts from 'typescript';
2
+ import type { InspectorState, InspectorLogger } from '../types.js';
3
+ /**
4
+ * Detect wireRemoteAddon({ name: '...', package: '...' }) call expressions and
5
+ * record the namespace as a **remote** addon declaration. Only name + package
6
+ * are needed for codegen (serverUrl/auth are runtime closures resolved by the
7
+ * RPC runner); the `remote` marker tells the consumer's RPC-map generator to
8
+ * import the addon's `.remote.gen` map instead of `.internal.gen`.
9
+ */
10
+ export declare function addWireRemoteAddon(node: ts.Node, state: InspectorState, logger: InspectorLogger): void;
@@ -0,0 +1,66 @@
1
+ import * as ts from 'typescript';
2
+ /**
3
+ * Detect wireRemoteAddon({ name: '...', package: '...' }) call expressions and
4
+ * record the namespace as a **remote** addon declaration. Only name + package
5
+ * are needed for codegen (serverUrl/auth are runtime closures resolved by the
6
+ * RPC runner); the `remote` marker tells the consumer's RPC-map generator to
7
+ * import the addon's `.remote.gen` map instead of `.internal.gen`.
8
+ */
9
+ export function addWireRemoteAddon(node, state, logger) {
10
+ if (!ts.isCallExpression(node))
11
+ return;
12
+ const { expression, arguments: args } = node;
13
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireRemoteAddon')
14
+ return;
15
+ const [firstArg] = args;
16
+ if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
17
+ return;
18
+ let name;
19
+ let pkg;
20
+ // The auth binding is a runtime closure; we only extract the statically
21
+ // knowable slot names so `pikku verify` can check they exist in the
22
+ // consumer's own wirings. `hasAuth` distinguishes "public" (omitted) from
23
+ // "bound via a custom resolve()" (present but no static id).
24
+ let hasAuth = false;
25
+ let authCredentialId;
26
+ let authSecretId;
27
+ for (const prop of firstArg.properties) {
28
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
29
+ continue;
30
+ const key = prop.name.text;
31
+ if (key === 'name' && ts.isStringLiteral(prop.initializer)) {
32
+ name = prop.initializer.text;
33
+ }
34
+ else if (key === 'package' && ts.isStringLiteral(prop.initializer)) {
35
+ pkg = prop.initializer.text;
36
+ }
37
+ else if (key === 'auth' &&
38
+ ts.isObjectLiteralExpression(prop.initializer)) {
39
+ hasAuth = true;
40
+ for (const authProp of prop.initializer.properties) {
41
+ if (!ts.isPropertyAssignment(authProp) || !ts.isIdentifier(authProp.name))
42
+ continue;
43
+ if (authProp.name.text === 'credentialId' &&
44
+ ts.isStringLiteral(authProp.initializer)) {
45
+ authCredentialId = authProp.initializer.text;
46
+ }
47
+ else if (authProp.name.text === 'secretId' &&
48
+ ts.isStringLiteral(authProp.initializer)) {
49
+ authSecretId = authProp.initializer.text;
50
+ }
51
+ }
52
+ }
53
+ }
54
+ if (!name || !pkg)
55
+ return;
56
+ logger.debug(`• Found wireRemoteAddon: ${name} → ${pkg} (remote)`);
57
+ state.rpc.wireAddonDeclarations.set(name, {
58
+ package: pkg,
59
+ remote: true,
60
+ hasAuth,
61
+ authCredentialId,
62
+ authSecretId,
63
+ });
64
+ state.rpc.usedAddons.add(name);
65
+ state.rpc.wireAddonFiles.add(node.getSourceFile().fileName);
66
+ }
@@ -2,6 +2,11 @@ import * as ts from 'typescript';
2
2
  import { ErrorCode } from '../error-codes.js';
3
3
  import { extractStringLiteral } from '../utils/extract-node-value.js';
4
4
  function extractAstValue(expr, refParamName, templateParamName) {
5
+ // `'GET' as const` / parenthesised values wrap the real initializer — see
6
+ // through them or the whole property is silently dropped from the meta.
7
+ while (ts.isAsExpression(expr) || ts.isParenthesizedExpression(expr)) {
8
+ expr = expr.expression;
9
+ }
5
10
  if (ts.isStringLiteral(expr)) {
6
11
  return expr.text;
7
12
  }
@@ -113,9 +118,13 @@ function extractInputMapping(node, _checker) {
113
118
  const refParamName = node.parameters.length > 0 && ts.isIdentifier(node.parameters[0].name)
114
119
  ? node.parameters[0].name.text
115
120
  : 'ref';
121
+ // The canonical form imports `template` from '@pikku/core/workflow' and calls
122
+ // it inside a single-param `(ref) => (...)` arrow; the older form passes it as
123
+ // a second arrow parameter. Support both — default to the imported name so a
124
+ // `template(...)` value is never dropped just because it isn't a 2nd param.
116
125
  const templateParamName = node.parameters.length > 1 && ts.isIdentifier(node.parameters[1].name)
117
126
  ? node.parameters[1].name.text
118
- : undefined;
127
+ : 'template';
119
128
  const input = {};
120
129
  for (const prop of bodyObj.properties) {
121
130
  if (!ts.isPropertyAssignment(prop))
@@ -138,6 +147,12 @@ function extractInputMapping(node, _checker) {
138
147
  * Extract next config (string, array, or record)
139
148
  */
140
149
  function extractNextConfig(node, _checker) {
150
+ // Key-based branch `next` (`{ key: [targets] }`) is authored with an `as any`
151
+ // cast because the graph's NextConfig can't narrow record targets to node-id
152
+ // literals; see through that (and parens) so the routing survives into the meta.
153
+ while (ts.isAsExpression(node) || ts.isParenthesizedExpression(node)) {
154
+ node = node.expression;
155
+ }
141
156
  if (ts.isStringLiteral(node)) {
142
157
  return node.text;
143
158
  }
@@ -207,6 +222,7 @@ function extractWorkflowGraphConfig(configArg, checker) {
207
222
  let name;
208
223
  let description;
209
224
  let tags;
225
+ let notes;
210
226
  let disabled;
211
227
  let nodesNode;
212
228
  let configNode;
@@ -231,6 +247,12 @@ function extractWorkflowGraphConfig(configArg, checker) {
231
247
  .filter(ts.isStringLiteral)
232
248
  .map((el) => el.text);
233
249
  }
250
+ else if (propName === 'notes' &&
251
+ ts.isArrayLiteralExpression(prop.initializer)) {
252
+ notes = prop.initializer.elements
253
+ .filter(ts.isStringLiteral)
254
+ .map((el) => el.text);
255
+ }
234
256
  else if (propName === 'nodes' &&
235
257
  ts.isObjectLiteralExpression(prop.initializer)) {
236
258
  nodesNode = prop.initializer;
@@ -240,7 +262,7 @@ function extractWorkflowGraphConfig(configArg, checker) {
240
262
  configNode = prop.initializer;
241
263
  }
242
264
  }
243
- return { name, description, tags, disabled, nodesNode, configNode };
265
+ return { name, description, tags, notes, disabled, nodesNode, configNode };
244
266
  }
245
267
  /**
246
268
  * Extract graph nodes from the new pikkuWorkflowGraph format
@@ -283,6 +305,7 @@ function extractGraphFromNewFormat(nodesNode, configNode, checker, state) {
283
305
  onError: undefined,
284
306
  retries: undefined,
285
307
  retryDelay: undefined,
308
+ notes: undefined,
286
309
  };
287
310
  }
288
311
  // Extract config for each node from 'config' property
@@ -305,6 +328,7 @@ function extractGraphFromNewFormat(nodesNode, configNode, checker, state) {
305
328
  nodes[nodeId].input = nodeConfig.input;
306
329
  nodes[nodeId].retries = nodeConfig.retries;
307
330
  nodes[nodeId].retryDelay = nodeConfig.retryDelay;
331
+ nodes[nodeId].notes = nodeConfig.notes;
308
332
  }
309
333
  }
310
334
  }
@@ -320,6 +344,7 @@ function extractNodeConfigFromObject(obj, checker) {
320
344
  let input = {};
321
345
  let retries = undefined;
322
346
  let retryDelay = undefined;
347
+ let notes = undefined;
323
348
  for (const prop of obj.properties) {
324
349
  if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
325
350
  continue;
@@ -346,8 +371,11 @@ function extractNodeConfigFromObject(obj, checker) {
346
371
  retryDelay = prop.initializer.text;
347
372
  }
348
373
  }
374
+ else if (propName === 'notes') {
375
+ notes = extractStringLiteral(prop.initializer, checker);
376
+ }
349
377
  }
350
- return { next, onError, input, retries, retryDelay };
378
+ return { next, onError, input, retries, retryDelay, notes };
351
379
  }
352
380
  /**
353
381
  * Inspector for pikkuWorkflowGraph() calls
@@ -402,6 +430,7 @@ export const addWorkflowGraph = (logger, node, checker, state) => {
402
430
  source: 'graph',
403
431
  description: graphConfig.description,
404
432
  tags: graphConfig.tags,
433
+ notes: graphConfig.notes,
405
434
  nodes: graphNodes,
406
435
  entryNodeIds,
407
436
  };
@@ -22,6 +22,9 @@ export declare enum ErrorCode {
22
22
  EXPECT_EVENTUALLY_SCENARIO_ONLY = "PKU675",
23
23
  WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = "PKU600",
24
24
  INVALID_DSL_WORKFLOW = "PKU641",
25
+ WORKFLOW_GRAPH_ADDON_NOT_WIRED = "PKU642",
26
+ REMOTE_ADDON_NOT_DEV_DEPENDENCY = "PKU338",
27
+ REMOTE_ADDON_AUTH_UNRESOLVED = "PKU339",
25
28
  DB_COLUMN_NAME_TYPE_CONTRADICTION = "PKU480",
26
29
  DB_JSON_COLUMN_UNTYPED = "PKU481",
27
30
  DB_FORMAT_ON_NON_STRING = "PKU482",
@@ -24,6 +24,10 @@ export var ErrorCode;
24
24
  ErrorCode["EXPECT_EVENTUALLY_SCENARIO_ONLY"] = "PKU675";
25
25
  ErrorCode["WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED"] = "PKU600";
26
26
  ErrorCode["INVALID_DSL_WORKFLOW"] = "PKU641";
27
+ ErrorCode["WORKFLOW_GRAPH_ADDON_NOT_WIRED"] = "PKU642";
28
+ // Remote addon (wireRemoteAddon) validation
29
+ ErrorCode["REMOTE_ADDON_NOT_DEV_DEPENDENCY"] = "PKU338";
30
+ ErrorCode["REMOTE_ADDON_AUTH_UNRESOLVED"] = "PKU339";
27
31
  // Database schema codegen warnings
28
32
  ErrorCode["DB_COLUMN_NAME_TYPE_CONTRADICTION"] = "PKU480";
29
33
  ErrorCode["DB_JSON_COLUMN_UNTYPED"] = "PKU481";
package/dist/inspector.js CHANGED
@@ -5,7 +5,8 @@ import { visitSetup, visitFunctions, visitRoutes } from './visit.js';
5
5
  import { TypesMap } from './types-map.js';
6
6
  import { getFilesAndMethods } from './utils/get-files-and-methods.js';
7
7
  import { findCommonAncestor } from './utils/find-root-dir.js';
8
- import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeDiagnostics, validateSchemaWiringSeparation, } from './utils/post-process.js';
8
+ import { createNestedProjectFilter } from './utils/nested-project-filter.js';
9
+ import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, validateRemoteAddonDependencies, validateRemoteAddonAuth, validateScopeReferences, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeDiagnostics, validateSchemaWiringSeparation, validateWorkflowGraphAddons, } from './utils/post-process.js';
9
10
  import { generateOpenAPISpec } from './utils/serialize-openapi-json.js';
10
11
  import { pikkuState } from '@pikku/core/internal';
11
12
  import { resolveLatestVersions } from './utils/resolve-versions.js';
@@ -58,7 +59,6 @@ export function getInitialInspectorState(rootDir) {
58
59
  },
59
60
  files: new Set(),
60
61
  routeMiddleware: new Map(),
61
- routePermissions: new Map(),
62
62
  },
63
63
  channels: {
64
64
  files: new Set(),
@@ -93,6 +93,8 @@ export function getInitialInspectorState(rootDir) {
93
93
  internalFiles: new Map(),
94
94
  exposedMeta: {},
95
95
  exposedFiles: new Map(),
96
+ remoteMeta: {},
97
+ remoteFiles: new Map(),
96
98
  invokedFunctions: new Set(),
97
99
  invokedFunctionsByFile: new Map(),
98
100
  usedAddons: new Set(),
@@ -134,6 +136,10 @@ export function getInitialInspectorState(rootDir) {
134
136
  definitions: [],
135
137
  files: new Set(),
136
138
  },
139
+ scopes: {
140
+ definitions: [],
141
+ files: new Set(),
142
+ },
137
143
  variables: {
138
144
  definitions: [],
139
145
  files: new Set(),
@@ -159,7 +165,6 @@ export function getInitialInspectorState(rootDir) {
159
165
  permissions: {
160
166
  definitions: {},
161
167
  instances: {},
162
- tagPermissions: new Map(),
163
168
  },
164
169
  serviceAggregation: {
165
170
  requiredServices: new Set(),
@@ -183,8 +188,6 @@ export function getInitialInspectorState(rootDir) {
183
188
  },
184
189
  permissionsGroupsMeta: {
185
190
  definitions: {},
186
- httpGroups: {},
187
- tagGroups: {},
188
191
  },
189
192
  requiredSchemas: new Set(),
190
193
  openAPISpec: null,
@@ -225,17 +228,22 @@ export const inspect = async (logger, routeFiles, options = {}) => {
225
228
  const startSourceFiles = performance.now();
226
229
  // node_modules under rootDir (e.g. a locally-installed addon) is a
227
230
  // dependency, not project source — scanning it double-counts the addon's
228
- // own application types (CoreConfig/Services/SingletonServices).
231
+ // own application types (CoreConfig/Services/SingletonServices). Nested
232
+ // pikku projects (a workspace addon at packages/<addon>) are the same case:
233
+ // TS resolves the node_modules symlink to its realpath under rootDir, so
234
+ // the node_modules check alone misses them.
229
235
  // Sort by file name so the sweeps populate state in a stable order. The
230
236
  // program's own file order depends on glob + import-graph resolution, which
231
237
  // varies run to run — leaving generated meta keys (and anything serialized
232
238
  // in insertion order) non-reproducible across identical `pikku all` runs.
233
239
  // Safe because function registration is a dedicated pass (visitFunctions)
234
240
  // that completes before any order-sensitive wiring resolution in visitRoutes.
241
+ const isNestedProjectFile = createNestedProjectFilter(rootDir);
235
242
  const sourceFiles = program
236
243
  .getSourceFiles()
237
244
  .filter((sf) => sf.fileName.startsWith(rootDir) &&
238
- !sf.fileName.includes('/node_modules/'))
245
+ !sf.fileName.includes('/node_modules/') &&
246
+ !isNestedProjectFile(sf.fileName))
239
247
  .sort((a, b) => a.fileName < b.fileName ? -1 : a.fileName > b.fileName ? 1 : 0);
240
248
  logger.debug(`Got source files in ${(performance.now() - startSourceFiles).toFixed(2)}ms`);
241
249
  const state = getInitialInspectorState(rootDir);
@@ -301,6 +309,7 @@ export const inspect = async (logger, routeFiles, options = {}) => {
301
309
  computePermissionsGroupsMeta(state);
302
310
  computeDiagnostics(state);
303
311
  validateSchemaWiringSeparation(logger, state);
312
+ validateWorkflowGraphAddons(logger, state);
304
313
  if (options.openAPI) {
305
314
  state.openAPISpec = await generateOpenAPISpec(logger, state.functions.meta, state.http.meta, state.schemas, options.openAPI.additionalInfo, pikkuState(null, 'misc', 'errors'));
306
315
  }
@@ -308,6 +317,9 @@ export const inspect = async (logger, routeFiles, options = {}) => {
308
317
  validateSecretOverrides(logger, state);
309
318
  validateVariableOverrides(logger, state);
310
319
  validateCredentialOverrides(logger, state);
320
+ validateRemoteAddonDependencies(logger, state);
321
+ validateRemoteAddonAuth(logger, state);
322
+ validateScopeReferences(logger, state);
311
323
  }
312
324
  state.program = program;
313
325
  return state;
package/dist/types.d.ts CHANGED
@@ -13,6 +13,7 @@ import type { CLICommandMeta } from '@pikku/core/cli';
13
13
  import type { NodesMeta } from '@pikku/core/node';
14
14
  import type { SecretDefinitions } from '@pikku/core/secret';
15
15
  import type { CredentialDefinitions } from '@pikku/core/credential';
16
+ import type { ScopeDefinitions } from '@pikku/core/scope';
16
17
  import type { VariableDefinitions } from '@pikku/core/variable';
17
18
  import type { TypesMap } from './types-map.js';
18
19
  import type { FunctionsMeta, FunctionServicesMeta, FunctionWiresMeta, JSONValue } from '@pikku/core';
@@ -39,21 +40,11 @@ export interface MiddlewareGroupMeta {
39
40
  instanceIds: string[];
40
41
  isFactory: boolean;
41
42
  }
42
- export interface PermissionGroupMeta {
43
- exportName: string | null;
44
- sourceFile: string;
45
- position: number;
46
- services: FunctionServicesMeta;
47
- count: number;
48
- instanceIds: string[];
49
- isFactory: boolean;
50
- }
51
43
  export interface InspectorHTTPState {
52
44
  metaInputTypes: MetaInputTypes;
53
45
  meta: HTTPWiringsMeta;
54
46
  files: Set<string>;
55
47
  routeMiddleware: Map<string, MiddlewareGroupMeta>;
56
- routePermissions: Map<string, PermissionGroupMeta>;
57
48
  }
58
49
  /**
59
50
  * Schema vendor types for Standard Schema compliant validators
@@ -184,7 +175,6 @@ export interface InspectorPermissionInstance {
184
175
  export interface InspectorPermissionState {
185
176
  definitions: Record<string, InspectorPermissionDefinition>;
186
177
  instances: Record<string, InspectorPermissionInstance>;
187
- tagPermissions: Map<string, PermissionGroupMeta>;
188
178
  }
189
179
  export type InspectorFilters = {
190
180
  names?: string[];
@@ -418,6 +408,12 @@ export interface InspectorState {
418
408
  path: string;
419
409
  exportedName: string;
420
410
  }>;
411
+ /** Functions marked `remote: true` — the surface a wireRemoteAddon consumer imports */
412
+ remoteMeta: Record<string, string>;
413
+ remoteFiles: Map<string, {
414
+ path: string;
415
+ exportedName: string;
416
+ }>;
421
417
  invokedFunctions: Set<string>;
422
418
  invokedFunctionsByFile: Map<string, Set<string>>;
423
419
  usedAddons: Set<string>;
@@ -425,6 +421,14 @@ export interface InspectorState {
425
421
  package: string;
426
422
  rpcEndpoint?: string;
427
423
  mcp?: boolean;
424
+ /** True when declared via `wireRemoteAddon` — import the addon's `.remote.gen` map, not `.internal.gen` */
425
+ remote?: boolean;
426
+ /** wireRemoteAddon: whether an `auth` binding was supplied (vs a public surface) */
427
+ hasAuth?: boolean;
428
+ /** wireRemoteAddon: statically-known `auth.credentialId`, for verify */
429
+ authCredentialId?: string;
430
+ /** wireRemoteAddon: statically-known `auth.secretId`, for verify */
431
+ authSecretId?: string;
428
432
  secretOverrides?: Record<string, string>;
429
433
  variableOverrides?: Record<string, string>;
430
434
  credentialOverrides?: Record<string, string>;
@@ -480,6 +484,10 @@ export interface InspectorState {
480
484
  definitions: CredentialDefinitions;
481
485
  files: Set<string>;
482
486
  };
487
+ scopes: {
488
+ definitions: ScopeDefinitions;
489
+ files: Set<string>;
490
+ };
483
491
  variables: {
484
492
  definitions: VariableDefinitions;
485
493
  files: Set<string>;
@@ -518,8 +526,6 @@ export interface InspectorState {
518
526
  };
519
527
  permissionsGroupsMeta: {
520
528
  definitions: Record<string, InspectorPermissionDefinition>;
521
- httpGroups: Record<string, PermissionGroupMeta>;
522
- tagGroups: Record<string, PermissionGroupMeta>;
523
529
  };
524
530
  requiredSchemas: Set<string>;
525
531
  openAPISpec: Record<string, any> | null;
@@ -5,7 +5,8 @@
5
5
  * ts-json-schema-generator can discover inline/custom types from typesMap.
6
6
  */
7
7
  export function sanitizeTypeName(name) {
8
- return name.replace(/[^a-zA-Z0-9_$]/g, '_');
8
+ const sanitized = name.replace(/[^a-zA-Z0-9_$]/g, '_');
9
+ return /^[0-9]/.test(sanitized) ? `_${sanitized}` : sanitized;
9
10
  }
10
11
  const CLASSIFICATION_WRAPPERS = new Set(['Private', 'Pii', 'Secret']);
11
12
  function findMatchingAngleBracket(type, startIndex) {
@@ -7,3 +7,18 @@ import type { InspectorState } from '../types.js';
7
7
  * extracts tags/middleware/permissions from the pikkuFunc() config.
8
8
  */
9
9
  export declare function ensureFunctionMetadata(state: InspectorState, pikkuFuncId: string, fallbackName?: string, funcInitializer?: ts.Node, checker?: ts.TypeChecker, isHelper?: boolean): void;
10
+ /**
11
+ * Register metadata for a `func:` inlined into a wiring (queue/scheduler/
12
+ * trigger/gateway/…).
13
+ *
14
+ * An inline function has no exported name, so `addFunctions` skips it (it bails
15
+ * on `__temp_` ids) and only the wiring's context-based id exists. Without this
16
+ * the wiring points at a function that was never registered and the worker
17
+ * resolves to nothing at runtime ("Missing generated metadata for ...").
18
+ *
19
+ * The stub also carries no session info, so the runtime would treat it as
20
+ * session-required and every invocation would fail with "Authentication
21
+ * required". Derive `sessionless` from the helper the function was built with —
22
+ * the same rule `addFunctions` applies to named ones (`!== 'pikkuFunc'`).
23
+ */
24
+ export declare function ensureInlineWiringFunction(state: InspectorState, pikkuFuncId: string, fallbackName: string | undefined, funcInitializer: ts.Node, checker: ts.TypeChecker, isHelper?: boolean): void;
@@ -207,6 +207,30 @@ export function ensureFunctionMetadata(state, pikkuFuncId, fallbackName, funcIni
207
207
  }
208
208
  }
209
209
  }
210
+ /**
211
+ * Register metadata for a `func:` inlined into a wiring (queue/scheduler/
212
+ * trigger/gateway/…).
213
+ *
214
+ * An inline function has no exported name, so `addFunctions` skips it (it bails
215
+ * on `__temp_` ids) and only the wiring's context-based id exists. Without this
216
+ * the wiring points at a function that was never registered and the worker
217
+ * resolves to nothing at runtime ("Missing generated metadata for ...").
218
+ *
219
+ * The stub also carries no session info, so the runtime would treat it as
220
+ * session-required and every invocation would fail with "Authentication
221
+ * required". Derive `sessionless` from the helper the function was built with —
222
+ * the same rule `addFunctions` applies to named ones (`!== 'pikkuFunc'`).
223
+ */
224
+ export function ensureInlineWiringFunction(state, pikkuFuncId, fallbackName, funcInitializer, checker, isHelper) {
225
+ ensureFunctionMetadata(state, pikkuFuncId, fallbackName, funcInitializer, checker, isHelper);
226
+ const meta = state.functions.meta[pikkuFuncId];
227
+ if (meta &&
228
+ meta.sessionless === undefined &&
229
+ ts.isCallExpression(funcInitializer) &&
230
+ ts.isIdentifier(funcInitializer.expression)) {
231
+ meta.sessionless = funcInitializer.expression.text !== 'pikkuFunc';
232
+ }
233
+ }
210
234
  function populateTypesLookup(state, pikkuFuncId, funcInitializer, checker) {
211
235
  const typeArgs = funcInitializer.typeArguments;
212
236
  if (typeArgs && typeArgs.length >= 1) {
@@ -334,6 +334,8 @@ export function filterInspectorState(state, filters, logger) {
334
334
  internalFiles: new Map(state.rpc.internalFiles),
335
335
  exposedMeta: { ...state.rpc.exposedMeta },
336
336
  exposedFiles: new Map(state.rpc.exposedFiles),
337
+ remoteMeta: { ...state.rpc.remoteMeta },
338
+ remoteFiles: new Map(state.rpc.remoteFiles),
337
339
  invokedFunctions: new Set(state.rpc.invokedFunctions),
338
340
  },
339
341
  cli: {
@@ -746,6 +748,13 @@ export function filterInspectorState(state, filters, logger) {
746
748
  filteredState.rpc.exposedFiles.delete(key);
747
749
  }
748
750
  }
751
+ for (const key of Object.keys(filteredState.rpc.remoteMeta)) {
752
+ const targetFuncId = filteredState.rpc.remoteMeta[key];
753
+ if (!survivingFuncIds.has(targetFuncId) && !survivingFuncIds.has(key)) {
754
+ delete filteredState.rpc.remoteMeta[key];
755
+ filteredState.rpc.remoteFiles.delete(key);
756
+ }
757
+ }
749
758
  // Prune invokedFunctions to match surviving functions
750
759
  for (const funcId of filteredState.rpc.invokedFunctions) {
751
760
  if (!survivingFuncIds.has(funcId)) {
@@ -1,9 +1,14 @@
1
1
  import * as ts from 'typescript';
2
2
  import { ErrorCode } from '../error-codes.js';
3
+ export declare const getArrayPropertyValue: (obj: ts.ObjectLiteralExpression, propertyName: string) => string[] | null;
3
4
  /**
4
- * Extracts an array of strings from an object property.
5
+ * Extracts a string->string record from an object property.
6
+ *
7
+ * `getPropertyValue` falls through to `getText()` for an object literal, which
8
+ * returns the raw source text rather than the record — so callers that need the
9
+ * entries (e.g. oauth2 `additionalParams`) must use this instead.
5
10
  */
6
- export declare const getArrayPropertyValue: (obj: ts.ObjectLiteralExpression, propertyName: string) => string[] | null;
11
+ export declare const getRecordPropertyValue: (obj: ts.ObjectLiteralExpression, propertyName: string) => Record<string, string> | null;
7
12
  /**
8
13
  * Wiring identity fields (`name`, `secretId`, `variableId`, …) are read
9
14
  * STATICALLY from source — a const or variable reference is keyed by its