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.
@@ -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
- * Auth function that determines access groups for a request
53
+ * Result returned by the auth function
54
54
  */
55
- export type AuthFunction = (req: Request) => Promise<string[]> | string[];
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
  */
@@ -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 access groups
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, accessGroups: string[]) => Promise<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.1",
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
  ".": {
@@ -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>
@@ -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
 
@@ -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 based on auth
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') return ['admin', 'support', 'public'];
72
- return ['support', 'public'];
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
- if ((error as NodeJS.ErrnoException).code === "ERR_MODULE_NOT_FOUND") {
74
- throw new Error(`Failed to load config: ${resolvedPath}`);
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 error;
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
+ }
@@ -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";
@@ -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
+ }
@@ -59,9 +59,22 @@ export interface FunctionDefinition<
59
59
  }
60
60
 
61
61
  /**
62
- * Auth function that determines access groups for a request
62
+ * Result returned by the auth function
63
63
  */
64
- export type AuthFunction = (req: Request) => Promise<string[]> | string[];
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,