ont-run 0.0.2 → 0.0.4

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
@@ -150,11 +150,16 @@ export default defineOntology({
150
150
  prod: { debug: false },
151
151
  },
152
152
 
153
+ // Auth returns access groups (and optional user identity)
153
154
  auth: async (req: Request) => {
154
155
  const token = req.headers.get('Authorization');
155
- if (!token) return ['public'];
156
- if (token === 'admin-secret') return ['admin', 'user', 'public'];
157
- return ['user', 'public'];
156
+ if (!token) return { groups: ['public'] };
157
+
158
+ const user = await verifyToken(token);
159
+ return {
160
+ groups: user.isAdmin ? ['admin', 'user', 'public'] : ['user', 'public'],
161
+ user: { id: user.id, email: user.email }, // Optional: for row-level access
162
+ };
158
163
  },
159
164
 
160
165
  accessGroups: {
@@ -180,6 +185,68 @@ export default defineOntology({
180
185
  });
181
186
  ```
182
187
 
188
+ ## Row-Level Access Control
189
+
190
+ The framework handles **group-based access** (user → group → function) out of the box. For **row-level ownership** (e.g., "users can only edit their own posts"), use `userContext()`:
191
+
192
+ ```typescript
193
+ import { defineOntology, userContext, z } from 'ont-run';
194
+
195
+ export default defineOntology({
196
+ // Auth must return user identity for userContext to work
197
+ auth: async (req) => {
198
+ const user = await verifyToken(req);
199
+ return {
200
+ groups: ['user'],
201
+ user: { id: user.id, email: user.email },
202
+ };
203
+ },
204
+
205
+ functions: {
206
+ editPost: {
207
+ description: 'Edit a post',
208
+ access: ['user', 'admin'],
209
+ entities: ['Post'],
210
+ inputs: z.object({
211
+ postId: z.string(),
212
+ title: z.string(),
213
+ // currentUser is injected at runtime, hidden from API callers
214
+ currentUser: userContext(z.object({
215
+ id: z.string(),
216
+ email: z.string(),
217
+ })),
218
+ }),
219
+ resolver: './resolvers/editPost.ts',
220
+ },
221
+ },
222
+ });
223
+ ```
224
+
225
+ In the resolver, you receive the typed user object:
226
+
227
+ ```typescript
228
+ // resolvers/editPost.ts
229
+ export default async function editPost(
230
+ ctx: ResolverContext,
231
+ args: { postId: string; title: string; currentUser: { id: string; email: string } }
232
+ ) {
233
+ const post = await db.posts.findById(args.postId);
234
+
235
+ // Row-level check: only author or admin can edit
236
+ if (args.currentUser.id !== post.authorId && !ctx.accessGroups.includes('admin')) {
237
+ throw new Error('Not authorized to edit this post');
238
+ }
239
+
240
+ return db.posts.update(args.postId, { title: args.title });
241
+ }
242
+ ```
243
+
244
+ **Key points:**
245
+ - `userContext()` fields are **injected** from `auth()` result's `user` field
246
+ - They're **hidden** from public API/MCP schemas (callers don't see or provide them)
247
+ - They're **type-safe** in resolvers
248
+ - The review UI shows a badge for functions using user context
249
+
183
250
  ## The Lockfile
184
251
 
185
252
  `ont.lock` is the enforcement mechanism. It contains a hash of your ontology:
package/dist/bin/ont.js CHANGED
@@ -6170,8 +6170,9 @@ function hasUserContextMetadata(schema) {
6170
6170
  }
6171
6171
  function getUserContextFields(schema) {
6172
6172
  const fields = [];
6173
- if (schema instanceof exports_external.ZodObject) {
6174
- const shape = schema.shape;
6173
+ const def = schema._def;
6174
+ if (def?.typeName === "ZodObject" && typeof def.shape === "function") {
6175
+ const shape = def.shape();
6175
6176
  for (const [key, value] of Object.entries(shape)) {
6176
6177
  if (hasUserContextMetadata(value)) {
6177
6178
  fields.push(key);
@@ -6182,7 +6183,6 @@ function getUserContextFields(schema) {
6182
6183
  }
6183
6184
  var FIELD_FROM_METADATA, USER_CONTEXT_METADATA;
6184
6185
  var init_categorical = __esm(() => {
6185
- init_zod();
6186
6186
  FIELD_FROM_METADATA = Symbol.for("ont:fieldFrom");
6187
6187
  USER_CONTEXT_METADATA = Symbol.for("ont:userContext");
6188
6188
  });
@@ -8656,10 +8656,21 @@ async function loadConfig(configPath) {
8656
8656
  }
8657
8657
  return { config, configDir, configPath: resolvedPath };
8658
8658
  } catch (error) {
8659
- if (error.code === "ERR_MODULE_NOT_FOUND") {
8660
- throw new Error(`Failed to load config: ${resolvedPath}`);
8659
+ const err = error;
8660
+ if (err.code === "ERR_MODULE_NOT_FOUND") {
8661
+ const message = err.message || "";
8662
+ if (message.includes("ont-run") || message.includes("zod")) {
8663
+ throw new Error(`Failed to load config: ${resolvedPath}
8664
+
8665
+ Missing dependencies. Run 'bun install' first.`);
8666
+ }
8667
+ throw new Error(`Failed to load config: ${resolvedPath}
8668
+
8669
+ Module not found: ${message}`);
8661
8670
  }
8662
- throw error;
8671
+ throw new Error(`Failed to load config: ${resolvedPath}
8672
+
8673
+ ${err.message || error}`);
8663
8674
  }
8664
8675
  }
8665
8676
 
@@ -10829,6 +10840,7 @@ function transformToGraphData(config) {
10829
10840
  const edges = [];
10830
10841
  const accessGroupCounts = {};
10831
10842
  const entityCounts = {};
10843
+ let userContextFunctionCount = 0;
10832
10844
  for (const groupName of Object.keys(config.accessGroups)) {
10833
10845
  accessGroupCounts[groupName] = 0;
10834
10846
  }
@@ -10846,6 +10858,9 @@ function transformToGraphData(config) {
10846
10858
  }
10847
10859
  const userContextFields = getUserContextFields(fn.inputs);
10848
10860
  const usesUserContext = userContextFields.length > 0;
10861
+ if (usesUserContext) {
10862
+ userContextFunctionCount++;
10863
+ }
10849
10864
  nodes.push({
10850
10865
  id: `function:${name}`,
10851
10866
  type: "function",
@@ -10916,7 +10931,8 @@ function transformToGraphData(config) {
10916
10931
  ontologyName: config.name,
10917
10932
  totalFunctions: Object.keys(config.functions).length,
10918
10933
  totalEntities: config.entities ? Object.keys(config.entities).length : 0,
10919
- totalAccessGroups: Object.keys(config.accessGroups).length
10934
+ totalAccessGroups: Object.keys(config.accessGroups).length,
10935
+ totalUserContextFunctions: userContextFunctionCount
10920
10936
  }
10921
10937
  };
10922
10938
  }
@@ -11162,6 +11178,9 @@ Ontology ${hasChanges ? "Review" : "Browser"} available at: ${url}`);
11162
11178
  });
