@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.
- package/dist/commands/dashboard.js +9 -45
- package/dist/commands/init.js +120 -7
- package/dist/commands/reset.js +29 -16
- package/dist/index.js +0 -6
- package/dist/templates/auth-sidecar/Dockerfile +13 -1
- package/dist/templates/auth-sidecar/package.json +9 -0
- package/dist/templates/auth-sidecar/server.js +114 -155
- package/dist/templates/docker-compose.yml +4 -0
- package/package.json +1 -1
- package/templates/auth-sidecar/Dockerfile +13 -1
- package/templates/auth-sidecar/package.json +9 -0
- package/templates/auth-sidecar/server.js +114 -155
- package/templates/docker-compose.yml +4 -0
|
@@ -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,
|
|
28
|
+
const [dashboardUp, dbExporterUp] = await Promise.all([
|
|
51
29
|
checkPort(3000),
|
|
52
|
-
checkPort(
|
|
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
|
|
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 (!
|
|
62
|
-
console.warn(chalk_1.default.yellow("⚠️
|
|
63
|
-
console.warn(chalk_1.default.gray("
|
|
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("✅
|
|
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
|
-
|
|
74
|
-
|
|
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
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
console.log("");
|
|
89
|
-
|
|
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");
|
package/dist/commands/reset.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
22
|
+
const containerName = "cybermem-mcp";
|
|
14
23
|
// Check if container exists
|
|
15
24
|
try {
|
|
16
|
-
(0, child_process_1.execSync)(`docker inspect ${containerName}`, { stdio:
|
|
25
|
+
(0, child_process_1.execSync)(`docker inspect ${containerName}`, { stdio: "pipe" });
|
|
17
26
|
}
|
|
18
27
|
catch {
|
|
19
|
-
spinner.fail(
|
|
28
|
+
spinner.fail("Container not found. Is CyberMem running?");
|
|
20
29
|
process.exit(1);
|
|
21
30
|
}
|
|
22
31
|
// Remove SQLite files
|
|
23
|
-
spinner.text =
|
|
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:
|
|
34
|
+
stdio: "pipe",
|
|
26
35
|
});
|
|
27
36
|
// Restart container
|
|
28
|
-
spinner.text =
|
|
29
|
-
(0, child_process_1.execSync)(`docker restart ${containerName}`, { stdio:
|
|
37
|
+
spinner.text = "Restarting container...";
|
|
38
|
+
(0, child_process_1.execSync)(`docker restart ${containerName}`, { stdio: "pipe" });
|
|
30
39
|
// Wait for health
|
|
31
|
-
spinner.text =
|
|
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)(
|
|
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(
|
|
56
|
+
spinner.fail("Container failed to become healthy");
|
|
46
57
|
process.exit(1);
|
|
47
58
|
}
|
|
48
59
|
// Restart exporters
|
|
49
|
-
spinner.text =
|
|
60
|
+
spinner.text = "Restarting exporters...";
|
|
50
61
|
try {
|
|
51
|
-
(0, child_process_1.execSync)(
|
|
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(
|
|
57
|
-
console.log(chalk_1.default.gray(
|
|
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
|
-
|
|
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"]
|
|
@@ -2,98 +2,95 @@
|
|
|
2
2
|
* CyberMem Auth Sidecar
|
|
3
3
|
*
|
|
4
4
|
* ForwardAuth service for Traefik that validates:
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
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
|
|
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
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
//
|
|
42
|
-
function
|
|
33
|
+
// Verify token against SQLite access_keys table
|
|
34
|
+
async function verifyToken(token) {
|
|
43
35
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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("
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
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.
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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":
|
|
169
|
-
"X-
|
|
170
|
-
"X-
|
|
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
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
156
|
+
console.log("Auth FAILED: No valid token");
|
|
205
157
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
206
|
-
res.end(
|
|
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 (
|
|
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,5 +1,17 @@
|
|
|
1
|
-
|
|
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"]
|
|
@@ -2,98 +2,95 @@
|
|
|
2
2
|
* CyberMem Auth Sidecar
|
|
3
3
|
*
|
|
4
4
|
* ForwardAuth service for Traefik that validates:
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
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
|
|
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
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
//
|
|
42
|
-
function
|
|
33
|
+
// Verify token against SQLite access_keys table
|
|
34
|
+
async function verifyToken(token) {
|
|
43
35
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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("
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
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.
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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":
|
|
169
|
-
"X-
|
|
170
|
-
"X-
|
|
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
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
156
|
+
console.log("Auth FAILED: No valid token");
|
|
205
157
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
206
|
-
res.end(
|
|
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 (
|
|
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:
|