@opentrust/dashboard 7.3.21 → 7.3.22

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.
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { join, dirname } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { sessionAuth } from "./middleware/session-auth.js";
9
9
  import { authRouter } from "./routes/auth.js";
10
- import { settingsRouter } from "./routes/settings.js";
10
+ import { settingsRouter, ensureSystemApiKey } from "./routes/settings.js";
11
11
  import { agentsRouter } from "./routes/agents.js";
12
12
  import { scannersRouter } from "./routes/scanners.js";
13
13
  import { policiesRouter } from "./routes/policies.js";
@@ -82,13 +82,15 @@ app.use("/api/commands", commandsRouter);
82
82
  app.use("/api/hosts", hostsRouter);
83
83
  app.use("/api/system", systemRouter);
84
84
  app.use(errorHandler);
85
- app.listen(PORT, () => {
85
+ app.listen(PORT, async () => {
86
86
  console.log(`OpenTrust API running on port ${PORT}`);
87
87
  console.log(`DashboardMode: ${DASHBOARD_MODE}`);
88
- if (process.env.DEV_MODE === "true") {
89
- console.log(`DevMode: ON — authentication disabled`);
88
+ console.log(`Auth: username/password login (default: admin/admin)`);
89
+ try {
90
+ const systemKey = await ensureSystemApiKey();
91
+ console.log(`System API Key: ${systemKey.slice(0, 12)}...${systemKey.slice(-4)}`);
90
92
  }
91
- else {
92
- console.log(`Auth: POST /api/auth/request send magic link`);
93
+ catch {
94
+ console.warn("Warning: failed to initialize system API key");
93
95
  }
94
96
  });
@@ -1,27 +1,55 @@
1
- const IS_DEV_MODE = process.env.DEV_MODE === "true";
1
+ import { db, settingsQueries } from "@opentrust/db";
2
2
  const CORE_URL = process.env.OG_CORE_URL || "http://localhost:53666";
3
3
  const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
4
- const DEV_TENANT_ID = "default";
5
- const DEV_EMAIL = "dev@localhost";
6
- const DEV_API_KEY = "sk-og-dev";
4
+ const KEY_CACHE_TTL_MS = 60_000; // 1 minute
5
+ const DEFAULT_TENANT_ID = "default";
7
6
  const sessionCache = new Map();
7
+ let cachedSystemKey = null;
8
+ let cachedSystemKeyAt = 0;
9
+ async function getSystemApiKey() {
10
+ if (cachedSystemKey && Date.now() - cachedSystemKeyAt < KEY_CACHE_TTL_MS) {
11
+ return cachedSystemKey;
12
+ }
13
+ try {
14
+ const settings = settingsQueries(db);
15
+ const key = await settings.get("system_api_key");
16
+ cachedSystemKey = key || null;
17
+ cachedSystemKeyAt = Date.now();
18
+ return cachedSystemKey;
19
+ }
20
+ catch {
21
+ return cachedSystemKey;
22
+ }
23
+ }
8
24
  /**
9
25
  * Session authentication middleware for OpenTrust.
10
26
  *
11
- * When DEV_MODE=true, skips all validation and uses default dev identity.
12
- * Otherwise validates the API key against the core (cached for 5 minutes).
13
- * Sets res.locals.tenantId, res.locals.userEmail, and res.locals.coreApiKey.
27
+ * Supports two authentication modes:
28
+ * 1. sk-ot-* keys: validated directly against the system_api_key in settings DB
29
+ * 2. sk-og-* keys: validated against Core (backward compatible)
14
30
  */
15
31
  export async function sessionAuth(req, res, next) {
16
- if (IS_DEV_MODE) {
17
- res.locals.tenantId = DEV_TENANT_ID;
18
- res.locals.userEmail = DEV_EMAIL;
19
- res.locals.coreApiKey = DEV_API_KEY;
20
- next();
32
+ const apiKey = req.headers.authorization?.replace("Bearer ", "")
33
+ || req.headers["x-api-key"];
34
+ if (!apiKey) {
35
+ res.status(401).json({ success: false, error: "Not authenticated" });
36
+ return;
37
+ }
38
+ // sk-ot-* keys: validate against system_api_key stored in Dashboard settings
39
+ if (apiKey.startsWith("sk-ot-")) {
40
+ const systemKey = await getSystemApiKey();
41
+ if (systemKey && apiKey === systemKey) {
42
+ res.locals.tenantId = DEFAULT_TENANT_ID;
43
+ res.locals.userEmail = "admin@opentrust";
44
+ res.locals.coreApiKey = apiKey;
45
+ next();
46
+ return;
47
+ }
48
+ res.status(401).json({ success: false, error: "Invalid system API key" });
21
49
  return;
22
50
  }
23
- const apiKey = req.headers.authorization?.replace("Bearer ", "");
24
- if (!apiKey?.startsWith("sk-og-")) {
51
+ // sk-og-* keys: validate against Core (backward compatible)
52
+ if (!apiKey.startsWith("sk-og-")) {
25
53
  res.status(401).json({ success: false, error: "Not authenticated" });
26
54
  return;
27
55
  }
@@ -1,125 +1,127 @@
1
1
  import { Router } from "express";
2
+ import { createHash } from "node:crypto";
3
+ import { db, settingsQueries } from "@opentrust/db";
4
+ import { ensureSystemApiKey } from "./settings.js";
2
5
  export const authRouter = Router();
3
- const IS_DEV_MODE = process.env.DEV_MODE === "true";
4
- const CORE_URL = process.env.OG_CORE_URL || "http://localhost:53666";
5
- const DEV_EMAIL = "dev@localhost";
6
- const DEV_RESPONSE = {
7
- success: true,
8
- email: DEV_EMAIL,
9
- agentId: "dev-agent",
10
- name: "Dev Agent",
11
- quotaTotal: 999999,
12
- quotaUsed: 0,
13
- quotaRemaining: 999999,
14
- agents: [],
15
- };
16
- async function fetchCoreAccount(apiKey) {
17
- try {
18
- const res = await fetch(`${CORE_URL}/api/v1/account`, {
19
- headers: { Authorization: `Bearer ${apiKey}` },
20
- });
21
- if (!res.ok)
22
- return null;
23
- return res.json();
24
- }
25
- catch {
26
- return null;
27
- }
6
+ const DEFAULT_USERNAME = "admin";
7
+ const DEFAULT_PASSWORD = "admin";
8
+ const settings = settingsQueries(db);
9
+ function hashPassword(password) {
10
+ return createHash("sha256").update(password).digest("hex");
28
11
  }
29
- async function fetchCoreAccounts(apiKey) {
12
+ async function ensureAdminCredentials() {
13
+ const existing = await settings.get("admin_password_hash");
14
+ if (existing)
15
+ return;
16
+ await settings.set("admin_username", DEFAULT_USERNAME);
17
+ await settings.set("admin_password_hash", hashPassword(DEFAULT_PASSWORD));
18
+ }
19
+ authRouter.get("/config", async (_req, res) => {
30
20
  try {
31
- const res = await fetch(`${CORE_URL}/api/v1/accounts`, {
32
- headers: { Authorization: `Bearer ${apiKey}` },
33
- });
34
- if (!res.ok)
35
- return null;
36
- return res.json();
21
+ const pwHash = await settings.get("admin_password_hash");
22
+ const isDefault = pwHash === hashPassword(DEFAULT_PASSWORD);
23
+ res.json({ defaultPassword: isDefault });
37
24
  }
38
25
  catch {
39
- return null;
26
+ res.json({ defaultPassword: true });
40
27
  }
41
- }
42
- authRouter.get("/config", (_req, res) => {
43
- res.json({ devMode: IS_DEV_MODE });
44
28
  });
45
29
  authRouter.post("/login", async (req, res, next) => {
46
- if (IS_DEV_MODE) {
47
- res.json(DEV_RESPONSE);
48
- return;
49
- }
50
30
  try {
51
- const { apiKey, email } = req.body;
52
- if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
53
- res.status(400).json({ success: false, error: "Valid email required" });
31
+ await ensureAdminCredentials();
32
+ const { username, password } = req.body;
33
+ if (!username || !password) {
34
+ res.status(400).json({ success: false, error: "Username and password required" });
54
35
  return;
55
36
  }
56
- if (!apiKey || !apiKey.startsWith("sk-og-")) {
57
- res.status(400).json({ success: false, error: "Valid API key required (sk-og-...)" });
37
+ const storedUsername = await settings.get("admin_username") || DEFAULT_USERNAME;
38
+ const storedHash = await settings.get("admin_password_hash") || hashPassword(DEFAULT_PASSWORD);
39
+ if (username !== storedUsername || hashPassword(password) !== storedHash) {
40
+ res.status(401).json({ success: false, error: "Invalid username or password" });
58
41
  return;
59
42
  }
60
- const account = await fetchCoreAccount(apiKey);
61
- if (!account?.success) {
62
- res.status(401).json({ success: false, error: "Invalid API key" });
63
- return;
64
- }
65
- if (!account.email) {
66
- res.status(403).json({ success: false, error: "Agent not yet activated. Complete email verification first." });
67
- return;
68
- }
69
- if (account.email.toLowerCase() !== email.toLowerCase()) {
70
- res.status(401).json({ success: false, error: "Email does not match this API key" });
71
- return;
72
- }
73
- const accounts = await fetchCoreAccounts(apiKey);
74
- const agents = accounts?.agents ?? [];
43
+ const apiKey = await ensureSystemApiKey();
75
44
  res.json({
76
45
  success: true,
77
- email: account.email,
78
- agentId: account.agentId,
79
- name: account.name,
80
- quotaTotal: account.quotaTotal,
81
- quotaUsed: account.quotaUsed,
82
- quotaRemaining: account.quotaRemaining,
83
- agents,
46
+ email: `${username}@opentrust`,
47
+ name: username,
48
+ apiKey,
84
49
  });
85
50
  }
86
51
  catch (err) {
87
52
  next(err);
88
53
  }
89
54
  });
90
- authRouter.get("/me", async (req, res, next) => {
91
- if (IS_DEV_MODE) {
92
- res.json(DEV_RESPONSE);
93
- return;
94
- }
55
+ authRouter.post("/change-password", async (req, res, next) => {
95
56
  try {
96
- const apiKey = req.headers.authorization?.replace("Bearer ", "");
97
- if (!apiKey?.startsWith("sk-og-")) {
57
+ const token = req.headers.authorization?.replace("Bearer ", "");
58
+ const systemKey = await ensureSystemApiKey();
59
+ if (!token || token !== systemKey) {
98
60
  res.status(401).json({ success: false, error: "Not authenticated" });
99
61
  return;
100
62
  }
101
- const account = await fetchCoreAccount(apiKey);
102
- if (!account?.success || !account.email) {
103
- res.status(401).json({ success: false, error: "Invalid or inactive API key" });
63
+ const { currentPassword, newPassword } = req.body;
64
+ if (!currentPassword || !newPassword) {
65
+ res.status(400).json({ success: false, error: "Current and new password required" });
104
66
  return;
105
67
  }
106
- const accounts = await fetchCoreAccounts(apiKey);
107
- const agents = accounts?.agents ?? [];
108
- res.json({
109
- success: true,
110
- email: account.email,
111
- agentId: account.agentId,
112
- name: account.name,
113
- quotaTotal: account.quotaTotal,
114
- quotaUsed: account.quotaUsed,
115
- quotaRemaining: account.quotaRemaining,
116
- agents,
117
- });
68
+ if (newPassword.length < 4) {
69
+ res.status(400).json({ success: false, error: "Password must be at least 4 characters" });
70
+ return;
71
+ }
72
+ const storedHash = await settings.get("admin_password_hash") || hashPassword(DEFAULT_PASSWORD);
73
+ if (hashPassword(currentPassword) !== storedHash) {
74
+ res.status(401).json({ success: false, error: "Current password is incorrect" });
75
+ return;
76
+ }
77
+ await settings.set("admin_password_hash", hashPassword(newPassword));
78
+ res.json({ success: true });
118
79
  }
119
80
  catch (err) {
120
81
  next(err);
121
82
  }
122
83
  });
84
+ authRouter.get("/me", async (req, res) => {
85
+ const token = req.headers.authorization?.replace("Bearer ", "")
86
+ || req.headers["x-api-key"];
87
+ if (!token) {
88
+ res.status(401).json({ success: false, error: "Not authenticated" });
89
+ return;
90
+ }
91
+ // Validate sk-ot-* system key
92
+ if (token.startsWith("sk-ot-")) {
93
+ const systemKey = await ensureSystemApiKey();
94
+ if (token === systemKey) {
95
+ const username = await settings.get("admin_username") || DEFAULT_USERNAME;
96
+ res.json({ success: true, email: `${username}@opentrust`, name: username });
97
+ return;
98
+ }
99
+ res.status(401).json({ success: false, error: "Invalid system API key" });
100
+ return;
101
+ }
102
+ // Backward compat: sk-og-* keys via Core
103
+ if (token.startsWith("sk-og-")) {
104
+ try {
105
+ const coreUrl = process.env.OG_CORE_URL || "http://localhost:53666";
106
+ const coreRes = await fetch(`${coreUrl}/api/v1/account`, {
107
+ headers: { Authorization: `Bearer ${token}` },
108
+ });
109
+ if (!coreRes.ok) {
110
+ res.status(401).json({ success: false, error: "Invalid API key" });
111
+ return;
112
+ }
113
+ const data = await coreRes.json();
114
+ if (data.success && data.email) {
115
+ res.json({ success: true, email: data.email, name: data.name || data.email });
116
+ return;
117
+ }
118
+ }
119
+ catch { }
120
+ res.status(401).json({ success: false, error: "Invalid or inactive API key" });
121
+ return;
122
+ }
123
+ res.status(401).json({ success: false, error: "Not authenticated" });
124
+ });
123
125
  authRouter.post("/logout", (_req, res) => {
124
126
  res.json({ success: true });
125
127
  });
@@ -1,17 +1,29 @@
1
1
  import { Router } from "express";
2
+ import { randomBytes } from "node:crypto";
2
3
  import { db, settingsQueries } from "@opentrust/db";
3
4
  import { maskSecret } from "@opentrust/shared";
4
5
  import { checkCoreHealth } from "../services/core-client.js";
5
6
  const settings = settingsQueries(db);
6
7
  export const settingsRouter = Router();
8
+ const SENSITIVE_KEYS = ["og_core_key", "session_token", "system_api_key"];
9
+ export function generateSystemApiKey() {
10
+ return `sk-ot-${randomBytes(16).toString("hex")}`;
11
+ }
12
+ export async function ensureSystemApiKey() {
13
+ const existing = await settings.get("system_api_key");
14
+ if (existing)
15
+ return existing;
16
+ const key = generateSystemApiKey();
17
+ await settings.set("system_api_key", key);
18
+ return key;
19
+ }
7
20
  // GET /api/settings
8
21
  settingsRouter.get("/", async (_req, res, next) => {
9
22
  try {
10
23
  const all = await settings.getAll();
11
- // Mask sensitive values
12
24
  const masked = {};
13
25
  for (const [key, value] of Object.entries(all)) {
14
- if (key === "og_core_key" || key === "session_token") {
26
+ if (SENSITIVE_KEYS.includes(key)) {
15
27
  masked[key] = maskSecret(value);
16
28
  }
17
29
  else {
@@ -32,8 +44,8 @@ settingsRouter.put("/", async (req, res, next) => {
32
44
  res.status(400).json({ success: false, error: "Request body must be a key-value object" });
33
45
  return;
34
46
  }
35
- // Prevent overwriting session_token via this endpoint
36
47
  delete updates.session_token;
48
+ delete updates.system_api_key;
37
49
  for (const [key, value] of Object.entries(updates)) {
38
50
  await settings.set(key, value);
39
51
  }
@@ -43,6 +55,34 @@ settingsRouter.put("/", async (req, res, next) => {
43
55
  next(err);
44
56
  }
45
57
  });
58
+ // GET /api/settings/api-key — reveal or masked system API key
59
+ settingsRouter.get("/api-key", async (req, res, next) => {
60
+ try {
61
+ const key = await ensureSystemApiKey();
62
+ const reveal = req.query.reveal === "true";
63
+ res.json({
64
+ success: true,
65
+ data: {
66
+ key: reveal ? key : maskSecret(key),
67
+ masked: !reveal,
68
+ },
69
+ });
70
+ }
71
+ catch (err) {
72
+ next(err);
73
+ }
74
+ });
75
+ // POST /api/settings/generate-key — regenerate system API key
76
+ settingsRouter.post("/generate-key", async (_req, res, next) => {
77
+ try {
78
+ const key = generateSystemApiKey();
79
+ await settings.set("system_api_key", key);
80
+ res.json({ success: true, data: { key } });
81
+ }
82
+ catch (err) {
83
+ next(err);
84
+ }
85
+ });
46
86
  // POST /api/settings/test-connection
47
87
  settingsRouter.post("/test-connection", async (_req, res, next) => {
48
88
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opentrust/dashboard",
3
- "version": "7.3.21",
3
+ "version": "7.3.22",
4
4
  "type": "module",
5
5
  "description": "OpenTrust Dashboard — management panel for AI Agent security (API + embedded web)",
6
6
  "main": "dist/index.js",
@@ -19,8 +19,8 @@
19
19
  "morgan": "^1.10.0",
20
20
  "nodemailer": "^8.0.1",
21
21
  "zod": "^3.23.0",
22
- "@opentrust/shared": "7.3.21",
23
- "@opentrust/db": "7.3.21"
22
+ "@opentrust/shared": "7.3.22",
23
+ "@opentrust/db": "7.3.22"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/cors": "^2.8.17",