From 4647ddf816a04e958bdd0852535273cb9dbe5aef Mon Sep 17 00:00:00 2001 From: coodos Date: Thu, 2 Apr 2026 21:48:45 +0530 Subject: [PATCH 1/3] feat: reference mapping --- .../client/src/database/data-source.ts | 2 + .../client/src/database/entities/Reference.ts | 45 +++++++++++++++ .../mappings/reference.mapping.json | 19 +++++++ .../src/web3adapter/watchers/subscriber.ts | 2 + .../src/controllers/ReferenceController.ts | 55 +++++++++++++++++++ platforms/ereputation/api/src/index.ts | 1 + .../mappings/reference.mapping.json | 19 +++++++ 7 files changed, 143 insertions(+) create mode 100644 platforms/cerberus/client/src/database/entities/Reference.ts create mode 100644 platforms/cerberus/client/src/web3adapter/mappings/reference.mapping.json create mode 100644 platforms/ereputation/api/src/web3adapter/mappings/reference.mapping.json diff --git a/platforms/cerberus/client/src/database/data-source.ts b/platforms/cerberus/client/src/database/data-source.ts index ad94c5875..f6d018efd 100644 --- a/platforms/cerberus/client/src/database/data-source.ts +++ b/platforms/cerberus/client/src/database/data-source.ts @@ -9,6 +9,7 @@ import path from "path"; import { UserEVaultMapping } from "./entities/UserEVaultMapping"; import { VotingObservation } from "./entities/VotingObservation"; import { CharterSignature } from "./entities/CharterSignature"; +import { Reference } from "./entities/Reference"; config({ path: path.resolve(__dirname, "../../../../.env") }); @@ -25,6 +26,7 @@ export const AppDataSource = new DataSource({ UserEVaultMapping, VotingObservation, CharterSignature, + Reference, ], migrations: [path.join(__dirname, "migrations", "*.ts")], subscribers: [PostgresSubscriber], diff --git a/platforms/cerberus/client/src/database/entities/Reference.ts b/platforms/cerberus/client/src/database/entities/Reference.ts new file mode 100644 index 000000000..4dab9a629 --- /dev/null +++ b/platforms/cerberus/client/src/database/entities/Reference.ts @@ -0,0 +1,45 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from "typeorm"; +import { User } from "./User"; + +@Entity("references") +export class Reference { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Column() + targetType!: string; // "user", "group", "platform" + + @Column() + targetId!: string; + + @Column() + targetName!: string; + + @Column("text") + content!: string; + + @Column() + referenceType!: string; // "general", "violation", etc. + + @Column("int", { nullable: true }) + numericScore?: number; // 1-5 score + + @Column() + authorId!: string; + + @ManyToOne(() => User) + @JoinColumn({ name: "authorId" }) + author!: User; + + @Column({ default: "signed" }) + status!: string; // "signed", "revoked" + + @Column({ default: false }) + anonymous!: boolean; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/platforms/cerberus/client/src/web3adapter/mappings/reference.mapping.json b/platforms/cerberus/client/src/web3adapter/mappings/reference.mapping.json new file mode 100644 index 000000000..ca744e865 --- /dev/null +++ b/platforms/cerberus/client/src/web3adapter/mappings/reference.mapping.json @@ -0,0 +1,19 @@ +{ + "tableName": "references", + "schemaId": "550e8400-e29b-41d4-a716-446655440010", + "ownerEnamePath": "users(author.ename)", + "localToUniversalMap": { + "id": "id", + "targetType": "targetType", + "targetId": "targetId", + "targetName": "targetName", + "content": "content", + "referenceType": "referenceType", + "numericScore": "numericScore", + "authorId": "users(author.id),author", + "status": "status", + "anonymous": "anonymous", + "createdAt": "__date(createdAt)", + "updatedAt": "__date(updatedAt)" + } +} diff --git a/platforms/cerberus/client/src/web3adapter/watchers/subscriber.ts b/platforms/cerberus/client/src/web3adapter/watchers/subscriber.ts index c4b569712..87b782204 100644 --- a/platforms/cerberus/client/src/web3adapter/watchers/subscriber.ts +++ b/platforms/cerberus/client/src/web3adapter/watchers/subscriber.ts @@ -448,6 +448,8 @@ export class PostgresSubscriber implements EntitySubscriberInterface { return ["sender", "group"]; case "CharterSignature": return ["user", "group"]; + case "Reference": + return ["author"]; default: return []; } diff --git a/platforms/ereputation/api/src/controllers/ReferenceController.ts b/platforms/ereputation/api/src/controllers/ReferenceController.ts index 53c95c966..e20bd2467 100644 --- a/platforms/ereputation/api/src/controllers/ReferenceController.ts +++ b/platforms/ereputation/api/src/controllers/ReferenceController.ts @@ -295,6 +295,61 @@ export class ReferenceController { } }; + /** + * Internal endpoint for service-to-service reference creation (e.g., from Cerberus). + * Requires X-Platform-Secret header matching PLATFORM_SHARED_SECRET env var. + * Creates references with "signed" status directly (no signing session needed). + */ + createSystemReference = async (req: Request, res: Response) => { + try { + const secret = process.env.PLATFORM_SHARED_SECRET; + if (!secret || req.headers['x-platform-secret'] !== secret) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const { targetType, targetId, targetName, content, referenceType, numericScore, authorId, anonymous } = req.body; + + if (!targetType || !targetId || !targetName || !content || !authorId) { + return res.status(400).json({ error: "Missing required fields: targetType, targetId, targetName, content, authorId" }); + } + + if (numericScore && (numericScore < 1 || numericScore > 5)) { + return res.status(400).json({ error: "Numeric score must be between 1 and 5" }); + } + + // Create reference directly with "signed" status (trusted platform call) + const reference = this.referenceService.referenceRepository.create({ + targetType, + targetId, + targetName, + content, + referenceType: referenceType || "violation", + numericScore, + authorId, + anonymous: anonymous ?? false, + status: "signed" + }); + + const saved = await this.referenceService.referenceRepository.save(reference); + + res.status(201).json({ + message: "System reference created successfully", + reference: { + id: saved.id, + targetType: saved.targetType, + targetName: saved.targetName, + content: saved.content, + numericScore: saved.numericScore, + status: saved.status, + createdAt: saved.createdAt + } + }); + } catch (error) { + console.error("Error creating system reference:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + deleteReference = async (req: Request, res: Response) => { try { const { referenceId } = req.params; diff --git a/platforms/ereputation/api/src/index.ts b/platforms/ereputation/api/src/index.ts index d0c17a915..a3eb91bfd 100644 --- a/platforms/ereputation/api/src/index.ts +++ b/platforms/ereputation/api/src/index.ts @@ -106,6 +106,7 @@ app.get("/api/dashboard/stats", authGuard, dashboardController.getStats); app.get("/api/dashboard/activities", authGuard, dashboardController.getActivities); // Reference routes +app.post("/api/references/system", referenceController.createSystemReference); app.post("/api/references", authGuard, referenceController.createReference); app.get("/api/references/target/:targetType/:targetId", referenceController.getReferencesForTarget); app.get("/api/references/my", authGuard, referenceController.getUserReferences); diff --git a/platforms/ereputation/api/src/web3adapter/mappings/reference.mapping.json b/platforms/ereputation/api/src/web3adapter/mappings/reference.mapping.json new file mode 100644 index 000000000..8c67079de --- /dev/null +++ b/platforms/ereputation/api/src/web3adapter/mappings/reference.mapping.json @@ -0,0 +1,19 @@ +{ + "tableName": "references", + "schemaId": "550e8400-e29b-41d4-a716-446655440010", + "ownerEnamePath": "users(author.ename)", + "localToUniversalMap": { + "targetType": "targetType", + "targetId": "targetId", + "targetName": "targetName", + "content": "content", + "referenceType": "referenceType", + "numericScore": "numericScore", + "authorId": "users(author.id),author", + "status": "status", + "anonymous": "anonymous", + "createdAt": "__date(createdAt)", + "updatedAt": "__date(updatedAt)" + }, + "readOnly": false +} From d112d4327ee84c348945b589a4f8105e27dbe31b Mon Sep 17 00:00:00 2001 From: coodos Date: Thu, 2 Apr 2026 22:05:33 +0530 Subject: [PATCH 2/3] feat: sync --- .../src/services/CerberusTriggerService.ts | 91 ++++++++++++-- .../src/services/ReferenceWriterService.ts | 118 ++++++++++++++++++ 2 files changed, 202 insertions(+), 7 deletions(-) create mode 100644 platforms/cerberus/client/src/services/ReferenceWriterService.ts diff --git a/platforms/cerberus/client/src/services/CerberusTriggerService.ts b/platforms/cerberus/client/src/services/CerberusTriggerService.ts index 6d3b5ffa6..760394118 100644 --- a/platforms/cerberus/client/src/services/CerberusTriggerService.ts +++ b/platforms/cerberus/client/src/services/CerberusTriggerService.ts @@ -5,6 +5,7 @@ import { GroupService } from "./GroupService"; import { UserService } from "./UserService"; import { PlatformEVaultService } from "./PlatformEVaultService"; import { VotingContextService } from "./VotingContextService"; +import { ReferenceWriterService } from "./ReferenceWriterService"; interface CharterViolation { violation: string; @@ -28,6 +29,7 @@ export class CerberusTriggerService { private userService: UserService; private platformService: PlatformEVaultService; private votingContextService: VotingContextService; + private referenceWriterService: ReferenceWriterService; private openaiApiKey: string; constructor() { @@ -36,6 +38,7 @@ export class CerberusTriggerService { this.userService = new UserService(); this.platformService = PlatformEVaultService.getInstance(); this.votingContextService = new VotingContextService(); + this.referenceWriterService = new ReferenceWriterService(); this.openaiApiKey = process.env.OPENAI_API_KEY || ""; } @@ -319,6 +322,14 @@ export class CerberusTriggerService { */ async analyzeCharterViolations(messages: Message[], group: Group, lastVoteMessage?: Message): Promise<{ violations: string[]; + userViolations: Array<{ + userId: string; + userName: string; + userEname: string | null; + violation: string; + severity: "low" | "medium" | "high"; + score: number; + }>; summary: string; hasVotingIssues: boolean; votingStatus: string; @@ -327,6 +338,7 @@ export class CerberusTriggerService { if (!this.openaiApiKey) { return { violations: [], + userViolations: [], summary: "⚠️ OpenAI API key not configured. Cannot analyze charter violations.", hasVotingIssues: false, votingStatus: "Not analyzed", @@ -385,10 +397,13 @@ VOTING CONTEXT: console.log(votingContext); } - // Format messages for analysis - const messagesText = messages.map(msg => - `[${msg.createdAt.toLocaleString()}] ${msg.sender ? msg.sender.name : 'System'}: ${msg.text}` - ).join('\n'); + // Format messages for analysis — include sender ID and ename for structured violation reporting + const messagesText = messages.map(msg => { + const senderInfo = msg.sender + ? `${msg.sender.name || 'Unknown'} (id:${msg.sender.id}${msg.sender.ename ? ', ename:' + msg.sender.ename : ''})` + : 'System'; + return `[${msg.createdAt.toLocaleString()}] ${senderInfo}: ${msg.text}`; + }).join('\n'); const charterText = group.charter || "No charter defined for this group."; @@ -430,6 +445,16 @@ CHARTER ENFORCEMENT: RESPOND WITH ONLY PURE JSON - NO MARKDOWN, NO CODE BLOCKS: { "violations": ["array of detailed violation descriptions with justifications"], + "userViolations": [ + { + "userId": "the user's id from the message (id:xxx)", + "userName": "the user's display name", + "userEname": "the user's ename if available, or null", + "violation": "one-sentence description of what rule was violated", + "severity": "low" | "medium" | "high", + "score": 1-5 (1 = severe, 5 = minor/warning) + } + ], "summary": "comprehensive summary with specific examples and actionable recommendations", "hasVotingIssues": boolean, "votingStatus": "string describing current voting situation with reasoning", @@ -486,8 +511,14 @@ Be thorough and justify your reasoning. Provide clear, actionable recommendation // Parse the JSON response try { - const analysis = JSON.parse(content); - + // Strip markdown code fences if present + let jsonContent = content.trim(); + if (jsonContent.startsWith("```")) { + jsonContent = jsonContent.replace(/^```(?:json)?\s*/, "").replace(/```\s*$/, "").trim(); + } + + const analysis = JSON.parse(jsonContent); + // Validate required fields if (!Array.isArray(analysis.violations) || typeof analysis.summary !== 'string' || @@ -497,6 +528,11 @@ Be thorough and justify your reasoning. Provide clear, actionable recommendation throw new Error("Invalid JSON structure from OpenAI"); } + // Ensure userViolations exists and is an array + if (!Array.isArray(analysis.userViolations)) { + analysis.userViolations = []; + } + return analysis; } catch (parseError) { @@ -505,6 +541,7 @@ Be thorough and justify your reasoning. Provide clear, actionable recommendation return { violations: [], + userViolations: [], summary: "❌ Error analyzing messages. Please check the logs.", hasVotingIssues: false, votingStatus: "Error occurred", @@ -516,6 +553,7 @@ Be thorough and justify your reasoning. Provide clear, actionable recommendation console.error("Error analyzing with AI:", error); return { violations: [], + userViolations: [], summary: "❌ Error analyzing charter violations. Please check the logs.", hasVotingIssues: false, votingStatus: "Error occurred", @@ -589,11 +627,50 @@ Be thorough and justify your reasoning. Provide clear, actionable recommendation }); } + // Write violation references for users who violated the charter + if (analysis.userViolations && analysis.userViolations.length > 0) { + try { + // Resolve a Cerberus system user as the author of the reference + // Use the first group participant as a fallback author, or try "cerberus" ename + let authorId: string | null = null; + const cerberusUser = await this.referenceWriterService.resolveUserId("cerberus"); + if (cerberusUser) { + authorId = cerberusUser.id; + } else { + // Use the trigger message sender or first available user + const allUsers = await this.userService.getAllUsers(); + if (allUsers.length > 0) authorId = allUsers[0].id; + } + + if (authorId) { + const violationRefs = analysis.userViolations.map((uv: any) => ({ + targetId: uv.userId, + targetName: uv.userName, + targetEname: uv.userEname || undefined, + content: `[Cerberus - Charter Violation in ${groupWithCharter.name}] ${uv.violation}`, + numericScore: Math.max(1, Math.min(5, uv.score || 2)) + })); + + await this.referenceWriterService.writeViolationReferences( + violationRefs, + triggerMessage.group.id, + groupWithCharter.name, + authorId + ); + console.log(`📝 Wrote ${violationRefs.length} violation references`); + } else { + console.warn("⚠️ No author user found for violation references"); + } + } catch (refError) { + console.error("❌ Error writing violation references:", refError); + } + } + // Build the final analysis text let analysisText: string; if (analysis.violations.length > 0) { analysisText = `🚨 CHARTER VIOLATIONS DETECTED!\n\n${analysis.summary}`; - + // Add enforcement information if available if (analysis.enforcement && analysis.enforcement !== "No enforcement possible - API not configured") { analysisText += `\n\n⚖️ ENFORCEMENT:\n${analysis.enforcement}`; diff --git a/platforms/cerberus/client/src/services/ReferenceWriterService.ts b/platforms/cerberus/client/src/services/ReferenceWriterService.ts new file mode 100644 index 000000000..3fc933476 --- /dev/null +++ b/platforms/cerberus/client/src/services/ReferenceWriterService.ts @@ -0,0 +1,118 @@ +import { AppDataSource } from "../database/data-source"; +import { Reference } from "../database/entities/Reference"; +import { User } from "../database/entities/User"; + +interface ViolationReference { + targetId: string; // User ID of the violator + targetName: string; // Display name of the violator + targetEname?: string; // ename of the violator + content: string; // Description of the violation + numericScore: number; // 1-5 (1 = severe violation, 5 = minor) +} + +export class ReferenceWriterService { + private referenceRepository = AppDataSource.getRepository(Reference); + private userRepository = AppDataSource.getRepository(User); + private eReputationBaseUrl: string; + private platformSecret: string; + + constructor() { + this.eReputationBaseUrl = process.env.PUBLIC_EREPUTATION_BASE_URL || "http://localhost:8765"; + this.platformSecret = process.env.PLATFORM_SHARED_SECRET || ""; + } + + /** + * Write violation references for users who violated the charter. + * Writes to both local DB (for eVault sync) and eReputation API directly. + */ + async writeViolationReferences( + violations: ViolationReference[], + groupId: string, + groupName: string, + authorId: string + ): Promise { + for (const violation of violations) { + try { + // 1. Write to local DB (triggers web3adapter → eVault sync) + await this.writeLocalReference(violation, authorId); + + // 2. Write directly to eReputation API + await this.writeToEReputationApi(violation, authorId); + + console.log(`✅ Violation reference written for ${violation.targetName} (${violation.targetId})`); + } catch (error) { + console.error(`❌ Failed to write violation reference for ${violation.targetName}:`, error); + } + } + } + + /** + * Write reference to local Cerberus DB — the web3adapter subscriber + * will pick it up and sync to eVault automatically. + */ + private async writeLocalReference(violation: ViolationReference, authorId: string): Promise { + const reference = this.referenceRepository.create({ + targetType: "user", + targetId: violation.targetId, + targetName: violation.targetName, + content: violation.content, + referenceType: "violation", + numericScore: violation.numericScore, + authorId, + status: "signed", + anonymous: false + }); + + return await this.referenceRepository.save(reference); + } + + /** + * Write reference directly to eReputation API using the platform shared secret. + */ + private async writeToEReputationApi(violation: ViolationReference, authorId: string): Promise { + if (!this.platformSecret) { + console.warn("⚠️ PLATFORM_SHARED_SECRET not set, skipping eReputation API call"); + return; + } + + try { + const response = await fetch(`${this.eReputationBaseUrl}/api/references/system`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Platform-Secret": this.platformSecret + }, + body: JSON.stringify({ + targetType: "user", + targetId: violation.targetId, + targetName: violation.targetName, + content: violation.content, + referenceType: "violation", + numericScore: violation.numericScore, + authorId, + anonymous: false + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`❌ eReputation API error (${response.status}): ${errorText}`); + } + } catch (error) { + console.error("❌ Failed to call eReputation API:", error); + } + } + + /** + * Resolve a user name/ename to their user ID in the local database. + */ + async resolveUserId(nameOrEname: string): Promise { + // Try ename first + let user = await this.userRepository.findOne({ where: { ename: nameOrEname } }); + if (user) return user; + + // Try name + user = await this.userRepository.findOne({ where: { name: nameOrEname } }); + return user; + } +} From fb137bc7b7f061679247f36967d5f985fc07a12b Mon Sep 17 00:00:00 2001 From: coodos Date: Thu, 2 Apr 2026 22:10:45 +0530 Subject: [PATCH 3/3] feat: reference --- .../api/src/controllers/WebhookController.ts | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/platforms/ereputation/api/src/controllers/WebhookController.ts b/platforms/ereputation/api/src/controllers/WebhookController.ts index bb2e269b2..5813c5f39 100644 --- a/platforms/ereputation/api/src/controllers/WebhookController.ts +++ b/platforms/ereputation/api/src/controllers/WebhookController.ts @@ -11,6 +11,7 @@ import { Group } from "../database/entities/Group"; import { Poll } from "../database/entities/Poll"; import { VoteReputationResult } from "../database/entities/VoteReputationResult"; import { Wishlist } from "../database/entities/Wishlist"; +import { Reference } from "../database/entities/Reference"; import { AppDataSource } from "../database/data-source"; import axios from "axios"; @@ -369,8 +370,66 @@ export class WebhookController { }); finalLocalId = savedWishlist.id; } + } else if (mapping.tableName === "references") { + const referenceRepository = AppDataSource.getRepository(Reference); + + // Get authorId from author reference + let authorId: string | null = null; + if (local.data.author) { + if (typeof local.data.author === "string" && local.data.author.includes("(")) { + authorId = local.data.author.split("(")[1].split(")")[0]; + } else if (typeof local.data.author === "object" && local.data.author !== null && "id" in local.data.author) { + authorId = (local.data.author as { id: string }).id; + } + } else if (local.data.authorId) { + authorId = local.data.authorId as string; + } + + if (localId) { + // Update existing reference + const reference = await referenceRepository.findOne({ + where: { id: localId } + }); + + if (reference) { + reference.targetType = local.data.targetType as string; + reference.targetId = local.data.targetId as string; + reference.targetName = local.data.targetName as string; + reference.content = local.data.content as string; + reference.referenceType = (local.data.referenceType as string) || "general"; + reference.numericScore = local.data.numericScore as number | undefined; + reference.status = (local.data.status as string) || "signed"; + reference.anonymous = (local.data.anonymous as boolean) ?? false; + if (authorId) reference.authorId = authorId; + + await referenceRepository.save(reference); + finalLocalId = reference.id; + } + } else { + // Create new reference + const reference = referenceRepository.create({ + targetType: local.data.targetType as string, + targetId: local.data.targetId as string, + targetName: local.data.targetName as string, + content: local.data.content as string, + referenceType: (local.data.referenceType as string) || "general", + numericScore: local.data.numericScore as number | undefined, + authorId: authorId || undefined, + status: (local.data.status as string) || "signed", + anonymous: (local.data.anonymous as boolean) ?? false + }); + + const savedReference = await referenceRepository.save(reference); + + this.adapter.addToLockedIds(savedReference.id); + await this.adapter.mappingDb.storeMapping({ + localId: savedReference.id, + globalId: req.body.id, + }); + finalLocalId = savedReference.id; + } } - + res.status(200).send(); } catch (e) { console.error("Webhook error:", e);