ont-run 0.0.1

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 (54) hide show
  1. package/README.md +228 -0
  2. package/bin/ont.ts +5 -0
  3. package/dist/bin/ont.d.ts +2 -0
  4. package/dist/bin/ont.js +13667 -0
  5. package/dist/index.js +23152 -0
  6. package/dist/src/browser/server.d.ts +16 -0
  7. package/dist/src/browser/transform.d.ts +87 -0
  8. package/dist/src/cli/commands/init.d.ts +12 -0
  9. package/dist/src/cli/commands/review.d.ts +17 -0
  10. package/dist/src/cli/index.d.ts +1 -0
  11. package/dist/src/cli/utils/config-loader.d.ts +13 -0
  12. package/dist/src/config/categorical.d.ts +76 -0
  13. package/dist/src/config/define.d.ts +46 -0
  14. package/dist/src/config/index.d.ts +4 -0
  15. package/dist/src/config/schema.d.ts +162 -0
  16. package/dist/src/config/types.d.ts +94 -0
  17. package/dist/src/index.d.ts +37 -0
  18. package/dist/src/lockfile/differ.d.ts +11 -0
  19. package/dist/src/lockfile/hasher.d.ts +31 -0
  20. package/dist/src/lockfile/index.d.ts +53 -0
  21. package/dist/src/lockfile/types.d.ts +90 -0
  22. package/dist/src/runtime/index.d.ts +28 -0
  23. package/dist/src/server/api/index.d.ts +20 -0
  24. package/dist/src/server/api/middleware.d.ts +34 -0
  25. package/dist/src/server/api/router.d.ts +18 -0
  26. package/dist/src/server/mcp/index.d.ts +23 -0
  27. package/dist/src/server/mcp/tools.d.ts +35 -0
  28. package/dist/src/server/resolver.d.ts +30 -0
  29. package/dist/src/server/start.d.ts +37 -0
  30. package/package.json +63 -0
  31. package/src/browser/server.ts +2567 -0
  32. package/src/browser/transform.ts +473 -0
  33. package/src/cli/commands/init.ts +226 -0
  34. package/src/cli/commands/review.ts +126 -0
  35. package/src/cli/index.ts +19 -0
  36. package/src/cli/utils/config-loader.ts +78 -0
  37. package/src/config/categorical.ts +101 -0
  38. package/src/config/define.ts +78 -0
  39. package/src/config/index.ts +23 -0
  40. package/src/config/schema.ts +196 -0
  41. package/src/config/types.ts +121 -0
  42. package/src/index.ts +53 -0
  43. package/src/lockfile/differ.ts +242 -0
  44. package/src/lockfile/hasher.ts +175 -0
  45. package/src/lockfile/index.ts +159 -0
  46. package/src/lockfile/types.ts +95 -0
  47. package/src/runtime/index.ts +114 -0
  48. package/src/server/api/index.ts +92 -0
  49. package/src/server/api/middleware.ts +118 -0
  50. package/src/server/api/router.ts +102 -0
  51. package/src/server/mcp/index.ts +182 -0
  52. package/src/server/mcp/tools.ts +199 -0
  53. package/src/server/resolver.ts +109 -0
  54. package/src/server/start.ts +151 -0
