ont-run 0.0.1 → 0.0.3
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 +2 -0
- package/dist/bin/ont.js +113 -15
- package/dist/index.js +181 -25
- package/dist/src/browser/transform.d.ts +1 -0
- package/dist/src/config/categorical.d.ts +59 -0
- package/dist/src/config/index.d.ts +3 -3
- package/dist/src/config/schema.d.ts +9 -0
- package/dist/src/config/types.d.ts +14 -2
- package/dist/src/index.d.ts +2 -2
- package/dist/src/lockfile/types.d.ts +4 -0
- package/dist/src/server/api/middleware.d.ts +7 -1
- package/dist/src/server/mcp/tools.d.ts +3 -3
- package/package.json +2 -2
- package/src/browser/server.ts +27 -1
- package/src/browser/transform.ts +7 -1
- package/src/cli/commands/init.ts +38 -6
- package/src/cli/utils/config-loader.ts +18 -3
- package/src/config/categorical.ts +90 -0
- package/src/config/index.ts +3 -1
- package/src/config/schema.ts +64 -1
- package/src/config/types.ts +15 -2
- package/src/index.ts +2 -1
- package/src/lockfile/differ.ts +9 -1
- package/src/lockfile/hasher.ts +6 -1
- package/src/lockfile/types.ts +4 -0
- package/src/server/api/middleware.ts +18 -4
- package/src/server/api/router.ts +26 -3
- package/src/server/mcp/index.ts +31 -12
- package/src/server/mcp/tools.ts +67 -6
- package/src/server/start.ts +9 -0
|
@@ -3,6 +3,10 @@ import { z } from "zod";
|
|
|
3
3
|
* Symbol for storing fieldFrom metadata on Zod schemas
|
|
4
4
|
*/
|
|
5
5
|
export declare const FIELD_FROM_METADATA: unique symbol;
|
|
6
|
+
/**
|
|
7
|
+
* Symbol for storing userContext metadata on Zod schemas
|
|
8
|
+
*/
|
|
9
|
+
export declare const USER_CONTEXT_METADATA: unique symbol;
|
|
6
10
|
/**
|
|
7
11
|
* Metadata stored on fieldFrom Zod schemas
|
|
8
12
|
*/
|
|
@@ -74,3 +78,58 @@ export declare function hasFieldFromMetadata(schema: unknown): schema is FieldFr
|
|
|
74
78
|
* Extract fieldFrom metadata from a Zod schema
|
|
75
79
|
*/
|
|
76
80
|
export declare function getFieldFromMetadata(schema: unknown): FieldFromMetadata | null;
|
|
81
|
+
/**
|
|
82
|
+
* Type for a Zod schema with userContext metadata
|
|
83
|
+
*/
|
|
84
|
+
export type UserContextSchema<T extends z.ZodType> = T & {
|
|
85
|
+
[USER_CONTEXT_METADATA]: true;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Mark a schema as user context that will be injected at runtime.
|
|
89
|
+
*
|
|
90
|
+
* Fields marked with `userContext()` are:
|
|
91
|
+
* - **Injected**: Populated from `auth()` result's `user` field
|
|
92
|
+
* - **Hidden**: Not exposed in public API/MCP schemas
|
|
93
|
+
* - **Type-safe**: Resolver receives typed user object
|
|
94
|
+
*
|
|
95
|
+
* @param schema - Zod schema for the user context shape
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* defineOntology({
|
|
100
|
+
* auth: async (req) => {
|
|
101
|
+
* const user = await verifyToken(req);
|
|
102
|
+
* return {
|
|
103
|
+
* groups: user.isAdmin ? ['admin'] : ['user'],
|
|
104
|
+
* user: { id: user.id, email: user.email }
|
|
105
|
+
* };
|
|
106
|
+
* },
|
|
107
|
+
*
|
|
108
|
+
* functions: {
|
|
109
|
+
* editPost: {
|
|
110
|
+
* description: 'Edit a post',
|
|
111
|
+
* access: ['user', 'admin'],
|
|
112
|
+
* entities: ['Post'],
|
|
113
|
+
* inputs: z.object({
|
|
114
|
+
* postId: z.string(),
|
|
115
|
+
* title: z.string(),
|
|
116
|
+
* currentUser: userContext(z.object({
|
|
117
|
+
* id: z.string(),
|
|
118
|
+
* email: z.string(),
|
|
119
|
+
* })),
|
|
120
|
+
* }),
|
|
121
|
+
* resolver: './resolvers/editPost.ts',
|
|
122
|
+
* },
|
|
123
|
+
* },
|
|
124
|
+
* })
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export declare function userContext<T extends z.ZodType>(schema: T): UserContextSchema<T>;
|
|
128
|
+
/**
|
|
129
|
+
* Check if a Zod schema has userContext metadata
|
|
130
|
+
*/
|
|
131
|
+
export declare function hasUserContextMetadata(schema: unknown): schema is UserContextSchema<z.ZodType>;
|
|
132
|
+
/**
|
|
133
|
+
* Get all userContext field names from a Zod object schema
|
|
134
|
+
*/
|
|
135
|
+
export declare function getUserContextFields(schema: z.ZodType): string[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { defineOntology } from "./define.js";
|
|
2
|
-
export { fieldFrom } from "./categorical.js";
|
|
3
|
-
export type { OntologyConfig, FunctionDefinition, AccessGroupConfig, EnvironmentConfig, EntityDefinition, AuthFunction, ResolverContext, ResolverFunction, FieldOption, } from "./types.js";
|
|
4
|
-
export { OntologyConfigSchema, FunctionDefinitionSchema, AccessGroupConfigSchema, EnvironmentConfigSchema, EntityDefinitionSchema, validateAccessGroups, validateEntityReferences, validateFieldFromReferences, } from "./schema.js";
|
|
2
|
+
export { fieldFrom, userContext } from "./categorical.js";
|
|
3
|
+
export type { OntologyConfig, FunctionDefinition, AccessGroupConfig, EnvironmentConfig, EntityDefinition, AuthFunction, AuthResult, ResolverContext, ResolverFunction, FieldOption, } from "./types.js";
|
|
4
|
+
export { OntologyConfigSchema, FunctionDefinitionSchema, AccessGroupConfigSchema, EnvironmentConfigSchema, EntityDefinitionSchema, validateAccessGroups, validateEntityReferences, validateFieldFromReferences, validateUserContextRequirements, } from "./schema.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import type { OntologyConfig } from "./types.js";
|
|
2
3
|
/**
|
|
3
4
|
* Schema for environment configuration
|
|
4
5
|
*/
|
|
@@ -160,3 +161,11 @@ export declare function validateEntityReferences(config: z.infer<typeof Ontology
|
|
|
160
161
|
* Validate that all fieldFrom() references point to existing functions
|
|
161
162
|
*/
|
|
162
163
|
export declare function validateFieldFromReferences(config: z.infer<typeof OntologyConfigSchema>): void;
|
|
164
|
+
/**
|
|
165
|
+
* Validate that functions using userContext() will receive user data from auth.
|
|
166
|
+
* This does a runtime check by calling auth with a mock request to verify it returns
|
|
167
|
+
* an AuthResult with a user field.
|
|
168
|
+
*
|
|
169
|
+
* @param config - The full OntologyConfig (needed for the actual auth function)
|
|
170
|
+
*/
|
|
171
|
+
export declare function validateUserContextRequirements(config: OntologyConfig): Promise<void>;
|
|
@@ -50,9 +50,21 @@ export interface FunctionDefinition<TGroups extends string = string, TEntities e
|
|
|
50
50
|
resolver: string;
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
*
|
|
53
|
+
* Result returned by the auth function
|
|
54
54
|
*/
|
|
55
|
-
export
|
|
55
|
+
export interface AuthResult {
|
|
56
|
+
/** Access groups for the current request */
|
|
57
|
+
groups: string[];
|
|
58
|
+
/** Optional user identity for row-level access control */
|
|
59
|
+
user?: Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Auth function that determines access groups for a request.
|
|
63
|
+
* Can return either:
|
|
64
|
+
* - `string[]` - just group names (backwards compatible)
|
|
65
|
+
* - `AuthResult` - groups plus optional user identity
|
|
66
|
+
*/
|
|
67
|
+
export type AuthFunction = (req: Request) => Promise<string[] | AuthResult> | string[] | AuthResult;
|
|
56
68
|
/**
|
|
57
69
|
* The main Ontology configuration object
|
|
58
70
|
*/
|
package/dist/src/index.d.ts
CHANGED
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
32
|
export { defineOntology } from "./config/define.js";
|
|
33
|
-
export { fieldFrom } from "./config/categorical.js";
|
|
33
|
+
export { fieldFrom, userContext } from "./config/categorical.js";
|
|
34
34
|
export { startOnt } from "./server/start.js";
|
|
35
35
|
export type { StartOntOptions, StartOntResult } from "./server/start.js";
|
|
36
|
-
export type { OntologyConfig, FunctionDefinition, AccessGroupConfig, EnvironmentConfig, EntityDefinition, AuthFunction, ResolverContext, ResolverFunction, FieldOption, } from "./config/types.js";
|
|
36
|
+
export type { OntologyConfig, FunctionDefinition, AccessGroupConfig, EnvironmentConfig, EntityDefinition, AuthFunction, AuthResult, ResolverContext, ResolverFunction, FieldOption, } from "./config/types.js";
|
|
37
37
|
export { z } from "zod";
|
|
@@ -23,6 +23,8 @@ export interface FunctionShape {
|
|
|
23
23
|
outputsSchema?: Record<string, unknown>;
|
|
24
24
|
/** Fields that reference other functions for their options */
|
|
25
25
|
fieldReferences?: FieldReference[];
|
|
26
|
+
/** Whether this function uses userContext() for row-level access control */
|
|
27
|
+
usesUserContext?: boolean;
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
30
|
* Complete snapshot of the ontology
|
|
@@ -66,6 +68,8 @@ export interface FunctionChange {
|
|
|
66
68
|
oldEntities?: string[];
|
|
67
69
|
newEntities?: string[];
|
|
68
70
|
fieldReferencesChanged?: boolean;
|
|
71
|
+
userContextChanged?: boolean;
|
|
72
|
+
usesUserContext?: boolean;
|
|
69
73
|
}
|
|
70
74
|
/**
|
|
71
75
|
* Diff between old and new ontology
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import type { OntologyConfig, ResolverContext, EnvironmentConfig } from "../../config/types.js";
|
|
1
|
+
import type { OntologyConfig, ResolverContext, EnvironmentConfig, AuthResult } from "../../config/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Normalize auth function result to AuthResult format.
|
|
4
|
+
* Supports both legacy string[] format and new AuthResult object.
|
|
5
|
+
*/
|
|
6
|
+
export declare function normalizeAuthResult(result: string[] | AuthResult): AuthResult;
|
|
2
7
|
/**
|
|
3
8
|
* Context variables added by middleware
|
|
4
9
|
*/
|
|
5
10
|
export interface OntologyVariables {
|
|
6
11
|
resolverContext: ResolverContext;
|
|
7
12
|
accessGroups: string[];
|
|
13
|
+
authResult: AuthResult;
|
|
8
14
|
}
|
|
9
15
|
/**
|
|
10
16
|
* Create auth middleware that calls the user's auth function
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OntologyConfig, EnvironmentConfig } from "../../config/types.js";
|
|
1
|
+
import type { OntologyConfig, EnvironmentConfig, AuthResult } from "../../config/types.js";
|
|
2
2
|
import { type Logger } from "../resolver.js";
|
|
3
3
|
/**
|
|
4
4
|
* Field reference info for MCP tools
|
|
@@ -30,6 +30,6 @@ export declare function generateMcpTools(config: OntologyConfig): McpTool[];
|
|
|
30
30
|
*/
|
|
31
31
|
export declare function filterToolsByAccess(tools: McpTool[], accessGroups: string[]): McpTool[];
|
|
32
32
|
/**
|
|
33
|
-
* Create a tool executor function that accepts per-request
|
|
33
|
+
* Create a tool executor function that accepts per-request auth result
|
|
34
34
|
*/
|
|
35
|
-
export declare function createToolExecutor(config: OntologyConfig, configDir: string, env: string, envConfig: EnvironmentConfig, logger: Logger): (toolName: string, args: unknown,
|
|
35
|
+
export declare function createToolExecutor(config: OntologyConfig, configDir: string, env: string, envConfig: EnvironmentConfig, logger: Logger): (toolName: string, args: unknown, authResult: AuthResult) => Promise<unknown>;
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ont-run",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Ontology-enforced API framework for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"ont": "./dist/bin/ont.js"
|
|
7
|
+
"ont-run": "./dist/bin/ont.js"
|
|
8
8
|
},
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
package/src/browser/server.ts
CHANGED
|
@@ -407,6 +407,27 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
|
|
|
407
407
|
color: var(--change-modified);
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
/* User Context Badge */
|
|
411
|
+
.user-context-badge {
|
|
412
|
+
display: inline-flex;
|
|
413
|
+
align-items: center;
|
|
414
|
+
gap: 4px;
|
|
415
|
+
padding: 2px 8px;
|
|
416
|
+
border-radius: 9999px;
|
|
417
|
+
font-size: 10px;
|
|
418
|
+
font-weight: 500;
|
|
419
|
+
background: rgba(21, 168, 168, 0.12);
|
|
420
|
+
color: var(--vanna-teal);
|
|
421
|
+
text-transform: uppercase;
|
|
422
|
+
letter-spacing: 0.03em;
|
|
423
|
+
margin-left: 8px;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.user-context-badge svg {
|
|
427
|
+
width: 12px;
|
|
428
|
+
height: 12px;
|
|
429
|
+
}
|
|
430
|
+
|
|
410
431
|
/* Review Footer */
|
|
411
432
|
.review-footer {
|
|
412
433
|
position: fixed;
|
|
@@ -1875,9 +1896,14 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
|
|
|
1875
1896
|
? \`<span class="detail-change-badge \${changeStatus}">\${changeStatus === 'added' ? 'New' : changeStatus === 'removed' ? 'Removed' : 'Modified'}</span>\`
|
|
1876
1897
|
: '';
|
|
1877
1898
|
|
|
1899
|
+
// Build user context badge if applicable
|
|
1900
|
+
const userContextBadge = data.metadata?.usesUserContext
|
|
1901
|
+
? \`<span class="user-context-badge"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>User Context</span>\`
|
|
1902
|
+
: '';
|
|
1903
|
+
|
|
1878
1904
|
let html = \`
|
|
1879
1905
|
<div class="detail-header">
|
|
1880
|
-
<div class="detail-type \${data.type}">\${formatType(data.type)}\${changeBadge}</div>
|
|
1906
|
+
<div class="detail-type \${data.type}">\${formatType(data.type)}\${changeBadge}\${userContextBadge}</div>
|
|
1881
1907
|
<div class="detail-name">\${data.label}</div>
|
|
1882
1908
|
<div class="detail-description">\${data.description || 'No description'}</div>
|
|
1883
1909
|
</div>
|
package/src/browser/transform.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
3
3
|
import type { OntologyConfig } from "../config/types.js";
|
|
4
4
|
import type { OntologyDiff, FunctionChange } from "../lockfile/types.js";
|
|
5
|
-
import { getFieldFromMetadata } from "../config/categorical.js";
|
|
5
|
+
import { getFieldFromMetadata, getUserContextFields } from "../config/categorical.js";
|
|
6
6
|
|
|
7
7
|
export type NodeType = "entity" | "function" | "accessGroup";
|
|
8
8
|
export type EdgeType = "operates-on" | "requires-access" | "depends-on";
|
|
@@ -18,6 +18,7 @@ export interface GraphNode {
|
|
|
18
18
|
outputs?: Record<string, unknown>;
|
|
19
19
|
resolver?: string;
|
|
20
20
|
functionCount?: number;
|
|
21
|
+
usesUserContext?: boolean;
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -154,6 +155,10 @@ export function transformToGraphData(config: OntologyConfig): GraphData {
|
|
|
154
155
|
entityCounts[entity] = (entityCounts[entity] || 0) + 1;
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
// Check if function uses userContext
|
|
159
|
+
const userContextFields = getUserContextFields(fn.inputs);
|
|
160
|
+
const usesUserContext = userContextFields.length > 0;
|
|
161
|
+
|
|
157
162
|
// Create function node
|
|
158
163
|
nodes.push({
|
|
159
164
|
id: `function:${name}`,
|
|
@@ -164,6 +169,7 @@ export function transformToGraphData(config: OntologyConfig): GraphData {
|
|
|
164
169
|
inputs: safeZodToJsonSchema(fn.inputs),
|
|
165
170
|
outputs: fn.outputs ? safeZodToJsonSchema(fn.outputs) : undefined,
|
|
166
171
|
resolver: fn.resolver,
|
|
172
|
+
usesUserContext: usesUserContext || undefined,
|
|
167
173
|
},
|
|
168
174
|
});
|
|
169
175
|
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -51,7 +51,7 @@ export const initCommand = defineCommand({
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// Write ontology.config.ts
|
|
54
|
-
const configTemplate = `import { defineOntology } from 'ont-run';
|
|
54
|
+
const configTemplate = `import { defineOntology, userContext } from 'ont-run';
|
|
55
55
|
import { z } from 'zod';
|
|
56
56
|
|
|
57
57
|
export default defineOntology({
|
|
@@ -63,13 +63,22 @@ export default defineOntology({
|
|
|
63
63
|
},
|
|
64
64
|
|
|
65
65
|
// Pluggable auth - customize this for your use case
|
|
66
|
+
// Return { groups, user } for row-level access control
|
|
66
67
|
auth: async (req) => {
|
|
67
68
|
const token = req.headers.get('Authorization');
|
|
68
|
-
// Return access groups
|
|
69
|
+
// Return access groups and optional user data
|
|
69
70
|
// This is where you'd verify JWTs, API keys, etc.
|
|
70
|
-
if (!token) return ['public'];
|
|
71
|
-
if (token === 'admin-secret')
|
|
72
|
-
|
|
71
|
+
if (!token) return { groups: ['public'] };
|
|
72
|
+
if (token === 'admin-secret') {
|
|
73
|
+
return {
|
|
74
|
+
groups: ['admin', 'support', 'public'],
|
|
75
|
+
user: { id: 'admin-1', email: 'admin@example.com' },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
groups: ['support', 'public'],
|
|
80
|
+
user: { id: 'user-1', email: 'user@example.com' },
|
|
81
|
+
};
|
|
73
82
|
},
|
|
74
83
|
|
|
75
84
|
accessGroups: {
|
|
@@ -78,21 +87,32 @@ export default defineOntology({
|
|
|
78
87
|
admin: { description: 'Administrators' },
|
|
79
88
|
},
|
|
80
89
|
|
|
90
|
+
entities: {
|
|
91
|
+
User: { description: 'A user account' },
|
|
92
|
+
},
|
|
93
|
+
|
|
81
94
|
functions: {
|
|
82
95
|
// Example: Public function
|
|
83
96
|
healthCheck: {
|
|
84
97
|
description: 'Check API health status',
|
|
85
98
|
access: ['public', 'support', 'admin'],
|
|
99
|
+
entities: [],
|
|
86
100
|
inputs: z.object({}),
|
|
87
101
|
resolver: './resolvers/healthCheck.ts',
|
|
88
102
|
},
|
|
89
103
|
|
|
90
|
-
// Example: Restricted function
|
|
104
|
+
// Example: Restricted function with row-level access
|
|
91
105
|
getUser: {
|
|
92
106
|
description: 'Get user details by ID',
|
|
93
107
|
access: ['support', 'admin'],
|
|
108
|
+
entities: ['User'],
|
|
94
109
|
inputs: z.object({
|
|
95
110
|
userId: z.string().uuid(),
|
|
111
|
+
// currentUser is injected from auth - not visible to API callers
|
|
112
|
+
currentUser: userContext(z.object({
|
|
113
|
+
id: z.string(),
|
|
114
|
+
email: z.string(),
|
|
115
|
+
})),
|
|
96
116
|
}),
|
|
97
117
|
resolver: './resolvers/getUser.ts',
|
|
98
118
|
},
|
|
@@ -101,6 +121,7 @@ export default defineOntology({
|
|
|
101
121
|
deleteUser: {
|
|
102
122
|
description: 'Delete a user account',
|
|
103
123
|
access: ['admin'],
|
|
124
|
+
entities: ['User'],
|
|
104
125
|
inputs: z.object({
|
|
105
126
|
userId: z.string().uuid(),
|
|
106
127
|
reason: z.string().optional(),
|
|
@@ -132,10 +153,21 @@ export default async function healthCheck(ctx: ResolverContext, args: {}) {
|
|
|
132
153
|
|
|
133
154
|
interface GetUserArgs {
|
|
134
155
|
userId: string;
|
|
156
|
+
currentUser: {
|
|
157
|
+
id: string;
|
|
158
|
+
email: string;
|
|
159
|
+
};
|
|
135
160
|
}
|
|
136
161
|
|
|
137
162
|
export default async function getUser(ctx: ResolverContext, args: GetUserArgs) {
|
|
138
163
|
ctx.logger.info(\`Getting user: \${args.userId}\`);
|
|
164
|
+
ctx.logger.info(\`Requested by: \${args.currentUser.email}\`);
|
|
165
|
+
|
|
166
|
+
// Example: Check if user can access this resource
|
|
167
|
+
// Support can only view their own account
|
|
168
|
+
if (!ctx.accessGroups.includes('admin') && args.userId !== args.currentUser.id) {
|
|
169
|
+
throw new Error('You can only view your own account');
|
|
170
|
+
}
|
|
139
171
|
|
|
140
172
|
// This is where you'd query your database
|
|
141
173
|
// Example response:
|
|
@@ -70,9 +70,24 @@ export async function loadConfig(configPath?: string): Promise<{
|
|
|
70
70
|
|
|
71
71
|
return { config, configDir, configPath: resolvedPath };
|
|
72
72
|
} catch (error) {
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
const err = error as NodeJS.ErrnoException & { message?: string };
|
|
74
|
+
if (err.code === "ERR_MODULE_NOT_FOUND") {
|
|
75
|
+
// Check if it's a missing dependency vs missing config
|
|
76
|
+
const message = err.message || "";
|
|
77
|
+
if (message.includes("ont-run") || message.includes("zod")) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Failed to load config: ${resolvedPath}\n\n` +
|
|
80
|
+
`Missing dependencies. Run 'bun install' first.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Failed to load config: ${resolvedPath}\n\n` +
|
|
85
|
+
`Module not found: ${message}`
|
|
86
|
+
);
|
|
75
87
|
}
|
|
76
|
-
throw
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Failed to load config: ${resolvedPath}\n\n` +
|
|
90
|
+
`${err.message || error}`
|
|
91
|
+
);
|
|
77
92
|
}
|
|
78
93
|
}
|
|
@@ -5,6 +5,11 @@ import { z } from "zod";
|
|
|
5
5
|
*/
|
|
6
6
|
export const FIELD_FROM_METADATA = Symbol.for("ont:fieldFrom");
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Symbol for storing userContext metadata on Zod schemas
|
|
10
|
+
*/
|
|
11
|
+
export const USER_CONTEXT_METADATA = Symbol.for("ont:userContext");
|
|
12
|
+
|
|
8
13
|
/**
|
|
9
14
|
* Metadata stored on fieldFrom Zod schemas
|
|
10
15
|
*/
|
|
@@ -99,3 +104,88 @@ export function getFieldFromMetadata(
|
|
|
99
104
|
}
|
|
100
105
|
return null;
|
|
101
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Type for a Zod schema with userContext metadata
|
|
110
|
+
*/
|
|
111
|
+
export type UserContextSchema<T extends z.ZodType> = T & {
|
|
112
|
+
[USER_CONTEXT_METADATA]: true;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Mark a schema as user context that will be injected at runtime.
|
|
117
|
+
*
|
|
118
|
+
* Fields marked with `userContext()` are:
|
|
119
|
+
* - **Injected**: Populated from `auth()` result's `user` field
|
|
120
|
+
* - **Hidden**: Not exposed in public API/MCP schemas
|
|
121
|
+
* - **Type-safe**: Resolver receives typed user object
|
|
122
|
+
*
|
|
123
|
+
* @param schema - Zod schema for the user context shape
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* defineOntology({
|
|
128
|
+
* auth: async (req) => {
|
|
129
|
+
* const user = await verifyToken(req);
|
|
130
|
+
* return {
|
|
131
|
+
* groups: user.isAdmin ? ['admin'] : ['user'],
|
|
132
|
+
* user: { id: user.id, email: user.email }
|
|
133
|
+
* };
|
|
134
|
+
* },
|
|
135
|
+
*
|
|
136
|
+
* functions: {
|
|
137
|
+
* editPost: {
|
|
138
|
+
* description: 'Edit a post',
|
|
139
|
+
* access: ['user', 'admin'],
|
|
140
|
+
* entities: ['Post'],
|
|
141
|
+
* inputs: z.object({
|
|
142
|
+
* postId: z.string(),
|
|
143
|
+
* title: z.string(),
|
|
144
|
+
* currentUser: userContext(z.object({
|
|
145
|
+
* id: z.string(),
|
|
146
|
+
* email: z.string(),
|
|
147
|
+
* })),
|
|
148
|
+
* }),
|
|
149
|
+
* resolver: './resolvers/editPost.ts',
|
|
150
|
+
* },
|
|
151
|
+
* },
|
|
152
|
+
* })
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export function userContext<T extends z.ZodType>(schema: T): UserContextSchema<T> {
|
|
156
|
+
const marked = schema as UserContextSchema<T>;
|
|
157
|
+
marked[USER_CONTEXT_METADATA] = true;
|
|
158
|
+
return marked;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check if a Zod schema has userContext metadata
|
|
163
|
+
*/
|
|
164
|
+
export function hasUserContextMetadata(
|
|
165
|
+
schema: unknown
|
|
166
|
+
): schema is UserContextSchema<z.ZodType> {
|
|
167
|
+
return (
|
|
168
|
+
schema !== null &&
|
|
169
|
+
typeof schema === "object" &&
|
|
170
|
+
USER_CONTEXT_METADATA in schema &&
|
|
171
|
+
(schema as Record<symbol, unknown>)[USER_CONTEXT_METADATA] === true
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get all userContext field names from a Zod object schema
|
|
177
|
+
*/
|
|
178
|
+
export function getUserContextFields(schema: z.ZodType): string[] {
|
|
179
|
+
const fields: string[] = [];
|
|
180
|
+
|
|
181
|
+
if (schema instanceof z.ZodObject) {
|
|
182
|
+
const shape = schema.shape;
|
|
183
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
184
|
+
if (hasUserContextMetadata(value)) {
|
|
185
|
+
fields.push(key);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return fields;
|
|
191
|
+
}
|
package/src/config/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { defineOntology } from "./define.js";
|
|
2
|
-
export { fieldFrom } from "./categorical.js";
|
|
2
|
+
export { fieldFrom, userContext } from "./categorical.js";
|
|
3
3
|
export type {
|
|
4
4
|
OntologyConfig,
|
|
5
5
|
FunctionDefinition,
|
|
@@ -7,6 +7,7 @@ export type {
|
|
|
7
7
|
EnvironmentConfig,
|
|
8
8
|
EntityDefinition,
|
|
9
9
|
AuthFunction,
|
|
10
|
+
AuthResult,
|
|
10
11
|
ResolverContext,
|
|
11
12
|
ResolverFunction,
|
|
12
13
|
FieldOption,
|
|
@@ -20,4 +21,5 @@ export {
|
|
|
20
21
|
validateAccessGroups,
|
|
21
22
|
validateEntityReferences,
|
|
22
23
|
validateFieldFromReferences,
|
|
24
|
+
validateUserContextRequirements,
|
|
23
25
|
} from "./schema.js";
|
package/src/config/schema.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getFieldFromMetadata } from "./categorical.js";
|
|
2
|
+
import { getFieldFromMetadata, getUserContextFields } from "./categorical.js";
|
|
3
|
+
import type { OntologyConfig } from "./types.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Schema for environment configuration
|
|
@@ -194,3 +195,65 @@ export function validateFieldFromReferences(
|
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Validate that functions using userContext() will receive user data from auth.
|
|
201
|
+
* This does a runtime check by calling auth with a mock request to verify it returns
|
|
202
|
+
* an AuthResult with a user field.
|
|
203
|
+
*
|
|
204
|
+
* @param config - The full OntologyConfig (needed for the actual auth function)
|
|
205
|
+
*/
|
|
206
|
+
export async function validateUserContextRequirements(
|
|
207
|
+
config: OntologyConfig
|
|
208
|
+
): Promise<void> {
|
|
209
|
+
// Find functions that use userContext
|
|
210
|
+
const functionsWithUserContext: string[] = [];
|
|
211
|
+
|
|
212
|
+
for (const [fnName, fn] of Object.entries(config.functions)) {
|
|
213
|
+
const userContextFields = getUserContextFields(fn.inputs as z.ZodType);
|
|
214
|
+
if (userContextFields.length > 0) {
|
|
215
|
+
functionsWithUserContext.push(fnName);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// If no functions use userContext, no validation needed
|
|
220
|
+
if (functionsWithUserContext.length === 0) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Create a mock request to test the auth function
|
|
225
|
+
const mockRequest = new Request("http://localhost/test", {
|
|
226
|
+
headers: { Authorization: "test-token" },
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const authResult = await config.auth(mockRequest);
|
|
231
|
+
|
|
232
|
+
// Check if auth returns an object with user field
|
|
233
|
+
const hasUserField =
|
|
234
|
+
authResult !== null &&
|
|
235
|
+
typeof authResult === "object" &&
|
|
236
|
+
!Array.isArray(authResult) &&
|
|
237
|
+
"user" in authResult;
|
|
238
|
+
|
|
239
|
+
if (!hasUserField) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`The following functions use userContext() but auth() does not return a user object:\n` +
|
|
242
|
+
` ${functionsWithUserContext.join(", ")}\n\n` +
|
|
243
|
+
`To fix this, update your auth function to return an AuthResult:\n` +
|
|
244
|
+
` auth: async (req) => {\n` +
|
|
245
|
+
` return {\n` +
|
|
246
|
+
` groups: ['user'],\n` +
|
|
247
|
+
` user: { id: '...', email: '...' } // Add user data here\n` +
|
|
248
|
+
` };\n` +
|
|
249
|
+
` }`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
// If auth throws, we can't validate - but that's ok, the error will surface at request time
|
|
254
|
+
if (error instanceof Error && error.message.includes("userContext")) {
|
|
255
|
+
throw error; // Re-throw our validation error
|
|
256
|
+
}
|
|
257
|
+
// Otherwise, auth function had an error with the mock request - skip validation
|
|
258
|
+
}
|
|
259
|
+
}
|
package/src/config/types.ts
CHANGED
|
@@ -59,9 +59,22 @@ export interface FunctionDefinition<
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
62
|
+
* Result returned by the auth function
|
|
63
63
|
*/
|
|
64
|
-
export
|
|
64
|
+
export interface AuthResult {
|
|
65
|
+
/** Access groups for the current request */
|
|
66
|
+
groups: string[];
|
|
67
|
+
/** Optional user identity for row-level access control */
|
|
68
|
+
user?: Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Auth function that determines access groups for a request.
|
|
73
|
+
* Can return either:
|
|
74
|
+
* - `string[]` - just group names (backwards compatible)
|
|
75
|
+
* - `AuthResult` - groups plus optional user identity
|
|
76
|
+
*/
|
|
77
|
+
export type AuthFunction = (req: Request) => Promise<string[] | AuthResult> | string[] | AuthResult;
|
|
65
78
|
|
|
66
79
|
/**
|
|
67
80
|
* The main Ontology configuration object
|
package/src/index.ts
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
// Main API
|
|
34
34
|
export { defineOntology } from "./config/define.js";
|
|
35
|
-
export { fieldFrom } from "./config/categorical.js";
|
|
35
|
+
export { fieldFrom, userContext } from "./config/categorical.js";
|
|
36
36
|
export { startOnt } from "./server/start.js";
|
|
37
37
|
export type { StartOntOptions, StartOntResult } from "./server/start.js";
|
|
38
38
|
|
|
@@ -44,6 +44,7 @@ export type {
|
|
|
44
44
|
EnvironmentConfig,
|
|
45
45
|
EntityDefinition,
|
|
46
46
|
AuthFunction,
|
|
47
|
+
AuthResult,
|
|
47
48
|
ResolverContext,
|
|
48
49
|
ResolverFunction,
|
|
49
50
|
FieldOption,
|