@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.
- package/CHANGELOG.md +10 -0
- package/dist/add/add-addon-bans.d.ts +7 -0
- package/dist/add/add-addon-bans.js +65 -0
- package/dist/add/add-channel.js +47 -6
- package/dist/add/add-cli.js +17 -0
- package/dist/add/add-http-route.d.ts +11 -1
- package/dist/add/add-http-route.js +37 -0
- package/dist/add/add-http-routes.d.ts +0 -3
- package/dist/add/add-http-routes.js +179 -36
- package/dist/error-codes.d.ts +3 -1
- package/dist/error-codes.js +3 -0
- package/dist/inspector.js +17 -5
- package/dist/types.d.ts +43 -1
- package/dist/utils/get-exported-variable-name.d.ts +2 -0
- package/dist/utils/get-exported-variable-name.js +34 -0
- package/dist/utils/load-addon-functions-meta.js +98 -0
- package/dist/utils/resolve-addon-package.js +3 -1
- package/dist/utils/resolve-ref-contract.d.ts +21 -0
- package/dist/utils/resolve-ref-contract.js +46 -0
- package/dist/utils/serialize-inspector-state.d.ts +1 -0
- package/dist/utils/serialize-inspector-state.js +9 -0
- package/dist/visit.js +24 -19
- package/package.json +1 -1
- package/src/add/add-addon-bans.ts +84 -0
- package/src/add/add-channel.ts +66 -7
- package/src/add/add-cli.ts +30 -0
- package/src/add/add-http-route.ts +75 -1
- package/src/add/add-http-routes.ts +283 -41
- package/src/add/addon-bans.test.ts +121 -0
- package/src/add/addon-contracts.test.ts +221 -0
- package/src/error-codes.ts +4 -0
- package/src/inspector.ts +17 -5
- package/src/types.ts +65 -1
- package/src/utils/get-exported-variable-name.ts +48 -0
- package/src/utils/load-addon-functions-meta.ts +164 -0
- package/src/utils/resolve-addon-package.ts +6 -1
- package/src/utils/resolve-ref-contract.ts +71 -0
- package/src/utils/serialize-inspector-state.ts +10 -0
- package/src/visit.ts +26 -19
- 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(
|
|
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 (${
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
@@ -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
|
+
}
|