@pikku/inspector 0.12.106 → 0.12.108

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 (96) hide show
  1. package/dist/add/add-auth.d.ts +1 -9
  2. package/dist/add/add-auth.js +64 -17
  3. package/dist/add/add-cli.js +4 -0
  4. package/dist/add/add-functions.js +27 -31
  5. package/dist/add/add-keyed-wiring.js +22 -2
  6. package/dist/add/add-middleware.js +17 -16
  7. package/dist/add/add-permission.d.ts +0 -3
  8. package/dist/add/add-permission.js +213 -22
  9. package/dist/add/add-scope.d.ts +1 -7
  10. package/dist/add/add-scope.js +1 -21
  11. package/dist/add/check-indirect-registrations.d.ts +9 -0
  12. package/dist/add/check-indirect-registrations.js +114 -0
  13. package/dist/error-codes.d.ts +1 -0
  14. package/dist/error-codes.js +1 -0
  15. package/dist/finalize.d.ts +10 -0
  16. package/dist/finalize.js +102 -0
  17. package/dist/index.d.ts +8 -6
  18. package/dist/index.js +6 -5
  19. package/dist/initial-state.d.ts +7 -0
  20. package/dist/initial-state.js +216 -0
  21. package/dist/inspector.d.ts +6 -4
  22. package/dist/inspector.js +28 -302
  23. package/dist/lite.d.ts +26 -0
  24. package/dist/lite.js +25 -0
  25. package/dist/source-file-cache.d.ts +11 -0
  26. package/dist/source-file-cache.js +34 -10
  27. package/dist/types-map.js +2 -1
  28. package/dist/types.d.ts +32 -0
  29. package/dist/utils/apply-declared-scopes.d.ts +8 -0
  30. package/dist/utils/apply-declared-scopes.js +21 -0
  31. package/dist/utils/canonical-type-string.d.ts +14 -0
  32. package/dist/utils/canonical-type-string.js +162 -0
  33. package/dist/utils/context-based-id.d.ts +7 -0
  34. package/dist/utils/context-based-id.js +9 -0
  35. package/dist/utils/custom-types-generator.d.ts +15 -1
  36. package/dist/utils/custom-types-generator.js +17 -5
  37. package/dist/utils/ensure-function-metadata.d.ts +0 -14
  38. package/dist/utils/ensure-function-metadata.js +4 -1
  39. package/dist/utils/extract-function-name.d.ts +1 -1
  40. package/dist/utils/extract-function-name.js +1 -3
  41. package/dist/utils/filter-inspector-state.js +31 -1
  42. package/dist/utils/get-property-value.d.ts +5 -0
  43. package/dist/utils/get-property-value.js +14 -0
  44. package/dist/utils/inspector-constants.d.ts +16 -0
  45. package/dist/utils/inspector-constants.js +16 -0
  46. package/dist/utils/post-process.d.ts +1 -2
  47. package/dist/utils/post-process.js +27 -27
  48. package/dist/utils/read-jsx-literal-text.js +31 -1
  49. package/dist/utils/resolve-callee-name.d.ts +27 -0
  50. package/dist/utils/resolve-callee-name.js +147 -0
  51. package/dist/utils/resolve-service-types.d.ts +8 -0
  52. package/dist/utils/resolve-service-types.js +21 -0
  53. package/dist/utils/schema-generator.js +29 -2
  54. package/dist/utils/serialize-inspector-state.d.ts +7 -1
  55. package/dist/utils/serialize-inspector-state.js +16 -1
  56. package/dist/utils/workflow/dsl/patterns.d.ts +1 -8
  57. package/dist/utils/workflow/dsl/patterns.js +1 -7
  58. package/dist/visit.js +2 -0
  59. package/package.json +25 -9
  60. package/CHANGELOG.md +0 -4368
  61. package/OPTIMIZATION-PLAN.md +0 -201
  62. package/knowledge/decisions/a-channel-with-auth-false-marks-its-connect-and-disconnect-sessionless.md +0 -22
  63. package/knowledge/decisions/a-declared-scope-id-never-shadows-a-definition.md +0 -20
  64. package/knowledge/decisions/a-dsl-step-virtual-user-is-the-literal-name-in-its-options.md +0 -27
  65. package/knowledge/decisions/a-feature-flags-anyof-is-read-from-the-ast-and-an-empty-list-is-refused.md +0 -20
  66. package/knowledge/decisions/a-non-literal-string-array-reads-as-absent.md +0 -25
  67. package/knowledge/decisions/a-permission-with-an-underscore-data-parameter-takes-no-data.md +0 -20
  68. package/knowledge/decisions/a-provider-login-virtual-user-is-never-runnable.md +0 -27
  69. package/knowledge/decisions/a-remote-addon-bound-credential-or-secret-must-be-wired.md +0 -27
  70. package/knowledge/decisions/a-route-auth-overrides-its-groups-auth.md +0 -22
  71. package/knowledge/decisions/a-route-requires-a-session-when-any-auth-signal-says-so.md +0 -29
  72. package/knowledge/decisions/a-webhook-source-with-verify-gets-a-singleton-secret-credential.md +0 -21
  73. package/knowledge/decisions/a-wire-with-auth-disabled-cannot-point-at-a-pikkufunc.md +0 -21
  74. package/knowledge/decisions/addon-contracts-cannot-wire-or-declare-handlers.md +0 -25
  75. package/knowledge/decisions/addon-declarations-name-the-secrets-and-credentials-the-app-lends-it.md +0 -19
  76. package/knowledge/decisions/an-inline-wiring-function-is-sessionless-unless-built-with-pikkufunc.md +0 -23
  77. package/knowledge/decisions/better-auth-admin-plugin-is-refused.md +0 -24
  78. package/knowledge/decisions/define-screens-scopes-must-be-string-literals.md +0 -18
  79. package/knowledge/decisions/index.md +0 -38
  80. package/knowledge/decisions/inspector-records-a-non-literal-global-grant-as-source-text.md +0 -20
  81. package/knowledge/decisions/only-one-pikku-better-auth-export-per-codebase.md +0 -19
  82. package/knowledge/decisions/scope-references-and-secret-aliases-are-critical-diagnostics.md +0 -23
  83. package/knowledge/decisions/scope-segments-may-not-be-wildcards-or-contain-colons.md +0 -23
  84. package/knowledge/decisions/shorthand-permission-entries-are-extracted.md +0 -22
  85. package/knowledge/decisions/system-roles-declare-literal-unqualified-scopes.md +0 -28
  86. package/knowledge/decisions/the-inspector-records-agent-permissions-only-to-aggregate-services.md +0 -14
  87. package/knowledge/decisions/vault-secrets-reach-no-sink-and-pii-stays-out-of-logs-and-webhooks.md +0 -30
  88. package/knowledge/decisions/webhook-gateway-wrappers-are-sessionless-and-gateway-auth-runs-inside-the-handler.md +0 -26
  89. package/knowledge/index.md +0 -14
  90. package/knowledge/questions/does-a-non-literal-remote-addon-auth-get-checked.md +0 -26
  91. package/knowledge/questions/index.md +0 -16
  92. package/knowledge/questions/is-an-incomplete-oauth2-block-dropped-without-a-log.md +0 -23
  93. package/knowledge/questions/should-app-functions-named-like-scenario-instrumentation-be-refused.md +0 -28
  94. package/knowledge/questions/should-the-openapi-document-default-every-operation-to-a-bearer-or-api-key.md +0 -22
  95. package/run-tests.sh +0 -68
  96. package/src/utils/test-data/inspector-state.json +0 -1687
