@cybermem/cli 0.8.5 → 0.8.9

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.
@@ -5,12 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.dashboard = dashboard;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
- const fs_1 = __importDefault(require("fs"));
9
8
  const net_1 = __importDefault(require("net"));
10
9
  const open_1 = __importDefault(require("open"));
11
- const os_1 = __importDefault(require("os"));
12
- const path_1 = __importDefault(require("path"));
13
- const TOKEN_FILE = path_1.default.join(os_1.default.homedir(), ".cybermem", "token.json");
14
10
  const checkPort = (port) => {
15
11
  return new Promise((resolve) => {
16
12
  const socket = new net_1.default.Socket();
@@ -27,64 +23,32 @@ const checkPort = (port) => {
27
23
  });
28
24
  });
29
25
  };
30
- /**
31
- * Get stored token from ~/.cybermem/token.json
32
- */
33
- function getStoredToken() {
34
- try {
35
- if (!fs_1.default.existsSync(TOKEN_FILE))
36
- return null;
37
- const data = JSON.parse(fs_1.default.readFileSync(TOKEN_FILE, "utf-8"));
38
- if (new Date(data.expires_at) < new Date()) {
39
- console.warn(chalk_1.default.yellow("Token expired. Run: cybermem-cli login"));
40
- return null;
41
- }
42
- return data.access_token;
43
- }
44
- catch {
45
- return null;
46
- }
47
- }
48
26
  async function dashboard(options) {
49
27
  console.log(chalk_1.default.blue("Checking CyberMem stack status..."));
50
- const [dashboardUp, prometheusUp] = await Promise.all([
28
+ const [dashboardUp, dbExporterUp] = await Promise.all([
51
29
  checkPort(3000),
52
- checkPort(9092),
30
+ checkPort(8000),
53
31
  ]);
54
32
  if (!dashboardUp) {
55
33
  console.error(chalk_1.default.red("❌ Dashboard is NOT running on port 3000."));
56
- console.log(chalk_1.default.yellow("Run 'cybermem up' or 'cd packages/dashboard && npm run dev' to start it."));
34
+ console.log(chalk_1.default.yellow("Run 'npx @cybermem/cli init' to start the stack."));
57
35
  }
58
36
  else {
59
37
  console.log(chalk_1.default.green("✅ Dashboard is running on port 3000."));
60
38
  }
61
- if (!prometheusUp) {
62
- console.warn(chalk_1.default.yellow("⚠️ Prometheus is NOT running on port 9092."));
63
- console.warn(chalk_1.default.gray(" Charts will be empty. Run 'cybermem up' or 'docker-compose up' to enable metrics."));
39
+ if (!dbExporterUp) {
40
+ console.warn(chalk_1.default.yellow("⚠️ db-exporter is NOT running on port 8000."));
41
+ console.warn(chalk_1.default.gray(" API may not be available. Run 'docker-compose up -d' to start services."));
64
42
  }
65
43
  else {
66
- console.log(chalk_1.default.green("✅ Prometheus is running on port 9092."));
44
+ console.log(chalk_1.default.green("✅ db-exporter is running on port 8000."));
67
45
  }
68
46
  if (dashboardUp) {
69
47
  console.log(chalk_1.default.blue("\nOpening dashboard..."));
70
48
  await (0, open_1.default)("http://localhost:3000");
71
49
  }
72
50
  else {
73
- // Try remote dashboard if local isn't up
74
- const token = getStoredToken();
75
- if (token) {
76
- // Check for remote URL from environment or config
77
- const remoteUrl = process.env.CYBERMEM_DASHBOARD_URL;
78
- if (remoteUrl) {
79
- console.log(chalk_1.default.blue("\nOpening remote dashboard..."));
80
- await (0, open_1.default)(`${remoteUrl}/api/auth/token?token=${token}`);
81
- }
82
- else {
83
- console.log(chalk_1.default.gray("\nTip: Set CYBERMEM_DASHBOARD_URL to open remote dashboard."));
84
- }
85
- }
86
- else {
87
- console.log(chalk_1.default.gray("\nTip: Run 'cybermem-cli login' to enable remote access."));
88
- }
51
+ console.log(chalk_1.default.gray("\nTip: Access remote dashboard at http://<your-server>:3000"));
52
+ console.log(chalk_1.default.gray(" Copy your access token from Settings to connect."));
89
53
  }
90
54
  }
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -11,6 +44,23 @@ const fs_1 = __importDefault(require("fs"));
11
44
  const inquirer_1 = __importDefault(require("inquirer"));
12
45
  const os_1 = __importDefault(require("os"));
13
46
  const path_1 = __importDefault(require("path"));
47
+ // Hash token using PBKDF2 (built-in, no bcrypt dependency)
48
+ async function hashToken(token) {
49
+ return new Promise((resolve, reject) => {
50
+ // Use a fixed salt prefix for deterministic validation
51
+ const salt = crypto_1.default
52
+ .createHash("sha256")
53
+ .update("cybermem-salt-v1")
54
+ .digest("hex")
55
+ .slice(0, 16);
56
+ crypto_1.default.pbkdf2(token, salt, 100000, 64, "sha512", (err, key) => {
57
+ if (err)
58
+ reject(err);
59
+ else
60
+ resolve(key.toString("hex"));
61
+ });
62
+ });
63
+ }
14
64
  async function init(options) {
15
65
  // Determine target from flags
16
66
  let target = "local";
@@ -80,13 +130,76 @@ async function init(options) {
80
130
  OM_API_KEY: "",
81
131
  },
82
132
  });
83
- console.log(chalk_1.default.green("\n🎉 CyberMem Installed!"));
84
- console.log("");
85
- console.log(chalk_1.default.bold("Next Steps:"));
86
- console.log(` 1. Open ${chalk_1.default.underline("http://localhost:3000/client-connect")} to connect your MCP clients`);
87
- console.log(` 2. Default password: ${chalk_1.default.bold("admin")} (you'll be prompted to change it)`);
88
- console.log("");
89
- console.log(chalk_1.default.dim("Local mode is active: No API key required for connections from this laptop."));
133
+ // Generate access token and store hash in SQLite
134
+ const accessToken = `sk-${crypto_1.default.randomBytes(24).toString("base64url")}`;
135
+ const bcryptHash = await hashToken(accessToken);
136
+ const dbPath = path_1.default.join(dataDir, "openmemory.sqlite");
137
+ // Wait for SQLite DB to be created by MCP server
138
+ console.log(chalk_1.default.blue("Initializing access token..."));
139
+ await new Promise((resolve) => setTimeout(resolve, 3000));
140
+ // Check if token already exists
141
+ try {
142
+ const sqlite3 = await Promise.resolve().then(() => __importStar(require("sqlite3")));
143
+ const db = new sqlite3.default.Database(dbPath);
144
+ // Create table if not exists (in case MCP hasn't run yet)
145
+ db.run(`CREATE TABLE IF NOT EXISTS access_keys (
146
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
147
+ key_hash TEXT NOT NULL,
148
+ name TEXT DEFAULT 'default',
149
+ user_id TEXT DEFAULT 'default',
150
+ created_at TEXT DEFAULT (datetime('now')),
151
+ last_used_at TEXT,
152
+ is_active INTEGER DEFAULT 1
153
+ );`);
154
+ // Check for existing key
155
+ db.get("SELECT COUNT(*) as count FROM access_keys WHERE is_active = 1", [], (err, row) => {
156
+ if (err || (row && row.count > 0)) {
157
+ db.close();
158
+ // Token exists, don't regenerate
159
+ console.log(chalk_1.default.gray("Access token already configured."));
160
+ printSuccessMessage(false);
161
+ return;
162
+ }
163
+ // Insert new key
164
+ db.run("INSERT INTO access_keys (id, key_hash, name, user_id) VALUES (?, ?, ?, ?)", [
165
+ crypto_1.default.randomBytes(8).toString("hex"),
166
+ bcryptHash,
167
+ "default",
168
+ "default",
169
+ ], (err) => {
170
+ db.close();
171
+ if (err) {
172
+ console.warn(chalk_1.default.yellow("Could not store access token: " + err.message));
173
+ printSuccessMessage(false);
174
+ }
175
+ else {
176
+ printSuccessMessage(true, accessToken);
177
+ }
178
+ });
179
+ });
180
+ }
181
+ catch (e) {
182
+ console.warn(chalk_1.default.yellow("Could not initialize access token: " + e.message));
183
+ console.log(chalk_1.default.gray("You can generate a token from the Dashboard Settings."));
184
+ printSuccessMessage(false);
185
+ }
186
+ function printSuccessMessage(showToken, token) {
187
+ console.log(chalk_1.default.green("\n🎉 CyberMem Installed!"));
188
+ console.log("");
189
+ if (showToken && token) {
190
+ console.log(chalk_1.default.bold("⚡ Your Access Token (save this!):"));
191
+ console.log(chalk_1.default.cyan.bold(` ${token}`));
192
+ console.log("");
193
+ console.log(chalk_1.default.gray(" Use this token to connect MCP clients from other devices."));
194
+ console.log(chalk_1.default.gray(" You can regenerate it from Dashboard Settings."));
195
+ console.log("");
196
+ }
197
+ console.log(chalk_1.default.bold("Next Steps:"));
198
+ console.log(` 1. Open ${chalk_1.default.underline("http://localhost:3000/client-setup")} to connect your MCP clients`);
199
+ console.log(` 2. Local access is auto-authenticated (no token needed on localhost)`);
200
+ console.log("");
201
+ console.log(chalk_1.default.dim("Local mode is active: No auth required for connections from this device."));
202
+ }
90
203
  }
91
204
  else if (target === "rpi" || target === "vps") {
92
205
  const composeFile = path_1.default.join(templateDir, "docker-compose.yml");
@@ -8,32 +8,43 @@ const chalk_1 = __importDefault(require("chalk"));
8
8
  const child_process_1 = require("child_process");
9
9
  const ora_1 = __importDefault(require("ora"));
10
10
  async function reset() {
11
- const spinner = (0, ora_1.default)('Resetting CyberMem database...').start();
11
+ // ⚠️ PRODUCTION PROTECTION - Never wipe RPi database
12
+ const cyberMemEnv = process.env.CYBERMEM_ENV || "";
13
+ const hostname = process.env.HOSTNAME || "";
14
+ if (cyberMemEnv === "production" || hostname.includes("raspberrypi")) {
15
+ console.error(chalk_1.default.red("❌ BLOCKED: Cannot reset database in production environment!"));
16
+ console.error(chalk_1.default.yellow(" CYBERMEM_ENV=" + cyberMemEnv + ", HOSTNAME=" + hostname));
17
+ console.error(chalk_1.default.gray(" Use npx @cybermem/cli backup first, then manually clear if needed."));
18
+ process.exit(1);
19
+ }
20
+ const spinner = (0, ora_1.default)("Resetting CyberMem database...").start();
12
21
  try {
13
- const containerName = 'cybermem-mcp';
22
+ const containerName = "cybermem-mcp";
14
23
  // Check if container exists
15
24
  try {
16
- (0, child_process_1.execSync)(`docker inspect ${containerName}`, { stdio: 'pipe' });
25
+ (0, child_process_1.execSync)(`docker inspect ${containerName}`, { stdio: "pipe" });
17
26
  }
18
27
  catch {
19
- spinner.fail('Container not found. Is CyberMem running?');
28
+ spinner.fail("Container not found. Is CyberMem running?");
20
29
  process.exit(1);
21
30
  }
22
31
  // Remove SQLite files
23
- spinner.text = 'Removing database files...';
32
+ spinner.text = "Removing database files...";
24
33
  (0, child_process_1.execSync)(`docker exec ${containerName} sh -c 'rm -f /data/openmemory.sqlite*'`, {
25
- stdio: 'pipe'
34
+ stdio: "pipe",
26
35
  });
27
36
  // Restart container
28
- spinner.text = 'Restarting container...';
29
- (0, child_process_1.execSync)(`docker restart ${containerName}`, { stdio: 'pipe' });
37
+ spinner.text = "Restarting container...";
38
+ (0, child_process_1.execSync)(`docker restart ${containerName}`, { stdio: "pipe" });
30
39
  // Wait for health
31
- spinner.text = 'Waiting for health check...';
40
+ spinner.text = "Waiting for health check...";
32
41
  let healthy = false;
33
42
  for (let i = 0; i < 30; i++) {
34
- await new Promise(r => setTimeout(r, 2000));
43
+ await new Promise((r) => setTimeout(r, 2000));
35
44
  try {
36
- (0, child_process_1.execSync)('curl -s http://localhost:8626/health | grep -q ok', { stdio: 'pipe' });
45
+ (0, child_process_1.execSync)("curl -s http://localhost:8626/health | grep -q ok", {
46
+ stdio: "pipe",
47
+ });
37
48
  healthy = true;
38
49
  break;
39
50
  }
@@ -42,19 +53,21 @@ async function reset() {
42
53
  }
43
54
  }
44
55
  if (!healthy) {
45
- spinner.fail('Container failed to become healthy');
56
+ spinner.fail("Container failed to become healthy");
46
57
  process.exit(1);
47
58
  }
48
59
  // Restart exporters
49
- spinner.text = 'Restarting exporters...';
60
+ spinner.text = "Restarting exporters...";
50
61
  try {
51
- (0, child_process_1.execSync)('docker restart cybermem-log-exporter cybermem-db-exporter', { stdio: 'pipe' });
62
+ (0, child_process_1.execSync)("docker restart cybermem-log-exporter cybermem-db-exporter", {
63
+ stdio: "pipe",
64
+ });
52
65
  }
53
66
  catch {
54
67
  // Exporters may not exist
55
68
  }
56
- spinner.succeed(chalk_1.default.green('Database reset successfully!'));
57
- console.log(chalk_1.default.gray(' All memories have been deleted.'));
69
+ spinner.succeed(chalk_1.default.green("Database reset successfully!"));
70
+ console.log(chalk_1.default.gray(" All memories have been deleted."));
58
71
  }
59
72
  catch (error) {
60
73
  spinner.fail(`Reset failed: ${error.message}`);
package/dist/index.js CHANGED
@@ -5,7 +5,6 @@ const commander_1 = require("commander");
5
5
  const backup_1 = require("./commands/backup");
6
6
  const dashboard_1 = require("./commands/dashboard");
7
7
  const init_1 = require("./commands/init");
8
- const login_1 = require("./commands/login");
9
8
  const reset_1 = require("./commands/reset");
10
9
  const restore_1 = require("./commands/restore");
11
10
  const upgrade_1 = require("./commands/upgrade");
@@ -14,11 +13,6 @@ program
14
13
  .name("mcp")
15
14
  .description("CyberMem - Deploy your AI memory server in one command")
16
15
  .version("1.0.0");
17
- // Command: Login
18
- program
19
- .command("login")
20
- .description("Login to CyberMem via GitHub (OAuth)")
21
- .action(login_1.login);
22
16
  // Command: Init (formerly deploy)
23
17
  program
24
18
  .command("init")
@@ -1,5 +1,17 @@
1
- FROM node:20-alpine
1
+ # Builder stage for native modules
2
+ FROM node:20-alpine AS builder
2
3
  WORKDIR /app
4
+ RUN apk add --no-cache python3 make g++
5
+ COPY package.json ./
6
+ RUN npm install
7
+
8
+ # Production stage
9
+ FROM node:20-alpine AS runner
10
+ WORKDIR /app
11
+ RUN apk add --no-cache libc6-compat
12
+ COPY --from=builder /app/node_modules ./node_modules
3
13
  COPY server.js .
14
+ COPY package.json .
15
+
4
16
  EXPOSE 3001
5
17
  CMD ["node", "server.js"]
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "cybermem-auth-sidecar",
3
+ "version": "0.1.0",
4
+ "description": "Auth sidecar for CyberMem",
5
+ "main": "server.js",
6
+ "dependencies": {
7
+ "sqlite3": "5.1.7"
8
+ }
9
+ }
@@ -2,98 +2,95 @@
2
2
  * CyberMem Auth Sidecar
3
3
  *
4
4
  * ForwardAuth service for Traefik that validates:
5
- * 1. JWT tokens (RS256) with embedded public key
6
- * 2. API keys (X-API-Key header) - deprecated fallback
7
- * 3. Local requests (localhost bypass)
5
+ * 1. Bearer tokens (sk-xxx) against SQLite access_keys table
6
+ * 2. Local requests bypass (localhost, *.local domains)
8
7
  *
9
- * NO SECRETS REQUIRED - public key is embedded.
8
+ * NO EXTERNAL DEPENDENCIES - uses built-in crypto and sqlite3.
10
9
  */
11
10
 
12
11
  const http = require("http");
13
- const fs = require("fs");
14
12
  const crypto = require("crypto");
13
+ const path = require("path");
15
14
 
16
15
  const PORT = process.env.PORT || 3001;
17
- const API_KEY_FILE = process.env.API_KEY_FILE || "/.env";
18
-
19
- // RSA Public Key for JWT verification (embedded - no secrets!)
20
- const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
21
- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkrWPslHt+dcX/lckX4mw
22
- AaI4koCqn7NqEkTtuyJuzFv969Da0ghhWdTIRR6H8pYfsTtqtX2UAZox8i5IJ9t9
23
- JS8nBfbL2fFiuEz51LMNKMSLw7j2dJT/g5iIdT64LyJZ/9+kLMXC
24
- EBWPIyEvx4GMzKSf2L+jNaUY/0J8n/JNAbKtIplKtfOU/tNWuoZfcj3SnoxrmApN
25
- Xw+LsE26EM2Gq7MKLQf3r3GUIm2dBgs7XUNJRiezrPgFzekiaiDyFsNhhk1jkx2I
26
- ljQgSslGQ4dODE73KB07b0Qi7zPWAtGlCyDQD5RLICzht1mMENta7x+TlPJfDv8g
27
- XeEmW5ihAgMBAAE=
28
- -----END PUBLIC KEY-----`;
29
-
30
- // Load API key from file (deprecated fallback)
31
- function loadApiKey() {
32
- try {
33
- const content = fs.readFileSync(API_KEY_FILE, "utf-8");
34
- const match = content.match(/OM_API_KEY=(.+)/);
35
- return match ? match[1].trim() : null;
36
- } catch {
37
- return null;
38
- }
16
+ const DB_PATH = process.env.OM_DB_PATH || "/data/openmemory.sqlite";
17
+
18
+ // Hash token using same PBKDF2 as CLI (for verification)
19
+ function hashToken(token) {
20
+ return new Promise((resolve, reject) => {
21
+ const salt = crypto
22
+ .createHash("sha256")
23
+ .update("cybermem-salt-v1")
24
+ .digest("hex")
25
+ .slice(0, 16);
26
+ crypto.pbkdf2(token, salt, 100000, 64, "sha512", (err, key) => {
27
+ if (err) reject(err);
28
+ else resolve(key.toString("hex"));
29
+ });
30
+ });
39
31
  }
40
32
 
41
- // RS256 JWT validation
42
- function validateJwt(token) {
33
+ // Verify token against SQLite access_keys table
34
+ async function verifyToken(token) {
43
35
  try {
44
- const parts = token.split(".");
45
- if (parts.length !== 3) return null;
46
-
47
- const [headerB64, payloadB64, signatureB64] = parts;
48
-
49
- // Decode header to check algorithm
50
- const header = JSON.parse(Buffer.from(headerB64, "base64url").toString());
51
- if (header.alg !== "RS256") {
52
- console.log("JWT: unsupported algorithm", header.alg);
53
- return null;
54
- }
55
-
56
- // Verify RS256 signature
57
- const data = `${headerB64}.${payloadB64}`;
58
- const signature = Buffer.from(signatureB64, "base64url");
59
-
60
- const verify = crypto.createVerify("RSA-SHA256");
61
- verify.update(data);
62
-
63
- if (!verify.verify(PUBLIC_KEY, signature)) {
64
- console.log("JWT: signature verification failed");
65
- return null;
66
- }
67
-
68
- // Decode payload
69
- const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
70
-
71
- // Check expiration
72
- if (payload.exp && payload.exp < Date.now() / 1000) {
73
- console.log("JWT: token expired");
74
- return null;
75
- }
76
-
77
- // Check issuer
78
- if (payload.iss !== "cybermem.dev") {
79
- console.log("JWT: invalid issuer", payload.iss);
80
- return null;
81
- }
82
-
83
- return payload;
36
+ const sqlite3 = require("sqlite3").verbose();
37
+ const db = new sqlite3.Database(DB_PATH);
38
+
39
+ const tokenHash = await hashToken(token);
40
+
41
+ return new Promise((resolve, reject) => {
42
+ db.get(
43
+ "SELECT user_id, name FROM access_keys WHERE key_hash = ? AND is_active = 1",
44
+ [tokenHash],
45
+ (err, row) => {
46
+ db.close();
47
+ if (err) {
48
+ console.log("DB error:", err.message);
49
+ resolve(null);
50
+ } else if (row) {
51
+ // Update last_used_at
52
+ const updateDb = new sqlite3.Database(DB_PATH);
53
+ updateDb.run(
54
+ "UPDATE access_keys SET last_used_at = datetime('now') WHERE key_hash = ?",
55
+ [tokenHash],
56
+ );
57
+ updateDb.close();
58
+ resolve({ userId: row.user_id, name: row.name });
59
+ } else {
60
+ resolve(null);
61
+ }
62
+ },
63
+ );
64
+ });
84
65
  } catch (err) {
85
- console.log("JWT validation error:", err.message);
66
+ console.log("Token verification error:", err.message);
86
67
  return null;
87
68
  }
88
69
  }
89
70
 
90
- // Check if request is from localhost
71
+ // Check if request is from localhost or local network
91
72
  function isLocalRequest(req) {
92
73
  const forwarded = req.headers["x-forwarded-for"];
93
74
  const realIp = req.headers["x-real-ip"];
94
- const ip = forwarded?.split(",")[0] || realIp || req.socket.remoteAddress;
95
-
96
- return ip === "127.0.0.1" || ip === "::1" || ip === "localhost";
75
+ const host = req.headers["x-forwarded-host"] || req.headers["host"] || "";
76
+ const ip =
77
+ forwarded?.split(",")[0]?.trim() || realIp || req.socket.remoteAddress;
78
+
79
+ // IP-based local check
80
+ const isLocalIp =
81
+ ip === "127.0.0.1" ||
82
+ ip === "::1" ||
83
+ ip === "::ffff:127.0.0.1" ||
84
+ ip === "localhost";
85
+
86
+ // Host-based local check (raspberrypi.local, localhost, *.local)
87
+ const isLocalHost =
88
+ host.includes("localhost") ||
89
+ host.includes("127.0.0.1") ||
90
+ host.includes("raspberrypi.local") ||
91
+ host.match(/\.local(:\d+)?$/);
92
+
93
+ return isLocalIp || isLocalHost;
97
94
  }
98
95
 
99
96
  // ForwardAuth handler
@@ -101,111 +98,73 @@ const server = http.createServer(async (req, res) => {
101
98
  // Health check
102
99
  if (req.url === "/health") {
103
100
  res.writeHead(200, { "Content-Type": "application/json" });
104
- res.end(JSON.stringify({ status: "ok" }));
101
+ res.end(JSON.stringify({ status: "ok", mode: "token-auth" }));
105
102
  return;
106
103
  }
107
104
 
108
105
  const authHeader = req.headers["authorization"];
109
106
  const apiKeyHeader = req.headers["x-api-key"];
110
107
 
111
- // 1. Check Bearer token (JWT or API Key)
108
+ // 1. Local bypass - no auth required for localhost
109
+ if (isLocalRequest(req)) {
110
+ console.log("Auth OK: Local bypass");
111
+ res.writeHead(200, {
112
+ "X-Auth-Method": "local",
113
+ "X-User-Id": "local",
114
+ });
115
+ res.end();
116
+ return;
117
+ }
118
+
119
+ // 2. Check Bearer token (sk-xxx format)
112
120
  if (authHeader?.startsWith("Bearer ")) {
113
121
  const token = authHeader.substring(7);
114
122
 
115
- // 1a. Check if Bearer token is actually an API key (MCP clients like Claude Desktop)
116
- const expectedKey = loadApiKey();
117
- if (expectedKey && token === expectedKey) {
118
- console.log("Auth OK: Bearer API Key");
119
- res.writeHead(200, {
120
- "X-Auth-Method": "bearer-api-key",
121
- });
122
- res.end();
123
- return;
124
- }
125
-
126
- // 1b. Try JWT RS256 validation
127
- const payload = validateJwt(token);
123
+ if (token.startsWith("sk-")) {
124
+ const result = await verifyToken(token);
128
125
 
129
- if (payload) {
130
- console.log(`Auth OK: JWT (${payload.email || payload.sub})`);
131
- res.writeHead(200, {
132
- "X-User-Id": payload.sub || "",
133
- "X-User-Email": payload.email || "",
134
- "X-User-Name": payload.name || "",
135
- "X-Auth-Method": "jwt",
136
- });
137
- res.end();
138
- return;
139
- }
140
-
141
- // 1c. Try GitHub OAuth token verification
142
- try {
143
- const https = require("https");
144
- const ghRes = await new Promise((resolve, reject) => {
145
- const req = https.get(
146
- "https://api.github.com/user",
147
- {
148
- headers: {
149
- Authorization: `Bearer ${token}`,
150
- "User-Agent": "CyberMem-Auth-Sidecar/1.0",
151
- Accept: "application/vnd.github+json",
152
- },
153
- },
154
- (res) => {
155
- let data = "";
156
- res.on("data", (chunk) => (data += chunk));
157
- res.on("end", () => resolve({ status: res.statusCode, data }));
158
- },
159
- );
160
- req.on("error", reject);
161
- req.end();
162
- });
163
-
164
- if (ghRes.status === 200) {
165
- const user = JSON.parse(ghRes.data);
166
- console.log(`Auth OK: GitHub OAuth (${user.login})`);
126
+ if (result) {
127
+ console.log(`Auth OK: Token (${result.name || result.userId})`);
167
128
  res.writeHead(200, {
168
- "X-User-Id": String(user.id),
169
- "X-User-Email": user.email || "",
170
- "X-User-Name": user.login,
171
- "X-Auth-Method": "github-oauth",
129
+ "X-User-Id": result.userId,
130
+ "X-Auth-Method": "token",
131
+ "X-Token-Name": result.name || "",
172
132
  });
173
133
  res.end();
174
134
  return;
175
135
  }
176
- } catch (err) {
177
- // GitHub verification failed, continue to other methods
178
136
  }
179
137
  }
180
138
 
181
- // 2. Check API Key (deprecated fallback)
182
- const expectedKey = loadApiKey();
183
- if (apiKeyHeader && expectedKey && apiKeyHeader === expectedKey) {
184
- console.log("Auth OK: API Key (deprecated)");
185
- res.writeHead(200, {
186
- "X-Auth-Method": "api-key",
187
- "X-Auth-Deprecated": "true",
188
- });
189
- res.end();
190
- return;
191
- }
139
+ // 3. Check X-API-Key header (sk-xxx format)
140
+ if (apiKeyHeader?.startsWith("sk-")) {
141
+ const result = await verifyToken(apiKeyHeader);
192
142
 
193
- // 3. Local bypass (development)
194
- if (isLocalRequest(req)) {
195
- console.log("Auth OK: Local bypass");
196
- res.writeHead(200, {
197
- "X-Auth-Method": "local",
198
- });
199
- res.end();
200
- return;
143
+ if (result) {
144
+ console.log(`Auth OK: API-Key Header (${result.name || result.userId})`);
145
+ res.writeHead(200, {
146
+ "X-User-Id": result.userId,
147
+ "X-Auth-Method": "api-key",
148
+ "X-Token-Name": result.name || "",
149
+ });
150
+ res.end();
151
+ return;
152
+ }
201
153
  }
202
154
 
203
155
  // 4. Unauthorized
204
- console.log("Auth FAILED: No valid credentials");
156
+ console.log("Auth FAILED: No valid token");
205
157
  res.writeHead(401, { "Content-Type": "application/json" });
206
- res.end(JSON.stringify({ error: "Unauthorized" }));
158
+ res.end(
159
+ JSON.stringify({
160
+ error: "Unauthorized",
161
+ message:
162
+ "Valid access token required. Get your token from Dashboard Settings.",
163
+ }),
164
+ );
207
165
  });
208
166
 
209
167
  server.listen(PORT, () => {
210
- console.log(`Auth sidecar (RS256) listening on port ${PORT}`);
168
+ console.log(`Auth sidecar (token-auth) listening on port ${PORT}`);
169
+ console.log(`DB path: ${DB_PATH}`);
211
170
  });
@@ -79,9 +79,11 @@ services:
79
79
  container_name: cybermem-auth-sidecar
80
80
  environment:
81
81
  PORT: "3001"
82
+ OM_DB_PATH: /data/openmemory.sqlite
82
83
  API_KEY_FILE: /.env
83
84
  volumes:
84
85
  - ${CYBERMEM_ENV_PATH:-${HOME}/.cybermem/.env}:/.env:ro
86
+ - ${HOME}/.cybermem/data:/data:ro
85
87
  labels:
86
88
  - traefik.enable=true
87
89
  healthcheck:
@@ -173,6 +175,8 @@ services:
173
175
  CYBERMEM_URL: http://mcp-server:8080
174
176
  OM_DB_PATH: /data/openmemory.sqlite
175
177
  OM_API_KEY: ${OM_API_KEY:-dev-secret-key}
178
+ # Tailscale domain for remote config (optional, auto-detected if not set)
179
+ TAILSCALE_DOMAIN: ${TAILSCALE_DOMAIN:-}
176
180
  ports:
177
181
  - "3000:3000"
178
182
  volumes:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cybermem/cli",
3
- "version": "0.8.5",
3
+ "version": "0.8.9",
4
4
  "description": "CyberMem — Universal Long-Term Memory for AI Agents",
5
5
  "homepage": "https://cybermem.dev",
6
6
  "repository": {
@@ -1,5 +1,17 @@
1
- FROM node:20-alpine
1
+ # Builder stage for native modules
2
+ FROM node:20-alpine AS builder
2
3
  WORKDIR /app
4
+ RUN apk add --no-cache python3 make g++
5
+ COPY package.json ./
6
+ RUN npm install
7
+
8
+ # Production stage
9
+ FROM node:20-alpine AS runner
10
+ WORKDIR /app
11
+ RUN apk add --no-cache libc6-compat
12
+ COPY --from=builder /app/node_modules ./node_modules
3
13
  COPY server.js .
14
+ COPY package.json .
15
+
4
16
  EXPOSE 3001
5
17
  CMD ["node", "server.js"]
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "cybermem-auth-sidecar",
3
+ "version": "0.1.0",
4
+ "description": "Auth sidecar for CyberMem",
5
+ "main": "server.js",
6
+ "dependencies": {
7
+ "sqlite3": "5.1.7"
8
+ }
9
+ }
@@ -2,98 +2,95 @@
2
2
  * CyberMem Auth Sidecar
3
3
  *
4
4
  * ForwardAuth service for Traefik that validates:
5
- * 1. JWT tokens (RS256) with embedded public key
6
- * 2. API keys (X-API-Key header) - deprecated fallback
7
- * 3. Local requests (localhost bypass)
5
+ * 1. Bearer tokens (sk-xxx) against SQLite access_keys table
6
+ * 2. Local requests bypass (localhost, *.local domains)
8
7
  *
9
- * NO SECRETS REQUIRED - public key is embedded.
8
+ * NO EXTERNAL DEPENDENCIES - uses built-in crypto and sqlite3.
10
9
  */
11
10
 
12
11
  const http = require("http");
13
- const fs = require("fs");
14
12
  const crypto = require("crypto");
13
+ const path = require("path");
15
14
 
16
15
  const PORT = process.env.PORT || 3001;
17
- const API_KEY_FILE = process.env.API_KEY_FILE || "/.env";
18
-
19
- // RSA Public Key for JWT verification (embedded - no secrets!)
20
- const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
21
- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkrWPslHt+dcX/lckX4mw
22
- AaI4koCqn7NqEkTtuyJuzFv969Da0ghhWdTIRR6H8pYfsTtqtX2UAZox8i5IJ9t9
23
- JS8nBfbL2fFiuEz51LMNKMSLw7j2dJT/g5iIdT64LyJZ/9+kLMXC
24
- EBWPIyEvx4GMzKSf2L+jNaUY/0J8n/JNAbKtIplKtfOU/tNWuoZfcj3SnoxrmApN
25
- Xw+LsE26EM2Gq7MKLQf3r3GUIm2dBgs7XUNJRiezrPgFzekiaiDyFsNhhk1jkx2I
26
- ljQgSslGQ4dODE73KB07b0Qi7zPWAtGlCyDQD5RLICzht1mMENta7x+TlPJfDv8g
27
- XeEmW5ihAgMBAAE=
28
- -----END PUBLIC KEY-----`;
29
-
30
- // Load API key from file (deprecated fallback)
31
- function loadApiKey() {
32
- try {
33
- const content = fs.readFileSync(API_KEY_FILE, "utf-8");
34
- const match = content.match(/OM_API_KEY=(.+)/);
35
- return match ? match[1].trim() : null;
36
- } catch {
37
- return null;
38
- }
16
+ const DB_PATH = process.env.OM_DB_PATH || "/data/openmemory.sqlite";
17
+
18
+ // Hash token using same PBKDF2 as CLI (for verification)
19
+ function hashToken(token) {
20
+ return new Promise((resolve, reject) => {
21
+ const salt = crypto
22
+ .createHash("sha256")
23
+ .update("cybermem-salt-v1")
24
+ .digest("hex")
25
+ .slice(0, 16);
26
+ crypto.pbkdf2(token, salt, 100000, 64, "sha512", (err, key) => {
27
+ if (err) reject(err);
28
+ else resolve(key.toString("hex"));
29
+ });
30
+ });
39
31
  }
40
32
 
41
- // RS256 JWT validation
42
- function validateJwt(token) {
33
+ // Verify token against SQLite access_keys table
34
+ async function verifyToken(token) {
43
35
  try {
44
- const parts = token.split(".");
45
- if (parts.length !== 3) return null;
46
-
47
- const [headerB64, payloadB64, signatureB64] = parts;
48
-
49
- // Decode header to check algorithm
50
- const header = JSON.parse(Buffer.from(headerB64, "base64url").toString());
51
- if (header.alg !== "RS256") {
52
- console.log("JWT: unsupported algorithm", header.alg);
53
- return null;
54
- }
55
-
56
- // Verify RS256 signature
57
- const data = `${headerB64}.${payloadB64}`;
58
- const signature = Buffer.from(signatureB64, "base64url");
59
-
60
- const verify = crypto.createVerify("RSA-SHA256");
61
- verify.update(data);
62
-
63
- if (!verify.verify(PUBLIC_KEY, signature)) {
64
- console.log("JWT: signature verification failed");
65
- return null;
66
- }
67
-
68
- // Decode payload
69
- const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
70
-
71
- // Check expiration
72
- if (payload.exp && payload.exp < Date.now() / 1000) {
73
- console.log("JWT: token expired");
74
- return null;
75
- }
76
-
77
- // Check issuer
78
- if (payload.iss !== "cybermem.dev") {
79
- console.log("JWT: invalid issuer", payload.iss);
80
- return null;
81
- }
82
-
83
- return payload;
36
+ const sqlite3 = require("sqlite3").verbose();
37
+ const db = new sqlite3.Database(DB_PATH);
38
+
39
+ const tokenHash = await hashToken(token);
40
+
41
+ return new Promise((resolve, reject) => {
42
+ db.get(
43
+ "SELECT user_id, name FROM access_keys WHERE key_hash = ? AND is_active = 1",
44
+ [tokenHash],
45
+ (err, row) => {
46
+ db.close();
47
+ if (err) {
48
+ console.log("DB error:", err.message);
49
+ resolve(null);
50
+ } else if (row) {
51
+ // Update last_used_at
52
+ const updateDb = new sqlite3.Database(DB_PATH);
53
+ updateDb.run(
54
+ "UPDATE access_keys SET last_used_at = datetime('now') WHERE key_hash = ?",
55
+ [tokenHash],
56
+ );
57
+ updateDb.close();
58
+ resolve({ userId: row.user_id, name: row.name });
59
+ } else {
60
+ resolve(null);
61
+ }
62
+ },
63
+ );
64
+ });
84
65
  } catch (err) {
85
- console.log("JWT validation error:", err.message);
66
+ console.log("Token verification error:", err.message);
86
67
  return null;
87
68
  }
88
69
  }
89
70
 
90
- // Check if request is from localhost
71
+ // Check if request is from localhost or local network
91
72
  function isLocalRequest(req) {
92
73
  const forwarded = req.headers["x-forwarded-for"];
93
74
  const realIp = req.headers["x-real-ip"];
94
- const ip = forwarded?.split(",")[0] || realIp || req.socket.remoteAddress;
95
-
96
- return ip === "127.0.0.1" || ip === "::1" || ip === "localhost";
75
+ const host = req.headers["x-forwarded-host"] || req.headers["host"] || "";
76
+ const ip =
77
+ forwarded?.split(",")[0]?.trim() || realIp || req.socket.remoteAddress;
78
+
79
+ // IP-based local check
80
+ const isLocalIp =
81
+ ip === "127.0.0.1" ||
82
+ ip === "::1" ||
83
+ ip === "::ffff:127.0.0.1" ||
84
+ ip === "localhost";
85
+
86
+ // Host-based local check (raspberrypi.local, localhost, *.local)
87
+ const isLocalHost =
88
+ host.includes("localhost") ||
89
+ host.includes("127.0.0.1") ||
90
+ host.includes("raspberrypi.local") ||
91
+ host.match(/\.local(:\d+)?$/);
92
+
93
+ return isLocalIp || isLocalHost;
97
94
  }
98
95
 
99
96
  // ForwardAuth handler
@@ -101,111 +98,73 @@ const server = http.createServer(async (req, res) => {
101
98
  // Health check
102
99
  if (req.url === "/health") {
103
100
  res.writeHead(200, { "Content-Type": "application/json" });
104
- res.end(JSON.stringify({ status: "ok" }));
101
+ res.end(JSON.stringify({ status: "ok", mode: "token-auth" }));
105
102
  return;
106
103
  }
107
104
 
108
105
  const authHeader = req.headers["authorization"];
109
106
  const apiKeyHeader = req.headers["x-api-key"];
110
107
 
111
- // 1. Check Bearer token (JWT or API Key)
108
+ // 1. Local bypass - no auth required for localhost
109
+ if (isLocalRequest(req)) {
110
+ console.log("Auth OK: Local bypass");
111
+ res.writeHead(200, {
112
+ "X-Auth-Method": "local",
113
+ "X-User-Id": "local",
114
+ });
115
+ res.end();
116
+ return;
117
+ }
118
+
119
+ // 2. Check Bearer token (sk-xxx format)
112
120
  if (authHeader?.startsWith("Bearer ")) {
113
121
  const token = authHeader.substring(7);
114
122
 
115
- // 1a. Check if Bearer token is actually an API key (MCP clients like Claude Desktop)
116
- const expectedKey = loadApiKey();
117
- if (expectedKey && token === expectedKey) {
118
- console.log("Auth OK: Bearer API Key");
119
- res.writeHead(200, {
120
- "X-Auth-Method": "bearer-api-key",
121
- });
122
- res.end();
123
- return;
124
- }
125
-
126
- // 1b. Try JWT RS256 validation
127
- const payload = validateJwt(token);
123
+ if (token.startsWith("sk-")) {
124
+ const result = await verifyToken(token);
128
125
 
129
- if (payload) {
130
- console.log(`Auth OK: JWT (${payload.email || payload.sub})`);
131
- res.writeHead(200, {
132
- "X-User-Id": payload.sub || "",
133
- "X-User-Email": payload.email || "",
134
- "X-User-Name": payload.name || "",
135
- "X-Auth-Method": "jwt",
136
- });
137
- res.end();
138
- return;
139
- }
140
-
141
- // 1c. Try GitHub OAuth token verification
142
- try {
143
- const https = require("https");
144
- const ghRes = await new Promise((resolve, reject) => {
145
- const req = https.get(
146
- "https://api.github.com/user",
147
- {
148
- headers: {
149
- Authorization: `Bearer ${token}`,
150
- "User-Agent": "CyberMem-Auth-Sidecar/1.0",
151
- Accept: "application/vnd.github+json",
152
- },
153
- },
154
- (res) => {
155
- let data = "";
156
- res.on("data", (chunk) => (data += chunk));
157
- res.on("end", () => resolve({ status: res.statusCode, data }));
158
- },
159
- );
160
- req.on("error", reject);
161
- req.end();
162
- });
163
-
164
- if (ghRes.status === 200) {
165
- const user = JSON.parse(ghRes.data);
166
- console.log(`Auth OK: GitHub OAuth (${user.login})`);
126
+ if (result) {
127
+ console.log(`Auth OK: Token (${result.name || result.userId})`);
167
128
  res.writeHead(200, {
168
- "X-User-Id": String(user.id),
169
- "X-User-Email": user.email || "",
170
- "X-User-Name": user.login,
171
- "X-Auth-Method": "github-oauth",
129
+ "X-User-Id": result.userId,
130
+ "X-Auth-Method": "token",
131
+ "X-Token-Name": result.name || "",
172
132
  });
173
133
  res.end();
174
134
  return;
175
135
  }
176
- } catch (err) {
177
- // GitHub verification failed, continue to other methods
178
136
  }
179
137
  }
180
138
 
181
- // 2. Check API Key (deprecated fallback)
182
- const expectedKey = loadApiKey();
183
- if (apiKeyHeader && expectedKey && apiKeyHeader === expectedKey) {
184
- console.log("Auth OK: API Key (deprecated)");
185
- res.writeHead(200, {
186
- "X-Auth-Method": "api-key",
187
- "X-Auth-Deprecated": "true",
188
- });
189
- res.end();
190
- return;
191
- }
139
+ // 3. Check X-API-Key header (sk-xxx format)
140
+ if (apiKeyHeader?.startsWith("sk-")) {
141
+ const result = await verifyToken(apiKeyHeader);
192
142
 
193
- // 3. Local bypass (development)
194
- if (isLocalRequest(req)) {
195
- console.log("Auth OK: Local bypass");
196
- res.writeHead(200, {
197
- "X-Auth-Method": "local",
198
- });
199
- res.end();
200
- return;
143
+ if (result) {
144
+ console.log(`Auth OK: API-Key Header (${result.name || result.userId})`);
145
+ res.writeHead(200, {
146
+ "X-User-Id": result.userId,
147
+ "X-Auth-Method": "api-key",
148
+ "X-Token-Name": result.name || "",
149
+ });
150
+ res.end();
151
+ return;
152
+ }
201
153
  }
202
154
 
203
155
  // 4. Unauthorized
204
- console.log("Auth FAILED: No valid credentials");
156
+ console.log("Auth FAILED: No valid token");
205
157
  res.writeHead(401, { "Content-Type": "application/json" });
206
- res.end(JSON.stringify({ error: "Unauthorized" }));
158
+ res.end(
159
+ JSON.stringify({
160
+ error: "Unauthorized",
161
+ message:
162
+ "Valid access token required. Get your token from Dashboard Settings.",
163
+ }),
164
+ );
207
165
  });
208
166
 
209
167
  server.listen(PORT, () => {
210
- console.log(`Auth sidecar (RS256) listening on port ${PORT}`);
168
+ console.log(`Auth sidecar (token-auth) listening on port ${PORT}`);
169
+ console.log(`DB path: ${DB_PATH}`);
211
170
  });
@@ -79,9 +79,11 @@ services:
79
79
  container_name: cybermem-auth-sidecar
80
80
  environment:
81
81
  PORT: "3001"
82
+ OM_DB_PATH: /data/openmemory.sqlite
82
83
  API_KEY_FILE: /.env
83
84
  volumes:
84
85
  - ${CYBERMEM_ENV_PATH:-${HOME}/.cybermem/.env}:/.env:ro
86
+ - ${HOME}/.cybermem/data:/data:ro
85
87
  labels:
86
88
  - traefik.enable=true
87
89
  healthcheck:
@@ -173,6 +175,8 @@ services:
173
175
  CYBERMEM_URL: http://mcp-server:8080
174
176
  OM_DB_PATH: /data/openmemory.sqlite
175
177
  OM_API_KEY: ${OM_API_KEY:-dev-secret-key}
178
+ # Tailscale domain for remote config (optional, auto-detected if not set)
179
+ TAILSCALE_DOMAIN: ${TAILSCALE_DOMAIN:-}
176
180
  ports:
177
181
  - "3000:3000"
178
182
  volumes: