From e439fde41a4a8a5cfd7511f55a67a078451c9607 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Fri, 14 Nov 2025 02:56:21 +0530 Subject: [PATCH 1/5] fix: make eVault provisioning in groups instant --- .../src/controllers/GroupController.ts | 50 ++++++++++- .../src/web3adapter/watchers/subscriber.ts | 83 ------------------- 2 files changed, 49 insertions(+), 84 deletions(-) diff --git a/platforms/group-charter-manager-api/src/controllers/GroupController.ts b/platforms/group-charter-manager-api/src/controllers/GroupController.ts index fd3e34ed..b31ec941 100644 --- a/platforms/group-charter-manager-api/src/controllers/GroupController.ts +++ b/platforms/group-charter-manager-api/src/controllers/GroupController.ts @@ -1,6 +1,10 @@ import { Request, Response } from "express"; import { GroupService } from "../services/GroupService"; import { CharterSignatureService } from "../services/CharterSignatureService"; +import { createGroupEVault } from "web3-adapter"; +import dotenv from "dotenv"; + +dotenv.config(); export class GroupController { private groupService = new GroupService(); @@ -110,7 +114,51 @@ export class GroupController { } const { charter } = req.body; - const updatedGroup = await this.groupService.updateGroup(id, { charter }); + + // Check if this is the first charter being added (charterless group getting a charter) + const needsEVault = !group.charter && !group.ename && charter; + + let updateData: any = { charter }; + + if (needsEVault) { + console.log("Group getting first charter, provisioning eVault instantly..."); + + // Provision eVault instantly + const registryUrl = process.env.PUBLIC_REGISTRY_URL; + const provisionerUrl = process.env.PUBLIC_PROVISIONER_URL; + + if (!registryUrl || !provisionerUrl) { + throw new Error("Missing required environment variables for eVault creation"); + } + + const groupData = { + name: group.name || "Unnamed Group", + avatar: group.avatarUrl, + description: group.description, + members: group.participants?.map((p: any) => p.id) || [], + admins: group.admins || [], + owner: group.owner, + charter: charter + }; + + console.log("Creating eVault with data:", groupData); + + const evaultResult = await createGroupEVault( + registryUrl, + provisionerUrl, + groupData + ); + + console.log("eVault created successfully:", evaultResult); + + // Set ename from eVault result + updateData.ename = evaultResult.w3id; + + console.log("Setting ename on group:", evaultResult.w3id); + } + + // Now save with both charter and ename (if provisioned) + const updatedGroup = await this.groupService.updateGroup(id, updateData); if (!updatedGroup) { return res.status(404).json({ error: "Group not found" }); diff --git a/platforms/group-charter-manager-api/src/web3adapter/watchers/subscriber.ts b/platforms/group-charter-manager-api/src/web3adapter/watchers/subscriber.ts index 96fd6316..38c1d754 100644 --- a/platforms/group-charter-manager-api/src/web3adapter/watchers/subscriber.ts +++ b/platforms/group-charter-manager-api/src/web3adapter/watchers/subscriber.ts @@ -7,7 +7,6 @@ import { ObjectLiteral, } from "typeorm"; import { Web3Adapter } from "web3-adapter"; -import { createGroupEVault } from "web3-adapter"; import path from "path"; import dotenv from "dotenv"; import { AppDataSource } from "../../database/data-source"; @@ -146,17 +145,6 @@ export class PostgresSubscriber implements EntitySubscriberInterface { }); if (fullEntity) { - // Check eVault creation BEFORE enriching the entity - if (entityName === "Group" && fullEntity.charter && fullEntity.charter.trim() !== "") { - // Check if this group doesn't have an ename yet (meaning eVault wasn't created) - if (!fullEntity.ename) { - // Fire and forget eVault creation - this.spinUpGroupEVault(fullEntity).catch(error => { - console.error("Failed to create eVault for group:", fullEntity.id, error); - }); - } - } - entity = (await this.enrichEntity( fullEntity, event.metadata.tableName, @@ -326,77 +314,6 @@ export class PostgresSubscriber implements EntitySubscriberInterface { } } - /** - * Spin up eVault for a newly chartered group - */ - private async spinUpGroupEVault(group: any): Promise { - try { - console.log("Starting eVault creation for group:", group.id); - - // Get environment variables for eVault creation - const registryUrl = process.env.PUBLIC_REGISTRY_URL; - const provisionerUrl = process.env.PUBLIC_PROVISIONER_URL; - - if (!registryUrl || !provisionerUrl) { - throw new Error("Missing required environment variables for eVault creation"); - } - - // Prepare group data for eVault creation - const groupData = { - name: group.name || "Unnamed Group", - avatar: group.avatarUrl, - description: group.description, - members: group.participants?.map((p: any) => p.id) || [], - admins: group.admins || [], - owner: group.owner, - charter: group.charter - }; - - console.log("Creating eVault with data:", groupData); - - // Create the eVault (this is the long-running operation) - const evaultResult = await createGroupEVault( - registryUrl, - provisionerUrl, - groupData - ); - - console.log("eVault created successfully:", evaultResult); - - // Update the group with the ename (w3id) - use save() to trigger ORM events - const groupRepository = AppDataSource.getRepository("Group"); - const groupToUpdate = await groupRepository.findOne({ where: { id: group.id } }); - if (groupToUpdate) { - groupToUpdate.ename = evaultResult.w3id; - await groupRepository.save(groupToUpdate); - } - - console.log("Group updated with ename:", evaultResult.w3id); - - // Wait 20 seconds before triggering handleChange to allow eVault to stabilize - console.log("Waiting 20 seconds before syncing updated group data..."); - setTimeout(async () => { - try { - // Fetch the updated group entity with relations to trigger handleChange - const updatedGroup = await groupRepository.findOne({ - where: { id: group.id }, - relations: this.getRelationsForEntity("Group") - }); - if (updatedGroup) { - console.log("Triggering handleChange for updated group with ename after timeout"); - await this.handleChange(updatedGroup, "groups"); - } - } catch (error) { - console.error("Error triggering handleChange after timeout for group:", group.id, error); - } - }, 20000); // 20 seconds timeout - - } catch (error: any) { - console.error("Error creating eVault for group:", group.id, error); - throw error; // Re-throw to be caught by the caller - } - } - /** * Convert TypeORM entity to plain object */ From 2d296d704fb8f919747dbb6c9ef055acadfb86e6 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Fri, 14 Nov 2025 04:04:05 +0530 Subject: [PATCH 2/5] chore: fix eid-wallet w3id logic --- .../src/routes/(app)/scan-qr/scanLogic.ts | 23 +------ .../web3adapter/watchers/firestoreWatcher.ts | 69 +++++++++++++++---- .../blabsy/src/lib/context/chat-context.tsx | 15 +++- .../src/services/CerberusTriggerService.ts | 61 ++++++++++++---- .../controllers/CharterSigningController.ts | 44 ++++++------ .../src/controllers/GroupController.ts | 1 - .../src/services/CharterSignatureService.ts | 10 +-- ...ingService.ts => SigningSessionService.ts} | 45 ++++++------ 8 files changed, 168 insertions(+), 100 deletions(-) rename platforms/group-charter-manager-api/src/services/{CharterSigningService.ts => SigningSessionService.ts} (90%) diff --git a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts index aae39b62..fddf2286 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts +++ b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts @@ -237,10 +237,7 @@ export function createScanLogic({ created ? "key-generated" : "key-exists", ); - const w3idResult = await globalState.keyService.getPublicKey( - vault.ename, - "signing", - ); + const w3idResult = vault.ename; if (!w3idResult) { throw new Error("Failed to get W3ID"); } @@ -260,18 +257,10 @@ export function createScanLogic({ const authPayload = { ename: vault.ename, session: get(session), - w3id: w3idResult, signature: signature, appVersion: "0.4.0", }; - console.log("šŸ” Auth payload with signature:", { - ename: authPayload.ename, - session: authPayload.session, - w3id: authPayload.w3id, - signatureLength: authPayload.signature.length, - }); - const redirectUrl = get(redirect); if (!redirectUrl) { throw new Error( @@ -534,10 +523,7 @@ export function createScanLogic({ created ? "key-generated" : "key-exists", ); - const w3idResult = await globalState.keyService.getPublicKey( - vault.ename, - "signing", - ); + const w3idResult = vault.ename; if (!w3idResult) { throw new Error("Failed to get W3ID"); } @@ -678,10 +664,7 @@ export function createScanLogic({ created ? "key-generated" : "key-exists", ); - const w3idResult = await globalState.keyService.getPublicKey( - vault.ename, - "signing", - ); + const w3idResult = vault.ename; if (!w3idResult) { throw new Error("Failed to get W3ID"); } diff --git a/platforms/blabsy-w3ds-auth-api/src/web3adapter/watchers/firestoreWatcher.ts b/platforms/blabsy-w3ds-auth-api/src/web3adapter/watchers/firestoreWatcher.ts index d3fc1a9a..dcdf9b6e 100644 --- a/platforms/blabsy-w3ds-auth-api/src/web3adapter/watchers/firestoreWatcher.ts +++ b/platforms/blabsy-w3ds-auth-api/src/web3adapter/watchers/firestoreWatcher.ts @@ -18,7 +18,8 @@ export class FirestoreWatcher { private retryCount = 0; private readonly maxRetries: number = 10; // Increased retries private readonly retryDelay: number = 1000; // 1 second - private isFirstSnapshot = true; // Skip the initial snapshot that contains all existing documents + private watcherStartTime: number = Date.now(); // Track when watcher starts + private firstSnapshotReceived = false; // Track if we've received the first snapshot // Track processed document IDs to prevent duplicates private processedIds = new Set(); @@ -57,23 +58,53 @@ export class FirestoreWatcher { // Reset stopped flag when starting this.stopped = false; + + // Reset watcher start time + this.watcherStartTime = Date.now(); + this.firstSnapshotReceived = false; try { - // Set up real-time listener (only for new changes, not existing documents) + // Set up real-time listener this.unsubscribe = this.collection.onSnapshot( async (snapshot) => { // Update last snapshot time for health monitoring this.lastSnapshotTime = Date.now(); - // Skip the first snapshot which contains all existing documents - if (this.isFirstSnapshot) { - console.log(`Skipping initial snapshot for ${collectionPath} (contains all existing documents)`); - this.isFirstSnapshot = false; + // On first snapshot, only skip documents that were created/modified BEFORE watcher started + // This ensures we don't miss any new documents created right as the watcher starts + if (!this.firstSnapshotReceived) { + console.log(`First snapshot received for ${collectionPath} with ${snapshot.size} documents`); + this.firstSnapshotReceived = true; + + // Process only documents modified AFTER watcher start time + const recentChanges = snapshot.docChanges().filter((change) => { + const doc = change.doc; + const data = doc.data(); + + // Check if document was modified after watcher started + // Use updatedAt if available, otherwise createdAt + const timestamp = data.updatedAt || data.createdAt; + if (timestamp && timestamp.toMillis) { + const docTime = timestamp.toMillis(); + return docTime >= this.watcherStartTime; + } + + // If no timestamp, process it to be safe + return true; + }); + + if (recentChanges.length > 0) { + console.log(`Processing ${recentChanges.length} recent changes from first snapshot`); + await this.processChanges(recentChanges); + } else { + console.log(`No recent changes in first snapshot, skipping`); + } + + this.retryCount = 0; return; } - // Don't skip snapshots - queue them instead to handle large databases - // Process snapshot asynchronously without blocking new snapshots + // For subsequent snapshots, process all changes normally this.processSnapshot(snapshot).catch((error) => { console.error("Error processing snapshot:", error); this.handleError(error); @@ -205,8 +236,9 @@ export class FirestoreWatcher { this.unsubscribe = null; } - // Reset first snapshot flag - this.isFirstSnapshot = true; + // Reset watcher state + this.watcherStartTime = Date.now(); + this.firstSnapshotReceived = false; this.lastSnapshotTime = Date.now(); // Reset reconnection attempt counter on successful reconnect @@ -323,8 +355,9 @@ export class FirestoreWatcher { this.unsubscribe = null; } - // Reset first snapshot flag when restarting - this.isFirstSnapshot = true; + // Reset watcher state when restarting + this.watcherStartTime = Date.now(); + this.firstSnapshotReceived = false; this.lastSnapshotTime = Date.now(); try { @@ -343,8 +376,10 @@ export class FirestoreWatcher { } } - private async processSnapshot(snapshot: QuerySnapshot): Promise { - const changes = snapshot.docChanges(); + /** + * Processes an array of document changes + */ + private async processChanges(changes: DocumentChange[]): Promise { const collectionPath = this.collection instanceof CollectionReference ? this.collection.path @@ -412,6 +447,12 @@ export class FirestoreWatcher { await Promise.all(processPromises); } + private async processSnapshot(snapshot: QuerySnapshot): Promise { + const changes = snapshot.docChanges(); + await this.processChanges(changes); + } + + private async handleCreateOrUpdate( doc: FirebaseFirestore.QueryDocumentSnapshot, data: DocumentData diff --git a/platforms/blabsy/src/lib/context/chat-context.tsx b/platforms/blabsy/src/lib/context/chat-context.tsx index 233af51e..8b61be18 100644 --- a/platforms/blabsy/src/lib/context/chat-context.tsx +++ b/platforms/blabsy/src/lib/context/chat-context.tsx @@ -117,8 +117,19 @@ export function ChatContextProvider({ return -1; if (!a.lastMessage?.timestamp && b.lastMessage?.timestamp) return 1; - // If neither has lastMessage, sort by updatedAt - return b.updatedAt.toMillis() - a.updatedAt.toMillis(); + // If neither has lastMessage, sort by updatedAt (with null checks) + if (a.updatedAt && b.updatedAt) { + return b.updatedAt.toMillis() - a.updatedAt.toMillis(); + } + // If only one has updatedAt, prioritize it + if (a.updatedAt && !b.updatedAt) return -1; + if (!a.updatedAt && b.updatedAt) return 1; + // If both are null, sort by createdAt as fallback + if (a.createdAt && b.createdAt) { + return b.createdAt.toMillis() - a.createdAt.toMillis(); + } + // If all else fails, maintain order + return 0; }); setChats(sortedChats); diff --git a/platforms/cerberus/src/services/CerberusTriggerService.ts b/platforms/cerberus/src/services/CerberusTriggerService.ts index 8b610e21..7a9c8920 100644 --- a/platforms/cerberus/src/services/CerberusTriggerService.ts +++ b/platforms/cerberus/src/services/CerberusTriggerService.ts @@ -84,10 +84,28 @@ export class CerberusTriggerService { return result; } - // Fallback: check if "Watchdog Name: Cerberus" appears anywhere - const fallbackResult = charterText.includes('watchdog name: cerberus'); - console.log(`šŸ” Fallback check result: ${fallbackResult}`); - return fallbackResult; + // Fallback 1: check if "Watchdog Name: Cerberus" appears anywhere + if (charterText.includes('watchdog name: cerberus')) { + console.log(`šŸ” Fallback 1: Found "watchdog name: cerberus" in charter`); + return true; + } + + // Fallback 2: check if "Automated Watchdog Policy" section mentions Cerberus + const policyMatch = charterText.match(/automated\s+watchdog\s+policy[\s\S]{0,500}cerberus/i); + if (policyMatch) { + console.log(`šŸ” Fallback 2: Found Cerberus in Automated Watchdog Policy section`); + return true; + } + + // Fallback 3: more permissive - just look for both "watchdog" and "cerberus" in the charter + const hasBothTerms = charterText.includes('watchdog') && charterText.includes('cerberus'); + if (hasBothTerms) { + console.log(`šŸ” Fallback 3: Found both "watchdog" and "cerberus" terms in charter`); + return true; + } + + console.log(`šŸ” No match found for Cerberus watchdog - charter may not specify Cerberus`); + return false; } catch (error) { console.error("Error checking if Cerberus is enabled for group:", error); return false; @@ -141,15 +159,6 @@ export class CerberusTriggerService { console.log(`šŸ” Old charter: ${oldCharter ? 'exists' : 'none'}`); console.log(`šŸ” New charter: ${newCharter ? 'exists' : 'none'}`); - // Check if Cerberus is enabled for this group - const cerberusEnabled = await this.isCerberusEnabled(groupId); - console.log(`šŸ” Cerberus enabled check result: ${cerberusEnabled}`); - - if (!cerberusEnabled) { - console.log(`Cerberus not enabled for group ${groupId} - skipping charter change processing`); - return; - } - let changeType: 'created' | 'updated' | 'removed'; if (!oldCharter && newCharter) { @@ -162,11 +171,33 @@ export class CerberusTriggerService { console.log(`šŸ” Change type determined: ${changeType}`); + // Check if Cerberus is enabled for this group + const cerberusEnabled = await this.isCerberusEnabled(groupId); + console.log(`šŸ” Cerberus enabled check result: ${cerberusEnabled}`); + + if (!cerberusEnabled) { + console.log(`Cerberus not enabled for group ${groupId} - sending notification about availability`); + + // Send a notification that charter was created/updated but Cerberus is not enabled + if (changeType === 'created') { + const notificationMessage = `$$system-message$$ Cerberus: A new charter has been created for this group. To enable automated charter monitoring and compliance checking by Cerberus, please add "Watchdog Name: Cerberus" to your charter's Automated Watchdog Policy section.`; + + await this.messageService.createSystemMessageWithoutPrefix({ + text: notificationMessage, + groupId: groupId, + }); + + console.log(`āœ… Cerberus availability notification sent for new charter`); + } + + return; + } + // Create a system message about the charter change const changeMessage = `$$system-message$$ Cerberus: Group charter has been ${changeType}. ${ - changeType === 'created' ? 'New charter is now in effect.' : + changeType === 'created' ? 'New charter is now in effect. I will monitor compliance with the charter rules.' : changeType === 'removed' ? 'Group is now operating without a charter.' : - 'Charter has been updated and new rules are now in effect.' + 'Charter has been updated and new rules are now in effect. All previous signatures have been invalidated.' }`; console.log(`šŸ” Creating system message: ${changeMessage.substring(0, 100)}...`); diff --git a/platforms/group-charter-manager-api/src/controllers/CharterSigningController.ts b/platforms/group-charter-manager-api/src/controllers/CharterSigningController.ts index 327e7624..ead5bedc 100644 --- a/platforms/group-charter-manager-api/src/controllers/CharterSigningController.ts +++ b/platforms/group-charter-manager-api/src/controllers/CharterSigningController.ts @@ -1,22 +1,22 @@ import { Request, Response } from "express"; -import { CharterSigningService } from "../services/CharterSigningService"; +import { SigningSessionService } from "../services/SigningSessionService"; export class CharterSigningController { - private signingService: CharterSigningService | null = null; + private signingService: SigningSessionService | null = null; constructor() { try { - this.signingService = new CharterSigningService(); + this.signingService = new SigningSessionService(); console.log("CharterSigningController initialized successfully"); } catch (error) { - console.error("Failed to initialize CharterSigningService:", error); + console.error("Failed to initialize SigningSessionService:", error); this.signingService = null; } } - private ensureService(): CharterSigningService { + private ensureService(): SigningSessionService { if (!this.signingService) { - throw new Error("CharterSigningService not initialized"); + throw new Error("SigningSessionService not initialized"); } return this.signingService; } @@ -35,13 +35,13 @@ export class CharterSigningController { const userId = (req as any).user?.id; if (!groupId || !charterData || !userId) { - return res.status(400).json({ - error: "Missing required fields: groupId, charterData, or userId" + return res.status(400).json({ + error: "Missing required fields: groupId, charterData, or userId" }); } const session = await this.ensureService().createSession(groupId, charterData, userId); - + res.json({ sessionId: session.sessionId, qrData: session.qrData, @@ -57,7 +57,7 @@ export class CharterSigningController { async getSigningSessionStatus(req: Request, res: Response) { try { const { sessionId } = req.params; - + if (!sessionId) { return res.status(400).json({ error: "Session ID is required" }); } @@ -80,11 +80,11 @@ export class CharterSigningController { // Poll for status changes const interval = setInterval(async () => { const session = await this.ensureService().getSessionStatus(sessionId); - + if (session) { if (session.status === "completed") { - res.write(`data: ${JSON.stringify({ - type: "signed", + res.write(`data: ${JSON.stringify({ + type: "signed", status: "completed", groupId: session.groupId })}\n\n`); @@ -121,18 +121,18 @@ export class CharterSigningController { // Handle signed payload callback from eID Wallet async handleSignedPayload(req: Request, res: Response) { try { - const { sessionId, signature, publicKey, message } = req.body; - + const { sessionId, signature, w3id, message } = req.body; + // Validate required fields const missingFields = []; if (!sessionId) missingFields.push('sessionId'); if (!signature) missingFields.push('signature'); - if (!publicKey) missingFields.push('publicKey'); + if (!w3id) missingFields.push('w3id'); if (!message) missingFields.push('message'); if (missingFields.length > 0) { - return res.status(400).json({ - error: `Missing required fields: ${missingFields.join(', ')}` + return res.status(400).json({ + error: `Missing required fields: ${missingFields.join(', ')}` }); } @@ -140,7 +140,7 @@ export class CharterSigningController { const result = await this.ensureService().processSignedPayload( sessionId, signature, - publicKey, + w3id, message ); @@ -170,13 +170,13 @@ export class CharterSigningController { async getSigningSession(req: Request, res: Response) { try { const { sessionId } = req.params; - + if (!sessionId) { return res.status(400).json({ error: "Session ID is required" }); } const session = await this.ensureService().getSession(sessionId); - + if (!session) { return res.status(404).json({ error: "Session not found" }); } @@ -188,4 +188,4 @@ export class CharterSigningController { res.status(500).json({ error: "Failed to get signing session" }); } } -} \ No newline at end of file +} diff --git a/platforms/group-charter-manager-api/src/controllers/GroupController.ts b/platforms/group-charter-manager-api/src/controllers/GroupController.ts index b31ec941..8da5637d 100644 --- a/platforms/group-charter-manager-api/src/controllers/GroupController.ts +++ b/platforms/group-charter-manager-api/src/controllers/GroupController.ts @@ -133,7 +133,6 @@ export class GroupController { const groupData = { name: group.name || "Unnamed Group", - avatar: group.avatarUrl, description: group.description, members: group.participants?.map((p: any) => p.id) || [], admins: group.admins || [], diff --git a/platforms/group-charter-manager-api/src/services/CharterSignatureService.ts b/platforms/group-charter-manager-api/src/services/CharterSignatureService.ts index 74dbcd35..c324a032 100644 --- a/platforms/group-charter-manager-api/src/services/CharterSignatureService.ts +++ b/platforms/group-charter-manager-api/src/services/CharterSignatureService.ts @@ -24,7 +24,7 @@ export class CharterSignatureService { message: string ): Promise { const charterHash = this.createCharterHash(charterContent); - + const charterSignature = this.signatureRepository.create({ groupId, userId, @@ -40,7 +40,7 @@ export class CharterSignatureService { // Get all signatures for a specific charter version async getSignaturesForCharter(groupId: string, charterContent: string): Promise { const charterHash = this.createCharterHash(charterContent); - + return await this.signatureRepository.find({ where: { groupId, @@ -70,7 +70,7 @@ export class CharterSignatureService { // Check if a user has signed the current charter version async hasUserSignedCharter(groupId: string, userId: string, charterContent: string): Promise { const charterHash = this.createCharterHash(charterContent); - + const signature = await this.signatureRepository.findOne({ where: { groupId, @@ -136,7 +136,7 @@ export class CharterSignatureService { const result = await this.signatureRepository.delete({ groupId }); - + console.log(`Deleted ${result.affected || 0} signatures for group ${groupId} due to charter content change`); } -} \ No newline at end of file +} diff --git a/platforms/group-charter-manager-api/src/services/CharterSigningService.ts b/platforms/group-charter-manager-api/src/services/SigningSessionService.ts similarity index 90% rename from platforms/group-charter-manager-api/src/services/CharterSigningService.ts rename to platforms/group-charter-manager-api/src/services/SigningSessionService.ts index 1acf492c..2751d6a9 100644 --- a/platforms/group-charter-manager-api/src/services/CharterSigningService.ts +++ b/platforms/group-charter-manager-api/src/services/SigningSessionService.ts @@ -1,7 +1,7 @@ import crypto from "crypto"; import { CharterSignatureService } from "./CharterSignatureService"; -export interface CharterSigningSession { +export interface SigningSession { sessionId: string; groupId: string; charterData: any; @@ -19,7 +19,7 @@ export interface SignedCharterPayload { message: string; } -export interface CharterSigningResult { +export interface SigningResult { success: boolean; error?: string; sessionId: string; @@ -31,11 +31,11 @@ export interface CharterSigningResult { type: "signed" | "security_violation"; } -export class CharterSigningService { - private sessions: Map = new Map(); +export class SigningSessionService { + private sessions: Map = new Map(); private signatureService = new CharterSignatureService(); - async createSession(groupId: string, charterData: any, userId: string): Promise { + async createSession(groupId: string, charterData: any, userId: string): Promise { const sessionId = crypto.randomUUID(); const now = new Date(); const expiresAt = new Date(now.getTime() + 15 * 60 * 1000); // 15 minutes @@ -45,14 +45,14 @@ export class CharterSigningService { message: `Sign charter for group: ${groupId}`, sessionId: sessionId }); - + const base64Data = Buffer.from(messageData).toString('base64'); const apiBaseUrl = process.env.PUBLIC_GROUP_CHARTER_BASE_URL || "http://localhost:3003"; const redirectUri = `${apiBaseUrl}/api/signing/callback`; - + const qrData = `w3ds://sign?session=${sessionId}&data=${base64Data}&redirect_uri=${encodeURIComponent(redirectUri)}`; - const session: CharterSigningSession = { + const session: SigningSession = { sessionId, groupId, charterData, @@ -78,9 +78,9 @@ export class CharterSigningService { return session; } - async getSession(sessionId: string): Promise { + async getSession(sessionId: string): Promise { const session = this.sessions.get(sessionId); - + if (!session) { return null; } @@ -94,12 +94,12 @@ export class CharterSigningService { return session; } - async processSignedPayload(sessionId: string, signature: string, publicKey: string, message: string): Promise { + async processSignedPayload(sessionId: string, signature: string, publicKey: string, message: string): Promise { console.log(`Processing signed payload for session: ${sessionId}`); console.log(`Available sessions:`, Array.from(this.sessions.keys())); - + const session = await this.getSession(sessionId); - + if (!session) { console.log(`Session ${sessionId} not found in available sessions`); throw new Error("Session not found"); @@ -120,7 +120,7 @@ export class CharterSigningService { const { UserService } = await import('./UserService'); const userService = new UserService(); const user = await userService.getUserById(session.userId); - + if (!user) { throw new Error("User not found for session"); } @@ -128,7 +128,7 @@ export class CharterSigningService { // Strip @ prefix from both enames before comparison const cleanPublicKey = publicKey.replace(/^@/, ''); const cleanUserEname = user.ename.replace(/^@/, ''); - + if (cleanPublicKey !== cleanUserEname) { console.error(`šŸ”’ SECURITY VIOLATION: publicKey mismatch!`, { publicKey, @@ -137,11 +137,13 @@ export class CharterSigningService { cleanUserEname, sessionUserId: session.userId }); - + + console.log(cleanPublicKey, cleanUserEname) + // Update session status to indicate security violation session.status = "security_violation"; this.sessions.set(sessionId, session); - + // Return error result instead of throwing return { success: false, @@ -152,7 +154,7 @@ export class CharterSigningService { type: "security_violation" }; } - + console.log(`āœ… Public key verification passed: ${cleanPublicKey} matches ${cleanUserEname}`); } catch (error) { console.error("Error during public key verification:", error); @@ -178,7 +180,7 @@ export class CharterSigningService { session.status = "completed"; this.sessions.set(sessionId, session); - const result: CharterSigningResult = { + const result: SigningResult = { success: true, sessionId, groupId: session.groupId, @@ -192,11 +194,12 @@ export class CharterSigningService { return result; } - async getSessionStatus(sessionId: string): Promise { + async getSessionStatus(sessionId: string): Promise { return this.getSession(sessionId); } testConnection(): boolean { return true; } -} \ No newline at end of file +} + From 53f037c07dc67cdf5a675bf4c2d0c30592ed6d68 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Fri, 14 Nov 2025 11:17:13 +0530 Subject: [PATCH 3/5] fix: cerberus issues with new infra --- .../evault-core/src/core/db/db.service.ts | 1 + .../src/core/protocol/vault-access-guard.ts | 8 + infrastructure/web3-adapter/src/index.ts | 820 +++++++++--------- .../src/controllers/WebhookController.ts | 207 ++--- .../cerberus/src/database/entities/Group.ts | 2 +- .../src/services/CerberusTriggerService.ts | 66 +- .../cerberus/src/services/OpenAIService.ts | 54 ++ .../web3adapter/mappings/group.mapping.json | 2 +- .../web3adapter/mappings/message.mapping.json | 1 + .../src/web3adapter/watchers/subscriber.ts | 16 +- .../src/controllers/GroupController.ts | 26 +- .../src/web3adapter/watchers/subscriber.ts | 5 +- 12 files changed, 651 insertions(+), 557 deletions(-) diff --git a/infrastructure/evault-core/src/core/db/db.service.ts b/infrastructure/evault-core/src/core/db/db.service.ts index b30cf595..4d66de12 100644 --- a/infrastructure/evault-core/src/core/db/db.service.ts +++ b/infrastructure/evault-core/src/core/db/db.service.ts @@ -552,6 +552,7 @@ export class DbService { id, ontology: meta.ontology, acl, + parsed: meta.payload, }, envelopes: createdEnvelopes, }; diff --git a/infrastructure/evault-core/src/core/protocol/vault-access-guard.ts b/infrastructure/evault-core/src/core/protocol/vault-access-guard.ts index 04568c05..f6b106b3 100644 --- a/infrastructure/evault-core/src/core/protocol/vault-access-guard.ts +++ b/infrastructure/evault-core/src/core/protocol/vault-access-guard.ts @@ -175,6 +175,14 @@ export class VaultAccessGuard { // Check if envelope exists and user has access const { hasAccess, exists } = await this.checkAccess(metaEnvelopeId, context); + + // For update operations, if envelope doesn't exist, allow the resolver to create it + if (!exists && args.input) { + // This is an update/create operation - let the resolver handle it + const result = await resolver(parent, args, context); + return this.filterACL(result); + } + if (!hasAccess) { // If envelope doesn't exist, return null (not found) if (!exists) { diff --git a/infrastructure/web3-adapter/src/index.ts b/infrastructure/web3-adapter/src/index.ts index 61a2222d..0f0a758a 100644 --- a/infrastructure/web3-adapter/src/index.ts +++ b/infrastructure/web3-adapter/src/index.ts @@ -16,67 +16,67 @@ import type { IMapping } from "./mapper/mapper.types"; * @returns Promise with eVault details (w3id, uri) */ export async function spinUpEVault( - registryUrl: string, - provisionerUrl: string, - verificationCode?: string, + registryUrl: string, + provisionerUrl: string, + verificationCode?: string, ): Promise<{ w3id: string; uri: string }> { - const DEMO_CODE_W3DS = "d66b7138-538a-465f-a6ce-f6985854c3f4"; - const finalVerificationCode = verificationCode || DEMO_CODE_W3DS; - - try { - const entropyResponse = await axios.get( - new URL("/entropy", registryUrl).toString(), - ); - const registryEntropy = entropyResponse.data.token; - - const namespace = uuidv4(); - - const provisionResponse = await axios.post( - new URL("/provision", provisionerUrl).toString(), - { - registryEntropy, - namespace, - verificationId: finalVerificationCode, - publicKey: "0x0000000000000000000000000000000000000000", - }, - ); - - if (!provisionResponse.data.success) { - throw new Error( - `Failed to provision eVault: ${provisionResponse.data.message || "Unknown error"}`, - ); - } - - return { - w3id: provisionResponse.data.w3id, - uri: provisionResponse.data.uri, - }; - } catch (error) { - if (axios.isAxiosError(error)) { - throw new Error( - `Failed to spin up eVault: ${error.response?.data?.message || error.message}`, - ); - } - throw new Error( - `Failed to spin up eVault: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } + const DEMO_CODE_W3DS = "d66b7138-538a-465f-a6ce-f6985854c3f4"; + const finalVerificationCode = verificationCode || DEMO_CODE_W3DS; + + try { + const entropyResponse = await axios.get( + new URL("/entropy", registryUrl).toString(), + ); + const registryEntropy = entropyResponse.data.token; + + const namespace = uuidv4(); + + const provisionResponse = await axios.post( + new URL("/provision", provisionerUrl).toString(), + { + registryEntropy, + namespace, + verificationId: finalVerificationCode, + publicKey: "0x0000000000000000000000000000000000000000", + }, + ); + + if (!provisionResponse.data.success) { + throw new Error( + `Failed to provision eVault: ${provisionResponse.data.message || "Unknown error"}`, + ); + } + + return { + w3id: provisionResponse.data.w3id, + uri: provisionResponse.data.uri, + }; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error( + `Failed to spin up eVault: ${error.response?.data?.message || error.message}`, + ); + } + throw new Error( + `Failed to spin up eVault: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } } /** * Interface for GroupManifest data */ interface GroupManifest { - eName: string; - name: string; - avatar?: string; - description?: string; - members: string[]; - charter?: string; - admins: string[]; - owner: string; - createdAt: string; - updatedAt: string; + eName: string; + name: string; + avatar?: string; + description?: string; + members: string[]; + charter?: string; + admins: string[]; + owner: string; + createdAt: string; + updatedAt: string; } /** @@ -88,105 +88,105 @@ interface GroupManifest { * @returns Promise with eVault details (w3id, uri, manifestId) */ export async function createGroupEVault( - registryUrl: string, - provisionerUrl: string, - groupData: { - name: string; - avatar?: string; - description?: string; - members: string[]; - admins: string[]; - owner: string; - charter?: string; - }, - verificationCode?: string, + registryUrl: string, + provisionerUrl: string, + groupData: { + name: string; + avatar?: string; + description?: string; + members: string[]; + admins: string[]; + owner: string; + charter?: string; + }, + verificationCode?: string, ): Promise<{ w3id: string; uri: string; manifestId: string }> { - const DEMO_CODE_W3DS = "d66b7138-538a-465f-a6ce-f6985854c3f4"; - const finalVerificationCode = verificationCode || DEMO_CODE_W3DS; - - try { - // Step 1: Spin up the eVault - const evault = await spinUpEVault( - registryUrl, - provisionerUrl, - finalVerificationCode, - ); - - // Step 2: Create GroupManifest with exponential backoff - const manifestId = await createGroupManifestWithRetry( - registryUrl, - evault.w3id, - groupData, - ); - - return { - w3id: evault.w3id, - uri: evault.uri, - manifestId, - }; - } catch (error) { - if (axios.isAxiosError(error)) { - throw new Error( - `Failed to create group eVault: ${error.response?.data?.message || error.message}`, - ); - } - throw new Error( - `Failed to create group eVault: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } + const DEMO_CODE_W3DS = "d66b7138-538a-465f-a6ce-f6985854c3f4"; + const finalVerificationCode = verificationCode || DEMO_CODE_W3DS; + + try { + // Step 1: Spin up the eVault + const evault = await spinUpEVault( + registryUrl, + provisionerUrl, + finalVerificationCode, + ); + + // Step 2: Create GroupManifest with exponential backoff + const manifestId = await createGroupManifestWithRetry( + registryUrl, + evault.w3id, + groupData, + ); + + return { + w3id: evault.w3id, + uri: evault.uri, + manifestId, + }; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error( + `Failed to create group eVault: ${error.response?.data?.message || error.message}`, + ); + } + throw new Error( + `Failed to create group eVault: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } } /** * Create GroupManifest in eVault with exponential backoff retry mechanism */ async function createGroupManifestWithRetry( - registryUrl: string, - w3id: string, - groupData: { - name: string; - avatar?: string; - description?: string; - members: string[]; - admins: string[]; - owner: string; - charter?: string; - }, - maxRetries = 10, + registryUrl: string, + w3id: string, + groupData: { + name: string; + avatar?: string; + description?: string; + members: string[]; + admins: string[]; + owner: string; + charter?: string; + }, + maxRetries = 10, ): Promise { - const now = new Date().toISOString(); - - const groupManifest: GroupManifest = { - eName: w3id, - name: groupData.name, - avatar: groupData.avatar, - description: groupData.description, - members: groupData.members, - charter: groupData.charter, - admins: groupData.admins, - owner: groupData.owner, - createdAt: now, - updatedAt: now, - }; - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - console.log( - `Attempting to create GroupManifest in eVault (attempt ${attempt}/${maxRetries})`, - ); - - const response = await axios.get( - new URL(`resolve?w3id=${w3id}`, registryUrl).toString(), - ); - const endpoint = new URL("/graphql", response.data.uri).toString(); - - const { GraphQLClient } = await import("graphql-request"); - const client = new GraphQLClient(endpoint, { - headers: { - "X-ENAME": w3id, - }, - }); - - const STORE_META_ENVELOPE = ` + const now = new Date().toISOString(); + + const groupManifest: GroupManifest = { + eName: w3id, + name: groupData.name, + avatar: groupData.avatar, + description: groupData.description, + members: groupData.members, + charter: groupData.charter, + admins: groupData.admins, + owner: groupData.owner, + createdAt: now, + updatedAt: now, + }; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + console.log( + `Attempting to create GroupManifest in eVault (attempt ${attempt}/${maxRetries})`, + ); + + const response = await axios.get( + new URL(`resolve?w3id=${w3id}`, registryUrl).toString(), + ); + const endpoint = new URL("/graphql", response.data.uri).toString(); + + const { GraphQLClient } = await import("graphql-request"); + const client = new GraphQLClient(endpoint, { + headers: { + "X-ENAME": w3id, + }, + }); + + const STORE_META_ENVELOPE = ` mutation StoreMetaEnvelope($input: MetaEnvelopeInput!) { storeMetaEnvelope(input: $input) { metaEnvelope { @@ -198,272 +198,272 @@ async function createGroupManifestWithRetry( } `; - interface MetaEnvelopeResponse { - storeMetaEnvelope: { - metaEnvelope: { - id: string; - ontology: string; - parsed: unknown; - }; - }; - } - - const result = await client.request( - STORE_META_ENVELOPE, - { - input: { - ontology: "550e8400-e29b-41d4-a716-446655440001", // GroupManifest schema ID - payload: groupManifest, - acl: ["*"], - }, - }, - ); - - const manifestId = result.storeMetaEnvelope.metaEnvelope.id; - console.log("GroupManifest created successfully in eVault:", manifestId); - return manifestId; - } catch (error) { - console.error( - `Failed to create GroupManifest in eVault (attempt ${attempt}/${maxRetries}):`, - error, - ); - - if (attempt === maxRetries) { - console.error( - "Max retries reached, giving up on GroupManifest creation", - ); - throw error; - } - - // Wait before retrying (exponential backoff) - const delay = Math.min(1000 * 2 ** (attempt - 1), 10000); - console.log(`Waiting ${delay}ms before retry...`); - await new Promise((resolve) => setTimeout(resolve, delay)); - } - } - - throw new Error("Failed to create GroupManifest after all retries"); + interface MetaEnvelopeResponse { + storeMetaEnvelope: { + metaEnvelope: { + id: string; + ontology: string; + parsed: unknown; + }; + }; + } + + const result = await client.request( + STORE_META_ENVELOPE, + { + input: { + ontology: "550e8400-e29b-41d4-a716-446655440003", // GroupManifest schema ID + payload: groupManifest, + acl: ["*"], + }, + }, + ); + + const manifestId = result.storeMetaEnvelope.metaEnvelope.id; + console.log("GroupManifest created successfully in eVault:", manifestId); + return manifestId; + } catch (error) { + console.error( + `Failed to create GroupManifest in eVault (attempt ${attempt}/${maxRetries}):`, + error, + ); + + if (attempt === maxRetries) { + console.error( + "Max retries reached, giving up on GroupManifest creation", + ); + throw error; + } + + // Wait before retrying (exponential backoff) + const delay = Math.min(1000 * 2 ** (attempt - 1), 10000); + console.log(`Waiting ${delay}ms before retry...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + + throw new Error("Failed to create GroupManifest after all retries"); } export class Web3Adapter { - mapping: Record = {}; - mappingDb: MappingDatabase; - evaultClient: EVaultClient; - lockedIds: string[] = []; - platform: string; - - constructor( - private readonly config: { - schemasPath: string; - dbPath: string; - registryUrl: string; - platform: string; - provisionerUrl?: string; - }, - ) { - this.readPaths(); - this.mappingDb = new MappingDatabase(config.dbPath); - this.evaultClient = new EVaultClient(config.registryUrl, config.platform); - this.platform = config.platform; - } - - async readPaths() { - const allRawFiles = await fs.readdir(this.config.schemasPath); - const mappingFiles = allRawFiles.filter((p: string) => p.endsWith(".json")); - - for (const mappingFile of mappingFiles) { - const mappingFileContent = await fs.readFile( - path.join(this.config.schemasPath, mappingFile), - ); - const mappingParsed = JSON.parse( - mappingFileContent.toString(), - ) as IMapping; - this.mapping[mappingParsed.tableName] = mappingParsed; - } - } - - addToLockedIds(id: string) { - this.lockedIds.push(id); - console.log("Added", this.lockedIds); - setTimeout(() => { - this.lockedIds = this.lockedIds.filter((f) => f !== id); - }, 15_000); - } - - async handleChange(props: { - data: Record; - tableName: string; - participants?: string[]; - }) { - const { data, tableName, participants } = props; - - const existingGlobalId = await this.mappingDb.getGlobalId( - data.id as string, - ); - - if (!this.mapping[tableName]) return; - - if (this.mapping[tableName].readOnly) { - // early return on mappings which are readonly so as to not - // sync any update to the eVault which is not warranted - return; - } - - if (existingGlobalId) { - if (this.lockedIds.includes(existingGlobalId)) return; - const global = await toGlobal({ - data, - mapping: this.mapping[tableName], - mappingStore: this.mappingDb, - }); - - this.evaultClient - .updateMetaEnvelopeById(existingGlobalId, { - id: existingGlobalId, - w3id: global.ownerEvault as string, - data: global.data, - schemaId: this.mapping[tableName].schemaId, - }) - .catch(() => console.error("failed to sync update")); - - logger.info({ - tableName, - id: existingGlobalId, - platform: this.platform, - w3id: global.ownerEvault, - }); - - return { - id: existingGlobalId, - w3id: global.ownerEvault as string, - schemaId: this.mapping[tableName].tableName, - }; - } - - const global = await toGlobal({ - data, - mapping: this.mapping[tableName], - mappingStore: this.mappingDb, - }); - - let globalId: string; - if (global.ownerEvault) { - globalId = await this.evaultClient.storeMetaEnvelope({ - id: null, - w3id: global.ownerEvault as string, - data: global.data, - schemaId: this.mapping[tableName].schemaId, - }); - console.log("created new meta-env", globalId); - } else { - return; - } - - // Store the mapping - await this.mappingDb.storeMapping({ - localId: data.id as string, - globalId, - }); - - // Handle references for other participants - const otherEvaults = (participants ?? []).filter( - (i: string) => i !== global.ownerEvault, - ); - for (const evault of otherEvaults) { - await this.evaultClient.storeReference( - `${global.ownerEvault}/${globalId}`, - evault, - ); - } - - logger.info({ - tableName, - id: globalId, - w3id: global.ownerEvault, - platform: this.platform, - }); - - return { - id: globalId, - w3id: global.ownerEvault as string, - data: global.data, - schemaId: this.mapping[tableName].schemaId, - }; - } - - async fromGlobal(props: { - data: Record; - mapping: IMapping; - }) { - const { data, mapping } = props; - - const local = await fromGlobal({ - data, - mapping, - mappingStore: this.mappingDb, - }); - - return local; - } - - /** - * Spins up an eVault by getting entropy from registry and provisioning it - * @param verificationCode - Optional verification code, defaults to demo code - * @param provisionerUrl - Optional provisioner URL, defaults to config - * @returns Promise with eVault details (w3id, uri) - */ - async spinUpEVault( - verificationCode?: string, - provisionerUrl?: string, - ): Promise<{ w3id: string; uri: string }> { - const finalProvisionerUrl = provisionerUrl || this.config.provisionerUrl; - - if (!finalProvisionerUrl) { - throw new Error( - "Provisioner URL is required. Please provide it in config or as parameter.", - ); - } - - return spinUpEVault( - this.config.registryUrl, - finalProvisionerUrl, - verificationCode, - ); - } - - /** - * Creates a group eVault with GroupManifest - * @param groupData - Group data for the manifest - * @param verificationCode - Optional verification code, defaults to demo code - * @param provisionerUrl - Optional provisioner URL, defaults to config - * @returns Promise with eVault details (w3id, uri, manifestId) - */ - async createGroupEVault( - groupData: { - name: string; - avatar?: string; - description?: string; - members: string[]; - admins: string[]; - owner: string; - charter?: string; - }, - verificationCode?: string, - provisionerUrl?: string, - ): Promise<{ w3id: string; uri: string; manifestId: string }> { - const finalProvisionerUrl = provisionerUrl || this.config.provisionerUrl; - - if (!finalProvisionerUrl) { - throw new Error( - "Provisioner URL is required. Please provide it in config or as parameter.", - ); - } - - return createGroupEVault( - this.config.registryUrl, - finalProvisionerUrl, - groupData, - verificationCode, - ); - } + mapping: Record = {}; + mappingDb: MappingDatabase; + evaultClient: EVaultClient; + lockedIds: string[] = []; + platform: string; + + constructor( + private readonly config: { + schemasPath: string; + dbPath: string; + registryUrl: string; + platform: string; + provisionerUrl?: string; + }, + ) { + this.readPaths(); + this.mappingDb = new MappingDatabase(config.dbPath); + this.evaultClient = new EVaultClient(config.registryUrl, config.platform); + this.platform = config.platform; + } + + async readPaths() { + const allRawFiles = await fs.readdir(this.config.schemasPath); + const mappingFiles = allRawFiles.filter((p: string) => p.endsWith(".json")); + + for (const mappingFile of mappingFiles) { + const mappingFileContent = await fs.readFile( + path.join(this.config.schemasPath, mappingFile), + ); + const mappingParsed = JSON.parse( + mappingFileContent.toString(), + ) as IMapping; + this.mapping[mappingParsed.tableName] = mappingParsed; + } + } + + addToLockedIds(id: string) { + this.lockedIds.push(id); + console.log("Added", this.lockedIds); + setTimeout(() => { + this.lockedIds = this.lockedIds.filter((f) => f !== id); + }, 15_000); + } + + async handleChange(props: { + data: Record; + tableName: string; + participants?: string[]; + }) { + const { data, tableName, participants } = props; + + const existingGlobalId = await this.mappingDb.getGlobalId( + data.id as string, + ); + + if (!this.mapping[tableName]) return; + + if (this.mapping[tableName].readOnly) { + // early return on mappings which are readonly so as to not + // sync any update to the eVault which is not warranted + return; + } + + if (existingGlobalId) { + if (this.lockedIds.includes(existingGlobalId)) return; + const global = await toGlobal({ + data, + mapping: this.mapping[tableName], + mappingStore: this.mappingDb, + }); + + this.evaultClient + .updateMetaEnvelopeById(existingGlobalId, { + id: existingGlobalId, + w3id: global.ownerEvault as string, + data: global.data, + schemaId: this.mapping[tableName].schemaId, + }) + .catch(() => console.error("failed to sync update")); + + logger.info({ + tableName, + id: existingGlobalId, + platform: this.platform, + w3id: global.ownerEvault, + }); + + return { + id: existingGlobalId, + w3id: global.ownerEvault as string, + schemaId: this.mapping[tableName].tableName, + }; + } + + const global = await toGlobal({ + data, + mapping: this.mapping[tableName], + mappingStore: this.mappingDb, + }); + + let globalId: string; + if (global.ownerEvault) { + globalId = await this.evaultClient.storeMetaEnvelope({ + id: null, + w3id: global.ownerEvault as string, + data: global.data, + schemaId: this.mapping[tableName].schemaId, + }); + console.log("created new meta-env", globalId); + } else { + return; + } + + // Store the mapping + await this.mappingDb.storeMapping({ + localId: data.id as string, + globalId, + }); + + // Handle references for other participants + const otherEvaults = (participants ?? []).filter( + (i: string) => i !== global.ownerEvault, + ); + for (const evault of otherEvaults) { + await this.evaultClient.storeReference( + `${global.ownerEvault}/${globalId}`, + evault, + ); + } + + logger.info({ + tableName, + id: globalId, + w3id: global.ownerEvault, + platform: this.platform, + }); + + return { + id: globalId, + w3id: global.ownerEvault as string, + data: global.data, + schemaId: this.mapping[tableName].schemaId, + }; + } + + async fromGlobal(props: { + data: Record; + mapping: IMapping; + }) { + const { data, mapping } = props; + + const local = await fromGlobal({ + data, + mapping, + mappingStore: this.mappingDb, + }); + + return local; + } + + /** + * Spins up an eVault by getting entropy from registry and provisioning it + * @param verificationCode - Optional verification code, defaults to demo code + * @param provisionerUrl - Optional provisioner URL, defaults to config + * @returns Promise with eVault details (w3id, uri) + */ + async spinUpEVault( + verificationCode?: string, + provisionerUrl?: string, + ): Promise<{ w3id: string; uri: string }> { + const finalProvisionerUrl = provisionerUrl || this.config.provisionerUrl; + + if (!finalProvisionerUrl) { + throw new Error( + "Provisioner URL is required. Please provide it in config or as parameter.", + ); + } + + return spinUpEVault( + this.config.registryUrl, + finalProvisionerUrl, + verificationCode, + ); + } + + /** + * Creates a group eVault with GroupManifest + * @param groupData - Group data for the manifest + * @param verificationCode - Optional verification code, defaults to demo code + * @param provisionerUrl - Optional provisioner URL, defaults to config + * @returns Promise with eVault details (w3id, uri, manifestId) + */ + async createGroupEVault( + groupData: { + name: string; + avatar?: string; + description?: string; + members: string[]; + admins: string[]; + owner: string; + charter?: string; + }, + verificationCode?: string, + provisionerUrl?: string, + ): Promise<{ w3id: string; uri: string; manifestId: string }> { + const finalProvisionerUrl = provisionerUrl || this.config.provisionerUrl; + + if (!finalProvisionerUrl) { + throw new Error( + "Provisioner URL is required. Please provide it in config or as parameter.", + ); + } + + return createGroupEVault( + this.config.registryUrl, + finalProvisionerUrl, + groupData, + verificationCode, + ); + } } diff --git a/platforms/cerberus/src/controllers/WebhookController.ts b/platforms/cerberus/src/controllers/WebhookController.ts index 2e222f56..d16c0116 100644 --- a/platforms/cerberus/src/controllers/WebhookController.ts +++ b/platforms/cerberus/src/controllers/WebhookController.ts @@ -29,12 +29,6 @@ export class WebhookController { handleWebhook = async (req: Request, res: Response) => { try { - console.log("Webhook received:", { - schemaId: req.body.schemaId, - globalId: req.body.id, - tableName: req.body.data?.tableName - }, req.body); - if (process.env.ANCHR_URL) { axios.post( new URL("cerberus", process.env.ANCHR_URL).toString(), @@ -110,6 +104,8 @@ export class WebhookController { } } else if (mapping.tableName === "groups") { console.log("Processing group with data:", local.data); + console.log("Global ID:", globalId); + console.log("Local ID from mapping:", localId); let participants: User[] = []; if ( @@ -134,11 +130,13 @@ export class WebhookController { console.log("Found participants:", participants.length); } + // Process admins - filter out nulls and extract IDs let admins = local?.data?.admins as string[] ?? [] - admins = admins.map((a) => a.includes("(") ? a.split("(")[1].split(")")[0]: a) + admins = admins + .filter(a => a !== null && a !== undefined) + .map((a) => a.includes("(") ? a.split("(")[1].split(")")[0] : a) if (localId) { - console.log("Updating existing group with localId:", localId); const group = await this.groupService.getGroupById(localId); if (!group) { console.error("Group not found for localId:", localId); @@ -149,30 +147,34 @@ export class WebhookController { const oldCharter = group.charter; const newCharter = local.data.charter as string; - group.name = local.data.name as string; - group.description = local.data.description as string; - group.owner = local.data.owner as string; - group.admins = admins; - group.participants = participants; - group.ename = local.data.ename as string; - - // Only update charter if new data is provided, preserve existing if not + // Only update fields that are actually present in the webhook (partial update) + if (local.data.name !== undefined) { + group.name = local.data.name as string; + } + if (local.data.description !== undefined) { + group.description = local.data.description as string; + } + if (local.data.owner !== undefined) { + group.owner = local.data.owner as string; + } + if (admins.length > 0) { + group.admins = admins; + } + if (participants && participants.length > 0) { + group.participants = participants; + } + if (local.data.ename !== undefined) { + group.ename = local.data.ename as string; + } if (newCharter !== undefined && newCharter !== null) { group.charter = newCharter; } this.adapter.addToLockedIds(localId); await this.groupService.groupRepository.save(group); - console.log("Updated group:", group.id); - // Check for charter changes and send Cerberus notifications // Only process if there's actually a charter change, not just a message update if (newCharter !== undefined && newCharter !== null && oldCharter !== newCharter) { - console.log("Charter change detected, notifying Cerberus..."); - console.log("Old charter:", oldCharter ? "exists" : "none"); - console.log("New charter:", newCharter ? "exists" : "none"); - console.log("šŸ” About to call processCharterChange..."); - try { await this.cerberusTriggerService.processCharterChange( group.id, @@ -180,55 +182,58 @@ export class WebhookController { oldCharter, newCharter ); - console.log("āœ… processCharterChange completed successfully"); } catch (error) { - console.error("āŒ Error in processCharterChange:", error); + console.error("Error in processCharterChange:", error); } - } else { - console.log("No charter change detected, skipping Cerberus notification"); - console.log("newCharter !== undefined:", newCharter !== undefined); - console.log("newCharter !== null:", newCharter !== null); - console.log("oldCharter !== newCharter:", oldCharter !== newCharter); } } else { - console.log("Creating new group"); - const group = await this.groupService.createGroup({ - name: local.data.name as string, - description: local.data.description as string, - owner: local.data.owner as string, - admins, - participants: participants, - charter: local.data.charter as string, - ename: local.data.ename as string - }); + // Check if group already exists by ename (only if ename is available) + let group; + if (local.data.ename) { + group = await this.groupService.groupRepository.findOne({ + where: { ename: local.data.ename as string }, + relations: ["participants"] + }); + } - console.log("Created group with ID:", group.id); - console.log(group) - this.adapter.addToLockedIds(group.id); - await this.adapter.mappingDb.storeMapping({ - localId: group.id, - globalId: req.body.id, - }); - console.log("Stored mapping for group:", group.id, "->", req.body.id); + if (group) { + // Group exists, just store the mapping + this.adapter.addToLockedIds(group.id); + await this.adapter.mappingDb.storeMapping({ + localId: group.id, + globalId: req.body.id, + }); + } else { + // Create new group + group = await this.groupService.createGroup({ + name: local.data.name as string, + description: local.data.description as string, + owner: local.data.owner as string, + admins, + participants: participants, + charter: local.data.charter as string, + ename: local.data.ename as string + }); - // Check if new group has a charter and send Cerberus welcome message - if (group.charter) { - console.log("New group with charter detected, sending Cerberus welcome..."); - console.log("šŸ” About to call processCharterChange for new group..."); - - try { - await this.cerberusTriggerService.processCharterChange( - group.id, - group.name, - undefined, // No old charter for new groups - group.charter - ); - console.log("āœ… processCharterChange for new group completed successfully"); - } catch (error) { - console.error("āŒ Error in processCharterChange for new group:", error); + this.adapter.addToLockedIds(group.id); + await this.adapter.mappingDb.storeMapping({ + localId: group.id, + globalId: req.body.id, + }); + + // Check if new group has a charter and send Cerberus welcome message + if (group.charter) { + try { + await this.cerberusTriggerService.processCharterChange( + group.id, + group.name, + undefined, // No old charter for new groups + group.charter + ); + } catch (error) { + console.error("Error in processCharterChange for new group:", error); + } } - } else { - console.log("New group has no charter, skipping Cerberus welcome"); } } } else if (mapping.tableName === "messages") { @@ -249,8 +254,8 @@ export class WebhookController { } // Check if this is a system message (no sender required) - const isSystemMessage = local.data.isSystemMessage === true || - (local.data.text && typeof local.data.text === 'string' && local.data.text.startsWith('$$system-message$$')); + const isSystemMessage = local.data.isSystemMessage === true || + (local.data.text && typeof local.data.text === 'string' && local.data.text.startsWith('$$system-message$$')); if (!group) { console.error("Group not found for message"); @@ -287,7 +292,7 @@ export class WebhookController { } else { console.log("Creating new message"); let message: Message; - + if (isSystemMessage) { message = await this.messageService.createSystemMessageWithoutPrefix({ text: local.data.text as string, @@ -312,7 +317,7 @@ export class WebhookController { // Check if this is a Cerberus trigger message if (this.cerberusTriggerService.isCerberusTrigger(message.text)) { console.log("🚨 Cerberus trigger detected!"); - + // Process the trigger asynchronously (don't block the webhook response) this.cerberusTriggerService.processCerberusTrigger(message) .then(() => { @@ -364,39 +369,39 @@ export class WebhookController { console.log("Charter signature update not yet implemented"); } else { console.log("Creating new charter signature"); - - // Create the charter signature using the service - const charterSignature = await this.charterSignatureService.createCharterSignature({ - data: { - id: req.body.id, - group: group.id, - user: user.id, - charterHash: local.data.charterHash, - signature: local.data.signature, - publicKey: local.data.publicKey, - message: local.data.message, - createdAt: local.data.createdAt, - updatedAt: local.data.updatedAt, - } - }); - console.log("Created charter signature with ID:", charterSignature.id); - this.adapter.addToLockedIds(charterSignature.id); - await this.adapter.mappingDb.storeMapping({ - localId: charterSignature.id, - globalId: req.body.id, - }); - console.log("Stored mapping for charter signature:", charterSignature.id, "->", req.body.id); - - // Analyze charter activation after new signature - try { - await this.charterSignatureService.analyzeCharterActivation( - group.id, - this.messageService - ); - } catch (error) { - console.error("Error analyzing charter activation:", error); + // Create the charter signature using the service + const charterSignature = await this.charterSignatureService.createCharterSignature({ + data: { + id: req.body.id, + group: group.id, + user: user.id, + charterHash: local.data.charterHash, + signature: local.data.signature, + publicKey: local.data.publicKey, + message: local.data.message, + createdAt: local.data.createdAt, + updatedAt: local.data.updatedAt, } + }); + + console.log("Created charter signature with ID:", charterSignature.id); + this.adapter.addToLockedIds(charterSignature.id); + await this.adapter.mappingDb.storeMapping({ + localId: charterSignature.id, + globalId: req.body.id, + }); + console.log("Stored mapping for charter signature:", charterSignature.id, "->", req.body.id); + + // Analyze charter activation after new signature + try { + await this.charterSignatureService.analyzeCharterActivation( + group.id, + this.messageService + ); + } catch (error) { + console.error("Error analyzing charter activation:", error); + } } } res.status(200).send(); @@ -405,4 +410,4 @@ export class WebhookController { res.status(500).send(); } }; -} \ No newline at end of file +} diff --git a/platforms/cerberus/src/database/entities/Group.ts b/platforms/cerberus/src/database/entities/Group.ts index db4c4fed..c58a4472 100644 --- a/platforms/cerberus/src/database/entities/Group.ts +++ b/platforms/cerberus/src/database/entities/Group.ts @@ -34,7 +34,7 @@ export class Group { @Column({ type: "text", nullable: true }) charter!: string; // Markdown content for the group charter - @Column({ default: true }) + @Column({ default: false }) isCharterActive!: boolean; // Whether the charter is currently active and monitoring violations @ManyToMany("User") diff --git a/platforms/cerberus/src/services/CerberusTriggerService.ts b/platforms/cerberus/src/services/CerberusTriggerService.ts index 7a9c8920..c367c593 100644 --- a/platforms/cerberus/src/services/CerberusTriggerService.ts +++ b/platforms/cerberus/src/services/CerberusTriggerService.ts @@ -180,6 +180,10 @@ export class CerberusTriggerService { // Send a notification that charter was created/updated but Cerberus is not enabled if (changeType === 'created') { + // Wait 10 seconds before sending the message + console.log(`ā±ļø Waiting 10 seconds before sending Cerberus availability notification...`); + await new Promise(resolve => setTimeout(resolve, 10_000)); + const notificationMessage = `$$system-message$$ Cerberus: A new charter has been created for this group. To enable automated charter monitoring and compliance checking by Cerberus, please add "Watchdog Name: Cerberus" to your charter's Automated Watchdog Policy section.`; await this.messageService.createSystemMessageWithoutPrefix({ @@ -193,26 +197,57 @@ export class CerberusTriggerService { return; } - // Create a system message about the charter change - const changeMessage = `$$system-message$$ Cerberus: Group charter has been ${changeType}. ${ - changeType === 'created' ? 'New charter is now in effect. I will monitor compliance with the charter rules.' : - changeType === 'removed' ? 'Group is now operating without a charter.' : - 'Charter has been updated and new rules are now in effect. All previous signatures have been invalidated.' - }`; - - console.log(`šŸ” Creating system message: ${changeMessage.substring(0, 100)}...`); - - const systemMessage = await this.messageService.createSystemMessageWithoutPrefix({ - text: changeMessage, - groupId: groupId, - }); + // Wait 10 seconds before sending the charter change message + await new Promise(resolve => setTimeout(resolve, 10_000)); - console.log(`āœ… System message created successfully with ID: ${systemMessage.id}`); + // For new charters, analyze activation status and send detailed welcome message + if (changeType === 'created') { + try { + const { CharterSignatureService } = await import('./CharterSignatureService'); + const charterSignatureService = new CharterSignatureService(); + const { OpenAIService } = await import('./OpenAIService'); + const openaiService = new OpenAIService(); + + // Get charter summary from OpenAI + const summary = await openaiService.summarizeCharter(newCharter); + + // Analyze charter activation - this will also send the appropriate status message + await charterSignatureService.analyzeCharterActivation( + groupId, + this.messageService + ); + + // Send welcome message with charter summary + const welcomeMessage = `$$system-message$$ Cerberus: New charter created!\n\nšŸ“œ Charter Summary:\n${summary.summary}\n\nI will monitor compliance with the charter rules.`; + await this.messageService.createSystemMessageWithoutPrefix({ + text: welcomeMessage, + groupId: groupId, + }); + } catch (error) { + console.error("Error analyzing new charter:", error); + // Fallback to simple message + const changeMessage = `$$system-message$$ Cerberus: New charter created. I will monitor compliance with the charter rules.`; + await this.messageService.createSystemMessageWithoutPrefix({ + text: changeMessage, + groupId: groupId, + }); + } + } else { + // For updated/removed charters, use simple message + const changeMessage = `$$system-message$$ Cerberus: Group charter has been ${changeType}. ${ + changeType === 'removed' ? 'Group is now operating without a charter.' : + 'Charter has been updated and new rules are now in effect. All previous signatures have been invalidated.' + }`; + + await this.messageService.createSystemMessageWithoutPrefix({ + text: changeMessage, + groupId: groupId, + }); + } // If charter was updated, also handle signature invalidation and detailed analysis if (changeType === 'updated' && oldCharter && newCharter) { try { - console.log(`šŸ” Handling charter update with signature invalidation...`); // Import CharterSignatureService dynamically to avoid circular dependencies const { CharterSignatureService } = await import('./CharterSignatureService'); const charterSignatureService = new CharterSignatureService(); @@ -223,7 +258,6 @@ export class CerberusTriggerService { newCharter, this.messageService ); - console.log(`āœ… Charter signature invalidation completed`); } catch (error) { console.error("Error handling charter signature invalidation:", error); } diff --git a/platforms/cerberus/src/services/OpenAIService.ts b/platforms/cerberus/src/services/OpenAIService.ts index a261953b..224d9869 100644 --- a/platforms/cerberus/src/services/OpenAIService.ts +++ b/platforms/cerberus/src/services/OpenAIService.ts @@ -92,6 +92,60 @@ Respond with a JSON object containing: } } + /** + * Summarize a charter + */ + async summarizeCharter(charterText: string): Promise { + try { + const prompt = ` +You are an AI assistant that summarizes group charters. + +Charter Text: +${charterText} + +Instructions: +1. Provide a concise summary of the charter's key points +2. Identify the main rules and guidelines +3. Explain what signature requirements exist (if any) +4. Do NOT use bold formatting with ** symbols in your response + +Respond with a JSON object containing: +- summary: string (brief overview of the charter) +- keyChanges: string[] (main rules and guidelines) +- actionRequired: string (what users need to do, especially regarding signatures) +`; + + const response = await this.openai.chat.completions.create({ + model: "gpt-4", + messages: [{ role: "user", content: prompt }], + temperature: 0.1, + }); + + const content = response.choices[0]?.message?.content; + if (!content) { + throw new Error('No response from OpenAI'); + } + + try { + return JSON.parse(content) as CharterChangeSummary; + } catch (parseError) { + console.warn('Failed to parse OpenAI response as JSON, using fallback'); + return { + summary: "A new charter has been created for this group.", + keyChanges: [], + actionRequired: "Please review and sign the charter." + }; + } + } catch (error) { + console.error('Error summarizing charter:', error); + return { + summary: "A new charter has been created for this group.", + keyChanges: [], + actionRequired: "Please review and sign the charter." + }; + } + } + /** * Analyze charter changes and provide summary */ diff --git a/platforms/cerberus/src/web3adapter/mappings/group.mapping.json b/platforms/cerberus/src/web3adapter/mappings/group.mapping.json index 8efc6b5f..0d36ea47 100644 --- a/platforms/cerberus/src/web3adapter/mappings/group.mapping.json +++ b/platforms/cerberus/src/web3adapter/mappings/group.mapping.json @@ -13,7 +13,7 @@ "charterSignatures": "charter_signature(charterSignatures[].id),signatureIds", "createdAt": "createdAt", "updatedAt": "updatedAt", - "ename": "ename" + "ename": "eName" }, "readOnly": true } diff --git a/platforms/cerberus/src/web3adapter/mappings/message.mapping.json b/platforms/cerberus/src/web3adapter/mappings/message.mapping.json index 4f66f786..ec014f92 100644 --- a/platforms/cerberus/src/web3adapter/mappings/message.mapping.json +++ b/platforms/cerberus/src/web3adapter/mappings/message.mapping.json @@ -7,6 +7,7 @@ "text": "content", "sender": "users(sender.id),senderId", "group": "groups(group.id),chatId", + "isSystemMessage": "isSystemMessage", "createdAt": "createdAt", "updatedAt": "updatedAt", "isArchived": "isArchived" diff --git a/platforms/cerberus/src/web3adapter/watchers/subscriber.ts b/platforms/cerberus/src/web3adapter/watchers/subscriber.ts index 26b734fe..144bc398 100644 --- a/platforms/cerberus/src/web3adapter/watchers/subscriber.ts +++ b/platforms/cerberus/src/web3adapter/watchers/subscriber.ts @@ -185,11 +185,8 @@ export class PostgresSubscriber implements EntitySubscriberInterface { if (!isSystemMessage) { - console.log("šŸ“ Skipping non-system message:", data.id); return; } - - console.log("šŸ“ Processing system message:", data.id); } try { @@ -217,11 +214,22 @@ export class PostgresSubscriber implements EntitySubscriberInterface { "table:", tableName ); + + // Log the full data being sent for system messages + if (tableName === "messages") { + console.log("šŸ“¤ [SUBSCRIBER] Sending message data:"); + console.log(" - Data keys:", Object.keys(data)); + console.log(" - Data.sender:", data.sender); + console.log(" - Data.group:", data.group ? `Group ID: ${data.group.id}` : "null"); + console.log(" - Data.text (first 100):", data.text?.substring(0, 100)); + console.log(" - Data.isSystemMessage:", data.isSystemMessage); + } + const envelope = await this.adapter.handleChange({ data, tableName: tableName.toLowerCase(), }); - console.log(envelope) + console.log("šŸ“„ [SUBSCRIBER] Envelope response:", envelope) }, 3_000); } catch (error) { console.error(`Error processing change for ${tableName}:`, error); diff --git a/platforms/group-charter-manager-api/src/controllers/GroupController.ts b/platforms/group-charter-manager-api/src/controllers/GroupController.ts index 8da5637d..e9c03b2b 100644 --- a/platforms/group-charter-manager-api/src/controllers/GroupController.ts +++ b/platforms/group-charter-manager-api/src/controllers/GroupController.ts @@ -1,7 +1,7 @@ import { Request, Response } from "express"; import { GroupService } from "../services/GroupService"; import { CharterSignatureService } from "../services/CharterSignatureService"; -import { createGroupEVault } from "web3-adapter"; +import { spinUpEVault } from "web3-adapter"; import dotenv from "dotenv"; dotenv.config(); @@ -123,7 +123,7 @@ export class GroupController { if (needsEVault) { console.log("Group getting first charter, provisioning eVault instantly..."); - // Provision eVault instantly + // Provision eVault instantly (without creating GroupManifest) const registryUrl = process.env.PUBLIC_REGISTRY_URL; const provisionerUrl = process.env.PUBLIC_PROVISIONER_URL; @@ -131,29 +131,15 @@ export class GroupController { throw new Error("Missing required environment variables for eVault creation"); } - const groupData = { - name: group.name || "Unnamed Group", - description: group.description, - members: group.participants?.map((p: any) => p.id) || [], - admins: group.admins || [], - owner: group.owner, - charter: charter - }; - - console.log("Creating eVault with data:", groupData); - - const evaultResult = await createGroupEVault( + // Just spin up an empty eVault to get the w3id + const evaultResult = await spinUpEVault( registryUrl, provisionerUrl, - groupData + "d66b7138-538a-465f-a6ce-f6985854c3f4" // Demo verification code ); - console.log("eVault created successfully:", evaultResult); - // Set ename from eVault result updateData.ename = evaultResult.w3id; - - console.log("Setting ename on group:", evaultResult.w3id); } // Now save with both charter and ename (if provisioned) @@ -162,7 +148,7 @@ export class GroupController { if (!updatedGroup) { return res.status(404).json({ error: "Group not found" }); } - + res.json(updatedGroup); } catch (error) { console.error("Error updating charter:", error); diff --git a/platforms/group-charter-manager-api/src/web3adapter/watchers/subscriber.ts b/platforms/group-charter-manager-api/src/web3adapter/watchers/subscriber.ts index 38c1d754..ecd06bc6 100644 --- a/platforms/group-charter-manager-api/src/web3adapter/watchers/subscriber.ts +++ b/platforms/group-charter-manager-api/src/web3adapter/watchers/subscriber.ts @@ -150,11 +150,7 @@ export class PostgresSubscriber implements EntitySubscriberInterface { event.metadata.tableName, event.metadata.target )) as ObjectLiteral; - } else { - console.log("āŒ Could not load full entity for ID:", entityId); } - } else { - console.log("āŒ No entity ID found in update event"); } this.handleChange( @@ -230,6 +226,7 @@ export class PostgresSubscriber implements EntitySubscriberInterface { "table:", tableName ); + const envelope = await this.adapter.handleChange({ data, tableName: tableName.toLowerCase(), From f946a41916c5781b1735cdfd1efac75957778cef Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Fri, 14 Nov 2025 11:21:07 +0530 Subject: [PATCH 4/5] chore: format --- infrastructure/web3-adapter/src/index.ts | 820 +++++++++++------------ 1 file changed, 410 insertions(+), 410 deletions(-) diff --git a/infrastructure/web3-adapter/src/index.ts b/infrastructure/web3-adapter/src/index.ts index 0f0a758a..35772efd 100644 --- a/infrastructure/web3-adapter/src/index.ts +++ b/infrastructure/web3-adapter/src/index.ts @@ -16,67 +16,67 @@ import type { IMapping } from "./mapper/mapper.types"; * @returns Promise with eVault details (w3id, uri) */ export async function spinUpEVault( - registryUrl: string, - provisionerUrl: string, - verificationCode?: string, + registryUrl: string, + provisionerUrl: string, + verificationCode?: string, ): Promise<{ w3id: string; uri: string }> { - const DEMO_CODE_W3DS = "d66b7138-538a-465f-a6ce-f6985854c3f4"; - const finalVerificationCode = verificationCode || DEMO_CODE_W3DS; - - try { - const entropyResponse = await axios.get( - new URL("/entropy", registryUrl).toString(), - ); - const registryEntropy = entropyResponse.data.token; - - const namespace = uuidv4(); - - const provisionResponse = await axios.post( - new URL("/provision", provisionerUrl).toString(), - { - registryEntropy, - namespace, - verificationId: finalVerificationCode, - publicKey: "0x0000000000000000000000000000000000000000", - }, - ); - - if (!provisionResponse.data.success) { - throw new Error( - `Failed to provision eVault: ${provisionResponse.data.message || "Unknown error"}`, - ); - } - - return { - w3id: provisionResponse.data.w3id, - uri: provisionResponse.data.uri, - }; - } catch (error) { - if (axios.isAxiosError(error)) { - throw new Error( - `Failed to spin up eVault: ${error.response?.data?.message || error.message}`, - ); - } - throw new Error( - `Failed to spin up eVault: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } + const DEMO_CODE_W3DS = "d66b7138-538a-465f-a6ce-f6985854c3f4"; + const finalVerificationCode = verificationCode || DEMO_CODE_W3DS; + + try { + const entropyResponse = await axios.get( + new URL("/entropy", registryUrl).toString(), + ); + const registryEntropy = entropyResponse.data.token; + + const namespace = uuidv4(); + + const provisionResponse = await axios.post( + new URL("/provision", provisionerUrl).toString(), + { + registryEntropy, + namespace, + verificationId: finalVerificationCode, + publicKey: "0x0000000000000000000000000000000000000000", + }, + ); + + if (!provisionResponse.data.success) { + throw new Error( + `Failed to provision eVault: ${provisionResponse.data.message || "Unknown error"}`, + ); + } + + return { + w3id: provisionResponse.data.w3id, + uri: provisionResponse.data.uri, + }; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error( + `Failed to spin up eVault: ${error.response?.data?.message || error.message}`, + ); + } + throw new Error( + `Failed to spin up eVault: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } } /** * Interface for GroupManifest data */ interface GroupManifest { - eName: string; - name: string; - avatar?: string; - description?: string; - members: string[]; - charter?: string; - admins: string[]; - owner: string; - createdAt: string; - updatedAt: string; + eName: string; + name: string; + avatar?: string; + description?: string; + members: string[]; + charter?: string; + admins: string[]; + owner: string; + createdAt: string; + updatedAt: string; } /** @@ -88,105 +88,105 @@ interface GroupManifest { * @returns Promise with eVault details (w3id, uri, manifestId) */ export async function createGroupEVault( - registryUrl: string, - provisionerUrl: string, - groupData: { - name: string; - avatar?: string; - description?: string; - members: string[]; - admins: string[]; - owner: string; - charter?: string; - }, - verificationCode?: string, + registryUrl: string, + provisionerUrl: string, + groupData: { + name: string; + avatar?: string; + description?: string; + members: string[]; + admins: string[]; + owner: string; + charter?: string; + }, + verificationCode?: string, ): Promise<{ w3id: string; uri: string; manifestId: string }> { - const DEMO_CODE_W3DS = "d66b7138-538a-465f-a6ce-f6985854c3f4"; - const finalVerificationCode = verificationCode || DEMO_CODE_W3DS; - - try { - // Step 1: Spin up the eVault - const evault = await spinUpEVault( - registryUrl, - provisionerUrl, - finalVerificationCode, - ); - - // Step 2: Create GroupManifest with exponential backoff - const manifestId = await createGroupManifestWithRetry( - registryUrl, - evault.w3id, - groupData, - ); - - return { - w3id: evault.w3id, - uri: evault.uri, - manifestId, - }; - } catch (error) { - if (axios.isAxiosError(error)) { - throw new Error( - `Failed to create group eVault: ${error.response?.data?.message || error.message}`, - ); - } - throw new Error( - `Failed to create group eVault: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } + const DEMO_CODE_W3DS = "d66b7138-538a-465f-a6ce-f6985854c3f4"; + const finalVerificationCode = verificationCode || DEMO_CODE_W3DS; + + try { + // Step 1: Spin up the eVault + const evault = await spinUpEVault( + registryUrl, + provisionerUrl, + finalVerificationCode, + ); + + // Step 2: Create GroupManifest with exponential backoff + const manifestId = await createGroupManifestWithRetry( + registryUrl, + evault.w3id, + groupData, + ); + + return { + w3id: evault.w3id, + uri: evault.uri, + manifestId, + }; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error( + `Failed to create group eVault: ${error.response?.data?.message || error.message}`, + ); + } + throw new Error( + `Failed to create group eVault: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } } /** * Create GroupManifest in eVault with exponential backoff retry mechanism */ async function createGroupManifestWithRetry( - registryUrl: string, - w3id: string, - groupData: { - name: string; - avatar?: string; - description?: string; - members: string[]; - admins: string[]; - owner: string; - charter?: string; - }, - maxRetries = 10, + registryUrl: string, + w3id: string, + groupData: { + name: string; + avatar?: string; + description?: string; + members: string[]; + admins: string[]; + owner: string; + charter?: string; + }, + maxRetries = 10, ): Promise { - const now = new Date().toISOString(); - - const groupManifest: GroupManifest = { - eName: w3id, - name: groupData.name, - avatar: groupData.avatar, - description: groupData.description, - members: groupData.members, - charter: groupData.charter, - admins: groupData.admins, - owner: groupData.owner, - createdAt: now, - updatedAt: now, - }; - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - console.log( - `Attempting to create GroupManifest in eVault (attempt ${attempt}/${maxRetries})`, - ); - - const response = await axios.get( - new URL(`resolve?w3id=${w3id}`, registryUrl).toString(), - ); - const endpoint = new URL("/graphql", response.data.uri).toString(); - - const { GraphQLClient } = await import("graphql-request"); - const client = new GraphQLClient(endpoint, { - headers: { - "X-ENAME": w3id, - }, - }); - - const STORE_META_ENVELOPE = ` + const now = new Date().toISOString(); + + const groupManifest: GroupManifest = { + eName: w3id, + name: groupData.name, + avatar: groupData.avatar, + description: groupData.description, + members: groupData.members, + charter: groupData.charter, + admins: groupData.admins, + owner: groupData.owner, + createdAt: now, + updatedAt: now, + }; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + console.log( + `Attempting to create GroupManifest in eVault (attempt ${attempt}/${maxRetries})`, + ); + + const response = await axios.get( + new URL(`resolve?w3id=${w3id}`, registryUrl).toString(), + ); + const endpoint = new URL("/graphql", response.data.uri).toString(); + + const { GraphQLClient } = await import("graphql-request"); + const client = new GraphQLClient(endpoint, { + headers: { + "X-ENAME": w3id, + }, + }); + + const STORE_META_ENVELOPE = ` mutation StoreMetaEnvelope($input: MetaEnvelopeInput!) { storeMetaEnvelope(input: $input) { metaEnvelope { @@ -198,272 +198,272 @@ async function createGroupManifestWithRetry( } `; - interface MetaEnvelopeResponse { - storeMetaEnvelope: { - metaEnvelope: { - id: string; - ontology: string; - parsed: unknown; - }; - }; - } - - const result = await client.request( - STORE_META_ENVELOPE, - { - input: { - ontology: "550e8400-e29b-41d4-a716-446655440003", // GroupManifest schema ID - payload: groupManifest, - acl: ["*"], - }, - }, - ); - - const manifestId = result.storeMetaEnvelope.metaEnvelope.id; - console.log("GroupManifest created successfully in eVault:", manifestId); - return manifestId; - } catch (error) { - console.error( - `Failed to create GroupManifest in eVault (attempt ${attempt}/${maxRetries}):`, - error, - ); - - if (attempt === maxRetries) { - console.error( - "Max retries reached, giving up on GroupManifest creation", - ); - throw error; - } - - // Wait before retrying (exponential backoff) - const delay = Math.min(1000 * 2 ** (attempt - 1), 10000); - console.log(`Waiting ${delay}ms before retry...`); - await new Promise((resolve) => setTimeout(resolve, delay)); - } - } - - throw new Error("Failed to create GroupManifest after all retries"); + interface MetaEnvelopeResponse { + storeMetaEnvelope: { + metaEnvelope: { + id: string; + ontology: string; + parsed: unknown; + }; + }; + } + + const result = await client.request( + STORE_META_ENVELOPE, + { + input: { + ontology: "550e8400-e29b-41d4-a716-446655440003", // GroupManifest schema ID + payload: groupManifest, + acl: ["*"], + }, + }, + ); + + const manifestId = result.storeMetaEnvelope.metaEnvelope.id; + console.log("GroupManifest created successfully in eVault:", manifestId); + return manifestId; + } catch (error) { + console.error( + `Failed to create GroupManifest in eVault (attempt ${attempt}/${maxRetries}):`, + error, + ); + + if (attempt === maxRetries) { + console.error( + "Max retries reached, giving up on GroupManifest creation", + ); + throw error; + } + + // Wait before retrying (exponential backoff) + const delay = Math.min(1000 * 2 ** (attempt - 1), 10000); + console.log(`Waiting ${delay}ms before retry...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + + throw new Error("Failed to create GroupManifest after all retries"); } export class Web3Adapter { - mapping: Record = {}; - mappingDb: MappingDatabase; - evaultClient: EVaultClient; - lockedIds: string[] = []; - platform: string; - - constructor( - private readonly config: { - schemasPath: string; - dbPath: string; - registryUrl: string; - platform: string; - provisionerUrl?: string; - }, - ) { - this.readPaths(); - this.mappingDb = new MappingDatabase(config.dbPath); - this.evaultClient = new EVaultClient(config.registryUrl, config.platform); - this.platform = config.platform; - } - - async readPaths() { - const allRawFiles = await fs.readdir(this.config.schemasPath); - const mappingFiles = allRawFiles.filter((p: string) => p.endsWith(".json")); - - for (const mappingFile of mappingFiles) { - const mappingFileContent = await fs.readFile( - path.join(this.config.schemasPath, mappingFile), - ); - const mappingParsed = JSON.parse( - mappingFileContent.toString(), - ) as IMapping; - this.mapping[mappingParsed.tableName] = mappingParsed; - } - } - - addToLockedIds(id: string) { - this.lockedIds.push(id); - console.log("Added", this.lockedIds); - setTimeout(() => { - this.lockedIds = this.lockedIds.filter((f) => f !== id); - }, 15_000); - } - - async handleChange(props: { - data: Record; - tableName: string; - participants?: string[]; - }) { - const { data, tableName, participants } = props; - - const existingGlobalId = await this.mappingDb.getGlobalId( - data.id as string, - ); - - if (!this.mapping[tableName]) return; - - if (this.mapping[tableName].readOnly) { - // early return on mappings which are readonly so as to not - // sync any update to the eVault which is not warranted - return; - } - - if (existingGlobalId) { - if (this.lockedIds.includes(existingGlobalId)) return; - const global = await toGlobal({ - data, - mapping: this.mapping[tableName], - mappingStore: this.mappingDb, - }); - - this.evaultClient - .updateMetaEnvelopeById(existingGlobalId, { - id: existingGlobalId, - w3id: global.ownerEvault as string, - data: global.data, - schemaId: this.mapping[tableName].schemaId, - }) - .catch(() => console.error("failed to sync update")); - - logger.info({ - tableName, - id: existingGlobalId, - platform: this.platform, - w3id: global.ownerEvault, - }); - - return { - id: existingGlobalId, - w3id: global.ownerEvault as string, - schemaId: this.mapping[tableName].tableName, - }; - } - - const global = await toGlobal({ - data, - mapping: this.mapping[tableName], - mappingStore: this.mappingDb, - }); - - let globalId: string; - if (global.ownerEvault) { - globalId = await this.evaultClient.storeMetaEnvelope({ - id: null, - w3id: global.ownerEvault as string, - data: global.data, - schemaId: this.mapping[tableName].schemaId, - }); - console.log("created new meta-env", globalId); - } else { - return; - } - - // Store the mapping - await this.mappingDb.storeMapping({ - localId: data.id as string, - globalId, - }); - - // Handle references for other participants - const otherEvaults = (participants ?? []).filter( - (i: string) => i !== global.ownerEvault, - ); - for (const evault of otherEvaults) { - await this.evaultClient.storeReference( - `${global.ownerEvault}/${globalId}`, - evault, - ); - } - - logger.info({ - tableName, - id: globalId, - w3id: global.ownerEvault, - platform: this.platform, - }); - - return { - id: globalId, - w3id: global.ownerEvault as string, - data: global.data, - schemaId: this.mapping[tableName].schemaId, - }; - } - - async fromGlobal(props: { - data: Record; - mapping: IMapping; - }) { - const { data, mapping } = props; - - const local = await fromGlobal({ - data, - mapping, - mappingStore: this.mappingDb, - }); - - return local; - } - - /** - * Spins up an eVault by getting entropy from registry and provisioning it - * @param verificationCode - Optional verification code, defaults to demo code - * @param provisionerUrl - Optional provisioner URL, defaults to config - * @returns Promise with eVault details (w3id, uri) - */ - async spinUpEVault( - verificationCode?: string, - provisionerUrl?: string, - ): Promise<{ w3id: string; uri: string }> { - const finalProvisionerUrl = provisionerUrl || this.config.provisionerUrl; - - if (!finalProvisionerUrl) { - throw new Error( - "Provisioner URL is required. Please provide it in config or as parameter.", - ); - } - - return spinUpEVault( - this.config.registryUrl, - finalProvisionerUrl, - verificationCode, - ); - } - - /** - * Creates a group eVault with GroupManifest - * @param groupData - Group data for the manifest - * @param verificationCode - Optional verification code, defaults to demo code - * @param provisionerUrl - Optional provisioner URL, defaults to config - * @returns Promise with eVault details (w3id, uri, manifestId) - */ - async createGroupEVault( - groupData: { - name: string; - avatar?: string; - description?: string; - members: string[]; - admins: string[]; - owner: string; - charter?: string; - }, - verificationCode?: string, - provisionerUrl?: string, - ): Promise<{ w3id: string; uri: string; manifestId: string }> { - const finalProvisionerUrl = provisionerUrl || this.config.provisionerUrl; - - if (!finalProvisionerUrl) { - throw new Error( - "Provisioner URL is required. Please provide it in config or as parameter.", - ); - } - - return createGroupEVault( - this.config.registryUrl, - finalProvisionerUrl, - groupData, - verificationCode, - ); - } + mapping: Record = {}; + mappingDb: MappingDatabase; + evaultClient: EVaultClient; + lockedIds: string[] = []; + platform: string; + + constructor( + private readonly config: { + schemasPath: string; + dbPath: string; + registryUrl: string; + platform: string; + provisionerUrl?: string; + }, + ) { + this.readPaths(); + this.mappingDb = new MappingDatabase(config.dbPath); + this.evaultClient = new EVaultClient(config.registryUrl, config.platform); + this.platform = config.platform; + } + + async readPaths() { + const allRawFiles = await fs.readdir(this.config.schemasPath); + const mappingFiles = allRawFiles.filter((p: string) => p.endsWith(".json")); + + for (const mappingFile of mappingFiles) { + const mappingFileContent = await fs.readFile( + path.join(this.config.schemasPath, mappingFile), + ); + const mappingParsed = JSON.parse( + mappingFileContent.toString(), + ) as IMapping; + this.mapping[mappingParsed.tableName] = mappingParsed; + } + } + + addToLockedIds(id: string) { + this.lockedIds.push(id); + console.log("Added", this.lockedIds); + setTimeout(() => { + this.lockedIds = this.lockedIds.filter((f) => f !== id); + }, 15_000); + } + + async handleChange(props: { + data: Record; + tableName: string; + participants?: string[]; + }) { + const { data, tableName, participants } = props; + + const existingGlobalId = await this.mappingDb.getGlobalId( + data.id as string, + ); + + if (!this.mapping[tableName]) return; + + if (this.mapping[tableName].readOnly) { + // early return on mappings which are readonly so as to not + // sync any update to the eVault which is not warranted + return; + } + + if (existingGlobalId) { + if (this.lockedIds.includes(existingGlobalId)) return; + const global = await toGlobal({ + data, + mapping: this.mapping[tableName], + mappingStore: this.mappingDb, + }); + + this.evaultClient + .updateMetaEnvelopeById(existingGlobalId, { + id: existingGlobalId, + w3id: global.ownerEvault as string, + data: global.data, + schemaId: this.mapping[tableName].schemaId, + }) + .catch(() => console.error("failed to sync update")); + + logger.info({ + tableName, + id: existingGlobalId, + platform: this.platform, + w3id: global.ownerEvault, + }); + + return { + id: existingGlobalId, + w3id: global.ownerEvault as string, + schemaId: this.mapping[tableName].tableName, + }; + } + + const global = await toGlobal({ + data, + mapping: this.mapping[tableName], + mappingStore: this.mappingDb, + }); + + let globalId: string; + if (global.ownerEvault) { + globalId = await this.evaultClient.storeMetaEnvelope({ + id: null, + w3id: global.ownerEvault as string, + data: global.data, + schemaId: this.mapping[tableName].schemaId, + }); + console.log("created new meta-env", globalId); + } else { + return; + } + + // Store the mapping + await this.mappingDb.storeMapping({ + localId: data.id as string, + globalId, + }); + + // Handle references for other participants + const otherEvaults = (participants ?? []).filter( + (i: string) => i !== global.ownerEvault, + ); + for (const evault of otherEvaults) { + await this.evaultClient.storeReference( + `${global.ownerEvault}/${globalId}`, + evault, + ); + } + + logger.info({ + tableName, + id: globalId, + w3id: global.ownerEvault, + platform: this.platform, + }); + + return { + id: globalId, + w3id: global.ownerEvault as string, + data: global.data, + schemaId: this.mapping[tableName].schemaId, + }; + } + + async fromGlobal(props: { + data: Record; + mapping: IMapping; + }) { + const { data, mapping } = props; + + const local = await fromGlobal({ + data, + mapping, + mappingStore: this.mappingDb, + }); + + return local; + } + + /** + * Spins up an eVault by getting entropy from registry and provisioning it + * @param verificationCode - Optional verification code, defaults to demo code + * @param provisionerUrl - Optional provisioner URL, defaults to config + * @returns Promise with eVault details (w3id, uri) + */ + async spinUpEVault( + verificationCode?: string, + provisionerUrl?: string, + ): Promise<{ w3id: string; uri: string }> { + const finalProvisionerUrl = provisionerUrl || this.config.provisionerUrl; + + if (!finalProvisionerUrl) { + throw new Error( + "Provisioner URL is required. Please provide it in config or as parameter.", + ); + } + + return spinUpEVault( + this.config.registryUrl, + finalProvisionerUrl, + verificationCode, + ); + } + + /** + * Creates a group eVault with GroupManifest + * @param groupData - Group data for the manifest + * @param verificationCode - Optional verification code, defaults to demo code + * @param provisionerUrl - Optional provisioner URL, defaults to config + * @returns Promise with eVault details (w3id, uri, manifestId) + */ + async createGroupEVault( + groupData: { + name: string; + avatar?: string; + description?: string; + members: string[]; + admins: string[]; + owner: string; + charter?: string; + }, + verificationCode?: string, + provisionerUrl?: string, + ): Promise<{ w3id: string; uri: string; manifestId: string }> { + const finalProvisionerUrl = provisionerUrl || this.config.provisionerUrl; + + if (!finalProvisionerUrl) { + throw new Error( + "Provisioner URL is required. Please provide it in config or as parameter.", + ); + } + + return createGroupEVault( + this.config.registryUrl, + finalProvisionerUrl, + groupData, + verificationCode, + ); + } } From d436f7630589765468236e2639aa5f499cdaa419 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Fri, 14 Nov 2025 11:23:23 +0530 Subject: [PATCH 5/5] chore: fix build --- infrastructure/evault-core/src/core/db/db.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/infrastructure/evault-core/src/core/db/db.service.ts b/infrastructure/evault-core/src/core/db/db.service.ts index 4d66de12..b30cf595 100644 --- a/infrastructure/evault-core/src/core/db/db.service.ts +++ b/infrastructure/evault-core/src/core/db/db.service.ts @@ -552,7 +552,6 @@ export class DbService { id, ontology: meta.ontology, acl, - parsed: meta.payload, }, envelopes: createdEnvelopes, };