@@ -1,14 +1,16 @@
1
1
  import * as ts from 'typescript';
2
- import { extractFunctionName } from '../utils/extract-function-name.js';
2
+ import { relative } from 'node:path';
3
+ import { extractFunctionName, isNamedExport, makeContextBasedId, } from '../utils/extract-function-name.js';
3
4
  import { extractServicesFromFunction, extractUsedWires, } from '../utils/extract-services.js';
4
5
  import { getPropertyValue } from '../utils/get-property-value.js';
5
6
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
6
- function isInsidePermissionContainer(node) {
7
+ import { PERMISSION_HELPERS, resolveCalleeName, } from '../utils/resolve-callee-name.js';
8
+ function isInsidePermissionContainer(node, checker) {
7
9
  let current = node.parent;
8
10
  while (current) {
9
11
  if (ts.isCallExpression(current) &&
10
- ts.isIdentifier(current.expression) &&
11
- current.expression.text === 'pikkuPermissionFactory') {
12
+ resolveCalleeName(current, checker, PERMISSION_HELPERS) ===
13
+ 'pikkuPermissionFactory') {
12
14
  return true;
13
15
  }
14
16
  current = current.parent;
@@ -16,20 +18,144 @@ function isInsidePermissionContainer(node) {
16
18
  return false;
17
19
  }
18
20
  /**
19
- * Inspect pikkuPermission, pikkuAuth, and pikkuPermissionFactory definitions.
21
+ * The id of a permission written inline in an `addGlobalPermission([...])`
22
+ * array, which has no variable to be named after. Derived from where it sits,
23
+ * so the registration and the definition agree on it without either having to
24
+ * be visited first.
20
25
  */
26
+ function inlineGlobalPermissionId(node, checker, rootDir) {
27
+ const array = node.parent;
28
+ if (!array || !ts.isArrayLiteralExpression(array))
29
+ return undefined;
30
+ const call = array.parent;
31
+ if (!call ||
32
+ !ts.isCallExpression(call) ||
33
+ call.arguments[0] !== array ||
34
+ resolveCalleeName(call, checker, PERMISSION_HELPERS) !==
35
+ 'addGlobalPermission') {
36
+ return undefined;
37
+ }
38
+ const sourceFile = node.getSourceFile();
39
+ return makeContextBasedId('global', 'permission', relative(rootDir, sourceFile.fileName), String(node.getStart()));
40
+ }
41
+ const unwrapExpression = (expression) => {
42
+ let current = expression;
43
+ while (ts.isParenthesizedExpression(current) ||
44
+ ts.isAsExpression(current) ||
45
+ ts.isSatisfiesExpression(current) ||
46
+ ts.isNonNullExpression(current) ||
47
+ ts.isTypeAssertionExpression(current)) {
48
+ current = current.expression;
49
+ }
50
+ return current;
51
+ };
52
+ /**
53
+ * Follows `addGlobalPermission`'s argument to the array or object literal it was
54
+ * written as: a literal itself, or a `const` (declared here or imported) that is
55
+ * initialised with one. `undefined` when it is built any other way.
56
+ */
57
+ const resolvePermissionLiteral = (argument, checker) => {
58
+ let current = unwrapExpression(argument);
59
+ for (let hops = 0; hops < 8; hops++) {
60
+ if (ts.isArrayLiteralExpression(current) ||
61
+ ts.isObjectLiteralExpression(current)) {
62
+ return current;
63
+ }
64
+ if (!ts.isIdentifier(current))
65
+ return undefined;
66
+ let symbol = checker.getSymbolAtLocation(current);
67
+ if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
68
+ symbol = checker.getAliasedSymbol(symbol);
69
+ }
70
+ const declaration = symbol?.valueDeclaration;
71
+ if (!declaration ||
72
+ !ts.isVariableDeclaration(declaration) ||
73
+ !declaration.initializer) {
74
+ return undefined;
75
+ }
76
+ current = unwrapExpression(declaration.initializer);
77
+ }
78
+ return undefined;
79
+ };
80
+ const isNamedExportedFunction = (declaration, checker) => {
81
+ if (!declaration.name)
82
+ return false;
83
+ const flags = ts.getCombinedModifierFlags(declaration);
84
+ if (flags & ts.ModifierFlags.Default)
85
+ return false;
86
+ if (flags & ts.ModifierFlags.Export)
87
+ return true;
88
+ const moduleSymbol = checker.getSymbolAtLocation(declaration.getSourceFile());
89
+ const symbol = checker.getSymbolAtLocation(declaration.name);
90
+ return (!!moduleSymbol &&
91
+ !!symbol &&
92
+ checker.getExportsOfModule(moduleSymbol).some((exported) => {
93
+ const resolved = exported.flags & ts.SymbolFlags.Alias
94
+ ? checker.getAliasedSymbol(exported)
95
+ : exported;
96
+ return resolved === symbol;
97
+ }));
98
+ };
99
+ const isImmediatelyInvoked = (fn) => {
100
+ let current = fn;
101
+ while (current.parent && ts.isParenthesizedExpression(current.parent)) {
102
+ current = current.parent;
103
+ }
104
+ return (!!current.parent &&
105
+ ts.isCallExpression(current.parent) &&
106
+ current.parent.expression === current);
107
+ };
108
+ /**
109
+ * How the generated permissions file makes this `addGlobalPermission` call run.
110
+ * Importing a file only evaluates its top level, so a call at the top level (or
111
+ * in a function invoked right where it is written) runs on import. A call inside
112
+ * an exported zero-argument function runs when the generated file calls that
113
+ * function. Anywhere else nothing ever calls it, so the gate would not exist.
114
+ */
115
+ const globalPermissionActivation = (call, checker) => {
116
+ let current = call.parent;
117
+ while (current && !ts.isSourceFile(current)) {
118
+ if (ts.isArrowFunction(current) ||
119
+ ts.isFunctionExpression(current) ||
120
+ ts.isFunctionDeclaration(current) ||
121
+ ts.isMethodDeclaration(current) ||
122
+ ts.isConstructorDeclaration(current) ||
123
+ ts.isGetAccessorDeclaration(current) ||
124
+ ts.isSetAccessorDeclaration(current)) {
125
+ if (isImmediatelyInvoked(current)) {
126
+ current = current.parent;
127
+ continue;
128
+ }
129
+ if (current.parameters.length > 0)
130
+ return { kind: 'unreachable' };
131
+ if ((ts.isArrowFunction(current) || ts.isFunctionExpression(current)) &&
132
+ ts.isVariableDeclaration(current.parent) &&
133
+ ts.isIdentifier(current.parent.name) &&
134
+ isNamedExport(current.parent, checker)) {
135
+ return { kind: 'factory', exportName: current.parent.name.text };
136
+ }
137
+ if (ts.isFunctionDeclaration(current) &&
138
+ current.name &&
139
+ isNamedExportedFunction(current, checker)) {
140
+ return { kind: 'factory', exportName: current.name.text };
141
+ }
142
+ return { kind: 'unreachable' };
143
+ }
144
+ current = current.parent;
145
+ }
146
+ return { kind: 'import' };
147
+ };
21
148
  export const addPermission = (logger, node, checker, state) => {
22
149
  if (!ts.isCallExpression(node))
23
150
  return;
24
- const { expression, arguments: args } = node;
25
- // only handle specific function calls
26
- if (!ts.isIdentifier(expression)) {
151
+ const { arguments: args } = node;
152
+ const callee = resolveCalleeName(node, checker, PERMISSION_HELPERS);
153
+ if (!callee)
27
154
  return;
28
- }
29
155
  // Handle pikkuPermission(...) - individual permission function definition
30
- if (expression.text === 'pikkuPermission') {
156
+ if (callee === 'pikkuPermission') {
31
157
  // Skip if nested inside pikkuPermissionFactory — the factory handler extracts services itself
32
- if (isInsidePermissionContainer(node))
158
+ if (isInsidePermissionContainer(node, checker))
33
159
  return;
34
160
  const arg = args[0];
35
161
  if (!arg)
@@ -70,9 +196,13 @@ export const addPermission = (logger, node, checker, state) => {
70
196
  pikkuFuncId = node.parent.name.text;
71
197
  }
72
198
  else {
73
- logger.error(`• pikkuPermission() must be assigned to a variable or object property. ` +
74
- `Extract it to a const: const myPermission = pikkuPermission(...)`);
75
- return;
199
+ const globalId = inlineGlobalPermissionId(node, checker, state.rootDir);
200
+ if (!globalId) {
201
+ logger.error(`• pikkuPermission() must be assigned to a variable or object property. ` +
202
+ `Extract it to a const: const myPermission = pikkuPermission(...)`);
203
+ return;
204
+ }
205
+ pikkuFuncId = globalId;
76
206
  }
77
207
  }
78
208
  const dataParam = actualHandler.parameters[1];
@@ -91,8 +221,8 @@ export const addPermission = (logger, node, checker, state) => {
91
221
  logger.debug(`• Found permission with services: ${services.services.join(', ')}${name ? ` (name: ${name})` : ''}${description ? ` (description: ${description})` : ''}${!requiresData ? ' (auth-only)' : ''}`);
92
222
  return;
93
223
  }
94
- if (expression.text === 'pikkuAuth') {
95
- if (isInsidePermissionContainer(node))
224
+ if (callee === 'pikkuAuth') {
225
+ if (isInsidePermissionContainer(node, checker))
96
226
  return;
97
227
  const arg = args[0];
98
228
  if (!arg)
@@ -137,9 +267,13 @@ export const addPermission = (logger, node, checker, state) => {
137
267
  pikkuFuncId = node.parent.name.text;
138
268
  }
139
269
  else {
140
- logger.error(`• pikkuAuth() must be assigned to a variable or object property. ` +
141
- `Extract it to a const: const myAuth = pikkuAuth(...)`);
142
- return;
270
+ const globalId = inlineGlobalPermissionId(node, checker, state.rootDir);
271
+ if (!globalId) {
272
+ logger.error(`• pikkuAuth() must be assigned to a variable or object property. ` +
273
+ `Extract it to a const: const myAuth = pikkuAuth(...)`);
274
+ return;
275
+ }
276
+ pikkuFuncId = globalId;
143
277
  }
144
278
  }
145
279
  state.permissions.definitions[pikkuFuncId] = {
@@ -156,7 +290,7 @@ export const addPermission = (logger, node, checker, state) => {
156
290
  return;
157
291
  }
158
292
  // Handle pikkuPermissionFactory(...) - permission factory function
159
- if (expression.text === 'pikkuPermissionFactory') {
293
+ if (callee === 'pikkuPermissionFactory') {
160
294
  const factoryNode = args[0];
161
295
  if (!factoryNode)
162
296
  return;
@@ -175,8 +309,8 @@ export const addPermission = (logger, node, checker, state) => {
175
309
  };
176
310
  const findPikkuPermissionCall = (node) => {
177
311
  if (ts.isCallExpression(node)) {
178
- const expr = node.expression;
179
- if (ts.isIdentifier(expr) && expr.text === 'pikkuPermission') {
312
+ if (resolveCalleeName(node, checker, PERMISSION_HELPERS) ===
313
+ 'pikkuPermission') {
180
314
  return node;
181
315
  }
182
316
  }
@@ -229,4 +363,61 @@ export const addPermission = (logger, node, checker, state) => {
229
363
  logger.debug(`• Found permission factory with services: ${services.services.join(', ')}`);
230
364
  return;
231
365
  }
366
+ if (callee === 'addGlobalPermission') {
367
+ const permissionsArg = args[0];
368
+ const sourceFile = node.getSourceFile().fileName;
369
+ let entries;
370
+ const literal = permissionsArg && resolvePermissionLiteral(permissionsArg, checker);
371
+ if (literal && ts.isArrayLiteralExpression(literal)) {
372
+ entries = literal.elements;
373
+ }
374
+ else if (literal) {
375
+ entries = literal.properties.flatMap((p) => ts.isPropertyAssignment(p)
376
+ ? [p.initializer]
377
+ : ts.isShorthandPropertyAssignment(p)
378
+ ? [p.name]
379
+ : []);
380
+ }
381
+ if (!entries) {
382
+ logger.warn(`• addGlobalPermission(...) in ${sourceFile} is not given an array or object it can follow to a literal, so the services its permissions use cannot be seen.`);
383
+ }
384
+ const definitionIds = (entries ?? []).flatMap((entry) => {
385
+ if (ts.isIdentifier(entry)) {
386
+ const { pikkuFuncId } = extractFunctionName(entry, checker, state.rootDir);
387
+ return [pikkuFuncId.startsWith('__temp_') ? entry.text : pikkuFuncId];
388
+ }
389
+ if (ts.isCallExpression(entry)) {
390
+ const inline = inlineGlobalPermissionId(entry, checker, state.rootDir);
391
+ if (inline)
392
+ return [inline];
393
+ if (ts.isPropertyAssignment(entry.parent) &&
394
+ ts.isIdentifier(entry.parent.name) &&
395
+ resolveCalleeName(entry, checker, PERMISSION_HELPERS)) {
396
+ return [entry.parent.name.text];
397
+ }
398
+ const { pikkuFuncId } = extractFunctionName(entry.expression, checker, state.rootDir);
399
+ return pikkuFuncId.startsWith('__temp_') &&
400
+ ts.isIdentifier(entry.expression)
401
+ ? [entry.expression.text]
402
+ : [pikkuFuncId];
403
+ }
404
+ return [];
405
+ });
406
+ const activation = globalPermissionActivation(node, checker);
407
+ if (activation.kind === 'unreachable') {
408
+ logger.warn(`• addGlobalPermission(...) in ${sourceFile} sits inside a function that nothing calls: only a call at the top level, or inside an exported function with no parameters, is run by the generated permissions file. This global permission is NOT registered. Export it as \`export const name = () => { addGlobalPermission(...) }\` or move it to the top level.`);
409
+ return;
410
+ }
411
+ const exportName = activation.kind === 'factory' ? activation.exportName : null;
412
+ const position = node.getStart();
413
+ if (!state.permissions.global.some((g) => g.sourceFile === sourceFile && g.position === position)) {
414
+ state.permissions.global.push({
415
+ sourceFile,
416
+ position,
417
+ exportName,
418
+ definitionIds,
419
+ });
420
+ }
421
+ logger.debug(`• Found global permissions in ${sourceFile}: [${definitionIds.join(', ')}]`);
422
+ }
232
423
  };
@@ -1,9 +1,3 @@
1
1
  import type { AddWiring } from '../types.js';
2
- /**
3
- * Folds `declareScopes` ids into `state.scopes.definitions`. An id the
4
- * definitions already hold (an addon's tree, a `defineScope`) is left alone;
5
- * a missing one is added as a bare node under its root, creating the root when
6
- * nothing declared it.
7
- */
8
- export declare const applyDeclaredScopes: (state: Parameters<AddWiring>[3]) => void;
2
+ export { applyDeclaredScopes } from '../utils/apply-declared-scopes.js';
9
3
  export declare const addScope: AddWiring;
@@ -111,27 +111,7 @@ const addDeclareScopes = (logger, node, state) => {
111
111
  }
112
112
  state.scopes.files.add(sourceFile);
113
113
  };
114
- /**
115
- * Folds `declareScopes` ids into `state.scopes.definitions`. An id the
116
- * definitions already hold (an addon's tree, a `defineScope`) is left alone;
117
- * a missing one is added as a bare node under its root, creating the root when
118
- * nothing declared it.
119
- */
120
- export const applyDeclaredScopes = (state) => {
121
- for (const [id, sourceFile] of state.scopes.declared ?? []) {
122
- const [rootName, ...rest] = id.split(SEPARATOR);
123
- let root = state.scopes.definitions.find((d) => d.name === rootName);
124
- if (!root) {
125
- root = { name: rootName, sourceFile };
126
- state.scopes.definitions.push(root);
127
- }
128
- let level = root;
129
- for (const segment of rest) {
130
- level.scopes ??= {};
131
- level = level.scopes[segment] ??= {};
132
- }
133
- }
134
- };
114
+ export { applyDeclaredScopes } from '../utils/apply-declared-scopes.js';
135
115
  export const addScope = (logger, node, checker, state, _options) => {
136
116
  if (!ts.isCallExpression(node)) {
137
117
  return;
@@ -0,0 +1,9 @@
1
+ import type { AddWiring } from '../types.js';
2
+ /**
3
+ * A middleware or permission helper the inspector can only see by its direct
4
+ * calls. Passed around as a value — into an array, through a ternary, as an
5
+ * argument — whatever it registers is invisible to codegen, and a gate that
6
+ * codegen never imports is a gate that never runs. Say so rather than
7
+ * generate an app that is silently open.
8
+ */
9
+ export declare const checkIndirectRegistrations: AddWiring;
@@ -0,0 +1,114 @@
1
+ import * as ts from 'typescript';
2
+ import { ErrorCode } from '../error-codes.js';
3
+ import { MIDDLEWARE_HELPERS, PERMISSION_HELPERS, calleeNames, isNamespaceReceiver, } from '../utils/resolve-callee-name.js';
4
+ const isGateHelper = (name) => MIDDLEWARE_HELPERS.has(name) || PERMISSION_HELPERS.has(name);
5
+ const localNamesCache = new WeakMap();
6
+ /** The names a file can reach a gate helper by: its own, and its import aliases. */
7
+ const localGateNames = (sourceFile) => {
8
+ const cached = localNamesCache.get(sourceFile);
9
+ if (cached)
10
+ return cached;
11
+ const names = new Set([...MIDDLEWARE_HELPERS, ...PERMISSION_HELPERS]);
12
+ for (const statement of sourceFile.statements) {
13
+ const bindings = ts.isImportDeclaration(statement)
14
+ ? statement.importClause?.namedBindings
15
+ : undefined;
16
+ if (!bindings || !ts.isNamedImports(bindings))
17
+ continue;
18
+ for (const element of bindings.elements) {
19
+ if (isGateHelper((element.propertyName ?? element.name).text)) {
20
+ names.add(element.name.text);
21
+ }
22
+ }
23
+ }
24
+ localNamesCache.set(sourceFile, names);
25
+ return names;
26
+ };
27
+ /**
28
+ * Whether a reference to a gate helper sits where the inspector already
29
+ * accounts for it: called directly, imported or re-exported, bound to another
30
+ * `const` name, named as a declaration, or used as a type.
31
+ */
32
+ const isAccountedFor = (reference) => {
33
+ const parent = reference.parent;
34
+ if (ts.isCallExpression(parent) && parent.expression === reference) {
35
+ return true;
36
+ }
37
+ if (ts.isImportSpecifier(parent) ||
38
+ ts.isExportSpecifier(parent) ||
39
+ ts.isImportClause(parent) ||
40
+ ts.isNamespaceImport(parent) ||
41
+ ts.isExportAssignment(parent) ||
42
+ ts.isQualifiedName(parent) ||
43
+ ts.isTypeNode(parent) ||
44
+ ts.isExpressionWithTypeArguments(parent)) {
45
+ return true;
46
+ }
47
+ if (ts.isVariableDeclaration(parent) &&
48
+ parent.initializer === reference &&
49
+ ts.getCombinedNodeFlags(parent) & ts.NodeFlags.Const) {
50
+ return true;
51
+ }
52
+ if ('name' in parent && parent.name === reference) {
53
+ return true;
54
+ }
55
+ for (let current = parent; current; current = current.parent) {
56
+ if ((ts.isVariableDeclaration(current) ||
57
+ ts.isFunctionDeclaration(current)) &&
58
+ current.name &&
59
+ ts.isIdentifier(current.name) &&
60
+ isGateHelper(current.name.text)) {
61
+ return true;
62
+ }
63
+ }
64
+ return false;
65
+ };
66
+ /**
67
+ * A middleware or permission helper the inspector can only see by its direct
68
+ * calls. Passed around as a value — into an array, through a ternary, as an
69
+ * argument — whatever it registers is invisible to codegen, and a gate that
70
+ * codegen never imports is a gate that never runs. Say so rather than
71
+ * generate an app that is silently open.
72
+ */
73
+ export const checkIndirectRegistrations = (logger, node, checker) => {
74
+ if (!ts.isIdentifier(node))
75
+ return;
76
+ const sourceFile = node.getSourceFile();
77
+ if (sourceFile.isDeclarationFile)
78
+ return;
79
+ if (!localGateNames(sourceFile).has(node.text))
80
+ return;
81
+ let reference = node;
82
+ if (ts.isPropertyAccessExpression(node.parent)) {
83
+ if (node.parent.name !== node)
84
+ return;
85
+ if (!isNamespaceReceiver(node.parent.expression, checker))
86
+ return;
87
+ reference = node.parent;
88
+ }
89
+ if (isAccountedFor(reference))
90
+ return;
91
+ const helper = [...calleeNames(reference, checker)]
92
+ .reverse()
93
+ .find(isGateHelper);
94
+ if (!helper)
95
+ return;
96
+ const symbol = checker.getSymbolAtLocation(node);
97
+ if (symbol?.valueDeclaration && ts.isParameter(symbol.valueDeclaration)) {
98
+ return;
99
+ }
100
+ const type = checker.getTypeAtLocation(reference);
101
+ if (type.getCallSignatures().length === 0 &&
102
+ !(type.flags & ts.TypeFlags.Any)) {
103
+ return;
104
+ }
105
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
106
+ logger.diagnostic({
107
+ severity: 'error',
108
+ code: ErrorCode.REGISTRATION_NOT_A_DIRECT_CALL,
109
+ message: `'${reference.getText()}' (${helper}) is used as a value at ${sourceFile.fileName}:${line + 1}. ` +
110
+ `The inspector records middleware and permissions only where ${helper} is called directly, ` +
111
+ `so whatever this registers is never imported by the generated code and does not run. ` +
112
+ `Call ${helper}(...) directly.`,
113
+ });
114
+ };
@@ -61,6 +61,7 @@ export declare enum ErrorCode {
61
61
  EXPOSED_FUNCTION_HAS_NO_GATE = "PKU574",
62
62
  TAG_RESOLVES_TO_NO_MIDDLEWARE = "PKU575",
63
63
  PERMISSIONS_IN_BODY_NOT_ALLOWED = "PKU576",
64
+ REGISTRATION_NOT_A_DIRECT_CALL = "PKU577",
64
65
  DUPLICATE_FUNCTION_VERSION = "PKU850",
65
66
  DUPLICATE_FUNCTION_NAME = "PKU851",
66
67
  MANIFEST_MISSING = "PKU860",
@@ -74,6 +74,7 @@ export var ErrorCode;
74
74
  ErrorCode["EXPOSED_FUNCTION_HAS_NO_GATE"] = "PKU574";
75
75
  ErrorCode["TAG_RESOLVES_TO_NO_MIDDLEWARE"] = "PKU575";
76
76
  ErrorCode["PERMISSIONS_IN_BODY_NOT_ALLOWED"] = "PKU576";
77
+ ErrorCode["REGISTRATION_NOT_A_DIRECT_CALL"] = "PKU577";
77
78
  // Versioning errors
78
79
  ErrorCode["DUPLICATE_FUNCTION_VERSION"] = "PKU850";
79
80
  ErrorCode["DUPLICATE_FUNCTION_NAME"] = "PKU851";
@@ -0,0 +1,10 @@
1
+ import type { InspectorState, InspectorLogger, InspectorOptions } from './types.js';
2
+ export { serializeInspectorState, deserializeInspectorState, } from './utils/serialize-inspector-state.js';
3
+ export type { SerializableInspectorState } from './utils/serialize-inspector-state.js';
4
+ /**
5
+ * The checker-free half of an inspection: everything that derives from the
6
+ * state analysis produced. It never loads `typescript`, so it runs the same
7
+ * against a live analysis and against a state deserialized from another
8
+ * engine.
9
+ */
10
+ export declare const finalizeInspectorState: <S extends Omit<InspectorState, "typesLookup">>(logger: InspectorLogger, analyzed: S, options?: InspectorOptions) => Promise<S>;
@@ -0,0 +1,102 @@
1
+ import { performance } from 'perf_hooks';
2
+ import { applyDeclaredScopes } from './utils/apply-declared-scopes.js';
3
+ import { getFilesAndMethods } from './utils/get-files-and-methods.js';
4
+ import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateAgentToolReferences, validateAgentScorerReferences, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, validateAddonUses, validateAddonContentGrants, validateRemoteAddonDependencies, validateRemoteAddonAuth, validateScopeReferences, validateSystemRoleScopes, validateVirtualUserRoles, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, validateNoSecretAliasServices, registerDerivedOAuth2AppSecrets, validateSecretUsage, computeDiagnostics, validateSchemaReferences, validateSchemaWiringSeparation, validateScenarioServices, validateScenarioSteps, validateScenarioFeatures, validateWorkflowGraphAddons, } from './utils/post-process.js';
5
+ import { validateExposedFunctionsGated } from './utils/validate-exposed-functions-gated.js';
6
+ import { validateTagsResolveToMiddleware } from './utils/validate-tags-resolve-to-middleware.js';
7
+ import { validateSingleStepWorkflows } from './utils/validate-single-step-workflows.js';
8
+ import { annotateHttpRouteAuth } from './utils/annotate-http-route-auth.js';
9
+ import { generateOpenAPISpec } from './utils/serialize-openapi-json.js';
10
+ import { pikkuState } from '@pikku/core/state';
11
+ import { finalizeWorkflows } from './utils/workflow/graph/finalize-workflows.js';
12
+ import { finalizeWorkflowHelperTypes, finalizeWorkflowWires, } from './utils/workflow/graph/finalize-workflow-wires.js';
13
+ import { loadAddonSchemas } from './utils/load-addon-functions-meta.js';
14
+ import { computeContractHashes, extractContractsFromMeta, updateManifest, createEmptyManifest, validateContracts, } from './utils/contract-hashes.js';
15
+ export { serializeInspectorState, deserializeInspectorState, } from './utils/serialize-inspector-state.js';
16
+ /**
17
+ * The checker-free half of an inspection: everything that derives from the
18
+ * state analysis produced. It never loads `typescript`, so it runs the same
19
+ * against a live analysis and against a state deserialized from another
20
+ * engine.
21
+ */
22
+ export const finalizeInspectorState = async (logger, analyzed, options = {}) => {
23
+ const state = analyzed;
24
+ if (!options.setupOnly) {
25
+ if (options.schemaConfig) {
26
+ computeContractHashes(state.schemas, state.functions.typesMap, state.functions.meta);
27
+ computeRequiredSchemas(state, options);
28
+ }
29
+ // Re-load addon schemas (generateAllSchemas replaces state.schemas)
30
+ await loadAddonSchemas(logger, state);
31
+ // After the addon merge, not before: an addon supplies its own schemas here,
32
+ // and checking earlier would report every one of them as unresolved.
33
+ if (options.schemaConfig) {
34
+ validateSchemaReferences(logger, state);
35
+ }
36
+ state.manifest.initial = options.manifest ?? null;
37
+ const contracts = extractContractsFromMeta(state.functions.meta);
38
+ const baseManifest = state.manifest.initial ?? createEmptyManifest();
39
+ state.manifest.current = updateManifest(baseManifest, contracts);
40
+ state.manifest.errors = validateContracts(baseManifest, contracts).errors;
41
+ finalizeWorkflows(state);
42
+ finalizeWorkflowHelperTypes(state);
43
+ finalizeWorkflowWires(state);
44
+ }
45
+ // Populate filesAndMethods
46
+ const startFilesAndMethods = performance.now();
47
+ const { result, errors } = getFilesAndMethods(state, options.types);
48
+ state.filesAndMethods = result;
49
+ state.filesAndMethodsErrors = errors;
50
+ logger.debug(`Get files and methods completed in ${(performance.now() - startFilesAndMethods).toFixed(2)}ms`);
51
+ if (!options.setupOnly) {
52
+ // Everything below re-derives from the state this pass just built, so it
53
+ // supersedes whatever an earlier pass concluded about a source graph the
54
+ // run has since finished generating into.
55
+ logger.beginValidationPass?.();
56
+ const startAggregate = performance.now();
57
+ // Apply the inspected auth handler service set before aggregation so it
58
+ // flows into requiredServices (the generated handler's own func is opaque).
59
+ stampAuthHandlerServices(state);
60
+ aggregateRequiredServices(state);
61
+ logger.debug(`Aggregate required services completed in ${(performance.now() - startAggregate).toFixed(2)}ms`);
62
+ computeResolvedIOTypes(state);
63
+ computeMiddlewareGroupsMeta(state);
64
+ computePermissionsGroupsMeta(state);
65
+ validateNoSecretAliasServices(logger, state);
66
+ registerDerivedOAuth2AppSecrets(state);
67
+ validateSecretUsage(logger, state);
68
+ computeDiagnostics(state);
69
+ validateSchemaWiringSeparation(logger, state);
70
+ validateWorkflowGraphAddons(logger, state);
71
+ validateSingleStepWorkflows(logger, state);
72
+ validateScenarioServices(logger, state);
73
+ validateScenarioSteps(logger, state);
74
+ validateScenarioFeatures(logger, state);
75
+ // Needs every route, every function and every wireAddon declaration
76
+ // together, so it can only run once the whole program is inspected.
77
+ annotateHttpRouteAuth(state);
78
+ // Last of the validators: they read function meta and every wireAddon
79
+ // declaration together, so they need the whole program inspected.
80
+ validateExposedFunctionsGated(logger, state);
81
+ validateTagsResolveToMiddleware(logger, state);
82
+ if (options.openAPI) {
83
+ state.openAPISpec = await generateOpenAPISpec(logger, state.functions.meta, state.http.meta, state.schemas, options.openAPI.additionalInfo, pikkuState(null, 'misc', 'errors'));
84
+ }
85
+ validateAgentModels(logger, state, options.modelAliases);
86
+ validateAgentToolReferences(logger, state, options);
87
+ validateAgentScorerReferences(logger, state);
88
+ validateSecretOverrides(logger, state);
89
+ validateVariableOverrides(logger, state);
90
+ validateCredentialOverrides(logger, state);
91
+ validateAddonUses(logger, state);
92
+ validateAddonContentGrants(logger, state);
93
+ validateRemoteAddonDependencies(logger, state);
94
+ validateRemoteAddonAuth(logger, state);
95
+ applyDeclaredScopes(state);
96
+ validateScopeReferences(logger, state);
97
+ validateSystemRoleScopes(logger, state);
98
+ validateVirtualUserRoles(logger, state);
99
+ logger.endValidationPass?.();
100
+ }
101
+ return analyzed;
102
+ };
package/dist/index.d.ts CHANGED
@@ -1,17 +1,20 @@
1
- export { inspect, getInitialInspectorState } from './inspector.js';
1
+ export { inspect, analyzeProgram } from './inspector.js';
2
+ export { getInitialInspectorState } from './initial-state.js';
3
+ export { finalizeInspectorState } from './finalize.js';
2
4
  export type { TypesMap } from './types-map.js';
3
- export type { AddonConfig, AuthDefinition, CredentialOverrideMeta, ScreensManifestMeta, AddonScreenMeta, InspectorDiagnostic, InspectorFilters, InspectorHTTPState, InspectorLogger, InspectorMiddlewareState, InspectorOptions, InspectorPermissionState, InspectorState, MetaInputTypes, MiddlewareGroupMeta, MiddlewareRegistration, SchemaRef, } from './types.js';
5
+ export type { AddonConfig, AuthDefinition, CredentialOverrideMeta, ScreensManifestMeta, AddonScreenMeta, InspectorDiagnostic, InspectorFilters, InspectorHTTPState, InspectorLogger, InspectorMiddlewareState, InspectorOptions, InspectorPermissionState, InspectorServiceTypes, InspectorState, MetaInputTypes, MiddlewareGroupMeta, MiddlewareRegistration, SchemaRef, } from './types.js';
4
6
  export type { FilesAndMethodsErrors } from './utils/get-files-and-methods.js';
5
7
  export { ErrorCode } from './error-codes.js';
6
8
  export type { DiagnosticSeverity, CodedDiagnostic } from './error-codes.js';
7
- export { AUTH_HANDLER_FUNC_ID } from './add/add-auth.js';
9
+ export { AUTH_HANDLER_FUNC_ID } from './utils/inspector-constants.js';
8
10
  export { SCENARIO_INSTRUMENTATION_FUNCTIONS, isScenarioInstrumentationFunction, } from './add/scenario-instrumentation.js';
9
11
  export type { ScenarioInstrumentationFunction } from './add/scenario-instrumentation.js';
10
12
  export { serializeInspectorState, deserializeInspectorState, } from './utils/serialize-inspector-state.js';
11
13
  export type { SerializableInspectorState } from './utils/serialize-inspector-state.js';
12
14
  export { filterInspectorState } from './utils/filter-inspector-state.js';
13
15
  export { incompatibleServicesFor, resolveDeployTarget, } from './utils/resolve-deploy-target.js';
14
- export { generateCustomTypes, sanitizeTypeName, } from './utils/custom-types-generator.js';
16
+ export { generateCustomTypes, customTypeNames, sanitizeTypeName, } from './utils/custom-types-generator.js';
17
+ export type { GenerateCustomTypesOptions } from './utils/custom-types-generator.js';
15
18
  export { unresolvedSchemaReferences } from './utils/post-process.js';
16
19
  export type { SurfaceUsageCounts } from './utils/extract-surface-usage.js';
17
20
  export { createEmptyManifest, serializeManifest, } from './utils/contract-hashes.js';
@@ -19,11 +22,10 @@ export type { ContractEntry, VersionHashEntry, VersionValidateError, VersionMani
19
22
  export type { InspectorFeature, InspectorFeatureEntry } from './types.js';
20
23
  export { serializeMCPJson } from './utils/serialize-mcp-json.js';
21
24
  export type { OpenAPISpecInfo } from './utils/serialize-openapi-json.js';
22
- export { deserializeDslWorkflow, deserializeGraphWorkflow, deserializeAllDslWorkflows, } from './utils/workflow/dsl/index.js';
25
+ export { deserializeDslWorkflow, deserializeGraphWorkflow, deserializeAllDslWorkflows, } from './utils/workflow/dsl/deserialize-dsl-workflow.js';
23
26
  export { getFilesAndMethods } from './utils/get-files-and-methods.js';
24
27
  export { resolveFunctionMeta } from './utils/resolve-function-meta.js';
25
28
  export type { SerializedWorkflowGraph, SerializedWorkflowGraphs, } from './utils/workflow/graph/index.js';
26
- export { resolveCoreType } from './utils/resolve-core-type.js';
27
29
  export { readModuleSpecifiers } from './utils/read-module-specifiers.js';
28
30
  export { readJsxLiteralText } from './utils/read-jsx-literal-text.js';
29
31
  export { readTsconfigOutDir } from './utils/read-tsconfig-out-dir.js';