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,121 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for an environment (dev, test, prod)
|
|
5
|
+
*/
|
|
6
|
+
export interface EnvironmentConfig {
|
|
7
|
+
/** Enable debug mode */
|
|
8
|
+
debug?: boolean;
|
|
9
|
+
/** Any additional environment-specific settings */
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Configuration for an access group
|
|
15
|
+
*/
|
|
16
|
+
export interface AccessGroupConfig {
|
|
17
|
+
/** Description of this access group */
|
|
18
|
+
description: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Definition of an entity in the ontology
|
|
23
|
+
*/
|
|
24
|
+
export interface EntityDefinition {
|
|
25
|
+
/** Human-readable description of this entity */
|
|
26
|
+
description: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Option returned by functions used as field sources.
|
|
31
|
+
* Functions referenced by `fieldFrom()` should return an array of these.
|
|
32
|
+
*/
|
|
33
|
+
export interface FieldOption {
|
|
34
|
+
/** The stored value */
|
|
35
|
+
value: string;
|
|
36
|
+
/** Human-readable label for display */
|
|
37
|
+
label: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Definition of a function in the ontology
|
|
42
|
+
*/
|
|
43
|
+
export interface FunctionDefinition<
|
|
44
|
+
TGroups extends string = string,
|
|
45
|
+
TEntities extends string = string,
|
|
46
|
+
> {
|
|
47
|
+
/** Human-readable description of what this function does */
|
|
48
|
+
description: string;
|
|
49
|
+
/** Which access groups can call this function */
|
|
50
|
+
access: TGroups[];
|
|
51
|
+
/** Which entities this function relates to (use empty array [] if none) */
|
|
52
|
+
entities: TEntities[];
|
|
53
|
+
/** Zod schema for input validation */
|
|
54
|
+
inputs: z.ZodType<unknown>;
|
|
55
|
+
/** Zod schema for output validation/documentation */
|
|
56
|
+
outputs?: z.ZodType<unknown>;
|
|
57
|
+
/** Path to the resolver file (relative to ontology.config.ts) */
|
|
58
|
+
resolver: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Auth function that determines access groups for a request
|
|
63
|
+
*/
|
|
64
|
+
export type AuthFunction = (req: Request) => Promise<string[]> | string[];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The main Ontology configuration object
|
|
68
|
+
*/
|
|
69
|
+
export interface OntologyConfig<
|
|
70
|
+
TGroups extends string = string,
|
|
71
|
+
TEntities extends string = string,
|
|
72
|
+
TFunctions extends Record<
|
|
73
|
+
string,
|
|
74
|
+
FunctionDefinition<TGroups, TEntities>
|
|
75
|
+
> = Record<string, FunctionDefinition<TGroups, TEntities>>,
|
|
76
|
+
> {
|
|
77
|
+
/** Name of this ontology/API */
|
|
78
|
+
name: string;
|
|
79
|
+
|
|
80
|
+
/** Environment configurations */
|
|
81
|
+
environments: Record<string, EnvironmentConfig>;
|
|
82
|
+
|
|
83
|
+
/** Pluggable auth function */
|
|
84
|
+
auth: AuthFunction;
|
|
85
|
+
|
|
86
|
+
/** Access group definitions */
|
|
87
|
+
accessGroups: Record<TGroups, AccessGroupConfig>;
|
|
88
|
+
|
|
89
|
+
/** Entity definitions for categorization */
|
|
90
|
+
entities?: Record<TEntities, EntityDefinition>;
|
|
91
|
+
|
|
92
|
+
/** Function definitions */
|
|
93
|
+
functions: TFunctions;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Context passed to resolvers
|
|
98
|
+
*/
|
|
99
|
+
export interface ResolverContext {
|
|
100
|
+
/** Current environment name */
|
|
101
|
+
env: string;
|
|
102
|
+
/** Environment configuration */
|
|
103
|
+
envConfig: EnvironmentConfig;
|
|
104
|
+
/** Logger instance */
|
|
105
|
+
logger: {
|
|
106
|
+
info: (message: string, ...args: unknown[]) => void;
|
|
107
|
+
warn: (message: string, ...args: unknown[]) => void;
|
|
108
|
+
error: (message: string, ...args: unknown[]) => void;
|
|
109
|
+
debug: (message: string, ...args: unknown[]) => void;
|
|
110
|
+
};
|
|
111
|
+
/** Access groups for the current request */
|
|
112
|
+
accessGroups: string[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resolver function signature
|
|
117
|
+
*/
|
|
118
|
+
export type ResolverFunction<TArgs = unknown, TResult = unknown> = (
|
|
119
|
+
ctx: ResolverContext,
|
|
120
|
+
args: TArgs
|
|
121
|
+
) => Promise<TResult> | TResult;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ontology - Ontology-first backends with human-approved AI access & edits
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { defineOntology, fieldFrom } from 'ont-run';
|
|
7
|
+
* import { z } from 'zod';
|
|
8
|
+
*
|
|
9
|
+
* export default defineOntology({
|
|
10
|
+
* name: 'my-api',
|
|
11
|
+
* environments: { dev: { debug: true }, prod: { debug: false } },
|
|
12
|
+
* auth: async (req) => req.headers.get('Authorization') ? ['admin'] : ['public'],
|
|
13
|
+
* accessGroups: {
|
|
14
|
+
* public: { description: 'Unauthenticated' },
|
|
15
|
+
* admin: { description: 'Administrators' },
|
|
16
|
+
* },
|
|
17
|
+
* entities: {
|
|
18
|
+
* User: { description: 'A user account' },
|
|
19
|
+
* },
|
|
20
|
+
* functions: {
|
|
21
|
+
* hello: {
|
|
22
|
+
* description: 'Say hello',
|
|
23
|
+
* access: ['public'],
|
|
24
|
+
* entities: [],
|
|
25
|
+
* inputs: z.object({ name: z.string() }),
|
|
26
|
+
* resolver: './resolvers/hello.ts',
|
|
27
|
+
* },
|
|
28
|
+
* },
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// Main API
|
|
34
|
+
export { defineOntology } from "./config/define.js";
|
|
35
|
+
export { fieldFrom } from "./config/categorical.js";
|
|
36
|
+
export { startOnt } from "./server/start.js";
|
|
37
|
+
export type { StartOntOptions, StartOntResult } from "./server/start.js";
|
|
38
|
+
|
|
39
|
+
// Types
|
|
40
|
+
export type {
|
|
41
|
+
OntologyConfig,
|
|
42
|
+
FunctionDefinition,
|
|
43
|
+
AccessGroupConfig,
|
|
44
|
+
EnvironmentConfig,
|
|
45
|
+
EntityDefinition,
|
|
46
|
+
AuthFunction,
|
|
47
|
+
ResolverContext,
|
|
48
|
+
ResolverFunction,
|
|
49
|
+
FieldOption,
|
|
50
|
+
} from "./config/types.js";
|
|
51
|
+
|
|
52
|
+
// Re-export Zod for convenience
|
|
53
|
+
export { z } from "zod";
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
OntologySnapshot,
|
|
3
|
+
OntologyDiff,
|
|
4
|
+
FunctionChange,
|
|
5
|
+
} from "./types.js";
|
|
6
|
+
import { hashOntology } from "./hasher.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compare two ontology snapshots and generate a diff.
|
|
10
|
+
* @param oldOntology - The previous ontology (from lockfile), or null if first run
|
|
11
|
+
* @param newOntology - The current ontology (from config)
|
|
12
|
+
*/
|
|
13
|
+
export function diffOntology(
|
|
14
|
+
oldOntology: OntologySnapshot | null,
|
|
15
|
+
newOntology: OntologySnapshot
|
|
16
|
+
): OntologyDiff {
|
|
17
|
+
const newHash = hashOntology(newOntology);
|
|
18
|
+
|
|
19
|
+
// First run - no old ontology
|
|
20
|
+
if (!oldOntology) {
|
|
21
|
+
const functions: FunctionChange[] = Object.entries(
|
|
22
|
+
newOntology.functions
|
|
23
|
+
).map(([name, fn]) => ({
|
|
24
|
+
name,
|
|
25
|
+
type: "added" as const,
|
|
26
|
+
newAccess: fn.access,
|
|
27
|
+
newDescription: fn.description,
|
|
28
|
+
newEntities: fn.entities,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
hasChanges: true,
|
|
33
|
+
addedGroups: newOntology.accessGroups,
|
|
34
|
+
removedGroups: [],
|
|
35
|
+
addedEntities: newOntology.entities ?? [],
|
|
36
|
+
removedEntities: [],
|
|
37
|
+
functions,
|
|
38
|
+
newOntology,
|
|
39
|
+
newHash,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Compare access groups
|
|
44
|
+
const oldGroupSet = new Set(oldOntology.accessGroups);
|
|
45
|
+
const newGroupSet = new Set(newOntology.accessGroups);
|
|
46
|
+
|
|
47
|
+
const addedGroups = newOntology.accessGroups.filter(
|
|
48
|
+
(g) => !oldGroupSet.has(g)
|
|
49
|
+
);
|
|
50
|
+
const removedGroups = oldOntology.accessGroups.filter(
|
|
51
|
+
(g) => !newGroupSet.has(g)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Compare entities
|
|
55
|
+
const oldEntitySet = new Set(oldOntology.entities ?? []);
|
|
56
|
+
const newEntitySet = new Set(newOntology.entities ?? []);
|
|
57
|
+
|
|
58
|
+
const addedEntities = (newOntology.entities ?? []).filter(
|
|
59
|
+
(e) => !oldEntitySet.has(e)
|
|
60
|
+
);
|
|
61
|
+
const removedEntities = (oldOntology.entities ?? []).filter(
|
|
62
|
+
(e) => !newEntitySet.has(e)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Compare functions
|
|
66
|
+
const functions: FunctionChange[] = [];
|
|
67
|
+
const oldFnNames = new Set(Object.keys(oldOntology.functions));
|
|
68
|
+
const newFnNames = new Set(Object.keys(newOntology.functions));
|
|
69
|
+
|
|
70
|
+
// Added functions
|
|
71
|
+
for (const name of newFnNames) {
|
|
72
|
+
if (!oldFnNames.has(name)) {
|
|
73
|
+
const fn = newOntology.functions[name];
|
|
74
|
+
functions.push({
|
|
75
|
+
name,
|
|
76
|
+
type: "added",
|
|
77
|
+
newAccess: fn.access,
|
|
78
|
+
newDescription: fn.description,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Removed functions
|
|
84
|
+
for (const name of oldFnNames) {
|
|
85
|
+
if (!newFnNames.has(name)) {
|
|
86
|
+
const fn = oldOntology.functions[name];
|
|
87
|
+
functions.push({
|
|
88
|
+
name,
|
|
89
|
+
type: "removed",
|
|
90
|
+
oldAccess: fn.access,
|
|
91
|
+
oldDescription: fn.description,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Modified functions
|
|
97
|
+
for (const name of newFnNames) {
|
|
98
|
+
if (oldFnNames.has(name)) {
|
|
99
|
+
const oldFn = oldOntology.functions[name];
|
|
100
|
+
const newFn = newOntology.functions[name];
|
|
101
|
+
|
|
102
|
+
// Check if anything changed
|
|
103
|
+
const accessChanged =
|
|
104
|
+
JSON.stringify(oldFn.access) !== JSON.stringify(newFn.access);
|
|
105
|
+
const descriptionChanged = oldFn.description !== newFn.description;
|
|
106
|
+
const inputsChanged =
|
|
107
|
+
JSON.stringify(oldFn.inputsSchema) !==
|
|
108
|
+
JSON.stringify(newFn.inputsSchema);
|
|
109
|
+
const outputsChanged =
|
|
110
|
+
JSON.stringify(oldFn.outputsSchema) !==
|
|
111
|
+
JSON.stringify(newFn.outputsSchema);
|
|
112
|
+
const entitiesChanged =
|
|
113
|
+
JSON.stringify(oldFn.entities) !== JSON.stringify(newFn.entities);
|
|
114
|
+
const fieldReferencesChanged =
|
|
115
|
+
JSON.stringify(oldFn.fieldReferences) !==
|
|
116
|
+
JSON.stringify(newFn.fieldReferences);
|
|
117
|
+
|
|
118
|
+
if (
|
|
119
|
+
accessChanged ||
|
|
120
|
+
descriptionChanged ||
|
|
121
|
+
inputsChanged ||
|
|
122
|
+
outputsChanged ||
|
|
123
|
+
entitiesChanged ||
|
|
124
|
+
fieldReferencesChanged
|
|
125
|
+
) {
|
|
126
|
+
functions.push({
|
|
127
|
+
name,
|
|
128
|
+
type: "modified",
|
|
129
|
+
oldAccess: accessChanged ? oldFn.access : undefined,
|
|
130
|
+
newAccess: accessChanged ? newFn.access : undefined,
|
|
131
|
+
oldDescription: descriptionChanged ? oldFn.description : undefined,
|
|
132
|
+
newDescription: descriptionChanged ? newFn.description : undefined,
|
|
133
|
+
inputsChanged: inputsChanged || undefined,
|
|
134
|
+
outputsChanged: outputsChanged || undefined,
|
|
135
|
+
entitiesChanged: entitiesChanged || undefined,
|
|
136
|
+
oldEntities: entitiesChanged ? oldFn.entities : undefined,
|
|
137
|
+
newEntities: entitiesChanged ? newFn.entities : undefined,
|
|
138
|
+
fieldReferencesChanged: fieldReferencesChanged || undefined,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const hasChanges =
|
|
145
|
+
addedGroups.length > 0 ||
|
|
146
|
+
removedGroups.length > 0 ||
|
|
147
|
+
addedEntities.length > 0 ||
|
|
148
|
+
removedEntities.length > 0 ||
|
|
149
|
+
functions.length > 0;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
hasChanges,
|
|
153
|
+
addedGroups,
|
|
154
|
+
removedGroups,
|
|
155
|
+
addedEntities,
|
|
156
|
+
removedEntities,
|
|
157
|
+
functions,
|
|
158
|
+
newOntology,
|
|
159
|
+
newHash,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Format a diff for console output
|
|
165
|
+
*/
|
|
166
|
+
export function formatDiffForConsole(diff: OntologyDiff): string {
|
|
167
|
+
if (!diff.hasChanges) {
|
|
168
|
+
return "No changes detected.";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const lines: string[] = ["Ontology changes detected:", ""];
|
|
172
|
+
|
|
173
|
+
if (diff.addedGroups.length > 0) {
|
|
174
|
+
lines.push("Added access groups:");
|
|
175
|
+
for (const group of diff.addedGroups) {
|
|
176
|
+
lines.push(` + ${group}`);
|
|
177
|
+
}
|
|
178
|
+
lines.push("");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (diff.removedGroups.length > 0) {
|
|
182
|
+
lines.push("Removed access groups:");
|
|
183
|
+
for (const group of diff.removedGroups) {
|
|
184
|
+
lines.push(` - ${group}`);
|
|
185
|
+
}
|
|
186
|
+
lines.push("");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (diff.addedEntities.length > 0) {
|
|
190
|
+
lines.push("Added entities:");
|
|
191
|
+
for (const entity of diff.addedEntities) {
|
|
192
|
+
lines.push(` + ${entity}`);
|
|
193
|
+
}
|
|
194
|
+
lines.push("");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (diff.removedEntities.length > 0) {
|
|
198
|
+
lines.push("Removed entities:");
|
|
199
|
+
for (const entity of diff.removedEntities) {
|
|
200
|
+
lines.push(` - ${entity}`);
|
|
201
|
+
}
|
|
202
|
+
lines.push("");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (diff.functions.length > 0) {
|
|
206
|
+
lines.push("Function changes:");
|
|
207
|
+
for (const fn of diff.functions) {
|
|
208
|
+
if (fn.type === "added") {
|
|
209
|
+
lines.push(` + ${fn.name}`);
|
|
210
|
+
lines.push(` Access: [${fn.newAccess?.join(", ")}]`);
|
|
211
|
+
if (fn.newEntities && fn.newEntities.length > 0) {
|
|
212
|
+
lines.push(` Entities: [${fn.newEntities.join(", ")}]`);
|
|
213
|
+
}
|
|
214
|
+
} else if (fn.type === "removed") {
|
|
215
|
+
lines.push(` - ${fn.name}`);
|
|
216
|
+
} else {
|
|
217
|
+
lines.push(` ~ ${fn.name}`);
|
|
218
|
+
if (fn.oldAccess && fn.newAccess) {
|
|
219
|
+
lines.push(
|
|
220
|
+
` Access: [${fn.oldAccess.join(", ")}] -> [${fn.newAccess.join(", ")}]`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (fn.oldEntities && fn.newEntities) {
|
|
224
|
+
lines.push(
|
|
225
|
+
` Entities: [${fn.oldEntities.join(", ")}] -> [${fn.newEntities.join(", ")}]`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (fn.inputsChanged) {
|
|
229
|
+
lines.push(` Inputs: schema changed`);
|
|
230
|
+
}
|
|
231
|
+
if (fn.outputsChanged) {
|
|
232
|
+
lines.push(` Outputs: schema changed`);
|
|
233
|
+
}
|
|
234
|
+
if (fn.fieldReferencesChanged) {
|
|
235
|
+
lines.push(` Field references: changed`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return lines.join("\n");
|
|
242
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
4
|
+
import type { OntologyConfig } from "../config/types.js";
|
|
5
|
+
import { getFieldFromMetadata } from "../config/categorical.js";
|
|
6
|
+
import type {
|
|
7
|
+
OntologySnapshot,
|
|
8
|
+
FunctionShape,
|
|
9
|
+
FieldReference,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Recursively extract fieldFrom references from a Zod schema
|
|
14
|
+
*/
|
|
15
|
+
function extractFieldReferences(
|
|
16
|
+
schema: z.ZodType<unknown>,
|
|
17
|
+
path: string = ""
|
|
18
|
+
): FieldReference[] {
|
|
19
|
+
const results: FieldReference[] = [];
|
|
20
|
+
|
|
21
|
+
// Check if this schema has fieldFrom metadata
|
|
22
|
+
const metadata = getFieldFromMetadata(schema);
|
|
23
|
+
if (metadata) {
|
|
24
|
+
results.push({
|
|
25
|
+
path: path || "(root)",
|
|
26
|
+
functionName: metadata.functionName,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Handle ZodObject - recurse into properties
|
|
31
|
+
if (schema instanceof z.ZodObject) {
|
|
32
|
+
const shape = schema.shape;
|
|
33
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
34
|
+
const fieldPath = path ? `${path}.${key}` : key;
|
|
35
|
+
results.push(
|
|
36
|
+
...extractFieldReferences(value as z.ZodType<unknown>, fieldPath)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Handle ZodOptional - unwrap
|
|
42
|
+
if (schema instanceof z.ZodOptional) {
|
|
43
|
+
results.push(...extractFieldReferences(schema.unwrap(), path));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Handle ZodNullable - unwrap
|
|
47
|
+
if (schema instanceof z.ZodNullable) {
|
|
48
|
+
results.push(...extractFieldReferences(schema.unwrap(), path));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Handle ZodArray - recurse into element
|
|
52
|
+
if (schema instanceof z.ZodArray) {
|
|
53
|
+
results.push(...extractFieldReferences(schema.element, `${path}[]`));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Handle ZodDefault - unwrap
|
|
57
|
+
if (schema instanceof z.ZodDefault) {
|
|
58
|
+
results.push(...extractFieldReferences(schema._def.innerType, path));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return results;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract the ontology snapshot from an OntologyConfig.
|
|
66
|
+
* This extracts ONLY the security-relevant parts:
|
|
67
|
+
* - Function names
|
|
68
|
+
* - Access lists
|
|
69
|
+
* - Input schemas
|
|
70
|
+
* - Output schemas
|
|
71
|
+
* - Descriptions
|
|
72
|
+
* - Entities
|
|
73
|
+
* - Field references (fieldFrom)
|
|
74
|
+
*
|
|
75
|
+
* It DOES NOT include:
|
|
76
|
+
* - Resolver paths (so resolver code can change freely)
|
|
77
|
+
* - Environment configs
|
|
78
|
+
* - Auth function
|
|
79
|
+
*/
|
|
80
|
+
export function extractOntology(config: OntologyConfig): OntologySnapshot {
|
|
81
|
+
const functions: Record<string, FunctionShape> = {};
|
|
82
|
+
|
|
83
|
+
for (const [name, fn] of Object.entries(config.functions)) {
|
|
84
|
+
// Convert Zod schema to JSON Schema for hashing
|
|
85
|
+
// This ensures we're comparing the shape, not the Zod instance
|
|
86
|
+
let inputsSchema: Record<string, unknown>;
|
|
87
|
+
try {
|
|
88
|
+
inputsSchema = zodToJsonSchema(fn.inputs, {
|
|
89
|
+
// Remove $schema to make hashing more stable
|
|
90
|
+
$refStrategy: "none",
|
|
91
|
+
}) as Record<string, unknown>;
|
|
92
|
+
// Remove $schema key if present
|
|
93
|
+
delete inputsSchema.$schema;
|
|
94
|
+
} catch {
|
|
95
|
+
// If zodToJsonSchema fails, use a placeholder
|
|
96
|
+
inputsSchema = { type: "unknown" };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Convert outputs schema if present
|
|
100
|
+
let outputsSchema: Record<string, unknown> | undefined;
|
|
101
|
+
if (fn.outputs) {
|
|
102
|
+
try {
|
|
103
|
+
outputsSchema = zodToJsonSchema(fn.outputs, {
|
|
104
|
+
$refStrategy: "none",
|
|
105
|
+
}) as Record<string, unknown>;
|
|
106
|
+
delete outputsSchema.$schema;
|
|
107
|
+
} catch {
|
|
108
|
+
outputsSchema = { type: "unknown" };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Extract field references
|
|
113
|
+
const fieldReferences = extractFieldReferences(fn.inputs);
|
|
114
|
+
|
|
115
|
+
functions[name] = {
|
|
116
|
+
description: fn.description,
|
|
117
|
+
// Sort access groups for consistent hashing
|
|
118
|
+
access: [...fn.access].sort(),
|
|
119
|
+
// Sort entities for consistent hashing
|
|
120
|
+
entities: [...fn.entities].sort(),
|
|
121
|
+
inputsSchema,
|
|
122
|
+
outputsSchema,
|
|
123
|
+
fieldReferences:
|
|
124
|
+
fieldReferences.length > 0 ? fieldReferences : undefined,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
name: config.name,
|
|
130
|
+
// Sort access groups for consistent hashing
|
|
131
|
+
accessGroups: Object.keys(config.accessGroups).sort(),
|
|
132
|
+
// Sort entities for consistent hashing
|
|
133
|
+
entities: config.entities
|
|
134
|
+
? Object.keys(config.entities).sort()
|
|
135
|
+
: undefined,
|
|
136
|
+
functions,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Create a deterministic hash of an ontology snapshot.
|
|
142
|
+
* Uses SHA256 and returns a 16-character hex string.
|
|
143
|
+
*/
|
|
144
|
+
export function hashOntology(ontology: OntologySnapshot): string {
|
|
145
|
+
// Sort all keys at all levels for deterministic hashing
|
|
146
|
+
const normalized = JSON.stringify(ontology, (_, value) => {
|
|
147
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
148
|
+
// Sort object keys
|
|
149
|
+
return Object.keys(value)
|
|
150
|
+
.sort()
|
|
151
|
+
.reduce(
|
|
152
|
+
(sorted, key) => {
|
|
153
|
+
sorted[key] = value[key];
|
|
154
|
+
return sorted;
|
|
155
|
+
},
|
|
156
|
+
{} as Record<string, unknown>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Extract ontology snapshot and compute hash in one step
|
|
167
|
+
*/
|
|
168
|
+
export function computeOntologyHash(config: OntologyConfig): {
|
|
169
|
+
ontology: OntologySnapshot;
|
|
170
|
+
hash: string;
|
|
171
|
+
} {
|
|
172
|
+
const ontology = extractOntology(config);
|
|
173
|
+
const hash = hashOntology(ontology);
|
|
174
|
+
return { ontology, hash };
|
|
175
|
+
}
|