@@ -0,0 +1,159 @@
1
+ import { readFile, writeFile } from "fs/promises";
2
+ import { existsSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import type { Lockfile, OntologySnapshot } from "./types.js";
5
+
6
+ export { extractOntology, hashOntology, computeOntologyHash } from "./hasher.js";
7
+ export { diffOntology, formatDiffForConsole } from "./differ.js";
8
+ export type {
9
+ Lockfile,
10
+ OntologySnapshot,
11
+ OntologyDiff,
12
+ FunctionChange,
13
+ FunctionShape,
14
+ } from "./types.js";
15
+
16
+ const LOCKFILE_NAME = "ont.lock";
17
+ const LOCKFILE_VERSION = 1;
18
+
19
+ /**
20
+ * Get the lockfile path for a given config directory
21
+ */
22
+ export function getLockfilePath(configDir: string): string {
23
+ return join(configDir, LOCKFILE_NAME);
24
+ }
25
+
26
+ /**
27
+ * Check if a lockfile exists
28
+ */
29
+ export function lockfileExists(configDir: string): boolean {
30
+ return existsSync(getLockfilePath(configDir));
31
+ }
32
+
33
+ /**
34
+ * Read the lockfile from disk
35
+ * @returns The lockfile contents, or null if it doesn't exist
36
+ */
37
+ export async function readLockfile(configDir: string): Promise<Lockfile | null> {
38
+ const path = getLockfilePath(configDir);
39
+
40
+ if (!existsSync(path)) {
41
+ return null;
42
+ }
43
+
44
+ try {
45
+ const content = await readFile(path, "utf-8");
46
+ const lockfile = JSON.parse(content) as Lockfile;
47
+
48
+ // Validate version
49
+ if (lockfile.version !== LOCKFILE_VERSION) {
50
+ throw new Error(
51
+ `Lockfile version mismatch. Expected ${LOCKFILE_VERSION}, got ${lockfile.version}`
52
+ );
53
+ }
54
+
55
+ return lockfile;
56
+ } catch (error) {
57
+ if (error instanceof SyntaxError) {
58
+ throw new Error(`Failed to parse lockfile: ${error.message}`);
59
+ }
60
+ throw error;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Write a lockfile to disk
66
+ */
67
+ export async function writeLockfile(
68
+ configDir: string,
69
+ ontology: OntologySnapshot,
70
+ hash: string
71
+ ): Promise<void> {
72
+ const lockfile: Lockfile = {
73
+ version: LOCKFILE_VERSION,
74
+ hash,
75
+ approvedAt: new Date().toISOString(),
76
+ ontology,
77
+ };
78
+
79
+ const path = getLockfilePath(configDir);
80
+ const content = JSON.stringify(lockfile, null, 2);
81
+
82
+ await writeFile(path, content, "utf-8");
83
+ }
84
+
85
+ /**
86
+ * Check if the current ontology matches the lockfile
87
+ * @returns { match: true } if hashes match, or { match: false, lockfile, currentHash } if not
88
+ */
89
+ export async function checkLockfile(
90
+ configDir: string,
91
+ currentHash: string
92
+ ): Promise<
93
+ | { match: true; lockfile: Lockfile }
94
+ | { match: false; lockfile: Lockfile | null; currentHash: string }
95
+ > {
96
+ const lockfile = await readLockfile(configDir);
97
+
98
+ if (!lockfile) {
99
+ return { match: false, lockfile: null, currentHash };
100
+ }
101
+
102
+ if (lockfile.hash === currentHash) {
103
+ return { match: true, lockfile };
104
+ }
105
+
106
+ return { match: false, lockfile, currentHash };
107
+ }
108
+
109
+ /**
110
+ * Result of lockfile validation
111
+ */
112
+ export interface LockfileValidationResult {
113
+ /** Status of the lockfile check */
114
+ status: "valid" | "missing" | "mismatch";
115
+ /** The diff if there are changes (only present for 'mismatch' status) */
116
+ diff?: import("./types.js").OntologyDiff;
117
+ /** Human-readable message */
118
+ message: string;
119
+ }
120
+
121
+ /**
122
+ * Validate the lockfile against the current ontology.
123
+ * This is the main entry point for library use.
124
+ *
125
+ * @param configDir - Directory containing the ontology.config.ts
126
+ * @param currentOntology - The current ontology snapshot
127
+ * @param currentHash - The current ontology hash
128
+ */
129
+ export async function validateLockfile(
130
+ configDir: string,
131
+ currentOntology: OntologySnapshot,
132
+ currentHash: string
133
+ ): Promise<LockfileValidationResult> {
134
+ const { diffOntology } = await import("./differ.js");
135
+
136
+ if (!lockfileExists(configDir)) {
137
+ return {
138
+ status: "missing",
139
+ message: "No ont.lock file found. Run `bun run review` to approve the initial ontology.",
140
+ };
141
+ }
142
+
143
+ const lockfile = await readLockfile(configDir);
144
+ const oldOntology = lockfile?.ontology || null;
145
+ const diff = diffOntology(oldOntology, currentOntology);
146
+
147
+ if (!diff.hasChanges) {
148
+ return {
149
+ status: "valid",
150
+ message: "Lockfile is up to date.",
151
+ };
152
+ }
153
+
154
+ return {
155
+ status: "mismatch",
156
+ diff,
157
+ message: "Ontology has changed since last review. Run `bun run review` to approve changes.",
158
+ };
159
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * A field that references another function for its options
3
+ */
4
+ export interface FieldReference {
5
+ /** Path to the field in the schema, e.g., "status" or "filters.country" */
6
+ path: string;
7
+ /** Name of the function that provides options for this field */
8
+ functionName: string;
9
+ }
10
+
11
+ /**
12
+ * Snapshot of a function's shape (what matters for security review)
13
+ */
14
+ export interface FunctionShape {
15
+ /** Description of the function */
16
+ description: string;
17
+ /** Sorted list of access groups */
18
+ access: string[];
19
+ /** Sorted list of entities this function relates to */
20
+ entities: string[];
21
+ /** JSON Schema representation of the input schema */
22
+ inputsSchema: Record<string, unknown>;
23
+ /** JSON Schema representation of the output schema */
24
+ outputsSchema?: Record<string, unknown>;
25
+ /** Fields that reference other functions for their options */
26
+ fieldReferences?: FieldReference[];
27
+ }
28
+
29
+ /**
30
+ * Complete snapshot of the ontology
31
+ */
32
+ export interface OntologySnapshot {
33
+ /** Name of the ontology */
34
+ name: string;
35
+ /** Sorted list of access group names */
36
+ accessGroups: string[];
37
+ /** Sorted list of entity names */
38
+ entities?: string[];
39
+ /** Function shapes keyed by name */
40
+ functions: Record<string, FunctionShape>;
41
+ }
42
+
43
+ /**
44
+ * The ont.lock file structure
45
+ */
46
+ export interface Lockfile {
47
+ /** Lockfile format version */
48
+ version: number;
49
+ /** SHA256 hash of the ontology */
50
+ hash: string;
51
+ /** When this was approved */
52
+ approvedAt: string;
53
+ /** The full ontology snapshot */
54
+ ontology: OntologySnapshot;
55
+ }
56
+
57
+ /**
58
+ * A single change in a function
59
+ */
60
+ export interface FunctionChange {
61
+ name: string;
62
+ type: "added" | "removed" | "modified";
63
+ oldAccess?: string[];
64
+ newAccess?: string[];
65
+ oldDescription?: string;
66
+ newDescription?: string;
67
+ inputsChanged?: boolean;
68
+ outputsChanged?: boolean;
69
+ entitiesChanged?: boolean;
70
+ oldEntities?: string[];
71
+ newEntities?: string[];
72
+ fieldReferencesChanged?: boolean;
73
+ }
74
+
75
+ /**
76
+ * Diff between old and new ontology
77
+ */
78
+ export interface OntologyDiff {
79
+ /** Whether there are any changes */
80
+ hasChanges: boolean;
81
+ /** Added access groups */
82
+ addedGroups: string[];
83
+ /** Removed access groups */
84
+ removedGroups: string[];
85
+ /** Added entities */
86
+ addedEntities: string[];
87
+ /** Removed entities */
88
+ removedEntities: string[];
89
+ /** Function changes */
90
+ functions: FunctionChange[];
91
+ /** The new ontology (for writing to lockfile on approve) */
92
+ newOntology: OntologySnapshot;
93
+ /** The new hash */
94
+ newHash: string;
95
+ }
@@ -0,0 +1,114 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
+ type AnyHono = { fetch: (request: Request, ...args: any[]) => Response | Promise<Response> };
3
+
4
+ /**
5
+ * Check if we're running in Bun
6
+ */
7
+ export function isBun(): boolean {
8
+ return typeof globalThis.Bun !== "undefined";
9
+ }
10
+
11
+ /**
12
+ * Check if we're running in Node.js
13
+ */
14
+ export function isNode(): boolean {
15
+ return (
16
+ typeof process !== "undefined" &&
17
+ process.versions != null &&
18
+ process.versions.node != null &&
19
+ !isBun()
20
+ );
21
+ }
22
+
23
+ /**
24
+ * Get the current runtime name
25
+ */
26
+ export function getRuntime(): "bun" | "node" | "unknown" {
27
+ if (isBun()) return "bun";
28
+ if (isNode()) return "node";
29
+ return "unknown";
30
+ }
31
+
32
+ export interface ServerHandle {
33
+ port: number;
34
+ stop: () => void | Promise<void>;
35
+ }
36
+
37
+ /**
38
+ * Start an HTTP server using the appropriate runtime
39
+ */
40
+ export async function serve(
41
+ app: AnyHono,
42
+ port: number
43
+ ): Promise<ServerHandle> {
44
+ if (isBun()) {
45
+ const server = Bun.serve({
46
+ port,
47
+ fetch: app.fetch,
48
+ // Disable idle timeout for SSE/streaming connections
49
+ idleTimeout: 0,
50
+ });
51
+ return {
52
+ port: server.port ?? port,
53
+ stop: () => server.stop(),
54
+ };
55
+ }
56
+
57
+ // Node.js
58
+ const { serve: nodeServe } = await import("@hono/node-server");
59
+
60
+ return new Promise((resolve) => {
61
+ const server = nodeServe({
62
+ fetch: app.fetch,
63
+ port,
64
+ }, (info) => {
65
+ resolve({
66
+ port: info.port,
67
+ stop: () => {
68
+ server.close();
69
+ },
70
+ });
71
+ });
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Find an available port starting from the given port
77
+ */
78
+ export async function findAvailablePort(startPort: number = 3456): Promise<number> {
79
+ if (isBun()) {
80
+ for (let port = startPort; port < startPort + 100; port++) {
81
+ try {
82
+ const server = Bun.serve({
83
+ port,
84
+ fetch: () => new Response("test"),
85
+ });
86
+ server.stop();
87
+ return port;
88
+ } catch {
89
+ // Port in use, try next
90
+ }
91
+ }
92
+ throw new Error("Could not find available port");
93
+ }
94
+
95
+ // Node.js - use net module to check port availability
96
+ const net = await import("net");
97
+
98
+ return new Promise((resolve, reject) => {
99
+ const tryPort = (port: number) => {
100
+ if (port >= startPort + 100) {
101
+ reject(new Error("Could not find available port"));
102
+ return;
103
+ }
104
+
105
+ const server = net.createServer();
106
+ server.listen(port, () => {
107
+ server.close(() => resolve(port));
108
+ });
109
+ server.on("error", () => tryPort(port + 1));
110
+ };
111
+
112
+ tryPort(startPort);
113
+ });
114
+ }
@@ -0,0 +1,92 @@
1
+ import { Hono } from "hono";
2
+ import { cors } from "hono/cors";
3
+ import consola from "consola";
4
+ import type { OntologyConfig } from "../../config/types.js";
5
+ import {
6
+ createAuthMiddleware,
7
+ createContextMiddleware,
8
+ errorHandler,
9
+ type OntologyVariables,
10
+ } from "./middleware.js";
11
+ import { createApiRoutes, getFunctionsInfo } from "./router.js";
12
+ import { findMissingResolvers } from "../resolver.js";
13
+
14
+ export interface ApiServerOptions {
15
+ /** The ontology configuration */
16
+ config: OntologyConfig;
17
+ /** Directory containing the ontology.config.ts (for resolving resolver paths) */
18
+ configDir: string;
19
+ /** Environment to use (e.g., 'dev', 'prod') */
20
+ env: string;
21
+ /** Enable CORS (default: true) */
22
+ cors?: boolean;
23
+ }
24
+
25
+ /**
26
+ * Create the Hono API app from an OntologyConfig
27
+ */
28
+ export function createApiApp(options: ApiServerOptions): Hono<{ Variables: OntologyVariables }> {
29
+ const { config, configDir, env, cors: enableCors = true } = options;
30
+
31
+ // Get environment config
32
+ const envConfig = config.environments[env];
33
+ if (!envConfig) {
34
+ throw new Error(
35
+ `Unknown environment "${env}". Available: ${Object.keys(config.environments).join(", ")}`
36
+ );
37
+ }
38
+
39
+ // Check for missing resolvers
40
+ const missingResolvers = findMissingResolvers(config, configDir);
41
+ if (missingResolvers.length > 0) {
42
+ consola.warn(`Missing resolvers (${missingResolvers.length}):`);
43
+ for (const resolver of missingResolvers) {
44
+ consola.warn(` - ${resolver}`);
45
+ }
46
+ }
47
+
48
+ const app = new Hono<{ Variables: OntologyVariables }>();
49
+
50
+ // Global middleware
51
+ if (enableCors) {
52
+ app.use("*", cors());
53
+ }
54
+ app.use("*", errorHandler());
55
+ app.use("*", createAuthMiddleware(config));
56
+ app.use("*", createContextMiddleware(env, envConfig));
57
+
58
+ // Health check endpoint (no auth required, override for this route)
59
+ app.get("/health", (c) => {
60
+ return c.json({
61
+ status: "ok",
62
+ name: config.name,
63
+ env,
64
+ });
65
+ });
66
+
67
+ // Introspection endpoint - list available functions
68
+ app.get("/api", (c) => {
69
+ const accessGroups = c.get("accessGroups") || [];
70
+ const allFunctions = getFunctionsInfo(config);
71
+
72
+ // Filter to only show functions the user has access to
73
+ const accessibleFunctions = allFunctions.filter((fn) =>
74
+ fn.access.some((group) => accessGroups.includes(group))
75
+ );
76
+
77
+ return c.json({
78
+ name: config.name,
79
+ env,
80
+ accessGroups,
81
+ functions: accessibleFunctions,
82
+ });
83
+ });
84
+
85
+ // Mount function routes under /api
86
+ const apiRoutes = createApiRoutes(config, configDir);
87
+ app.route("/api", apiRoutes);
88
+
89
+ return app;
90
+ }
91
+
92
+ export { getFunctionsInfo } from "./router.js";
@@ -0,0 +1,118 @@
1
+ import { createMiddleware } from "hono/factory";
2
+ import type { Context, Next } from "hono";
3
+ import type { ContentfulStatusCode } from "hono/utils/http-status";
4
+ import type { OntologyConfig, ResolverContext, EnvironmentConfig } from "../../config/types.js";
5
+ import { createLogger } from "../resolver.js";
6
+
7
+ /**
8
+ * Context variables added by middleware
9
+ */
10
+ export interface OntologyVariables {
11
+ resolverContext: ResolverContext;
12
+ accessGroups: string[];
13
+ }
14
+
15
+ /**
16
+ * Create auth middleware that calls the user's auth function
17
+ * and sets the access groups on the context
18
+ */
19
+ export function createAuthMiddleware(config: OntologyConfig) {
20
+ return createMiddleware<{ Variables: OntologyVariables }>(
21
+ async (c: Context<{ Variables: OntologyVariables }>, next: Next) => {
22
+ try {
23
+ // Call user's auth function
24
+ const accessGroups = await config.auth(c.req.raw);
25
+ c.set("accessGroups", accessGroups);
26
+ await next();
27
+ } catch (error) {
28
+ return c.json(
29
+ {
30
+ error: "Authentication failed",
31
+ message: error instanceof Error ? error.message : "Unknown error",
32
+ },
33
+ 401
34
+ );
35
+ }
36
+ }
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Create context middleware that builds the ResolverContext
42
+ */
43
+ export function createContextMiddleware(
44
+ env: string,
45
+ envConfig: EnvironmentConfig
46
+ ) {
47
+ const logger = createLogger(envConfig.debug);
48
+
49
+ return createMiddleware<{ Variables: OntologyVariables }>(
50
+ async (c: Context<{ Variables: OntologyVariables }>, next: Next) => {
51
+ const accessGroups = c.get("accessGroups") || [];
52
+
53
+ const resolverContext: ResolverContext = {
54
+ env,
55
+ envConfig,
56
+ logger,
57
+ accessGroups,
58
+ };
59
+
60
+ c.set("resolverContext", resolverContext);
61
+ await next();
62
+ }
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Create access control middleware for a specific function
68
+ */
69
+ export function createAccessControlMiddleware(requiredAccess: string[]) {
70
+ return createMiddleware<{ Variables: OntologyVariables }>(
71
+ async (c: Context<{ Variables: OntologyVariables }>, next: Next) => {
72
+ const accessGroups = c.get("accessGroups") || [];
73
+
74
+ // Check if user has at least one of the required access groups
75
+ const hasAccess = requiredAccess.some((group) =>
76
+ accessGroups.includes(group)
77
+ );
78
+
79
+ if (!hasAccess) {
80
+ return c.json(
81
+ {
82
+ error: "Access denied",
83
+ message: `This function requires one of: ${requiredAccess.join(", ")}`,
84
+ yourGroups: accessGroups,
85
+ },
86
+ 403
87
+ );
88
+ }
89
+
90
+ await next();
91
+ }
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Error handling middleware
97
+ */
98
+ export function errorHandler() {
99
+ return createMiddleware(async (c: Context, next: Next) => {
100
+ try {
101
+ await next();
102
+ } catch (error) {
103
+ console.error("Request error:", error);
104
+
105
+ const status = (error as { status?: number }).status || 500;
106
+ const message =
107
+ error instanceof Error ? error.message : "Internal server error";
108
+
109
+ return c.json(
110
+ {
111
+ error: "Request failed",
112
+ message,
113
+ },
114
+ status as ContentfulStatusCode
115
+ );
116
+ }
117
+ });
118
+ }
@@ -0,0 +1,102 @@
1
+ import { Hono } from "hono";
2
+ import type { OntologyConfig, FunctionDefinition } from "../../config/types.js";
3
+ import { loadResolver } from "../resolver.js";
4
+ import {
5
+ createAccessControlMiddleware,
6
+ type OntologyVariables,
7
+ } from "./middleware.js";
8
+
9
+ /**
10
+ * Create API routes from function definitions
11
+ */
12
+ export function createApiRoutes(
13
+ config: OntologyConfig,
14
+ configDir: string
15
+ ): Hono<{ Variables: OntologyVariables }> {
16
+ const router = new Hono<{ Variables: OntologyVariables }>();
17
+
18
+ // Create a route for each function
19
+ for (const [name, fn] of Object.entries(config.functions)) {
20
+ const path = `/${name}`;
21
+
22
+ router.post(
23
+ path,
24
+ // Access control for this specific function
25
+ createAccessControlMiddleware(fn.access),
26
+ // Handler
27
+ async (c) => {
28
+ const resolverContext = c.get("resolverContext");
29
+
30
+ // Parse and validate input
31
+ let args: unknown;
32
+ try {
33
+ const body = await c.req.json();
34
+ const parsed = fn.inputs.safeParse(body);
35
+
36
+ if (!parsed.success) {
37
+ return c.json(
38
+ {
39
+ error: "Validation failed",
40
+ issues: parsed.error.issues,
41
+ },
42
+ 400
43
+ );
44
+ }
45
+
46
+ args = parsed.data;
47
+ } catch {
48
+ // No body or invalid JSON - try with empty object
49
+ const parsed = fn.inputs.safeParse({});
50
+ if (!parsed.success) {
51
+ return c.json(
52
+ {
53
+ error: "Validation failed",
54
+ message: "Request body is required",
55
+ issues: parsed.error.issues,
56
+ },
57
+ 400
58
+ );
59
+ }
60
+ args = parsed.data;
61
+ }
62
+
63
+ // Load and execute resolver
64
+ try {
65
+ const resolver = await loadResolver(fn.resolver, configDir);
66
+ const result = await resolver(resolverContext, args);
67
+ return c.json(result);
68
+ } catch (error) {
69
+ console.error(`Error in resolver ${name}:`, error);
70
+ return c.json(
71
+ {
72
+ error: "Resolver failed",
73
+ message: error instanceof Error ? error.message : "Unknown error",
74
+ },
75
+ 500
76
+ );
77
+ }
78
+ }
79
+ );
80
+ }
81
+
82
+ return router;
83
+ }
84
+
85
+ /**
86
+ * Get info about all available functions (for introspection)
87
+ */
88
+ export function getFunctionsInfo(
89
+ config: OntologyConfig
90
+ ): Array<{
91
+ name: string;
92
+ description: string;
93
+ access: string[];
94
+ path: string;
95
+ }> {
96
+ return Object.entries(config.functions).map(([name, fn]) => ({
97
+ name,
98
+ description: fn.description,
99
+ access: fn.access,
100
+ path: `/api/${name}`,
101
+ }));
102
+ }