feat: Add presence heartbeat for Matrix online status

- matrix-gateway: POST /internal/matrix/presence/online endpoint
- usePresenceHeartbeat hook with activity tracking
- Auto away after 5 min inactivity
- Offline on page close/visibility change
- Integrated in MatrixChatRoom component
This commit is contained in:
Apple
2025-11-27 00:19:40 -08:00
parent 5bed515852
commit 3de3c8cb36
6371 changed files with 1317450 additions and 932 deletions

View File

@@ -0,0 +1,53 @@
/**
* Agents Routes (MVP Stub)
* Based on: api-mvp.md
*
* Endpoints:
* - POST /api/v1/dao/{dao_id}/agents/{agent_id}/invoke - Invoke agent
*/
import { Router } from 'express';
import { pdpService } from '../../services/pdp/pdp.service';
export const agentsRoutes = Router();
// POST /api/v1/dao/{dao_id}/agents/{agent_id}/invoke - Invoke agent
agentsRoutes.post('/:daoId/agents/:agentId/invoke', async (req, res) => {
try {
const userId = (req as any).userId;
const { daoId, agentId } = req.params;
const { input, metadata } = req.body;
// Check PDP policy
const pdpResult = await pdpService.check(
'policy.agent.run',
{ agentId },
{ userId, daoId }
);
if (pdpResult.decision !== 'allow') {
res.status(403).json({
error: 'ACCESS_DENIED',
message: pdpResult.reason || 'PDP denied',
});
return;
}
// MVP: Return stub response
// TODO: Implement actual agent invocation
const runId = `run_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
res.json({
run_id: runId,
status: 'queued',
output: null,
});
} catch (error: any) {
res.status(500).json({
error: 'INTERNAL_ERROR',
message: error.message,
});
}
});

View File

@@ -0,0 +1,78 @@
/**
* DAO Routes
* Based on: api-mvp.md
*
* Endpoints:
* - POST /api/v1/dao - Create DAO
* - GET /api/v1/dao/{dao_id} - Get DAO by ID
* - GET /api/v1/dao - List DAOs
*/
import { Router } from 'express';
import { daoFactoryService } from '../../services/dao-factory/dao-factory.service';
import { registryService } from '../../services/registry/registry.service';
import type { CreateDaoInput } from '../../domain/dao/types';
export const daoRoutes = Router();
// POST /api/v1/dao - Create DAO
daoRoutes.post('/', async (req, res) => {
try {
const userId = (req as any).userId;
const input: CreateDaoInput = req.body;
const result = await daoFactoryService.createDao(userId, input);
res.status(201).json(result);
} catch (error: any) {
res.status(400).json({
error: error.message || 'BAD_REQUEST',
message: error.message,
});
}
});
// GET /api/v1/dao/{dao_id} - Get DAO by ID
daoRoutes.get('/:daoId', async (req, res) => {
try {
const { daoId } = req.params;
const dao = await registryService.getDaoById(daoId);
if (!dao) {
res.status(404).json({
error: 'NOT_FOUND',
message: `DAO ${daoId} not found`,
});
return;
}
res.json(dao);
} catch (error: any) {
res.status(500).json({
error: 'INTERNAL_ERROR',
message: error.message,
});
}
});
// GET /api/v1/dao - List DAOs
daoRoutes.get('/', async (req, res) => {
try {
const { level, type } = req.query;
const filter = {
level: level as string | undefined,
type: type as string | undefined,
};
const daos = await registryService.listDaos(filter);
res.json({ items: daos });
} catch (error: any) {
res.status(500).json({
error: 'INTERNAL_ERROR',
message: error.message,
});
}
});

View File

@@ -0,0 +1,38 @@
/**
* PDP Routes
* Based on: api-mvp.md
*
* Endpoints:
* - POST /api/v1/pdp/check - Check policy
*/
import { Router } from 'express';
import { pdpService } from '../../services/pdp/pdp.service';
import type { PdpRequest } from '../../domain/pdp/policy.model';
export const pdpRoutes = Router();
// POST /api/v1/pdp/check - Check policy
pdpRoutes.post('/check', async (req, res) => {
try {
const userId = (req as any).userId;
const { policy, resource, context }: PdpRequest = req.body;
const result = await pdpService.check(policy, resource, {
...context,
userId: context.userId || userId,
});
res.json({
decision: result.decision,
reason: result.reason || null,
});
} catch (error: any) {
res.status(500).json({
error: 'INTERNAL_ERROR',
message: error.message,
});
}
});

View File

@@ -0,0 +1,48 @@
/**
* Platforms Routes
* Based on: api-mvp.md
*
* Endpoints:
* - POST /api/v1/platforms - Create platform
* - GET /api/v1/platforms - List platforms
*/
import { Router } from 'express';
import { daoFactoryService } from '../../services/dao-factory/dao-factory.service';
import { registryService } from '../../services/registry/registry.service';
import type { CreatePlatformInput } from '../../domain/dao/types';
export const platformsRoutes = Router();
// POST /api/v1/platforms - Create platform
platformsRoutes.post('/', async (req, res) => {
try {
const userId = (req as any).userId;
const input: CreatePlatformInput = req.body;
const result = await daoFactoryService.createPlatform(userId, input);
res.status(201).json(result);
} catch (error: any) {
res.status(400).json({
error: error.message || 'BAD_REQUEST',
message: error.message,
});
}
});
// GET /api/v1/platforms - List platforms
platformsRoutes.get('/', async (req, res) => {
try {
const platforms = await registryService.listPlatforms();
res.json({ items: platforms });
} catch (error: any) {
res.status(500).json({
error: 'INTERNAL_ERROR',
message: error.message,
});
}
});

View File

@@ -0,0 +1,156 @@
/**
* Teams Routes (MicroDAO)
* Based on: api-mvp.md, updated for MicroDAO creation with balance checks
*
* Endpoints:
* - POST /api/v1/teams - Create MicroDAO (requires 1 DAARION on balance)
* - GET /api/v1/teams - List teams/MicroDAO
* - GET /api/v1/teams/:teamId - Get team/MicroDAO by ID
* - POST /api/v1/teams/:teamId/members - Invite member (requires balance check)
*/
import { Router } from 'express';
import { walletService } from '../../services/wallet/wallet.service';
import { daoFactoryService } from '../../services/dao-factory/dao-factory.service';
import type { CreateTeamRequest } from '../../../types/api';
export const teamsRoutes = Router();
// POST /api/v1/teams - Create MicroDAO
teamsRoutes.post('/', async (req, res) => {
try {
const userId = (req as any).userId;
const input: CreateTeamRequest = req.body;
// 1. Check wallet balance - need 1 DAARION on balance
const hasEnough = await walletService.hasEnoughForMicroDaoCreate(userId);
if (!hasEnough) {
res.status(403).json({
error: 'INSUFFICIENT_BALANCE',
message: 'Need 1 DAARION on balance to create MicroDAO',
required: { daarion: 1.0 },
});
return;
}
// 2. Create MicroDAO through DAOFactory
const daoResult = await daoFactoryService.createDao(userId, {
name: input.name,
description: input.description,
type: input.mode === 'confidential' ? 'private' : 'public',
level: 'A4', // User-created MicroDAO are A4 level
});
// 3. TODO: Create team record in database
// For now, return DAO result
res.status(201).json({
id: daoResult.daoId,
name: input.name,
description: input.description,
mode: input.mode || 'public',
type: input.type || 'community',
created_at: new Date().toISOString(),
});
} catch (error: any) {
res.status(400).json({
error: error.message?.includes('INSUFFICIENT_BALANCE') ? 'INSUFFICIENT_BALANCE' : 'BAD_REQUEST',
message: error.message,
});
}
});
// GET /api/v1/teams - List teams/MicroDAO
teamsRoutes.get('/', async (req, res) => {
try {
const userId = (req as any).userId;
// TODO: Get teams from database
// For now, return empty list
res.json({ teams: [] });
} catch (error: any) {
res.status(500).json({
error: 'INTERNAL_ERROR',
message: error.message,
});
}
});
// GET /api/v1/teams/:teamId - Get team/MicroDAO by ID
teamsRoutes.get('/:teamId', async (req, res) => {
try {
const { teamId } = req.params;
// TODO: Get team from database
res.status(404).json({
error: 'NOT_FOUND',
message: `Team ${teamId} not found`,
});
} catch (error: any) {
res.status(500).json({
error: 'INTERNAL_ERROR',
message: error.message,
});
}
});
// POST /api/v1/teams/:teamId/members - Invite member
teamsRoutes.post('/:teamId/members', async (req, res) => {
try {
const userId = (req as any).userId; // Current user (admin)
const { teamId } = req.params;
const { email, role = 'member' } = req.body;
// 1. Check if current user is admin (has 1 DAARION)
const isAdmin = await walletService.hasEnoughForAdminRole(userId);
if (!isAdmin) {
res.status(403).json({
error: 'ACCESS_DENIED',
message: 'Need 1 DAARION on balance to invite members',
});
return;
}
// 2. Check invited user balance based on role
// TODO: Get invited user ID from email
const invitedUserId = `user_${email}`; // Placeholder
if (role === 'admin') {
// Admin role requires 1 DAARION
const hasEnough = await walletService.hasEnoughForAdminRole(invitedUserId);
if (!hasEnough) {
res.status(403).json({
error: 'INSUFFICIENT_BALANCE',
message: 'Invited user needs 1 DAARION on balance to be Admin',
required: { daarion: 1.0 },
});
return;
}
} else {
// Member role requires 0.01 DAARION
const hasEnough = await walletService.hasEnoughForMicroDaoUsage(invitedUserId);
if (!hasEnough) {
res.status(403).json({
error: 'INSUFFICIENT_BALANCE',
message: 'Invited user needs 0.01 DAARION on balance to use MicroDAO',
required: { daarion: 0.01 },
});
return;
}
}
// 3. TODO: Create team member record in database
res.status(201).json({
team_id: teamId,
user_id: invitedUserId,
email,
role,
status: 'invited',
});
} catch (error: any) {
res.status(400).json({
error: 'BAD_REQUEST',
message: error.message,
});
}
});

View File

@@ -0,0 +1,52 @@
/**
* Vendor Routes
* Based on: api-mvp.md
*
* Endpoints:
* - POST /api/v1/platforms/{platform_id}/vendors - Register vendor
*/
import { Router } from 'express';
import { pdpService } from '../../services/pdp/pdp.service';
export const vendorRoutes = Router();
// POST /api/v1/platforms/{platform_id}/vendors - Register vendor
vendorRoutes.post('/:platformId/vendors', async (req, res) => {
try {
const userId = (req as any).userId;
const { platformId } = req.params;
const { display_name, contact } = req.body;
// Check PDP policy
const pdpResult = await pdpService.check(
'policy.vendor.register',
{ platformId },
{ userId, daoId: platformId, daoLevel: 'A2' }
);
if (pdpResult.decision !== 'allow') {
res.status(403).json({
error: 'ACCESS_DENIED',
message: pdpResult.reason || 'PDP denied',
});
return;
}
// TODO: Save vendor to database
const vendorId = `vendor_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
res.status(201).json({
vendor_id: vendorId,
platform_id: platformId,
status: 'approved',
});
} catch (error: any) {
res.status(500).json({
error: 'INTERNAL_ERROR',
message: error.message,
});
}
});

