ont-run 0.0.3 → 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
  });
@@ -10840,6 +10840,7 @@ function transformToGraphData(config) {
10840
10840
  const edges = [];
10841
10841
  const accessGroupCounts = {};
10842
10842
  const entityCounts = {};
10843
+ let userContextFunctionCount = 0;
10843
10844
  for (const groupName of Object.keys(config.accessGroups)) {
10844
10845
  accessGroupCounts[groupName] = 0;
10845
10846
  }
@@ -10857,6 +10858,9 @@ function transformToGraphData(config) {
10857
10858
  }
10858
10859
  const userContextFields = getUserContextFields(fn.inputs);
10859
10860
  const usesUserContext = userContextFields.length > 0;
10861
+ if (usesUserContext) {
10862
+ userContextFunctionCount++;
10863
+ }
10860
10864
  nodes.push({
10861
10865
  id: `function:${name}`,
10862
10866
  type: "function",
@@ -10927,7 +10931,8 @@ function transformToGraphData(config) {
10927
10931
  ontologyName: config.name,
10928
10932
  totalFunctions: Object.keys(config.functions).length,
10929
10933
  totalEntities: config.entities ? Object.keys(config.entities).length : 0,
10930
- totalAccessGroups: Object.keys(config.accessGroups).length
10934
+ totalAccessGroups: Object.keys(config.accessGroups).length,
10935
+ totalUserContextFunctions: userContextFunctionCount
10931
10936
  }
10932
10937
  };
10933
10938
  }
@@ -11173,6 +11178,9 @@ Ontology ${hasChanges ? "Review" : "Browser"} available at: ${url}`);
11173
11178
  });
11174
11179
  }
11175
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>` : "";
11176
11184
  return `<!DOCTYPE html>
11177
11185
  <html lang="en">
11178
11186
  <head>
@@ -12491,6 +12499,7 @@ function generateBrowserUI(graphData) {
12491
12499
  <button class="filter-btn" data-filter="accessGroup">
12492
12500
  <span class="dot access"></span> Access
12493
12501
  </button>
12502
+ ${userContextFilterBtn}
12494
12503
  </div>
12495
12504
 
12496
12505
  <div class="layout-selector">
@@ -12635,6 +12644,7 @@ function generateBrowserUI(graphData) {
12635
12644
  metadata: node.metadata,
12636
12645
  changeStatus: node.changeStatus || 'unchanged',
12637
12646
  changeDetails: node.changeDetails || null,
12647
+ usesUserContext: node.metadata?.usesUserContext || false,
12638
12648
  },
12639
12649
  });
12640
12650
  }
@@ -12687,6 +12697,19 @@ function generateBrowserUI(graphData) {
12687
12697
  'height': 55,
12688
12698
  },
12689
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
+ },
12690
12713
  // Entity nodes - Teal
12691
12714
  {
12692
12715
  selector: 'node[type="entity"]',
@@ -13228,6 +13251,16 @@ function generateBrowserUI(graphData) {
13228
13251
  if (filter === 'all') {
13229
13252
  cy.nodes().removeClass('hidden');
13230
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');
13231
13264
  } else {
13232
13265
  cy.nodes().forEach(node => {
13233
13266
  if (node.data('type') === filter) {
@@ -13744,13 +13777,77 @@ var reviewCommand = defineCommand({
13744
13777
  }
13745
13778
  }
13746
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
+ };
13747
13844
 
13748
13845
  // src/cli/index.ts
13749
13846
  var main = defineCommand({
13750
13847
  meta: {
13751
13848
  name: "ont",
13752
13849
  description: "Ontology - Ontology-first backends with human-approved AI access & edits",
13753
- version: "0.1.0"
13850
+ version: package_default.version
13754
13851
  },
13755
13852
  subCommands: {
13756
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);
@@ -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.3",
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,
@@ -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);