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/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..35772efd 100644 --- a/infrastructure/web3-adapter/src/index.ts +++ b/infrastructure/web3-adapter/src/index.ts @@ -212,7 +212,7 @@ async function createGroupManifestWithRetry( STORE_META_ENVELOPE, { input: { - ontology: "550e8400-e29b-41d4-a716-446655440001", // GroupManifest schema ID + ontology: "550e8400-e29b-41d4-a716-446655440003", // GroupManifest schema ID payload: groupManifest, acl: ["*"], }, 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/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 8b610e21..c367c593 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,26 +171,83 @@ export class CerberusTriggerService { console.log(`šŸ” Change type determined: ${changeType}`); - // 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 === 'removed' ? 'Group is now operating without a charter.' : - 'Charter has been updated and new rules are now in effect.' - }`; - - console.log(`šŸ” Creating system message: ${changeMessage.substring(0, 100)}...`); + // 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') { + // 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({ + text: notificationMessage, + groupId: groupId, + }); + + console.log(`āœ… Cerberus availability notification sent for new charter`); + } + + return; + } - 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(); @@ -192,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/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 fd3e34ed..e9c03b2b 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 { spinUpEVault } from "web3-adapter"; +import dotenv from "dotenv"; + +dotenv.config(); export class GroupController { private groupService = new GroupService(); @@ -110,12 +114,41 @@ 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 (without creating GroupManifest) + 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"); + } + + // Just spin up an empty eVault to get the w3id + const evaultResult = await spinUpEVault( + registryUrl, + provisionerUrl, + "d66b7138-538a-465f-a6ce-f6985854c3f4" // Demo verification code + ); + + // Set ename from eVault result + updateData.ename = 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" }); } - + res.json(updatedGroup); } catch (error) { console.error("Error updating charter:", error); 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 +} + 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..ecd06bc6 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,27 +145,12 @@ 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, 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( @@ -242,6 +226,7 @@ export class PostgresSubscriber implements EntitySubscriberInterface { "table:", tableName ); + const envelope = await this.adapter.handleChange({ data, tableName: tableName.toLowerCase(), @@ -326,77 +311,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 */