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.
- package/README.md +228 -0
- package/bin/ont.ts +5 -0
- package/dist/bin/ont.d.ts +2 -0
- package/dist/bin/ont.js +13667 -0
- package/dist/index.js +23152 -0
- package/dist/src/browser/server.d.ts +16 -0
- package/dist/src/browser/transform.d.ts +87 -0
- package/dist/src/cli/commands/init.d.ts +12 -0
- package/dist/src/cli/commands/review.d.ts +17 -0
- package/dist/src/cli/index.d.ts +1 -0
- package/dist/src/cli/utils/config-loader.d.ts +13 -0
- package/dist/src/config/categorical.d.ts +76 -0
- package/dist/src/config/define.d.ts +46 -0
- package/dist/src/config/index.d.ts +4 -0
- package/dist/src/config/schema.d.ts +162 -0
- package/dist/src/config/types.d.ts +94 -0
- package/dist/src/index.d.ts +37 -0
- package/dist/src/lockfile/differ.d.ts +11 -0
- package/dist/src/lockfile/hasher.d.ts +31 -0
- package/dist/src/lockfile/index.d.ts +53 -0
- package/dist/src/lockfile/types.d.ts +90 -0
- package/dist/src/runtime/index.d.ts +28 -0
- package/dist/src/server/api/index.d.ts +20 -0
- package/dist/src/server/api/middleware.d.ts +34 -0
- package/dist/src/server/api/router.d.ts +18 -0
- package/dist/src/server/mcp/index.d.ts +23 -0
- package/dist/src/server/mcp/tools.d.ts +35 -0
- package/dist/src/server/resolver.d.ts +30 -0
- package/dist/src/server/start.d.ts +37 -0
- package/package.json +63 -0
- package/src/browser/server.ts +2567 -0
- package/src/browser/transform.ts +473 -0
- package/src/cli/commands/init.ts +226 -0
- package/src/cli/commands/review.ts +126 -0
- package/src/cli/index.ts +19 -0
- package/src/cli/utils/config-loader.ts +78 -0
- package/src/config/categorical.ts +101 -0
- package/src/config/define.ts +78 -0
- package/src/config/index.ts +23 -0
- package/src/config/schema.ts +196 -0
- package/src/config/types.ts +121 -0
- package/src/index.ts +53 -0
- package/src/lockfile/differ.ts +242 -0
- package/src/lockfile/hasher.ts +175 -0
- package/src/lockfile/index.ts +159 -0
- package/src/lockfile/types.ts +95 -0
- package/src/runtime/index.ts +114 -0
- package/src/server/api/index.ts +92 -0
- package/src/server/api/middleware.ts +118 -0
- package/src/server/api/router.ts +102 -0
- package/src/server/mcp/index.ts +182 -0
- package/src/server/mcp/tools.ts +199 -0
- package/src/server/resolver.ts +109 -0
- 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
|
+
}
|