View File

@@ -0,0 +1,72 @@
/**
* Wallet Routes
* Based on: api-mvp.md
*
* Endpoints:
* - GET /api/v1/wallet/me - Get user balances
* - POST /api/v1/wallet/check-access - Check access for action
*/
import { Router } from 'express';
import { walletService } from '../../services/wallet/wallet.service';
import type { AccessCheck } from '../../domain/wallet/types';
export const walletRoutes = Router();
// GET /api/v1/wallet/me - Get user balances
walletRoutes.get('/me', async (req, res) => {
try {
const userId = (req as any).userId;
const balances = await walletService.getBalances(userId);
res.json({
user_id: userId,
balances,
});
} catch (error: any) {
res.status(500).json({
error: 'INTERNAL_ERROR',
message: error.message,
});
}
});
// POST /api/v1/wallet/check-access - Check access
walletRoutes.post('/check-access', async (req, res) => {
try {
const userId = (req as any).userId;
const { check }: AccessCheck = req.body;
let allowed = false;
let reason: string | undefined;
switch (check) {
case 'dao.create':
allowed = await walletService.hasEnoughForDaoCreate(userId);
if (!allowed) reason = 'INSUFFICIENT_BALANCE';
break;
case 'vendor.register':
allowed = await walletService.hasEnoughForVendorRegister(userId);
if (!allowed) reason = 'INSUFFICIENT_STAKED_DAARION';
break;
case 'platform.create':
allowed = await walletService.hasEnoughForPlatformCreate(userId);
if (!allowed) reason = 'INSUFFICIENT_STAKED_DAARION';
break;
default:
reason = 'UNKNOWN_CHECK';
}
res.json({
allowed,
reason: allowed ? null : reason,
});
} catch (error: any) {
res.status(500).json({
error: 'INTERNAL_ERROR',
message: error.message,
});
}
});