@pikku/inspector 0.12.22 → 0.12.23

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 (40) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/add/add-addon-bans.d.ts +7 -0
  3. package/dist/add/add-addon-bans.js +65 -0
  4. package/dist/add/add-channel.js +47 -6
  5. package/dist/add/add-cli.js +17 -0
  6. package/dist/add/add-http-route.d.ts +11 -1
  7. package/dist/add/add-http-route.js +37 -0
  8. package/dist/add/add-http-routes.d.ts +0 -3
  9. package/dist/add/add-http-routes.js +179 -36
  10. package/dist/error-codes.d.ts +3 -1
  11. package/dist/error-codes.js +3 -0
  12. package/dist/inspector.js +17 -5
  13. package/dist/types.d.ts +43 -1
  14. package/dist/utils/get-exported-variable-name.d.ts +2 -0
  15. package/dist/utils/get-exported-variable-name.js +34 -0
  16. package/dist/utils/load-addon-functions-meta.js +98 -0
  17. package/dist/utils/resolve-addon-package.js +3 -1
  18. package/dist/utils/resolve-ref-contract.d.ts +21 -0
  19. package/dist/utils/resolve-ref-contract.js +46 -0
  20. package/dist/utils/serialize-inspector-state.d.ts +1 -0
  21. package/dist/utils/serialize-inspector-state.js +9 -0
  22. package/dist/visit.js +24 -19
  23. package/package.json +1 -1
  24. package/src/add/add-addon-bans.ts +84 -0
  25. package/src/add/add-channel.ts +66 -7
  26. package/src/add/add-cli.ts +30 -0
  27. package/src/add/add-http-route.ts +75 -1
  28. package/src/add/add-http-routes.ts +283 -41
  29. package/src/add/addon-bans.test.ts +121 -0
  30. package/src/add/addon-contracts.test.ts +221 -0
  31. package/src/error-codes.ts +4 -0
  32. package/src/inspector.ts +17 -5
  33. package/src/types.ts +65 -1
  34. package/src/utils/get-exported-variable-name.ts +48 -0
  35. package/src/utils/load-addon-functions-meta.ts +164 -0
  36. package/src/utils/resolve-addon-package.ts +6 -1
  37. package/src/utils/resolve-ref-contract.ts +71 -0
  38. package/src/utils/serialize-inspector-state.ts +10 -0
  39. package/src/visit.ts +26 -19
  40. package/tsconfig.tsbuildinfo +1 -1
package/dist/inspector.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as ts from 'typescript';
2
2
  import { performance } from 'perf_hooks';
3
+ import { resolve } from 'path';
3
4
  import { visitSetup, visitRoutes } from './visit.js';
4
5
  import { TypesMap } from './types-map.js';
5
6
  import { getFilesAndMethods } from './utils/get-files-and-methods.js';
@@ -185,10 +186,19 @@ export function getInitialInspectorState(rootDir) {
185
186
  openAPISpec: null,
186
187
  diagnostics: [],
187
188
  addonFunctions: {},
189
+ exportedContracts: {
190
+ http: {},
191
+ cli: {},
192
+ channel: {},
193
+ addonHttp: {},
194
+ addonCli: {},
195
+ addonChannel: {},
196
+ },
188
197
  program: null,
189
198
  };
190
199
  }
191
200
  export const inspect = async (logger, routeFiles, options = {}) => {
201
+ const normalizedRouteFiles = routeFiles.map((file) => resolve(file));
192
202
  const compilerOptions = {
193
203
  target: ts.ScriptTarget.ESNext,
194
204
  module: ts.ModuleKind.Node16,
@@ -200,14 +210,14 @@ export const inspect = async (logger, routeFiles, options = {}) => {
200
210
  checkJs: false,
201
211
  };
202
212
  const startProgram = performance.now();
203
- const program = ts.createProgram(routeFiles, compilerOptions, undefined, // host
213
+ const program = ts.createProgram(normalizedRouteFiles, compilerOptions, undefined, // host
204
214
  options.oldProgram);
205
- logger.debug(`Created program in ${(performance.now() - startProgram).toFixed(0)}ms (${routeFiles.length} files${options.oldProgram ? ', incremental' : ''})`);
215
+ logger.debug(`Created program in ${(performance.now() - startProgram).toFixed(0)}ms (${normalizedRouteFiles.length} files${options.oldProgram ? ', incremental' : ''})`);
206
216
  const startChecker = performance.now();
207
217
  const checker = program.getTypeChecker();
208
218
  logger.debug(`Got type checker in ${(performance.now() - startChecker).toFixed(2)}ms`);
209
219
  // Use provided rootDir or infer from source files
210
- const rootDir = options.rootDir || findCommonAncestor(routeFiles);
220
+ const rootDir = options.rootDir || findCommonAncestor(normalizedRouteFiles);
211
221
  const startSourceFiles = performance.now();
212
222
  // node_modules under rootDir (e.g. a locally-installed addon) is a
213
223
  // dependency, not project source — scanning it double-counts the addon's
@@ -221,7 +231,8 @@ export const inspect = async (logger, routeFiles, options = {}) => {
221
231
  // First sweep: add all functions
222
232
  const startSetup = performance.now();
223
233
  for (const sourceFile of sourceFiles) {
224
- ts.forEachChild(sourceFile, (child) => visitSetup(logger, checker, child, state, options));
234
+ const sourceOptions = { ...options, sourceFile };
235
+ ts.forEachChild(sourceFile, (child) => visitSetup(logger, checker, child, state, sourceOptions));
225
236
  }
226
237
  logger.debug(`Visit setup phase completed in ${(performance.now() - startSetup).toFixed(0)}ms`);
227
238
  // Load addon function metadata so wirings can reference addon functions
@@ -230,7 +241,8 @@ export const inspect = async (logger, routeFiles, options = {}) => {
230
241
  // Second sweep: add all transports
231
242
  const startRoutes = performance.now();
232
243
  for (const sourceFile of sourceFiles) {
233
- ts.forEachChild(sourceFile, (child) => visitRoutes(logger, checker, child, state, options));
244
+ const sourceOptions = { ...options, sourceFile };
245
+ ts.forEachChild(sourceFile, (child) => visitRoutes(logger, checker, child, state, sourceOptions));
234
246
  }
235
247
  logger.debug(`Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(0)}ms`);
236
248
  resolveLatestVersions(state, logger);
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type * as ts from 'typescript';
2
- import type { ChannelsMeta } from '@pikku/core/channel';
2
+ import type { ChannelMessageMeta, ChannelsMeta } from '@pikku/core/channel';
3
3
  import type { GatewaysMeta } from '@pikku/core/gateway';
4
4
  import type { HTTPWiringsMeta } from '@pikku/core/http';
5
5
  import type { ScheduledTasksMeta } from '@pikku/core/scheduler';
@@ -9,6 +9,7 @@ import type { WorkflowsMeta } from '@pikku/core/workflow';
9
9
  import type { MCPResourceMeta, MCPToolMeta, MCPPromptMeta } from '@pikku/core/mcp';
10
10
  import type { AIAgentMeta } from '@pikku/core/ai-agent';
11
11
  import type { CLIMeta } from '@pikku/core/cli';
12
+ import type { CLICommandMeta } from '@pikku/core/cli';
12
13
  import type { NodesMeta } from '@pikku/core/node';
13
14
  import type { SecretDefinitions } from '@pikku/core/secret';
14
15
  import type { CredentialDefinitions } from '@pikku/core/credential';
@@ -80,6 +81,45 @@ export interface InspectorChannelState {
80
81
  meta: ChannelsMeta;
81
82
  files: Set<string>;
82
83
  }
84
+ export interface ExportedHTTPRouteFunctionMeta {
85
+ pikkuFuncId: string;
86
+ packageName?: string;
87
+ }
88
+ export interface ExportedHTTPRouteConfigMeta {
89
+ method: string;
90
+ route: string;
91
+ func: ExportedHTTPRouteFunctionMeta;
92
+ auth?: boolean;
93
+ tags?: string[];
94
+ sse?: boolean;
95
+ contentType?: string;
96
+ timeout?: number;
97
+ headers?: Record<string, string>;
98
+ }
99
+ export interface ExportedHTTPRoutesGroupMeta {
100
+ basePath?: string;
101
+ tags?: string[];
102
+ auth?: boolean;
103
+ routes: ExportedHTTPRouteMapMeta;
104
+ }
105
+ export type ExportedHTTPRouteEntryMeta = ExportedHTTPRouteConfigMeta | ExportedHTTPRoutesGroupMeta | ExportedHTTPRouteMapMeta;
106
+ export interface ExportedHTTPRouteMapMeta {
107
+ [key: string]: ExportedHTTPRouteEntryMeta;
108
+ }
109
+ export type ExportedHTTPContractsMeta = Record<string, ExportedHTTPRoutesGroupMeta>;
110
+ export interface ExportedChannelRouteMeta extends ChannelMessageMeta {
111
+ auth?: boolean;
112
+ }
113
+ export type ExportedChannelContractsMeta = Record<string, Record<string, ExportedChannelRouteMeta>>;
114
+ export type ExportedCLIContractsMeta = Record<string, Record<string, CLICommandMeta>>;
115
+ export interface InspectorExportedContractsState {
116
+ http: ExportedHTTPContractsMeta;
117
+ cli: ExportedCLIContractsMeta;
118
+ channel: ExportedChannelContractsMeta;
119
+ addonHttp: Record<string, ExportedHTTPContractsMeta>;
120
+ addonCli: Record<string, ExportedCLIContractsMeta>;
121
+ addonChannel: Record<string, ExportedChannelContractsMeta>;
122
+ }
83
123
  export interface InspectorMiddlewareDefinition {
84
124
  services: FunctionServicesMeta;
85
125
  wires?: FunctionWiresMeta;
@@ -167,6 +207,7 @@ export type InspectorOptions = Partial<{
167
207
  setupOnly: boolean;
168
208
  rootDir: string;
169
209
  isAddon: boolean;
210
+ sourceFile: ts.SourceFile;
170
211
  types: Partial<{
171
212
  configFileType: string;
172
213
  userSessionType: string;
@@ -444,5 +485,6 @@ export interface InspectorState {
444
485
  openAPISpec: Record<string, any> | null;
445
486
  diagnostics: InspectorDiagnostic[];
446
487
  addonFunctions: Record<string, FunctionsMeta>;
488
+ exportedContracts: InspectorExportedContractsState;
447
489
  program: ts.Program | null;
448
490
  }
@@ -0,0 +1,2 @@
1
+ import * as ts from 'typescript';
2
+ export declare const getExportedVariableName: (node: ts.Node, sourceFile: ts.SourceFile | undefined) => string | null;
@@ -0,0 +1,34 @@
1
+ import * as ts from 'typescript';
2
+ const isExportedVariableStatement = (statement) => ts.isVariableStatement(statement) &&
3
+ (statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ??
4
+ false);
5
+ export const getExportedVariableName = (node, sourceFile) => {
6
+ if (!sourceFile) {
7
+ return null;
8
+ }
9
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
10
+ for (const statement of sourceFile.statements) {
11
+ if (!isExportedVariableStatement(statement))
12
+ continue;
13
+ for (const declaration of statement.declarationList.declarations) {
14
+ if (declaration === node) {
15
+ return node.name.text;
16
+ }
17
+ }
18
+ }
19
+ }
20
+ if (!ts.isCallExpression(node)) {
21
+ return null;
22
+ }
23
+ for (const statement of sourceFile.statements) {
24
+ if (!isExportedVariableStatement(statement))
25
+ continue;
26
+ for (const declaration of statement.declarationList.declarations) {
27
+ if (ts.isIdentifier(declaration.name) &&
28
+ declaration.initializer === node) {
29
+ return declaration.name.text;
30
+ }
31
+ }
32
+ }
33
+ return null;
34
+ };
@@ -1,6 +1,74 @@
1
1
  import { readFile, readdir } from 'fs/promises';
2
2
  import { createRequire } from 'module';
3
3
  import { join, dirname } from 'path';
4
+ const isHTTPRouteConfig = (value) => typeof value === 'object' &&
5
+ value !== null &&
6
+ 'method' in value &&
7
+ 'func' in value &&
8
+ 'route' in value;
9
+ const isHTTPRouteGroup = (value) => typeof value === 'object' &&
10
+ value !== null &&
11
+ 'routes' in value &&
12
+ !('method' in value);
13
+ const applyPackageToHTTPRouteMap = (routes, packageName, namespace) => {
14
+ for (const value of Object.values(routes)) {
15
+ if (!value || typeof value !== 'object')
16
+ continue;
17
+ if (isHTTPRouteConfig(value)) {
18
+ if (!value.func.packageName) {
19
+ value.func.packageName = packageName;
20
+ }
21
+ if (namespace && !value.func.pikkuFuncId.includes(':')) {
22
+ value.func.pikkuFuncId = `${namespace}:${value.func.pikkuFuncId}`;
23
+ }
24
+ continue;
25
+ }
26
+ if (isHTTPRouteGroup(value)) {
27
+ applyPackageToHTTPRouteMap(value.routes, packageName, namespace);
28
+ continue;
29
+ }
30
+ applyPackageToHTTPRouteMap(value, packageName, namespace);
31
+ }
32
+ };
33
+ const applyPackageToHTTPContracts = (contracts, packageName, namespace) => {
34
+ for (const contract of Object.values(contracts)) {
35
+ applyPackageToHTTPRouteMap(contract.routes, packageName, namespace);
36
+ }
37
+ };
38
+ const applyPackageToCLICommands = (commands, packageName, namespace) => {
39
+ for (const command of Object.values(commands)) {
40
+ if (command && typeof command === 'object') {
41
+ if (!command.packageName && command.pikkuFuncId) {
42
+ command.packageName = packageName;
43
+ }
44
+ if (namespace &&
45
+ typeof command.pikkuFuncId === 'string' &&
46
+ !command.pikkuFuncId.includes(':')) {
47
+ command.pikkuFuncId = `${namespace}:${command.pikkuFuncId}`;
48
+ }
49
+ if (command.subcommands) {
50
+ applyPackageToCLICommands(command.subcommands, packageName, namespace);
51
+ }
52
+ }
53
+ }
54
+ };
55
+ const applyPackageToCLIContracts = (contracts, packageName, namespace) => {
56
+ for (const commands of Object.values(contracts)) {
57
+ applyPackageToCLICommands(commands, packageName, namespace);
58
+ }
59
+ };
60
+ const applyPackageToChannelContracts = (contracts, packageName, namespace) => {
61
+ for (const routes of Object.values(contracts)) {
62
+ for (const route of Object.values(routes)) {
63
+ if (!route.packageName) {
64
+ route.packageName = packageName;
65
+ }
66
+ if (!route.pikkuFuncId.includes(':')) {
67
+ route.pikkuFuncId = `${namespace}:${route.pikkuFuncId}`;
68
+ }
69
+ }
70
+ }
71
+ };
4
72
  /**
5
73
  * After the setup sweep discovers wireAddon() declarations, load each addon
6
74
  * package's function metadata so that wiring handlers (channels, HTTP routes,
@@ -81,6 +149,36 @@ export async function loadAddonFunctionsMeta(logger, state) {
81
149
  catch {
82
150
  // No services gen — addon may not have requiredParentServices
83
151
  }
152
+ try {
153
+ const httpContractsPath = require.resolve(`${decl.package}/.pikku/http/pikku-http-contracts-meta.gen.json`);
154
+ const httpContractsRaw = await readFile(httpContractsPath, 'utf-8');
155
+ const httpContracts = JSON.parse(httpContractsRaw);
156
+ applyPackageToHTTPContracts(httpContracts, decl.package, namespace);
157
+ state.exportedContracts.addonHttp[namespace] = httpContracts;
158
+ }
159
+ catch {
160
+ // No addon HTTP contracts metadata
161
+ }
162
+ try {
163
+ const cliContractsPath = require.resolve(`${decl.package}/.pikku/cli/pikku-cli-contracts-meta.gen.json`);
164
+ const cliContractsRaw = await readFile(cliContractsPath, 'utf-8');
165
+ const cliContracts = JSON.parse(cliContractsRaw);
166
+ applyPackageToCLIContracts(cliContracts, decl.package, namespace);
167
+ state.exportedContracts.addonCli[namespace] = cliContracts;
168
+ }
169
+ catch {
170
+ // No addon CLI contracts metadata
171
+ }
172
+ try {
173
+ const channelContractsPath = require.resolve(`${decl.package}/.pikku/channel/pikku-channel-contracts-meta.gen.json`);
174
+ const channelContractsRaw = await readFile(channelContractsPath, 'utf-8');
175
+ const channelContracts = JSON.parse(channelContractsRaw);
176
+ applyPackageToChannelContracts(channelContracts, decl.package, namespace);
177
+ state.exportedContracts.addonChannel[namespace] = channelContracts;
178
+ }
179
+ catch {
180
+ // No addon channel contracts metadata
181
+ }
84
182
  }
85
183
  catch (error) {
86
184
  logger.warn(`Failed to load addon function metadata for '${namespace}' (${decl.package}): ${error.message}`);
@@ -61,8 +61,10 @@ export const resolveAddonName = (identifier, checker, wireAddonDeclarations) =>
61
61
  // Bare package import path
62
62
  if (candidatePackage && !candidatePackage.startsWith('.')) {
63
63
  for (const addonDecl of wireAddonDeclarations.values()) {
64
- if (addonDecl.package === candidatePackage)
64
+ if (addonDecl.package === candidatePackage ||
65
+ candidatePackage.startsWith(`${addonDecl.package}/`)) {
65
66
  return addonDecl.package;
67
+ }
66
68
  }
67
69
  }
68
70
  // Fall back to package.json lookup based on the declaration's source file.
@@ -0,0 +1,21 @@
1
+ import * as ts from 'typescript';
2
+ export interface RefContractResolution<T> {
3
+ contract: T;
4
+ /**
5
+ * Optional basePath override supplied by the consumer via the second
6
+ * argument, e.g. refHTTP('ns:routes', { basePath: '/ext' }). When undefined
7
+ * the addon contract's own basePath is preserved.
8
+ */
9
+ basePath?: string;
10
+ }
11
+ /**
12
+ * Resolve a refHTTP / refChannel / refCLI call expression against the addon
13
+ * contracts already loaded (and namespaced) by loadAddonFunctionsMeta.
14
+ *
15
+ * The first string argument has the form 'namespace:contractName', mirroring
16
+ * how ref('namespace:fn') references an addon function. Detection is purely
17
+ * syntactic — no import resolution is required because the namespace and
18
+ * contract name are carried in the string literal. An optional second object
19
+ * argument may override mount details such as basePath.
20
+ */
21
+ export declare const resolveRefContract: <T>(node: ts.Node, helperName: "refHTTP" | "refChannel" | "refCLI", addonContracts: Record<string, Record<string, T>>) => RefContractResolution<T> | null;
@@ -0,0 +1,46 @@
1
+ import * as ts from 'typescript';
2
+ const getStringProperty = (obj, name) => {
3
+ for (const prop of obj.properties) {
4
+ if (ts.isPropertyAssignment(prop) &&
5
+ (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
6
+ prop.name.text === name &&
7
+ ts.isStringLiteral(prop.initializer)) {
8
+ return prop.initializer.text;
9
+ }
10
+ }
11
+ return undefined;
12
+ };
13
+ /**
14
+ * Resolve a refHTTP / refChannel / refCLI call expression against the addon
15
+ * contracts already loaded (and namespaced) by loadAddonFunctionsMeta.
16
+ *
17
+ * The first string argument has the form 'namespace:contractName', mirroring
18
+ * how ref('namespace:fn') references an addon function. Detection is purely
19
+ * syntactic — no import resolution is required because the namespace and
20
+ * contract name are carried in the string literal. An optional second object
21
+ * argument may override mount details such as basePath.
22
+ */
23
+ export const resolveRefContract = (node, helperName, addonContracts) => {
24
+ if (!ts.isCallExpression(node))
25
+ return null;
26
+ if (!ts.isIdentifier(node.expression) ||
27
+ node.expression.text !== helperName) {
28
+ return null;
29
+ }
30
+ const [arg, optionsArg] = node.arguments;
31
+ if (!arg || !ts.isStringLiteral(arg))
32
+ return null;
33
+ const separator = arg.text.indexOf(':');
34
+ if (separator === -1)
35
+ return null;
36
+ const namespace = arg.text.slice(0, separator);
37
+ const contractName = arg.text.slice(separator + 1);
38
+ const contract = addonContracts[namespace]?.[contractName];
39
+ if (contract === undefined)
40
+ return null;
41
+ let basePath;
42
+ if (optionsArg && ts.isObjectLiteralExpression(optionsArg)) {
43
+ basePath = getStringProperty(optionsArg, 'basePath');
44
+ }
45
+ return { contract, basePath };
46
+ };
@@ -267,6 +267,7 @@ export interface SerializableInspectorState {
267
267
  openAPISpec: Record<string, any> | null;
268
268
  diagnostics: InspectorDiagnostic[];
269
269
  addonFunctions: InspectorState['addonFunctions'];
270
+ exportedContracts: InspectorState['exportedContracts'];
270
271
  }
271
272
  /**
272
273
  * Serializes InspectorState to a JSON-compatible format
@@ -150,6 +150,7 @@ export function serializeInspectorState(state) {
150
150
  openAPISpec: state.openAPISpec,
151
151
  diagnostics: state.diagnostics,
152
152
  addonFunctions: state.addonFunctions,
153
+ exportedContracts: state.exportedContracts,
153
154
  };
154
155
  }
155
156
  /**
@@ -314,6 +315,14 @@ export function deserializeInspectorState(data) {
314
315
  openAPISpec: data.openAPISpec || null,
315
316
  diagnostics: data.diagnostics || [],
316
317
  addonFunctions: data.addonFunctions || {},
318
+ exportedContracts: data.exportedContracts || {
319
+ http: {},
320
+ cli: {},
321
+ channel: {},
322
+ addonHttp: {},
323
+ addonCli: {},
324
+ addonChannel: {},
325
+ },
317
326
  program: null,
318
327
  };
319
328
  }
package/dist/visit.js CHANGED
@@ -3,6 +3,7 @@ import { addFileWithFactory } from './add/add-file-with-factory.js';
3
3
  import { addFileExtendsCoreType } from './add/add-file-extends-core-type.js';
4
4
  import { addHTTPRoute } from './add/add-http-route.js';
5
5
  import { addHTTPRoutes } from './add/add-http-routes.js';
6
+ import { checkAddonBans } from './add/add-addon-bans.js';
6
7
  import { addSchedule } from './add/add-schedule.js';
7
8
  import { addTrigger } from './add/add-trigger.js';
8
9
  import { addQueueWorker } from './add/add-queue-worker.js';
@@ -41,23 +42,27 @@ export const visitSetup = (logger, checker, node, state, options) => {
41
42
  ts.forEachChild(node, (child) => visitSetup(logger, checker, child, state, options));
42
43
  };
43
44
  export const visitRoutes = (logger, checker, node, state, options) => {
44
- addFunctions(logger, node, checker, state, options);
45
- addAuth(logger, node, checker, state, options);
46
- addSecret(logger, node, checker, state, options);
47
- addCredential(logger, node, checker, state, options);
48
- addVariable(logger, node, checker, state, options);
49
- addHTTPRoute(logger, node, checker, state, options);
50
- addHTTPRoutes(logger, node, checker, state, options);
51
- addSchedule(logger, node, checker, state, options);
52
- addTrigger(logger, node, checker, state, options);
53
- addQueueWorker(logger, node, checker, state, options);
54
- addChannel(logger, node, checker, state, options);
55
- addGateway(logger, node, checker, state, options);
56
- addCLI(logger, node, checker, state, options);
57
- addCLIRenderers(logger, node, checker, state, options);
58
- addMCPResource(logger, node, checker, state, options);
59
- addMCPPrompt(logger, node, checker, state, options);
60
- addWorkflowGraph(logger, node, checker, state, options);
61
- addAIAgent(logger, node, checker, state, options);
62
- ts.forEachChild(node, (child) => visitRoutes(logger, checker, child, state, options));
45
+ const nextOptions = ts.isSourceFile(node)
46
+ ? { ...options, sourceFile: node }
47
+ : options;
48
+ checkAddonBans(logger, node, checker, state, nextOptions);
49
+ addFunctions(logger, node, checker, state, nextOptions);
50
+ addAuth(logger, node, checker, state, nextOptions);
51
+ addSecret(logger, node, checker, state, nextOptions);
52
+ addCredential(logger, node, checker, state, nextOptions);
53
+ addVariable(logger, node, checker, state, nextOptions);
54
+ addHTTPRoute(logger, node, checker, state, nextOptions);
55
+ addHTTPRoutes(logger, node, checker, state, nextOptions);
56
+ addSchedule(logger, node, checker, state, nextOptions);
57
+ addTrigger(logger, node, checker, state, nextOptions);
58
+ addQueueWorker(logger, node, checker, state, nextOptions);
59
+ addChannel(logger, node, checker, state, nextOptions);
60
+ addGateway(logger, node, checker, state, nextOptions);
61
+ addCLI(logger, node, checker, state, nextOptions);
62
+ addCLIRenderers(logger, node, checker, state, nextOptions);
63
+ addMCPResource(logger, node, checker, state, nextOptions);
64
+ addMCPPrompt(logger, node, checker, state, nextOptions);
65
+ addWorkflowGraph(logger, node, checker, state, nextOptions);
66
+ addAIAgent(logger, node, checker, state, nextOptions);
67
+ ts.forEachChild(node, (child) => visitRoutes(logger, checker, child, state, nextOptions));
63
68
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.22",
3
+ "version": "0.12.23",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
@@ -0,0 +1,84 @@
1
+ import * as ts from 'typescript'
2
+ import type { AddWiring } from '../types.js'
3
+ import { ErrorCode } from '../error-codes.js'
4
+
5
+ /**
6
+ * Wiring helpers an addon must not call. Addons declare contracts with the
7
+ * define* helpers and export functions; the consuming app does the wiring via
8
+ * refHTTP / refChannel / refCLI. Service declarations remain allowed.
9
+ */
10
+ const BANNED_WIRINGS = new Set([
11
+ 'wireAddon',
12
+ 'wireChannel',
13
+ 'wireCLI',
14
+ 'wireGateway',
15
+ 'wireHTTP',
16
+ 'wireHTTPRoutes',
17
+ 'wireMCPPrompt',
18
+ 'wireMCPResource',
19
+ 'wireQueueWorker',
20
+ 'wireScheduler',
21
+ 'wireTrigger',
22
+ 'wireTriggerSource',
23
+ ])
24
+
25
+ const CONTRACT_DEFINERS = new Set([
26
+ 'defineHTTPRoutes',
27
+ 'defineChannelRoutes',
28
+ 'defineCLICommands',
29
+ ])
30
+
31
+ const hasHandlerProperty = (node: ts.Node): boolean => {
32
+ let found = false
33
+ const visit = (current: ts.Node) => {
34
+ if (found) return
35
+ if (
36
+ ts.isPropertyAssignment(current) &&
37
+ (ts.isIdentifier(current.name) || ts.isStringLiteral(current.name)) &&
38
+ (current.name.text === 'middleware' ||
39
+ current.name.text === 'permissions')
40
+ ) {
41
+ found = true
42
+ return
43
+ }
44
+ ts.forEachChild(current, visit)
45
+ }
46
+ visit(node)
47
+ return found
48
+ }
49
+
50
+ /**
51
+ * Enforce addon authoring rules. Only runs when inspecting an addon package
52
+ * (options.isAddon). Addons cannot wire transports, and their contracts cannot
53
+ * carry middleware or permissions — those are the consuming app's concern.
54
+ */
55
+ export const checkAddonBans: AddWiring = (
56
+ logger,
57
+ node,
58
+ _checker,
59
+ _state,
60
+ options
61
+ ) => {
62
+ if (!options.isAddon) return
63
+ if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression)) return
64
+
65
+ const name = node.expression.text
66
+
67
+ if (BANNED_WIRINGS.has(name)) {
68
+ logger.critical(
69
+ ErrorCode.ADDON_WIRING_NOT_ALLOWED,
70
+ `Addons must not call '${name}'. Declare contracts with define* and export functions; the consuming app wires them via refHTTP / refChannel / refCLI.`
71
+ )
72
+ return
73
+ }
74
+
75
+ if (CONTRACT_DEFINERS.has(name)) {
76
+ const [arg] = node.arguments
77
+ if (arg && hasHandlerProperty(arg)) {
78
+ logger.critical(
79
+ ErrorCode.ADDON_CONTRACT_HANDLERS_NOT_ALLOWED,
80
+ `Addon contract '${name}' must not declare middleware or permissions — these are applied by the consuming app, not the addon.`
81
+ )
82
+ }
83
+ }
84
+ }