@pikku/inspector 0.12.50 → 0.12.54

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 (116) hide show
  1. package/CHANGELOG.md +743 -0
  2. package/OPTIMIZATION-PLAN.md +9 -3
  3. package/dist/add/add-auth.d.ts +1 -1
  4. package/dist/add/add-auth.js +1 -1
  5. package/dist/add/add-cli.js +7 -0
  6. package/dist/add/add-credential.js +1 -1
  7. package/dist/add/add-functions.js +199 -8
  8. package/dist/add/add-http-route.js +16 -6
  9. package/dist/add/add-middleware.js +46 -3
  10. package/dist/add/add-personas.d.ts +13 -0
  11. package/dist/add/add-personas.js +216 -0
  12. package/dist/add/add-scope.js +1 -1
  13. package/dist/add/add-secret.js +1 -1
  14. package/dist/add/add-system-role.d.ts +2 -0
  15. package/dist/add/add-system-role.js +102 -0
  16. package/dist/add/add-variable.js +1 -1
  17. package/dist/add/add-wire-addon.js +31 -0
  18. package/dist/add/add-workflow.js +12 -1
  19. package/dist/error-codes.d.ts +17 -1
  20. package/dist/error-codes.js +23 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/inspector.js +33 -1
  23. package/dist/types.d.ts +55 -2
  24. package/dist/utils/annotate-http-route-auth.d.ts +22 -0
  25. package/dist/utils/annotate-http-route-auth.js +48 -0
  26. package/dist/utils/check-secret-sinks.d.ts +31 -0
  27. package/dist/utils/check-secret-sinks.js +117 -0
  28. package/dist/utils/extract-function-name.js +13 -4
  29. package/dist/utils/extract-function-node.js +18 -1
  30. package/dist/utils/extract-secret-usage.d.ts +9 -0
  31. package/dist/utils/extract-secret-usage.js +61 -0
  32. package/dist/utils/literal-properties.d.ts +22 -0
  33. package/dist/utils/literal-properties.js +69 -0
  34. package/dist/utils/load-addon-functions-meta.js +26 -1
  35. package/dist/utils/permissions.js +8 -0
  36. package/dist/utils/post-process.d.ts +68 -3
  37. package/dist/utils/post-process.js +246 -13
  38. package/dist/utils/secret-alias-services.d.ts +8 -0
  39. package/dist/utils/secret-alias-services.js +45 -0
  40. package/dist/utils/secret-brokers.d.ts +2 -0
  41. package/dist/utils/secret-brokers.js +9 -0
  42. package/dist/utils/serialize-inspector-state.d.ts +10 -0
  43. package/dist/utils/serialize-inspector-state.js +18 -0
  44. package/dist/utils/serialize-middleware-groups-meta.js +3 -0
  45. package/dist/utils/single-declaration.d.ts +32 -0
  46. package/dist/utils/single-declaration.js +43 -0
  47. package/dist/utils/validate-exposed-functions-gated.d.ts +14 -0
  48. package/dist/utils/validate-exposed-functions-gated.js +93 -0
  49. package/dist/utils/validate-tags-resolve-to-middleware.d.ts +20 -0
  50. package/dist/utils/validate-tags-resolve-to-middleware.js +41 -0
  51. package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +6 -0
  52. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +48 -1
  53. package/dist/utils/workflow/dsl/patterns.d.ts +9 -4
  54. package/dist/utils/workflow/dsl/patterns.js +26 -4
  55. package/dist/visit.js +4 -0
  56. package/package.json +3 -3
  57. package/src/add/add-auth.test.ts +5 -3
  58. package/src/add/add-auth.ts +1 -1
  59. package/src/add/add-cli.ts +8 -0
  60. package/src/add/add-credential.ts +1 -1
  61. package/src/add/add-functions.test.ts +263 -1
  62. package/src/add/add-functions.ts +273 -11
  63. package/src/add/add-http-route.ts +20 -7
  64. package/src/add/add-middleware.test.ts +196 -0
  65. package/src/add/add-middleware.ts +64 -4
  66. package/src/add/add-personas.test.ts +382 -0
  67. package/src/add/add-personas.ts +285 -0
  68. package/src/add/add-scenario-step.test.ts +87 -5
  69. package/src/add/add-scope.test.ts +103 -30
  70. package/src/add/add-scope.ts +1 -1
  71. package/src/add/add-secret.ts +1 -1
  72. package/src/add/add-subject-scenario-steps.test.ts +161 -0
  73. package/src/add/add-system-role.test.ts +297 -0
  74. package/src/add/add-system-role.ts +164 -0
  75. package/src/add/add-variable.ts +1 -1
  76. package/src/add/add-wire-addon.test.ts +111 -0
  77. package/src/add/add-wire-addon.ts +29 -0
  78. package/src/add/add-workflow.ts +21 -1
  79. package/src/add/addon-bans.test.ts +4 -4
  80. package/src/add/allow-escape-hatches.test.ts +144 -0
  81. package/src/add/audit-meta.test.ts +75 -0
  82. package/src/add/config-value-metadata.test.ts +11 -11
  83. package/src/add/secret-sink-check.test.ts +211 -0
  84. package/src/add/wire-name-literal.test.ts +2 -2
  85. package/src/error-codes.ts +26 -0
  86. package/src/index.ts +4 -1
  87. package/src/inspector.ts +39 -0
  88. package/src/types.ts +56 -2
  89. package/src/utils/annotate-http-route-auth.test.ts +163 -0
  90. package/src/utils/annotate-http-route-auth.ts +52 -0
  91. package/src/utils/check-secret-sinks.ts +145 -0
  92. package/src/utils/extract-function-name.ts +14 -4
  93. package/src/utils/extract-function-node.ts +18 -6
  94. package/src/utils/extract-secret-usage.test.ts +81 -0
  95. package/src/utils/extract-secret-usage.ts +73 -0
  96. package/src/utils/literal-properties.ts +97 -0
  97. package/src/utils/load-addon-functions-meta.ts +32 -2
  98. package/src/utils/permissions-shorthand.test.ts +79 -0
  99. package/src/utils/permissions.ts +7 -0
  100. package/src/utils/post-process.test.ts +133 -0
  101. package/src/utils/post-process.ts +313 -12
  102. package/src/utils/secret-alias-services.test.ts +146 -0
  103. package/src/utils/secret-alias-services.ts +58 -0
  104. package/src/utils/secret-brokers.test.ts +53 -0
  105. package/src/utils/secret-brokers.ts +11 -0
  106. package/src/utils/serialize-inspector-state.ts +28 -0
  107. package/src/utils/serialize-middleware-groups-meta.ts +3 -0
  108. package/src/utils/single-declaration.ts +57 -0
  109. package/src/utils/validate-exposed-functions-gated.test.ts +230 -0
  110. package/src/utils/validate-exposed-functions-gated.ts +103 -0
  111. package/src/utils/validate-tags-resolve-to-middleware.test.ts +105 -0
  112. package/src/utils/validate-tags-resolve-to-middleware.ts +48 -0
  113. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +57 -0
  114. package/src/utils/workflow/dsl/patterns.ts +33 -4
  115. package/src/visit.ts +4 -0
  116. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,216 @@