11163
11179
  }
11164
11180
  function generateBrowserUI(graphData) {
11181
+ const userContextFilterBtn = graphData.meta.totalUserContextFunctions > 0 ? `<button class="filter-btn" data-filter="userContext" title="Functions using userContext()">
11182
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;vertical-align:middle;margin-right:4px;"><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 (${graphData.meta.totalUserContextFunctions})
11183
+ </button>` : "";
11165
11184
  return `<!DOCTYPE html>
11166
11185
  <html lang="en">
11167
11186
  <head>
@@ -12480,6 +12499,7 @@ function generateBrowserUI(graphData) {
12480
12499
  <button class="filter-btn" data-filter="accessGroup">
12481
12500
  <span class="dot access"></span> Access
12482
12501
  </button>
12502
+ ${userContextFilterBtn}
12483
12503
  </div>
12484
12504
 
12485
12505
  <div class="layout-selector">
@@ -12624,6 +12644,7 @@ function generateBrowserUI(graphData) {
12624
12644
  metadata: node.metadata,
12625
12645
  changeStatus: node.changeStatus || 'unchanged',
12626
12646
  changeDetails: node.changeDetails || null,
12647
+ usesUserContext: node.metadata?.usesUserContext || false,
12627
12648
  },
12628
12649
  });
12629
12650
  }
@@ -12676,6 +12697,19 @@ function generateBrowserUI(graphData) {
12676
12697
  'height': 55,
12677
12698
  },
12678
12699
  },
12700
+ // Function nodes with userContext - show indicator below label
12701
+ {
12702
+ selector: 'node[type="function"][?usesUserContext]',
12703
+ style: {
12704
+ 'background-image': 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#e8f4f8" stroke="#023d60" stroke-width="1.5"/><g transform="translate(4, 4)" fill="none" stroke="#023d60" 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"/></g></svg>'),
12705
+ 'background-width': '18px',
12706
+ 'background-height': '18px',
12707
+ 'background-position-x': '50%',
12708
+ 'background-position-y': '75%',
12709
+ 'text-valign': 'center',
12710
+ 'text-margin-y': -8,
12711
+ },
12712
+ },
12679
12713
  // Entity nodes - Teal
12680
12714
  {
12681
12715
  selector: 'node[type="entity"]',
@@ -13217,6 +13251,16 @@ function generateBrowserUI(graphData) {
13217
13251
  if (filter === 'all') {
13218
13252
  cy.nodes().removeClass('hidden');
13219
13253
  cy.edges().removeClass('hidden');
13254
+ } else if (filter === 'userContext') {
13255
+ // Special filter: show only functions with userContext
13256
+ cy.nodes().forEach(node => {
13257
+ if (node.data('type') === 'function' && node.data('usesUserContext')) {
13258
+ node.removeClass('hidden');
13259
+ } else {
13260
+ node.addClass('hidden');
13261
+ }
13262
+ });
13263
+ cy.edges().addClass('hidden');
13220
13264
  } else {
13221
13265
  cy.nodes().forEach(node => {
13222
13266
  if (node.data('type') === filter) {
@@ -13733,13 +13777,77 @@ var reviewCommand = defineCommand({
13733
13777
  }
13734
13778
  }
13735
13779
  });
13780
+ // package.json
13781
+ var package_default = {
13782
+ name: "ont-run",
13783
+ version: "0.0.4",
13784
+ description: "Ontology-enforced API framework for AI coding agents",
13785
+ type: "module",
13786
+ bin: {
13787
+ "ont-run": "./dist/bin/ont.js"
13788
+ },
13789
+ exports: {
13790
+ ".": {
13791
+ types: "./dist/src/index.d.ts",
13792
+ bun: "./src/index.ts",
13793
+ default: "./dist/index.js"
13794
+ }
13795
+ },
13796
+ files: [
13797
+ "dist",
13798
+ "src",
13799
+ "bin"
13800
+ ],
13801
+ scripts: {
13802
+ dev: "bun run bin/ont.ts",
13803
+ build: "bun build ./src/index.ts --outdir ./dist --target node",
13804
+ "build:cli": "bun build ./bin/ont.ts --outfile ./dist/bin/ont.js --target node --external @modelcontextprotocol/sdk",
13805
+ "build:types": "tsc --declaration --emitDeclarationOnly",
13806
+ prepublishOnly: "bun run build && bun run build:cli && bun run build:types",
13807
+ docs: "cd docs && bun run dev",
13808
+ "docs:build": "cd docs && bun run build",
13809
+ "docs:preview": "cd docs && bun run preview"
13810
+ },
13811
+ dependencies: {
13812
+ "@hono/node-server": "^1.19.8",
13813
+ "@modelcontextprotocol/sdk": "^1.0.0",
13814
+ citty: "^0.1.6",
13815
+ consola: "^3.2.0",
13816
+ hono: "^4.6.0",
13817
+ open: "^10.0.0",
13818
+ zod: "^3.24.0",
13819
+ "zod-to-json-schema": "^3.23.0"
13820
+ },
13821
+ devDependencies: {
13822
+ "@types/bun": "latest",
13823
+ typescript: "^5.5.0"
13824
+ },
13825
+ peerDependencies: {
13826
+ bun: ">=1.0.0"
13827
+ },
13828
+ peerDependenciesMeta: {
13829
+ bun: {
13830
+ optional: true
13831
+ }
13832
+ },
13833
+ keywords: [
13834
+ "api",
13835
+ "framework",
13836
+ "access-control",
13837
+ "ai-agents",
13838
+ "hono",
13839
+ "zod",
13840
+ "typescript"
13841
+ ],
13842
+ license: "MIT"
13843
+ };
13736
13844
 
13737
13845
  // src/cli/index.ts
13738
13846
  var main = defineCommand({
13739
13847
  meta: {
13740
13848
  name: "ont",
13741
13849
  description: "Ontology - Ontology-first backends with human-approved AI access & edits",
13742
- version: "0.1.0"
13850
+ version: package_default.version
13743
13851
  },
13744
13852
  subCommands: {
13745
13853
  init: initCommand,
package/dist/index.js CHANGED
@@ -4017,8 +4017,9 @@ function hasUserContextMetadata(schema) {
4017
4017
  }
4018
4018
  function getUserContextFields(schema) {
4019
4019
  const fields = [];
4020
- if (schema instanceof exports_external.ZodObject) {
4021
- const shape = schema.shape;
4020
+ const def = schema._def;
4021
+ if (def?.typeName === "ZodObject" && typeof def.shape === "function") {
4022
+ const shape = def.shape();
4022
4023
  for (const [key, value] of Object.entries(shape)) {
4023
4024
  if (hasUserContextMetadata(value)) {
4024
4025
  fields.push(key);
@@ -14623,10 +14624,21 @@ async function loadConfig(configPath) {
14623
14624
  }
14624
14625
  return { config, configDir, configPath: resolvedPath };
14625
14626
  } catch (error) {
14626
- if (error.code === "ERR_MODULE_NOT_FOUND") {
14627
- throw new Error(`Failed to load config: ${resolvedPath}`);
14627
+ const err = error;
14628
+ if (err.code === "ERR_MODULE_NOT_FOUND") {
14629
+ const message = err.message || "";
14630
+ if (message.includes("ont-run") || message.includes("zod")) {
14631
+ throw new Error(`Failed to load config: ${resolvedPath}
14632
+
14633
+ Missing dependencies. Run 'bun install' first.`);
14634
+ }
14635
+ throw new Error(`Failed to load config: ${resolvedPath}
14636
+
14637
+ Module not found: ${message}`);
14628
14638
  }
14629
- throw error;
14639
+ throw new Error(`Failed to load config: ${resolvedPath}
14640
+
14641
+ ${err.message || error}`);
14630
14642
  }
14631
14643
  }
14632
14644
 
@@ -31,6 +31,7 @@ export interface GraphData {
31
31
  totalFunctions: number;
32
32
  totalEntities: number;
33
33
  totalAccessGroups: number;
34
+ totalUserContextFunctions: number;
34
35
  };
35
36
  }
36
37
  export interface EnhancedGraphNode extends GraphNode {
@@ -131,5 +131,8 @@ export declare function userContext<T extends z.ZodType>(schema: T): UserContext
131
131
  export declare function hasUserContextMetadata(schema: unknown): schema is UserContextSchema<z.ZodType>;
132
132
  /**
133
133
  * Get all userContext field names from a Zod object schema
134
+ *
135
+ * Note: Uses _def.typeName check instead of instanceof to work across
136
+ * module boundaries in bundled CLI.
134
137
  */
135
138
  export declare function getUserContextFields(schema: z.ZodType): string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ont-run",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Ontology-enforced API framework for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -122,6 +122,12 @@ export async function startBrowserServer(options: BrowserServerOptions): Promise
122
122
  }
123
123
 
124
124
  function generateBrowserUI(graphData: EnhancedGraphData): string {
125
+ const userContextFilterBtn = graphData.meta.totalUserContextFunctions > 0
126
+ ? `<button class="filter-btn" data-filter="userContext" title="Functions using userContext()">
127
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;vertical-align:middle;margin-right:4px;"><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 (${graphData.meta.totalUserContextFunctions})
128
+ </button>`
129
+ : '';
130
+
125
131
  return `<!DOCTYPE html>
126
132
  <html lang="en">
127
133
  <head>
@@ -1440,6 +1446,7 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
1440
1446
  <button class="filter-btn" data-filter="accessGroup">
1441
1447
  <span class="dot access"></span> Access
1442
1448
  </button>
1449
+ ${userContextFilterBtn}
1443
1450
  </div>
1444
1451
 
1445
1452
  <div class="layout-selector">
@@ -1584,6 +1591,7 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
1584
1591
  metadata: node.metadata,
1585
1592
  changeStatus: node.changeStatus || 'unchanged',
1586
1593
  changeDetails: node.changeDetails || null,
1594
+ usesUserContext: node.metadata?.usesUserContext || false,
1587
1595
  },
