ont-run 0.0.1 → 0.0.2

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 CHANGED
@@ -21,12 +21,14 @@ export default defineOntology({
21
21
  getTicket: {
22
22
  description: 'Get ticket details',
23
23
  access: ['support', 'admin'],
24
+ entities: ['Ticket'],
24
25
  inputs: z.object({ ticketId: z.string().uuid() }),
25
26
  resolver: './resolvers/getTicket.ts',
26
27
  },
27
28
  assignTicket: {
28
29
  description: 'Assign ticket to an agent',
29
30
  access: ['admin'], // If AI tries to add 'public' here, review is triggered
31
+ entities: ['Ticket'],
30
32
  inputs: z.object({ ticketId: z.string().uuid(), assignee: z.string() }),
31
33
  resolver: './resolvers/assignTicket.ts',
32
34
  },
package/dist/bin/ont.js CHANGED
@@ -6165,9 +6165,26 @@ function getFieldFromMetadata(schema) {
6165
6165
  }
6166
6166
  return null;
6167
6167
  }
6168
- var FIELD_FROM_METADATA;
6168
+ function hasUserContextMetadata(schema) {
6169
+ return schema !== null && typeof schema === "object" && USER_CONTEXT_METADATA in schema && schema[USER_CONTEXT_METADATA] === true;
6170
+ }
6171
+ function getUserContextFields(schema) {
6172
+ const fields = [];
6173
+ if (schema instanceof exports_external.ZodObject) {
6174
+ const shape = schema.shape;
6175
+ for (const [key, value] of Object.entries(shape)) {
6176
+ if (hasUserContextMetadata(value)) {
6177
+ fields.push(key);
6178
+ }
6179
+ }
6180
+ }
6181
+ return fields;
6182
+ }
6183
+ var FIELD_FROM_METADATA, USER_CONTEXT_METADATA;
6169
6184
  var init_categorical = __esm(() => {
6185
+ init_zod();
6170
6186
  FIELD_FROM_METADATA = Symbol.for("ont:fieldFrom");
6187
+ USER_CONTEXT_METADATA = Symbol.for("ont:userContext");
6171
6188
  });
6172
6189
 
6173
6190
  // src/lockfile/hasher.ts
@@ -6226,13 +6243,16 @@ function extractOntology(config) {
6226
6243
  }
6227
6244
  }
6228
6245
  const fieldReferences = extractFieldReferences(fn.inputs);
6246
+ const userContextFields = getUserContextFields(fn.inputs);
6247
+ const usesUserContext = userContextFields.length > 0;
6229
6248
  functions[name] = {
6230
6249
  description: fn.description,
6231
6250
  access: [...fn.access].sort(),
6232
6251
  entities: [...fn.entities].sort(),
6233
6252
  inputsSchema,
6234
6253
  outputsSchema,
6235
- fieldReferences: fieldReferences.length > 0 ? fieldReferences : undefined
6254
+ fieldReferences: fieldReferences.length > 0 ? fieldReferences : undefined,
6255
+ usesUserContext: usesUserContext || undefined
6236
6256
  };
6237
6257
  }
