UNPKG

@schoolai/spicedb-zed-schema-parser

Version:

SpiceDB .zed file format parser and analyzer written in Typescript

1,619 lines (1,599 loc) 65.2 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/index.ts var index_exports = {}; __export(index_exports, { Operations: () => Operations, PermissionOperations: () => PermissionOperations, Permissions: () => Permissions, SemanticAnalyzer: () => SemanticAnalyzer, SpiceDBLexer: () => SpiceDBLexer, SpiceDBParser: () => SpiceDBParser, SpiceDBVisitor: () => SpiceDBVisitor, analyzeSpiceDbSchema: () => analyzeSpiceDbSchema, createPermissions: () => createPermissions, generateSDK: () => generateSDK, parseSpiceDBSchema: () => parseSpiceDBSchema, parserInstance: () => parserInstance }); module.exports = __toCommonJS(index_exports); // src/builder/check.ts var import_authzed_node = require("@authzed/authzed-node"); // src/builder/types.ts function parseReference(ref) { const [part1, part2] = ref.split(":"); if (!part1 || !part2) { throw new Error( `Invalid reference format: ${ref}. Expected format: type:id` ); } return [part1, part2]; } // src/builder/check.ts var CheckOperation = class { constructor(permission) { this.permission = permission; __publicField(this, "subjectRef"); __publicField(this, "resourceRef"); __publicField(this, "consistency"); } subject(ref) { this.subjectRef = ref; return this; } resource(ref) { this.resourceRef = ref; return this; } withConsistency(token) { this.consistency = import_authzed_node.v1.Consistency.create({ requirement: { oneofKind: "atLeastAsFresh", atLeastAsFresh: import_authzed_node.v1.ZedToken.create({ token }) } }); return this; } async execute(client) { if (!this.subjectRef || !this.resourceRef) { throw new Error("Check operation requires both subject and resource"); } const [subjectType, subjectId] = parseReference(this.subjectRef); const [resourceType, resourceId] = parseReference(this.resourceRef); const request = import_authzed_node.v1.CheckPermissionRequest.create({ resource: import_authzed_node.v1.ObjectReference.create({ objectType: resourceType, objectId: resourceId }), permission: this.permission, subject: import_authzed_node.v1.SubjectReference.create({ object: import_authzed_node.v1.ObjectReference.create({ objectType: subjectType, objectId: subjectId }) }), consistency: this.consistency }); const response = await client.checkPermission(request); return response.permissionship === import_authzed_node.v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION; } toJSON() { return { permission: this.permission, subject: this.subjectRef, resource: this.resourceRef, consistency: this.consistency }; } }; var BoundCheckOperation = class extends CheckOperation { constructor(client, permission) { super(permission); this.client = client; } async execute() { return super.execute(this.client); } }; // src/builder/delete.ts var import_authzed_node2 = require("@authzed/authzed-node"); var DeleteOperation = class { constructor() { __publicField(this, "filter", {}); } subject(ref) { const [type, id] = parseReference(ref); this.filter.subjectType = type; this.filter.subjectId = id; return this; } relation(rel) { this.filter.relation = rel; return this; } resource(ref) { const [type, id] = parseReference(ref); this.filter.resourceType = type; this.filter.resourceId = id; return this; } where(filter) { this.filter = { ...this.filter, ...filter }; return this; } async execute(client) { const relationshipFilter = {}; if (this.filter.resourceType) { relationshipFilter.resourceType = this.filter.resourceType; } if (this.filter.resourceId) { relationshipFilter.optionalResourceId = this.filter.resourceId; } if (this.filter.relation) { relationshipFilter.optionalRelation = this.filter.relation; } if (this.filter.subjectType || this.filter.subjectId) { const subjectFilter = {}; if (this.filter.subjectType) { subjectFilter.subjectType = this.filter.subjectType; } if (this.filter.subjectId) { subjectFilter.optionalSubjectId = this.filter.subjectId; } relationshipFilter.optionalSubjectFilter = import_authzed_node2.v1.SubjectFilter.create(subjectFilter); } const request = import_authzed_node2.v1.DeleteRelationshipsRequest.create({ relationshipFilter: import_authzed_node2.v1.RelationshipFilter.create(relationshipFilter) }); const response = await client.deleteRelationships(request); return response.deletedAt?.token || null; } toJSON() { return { filter: this.filter }; } }; var BoundDeleteOperation = class extends DeleteOperation { constructor(client) { super(); this.client = client; } async execute() { return super.execute(this.client); } }; // src/builder/lookup.ts var import_authzed_node3 = require("@authzed/authzed-node"); var LookupOperation = class { constructor() { __publicField(this, "lookupType"); __publicField(this, "resourceFilter"); __publicField(this, "subjectFilter"); __publicField(this, "permission"); __publicField(this, "consistency"); } resourcesAccessibleBy(subjectRef) { this.lookupType = "resources"; const [type, id] = parseReference(subjectRef); this.subjectFilter = { type, id }; return this; } subjectsWithAccessTo(resourceRef) { this.lookupType = "subjects"; const [type, id] = parseReference(resourceRef); this.resourceFilter = { type, id }; return this; } ofType(type) { if (this.lookupType === "resources") { this.resourceFilter = { type }; } else if (this.lookupType === "subjects") { this.subjectFilter = { type }; } return this; } withPermission(permission) { this.permission = permission; return this; } withConsistency(token) { this.consistency = import_authzed_node3.v1.Consistency.create({ requirement: { oneofKind: "atLeastAsFresh", atLeastAsFresh: import_authzed_node3.v1.ZedToken.create({ token }) } }); return this; } async execute(client) { if (!this.permission) { throw new Error("Lookup operation requires permission"); } if (this.lookupType === "resources" && this.subjectFilter) { const request = import_authzed_node3.v1.LookupResourcesRequest.create({ resourceObjectType: this.resourceFilter?.type || "document", permission: this.permission, subject: import_authzed_node3.v1.SubjectReference.create({ object: import_authzed_node3.v1.ObjectReference.create({ objectType: this.subjectFilter.type, objectId: this.subjectFilter.id }) }), consistency: this.consistency }); const stream = await client.lookupResources(request); const results = []; for (const result of stream) { if (result.resourceObjectId) { results.push({ type: this.resourceFilter?.type || "document", id: result.resourceObjectId, permissionship: result.permissionship }); } } return results; } if (this.lookupType === "subjects" && this.resourceFilter?.id) { const request = import_authzed_node3.v1.LookupSubjectsRequest.create({ resource: import_authzed_node3.v1.ObjectReference.create({ objectType: this.resourceFilter.type, objectId: this.resourceFilter.id }), permission: this.permission, subjectObjectType: this.subjectFilter?.type || "user", consistency: this.consistency }); const stream = await client.lookupSubjects(request); const results = []; for (const result of stream) { if (result.subject?.subjectObjectId) { results.push({ type: this.subjectFilter?.type || "user", id: result.subject.subjectObjectId, permissionship: result.subject.permissionship }); } } return results; } throw new Error("Invalid lookup configuration"); } /** * Special helper for looking up subjects with multiple permission levels */ async withPermissions(permissions, client) { if (!this.resourceFilter?.id) { throw new Error("Multiple permission lookup requires a specific resource"); } if (!client) { throw new Error( "withPermissions requires a client. Use execute(client) or pass client as second parameter." ); } const resultMap = /* @__PURE__ */ new Map(); for (const permission of permissions) { const request = import_authzed_node3.v1.LookupSubjectsRequest.create({ resource: import_authzed_node3.v1.ObjectReference.create({ objectType: this.resourceFilter.type, objectId: this.resourceFilter.id }), permission, subjectObjectType: this.subjectFilter?.type || "user", consistency: this.consistency }); const stream = await client.lookupSubjects(request); for (const result of stream) { if (result.subject?.subjectObjectId) { if (!resultMap.has(result.subject.subjectObjectId)) { resultMap.set(result.subject.subjectObjectId, permission); } } } } return resultMap; } toJSON() { return { lookupType: this.lookupType, resourceFilter: this.resourceFilter, subjectFilter: this.subjectFilter, permission: this.permission, consistency: this.consistency }; } }; var BoundLookupOperation = class extends LookupOperation { constructor(client) { super(); this.client = client; } async execute() { return super.execute(this.client); } async withPermissions(permissions) { return super.withPermissions(permissions, this.client); } }; // src/builder/query.ts var import_authzed_node4 = require("@authzed/authzed-node"); var QueryOperation = class { constructor() { __publicField(this, "filter", {}); __publicField(this, "queryType"); __publicField(this, "permission"); __publicField(this, "consistency"); } subjects(type) { this.queryType = "subjects"; if (type) this.filter.subjectType = type; return this; } resources(type) { this.queryType = "resources"; if (type) this.filter.resourceType = type; return this; } subject(ref) { if (ref.includes("*")) { const [type] = parseReference(ref); this.filter.subjectType = type; } else { const [type, id] = parseReference(ref); this.filter.subjectType = type; this.filter.subjectId = id; } return this; } relation(rel) { if (rel !== "*") { this.filter.relation = rel; } return this; } resource(ref) { if (ref.includes("*")) { const [type] = parseReference(ref); this.filter.resourceType = type; } else { const [type, id] = parseReference(ref); this.filter.resourceType = type; this.filter.resourceId = id; } return this; } withPermission(permission) { this.permission = permission; return this; } withConsistency(token) { this.consistency = import_authzed_node4.v1.Consistency.create({ requirement: { oneofKind: "atLeastAsFresh", atLeastAsFresh: import_authzed_node4.v1.ZedToken.create({ token }) } }); return this; } async execute(client) { if (this.queryType === "subjects" && this.filter.resourceType && this.filter.resourceId && this.permission) { const request2 = import_authzed_node4.v1.LookupSubjectsRequest.create({ resource: import_authzed_node4.v1.ObjectReference.create({ objectType: this.filter.resourceType, objectId: this.filter.resourceId }), permission: this.permission, subjectObjectType: this.filter.subjectType || "user", consistency: this.consistency }); const stream2 = await client.lookupSubjects(request2); const results2 = []; for (const result of stream2) { if (result.subject?.subjectObjectId) { results2.push({ type: this.filter.subjectType || "user", id: result.subject.subjectObjectId, relation: result.subject.permissionship === import_authzed_node4.v1.LookupPermissionship.HAS_PERMISSION ? this.permission : void 0 }); } } return results2; } if (this.queryType === "resources" && this.filter.subjectType && this.filter.subjectId && this.permission) { const request2 = import_authzed_node4.v1.LookupResourcesRequest.create({ resourceObjectType: this.filter.resourceType || "document", permission: this.permission, subject: import_authzed_node4.v1.SubjectReference.create({ object: import_authzed_node4.v1.ObjectReference.create({ objectType: this.filter.subjectType, objectId: this.filter.subjectId }) }), consistency: this.consistency }); const stream2 = await client.lookupResources(request2); const results2 = []; for (const result of stream2) { if (result.resourceObjectId) { results2.push({ type: this.filter.resourceType || "document", id: result.resourceObjectId, permissionship: result.permissionship }); } } return results2; } const filter = {}; if (this.filter.resourceType) { filter.resourceType = this.filter.resourceType; } if (this.filter.resourceId) { filter.optionalResourceId = this.filter.resourceId; } if (this.filter.relation) { filter.optionalRelation = this.filter.relation; } if (this.filter.subjectType || this.filter.subjectId) { const subjectFilter = {}; if (this.filter.subjectType) { subjectFilter.subjectType = this.filter.subjectType; } if (this.filter.subjectId) { subjectFilter.optionalSubjectId = this.filter.subjectId; } filter.optionalSubjectFilter = import_authzed_node4.v1.SubjectFilter.create(subjectFilter); } const request = import_authzed_node4.v1.ReadRelationshipsRequest.create({ relationshipFilter: import_authzed_node4.v1.RelationshipFilter.create(filter), consistency: this.consistency }); const stream = await client.readRelationships(request); const results = []; for (const result of stream) { if (result.relationship) { results.push({ type: result.relationship.resource?.objectType || "", id: result.relationship.resource?.objectId || "", relation: result.relationship.relation, subjectType: result.relationship.subject?.object?.objectType || "", subjectId: result.relationship.subject?.object?.objectId || "" }); } } return results; } toJSON() { return { queryType: this.queryType, filter: this.filter, permission: this.permission, consistency: this.consistency }; } }; var BoundQueryOperation = class extends QueryOperation { constructor(client) { super(); this.client = client; } async execute() { return super.execute(this.client); } }; // src/builder/transaction.ts var import_authzed_node5 = require("@authzed/authzed-node"); var Transaction = class { constructor() { __publicField(this, "operations", []); } grant(relation) { return new TransactionWriteOperation(this, "grant", relation); } revoke(relation) { return new TransactionWriteOperation(this, "revoke", relation); } add(operation) { this.operations.push(operation); return this; } async execute(client) { const updates = this.operations.map((op) => op()); const request = import_authzed_node5.v1.WriteRelationshipsRequest.create({ updates }); const response = await client.writeRelationships(request); return { token: response.writtenAt?.token || null, succeeded: true, operationCount: updates.length }; } toJSON() { return { operationCount: this.operations.length }; } }; var BoundTransaction = class extends Transaction { constructor(client) { super(); this.client = client; } grant(relation) { return new TransactionWriteOperation(this, "grant", relation); } revoke(relation) { return new TransactionWriteOperation(this, "revoke", relation); } async execute() { return super.execute(this.client); } async commit() { return this.execute(); } }; var TransactionWriteOperation = class { constructor(transaction, operation, relation) { this.transaction = transaction; this.operation = operation; this.relation = relation; __publicField(this, "subjects", []); __publicField(this, "resources", []); } subject(ref) { this.subjects = Array.isArray(ref) ? ref : [ref]; return this; } resource(ref) { this.resources = Array.isArray(ref) ? ref : [ref]; return this; } and() { for (const subjectRef of this.subjects) { for (const resourceRef of this.resources) { this.transaction.add(() => { const [subjectType, subjectId] = parseReference(subjectRef); const [resourceType, resourceId] = parseReference(resourceRef); const relationship = import_authzed_node5.v1.Relationship.create({ resource: import_authzed_node5.v1.ObjectReference.create({ objectType: resourceType, objectId: resourceId }), relation: this.relation, subject: import_authzed_node5.v1.SubjectReference.create({ object: import_authzed_node5.v1.ObjectReference.create({ objectType: subjectType, objectId: subjectId }) }) }); return import_authzed_node5.v1.RelationshipUpdate.create({ relationship, operation: this.operation === "grant" ? import_authzed_node5.v1.RelationshipUpdate_Operation.TOUCH : import_authzed_node5.v1.RelationshipUpdate_Operation.DELETE }); }); } } return this.transaction; } }; // src/builder/write.ts var import_authzed_node6 = require("@authzed/authzed-node"); var WriteOperation = class { constructor(operation, relation) { this.operation = operation; this.relation = relation; __publicField(this, "subjects", []); __publicField(this, "resources", []); __publicField(this, "consistency"); } subject(ref) { this.subjects = Array.isArray(ref) ? ref : [ref]; return this; } resource(ref) { this.resources = Array.isArray(ref) ? ref : [ref]; return this; } withConsistency(token) { this.consistency = import_authzed_node6.v1.Consistency.create({ requirement: { oneofKind: "atLeastAsFresh", atLeastAsFresh: import_authzed_node6.v1.ZedToken.create({ token }) } }); return this; } async execute(client) { const updates = []; for (const subjectRef of this.subjects) { for (const resourceRef of this.resources) { const [subjectType, subjectId] = parseReference(subjectRef); const [resourceType, resourceId] = parseReference(resourceRef); const relationship = import_authzed_node6.v1.Relationship.create({ resource: import_authzed_node6.v1.ObjectReference.create({ objectType: resourceType, objectId: resourceId }), relation: this.relation, subject: import_authzed_node6.v1.SubjectReference.create({ object: import_authzed_node6.v1.ObjectReference.create({ objectType: subjectType, objectId: subjectId }) }) }); updates.push( import_authzed_node6.v1.RelationshipUpdate.create({ relationship, operation: this.operation === "grant" ? import_authzed_node6.v1.RelationshipUpdate_Operation.TOUCH : import_authzed_node6.v1.RelationshipUpdate_Operation.DELETE }) ); } } const request = import_authzed_node6.v1.WriteRelationshipsRequest.create({ updates }); const response = await client.writeRelationships(request); return response.writtenAt?.token || null; } /** * Convert to a plain object for serialization */ toJSON() { return { operation: this.operation, relation: this.relation, subjects: this.subjects, resources: this.resources, consistency: this.consistency }; } }; var BoundWriteOperation = class extends WriteOperation { constructor(client, operation, relation) { super(operation, relation); this.client = client; } async execute() { return super.execute(this.client); } }; // src/builder/operations.ts var PermissionOperations = class { /** * Create a grant operation */ static grant(relation) { return new WriteOperation("grant", relation); } /** * Create a revoke operation */ static revoke(relation) { return new WriteOperation("revoke", relation); } /** * Create a check operation */ static check(permission) { return new CheckOperation(permission); } /** * Create a find/query operation */ static find() { return new QueryOperation(); } /** * Create a delete operation */ static delete() { return new DeleteOperation(); } /** * Create a batch transaction */ static batch() { return new Transaction(); } /** * Create a lookup operation */ static lookup() { return new LookupOperation(); } }; var Permissions = class { constructor(client) { this.client = client; } /** * Grant a relation between subjects and resources */ grant(relation) { return new BoundWriteOperation(this.client, "grant", relation); } /** * Revoke a relation between subjects and resources */ revoke(relation) { return new BoundWriteOperation(this.client, "revoke", relation); } /** * Check if a subject has a permission on a resource */ check(permission) { return new BoundCheckOperation(this.client, permission); } /** * Find subjects or resources matching criteria */ find() { return new BoundQueryOperation(this.client); } /** * Delete relationships matching a filter */ delete() { return new BoundDeleteOperation(this.client); } /** * Create a batch transaction */ batch() { return new BoundTransaction(this.client); } /** * Lookup resources accessible to a subject */ lookup() { return new BoundLookupOperation(this.client); } /** * Execute a pure operation with this instance's client */ async execute(operation) { return operation.execute(this.client); } }; function createPermissions(client) { return new Permissions(client); } var Operations = PermissionOperations; // src/generate-sdk.ts function generateSDK(schema, parserImport = "@schoolai/spicedb-zed-schema-parser") { const objectDefs = schema.definitions.filter( (def) => def.type === "definition" ); let code = `// Generated by @schoolai/spicedb-zed-schema-parser // Do not edit manually. import { PermissionOperations } from '${parserImport}'; // --------------- GENERIC TYPES --------------- export type Subject<T extends string> = \`\${T}:\${string}\`; export type Resource<T extends string> = \`\${T}:\${string}\`; // --------------- RESOURCE TYPES --------------- `; for (const def of objectDefs) { code += `export type ${toPascalCase(def.name)}Resource = Resource<'${def.name}'>; `; } code += "\nexport const permissions = {\n"; for (const def of objectDefs) { if (def.relations.length === 0 && def.permissions.length === 0) { continue; } code += ` ${toCamelCase(def.name)}: { `; code += generateGrantRevoke(def); code += generateCheck(def); code += generateFind(def); code += ` }, `; } code += "};\n"; return code; } function generateGrantRevoke(def) { const resourceType = `${toPascalCase(def.name)}Resource`; let code = " grant: {\n"; for (const rel of def.relations) { const subjectTypeLiterals = [ ...new Set(rel.types.map((t) => `'${t.typeName}'`)) ].join(" | "); const subjectTypes = `Subject<${subjectTypeLiterals || "never"}>`; code += ` ${toCamelCase(rel.name)}: (subject: ${subjectTypes}, resource: ${resourceType}) => PermissionOperations.grant('${rel.name}').subject(subject).resource(resource), `; } code += " },\n"; code += " revoke: {\n"; for (const rel of def.relations) { const subjectTypeLiterals = [ ...new Set(rel.types.map((t) => `'${t.typeName}'`)) ].join(" | "); const subjectTypes = `Subject<${subjectTypeLiterals || "never"}>`; code += ` ${toCamelCase(rel.name)}: (subject: ${subjectTypes}, resource: ${resourceType}) => PermissionOperations.revoke('${rel.name}').subject(subject).resource(resource), `; } code += " },\n"; return code; } function generateFind(def) { let code = " find: {\n"; for (const rel of def.relations) { const pascalRel = toPascalCase(rel.name); const subjectTypeLiterals = [ ...new Set(rel.types.map((t) => `'${t.typeName}'`)) ].join(" | "); const subjectTypes = `Subject<${subjectTypeLiterals || "never"}>`; code += ` by${pascalRel}: (subject: ${subjectTypes}) => PermissionOperations.find().relation('${rel.name}').subject(subject), `; } code += " },\n"; return code; } function generateCheck(def) { const resourceType = `${toPascalCase(def.name)}Resource`; let code = " check: {\n"; for (const perm of def.permissions) { const subjectTypeLiterals = [ ...new Set(perm.inferredSubjectTypes?.map((t) => `'${t.typeName}'`)) ].join(" | "); const subjectTypes = `Subject<${subjectTypeLiterals || "never"}>`; code += ` ${toCamelCase(perm.name)}: (subject: ${subjectTypes}, resource: ${resourceType}) => PermissionOperations.check('${perm.name}').subject(subject).resource(resource), `; } code += " },\n"; return code; } function toPascalCase(name) { return name.split(/[_\-\s]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(""); } function toCamelCase(name) { if (name.length === 0) return name; return toPascalCase(name).charAt(0).toLowerCase() + toPascalCase(name).slice(1); } // src/semantic-analyzer/dependency-graph.ts var DependencyGraph = class { constructor() { __publicField(this, "adjacencyList", /* @__PURE__ */ new Map()); __publicField(this, "nodes", /* @__PURE__ */ new Map()); } addNode(node) { this.nodes.set(node.fullName, node); if (!this.adjacencyList.has(node.fullName)) { this.adjacencyList.set(node.fullName, /* @__PURE__ */ new Set()); } } addEdge(from, to) { if (!this.adjacencyList.has(from)) { this.adjacencyList.set(from, /* @__PURE__ */ new Set()); } this.adjacencyList.get(from).add(to); } // Find cycles via depth-first search findCycles() { const cycles = []; const visited = /* @__PURE__ */ new Set(); const recursionStack = /* @__PURE__ */ new Set(); const path = []; const dfs = (node) => { visited.add(node); recursionStack.add(node); path.push(node); const neighbors = this.adjacencyList.get(node) || /* @__PURE__ */ new Set(); for (const neighbor of neighbors) { if (!visited.has(neighbor)) { dfs(neighbor); } else if (recursionStack.has(neighbor)) { const cycleStart = path.indexOf(neighbor); cycles.push(path.slice(cycleStart).concat(neighbor)); } } path.pop(); recursionStack.delete(node); }; for (const node of this.adjacencyList.keys()) { if (!visited.has(node)) { dfs(node); } } return cycles; } }; // src/semantic-analyzer/symbol-table.ts var SymbolTable = class { constructor() { __publicField(this, "symbols", /* @__PURE__ */ new Map()); } addDefinition(def) { const info = { type: def.type === "definition" ? "definition" : "caveat", definition: def, relations: /* @__PURE__ */ new Map(), permissions: /* @__PURE__ */ new Map() }; if (def.type === "definition") { for (const rel of def.relations) { info.relations.set(rel.name, rel); } for (const perm of def.permissions) { info.permissions.set(perm.name, perm); } } this.symbols.set(def.name, info); } getDefinition(name) { return this.symbols.get(name); } hasDefinition(name) { return this.symbols.has(name); } getAllDefinitions() { return Array.from(this.symbols.values()); } getRelation(typeName, relationName) { const info = this.symbols.get(typeName); return info?.relations.get(relationName); } getPermission(typeName, permissionName) { const info = this.symbols.get(typeName); return info?.permissions.get(permissionName); } hasRelationOrPermission(typeName, name) { const info = this.symbols.get(typeName); if (!info) return false; return info.relations.has(name) || info.permissions.has(name); } }; // src/semantic-analyzer/type-inference.ts var TypeInferenceEngine = class { constructor(symbolTable) { this.symbolTable = symbolTable; } inferExpressionType(defName, expr, callStack = /* @__PURE__ */ new Set()) { switch (expr.type) { case "identifier": { const rel = this.symbolTable.getRelation(defName, expr.name); if (rel) { const resolvedTypes = []; for (const typeRef of rel.types) { if (typeRef.relation) { const subjectRelationTypes = this.inferSubjectRelationType( typeRef.typeName, typeRef.relation, callStack ); if (subjectRelationTypes) { resolvedTypes.push(...subjectRelationTypes); } } else { resolvedTypes.push(typeRef); } } return this.deduplicateTypes(resolvedTypes); } const perm = this.symbolTable.getPermission(defName, expr.name); if (perm) { const stackKey = `${defName}#${expr.name}`; if (callStack.has(stackKey)) { return null; } callStack.add(stackKey); const result = this.inferExpressionType( defName, perm.expression, callStack ); callStack.delete(stackKey); return result; } return null; } case "union": { const allTypes = []; for (const operand of expr.operands) { const types = this.inferExpressionType(defName, operand, callStack); if (types) { allTypes.push(...types); } } return this.deduplicateTypes(allTypes); } case "intersection": { let commonTypes = null; for (const operand of expr.operands) { const types = this.inferExpressionType(defName, operand, callStack); if (!types) return null; if (!commonTypes) { commonTypes = types; } else { commonTypes = this.intersectTypes(commonTypes, types); } } return commonTypes; } case "exclusion": return this.inferExpressionType(defName, expr.left, callStack); case "arrow": case "any": case "all": { const leftTypes = this.inferExpressionType( defName, expr.left, callStack ); if (!leftTypes) return null; const resultTypes = []; for (const leftType of leftTypes) { const targetRel = this.symbolTable.getRelation( leftType.typeName, expr.target ); if (targetRel) { for (const typeRef of targetRel.types) { if (typeRef.relation) { const subjectRelationTypes = this.inferSubjectRelationType( typeRef.typeName, typeRef.relation, callStack ); if (subjectRelationTypes) { resultTypes.push(...subjectRelationTypes); } } else { resultTypes.push(typeRef); } } } const targetPerm = this.symbolTable.getPermission( leftType.typeName, expr.target ); if (targetPerm) { const stackKey = `${leftType.typeName}#${expr.target}`; if (callStack.has(stackKey)) { continue; } callStack.add(stackKey); const permTypes = this.inferExpressionType( leftType.typeName, targetPerm.expression, callStack ); callStack.delete(stackKey); if (permTypes) { resultTypes.push(...permTypes); } } } return this.deduplicateTypes(resultTypes); } default: return null; } } inferSubjectRelationType(typeName, relationName, callStack) { const targetRel = this.symbolTable.getRelation(typeName, relationName); if (targetRel) { const resolvedTypes = []; for (const typeRef of targetRel.types) { if (typeRef.relation) { const subjectRelationTypes = this.inferSubjectRelationType( typeRef.typeName, typeRef.relation, callStack ); if (subjectRelationTypes) { resolvedTypes.push(...subjectRelationTypes); } } else { resolvedTypes.push(typeRef); } } return this.deduplicateTypes(resolvedTypes); } const targetPerm = this.symbolTable.getPermission(typeName, relationName); if (targetPerm) { const stackKey = `${typeName}#${relationName}`; if (callStack.has(stackKey)) { return null; } callStack.add(stackKey); const result = this.inferExpressionType( typeName, targetPerm.expression, callStack ); callStack.delete(stackKey); return result; } return null; } deduplicateTypes(types) { const seen = /* @__PURE__ */ new Set(); const result = []; for (const type of types) { const key = `${type.typeName}${type.wildcard ? ":*" : ""}${type.relation ? "#" + type.relation : ""}`; if (!seen.has(key)) { seen.add(key); result.push(type); } } return result; } intersectTypes(a, b) { const result = []; const bKeys = new Set( b.map( (type) => `${type.typeName}${type.wildcard ? ":*" : ""}${type.relation ? "#" + type.relation : ""}` ) ); for (const typeA of a) { const keyA = `${typeA.typeName}${typeA.wildcard ? ":*" : ""}${typeA.relation ? "#" + typeA.relation : ""}`; if (bKeys.has(keyA)) { result.push(typeA); } } return this.deduplicateTypes(result); } }; // src/semantic-analyzer/analyzer.ts var criticalPreAugmentationErrorCodes = [ "DUPLICATE_DEFINITION", "UNDEFINED_TYPE", "UNDEFINED_RELATION", "DUPLICATE_MEMBER_NAME" ]; var fatalForUsageErrorCodes = [ ...criticalPreAugmentationErrorCodes, // Errors caught before augmentation "CIRCULAR_DEPENDENCY", // Cycles make the graph unusable "UNDEFINED_IDENTIFIER", // Referenced name doesn't exist "UNDEFINED_ARROW_TARGET", // Arrow target doesn't exist on resolved types "AUGMENTATION_INTERNAL_ERROR" // If augmentation process itself failed ]; var SemanticAnalyzer = class { constructor() { __publicField(this, "symbolTable"); __publicField(this, "typeInference"); __publicField(this, "errors", []); __publicField(this, "warnings", []); this.symbolTable = new SymbolTable(); this.typeInference = new TypeInferenceEngine(this.symbolTable); } analyze(ast) { this.errors = []; this.warnings = []; this.symbolTable = new SymbolTable(); this.typeInference = new TypeInferenceEngine(this.symbolTable); this.buildSymbolTable(ast); this.validateDefinitions(ast); this.checkForCycles(ast); this.validateExpressions(ast); this.performAdditionalChecks(ast); const isValid = this.errors.length === 0; const isFatalForUsage = this.errors.some( (e) => fatalForUsageErrorCodes.includes(e.code) ); const augmentedAst = this.augmentAst(ast); if (isFatalForUsage) { return { augmentedAst: void 0, symbolTable: this.symbolTable, // Always return the symbol table errors: this.errors, warnings: this.warnings, isValid }; } return { augmentedAst, symbolTable: this.symbolTable, // Always return the symbol table errors: this.errors, warnings: this.warnings, isValid }; } augmentAst(ast) { try { const augmentedDefinitions = []; for (const def of ast.definitions) { if (def.type === "definition") { const augmentedRelations = def.relations.map((rel) => ({ ...rel })); const augmentedPermissions = []; for (const perm of def.permissions) { const inferredTypes = this.typeInference.inferExpressionType( def.name, perm.expression ); augmentedPermissions.push({ ...perm, inferredSubjectTypes: inferredTypes }); } const augmentedDef = { type: "definition", // Explicitly set type name: def.name, relations: augmentedRelations, permissions: augmentedPermissions, // Safely access comments, assuming ObjectTypeDefinition might have 'comments' // even if not in its strict imported type, consistent with BaseNode expectation. comments: def.comments }; augmentedDefinitions.push(augmentedDef); } else if (def.type === "caveat") { augmentedDefinitions.push(def); } } return { definitions: augmentedDefinitions }; } catch (e) { this.addError( "AUGMENTATION_INTERNAL_ERROR", `Internal error during AST augmentation: ${e.message}`, {} ); return void 0; } } // Phase 1: Build symbol table buildSymbolTable(ast) { const definedNames = /* @__PURE__ */ new Set(); for (const def of ast.definitions) { if (definedNames.has(def.name)) { this.addError( "DUPLICATE_DEFINITION", `Duplicate definition name: ${def.name}`, { definition: def.name } ); } definedNames.add(def.name); this.symbolTable.addDefinition(def); } } // Phase 2: Validate definitions validateDefinitions(ast) { for (const def of ast.definitions) { if (def.type === "definition") { this.validateObjectTypeDefinition(def); } else if (def.type === "caveat") { this.validateCaveatDefinition(def); } } } validateObjectTypeDefinition(def) { const names = /* @__PURE__ */ new Set(); for (const rel of def.relations) { if (names.has(rel.name)) { this.addError( "DUPLICATE_MEMBER_NAME", `Duplicate relation/permission name '${rel.name}' in ${def.name}`, { definition: def.name, relation: rel.name } ); } names.add(rel.name); this.validateRelation(def.name, rel); } for (const perm of def.permissions) { if (names.has(perm.name)) { this.addError( "DUPLICATE_MEMBER_NAME", `Duplicate relation/permission name '${perm.name}' in ${def.name}`, { definition: def.name, permission: perm.name } ); } names.add(perm.name); } } validateRelation(defName, rel) { for (const type of rel.types) { if (!this.symbolTable.hasDefinition(type.typeName)) { this.addError( "UNDEFINED_TYPE", `Undefined type '${type.typeName}' in relation '${rel.name}'`, { definition: defName, relation: rel.name } ); } if (type.relation) { const targetType = this.symbolTable.getDefinition(type.typeName); if (targetType && targetType.type === "definition") { if (!this.symbolTable.hasRelationOrPermission( type.typeName, type.relation )) { this.addError( "UNDEFINED_RELATION", `Undefined relation '${type.relation}' on type '${type.typeName}'`, { definition: defName, relation: rel.name } ); } } } if (type.wildcard) { this.addWarning( "WILDCARD_USAGE", `Wildcard used in relation '${rel.name}'. Be careful with public access.`, { definition: defName, relation: rel.name } ); } } } validateCaveatDefinition(def) { const validTypes = [ "int", "uint", "string", "bool", "bytes", "list", "map", "timestamp", "duration" ]; for (const param of def.parameters) { if (!validTypes.includes(param.type)) { this.addError( "INVALID_PARAMETER_TYPE", `Invalid parameter type '${param.type}' in caveat '${def.name}'`, { definition: def.name } ); } } const paramNames = new Set(def.parameters.map((p) => p.name)); if (!paramNames.has(def.expression.left)) { this.addError( "UNDEFINED_CAVEAT_PARAMETER", `Unknown parameter '${def.expression.left}' in caveat expression`, { definition: def.name } ); } } // Phase 3: Check for cycles checkForCycles(ast) { const graph = new DependencyGraph(); for (const def of ast.definitions) { if (def.type === "definition") { for (const perm of def.permissions) { const fromNode = `${def.name}#${perm.name}`; graph.addNode({ type: "permission", name: perm.name, fullName: fromNode }); this.addExpressionDependencies( graph, def.name, perm.name, perm.expression ); } } } const cycles = graph.findCycles(); for (const cycle of cycles) { if (cycle.length === 2 && cycle[0] === cycle[1]) { continue; } this.addError( "CIRCULAR_DEPENDENCY", `Circular dependency detected: ${cycle.join(" -> ")}`, {} ); } } addExpressionDependencies(graph, defName, permName, expr) { const fromNode = `${defName}#${permName}`; switch (expr.type) { case "identifier": if (this.symbolTable.hasRelationOrPermission(defName, expr.name)) { const toNode = `${defName}#${expr.name}`; graph.addNode({ type: "relation_or_permission", name: expr.name, fullName: toNode }); graph.addEdge(fromNode, toNode); } break; case "union": case "intersection": for (const operand of expr.operands) { this.addExpressionDependencies(graph, defName, permName, operand); } break; case "exclusion": this.addExpressionDependencies(graph, defName, permName, expr.left); this.addExpressionDependencies(graph, defName, permName, expr.right); break; case "arrow": case "any": case "all": { const leftTypes = this.typeInference.inferExpressionType( defName, expr.left ); if (leftTypes) { for (const leftType of leftTypes) { const toNode = `${leftType.typeName}#${expr.target}`; graph.addNode({ type: "relation_or_permission", name: expr.target, fullName: toNode }); graph.addEdge(fromNode, toNode); } } this.addExpressionDependencies(graph, defName, permName, expr.left); break; } } } // Phase 4: Validate expressions validateExpressions(ast) { for (const def of ast.definitions) { if (def.type === "definition") { for (const perm of def.permissions) { this.validateExpression(def.name, perm.expression); } } } } validateExpression(defName, expr) { switch (expr.type) { case "identifier": if (!this.symbolTable.hasRelationOrPermission(defName, expr.name)) { this.addError( "UNDEFINED_IDENTIFIER", `Undefined identifier '${expr.name}' in type '${defName}'`, { definition: defName } ); } break; case "union": case "intersection": if (expr.operands.length < 2) { this.addError( "INVALID_EXPRESSION", `${expr.type} expression must have at least 2 operands`, { definition: defName } ); } for (const operand of expr.operands) { this.validateExpression(defName, operand); } break; case "exclusion": this.validateExpression(defName, expr.left); this.validateExpression(defName, expr.right); break; case "arrow": case "any": case "all": { this.validateExpression(defName, expr.left); const leftTypes = this.typeInference.inferExpressionType( defName, expr.left ); if (leftTypes) { let targetFound = false; for (const leftType of leftTypes) { if (this.symbolTable.hasRelationOrPermission( leftType.typeName, expr.target )) { targetFound = true; break; } } if (!targetFound) { const resolvedTypeNames = leftTypes.map((t) => t.typeName).join(", "); this.addError( "UNDEFINED_ARROW_TARGET", `Target '${expr.target}' not found on resolved types [${resolvedTypeNames}] for expression starting with '${expr.left.name}'`, { definition: defName } ); } } break; } } } // Phase 5: Additional checks performAdditionalChecks(ast) { const usedTypes = /* @__PURE__ */ new Set(); for (const def of ast.definitions) { if (def.type === "definition") { for (const rel of def.relations) { for (const type of rel.types) { usedTypes.add(type.typeName); } } } } for (const def of ast.definitions) { if (def.type === "definition" && !usedTypes.has(def.name)) { const isUsed = false; if (!isUsed && def.name !== "user") { this.addWarning( "UNUSED_DEFINITION", `Definition '${def.name}' is not referenced anywhere`, { definition: def.name } ); } } } for (const def of ast.definitions) { if (def.type === "definition") { for (const perm of def.permissions) { if (this.isEmptyPermission(perm.expression)) { this.addWarning( "EMPTY_PERMISSION", `Permission '${perm.name}' in '${def.name}' has no granting mechanism`, { definition: def.name, permission: perm.name } ); } } } } } isEmptyPermission(_expr) { return false; } // Helper methods addError(code, message, location) { this.errors.push({ type: "semantic_error", code, message, location }); } addWarning(code, message, location) { this.warnings.push({ type: "semantic_error", code, message, location }); } }; function analyzeSpiceDbSchema(ast) { const analyzer = new SemanticAnalyzer(); return analyzer.analyze(ast); } // src/schema-parser/parser.ts var import_chevrotain = require("chevrotain"); var Identifier = (0, import_chevrotain.createToken)({ name: "Identifier", pattern: /[a-zA-Z_][a-zA-Z0-9_]*/ }); var Integer = (0, import_chevrotain.createToken)({ name: "Integer", pattern: /0|[1-9]\d*/ }); var StringLiteral = (0, import_chevrotain.createToken)({ name: "StringLiteral", pattern: /"[^"]*"/ }); var Definition = (0, import_chevrotain.createToken)({ name: "Definition", pattern: /definition/, longer_alt: Identif