@opentrust/dashboard 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ import { Router } from "express";
2
+ import { db, observationQueries } from "@opentrust/db";
3
+ const observations = observationQueries(db);
4
+ export const observationsRouter = Router();
5
+ // POST /api/observations — Record one or more tool call observations
6
+ observationsRouter.post("/", async (req, res, next) => {
7
+ try {
8
+ const tenantId = res.locals.tenantId;
9
+ const body = req.body;
10
+ // Accept single object or array
11
+ const items = Array.isArray(body) ? body : [body];
12
+ for (const item of items) {
13
+ if (!item.agentId || !item.toolName || !item.phase) {
14
+ res.status(400).json({
15
+ success: false,
16
+ error: "agentId, toolName, and phase are required",
17
+ });
18
+ return;
19
+ }
20
+ await observations.record({
21
+ agentId: item.agentId,
22
+ sessionKey: item.sessionKey,
23
+ toolName: item.toolName,
24
+ params: item.params,
25
+ phase: item.phase,
26
+ result: item.result,
27
+ error: item.error,
28
+ durationMs: item.durationMs,
29
+ blocked: item.blocked,
30
+ blockReason: item.blockReason,
31
+ tenantId,
32
+ });
33
+ }
34
+ res.status(201).json({ success: true });
35
+ }
36
+ catch (err) {
37
+ next(err);
38
+ }
39
+ });
40
+ // GET /api/observations — Recent observations (optional ?agentId= filter)
41
+ observationsRouter.get("/", async (req, res, next) => {
42
+ try {
43
+ const tenantId = res.locals.tenantId;
44
+ const agentId = req.query.agentId;
45
+ const limit = parseInt(req.query.limit) || 50;
46
+ const data = await observations.findRecent({ agentId, limit, tenantId });
47
+ res.json({ success: true, data });
48
+ }
49
+ catch (err) {
50
+ next(err);
51
+ }
52
+ });
53
+ // GET /api/observations/permissions — All permissions across all agents
54
+ observationsRouter.get("/permissions", async (req, res, next) => {
55
+ try {
56
+ const tenantId = res.locals.tenantId;
57
+ const data = await observations.getAllPermissions(tenantId);
58
+ res.json({ success: true, data });
59
+ }
60
+ catch (err) {
61
+ next(err);
62
+ }
63
+ });
64
+ // GET /api/observations/anomalies — First-seen tool calls
65
+ observationsRouter.get("/anomalies", async (req, res, next) => {
66
+ try {
67
+ const tenantId = res.locals.tenantId;
68
+ const limit = parseInt(req.query.limit) || 20;
69
+ const data = await observations.findAnomalies(tenantId, limit);
70
+ res.json({ success: true, data });
71
+ }
72
+ catch (err) {
73
+ next(err);
74
+ }
75
+ });
76
+ // GET /api/observations/summary — Per-agent summary
77
+ observationsRouter.get("/summary", async (req, res, next) => {
78
+ try {
79
+ const tenantId = res.locals.tenantId;
80
+ const data = await observations.summary(tenantId);
81
+ res.json({ success: true, data });
82
+ }
83
+ catch (err) {
84
+ next(err);
85
+ }
86
+ });
87
+ // GET /api/agents/:id/permissions — Permission profile for an agent
88
+ observationsRouter.get("/agents/:id/permissions", async (req, res, next) => {
89
+ try {
90
+ const tenantId = res.locals.tenantId;
91
+ const agentId = req.params.id;
92
+ const data = await observations.getPermissions(agentId, tenantId);
93
+ res.json({ success: true, data });
94
+ }
95
+ catch (err) {
96
+ next(err);
97
+ }
98
+ });
99
+ // GET /api/agents/:id/observations — Observations for a specific agent
100
+ observationsRouter.get("/agents/:id/observations", async (req, res, next) => {
101
+ try {
102
+ const tenantId = res.locals.tenantId;
103
+ const agentId = req.params.id;
104
+ const limit = parseInt(req.query.limit) || 50;
105
+ const data = await observations.findRecent({ agentId, limit, tenantId });
106
+ res.json({ success: true, data });
107
+ }
108
+ catch (err) {
109
+ next(err);
110
+ }
111
+ });
@@ -0,0 +1,64 @@
1
+ import { Router } from "express";
2
+ import { db, policyQueries } from "@opentrust/db";
3
+ const policies = policyQueries(db);
4
+ export const policiesRouter = Router();
5
+ // GET /api/policies
6
+ policiesRouter.get("/", async (_req, res, next) => {
7
+ try {
8
+ const tenantId = res.locals.tenantId;
9
+ const data = await policies.findAll(tenantId);
10
+ res.json({ success: true, data });
11
+ }
12
+ catch (err) {
13
+ next(err);
14
+ }
15
+ });
16
+ // POST /api/policies
17
+ policiesRouter.post("/", async (req, res, next) => {
18
+ try {
19
+ const tenantId = res.locals.tenantId;
20
+ const { name, description, scannerIds, action, sensitivityThreshold } = req.body;
21
+ if (!name || !scannerIds || !action) {
22
+ res.status(400).json({ success: false, error: "name, scannerIds, and action are required" });
23
+ return;
24
+ }
25
+ const policy = await policies.create({
26
+ name,
27
+ description: description || null,
28
+ scannerIds,
29
+ action,
30
+ sensitivityThreshold,
31
+ tenantId,
32
+ });
33
+ res.status(201).json({ success: true, data: policy });
34
+ }
35
+ catch (err) {
36
+ next(err);
37
+ }
38
+ });
39
+ // PUT /api/policies/:id
40
+ policiesRouter.put("/:id", async (req, res, next) => {
41
+ try {
42
+ const tenantId = res.locals.tenantId;
43
+ const policy = await policies.update(req.params.id, req.body, tenantId);
44
+ if (!policy) {
45
+ res.status(404).json({ success: false, error: "Policy not found" });
46
+ return;
47
+ }
48
+ res.json({ success: true, data: policy });
49
+ }
50
+ catch (err) {
51
+ next(err);
52
+ }
53
+ });
54
+ // DELETE /api/policies/:id
55
+ policiesRouter.delete("/:id", async (req, res, next) => {
56
+ try {
57
+ const tenantId = res.locals.tenantId;
58
+ await policies.delete(req.params.id, tenantId);
59
+ res.json({ success: true });
60
+ }
61
+ catch (err) {
62
+ next(err);
63
+ }
64
+ });
@@ -0,0 +1,20 @@
1
+ import { Router } from "express";
2
+ import { db, detectionResultQueries } from "@opentrust/db";
3
+ const results = detectionResultQueries(db);
4
+ export const resultsRouter = Router();
5
+ // GET /api/results
6
+ resultsRouter.get("/", async (req, res, next) => {
7
+ try {
8
+ const tenantId = res.locals.tenantId;
9
+ const limit = parseInt(req.query.limit) || 50;
10
+ const offset = parseInt(req.query.offset) || 0;
11
+ const agentId = req.query.agentId;
12
+ const data = agentId
13
+ ? await results.findByAgentId(agentId, { limit, offset, tenantId })
14
+ : await results.findAll({ limit, offset, tenantId });
15
+ res.json({ success: true, data });
16
+ }
17
+ catch (err) {
18
+ next(err);
19
+ }
20
+ });
@@ -0,0 +1,40 @@
1
+ import { Router } from "express";
2
+ import { db, scannerQueries } from "@opentrust/db";
3
+ const scanners = scannerQueries(db);
4
+ export const scannersRouter = Router();
5
+ // GET /api/scanners
6
+ scannersRouter.get("/", async (_req, res, next) => {
7
+ try {
8
+ const tenantId = res.locals.tenantId;
9
+ const data = await scanners.getAll(tenantId);
10
+ res.json({ success: true, data });
11
+ }
12
+ catch (err) {
13
+ next(err);
14
+ }
15
+ });
16
+ // PUT /api/scanners
17
+ scannersRouter.put("/", async (req, res, next) => {
18
+ try {
19
+ const tenantId = res.locals.tenantId;
20
+ const updates = req.body;
21
+ if (!Array.isArray(updates)) {
22
+ res.status(400).json({ success: false, error: "Request body must be an array of scanner updates" });
23
+ return;
24
+ }
25
+ for (const update of updates) {
26
+ await scanners.upsert({
27
+ scannerId: update.scannerId,
28
+ name: update.name,
29
+ description: update.description,
30
+ isEnabled: update.isEnabled,
31
+ tenantId,
32
+ });
33
+ }
34
+ const data = await scanners.getAll(tenantId);
35
+ res.json({ success: true, data });
36
+ }
37
+ catch (err) {
38
+ next(err);
39
+ }
40
+ });
@@ -0,0 +1,71 @@
1
+ import { Router } from "express";
2
+ import { db, settingsQueries } from "@opentrust/db";
3
+ import { maskSecret } from "@opentrust/shared";
4
+ import { checkCoreHealth } from "../services/core-client.js";
5
+ const settings = settingsQueries(db);
6
+ export const settingsRouter = Router();
7
+ // GET /api/settings
8
+ settingsRouter.get("/", async (_req, res, next) => {
9
+ try {
10
+ const all = await settings.getAll();
11
+ // Mask sensitive values
12
+ const masked = {};
13
+ for (const [key, value] of Object.entries(all)) {
14
+ if (key === "og_core_key" || key === "session_token") {
15
+ masked[key] = maskSecret(value);
16
+ }
17
+ else {
18
+ masked[key] = value;
19
+ }
20
+ }
21
+ res.json({ success: true, data: masked });
22
+ }
23
+ catch (err) {
24
+ next(err);
25
+ }
26
+ });
27
+ // PUT /api/settings
28
+ settingsRouter.put("/", async (req, res, next) => {
29
+ try {
30
+ const updates = req.body;
31
+ if (!updates || typeof updates !== "object") {
32
+ res.status(400).json({ success: false, error: "Request body must be a key-value object" });
33
+ return;
34
+ }
35
+ // Prevent overwriting session_token via this endpoint
36
+ delete updates.session_token;
37
+ for (const [key, value] of Object.entries(updates)) {
38
+ await settings.set(key, value);
39
+ }
40
+ res.json({ success: true });
41
+ }
42
+ catch (err) {
43
+ next(err);
44
+ }
45
+ });
46
+ // POST /api/settings/test-connection
47
+ settingsRouter.post("/test-connection", async (_req, res, next) => {
48
+ try {
49
+ const ogCoreUrl = await settings.get("og_core_url");
50
+ const ogCoreKey = await settings.get("og_core_key");
51
+ if (!ogCoreUrl && !ogCoreKey) {
52
+ res.json({
53
+ success: true,
54
+ data: { connected: false, message: "No core URL or key configured" },
55
+ });
56
+ return;
57
+ }
58
+ const healthy = await checkCoreHealth();
59
+ res.json({
60
+ success: true,
61
+ data: {
62
+ connected: healthy,
63
+ message: healthy ? "Connected to core" : "Failed to connect to core",
64
+ url: ogCoreUrl || process.env.OG_CORE_URL || "http://localhost:53666",
65
+ },
66
+ });
67
+ }
68
+ catch (err) {
69
+ next(err);
70
+ }
71
+ });
@@ -0,0 +1,40 @@
1
+ import { Router } from "express";
2
+ import { db, usageQueries } from "@opentrust/db";
3
+ const usage = usageQueries(db);
4
+ export const usageRouter = Router();
5
+ // GET /api/usage/summary
6
+ usageRouter.get("/summary", async (req, res, next) => {
7
+ try {
8
+ const tenantId = res.locals.tenantId;
9
+ // Default to last 30 days
10
+ const end = new Date();
11
+ const start = new Date(end.getTime() - 30 * 24 * 60 * 60 * 1000);
12
+ const stats = await usage.summary(start.toISOString(), end.toISOString(), tenantId);
13
+ res.json({
14
+ success: true,
15
+ data: {
16
+ totalCalls: stats.totalCalls,
17
+ safeCount: stats.safeCount ?? 0,
18
+ unsafeCount: stats.unsafeCount ?? 0,
19
+ periodStart: start.toISOString(),
20
+ periodEnd: end.toISOString(),
21
+ },
22
+ });
23
+ }
24
+ catch (err) {
25
+ next(err);
26
+ }
27
+ });
28
+ // GET /api/usage/daily
29
+ usageRouter.get("/daily", async (req, res, next) => {
30
+ try {
31
+ const tenantId = res.locals.tenantId;
32
+ const end = new Date();
33
+ const start = new Date(end.getTime() - 30 * 24 * 60 * 60 * 1000);
34
+ const data = await usage.daily(start.toISOString(), end.toISOString(), tenantId);
35
+ res.json({ success: true, data });
36
+ }
37
+ catch (err) {
38
+ next(err);
39
+ }
40
+ });
@@ -0,0 +1,77 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * OpenTrust API — Zod Validation Schemas
4
+ *
5
+ * Centralised request validation schemas for key API endpoints.
6
+ * Import and use with schema.parse(req.body) or schema.safeParse(req.body).
7
+ */
8
+ // ── Auth ──────────────────────────────────────────────────────────────────────
9
+ /** POST /api/auth/login */
10
+ export const loginSchema = z.object({
11
+ email: z.string().email("A valid email address is required"),
12
+ apiKey: z.string().min(10, "API key must be at least 10 characters"),
13
+ });
14
+ // ── Agents ────────────────────────────────────────────────────────────────────
15
+ /** POST /api/agents */
16
+ export const createAgentSchema = z.object({
17
+ name: z.string().min(1, "Name is required").max(100, "Name must be 100 characters or fewer"),
18
+ description: z.string().optional(),
19
+ provider: z
20
+ .enum(["openclaw", "langchain", "crewai", "autogen", "custom"])
21
+ .optional(),
22
+ });
23
+ /** PUT /api/agents/:id */
24
+ export const updateAgentSchema = z.object({
25
+ name: z.string().min(1).max(100).optional(),
26
+ description: z.string().optional(),
27
+ status: z.string().optional(),
28
+ provider: z
29
+ .enum(["openclaw", "langchain", "crewai", "autogen", "custom"])
30
+ .optional(),
31
+ metadata: z.record(z.unknown()).optional(),
32
+ });
33
+ // ── Policies ──────────────────────────────────────────────────────────────────
34
+ /** POST /api/policies */
35
+ export const createPolicySchema = z.object({
36
+ name: z.string().min(1, "Name is required"),
37
+ description: z.string().optional(),
38
+ scannerIds: z.array(z.string()).min(1, "At least one scannerId is required"),
39
+ action: z.enum(["block", "alert", "log"]),
40
+ sensitivityThreshold: z
41
+ .number()
42
+ .min(0, "sensitivityThreshold must be between 0 and 1")
43
+ .max(1, "sensitivityThreshold must be between 0 and 1"),
44
+ });
45
+ // ── Detection ─────────────────────────────────────────────────────────────────
46
+ /** POST /api/detect */
47
+ export const detectSchema = z.object({
48
+ messages: z
49
+ .array(z.unknown())
50
+ .min(1, "messages array must contain at least one item"),
51
+ format: z
52
+ .enum(["openai", "anthropic", "gemini", "raw"])
53
+ .optional(),
54
+ role: z
55
+ .enum(["system", "user", "assistant", "tool"])
56
+ .optional(),
57
+ agentId: z.string().optional(),
58
+ });
59
+ // ── Observations ──────────────────────────────────────────────────────────────
60
+ /** POST /api/observations (single item) */
61
+ export const observationSchema = z.object({
62
+ agentId: z.string().min(1, "agentId is required"),
63
+ toolName: z.string().min(1, "toolName is required"),
64
+ params: z.record(z.unknown()).optional(),
65
+ phase: z.enum(["before", "after"]),
66
+ sessionKey: z.string().optional(),
67
+ result: z.unknown().optional(),
68
+ error: z.string().optional(),
69
+ durationMs: z.number().int().nonnegative().optional(),
70
+ blocked: z.boolean().optional(),
71
+ blockReason: z.string().optional(),
72
+ });
73
+ /** POST /api/observations (accepts single object or array) */
74
+ export const observationsBodySchema = z.union([
75
+ observationSchema,
76
+ z.array(observationSchema).min(1),
77
+ ]);
@@ -0,0 +1,56 @@
1
+ import { db, settingsQueries } from "@opentrust/db";
2
+ const settings = settingsQueries(db);
3
+ /** Get core URL from settings or env */
4
+ async function getCoreUrl() {
5
+ return (await settings.get("og_core_url")) || process.env.OG_CORE_URL || "http://localhost:53666";
6
+ }
7
+ /** Get core key from settings */
8
+ async function getCoreKey() {
9
+ return (await settings.get("og_core_key")) || "";
10
+ }
11
+ /**
12
+ * Call core detection API.
13
+ * Uses core key from settings for authentication.
14
+ */
15
+ export async function callCoreDetect(messages, scanners, options) {
16
+ const coreUrl = await getCoreUrl();
17
+ const coreKey = await getCoreKey();
18
+ const body = {
19
+ messages,
20
+ scanners,
21
+ format: options?.format,
22
+ role: options?.role,
23
+ };
24
+ const headers = {
25
+ "Content-Type": "application/json",
26
+ };
27
+ if (coreKey) {
28
+ headers["Authorization"] = `Bearer ${coreKey}`;
29
+ }
30
+ const res = await fetch(`${coreUrl}/v1/detect`, {
31
+ method: "POST",
32
+ headers,
33
+ body: JSON.stringify(body),
34
+ });
35
+ if (!res.ok) {
36
+ const text = await res.text();
37
+ throw new Error(`core returned ${res.status}: ${text}`);
38
+ }
39
+ const json = await res.json();
40
+ if (!json.success) {
41
+ throw new Error(`core error: ${json.error}`);
42
+ }
43
+ return json.data;
44
+ }
45
+ /** Check core health */
46
+ export async function checkCoreHealth() {
47
+ try {
48
+ const coreUrl = await getCoreUrl();
49
+ const res = await fetch(`${coreUrl}/health`);
50
+ const json = await res.json();
51
+ return json.status === "ok";
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import { TIERS } from "@opentrust/shared";
2
+ /** Check if a tier has a specific feature */
3
+ export function tierHasFeature(tier, feature) {
4
+ const config = TIERS[tier];
5
+ return config ? config.features.includes(feature) : false;
6
+ }
7
+ /** Get all features available for a tier */
8
+ export function getTierFeatures(tier) {
9
+ const config = TIERS[tier];
10
+ return config ? [...config.features] : [];
11
+ }
12
+ /** Check if agent count is within tier limits */
13
+ export function canAddAgent(tier, currentCount) {
14
+ const config = TIERS[tier];
15
+ if (!config)
16
+ return false;
17
+ return currentCount < config.maxAgents;
18
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@opentrust/dashboard",
3
+ "version": "7.1.0",
4
+ "type": "module",
5
+ "description": "OpenTrust Dashboard — management panel for AI Agent security (API + embedded web)",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "opentrust-dashboard": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "public"
13
+ ],
14
+ "dependencies": {
15
+ "cors": "^2.8.5",
16
+ "express": "^4.21.0",
17
+ "express-rate-limit": "^7.4.0",
18
+ "helmet": "^8.0.0",
19
+ "morgan": "^1.10.0",
20
+ "nodemailer": "^8.0.1",
21
+ "zod": "^3.23.0",
22
+ "@opentrust/db": "7.1.0",
23
+ "@opentrust/shared": "7.1.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/cors": "^2.8.17",
27
+ "@types/express": "^5.0.0",
28
+ "@types/morgan": "^1.9.9",
29
+ "@types/node": "^22.0.0",
30
+ "@types/nodemailer": "^7.0.10",
31
+ "tsx": "^4.19.0",
32
+ "typescript": "^5.7.0"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/opentrust/opentrust.git",
40
+ "directory": "dashboard/apps/api"
41
+ },
42
+ "author": "OpenTrust",
43
+ "license": "Apache-2.0",
44
+ "scripts": {
45
+ "build": "tsc",
46
+ "dev": "tsx watch --env-file-if-exists=../../.env src/index.ts",
47
+ "start": "node dist/index.js"
48
+ }
49
+ }