/home/techb158/trellopowerup.abdallabala.com/src
Edit: /home/techb158/trellopowerup.abdallabala.com/src/server.js (7079B)
import cors from 'cors';
import dotenv from 'dotenv';
import express from 'express';
import helmet from 'helmet';
import morgan from 'morgan';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { buildBoardCsv, buildBoardReviewQueueFromSummary, buildBoardSummaryFromAssessments, buildReviewQueueCsv, decorateAssessment, withCardHistoryTrends } from './assessmentAnalytics.js';
import { getReferenceTaxonomy, scoreRiskAssessment } from './riskEngine.js';
import { deleteAssessmentsByCard, getStoreInfo, listAssessmentsByBoard, listAssessmentsByBoardCard, listAssessmentsByCard, saveAssessment, upsertBoard, upsertCard } from './store.js';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const port = Number(process.env.PORT || 3000);
const publicDir = path.join(__dirname, '..', 'public');
const appVersion = '0.4.5';
const staleDaysSetting = Number(process.env.COSMIC_REVIEW_STALE_DAYS || 30);
const reviewStaleDays = Number.isFinite(staleDaysSetting) && staleDaysSetting > 0 ? staleDaysSetting : 30;
const MS_PER_DAY = 24 * 60 * 60 * 1000;
const allowedOrigins = (process.env.ALLOWED_ORIGINS || 'http://localhost:3000,https://trello.com,https://*.trello.com')
.split(',')
.map((value) => value.trim())
.filter(Boolean);
function originAllowed(origin) {
if (!origin) return true;
return allowedOrigins.some((allowed) => {
if (allowed.includes('*')) {
const pattern = new RegExp(`^${allowed.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace('\\*', '.*')}$`);
return pattern.test(origin);
}
return allowed === origin;
});
}
function analyticsOptions() {
return { reviewStaleDays };
}
function buildBoardSummary(boardId) {
return buildBoardSummaryFromAssessments(boardId, listAssessmentsByBoard(boardId), analyticsOptions());
}
app.disable('x-powered-by');
/*
Trello Power-Ups run inside Trello iframes.
These security headers must be configured before static files are served.
*/
app.use(helmet({
contentSecurityPolicy: false,
crossOriginEmbedderPolicy: false,
crossOriginOpenerPolicy: false,
crossOriginResourcePolicy: false,
originAgentCluster: false,
frameguard: false
}));
app.use((_req, res, next) => {
res.removeHeader('X-Frame-Options');
res.removeHeader('Cross-Origin-Opener-Policy');
res.removeHeader('Cross-Origin-Resource-Policy');
/*
This allows Trello to embed the Power-Up connector page.
Do not set X-Frame-Options for this app.
*/
res.setHeader('Content-Security-Policy', 'frame-ancestors https://trello.com https://*.trello.com;');
next();
});
app.use(cors({
origin: (origin, cb) => cb(originAllowed(origin) ? null : new Error('Origin not allowed'), true)
}));
app.use(express.json({ limit: '1mb' }));
app.use(morgan('tiny'));
app.get('/config.js', (_req, res) => {
const config = {
COSMIC_TRELLO_API_KEY: process.env.TRELLO_API_KEY || 'replace_with_power_up_api_key',
COSMIC_APP_NAME: process.env.TRELLO_APP_NAME || 'COSMIC AI-Risk Management',
COSMIC_API_BASE: process.env.PUBLIC_BASE_URL || `http://localhost:${port}`
};
res
.type('application/javascript')
.setHeader('Cache-Control', 'no-store')
.send(
Object.entries(config)
.map(([key, value]) => `window.${key} = ${JSON.stringify(value)};`)
.join('\n')
);
});
/*
Serve static files only after Helmet and iframe-safe headers are configured.
The previous version served static files before removing X-Frame-Options.
*/
app.use(express.static(publicDir));
app.get('/api/health', (_req, res) => {
res.json({ ok: true, service: 'cosmic-ai-risk-trello-powerup', version: appVersion, store: getStoreInfo() });
});
app.get('/api/taxonomy', (_req, res) => {
res.json(getReferenceTaxonomy());
});
app.post('/api/boards/:boardId', (req, res) => {
const board = upsertBoard(req.params.boardId, req.body || {});
res.status(200).json(board);
});
app.post('/api/cards/:cardId', (req, res) => {
const card = upsertCard(req.params.cardId, req.body || {});
res.status(200).json(card);
});
app.post('/api/risks/score', (req, res) => {
const assessment = scoreRiskAssessment(req.body || {});
res.json(assessment);
});
app.post('/api/boards/:boardId/cards/:cardId/assessments', (req, res) => {
const score = scoreRiskAssessment(req.body || {});
const previous = listAssessmentsByBoardCard(req.params.boardId, req.params.cardId)[0];
const assessment = saveAssessment({
boardId: req.params.boardId,
cardId: req.params.cardId,
input: req.body,
score
});
res.status(201).json(decorateAssessment(assessment, previous, analyticsOptions()));
});
app.delete('/api/boards/:boardId/cards/:cardId/assessments', (req, res) => {
res.json(deleteAssessmentsByCard(req.params.boardId, req.params.cardId));
});
app.get('/api/boards/:boardId/cards/:cardId/assessments/latest', (req, res) => {
const assessments = listAssessmentsByBoardCard(req.params.boardId, req.params.cardId);
res.json({ assessment: decorateAssessment(assessments[0], assessments[1], analyticsOptions()) });
});
app.get('/api/boards/:boardId/cards/:cardId/assessments', (req, res) => {
res.json(withCardHistoryTrends(listAssessmentsByBoardCard(req.params.boardId, req.params.cardId), analyticsOptions()));
});
app.get('/api/boards/:boardId/summary', (req, res) => {
res.json(buildBoardSummary(req.params.boardId));
});
app.get('/api/boards/:boardId/review-queue', (req, res) => {
res.json(buildBoardReviewQueueFromSummary(buildBoardSummary(req.params.boardId)));
});
app.get('/api/boards/:boardId/review-queue.csv', (req, res) => {
const queue = buildBoardReviewQueueFromSummary(buildBoardSummary(req.params.boardId));
res
.type('text/csv')
.setHeader('Content-Disposition', `attachment; filename="cosmic-review-queue-${req.params.boardId}.csv"`)
.send(buildReviewQueueCsv(queue));
});
app.get('/api/boards/:boardId/export.json', (req, res) => {
const summary = buildBoardSummary(req.params.boardId);
res
.type('application/json')
.setHeader('Content-Disposition', `attachment; filename="cosmic-risk-${req.params.boardId}.json"`)
.json({ reportType: 'cosmic-board-risk-export', version: appVersion, ...summary });
});
app.get('/api/boards/:boardId/export.csv', (req, res) => {
const summary = buildBoardSummary(req.params.boardId);
res
.type('text/csv')
.setHeader('Content-Disposition', `attachment; filename="cosmic-risk-${req.params.boardId}.csv"`)
.send(buildBoardCsv(summary));
});
app.get('/api/cards/:cardId/assessments/latest', (req, res) => {
const assessments = listAssessmentsByCard(req.params.cardId);
res.json({ assessment: decorateAssessment(assessments[0], assessments[1], analyticsOptions()) });
});
app.get('/api/cards/:cardId/assessments', (req, res) => {
res.json(withCardHistoryTrends(listAssessmentsByCard(req.params.cardId), analyticsOptions()));
});
app.listen(port, () => {
console.log(`COSMIC AI-Risk Power-Up running at http://localhost:${port}`);
});