1
+ import * as ts from 'typescript';
2
+ import { DISPOSITIONS } from '@pikku/core/virtual-user';
3
+ import { isRunnablePersona } from '@pikku/core/persona';
4
+ import { ErrorCode } from '../error-codes.js';
5
+ import { claimSingleDeclaration } from '../utils/single-declaration.js';
6
+ import { booleanProperty, getProperty, numberProperty, stringArrayProperty, stringProperty, unwrapAs, } from '../utils/literal-properties.js';
7
+ /** A dial and the range outside which it is a mistake rather than a choice. */
8
+ const RATES = [
9
+ ['temperature', 0, 2],
10
+ ['repeatRate', 0, 1],
11
+ ['reReadRate', 0, 1],
12
+ ];
13
+ /**
14
+ * `{ moves, temperature, repeatRate, … }` — a disposition's dials, overridden.
15
+ *
16
+ * The ranges are checked here rather than at run time because these are the
17
+ * kind of typo that does not throw: `repeatRate: 18` meaning 18% would silently
18
+ * double every call the persona makes, and you would read the run as a product
19
+ * bug.
20
+ */
21
+ const tuningProperty = (config, onError) => {
22
+ const value = getProperty(config, 'tuning');
23
+ if (!value || !ts.isObjectLiteralExpression(value)) {
24
+ return undefined;
25
+ }
26
+ const tuning = {};
27
+ const movesValue = getProperty(value, 'moves');
28
+ if (movesValue && ts.isObjectLiteralExpression(movesValue)) {
29
+ const moves = {};
30
+ for (const move of ['continue', 'suspend', 'resume', 'abandon']) {
31
+ const weight = numberProperty(movesValue, move);
32
+ if (weight === undefined) {
33
+ continue;
34
+ }
35
+ if (weight < 0) {
36
+ onError(`tuning.moves.${move} must not be negative (got ${weight}).`);
37
+ continue;
38
+ }
39
+ moves[move] = weight;
40
+ }
41
+ if (Object.keys(moves).length) {
42
+ tuning.moves = moves;
43
+ }
44
+ }
45
+ for (const [name, min, max] of RATES) {
46
+ const rate = numberProperty(value, name);
47
+ if (rate === undefined) {
48
+ continue;
49
+ }
50
+ if (rate < min || rate > max) {
51
+ onError(`tuning.${name} must be between ${min} and ${max} (got ${rate}).`);
52
+ continue;
53
+ }
54
+ Object.assign(tuning, { [name]: rate });
55
+ }
56
+ for (const flag of ['emptyMemory', 'readOnly', 'invertedOracle']) {
57
+ const declared = booleanProperty(value, flag);
58
+ if (declared !== undefined) {
59
+ tuning[flag] = declared;
60
+ }
61
+ }
62
+ const instructions = stringProperty(value, 'instructions');
63
+ if (instructions !== undefined) {
64
+ tuning.instructions = instructions;
65
+ }
66
+ return Object.keys(tuning).length ? tuning : undefined;
67
+ };
68
+ const accountProperty = (value) => {
69
+ if (!value || !ts.isObjectLiteralExpression(value)) {
70
+ return undefined;
71
+ }
72
+ const provider = stringProperty(value, 'provider');
73
+ return provider === undefined ? {} : { provider };
74
+ };
75
+ const linkedAccountsProperty = (config, personaId, logger) => {
76
+ const value = getProperty(config, 'linkedAccounts');
77
+ if (!value || !ts.isObjectLiteralExpression(value)) {
78
+ return undefined;
79
+ }
80
+ const accounts = {};
81
+ for (const prop of value.properties) {
82
+ if (!ts.isPropertyAssignment(prop)) {
83
+ continue;
84
+ }
85
+ if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
86
+ logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, `Persona '${personaId}' has a linked account whose key is not a literal.`);
87
+ continue;
88
+ }
89
+ const account = accountProperty(prop.initializer);
90
+ if (!account) {
91
+ logger.critical(ErrorCode.INVALID_VALUE, `Persona '${personaId}' linked account '${prop.name.text}' must be an object literal.`);
92
+ continue;
93
+ }
94
+ accounts[prop.name.text] = account;
95
+ }
96
+ return Object.keys(accounts).length ? accounts : undefined;
97
+ };
98
+ /**
99
+ * Inspector for `definePersonas()` calls.
100
+ *
101
+ * A persona is entirely declaration — a person, their roles, what they want —
102
+ * so there is no code body and nothing to resolve at runtime. What is read here
103
+ * is the whole thing, and it is generated straight to `personas.gen.json` for
104
+ * the CLI and the console to read without loading the app.
105
+ *
106
+ * `name` is the one field that cannot be defaulted. A persona without one is
107
+ * not a person, and the console has nothing to show.
108
+ */
109
+ export const addPersonas = (logger, node, _checker, state) => {
110
+ if (!ts.isCallExpression(node)) {
111
+ return;
112
+ }
113
+ const expression = node.expression;
114
+ if (!ts.isIdentifier(expression) || expression.text !== 'definePersonas') {
115
+ return;
116
+ }
117
+ const firstArg = node.arguments[0];
118
+ if (!firstArg) {
119
+ return;
120
+ }
121
+ const unwrapped = unwrapAs(firstArg);
122
+ if (!ts.isObjectLiteralExpression(unwrapped)) {
123
+ logger.critical(ErrorCode.INVALID_VALUE, 'definePersonas() needs an inline object — personas are read statically, never evaluated.');
124
+ return;
125
+ }
126
+ const sourceFile = node.getSourceFile().fileName;
127
+ if (!claimSingleDeclaration(logger, state.personas.files, ErrorCode.DUPLICATE_PERSONAS_DEFINITION, 'definePersonas', sourceFile)) {
128
+ return;
129
+ }
130
+ const known = Object.keys(DISPOSITIONS);
131
+ for (const prop of unwrapped.properties) {
132
+ if (!ts.isPropertyAssignment(prop)) {
133
+ continue;
134
+ }
135
+ if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
136
+ logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, 'A persona is declared with a key that is not a literal.');
137
+ continue;
138
+ }
139
+ const id = prop.name.text;
140
+ if (!ts.isObjectLiteralExpression(prop.initializer)) {
141
+ logger.critical(ErrorCode.INVALID_VALUE, `Persona '${id}' must be an object literal.`);
142
+ continue;
143
+ }
144
+ const config = prop.initializer;
145
+ const name = stringProperty(config, 'name');
146
+ if (!name) {
147
+ logger.critical(ErrorCode.INVALID_VALUE, `Persona '${id}' needs a literal 'name' — a persona is a person, and the console has nothing to show without one.`);
148
+ continue;
149
+ }
150
+ // The generated `definePersonas` types this as a union, so a bad value only
151
+ // reaches here in source that does not compile. Caught anyway: a
152
+ // disposition is the entire behaviour, and defaulting a typo to `realistic`
153
+ // would run a different person than the one that was written.
154
+ const disposition = stringProperty(config, 'disposition');
155
+ if (disposition !== undefined && !known.includes(disposition)) {
156
+ logger.critical(ErrorCode.INVALID_VALUE, `Persona '${id}' has an unknown disposition '${disposition}'. One of: ${known.join(', ')}`);
157
+ continue;
158
+ }
159
+ let tuningFailed = false;
160
+ const tuning = tuningProperty(config, (message) => {
161
+ tuningFailed = true;
162
+ logger.critical(ErrorCode.INVALID_VALUE, `Persona '${id}': ${message}`);
163
+ });
164
+ if (tuningFailed) {
165
+ continue;
166
+ }
167
+ const account = accountProperty(getProperty(config, 'account'));
168
+ const declaredRunnable = booleanProperty(config, 'runnable');
169
+ const persona = {
170
+ id,
171
+ name,
172
+ roles: stringArrayProperty(config, 'roles') ?? [],
173
+ goals: stringArrayProperty(config, 'goals') ?? [],
174
+ tags: stringArrayProperty(config, 'tags') ?? [],
175
+ runnable: isRunnablePersona({
176
+ ...(declaredRunnable !== undefined
177
+ ? { runnable: declaredRunnable }
178
+ : {}),
179
+ ...(account ? { account } : {}),
180
+ }),
181
+ sourceFile,
182
+ };
183
+ const jobTitle = stringProperty(config, 'jobTitle');
184
+ if (jobTitle !== undefined)
185
+ persona.jobTitle = jobTitle;
186
+ const description = stringProperty(config, 'description');
187
+ if (description !== undefined)
188
+ persona.description = description;
189
+ const avatarUrl = stringProperty(config, 'avatarUrl');
190
+ if (avatarUrl !== undefined)
191
+ persona.avatarUrl = avatarUrl;
192
+ const personality = stringProperty(config, 'personality');
193
+ if (personality !== undefined)
194
+ persona.personality = personality;
195
+ if (disposition !== undefined) {
196
+ persona.disposition = disposition;
197
+ }
198
+ if (tuning !== undefined)
199
+ persona.tuning = tuning;
200
+ const fixtures = stringArrayProperty(config, 'fixtures');
201
+ if (fixtures !== undefined)
202
+ persona.fixtures = fixtures;
203
+ // Left absent rather than resolved here: "everywhere but production" is a
204
+ // fact about the config, and the inspector has no business baking today's
205
+ // environment list into a meta file that outlives it.
206
+ const environments = stringArrayProperty(config, 'environments');
207
+ if (environments !== undefined)
208
+ persona.environments = environments;
209
+ if (account !== undefined)
210
+ persona.account = account;
211
+ const linkedAccounts = linkedAccountsProperty(config, id, logger);
212
+ if (linkedAccounts !== undefined)
213
+ persona.linkedAccounts = linkedAccounts;
214
+ state.personas.definitions.push(persona);
215
+ }
216
+ };
@@ -89,7 +89,7 @@ export const addScope = (logger, node, checker, state, _options) => {
89
89
  return;
90
90
  }