6238
6258
  return {
@@ -6330,7 +6350,8 @@ function diffOntology(oldOntology, newOntology) {
6330
6350
  const outputsChanged = JSON.stringify(oldFn.outputsSchema) !== JSON.stringify(newFn.outputsSchema);
6331
6351
  const entitiesChanged = JSON.stringify(oldFn.entities) !== JSON.stringify(newFn.entities);
6332
6352
  const fieldReferencesChanged = JSON.stringify(oldFn.fieldReferences) !== JSON.stringify(newFn.fieldReferences);
6333
- if (accessChanged || descriptionChanged || inputsChanged || outputsChanged || entitiesChanged || fieldReferencesChanged) {
6353
+ const userContextChanged = !!oldFn.usesUserContext !== !!newFn.usesUserContext;
6354
+ if (accessChanged || descriptionChanged || inputsChanged || outputsChanged || entitiesChanged || fieldReferencesChanged || userContextChanged) {
6334
6355
  functions.push({
6335
6356
  name,
6336
6357
  type: "modified",
@@ -6343,7 +6364,9 @@ function diffOntology(oldOntology, newOntology) {
6343
6364
  entitiesChanged: entitiesChanged || undefined,
6344
6365
  oldEntities: entitiesChanged ? oldFn.entities : undefined,
6345
6366
  newEntities: entitiesChanged ? newFn.entities : undefined,
6346
- fieldReferencesChanged: fieldReferencesChanged || undefined
6367
+ fieldReferencesChanged: fieldReferencesChanged || undefined,
6368
+ userContextChanged: userContextChanged || undefined,
6369
+ usesUserContext: userContextChanged ? newFn.usesUserContext : undefined
6347
6370
  });
6348
6371
  }
6349
6372
  }
@@ -6421,6 +6444,9 @@ function formatDiffForConsole(diff) {
6421
6444
  if (fn.fieldReferencesChanged) {
6422
6445
  lines.push(` Field references: changed`);
6423
6446
  }
6447
+ if (fn.userContextChanged) {
6448
+ lines.push(` User context: ${fn.usesUserContext ? "added" : "removed"}`);
6449
+ }
6424
6450
  }
6425
6451
  }
6426
6452
  }
@@ -8405,7 +8431,7 @@ var initCommand = defineCommand({
8405
8431
  if (!existsSync(resolversDir)) {
8406
8432
  mkdirSync(resolversDir, { recursive: true });
8407
8433
  }
8408
- const configTemplate = `import { defineOntology } from 'ont-run';
8434
+ const configTemplate = `import { defineOntology, userContext } from 'ont-run';
8409
8435
  import { z } from 'zod';
8410
8436
 
8411
8437
  export default defineOntology({
@@ -8417,13 +8443,22 @@ export default defineOntology({
8417
8443
  },
8418
8444
 
8419
8445
  // Pluggable auth - customize this for your use case
8446
+ // Return { groups, user } for row-level access control
8420
8447
  auth: async (req) => {
8421
8448
  const token = req.headers.get('Authorization');
8422
- // Return access groups based on auth
8449
+ // Return access groups and optional user data
8423
8450
  // This is where you'd verify JWTs, API keys, etc.
8424
- if (!token) return ['public'];
8425
- if (token === 'admin-secret') return ['admin', 'support', 'public'];
8426
- return ['support', 'public'];
8451
+ if (!token) return { groups: ['public'] };
8452
+ if (token === 'admin-secret') {
8453
+ return {
8454
+ groups: ['admin', 'support', 'public'],
8455
+ user: { id: 'admin-1', email: 'admin@example.com' },
8456
+ };
8457
+ }
8458
+ return {
8459
+ groups: ['support', 'public'],
8460
+ user: { id: 'user-1', email: 'user@example.com' },
8461
+ };
8427
8462
  },
8428
8463
 
8429
8464
  accessGroups: {
@@ -8432,21 +8467,32 @@ export default defineOntology({
8432
8467
  admin: { description: 'Administrators' },
8433
8468
  },
8434
8469
 
8470
+ entities: {
8471
+ User: { description: 'A user account' },
8472
+ },
8473
+
8435
8474
  functions: {
8436
8475
  // Example: Public function
8437
8476
  healthCheck: {
8438
8477
  description: 'Check API health status',
8439
8478
  access: ['public', 'support', 'admin'],
8479
+ entities: [],
8440
8480
  inputs: z.object({}),
8441
8481
  resolver: './resolvers/healthCheck.ts',
8442
8482
  },
8443
8483
 
8444
- // Example: Restricted function
8484
+ // Example: Restricted function with row-level access
8445
8485
  getUser: {
8446
8486
  description: 'Get user details by ID',
8447
8487
  access: ['support', 'admin'],
8488
+ entities: ['User'],
8448
8489
  inputs: z.object({
8449
8490
  userId: z.string().uuid(),
8491
+ // currentUser is injected from auth - not visible to API callers
8492
+ currentUser: userContext(z.object({
8493
+ id: z.string(),
8494
+ email: z.string(),
8495
+ })),
8450
8496
  }),
8451
8497
  resolver: './resolvers/getUser.ts',
8452
8498
  },
@@ -8455,6 +8501,7 @@ export default defineOntology({
8455
8501
  deleteUser: {
8456
8502
  description: 'Delete a user account',
8457
8503
  access: ['admin'],
8504
+ entities: ['User'],
8458
8505
  inputs: z.object({
8459
8506
  userId: z.string().uuid(),
8460
8507
  reason: z.string().optional(),
@@ -8482,10 +8529,21 @@ export default async function healthCheck(ctx: ResolverContext, args: {}) {
8482
8529
 
8483
8530
  interface GetUserArgs {
8484
8531
  userId: string;
8532
+ currentUser: {
8533
+ id: string;
8534
+ email: string;
8535
+ };
8485
8536
  }
8486
8537
 
8487
8538
  export default async function getUser(ctx: ResolverContext, args: GetUserArgs) {
8488
8539
  ctx.logger.info(\`Getting user: \${args.userId}\`);
8540
+ ctx.logger.info(\`Requested by: \${args.currentUser.email}\`);
8541
+
8542
+ // Example: Check if user can access this resource
8543
+ // Support can only view their own account
8544
+ if (!ctx.accessGroups.includes('admin') && args.userId !== args.currentUser.id) {
8545
+ throw new Error('You can only view your own account');
8546
+ }
8489
8547
 
8490
8548
  // This is where you'd query your database
8491
8549
  // Example response:
@@ -10786,6 +10844,8 @@ function transformToGraphData(config) {
10786
10844
  for (const entity of fn.entities) {
10787
10845
  entityCounts[entity] = (entityCounts[entity] || 0) + 1;
10788
10846
  }
10847
+ const userContextFields = getUserContextFields(fn.inputs);
10848
+ const usesUserContext = userContextFields.length > 0;
10789
10849
  nodes.push({
10790
10850
  id: `function:${name}`,
10791
10851
  type: "function",
@@ -10794,7 +10854,8 @@ function transformToGraphData(config) {
10794
10854
  metadata: {
10795
10855
  inputs: safeZodToJsonSchema(fn.inputs),
10796
10856
  outputs: fn.outputs ? safeZodToJsonSchema(fn.outputs) : undefined,
10797
- resolver: fn.resolver
10857
+ resolver: fn.resolver,
10858
+ usesUserContext: usesUserContext || undefined
10798
10859
  }
10799
10860
  });
10800
10861
  for (const group of fn.access) {
@@ -11386,6 +11447,27 @@ function generateBrowserUI(graphData) {
11386
11447
  color: var(--change-modified);
11387
11448
  }
11388
11449
 
11450
+ /* User Context Badge */
11451
+ .user-context-badge {
11452
+ display: inline-flex;
11453
+ align-items: center;
11454
+ gap: 4px;
11455
+ padding: 2px 8px;
11456
+ border-radius: 9999px;
11457
+ font-size: 10px;
11458
+ font-weight: 500;
11459
+ background: rgba(21, 168, 168, 0.12);
11460
+ color: var(--vanna-teal);
11461
+ text-transform: uppercase;
11462
+ letter-spacing: 0.03em;
11463
+ margin-left: 8px;
11464
+ }
11465
+
11466
+ .user-context-badge svg {
11467
+ width: 12px;
11468
+ height: 12px;
11469
+ }
11470
+
11389
11471
  /* Review Footer */
11390
11472
  .review-footer {
11391
11473
  position: fixed;
@@ -12854,9 +12936,14 @@ function generateBrowserUI(graphData) {
12854
12936
  ? \`<span class="detail-change-badge \${changeStatus}">\${changeStatus === 'added' ? 'New' : changeStatus === 'removed' ? 'Removed' : 'Modified'}</span>\`
12855
12937
  : '';
12856
12938
 
12939
+ // Build user context badge if applicable
12940
+ const userContextBadge = data.metadata?.usesUserContext
12941
+ ? \`<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>\`
12942
+ : '';
12943
+
12857
12944
  let html = \`
12858
12945
  <div class="detail-header">
12859
- <div class="detail-type \${data.type}">\${formatType(data.type)}\${changeBadge}</div>
12946
+ <div class="detail-type \${data.type}">\${formatType(data.type)}\${changeBadge}\${userContextBadge}</div>
12860
12947
  <div class="detail-name">\${data.label}</div>
12861
12948
  <div class="detail-description">\${data.description || 'No description'}</div>
12862
12949
  </div>
package/dist/index.js CHANGED
@@ -4007,10 +4007,31 @@ function getFieldFromMetadata(schema) {
4007
4007
  }
4008
4008
  return null;
4009
4009
  }
4010
- var FIELD_FROM_METADATA;
4010
+ function userContext(schema) {
4011
+ const marked = schema;
4012
+ marked[USER_CONTEXT_METADATA] = true;
4013
+ return marked;
4014
+ }
4015
+ function hasUserContextMetadata(schema) {
4016
+ return schema !== null && typeof schema === "object" && USER_CONTEXT_METADATA in schema && schema[USER_CONTEXT_METADATA] === true;
4017
+ }
4018
+ function getUserContextFields(schema) {
4019
+ const fields = [];
4020
+ if (schema instanceof exports_external.ZodObject) {
4021
+ const shape = schema.shape;
4022
+ for (const [key, value] of Object.entries(shape)) {
4023
+ if (hasUserContextMetadata(value)) {
4024
+ fields.push(key);
4025
+ }
4026
+ }
4027
+ }
4028
+ return fields;
4029
+ }
4030
+ var FIELD_FROM_METADATA, USER_CONTEXT_METADATA;
4011
4031
  var init_categorical = __esm(() => {
4012
4032
  init_zod();
4013
4033
  FIELD_FROM_METADATA = Symbol.for("ont:fieldFrom");
4034
+ USER_CONTEXT_METADATA = Symbol.for("ont:userContext");
4014
4035
  });
4015
4036
 
4016
4037
  // node_modules/consola/dist/chunks/prompt.mjs
@@ -6229,13 +6250,16 @@ function extractOntology(config) {
6229
6250
  }
6230
6251
  }
6231
6252
  const fieldReferences = extractFieldReferences(fn.inputs);
6253
+ const userContextFields = getUserContextFields(fn.inputs);
6254
+ const usesUserContext = userContextFields.length > 0;
6232
6255
  functions[name] = {
6233
6256
  description: fn.description,
6234
6257
  access: [...fn.access].sort(),
6235
6258
  entities: [...fn.entities].sort(),
6236
6259
  inputsSchema,
6237
6260
  outputsSchema,
6238
- fieldReferences: fieldReferences.length > 0 ? fieldReferences : undefined
6261
+ fieldReferences: fieldReferences.length > 0 ? fieldReferences : undefined,
6262
+ usesUserContext: usesUserContext || undefined
6239
6263
  };
6240
6264
  }
6241
6265
  return {
@@ -6333,7 +6357,8 @@ function diffOntology(oldOntology, newOntology) {
6333
6357
  const outputsChanged = JSON.stringify(oldFn.outputsSchema) !== JSON.stringify(newFn.outputsSchema);
6334
6358
  const entitiesChanged = JSON.stringify(oldFn.entities) !== JSON.stringify(newFn.entities);
6335
6359
  const fieldReferencesChanged = JSON.stringify(oldFn.fieldReferences) !== JSON.stringify(newFn.fieldReferences);
6336
- if (accessChanged || descriptionChanged || inputsChanged || outputsChanged || entitiesChanged || fieldReferencesChanged) {
6360
+ const userContextChanged = !!oldFn.usesUserContext !== !!newFn.usesUserContext;
6361
+ if (accessChanged || descriptionChanged || inputsChanged || outputsChanged || entitiesChanged || fieldReferencesChanged || userContextChanged) {
6337
6362
  functions.push({
6338
6363
  name,
6339
6364
  type: "modified",
@@ -6346,7 +6371,9 @@ function diffOntology(oldOntology, newOntology) {
6346
6371
  entitiesChanged: entitiesChanged || undefined,
6347
6372
  oldEntities: entitiesChanged ? oldFn.entities : undefined,
6348
6373
  newEntities: entitiesChanged ? newFn.entities : undefined,
6349
- fieldReferencesChanged: fieldReferencesChanged || undefined
6374
+ fieldReferencesChanged: fieldReferencesChanged || undefined,
6375
+ userContextChanged: userContextChanged || undefined,
6376
+ usesUserContext: userContextChanged ? newFn.usesUserContext : undefined
6350
6377
  });
6351
6378
  }
6352
6379
  }
@@ -6424,6 +6451,9 @@ function formatDiffForConsole(diff) {
6424
6451
  if (fn.fieldReferencesChanged) {
6425
6452
  lines.push(` Field references: changed`);
6426
6453
  }
6454
+ if (fn.userContextChanged) {
6455
+ lines.push(` User context: ${fn.usesUserContext ? "added" : "removed"}`);
6456
+ }
6427
6457
  }
6428
6458
  }
6429
6459
  }
@@ -13531,6 +13561,41 @@ function validateFieldFromReferences(config) {
13531
13561
  }
13532
13562
  }
13533
13563
  }
13564
+ async function validateUserContextRequirements(config) {
13565
+ const functionsWithUserContext = [];
13566
+ for (const [fnName, fn] of Object.entries(config.functions)) {
13567
+ const userContextFields = getUserContextFields(fn.inputs);
13568
+ if (userContextFields.length > 0) {
13569
+ functionsWithUserContext.push(fnName);
13570
+ }
13571
+ }
13572
+ if (functionsWithUserContext.length === 0) {
13573
+ return;
13574
+ }
13575
+ const mockRequest = new Request("http://localhost/test", {
13576
+ headers: { Authorization: "test-token" }
13577
+ });
13578
+ try {
13579
+ const authResult = await config.auth(mockRequest);
13580
+ const hasUserField = authResult !== null && typeof authResult === "object" && !Array.isArray(authResult) && "user" in authResult;
13581
+ if (!hasUserField) {
13582
+ throw new Error(`The following functions use userContext() but auth() does not return a user object:
13583
+ ` + ` ${functionsWithUserContext.join(", ")}
13584
+
13585
+ ` + `To fix this, update your auth function to return an AuthResult:
13586
+ ` + ` auth: async (req) => {
13587
+ ` + ` return {
13588
+ ` + ` groups: ['user'],
13589
+ ` + ` user: { id: '...', email: '...' } // Add user data here
13590
+ ` + ` };
13591
+ ` + ` }`);
13592
+ }
13593
+ } catch (error) {
13594
+ if (error instanceof Error && error.message.includes("userContext")) {
13595
+ throw error;
13596
+ }
13597
+ }
13598
+ }
13534
13599
 
13535
13600
  // src/config/define.ts
13536
13601
  function defineOntology(config) {
@@ -16277,11 +16342,19 @@ function createLogger(debug = false) {
16277
16342
  }
16278
16343
 
16279
16344
  // src/server/api/middleware.ts
16345
+ function normalizeAuthResult(result) {
16346
+ if (Array.isArray(result)) {
16347
+ return { groups: result };
16348
+ }
16349
+ return result;
16350
+ }
16280
16351
  function createAuthMiddleware(config) {
16281
16352
  return createMiddleware(async (c3, next) => {
16282
16353
  try {
16283
- const accessGroups = await config.auth(c3.req.raw);
16284
- c3.set("accessGroups", accessGroups);
16354
+ const rawResult = await config.auth(c3.req.raw);
16355
+ const authResult = normalizeAuthResult(rawResult);
16356
+ c3.set("authResult", authResult);
16357
+ c3.set("accessGroups", authResult.groups);
16285
16358
  await next();
16286
16359
  } catch (error) {
16287
16360
  return c3.json({
@@ -16336,15 +16409,24 @@ function errorHandler2() {
16336
16409
  }
16337
16410
 
16338
16411
  // src/server/api/router.ts
16412
+ init_categorical();
16339
16413
  function createApiRoutes(config, configDir) {
16340
16414
  const router = new Hono2;
16341
16415
  for (const [name, fn] of Object.entries(config.functions)) {
16342
16416
  const path = `/${name}`;
16417
+ const userContextFields = getUserContextFields(fn.inputs);
16343
16418
  router.post(path, createAccessControlMiddleware(fn.access), async (c3) => {
16344
16419
  const resolverContext = c3.get("resolverContext");
16420
+ const authResult = c3.get("authResult");
16345
16421
  let args;
16346
16422
  try {
16347
- const body = await c3.req.json();
16423
+ let body = await c3.req.json();
16424
+ if (userContextFields.length > 0 && authResult.user) {
16425
+ body = { ...body };
16426
+ for (const field of userContextFields) {
16427
+ body[field] = authResult.user;
16428
+ }
16429
+ }
16348
16430
  const parsed = fn.inputs.safeParse(body);
16349
16431
  if (!parsed.success) {
16350
16432
  return c3.json({
@@ -16354,7 +16436,13 @@ function createApiRoutes(config, configDir) {
16354
16436
  }
16355
16437
  args = parsed.data;
16356
16438
  } catch {
16357
- const parsed = fn.inputs.safeParse({});
16439
+ let emptyBody = {};
16440
+ if (userContextFields.length > 0 && authResult.user) {
16441
+ for (const field of userContextFields) {
16442
+ emptyBody[field] = authResult.user;
16443
+ }
16444
+ }
16445
+ const parsed = fn.inputs.safeParse(emptyBody);
16358
16446
  if (!parsed.success) {
16359
16447
  return c3.json({
16360
16448
  error: "Validation failed",
@@ -22822,6 +22910,31 @@ data:
22822
22910
  init_zod();
22823
22911
  init_esm();
22824
22912
  init_categorical();
22913
+ function stripUserContextFromJsonSchema(jsonSchema, zodSchema) {
22914
+ if (jsonSchema.type !== "object" || !jsonSchema.properties) {
22915
+ return jsonSchema;
22916
+ }
22917
+ const userContextFields = getUserContextFields(zodSchema);
22918
+ if (userContextFields.length === 0) {
22919
+ return jsonSchema;
22920
+ }
22921
+ const properties = { ...jsonSchema.properties };
22922
+ const required2 = jsonSchema.required ? [...jsonSchema.required] : undefined;
22923
+ for (const field of userContextFields) {
22924
+ delete properties[field];
22925
+ if (required2) {
22926
+ const idx = required2.indexOf(field);
22927
+ if (idx !== -1) {
22928
+ required2.splice(idx, 1);
22929
+ }
22930
+ }
22931
+ }
22932
+ return {
22933
+ ...jsonSchema,
22934
+ properties,
22935
+ required: required2 && required2.length > 0 ? required2 : undefined
22936
+ };
22937
+ }
22825
22938
  function extractFieldReferencesForMcp(schema, path = "") {
22826
22939
  const results = [];
22827
22940
  const metadata = getFieldFromMetadata(schema);
@@ -22861,6 +22974,7 @@ function generateMcpTools(config2) {
22861
22974
  $refStrategy: "none"
22862
22975
  });
22863
22976
  delete inputSchema.$schema;
22977
+ inputSchema = stripUserContextFromJsonSchema(inputSchema, fn.inputs);
22864
22978
  } catch {
22865
22979
  inputSchema = { type: "object", properties: {} };
22866
22980
  }
@@ -22892,16 +23006,28 @@ function filterToolsByAccess(tools, accessGroups) {
22892
23006
  return tools.filter((tool) => tool.access.some((group) => accessGroups.includes(group)));
22893
23007
  }
22894
23008
  function createToolExecutor(config2, configDir, env2, envConfig, logger) {
22895
- return async (toolName, args, accessGroups) => {
23009
+ const userContextFieldsCache = new Map;
23010
+ for (const [name, fn] of Object.entries(config2.functions)) {
23011
+ userContextFieldsCache.set(name, getUserContextFields(fn.inputs));
23012
+ }
23013
+ return async (toolName, args, authResult) => {
22896
23014
  const fn = config2.functions[toolName];
22897
23015
  if (!fn) {
22898
23016
  throw new Error(`Unknown tool: ${toolName}`);
22899
23017
  }
22900
- const hasAccess = fn.access.some((group) => accessGroups.includes(group));
23018
+ const hasAccess = fn.access.some((group) => authResult.groups.includes(group));
22901
23019
  if (!hasAccess) {
22902
23020
  throw new Error(`Access denied to tool "${toolName}". Requires: ${fn.access.join(", ")}`);
22903
23021
  }
22904
- const parsed = fn.inputs.safeParse(args);
23022
+ const userContextFields = userContextFieldsCache.get(toolName) || [];
23023
+ let argsWithContext = args;
23024
+ if (userContextFields.length > 0 && authResult.user) {
23025
+ argsWithContext = { ...args };
23026
+ for (const field of userContextFields) {
23027
+ argsWithContext[field] = authResult.user;
23028
+ }
23029
+ }
23030
+ const parsed = fn.inputs.safeParse(argsWithContext);
22905
23031
  if (!parsed.success) {
22906
23032
  throw new Error(`Invalid input for tool "${toolName}": ${parsed.error.message}`);
22907
23033
  }
@@ -22909,7 +23035,7 @@ function createToolExecutor(config2, configDir, env2, envConfig, logger) {
22909
23035
  env: env2,
22910
23036
  envConfig,
22911
23037
  logger,
22912
- accessGroups
23038
+ accessGroups: authResult.groups
22913
23039
  };
22914
23040
  const resolver = await loadResolver(fn.resolver, configDir);
22915
23041
  return resolver(resolverContext, parsed.data);
@@ -22949,10 +23075,20 @@ async function serve2(app, port) {
22949
23075
  }
22950
23076
 
22951
23077
  // src/server/mcp/index.ts
22952
- function getAccessGroups(authInfo) {
22953
- if (!authInfo?.extra?.accessGroups)
22954
- return [];
22955
- return authInfo.extra.accessGroups;
23078
+ function normalizeAuthResult2(result) {
23079
+ if (Array.isArray(result)) {
23080
+ return { groups: result };
23081
+ }
23082
+ return result;
23083
+ }
23084
+ function getAuthResult(authInfo) {
23085
+ if (!authInfo?.extra?.authResult) {
23086
+ if (authInfo?.extra?.accessGroups) {
23087
+ return { groups: authInfo.extra.accessGroups };
23088
+ }
23089
+ return { groups: [] };
23090
+ }
23091
+ return authInfo.extra.authResult;
22956
23092
  }
22957
23093
  function createMcpServer(options) {
22958
23094
  const { config: config2, configDir, env: env2 } = options;
@@ -22972,8 +23108,8 @@ function createMcpServer(options) {
22972
23108
  }
22973
23109
  });
22974
23110
  server.setRequestHandler(ListToolsRequestSchema, async (_request, extra) => {
22975
- const accessGroups = getAccessGroups(extra.authInfo);
22976
- const accessibleTools = filterToolsByAccess(allTools, accessGroups);
23111
+ const authResult = getAuthResult(extra.authInfo);
23112
+ const accessibleTools = filterToolsByAccess(allTools, authResult.groups);
22977
23113
  return {
22978
23114
  tools: accessibleTools.map((tool) => ({
22979
23115
  name: tool.name,
@@ -22984,9 +23120,9 @@ function createMcpServer(options) {
22984
23120
  });
22985
23121
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
22986
23122
  const { name, arguments: args } = request.params;
22987
- const accessGroups = getAccessGroups(extra.authInfo);
23123
+ const authResult = getAuthResult(extra.authInfo);
22988
23124
  try {
22989
- const result = await executeToolWithAccess(name, args || {}, accessGroups);
23125
+ const result = await executeToolWithAccess(name, args || {}, authResult);
22990
23126
  return {
22991
23127
  content: [
22992
23128
  {
@@ -23027,13 +23163,15 @@ async function startMcpServer(options) {
23027
23163
  });
23028
23164
  app.all("/mcp", async (c3) => {
23029
23165
  try {
23030
- const accessGroups = await config2.auth(c3.req.raw);
23166
+ const rawResult = await config2.auth(c3.req.raw);
23167
+ const authResult = normalizeAuthResult2(rawResult);
23031
23168
  const authInfo = {
23032
23169
  token: c3.req.header("Authorization") || "",
23033
23170
  clientId: "ontology-client",
23034
23171
  scopes: [],
23035
23172
  extra: {
23036
- accessGroups
23173
+ authResult,
23174
+ accessGroups: authResult.groups
23037
23175
  }
23038
23176
  };
23039
23177
  return transport.handleRequest(c3.req.raw, { authInfo });
@@ -23071,6 +23209,12 @@ async function startOnt(options = {}) {
23071
23209
  const isDev = mode === "development";
23072
23210
  consola.info("Loading ontology config...");
23073
23211
  const { config: config2, configDir } = await loadConfig();
23212
+ try {
23213
+ await validateUserContextRequirements(config2);
23214
+ } catch (error2) {
23215
+ consola.error("User context validation failed:");
23216
+ throw error2;
23217
+ }
23074
23218
  consola.info("Checking lockfile...");
23075
23219
  const { ontology, hash } = computeOntologyHash(config2);
23076
23220
  if (!lockfileExists(configDir)) {
@@ -23146,6 +23290,7 @@ Run \`bun run review\` to approve the changes.`;
23146
23290
  init_zod();
23147
23291
  export {
23148
23292
  exports_external as z,
23293
+ userContext,
23149
23294
  startOnt,
23150
23295
  fieldFrom,
23151
23296
  defineOntology
@@ -13,6 +13,7 @@ export interface GraphNode {
13
13
  outputs?: Record<string, unknown>;
14
14
  resolver?: string;
15
15
  functionCount?: number;
16
+ usesUserContext?: boolean;
16
17
  };
17
18
  }
18
19
  export interface GraphEdge {
@@ -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";