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:
110
backend/services/dao-factory/dao-factory.service.ts
Normal file
110
backend/services/dao-factory/dao-factory.service.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* DAOFactory Service (MVP)
|
||||
* Based on: core-services-mvp.md
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Create new DAO (A3/A4)
|
||||
* - Create platforms (A2)
|
||||
* - Validate input
|
||||
* - Call PDP for access checks
|
||||
* - Write DAO to Registry
|
||||
*/
|
||||
|
||||
import type { CreateDaoInput, CreatePlatformInput } from '../../domain/dao/types';
|
||||
import { pdpService } from '../pdp/pdp.service';
|
||||
import { walletService } from '../wallet/wallet.service';
|
||||
import { registryService } from '../registry/registry.service';
|
||||
|
||||
export class DaoFactoryService {
|
||||
/**
|
||||
* Create a new MicroDAO (A3 or A4)
|
||||
* Requires: 1 DAARION on balance (not staked)
|
||||
*/
|
||||
async createDao(userId: string, input: CreateDaoInput): Promise<{ daoId: string }> {
|
||||
// 1. Check wallet balance - need 1 DAARION on balance
|
||||
const hasEnough = await walletService.hasEnoughForMicroDaoCreate(userId);
|
||||
if (!hasEnough) {
|
||||
throw new Error('INSUFFICIENT_BALANCE: Need 1 DAARION on balance to create MicroDAO');
|
||||
}
|
||||
|
||||
// 2. Check PDP policy
|
||||
const pdpResult = await pdpService.check(
|
||||
'policy.dao.create',
|
||||
{ type: 'dao' },
|
||||
{ userId, daoLevel: input.level }
|
||||
);
|
||||
|
||||
if (pdpResult.decision !== 'allow') {
|
||||
throw new Error(`ACCESS_DENIED: ${pdpResult.reason || 'PDP denied'}`);
|
||||
}
|
||||
|
||||
// 3. Create DAO record
|
||||
const daoId = this.generateDaoId();
|
||||
const daoRecord = {
|
||||
daoId,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
level: input.level,
|
||||
type: input.type,
|
||||
parentDaoId: null,
|
||||
federationMode: 'none' as const,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 4. Save to Registry
|
||||
await registryService.saveDao(daoRecord);
|
||||
|
||||
return { daoId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new platform (A2)
|
||||
*/
|
||||
async createPlatform(userId: string, input: CreatePlatformInput): Promise<{ daoId: string }> {
|
||||
// 1. Check wallet balance
|
||||
const hasEnough = await walletService.hasEnoughForPlatformCreate(userId);
|
||||
if (!hasEnough) {
|
||||
throw new Error('INSUFFICIENT_BALANCE: Need 1 DAARION staked');
|
||||
}
|
||||
|
||||
// 2. Check PDP policy
|
||||
const pdpResult = await pdpService.check(
|
||||
'policy.platform.create',
|
||||
{ type: 'platform' },
|
||||
{ userId, daoLevel: 'A2' }
|
||||
);
|
||||
|
||||
if (pdpResult.decision !== 'allow') {
|
||||
throw new Error(`ACCESS_DENIED: ${pdpResult.reason || 'PDP denied'}`);
|
||||
}
|
||||
|
||||
// 3. Create platform record
|
||||
const daoId = this.generateDaoId();
|
||||
const daoRecord = {
|
||||
daoId,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
level: 'A2' as const,
|
||||
type: 'platform' as const,
|
||||
parentDaoId: null,
|
||||
federationMode: 'none' as const,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 4. Save to Registry
|
||||
await registryService.saveDao(daoRecord);
|
||||
|
||||
return { daoId };
|
||||
}
|
||||
|
||||
private generateDaoId(): string {
|
||||
// MVP: Simple UUID-like generation
|
||||
// TODO: Use proper UUID library
|
||||
return `dao_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const daoFactoryService = new DaoFactoryService();
|
||||
|
||||
|
||||
100
backend/services/pdp/pdp.service.ts
Normal file
100
backend/services/pdp/pdp.service.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* PDP Service (MVP)
|
||||
* Based on: pdp_access.md, core-services-mvp.md
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Centralized access decision making
|
||||
* - Interpret policies from pdp_access.md
|
||||
* - Provide simple API for other services
|
||||
*/
|
||||
|
||||
import type { PdpRequest, PdpResponse, PolicyId, PdpContext } from '../../domain/pdp/policy.model';
|
||||
import { policiesConfig } from './policies.config';
|
||||
import { walletService } from '../wallet/wallet.service';
|
||||
|
||||
export class PdpService {
|
||||
/**
|
||||
* Check policy and return decision
|
||||
*/
|
||||
async check(
|
||||
policyId: PolicyId,
|
||||
resource: Record<string, unknown>,
|
||||
context: PdpContext
|
||||
): Promise<PdpResponse> {
|
||||
const policy = policiesConfig[policyId];
|
||||
|
||||
if (!policy) {
|
||||
return {
|
||||
decision: 'deny',
|
||||
reason: `Policy ${policyId} not found`,
|
||||
};
|
||||
}
|
||||
|
||||
// Evaluate policy conditions
|
||||
const result = await this.evaluatePolicy(policy, resource, context);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async evaluatePolicy(
|
||||
policy: any,
|
||||
resource: Record<string, unknown>,
|
||||
context: PdpContext
|
||||
): Promise<PdpResponse> {
|
||||
// MVP: Simple evaluation
|
||||
// Future: More complex condition evaluation
|
||||
|
||||
// Example: policy.dao.create
|
||||
if (policy.id === 'policy.dao.create') {
|
||||
const hasEnough = await walletService.hasEnoughForDaoCreate(context.userId || '');
|
||||
if (!hasEnough) {
|
||||
return {
|
||||
decision: 'deny',
|
||||
reason: 'INSUFFICIENT_BALANCE',
|
||||
details: {
|
||||
required: { DAAR: 1.0, DAARION: 0.01 },
|
||||
},
|
||||
};
|
||||
}
|
||||
return { decision: 'allow' };
|
||||
}
|
||||
|
||||
// Example: policy.platform.create
|
||||
if (policy.id === 'policy.platform.create') {
|
||||
const hasEnough = await walletService.hasEnoughForPlatformCreate(context.userId || '');
|
||||
if (!hasEnough) {
|
||||
return {
|
||||
decision: 'deny',
|
||||
reason: 'INSUFFICIENT_BALANCE',
|
||||
details: {
|
||||
required: { DAARION: 1.0 },
|
||||
},
|
||||
};
|
||||
}
|
||||
return { decision: 'allow' };
|
||||
}
|
||||
|
||||
// Example: policy.vendor.register
|
||||
if (policy.id === 'policy.vendor.register') {
|
||||
const hasEnough = await walletService.hasEnoughForVendorRegister(context.userId || '');
|
||||
if (!hasEnough) {
|
||||
return {
|
||||
decision: 'deny',
|
||||
reason: 'INSUFFICIENT_STAKED_DAARION',
|
||||
details: {
|
||||
required: { DAARION: 0.01 },
|
||||
},
|
||||
};
|
||||
}
|
||||
return { decision: 'allow' };
|
||||
}
|
||||
|
||||
// Default: allow (for MVP, can be more restrictive later)
|
||||
return { decision: 'allow' };
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const pdpService = new PdpService();
|
||||
|
||||
|
||||
76
backend/services/pdp/policies.config.ts
Normal file
76
backend/services/pdp/policies.config.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Policies Configuration
|
||||
* Based on: pdp_access.md
|
||||
*
|
||||
* Initial set of policies for MVP
|
||||
*/
|
||||
|
||||
export const policiesConfig = {
|
||||
'policy.dao.create': {
|
||||
id: 'policy.dao.create',
|
||||
description: 'Створення нового MicroDAO',
|
||||
conditions: [
|
||||
{
|
||||
type: 'or',
|
||||
rules: [
|
||||
{ type: 'balance', token: 'DAAR', gte: 1 },
|
||||
{ type: 'balance', token: 'DAARION', gte: 0.01 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
'policy.vendor.register': {
|
||||
id: 'policy.vendor.register',
|
||||
description: 'Реєстрація вендора на платформі',
|
||||
conditions: [
|
||||
{ type: 'staked', token: 'DAARION', gte: 0.01 },
|
||||
],
|
||||
},
|
||||
'policy.platform.create': {
|
||||
id: 'policy.platform.create',
|
||||
description: 'Створення платформи',
|
||||
conditions: [
|
||||
{ type: 'staked', token: 'DAARION', gte: 1 },
|
||||
],
|
||||
},
|
||||
'policy.federation.join': {
|
||||
id: 'policy.federation.join',
|
||||
description: 'Вступ DAO до SuperDAO',
|
||||
conditions: [
|
||||
{ type: 'role', value: 'owner' },
|
||||
{ type: 'target', property: 'federation_mode', value: 'superdao' },
|
||||
],
|
||||
},
|
||||
'policy.federation.leave': {
|
||||
id: 'policy.federation.leave',
|
||||
description: 'Вихід DAO з SuperDAO',
|
||||
conditions: [
|
||||
{ type: 'role', value: 'owner' },
|
||||
],
|
||||
},
|
||||
'policy.federation.create-superdao': {
|
||||
id: 'policy.federation.create-superdao',
|
||||
description: 'Створення SuperDAO',
|
||||
conditions: [
|
||||
{ type: 'role', value: 'owner' },
|
||||
{ type: 'dao', property: 'child_count', gte: 1 },
|
||||
],
|
||||
},
|
||||
'policy.federation.dissolve': {
|
||||
id: 'policy.federation.dissolve',
|
||||
description: 'Розформування федерації',
|
||||
conditions: [
|
||||
{ type: 'role', value: 'owner' },
|
||||
{ type: 'dao', property: 'level', ne: 'A1' },
|
||||
],
|
||||
},
|
||||
'policy.agent.run': {
|
||||
id: 'policy.agent.run',
|
||||
description: 'Запуск агента',
|
||||
conditions: [
|
||||
{ type: 'agent', property: 'registered', value: true },
|
||||
],
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
||||
47
backend/services/registry/registry.service.ts
Normal file
47
backend/services/registry/registry.service.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Registry Service (MVP)
|
||||
* Based on: core-services-mvp.md
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Store all DAO information
|
||||
* - Mark DAO as platform (A2) or MicroDAO (A3/A4)
|
||||
* - Provide public catalog of DAO/platforms
|
||||
*/
|
||||
|
||||
import type { DaoRecord } from '../../domain/dao/types';
|
||||
import { daoRepository } from '../../infra/db/dao.repository';
|
||||
|
||||
export class RegistryService {
|
||||
/**
|
||||
* Save DAO record to registry
|
||||
*/
|
||||
async saveDao(record: DaoRecord): Promise<void> {
|
||||
await daoRepository.save(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DAO by ID
|
||||
*/
|
||||
async getDaoById(daoId: string): Promise<DaoRecord | null> {
|
||||
return daoRepository.findById(daoId);
|
||||
}
|
||||
|
||||
/**
|
||||
* List DAOs with optional filters
|
||||
*/
|
||||
async listDaos(filter?: { level?: string; type?: string }): Promise<DaoRecord[]> {
|
||||
return daoRepository.findAll(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all platforms (A2, type=platform)
|
||||
*/
|
||||
async listPlatforms(): Promise<DaoRecord[]> {
|
||||
return daoRepository.findAll({ level: 'A2', type: 'platform' });
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const registryService = new RegistryService();
|
||||
|
||||
|
||||
36
backend/services/wallet/wallet.adapter.ts
Normal file
36
backend/services/wallet/wallet.adapter.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Wallet Adapter (MVP Stub)
|
||||
*
|
||||
* On MVP: returns mock data or reads from DB/stub
|
||||
* Future: integrate with on-chain data
|
||||
*/
|
||||
|
||||
import type { Balance } from '../../domain/wallet/types';
|
||||
|
||||
/**
|
||||
* Get balances from external source (on-chain / DB / stub)
|
||||
*/
|
||||
export async function getBalances(userId: string): Promise<Balance[]> {
|
||||
// MVP: Return mock data
|
||||
// TODO: Replace with actual DB/on-chain integration
|
||||
return [
|
||||
{ symbol: 'DAAR', amount: '0.0' },
|
||||
{ symbol: 'DAARION', amount: '0.0' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get staked DAARION amount
|
||||
*/
|
||||
export async function getStakedDaarion(userId: string): Promise<number> {
|
||||
// MVP: Return mock data
|
||||
// TODO: Replace with actual DB/on-chain integration
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
export const walletAdapter = {
|
||||
getBalances,
|
||||
getStakedDaarion,
|
||||
};
|
||||
|
||||
|
||||
23
backend/services/wallet/wallet.interface.ts
Normal file
23
backend/services/wallet/wallet.interface.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Wallet Service Interface
|
||||
* Based on: core-services-mvp.md, updated for MicroDAO requirements
|
||||
*/
|
||||
|
||||
import type { Balance } from '../../domain/wallet/types';
|
||||
|
||||
export interface WalletService {
|
||||
getBalances(userId: string): Promise<Balance[]>;
|
||||
getDaarionBalance(userId: string): Promise<number>;
|
||||
|
||||
// MicroDAO access checks (balance-based, no staking)
|
||||
hasEnoughForMicroDaoCreate(userId: string): Promise<boolean>; // 1 DAARION
|
||||
hasEnoughForAdminRole(userId: string): Promise<boolean>; // 1 DAARION
|
||||
hasEnoughForMicroDaoUsage(userId: string): Promise<boolean>; // 0.01 DAARION
|
||||
|
||||
// Legacy methods (deprecated)
|
||||
hasEnoughForDaoCreate(userId: string): Promise<boolean>;
|
||||
hasEnoughForVendorRegister(userId: string): Promise<boolean>;
|
||||
hasEnoughForPlatformCreate(userId: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
|
||||
85
backend/services/wallet/wallet.service.ts
Normal file
85
backend/services/wallet/wallet.service.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Wallet Service (MVP)
|
||||
* Based on: core-services-mvp.md
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Read DAAR / DAARION balances
|
||||
* - Provide helper functions for access checks
|
||||
*/
|
||||
|
||||
import type { WalletService as IWalletService } from './wallet.interface';
|
||||
import { walletAdapter } from './wallet.adapter';
|
||||
import type { Balance, TokenSymbol } from '../../domain/wallet/types';
|
||||
|
||||
export class WalletService implements IWalletService {
|
||||
/**
|
||||
* Get user balances for DAAR and DAARION
|
||||
*/
|
||||
async getBalances(userId: string): Promise<Balance[]> {
|
||||
return walletAdapter.getBalances(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has enough DAARION to create a MicroDAO
|
||||
* Requires: 1 DAARION on balance (not staked)
|
||||
*/
|
||||
async hasEnoughForMicroDaoCreate(userId: string): Promise<boolean> {
|
||||
const balances = await this.getBalances(userId);
|
||||
const daarion = balances.find(b => b.symbol === 'DAARION');
|
||||
return daarion ? parseFloat(daarion.amount) >= 1.0 : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has enough DAARION to be Admin
|
||||
* Requires: 1 DAARION on balance (not staked)
|
||||
*/
|
||||
async hasEnoughForAdminRole(userId: string): Promise<boolean> {
|
||||
return this.hasEnoughForMicroDaoCreate(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has enough DAARION to use MicroDAO service
|
||||
* Requires: 0.01 DAARION on balance (not staked)
|
||||
*/
|
||||
async hasEnoughForMicroDaoUsage(userId: string): Promise<boolean> {
|
||||
const balances = await this.getBalances(userId);
|
||||
const daarion = balances.find(b => b.symbol === 'DAARION');
|
||||
return daarion ? parseFloat(daarion.amount) >= 0.01 : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DAARION balance for user
|
||||
*/
|
||||
async getDaarionBalance(userId: string): Promise<number> {
|
||||
const balances = await this.getBalances(userId);
|
||||
const daarion = balances.find(b => b.symbol === 'DAARION');
|
||||
return daarion ? parseFloat(daarion.amount) : 0;
|
||||
}
|
||||
|
||||
// Legacy methods (deprecated, kept for backward compatibility)
|
||||
/**
|
||||
* @deprecated Use hasEnoughForMicroDaoCreate instead
|
||||
*/
|
||||
async hasEnoughForDaoCreate(userId: string): Promise<boolean> {
|
||||
return this.hasEnoughForMicroDaoCreate(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Not used in current implementation
|
||||
*/
|
||||
async hasEnoughForVendorRegister(userId: string): Promise<boolean> {
|
||||
return this.hasEnoughForMicroDaoUsage(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Not used in current implementation
|
||||
*/
|
||||
async hasEnoughForPlatformCreate(userId: string): Promise<boolean> {
|
||||
return this.hasEnoughForMicroDaoCreate(userId);
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const walletService = new WalletService();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user