91
91
  const expression = node.expression;
92
- if (!ts.isIdentifier(expression) || expression.text !== 'wireScope') {
92
+ if (!ts.isIdentifier(expression) || expression.text !== 'defineScope') {
93
93
  return;
94
94
  }
95
95
  const firstArg = node.arguments[0];
@@ -1,6 +1,6 @@
1
1
  import { createAddKeyedWiring } from './add-keyed-wiring.js';
2
2
  export const addSecret = createAddKeyedWiring({
3
- functionName: 'wireSecret',
3
+ functionName: 'defineSecret',
4
4
  idField: 'secretId',
5
5
  label: 'Secret',
6
6
  schemaPrefix: 'SecretSchema',
@@ -0,0 +1,2 @@
1
+ import type { AddWiring } from '../types.js';
2
+ export declare const addSystemRole: AddWiring;
@@ -0,0 +1,102 @@
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
+ /**
6
+ * Unwraps `x as const` / `x satisfies T` so a cast declaration is still
7
+ * extracted rather than silently skipped.
8
+ */
9
+ const unwrapAs = (node) => ts.isAsExpression(node) || ts.isSatisfiesExpression(node)
10
+ ? unwrapAs(node.expression)
11
+ : node;
12
+ /**
13
+ * Reads a `scopes: [...]` array of string literals.
14
+ *
15
+ * Returns `undefined` rather than an empty array when the property is absent
16
+ * or unreadable, so the caller can tell "declared no scopes" from "we could
17
+ * not see the scopes" — the first is a legitimate placeholder role, the second
18
+ * would silently produce one.
19
+ */
20
+ const extractScopes = (obj, roleName, logger) => {
21
+ const prop = obj.properties.find((p) => ts.isPropertyAssignment(p) &&
22
+ (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
23
+ p.name.text === 'scopes');
24
+ if (!prop) {
25
+ logger.critical(ErrorCode.INVALID_VALUE, `System role '${roleName}' has no 'scopes'. A role that grants nothing should say so with 'scopes: []'.`);
26
+ return undefined;
27
+ }
28
+ if (!ts.isPropertyAssignment(prop) ||
29
+ !ts.isArrayLiteralExpression(prop.initializer)) {
30
+ logger.critical(ErrorCode.INVALID_VALUE, `System role '${roleName}' must declare 'scopes' as an array literal.`);
31
+ return undefined;
32
+ }
33
+ const scopes = [];
34
+ for (const element of prop.initializer.elements) {
35
+ const unwrapped = unwrapAs(element);
36
+ if (!ts.isStringLiteral(unwrapped)) {
37
+ logger.critical(ErrorCode.INVALID_VALUE, `System role '${roleName}' lists a scope that is not a string literal. ` +
38
+ `Scopes are extracted by AST, so a computed value cannot be checked against the declared set.`);
39
+ continue;
40
+ }
41
+ scopes.push(unwrapped.text);
42
+ }
43
+ return scopes;
44
+ };
45
+ export const addSystemRole = (logger, node, _checker, state, _options) => {
46
+ if (!ts.isCallExpression(node)) {
47
+ return;
48
+ }
49
+ const expression = node.expression;
50
+ if (!ts.isIdentifier(expression) || expression.text !== 'defineSystemRole') {
51
+ return;
52
+ }
53
+ const firstArg = node.arguments[0];
54
+ if (!firstArg) {
55
+ return;
56
+ }
57
+ const unwrapped = unwrapAs(firstArg);
58
+ if (!ts.isObjectLiteralExpression(unwrapped)) {
59
+ return;
60
+ }
61
+ const sourceFile = node.getSourceFile().fileName;
62
+ for (const prop of unwrapped.properties) {
63
+ if (!ts.isPropertyAssignment(prop)) {
64
+ continue;
65
+ }
66
+ let name;
67
+ if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) {
68
+ name = prop.name.text;
69
+ }
70
+ if (name === undefined) {
71
+ logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, 'A system role is declared with a key that is not a literal.');
72
+ continue;
73
+ }
74
+ if (name.length === 0) {
75
+ logger.critical(ErrorCode.INVALID_VALUE, 'A system role is declared with an empty name.');
76
+ continue;
77
+ }
78
+ if (name.includes(SEPARATOR)) {
79
+ logger.critical(ErrorCode.INVALID_VALUE, `System role '${name}' contains the '${SEPARATOR}' separator. ` +
80
+ `'${SEPARATOR}' delimits scope ids; a role named like a scope reads as one.`);
81
+ continue;
82
+ }
83
+ if (!ts.isObjectLiteralExpression(prop.initializer)) {
84
+ logger.critical(ErrorCode.INVALID_VALUE, `System role '${name}' must be an object literal.`);
85
+ continue;
86
+ }
87
+ const scopes = extractScopes(prop.initializer, name, logger);
88
+ if (scopes === undefined) {
89
+ continue;
90
+ }
91
+ const displayName = getPropertyValue(prop.initializer, 'displayName');
92
+ const description = getPropertyValue(prop.initializer, 'description');
93
+ state.systemRoles.files.add(sourceFile);
94
+ state.systemRoles.definitions.push({
95
+ name,
96
+ displayName: displayName || undefined,
97
+ description: description || undefined,
98
+ scopes,
99
+ sourceFile,
100
+ });
101
+ }
102
+ };
@@ -1,6 +1,6 @@
1
1
  import { createAddKeyedWiring } from './add-keyed-wiring.js';
2
2
  export const addVariable = createAddKeyedWiring({
3
- functionName: 'wireVariable',
3
+ functionName: 'defineVariable',
4
4
  idField: 'variableId',
5
5
  label: 'Variable',
6
6
  schemaPrefix: 'VariableSchema',
@@ -1,4 +1,18 @@
1
1
  import * as ts from 'typescript';
2
+ function parseStringArray(node) {
3
+ if (!ts.isArrayLiteralExpression(node))
4
+ return undefined;
5
+ const values = [];
6
+ for (const element of node.elements) {
7
+ // A non-literal entry (a spread, a const reference) is not statically
8
+ // knowable. Dropping the whole array rather than a silent partial keeps a
9
+ // consumer from reading a short list as the complete set of gates.
10
+ if (!ts.isStringLiteral(element))
11
+ return undefined;
12
+ values.push(element.text);
13
+ }
14
+ return values;
15
+ }
2
16
  function parseStringRecord(obj) {
3
17
  const result = {};
4
18
  for (const prop of obj.properties) {
@@ -33,6 +47,9 @@ export function addWireAddon(node, state, logger) {
33
47
  let pkg;
34
48
  let rpcEndpoint;
35
49
  let mcp;
50
+ let auth;
51
+ let tags;
52
+ let scopes;
36
53
  let secretOverrides;
37
54
  let variableOverrides;
38
55
  let credentialOverrides;
@@ -54,6 +71,17 @@ export function addWireAddon(node, state, logger) {
54
71
  prop.initializer.kind === ts.SyntaxKind.FalseKeyword)) {
55
72
  mcp = prop.initializer.kind === ts.SyntaxKind.TrueKeyword;
56
73
  }
74
+ else if (key === 'auth' &&
75
+ (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
76
+ prop.initializer.kind === ts.SyntaxKind.FalseKeyword)) {
77
+ auth = prop.initializer.kind === ts.SyntaxKind.TrueKeyword;
78
+ }
79
+ else if (key === 'tags') {
80
+ tags = parseStringArray(prop.initializer);
81
+ }
82
+ else if (key === 'scopes') {
83
+ scopes = parseStringArray(prop.initializer);
84
+ }
57
85
  else if (key === 'secretOverrides' &&
58
86
  ts.isObjectLiteralExpression(prop.initializer)) {
59
87
  secretOverrides = parseStringRecord(prop.initializer);
@@ -74,6 +102,9 @@ export function addWireAddon(node, state, logger) {
74
102
  package: pkg,
75
103
  rpcEndpoint,
76
104
  mcp,
105
+ auth,
106
+ tags,
107
+ scopes,
77
108
  secretOverrides,
78
109
  variableOverrides,
79
110
  credentialOverrides,
@@ -208,7 +208,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
208
208
  * Inspector for pikkuWorkflowFunc(), pikkuWorkflowComplexFunc() and
209
209
  * pikkuScenario() calls. Detects workflow registration and extracts metadata.
210
210
  */
211
- export const addWorkflow = (logger, node, checker, state) => {
211
+ export const addWorkflow = (logger, node, checker, state, options) => {
212
212
  if (!ts.isCallExpression(node)) {
213
213
  return;
214
214
  }
@@ -223,6 +223,14 @@ export const addWorkflow = (logger, node, checker, state) => {
223
223
  wrapperType = 'dsl';
224
224
  }
225
225
  else if (expression.text === 'pikkuWorkflowComplexFunc') {
226
+ if (!options.allow?.complexWorkflows) {
227
+ logger.critical(ErrorCode.COMPLEX_WORKFLOW_NOT_ALLOWED, `'${expression.text}' is used here, but this project has not enabled complex workflows. ` +
228
+ `Their inline steps cannot be serialized, so they are absent from the workflow graph and ` +
229
+ `cannot be replayed or migrated across a definition change. Express the workflow with ` +
230
+ `pikkuWorkflowFunc and the step DSL, or set \`allow: { complexWorkflows: true }\` in ` +
231
+ `pikku.config.json if the project accepts that trade.`);
232
+ return;
233
+ }
226
234
  wrapperType = 'complex';
227
235
  }
228
236
  else if (expression.text === 'pikkuScenario') {
@@ -292,6 +300,7 @@ export const addWorkflow = (logger, node, checker, state) => {
292
300
  let steps = [];
293
301
  let context = undefined;
294
302
  let dsl = undefined;
303
+ let asserts = undefined;
295
304
  // Try DSL workflow extraction first
296
305
  // Pass the whole CallExpression node so findWorkflowFunction can find the arrow function
297
306
  const result = extractDSLWorkflow(node, checker, {
@@ -301,6 +310,7 @@ export const addWorkflow = (logger, node, checker, state) => {
301
310
  // Extraction succeeded
302
311
  steps = result.steps;
303
312
  context = result.context;
313
+ asserts = result.asserts;
304
314
  // Check if workflow contains inline steps (non-serializable)
305
315
  if (hasInlineSteps(steps)) {
306
316
  if (wrapperType === 'dsl') {
@@ -374,6 +384,7 @@ export const addWorkflow = (logger, node, checker, state) => {
374
384
  expose,
375
385
  skip,
376
386
  scenario: wrapperType === 'scenario' ? true : undefined,
387
+ asserts,
377
388
  actors: actorNames.length > 0 ? actorNames : undefined,
378
389
  };
379
390
  // The scenario "logger/config only" rule (PKU673) and the implicit
@@ -23,22 +23,31 @@ export declare enum ErrorCode {
23
23
  SCENARIO_BROWSER_STEP_NEEDS_ACTOR = "PKU677",
24
24
  SCENARIO_STEP_TARGET_NOT_STATIC = "PKU678",
25
25
  SCENARIO_NOT_EXTRACTABLE = "PKU679",
26
+ SCENARIO_HAS_NO_ASSERTION = "PKU680",
26
27
  WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = "PKU600",
27
28
  INVALID_DSL_WORKFLOW = "PKU641",
28
29
  WORKFLOW_GRAPH_ADDON_NOT_WIRED = "PKU642",
30
+ COMPLEX_WORKFLOW_NOT_ALLOWED = "PKU643",
29
31
  REMOTE_ADDON_NOT_DEV_DEPENDENCY = "PKU338",
30
32
  REMOTE_ADDON_AUTH_UNRESOLVED = "PKU339",
31
33
  DB_COLUMN_NAME_TYPE_CONTRADICTION = "PKU480",
32
34
  DB_JSON_COLUMN_UNTYPED = "PKU481",
33
35
  DB_FORMAT_ON_NON_STRING = "PKU482",
36
+ DB_SECRET_COLUMN_STORED_PLAIN = "PKU483",
37
+ DB_FORM_ON_NON_STRING = "PKU484",
38
+ DB_DEFAULT_SCHEMA_NAME_COLLISION = "PKU485",
34
39
  SCHEMA_GENERATION_ERROR = "PKU456",
35
40
  INLINE_SCHEMA = "PKU489",
36
41
  FUNCTION_METADATA_NOT_FOUND = "PKU559",
37
42
  DUPLICATE_AUTH_DEFINITION = "PKU581",
38
43
  AUTH_NOT_EXPORTED = "PKU582",
44
+ DUPLICATE_PERSONAS_DEFINITION = "PKU585",
39
45
  ROUTE_PARAM_MISMATCH = "PKU571",
40
46
  ROUTE_QUERY_MISMATCH = "PKU572",
41
47
  AUTH_DISABLED_REQUIRES_SESSIONLESS = "PKU573",
48
+ EXPOSED_FUNCTION_HAS_NO_GATE = "PKU574",
49
+ TAG_RESOLVES_TO_NO_MIDDLEWARE = "PKU575",
50
+ PERMISSIONS_IN_BODY_NOT_ALLOWED = "PKU576",
42
51
  DUPLICATE_FUNCTION_VERSION = "PKU850",
43
52
  DUPLICATE_FUNCTION_NAME = "PKU851",
44
53
  MANIFEST_MISSING = "PKU860",
@@ -49,6 +58,9 @@ export declare enum ErrorCode {
49
58
  MANIFEST_INTEGRITY_ERROR = "PKU865",
50
59
  MISSING_MODEL = "PKU145",
51
60
  INVALID_MODEL = "PKU146",
61
+ AGENT_TOOL_UNKNOWN_NAMESPACE = "PKU152",
62
+ AGENT_TOOL_NOT_FOUND = "PKU153",
63
+ AGENT_TOOL_MISSING_DESCRIPTION = "PKU154",
52
64
  SCHEMA_AND_WIRING_COLOCATED = "PKU490",
53
65
  SERVICES_NOT_DESTRUCTURED = "PKU410",
54
66
  WIRES_NOT_DESTRUCTURED = "PKU411",
@@ -58,7 +70,11 @@ export declare enum ErrorCode {
58
70
  PII_IN_OUTPUT = "PKU910",
59
71
  ADDON_WIRING_NOT_ALLOWED = "PKU920",
60
72
  ADDON_CONTRACT_HANDLERS_NOT_ALLOWED = "PKU921",
61
- RPC_INVOCATION_TYPE_CAST = "PKU940"
73
+ RPC_INVOCATION_TYPE_CAST = "PKU940",
74
+ SECRET_SERVICE_ALIASED = "PKU950",
75
+ SECRET_NOT_DECLARED = "PKU951",
76
+ SECRET_KEY_NOT_STATIC = "PKU952",
77
+ SECRET_REVEALED_INTO_SINK = "PKU953"
62
78
  }
63
79
  /**
64
80
  * Severity of a tracked, coded diagnostic. `critical` always blocks the build;
@@ -25,9 +25,11 @@ export var ErrorCode;
25
25
  ErrorCode["SCENARIO_BROWSER_STEP_NEEDS_ACTOR"] = "PKU677";
26
26
  ErrorCode["SCENARIO_STEP_TARGET_NOT_STATIC"] = "PKU678";
27
27
  ErrorCode["SCENARIO_NOT_EXTRACTABLE"] = "PKU679";
28
+ ErrorCode["SCENARIO_HAS_NO_ASSERTION"] = "PKU680";
28
29
  ErrorCode["WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED"] = "PKU600";
29
30
  ErrorCode["INVALID_DSL_WORKFLOW"] = "PKU641";
30
31
  ErrorCode["WORKFLOW_GRAPH_ADDON_NOT_WIRED"] = "PKU642";
32
+ ErrorCode["COMPLEX_WORKFLOW_NOT_ALLOWED"] = "PKU643";
31
33
  // Remote addon (wireRemoteAddon) validation
32
34
  ErrorCode["REMOTE_ADDON_NOT_DEV_DEPENDENCY"] = "PKU338";
33
35
  ErrorCode["REMOTE_ADDON_AUTH_UNRESOLVED"] = "PKU339";
@@ -35,6 +37,9 @@ export var ErrorCode;
35
37
  ErrorCode["DB_COLUMN_NAME_TYPE_CONTRADICTION"] = "PKU480";
36
38
  ErrorCode["DB_JSON_COLUMN_UNTYPED"] = "PKU481";
37
39
  ErrorCode["DB_FORMAT_ON_NON_STRING"] = "PKU482";
40
+ ErrorCode["DB_SECRET_COLUMN_STORED_PLAIN"] = "PKU483";
41
+ ErrorCode["DB_FORM_ON_NON_STRING"] = "PKU484";
42
+ ErrorCode["DB_DEFAULT_SCHEMA_NAME_COLLISION"] = "PKU485";
38
43
  // Configuration errors
39
44
  ErrorCode["SCHEMA_GENERATION_ERROR"] = "PKU456";
40
45
  ErrorCode["INLINE_SCHEMA"] = "PKU489";
@@ -43,10 +48,19 @@ export var ErrorCode;
43
48
  // Auth errors
44
49
  ErrorCode["DUPLICATE_AUTH_DEFINITION"] = "PKU581";
45
50
  ErrorCode["AUTH_NOT_EXPORTED"] = "PKU582";
51
+ // Single-declaration constructs — one call site per codebase.
52
+ // PKU583 (defineScope) and PKU584 (defineSystemRole) are retired, not free:
53
+ // scopes and roles held this rule until the generated `admin` tree made it
54
+ // unsatisfiable, and get it back once `admin` is a default scope nobody
55
+ // declares. Do not reuse those two numbers for anything else.
56
+ ErrorCode["DUPLICATE_PERSONAS_DEFINITION"] = "PKU585";
46
57
  // HTTP Route errors
47
58
  ErrorCode["ROUTE_PARAM_MISMATCH"] = "PKU571";
48
59
  ErrorCode["ROUTE_QUERY_MISMATCH"] = "PKU572";
49
60
  ErrorCode["AUTH_DISABLED_REQUIRES_SESSIONLESS"] = "PKU573";
61
+ ErrorCode["EXPOSED_FUNCTION_HAS_NO_GATE"] = "PKU574";
62
+ ErrorCode["TAG_RESOLVES_TO_NO_MIDDLEWARE"] = "PKU575";
63
+ ErrorCode["PERMISSIONS_IN_BODY_NOT_ALLOWED"] = "PKU576";
50
64
  // Versioning errors
51
65
  ErrorCode["DUPLICATE_FUNCTION_VERSION"] = "PKU850";
52
66
  ErrorCode["DUPLICATE_FUNCTION_NAME"] = "PKU851";
@@ -60,6 +74,10 @@ export var ErrorCode;
60
74
  // Model configuration errors
61
75
  ErrorCode["MISSING_MODEL"] = "PKU145";
62
76
  ErrorCode["INVALID_MODEL"] = "PKU146";
77
+ // Agent tool reference errors
78
+ ErrorCode["AGENT_TOOL_UNKNOWN_NAMESPACE"] = "PKU152";
79
+ ErrorCode["AGENT_TOOL_NOT_FOUND"] = "PKU153";
80
+ ErrorCode["AGENT_TOOL_MISSING_DESCRIPTION"] = "PKU154";
63
81
  // File structure errors
64
82
  ErrorCode["SCHEMA_AND_WIRING_COLOCATED"] = "PKU490";
65
83
  // Optimization diagnostics
@@ -75,4 +93,9 @@ export var ErrorCode;
75
93
  ErrorCode["ADDON_WIRING_NOT_ALLOWED"] = "PKU920";
76
94
  ErrorCode["ADDON_CONTRACT_HANDLERS_NOT_ALLOWED"] = "PKU921";
77
95
  ErrorCode["RPC_INVOCATION_TYPE_CAST"] = "PKU940";
96
+ // Secret boundary errors
97
+ ErrorCode["SECRET_SERVICE_ALIASED"] = "PKU950";
98
+ ErrorCode["SECRET_NOT_DECLARED"] = "PKU951";
99
+ ErrorCode["SECRET_KEY_NOT_STATIC"] = "PKU952";
100
+ ErrorCode["SECRET_REVEALED_INTO_SINK"] = "PKU953";
78
101
  })(ErrorCode || (ErrorCode = {}));
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ export { resolveDeployTarget } from './utils/resolve-deploy-target.js';
14
14
  export { generateCustomTypes, sanitizeTypeName, } from './utils/custom-types-generator.js';
15
15
  export { createEmptyManifest, serializeManifest, } from './utils/contract-hashes.js';
16
16
  export type { ContractEntry, VersionHashEntry, VersionValidateError, VersionManifest, VersionManifestEntry, } from './utils/contract-hashes.js';
17
- export type { InspectorFeature, InspectorFeatureEntry } from './types.js';
17
+ export type { InspectorFeature, InspectorFeatureEntry, } from './types.js';
18
18
  export { serializeMCPJson } from './utils/serialize-mcp-json.js';
19
19
  export type { OpenAPISpecInfo } from './utils/serialize-openapi-json.js';
20
20
  export { deserializeDslWorkflow, deserializeGraphWorkflow, deserializeAllDslWorkflows, } from './utils/workflow/dsl/index.js';
package/dist/inspector.js CHANGED
@@ -6,13 +6,17 @@ 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
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, validateScenarioServices, validateScenarioSteps, validateWorkflowGraphAddons, } from './utils/post-process.js';
9
+ import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateAgentToolReferences, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, validateRemoteAddonDependencies, validateRemoteAddonAuth, validateScopeReferences, validateSystemRoleScopes, validatePersonaRoles, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeSecretBrokers, validateNoSecretAliasServices, validateSecretUsage, computeDiagnostics, validateSchemaWiringSeparation, validateScenarioServices, validateScenarioSteps, validateWorkflowGraphAddons, } from './utils/post-process.js';
10
+ import { validateExposedFunctionsGated } from './utils/validate-exposed-functions-gated.js';
11
+ import { validateTagsResolveToMiddleware } from './utils/validate-tags-resolve-to-middleware.js';
12
+ import { annotateHttpRouteAuth } from './utils/annotate-http-route-auth.js';
10
13
  import { generateOpenAPISpec } from './utils/serialize-openapi-json.js';
11
14
  import { pikkuState } from '@pikku/core/internal';
12
15
  import { resolveLatestVersions } from './utils/resolve-versions.js';
13
16
  import { finalizeWorkflows } from './utils/workflow/graph/finalize-workflows.js';
14
17
  import { finalizeWorkflowHelperTypes, finalizeWorkflowWires, } from './utils/workflow/graph/finalize-workflow-wires.js';
15
18
  import { generateAllSchemas } from './utils/schema-generator.js';
19
+ import { extractSecretUsage } from './utils/extract-secret-usage.js';
16
20
  import { loadAddonFunctionsMeta, loadAddonSchemas, } from './utils/load-addon-functions-meta.js';
17
21
  import { computeContractHashes, extractContractsFromMeta, updateManifest, createEmptyManifest, validateContracts, } from './utils/contract-hashes.js';
18
22
  /**
@@ -132,6 +136,7 @@ export function getInitialInspectorState(rootDir) {
132
136
  secrets: {
133
137
  definitions: [],
134
138
  files: new Set(),
139
+ usage: new Map(),
135
140
  },
136
141
  credentials: {
137
142
  definitions: [],
@@ -141,6 +146,14 @@ export function getInitialInspectorState(rootDir) {
141
146
  definitions: [],
142
147
  files: new Set(),
143
148
  },
149
+ systemRoles: {
150
+ definitions: [],
151
+ files: new Set(),
152
+ },
153
+ personas: {
154
+ definitions: [],
155
+ files: new Set(),
156
+ },
144
157
  variables: {
145
158
  definitions: [],
146
159
  files: new Set(),
@@ -282,6 +295,12 @@ export const inspect = async (logger, routeFiles, options = {}) => {
282
295
  .sort((a, b) => a.fileName < b.fileName ? -1 : a.fileName > b.fileName ? 1 : 0);
283
296
  logger.debug(`Got source files in ${(performance.now() - startSourceFiles).toFixed(2)}ms`);
284
297
  const state = getInitialInspectorState(rootDir);
298
+ for (const sourceFile of sourceFiles) {
299
+ const usage = extractSecretUsage(sourceFile);
300
+ if (usage.keys.length > 0 || usage.dynamic.length > 0) {
301
+ state.secrets.usage.set(sourceFile.fileName, usage);
302
+ }
303
+ }
285
304
  // First sweep: add all functions
286
305
  const startSetup = performance.now();
287
306
  for (const sourceFile of sourceFiles) {
@@ -342,21 +361,34 @@ export const inspect = async (logger, routeFiles, options = {}) => {
342
361
  computeResolvedIOTypes(state);
343
362
  computeMiddlewareGroupsMeta(state);
344
363
  computePermissionsGroupsMeta(state);
364
+ computeSecretBrokers(state);
365
+ validateNoSecretAliasServices(logger, checker, state);
366
+ validateSecretUsage(logger, state);
345
367
  computeDiagnostics(state);
346
368
  validateSchemaWiringSeparation(logger, state);
347
369
  validateWorkflowGraphAddons(logger, state);
348
370
  validateScenarioServices(logger, state);
349
371
  validateScenarioSteps(logger, state);
372
+ // Needs every route, every function and every wireAddon declaration
373
+ // together, so it can only run once the whole program is inspected.
374
+ annotateHttpRouteAuth(state);
375
+ // Last of the validators: they read function meta and every wireAddon
376
+ // declaration together, so they need the whole program inspected.
377
+ validateExposedFunctionsGated(logger, state);
378
+ validateTagsResolveToMiddleware(logger, state);
350
379
  if (options.openAPI) {
351
380
  state.openAPISpec = await generateOpenAPISpec(logger, state.functions.meta, state.http.meta, state.schemas, options.openAPI.additionalInfo, pikkuState(null, 'misc', 'errors'));
352
381
  }
353
382
  validateAgentModels(logger, state);
383
+ validateAgentToolReferences(logger, state, options);
354
384
  validateSecretOverrides(logger, state);
355
385
  validateVariableOverrides(logger, state);
356
386
  validateCredentialOverrides(logger, state);
357
387
  validateRemoteAddonDependencies(logger, state);
358
388
  validateRemoteAddonAuth(logger, state);
359
389
  validateScopeReferences(logger, state);
390
+ validateSystemRoleScopes(logger, state);
391
+ validatePersonaRoles(logger, state);
360
392
  }
361
393
  state.program = program;
362
394
  return state;