1588
1596
  });
1589
1597
  }
@@ -1636,6 +1644,19 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
1636
1644
  'height': 55,
1637
1645
  },
1638
1646
  },
1647
+ // Function nodes with userContext - show indicator below label
1648
+ {
1649
+ selector: 'node[type="function"][?usesUserContext]',
1650
+ style: {
1651
+ 'background-image': 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#e8f4f8" stroke="#023d60" stroke-width="1.5"/><g transform="translate(4, 4)" fill="none" stroke="#023d60" 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"/></g></svg>'),
1652
+ 'background-width': '18px',
1653
+ 'background-height': '18px',
1654
+ 'background-position-x': '50%',
1655
+ 'background-position-y': '75%',
1656
+ 'text-valign': 'center',
1657
+ 'text-margin-y': -8,
1658
+ },
1659
+ },
1639
1660
  // Entity nodes - Teal
1640
1661
  {
1641
1662
  selector: 'node[type="entity"]',
@@ -2177,6 +2198,16 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
2177
2198
  if (filter === 'all') {
2178
2199
  cy.nodes().removeClass('hidden');
2179
2200
  cy.edges().removeClass('hidden');
2201
+ } else if (filter === 'userContext') {
2202
+ // Special filter: show only functions with userContext
2203
+ cy.nodes().forEach(node => {
2204
+ if (node.data('type') === 'function' && node.data('usesUserContext')) {
2205
+ node.removeClass('hidden');
2206
+ } else {
2207
+ node.addClass('hidden');
2208
+ }
2209
+ });
2210
+ cy.edges().addClass('hidden');
2180
2211
  } else {
2181
2212
  cy.nodes().forEach(node => {
2182
2213
  if (node.data('type') === filter) {
@@ -38,6 +38,7 @@ export interface GraphData {
38
38
  totalFunctions: number;
39
39
  totalEntities: number;
40
40
  totalAccessGroups: number;
41
+ totalUserContextFunctions: number;
41
42
  };
42
43
  }
43
44
 
@@ -132,6 +133,7 @@ export function transformToGraphData(config: OntologyConfig): GraphData {
132
133
  // Track function counts for access groups and entities
133
134
  const accessGroupCounts: Record<string, number> = {};
134
135
  const entityCounts: Record<string, number> = {};
136
+ let userContextFunctionCount = 0;
135
137
 
136
138
  // Initialize counts
137
139
  for (const groupName of Object.keys(config.accessGroups)) {
@@ -158,6 +160,9 @@ export function transformToGraphData(config: OntologyConfig): GraphData {
158
160
  // Check if function uses userContext
159
161
  const userContextFields = getUserContextFields(fn.inputs);
160
162
  const usesUserContext = userContextFields.length > 0;
163
+ if (usesUserContext) {
164
+ userContextFunctionCount++;
165
+ }
161
166
 
162
167
  // Create function node
163
168
  nodes.push({
@@ -242,6 +247,7 @@ export function transformToGraphData(config: OntologyConfig): GraphData {
242
247
  totalFunctions: Object.keys(config.functions).length,
243
248
  totalEntities: config.entities ? Object.keys(config.entities).length : 0,
244
249
  totalAccessGroups: Object.keys(config.accessGroups).length,
250
+ totalUserContextFunctions: userContextFunctionCount,
245
251
  },
246
252
  };
247
253
  }
package/src/cli/index.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import { defineCommand, runMain } from "citty";
2
2
  import { initCommand } from "./commands/init.js";
3
3
  import { reviewCommand } from "./commands/review.js";
4
+ import pkg from "../../package.json";
4
5
 
5
6
  const main = defineCommand({
6
7
  meta: {
7
8
  name: "ont",
8
9
  description: "Ontology - Ontology-first backends with human-approved AI access & edits",
9
- version: "0.1.0",
10
+ version: pkg.version,
10
11
  },
11
12
  subCommands: {
12
13
  init: initCommand,
@@ -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
  }
@@ -174,12 +174,17 @@ export function hasUserContextMetadata(
174
174
 
175
175
  /**
176
176
  * Get all userContext field names from a Zod object schema
177
+ *
178
+ * Note: Uses _def.typeName check instead of instanceof to work across
179
+ * module boundaries in bundled CLI.
177
180
  */
178
181
  export function getUserContextFields(schema: z.ZodType): string[] {
179
182
  const fields: string[] = [];
180
183
 
181
- if (schema instanceof z.ZodObject) {
182
- const shape = schema.shape;
184
+ // Use _def.typeName check for bundler compatibility (instanceof fails across module boundaries)
185
+ const def = (schema as unknown as { _def?: { typeName?: string; shape?: () => Record<string, unknown> } })._def;
186
+ if (def?.typeName === "ZodObject" && typeof def.shape === "function") {
187
+ const shape = def.shape();
183
188
  for (const [key, value] of Object.entries(shape)) {
184
189
  if (hasUserContextMetadata(value)) {
185
190
  fields.push(key);