@cybermem/cli 0.9.12 → 0.13.3

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.
Files changed (56) hide show
  1. package/dist/commands/install.js +430 -0
  2. package/dist/commands/reset.js +18 -2
  3. package/dist/commands/uninstall.js +145 -0
  4. package/dist/commands/upgrade.js +91 -52
  5. package/dist/index.js +18 -5
  6. package/dist/templates/ansible/playbooks/deploy-cybermem.yml +201 -24
  7. package/dist/templates/ansible/playbooks/reset-db.yml +44 -0
  8. package/dist/templates/auth-sidecar/Dockerfile +2 -10
  9. package/dist/templates/auth-sidecar/package.json +1 -3
  10. package/dist/templates/auth-sidecar/server.js +149 -110
  11. package/dist/templates/charts/cybermem/.helmignore +13 -0
  12. package/dist/templates/charts/cybermem/templates/dashboard-deployment.yaml +31 -7
  13. package/dist/templates/charts/cybermem/templates/dashboard-service.yaml +4 -4
  14. package/dist/templates/charts/cybermem/templates/openmemory-deployment.yaml +14 -8
  15. package/dist/templates/charts/cybermem/templates/openmemory-service.yaml +3 -3
  16. package/dist/templates/charts/cybermem/templates/secret.yaml +9 -0
  17. package/dist/templates/charts/cybermem/templates/traefik-config.yaml +67 -0
  18. package/dist/templates/charts/cybermem/templates/traefik-deployment.yaml +53 -0
  19. package/dist/templates/charts/cybermem/templates/traefik-service.yaml +17 -0
  20. package/dist/templates/charts/cybermem/values-vps.yaml +8 -4
  21. package/dist/templates/charts/cybermem/values.yaml +17 -9
  22. package/dist/templates/docker-compose.yml +103 -78
  23. package/dist/templates/monitoring/log_exporter/exporter.py +22 -29
  24. package/dist/templates/monitoring/traefik/traefik.yml +1 -4
  25. package/package.json +9 -3
  26. package/templates/ansible/playbooks/deploy-cybermem.yml +201 -24
  27. package/templates/ansible/playbooks/reset-db.yml +44 -0
  28. package/templates/auth-sidecar/Dockerfile +2 -10
  29. package/templates/auth-sidecar/package.json +1 -3
  30. package/templates/auth-sidecar/server.js +149 -110
  31. package/templates/charts/cybermem/.helmignore +13 -0
  32. package/templates/charts/cybermem/templates/dashboard-deployment.yaml +31 -7
  33. package/templates/charts/cybermem/templates/dashboard-service.yaml +4 -4
  34. package/templates/charts/cybermem/templates/openmemory-deployment.yaml +14 -8
  35. package/templates/charts/cybermem/templates/openmemory-service.yaml +3 -3
  36. package/templates/charts/cybermem/templates/secret.yaml +9 -0
  37. package/templates/charts/cybermem/templates/traefik-config.yaml +67 -0
  38. package/templates/charts/cybermem/templates/traefik-deployment.yaml +53 -0
  39. package/templates/charts/cybermem/templates/traefik-service.yaml +17 -0
  40. package/templates/charts/cybermem/values-vps.yaml +8 -4
  41. package/templates/charts/cybermem/values.yaml +17 -9
  42. package/templates/docker-compose.yml +103 -78
  43. package/templates/monitoring/log_exporter/exporter.py +22 -29
  44. package/templates/monitoring/traefik/traefik.yml +1 -4
  45. package/dist/commands/__tests__/backup.test.js +0 -75
  46. package/dist/commands/__tests__/restore.test.js +0 -70
  47. package/dist/commands/deploy.js +0 -239
  48. package/dist/commands/init.js +0 -362
  49. package/dist/commands/login.js +0 -165
  50. package/dist/templates/envs/local.example +0 -27
  51. package/dist/templates/envs/rpi.example +0 -27
  52. package/dist/templates/envs/vps.example +0 -25
  53. package/dist/templates/monitoring/instructions_injector/Dockerfile +0 -15
  54. package/dist/templates/monitoring/instructions_injector/injector.py +0 -137
  55. package/dist/templates/monitoring/instructions_injector/requirements.txt +0 -3
  56. package/dist/templates/openmemory/Dockerfile +0 -19
@@ -1,15 +1,21 @@
1
1
  ---
2
2
  - name: Deploy CyberMem to Raspberry Pi
3
- hosts: rpi
4
- become: yes
3
+ hosts: all
4
+ become: true
5
5
  vars:
6
6
  project_dir: /home/{{ ansible_user }}/cybermem
7
7
  env_file: .env.rpi
8
+ # Default ports based on environment
9
+ TRAEFIK_PORT_STAGING: "8625"
10
+ TRAEFIK_PORT_PROD: "8626"
11
+ TRAEFIK_PORT_DEFAULT: "{{ TRAEFIK_PORT_STAGING if cybermem_env | default('prod') == 'staging' else TRAEFIK_PORT_PROD }}"
12
+ # Port to use consistently across all tasks
13
+ EFFECTIVE_TRAEFIK_PORT: "{{ TRAEFIK_PORT | default(TRAEFIK_PORT_DEFAULT) }}"
8
14
 
9
15
  tasks:
10
16
  - name: Update apt cache
11
17
  apt:
12
- update_cache: yes
18
+ update_cache: true
13
19
  cache_valid_time: 3600
14
20
 
15
21
  - name: Install dependencies
@@ -23,13 +29,13 @@
23
29
  service:
24
30
  name: docker
25
31
  state: started
26
- enabled: yes
32
+ enabled: true
27
33
 
28
34
  - name: Add user to docker group
29
35
  user:
30
36
  name: "{{ ansible_user }}"
31
37
  groups: docker
32
- append: yes
38
+ append: true
33
39
 
34
40
  - name: Create project directory
35
41
  file:
@@ -39,33 +45,204 @@
39
45
  group: "{{ ansible_user }}"
40
46
  mode: "0755"
41
47
 
42
- - name: Synchronize project files
48
+ - name: Ensure backup directory exists
49
+ file:
50
+ path: "{{ project_dir }}/backups"
51
+ state: directory
52
+ owner: "{{ ansible_user }}"
53
+ group: "{{ ansible_user }}"
54
+ mode: "0755"
55
+
56
+ - name: Ensure secrets directory exists
57
+ file:
58
+ path: "{{ project_dir }}/secrets"
59
+ state: directory
60
+ owner: "{{ ansible_user }}"
61
+ group: "{{ ansible_user }}"
62
+ mode: "0755"
63
+
64
+ - name: Write API Key Secret File
65
+ copy:
66
+ content: "{{ auth_token_value }}"
67
+ dest: "{{ project_dir }}/secrets/om_api_key"
68
+ owner: "{{ ansible_user }}"
69
+ group: "{{ ansible_user }}"
70
+ mode: "0600"
71
+ when: auth_token_value is defined
72
+ ignore_errors: true
73
+
74
+ - name: Ensure data directory exists
75
+ file:
76
+ path: "{{ project_dir }}/data"
77
+ state: directory
78
+ owner: "{{ ansible_user }}"
79
+ group: "{{ ansible_user }}"
80
+ mode: "0755"
81
+
82
+ - name: Check if database exists
83
+ stat:
84
+ path: "{{ project_dir }}/data/openmemory.sqlite"
85
+ register: db_file
86
+
87
+ - name: Create pre-deploy database backup
88
+ shell: "cp {{ project_dir }}/data/openmemory.sqlite {{ project_dir }}/backups/openmemory.sqlite.{{ ansible_facts['date_time']['iso8601_basic_short'] }}.bak"
89
+ when: db_file.stat.exists
90
+ ignore_errors: true
91
+
92
+ - name: Copy Docker Compose template
93
+ copy:
94
+ src: "../../docker-compose.yml"
95
+ dest: "{{ project_dir }}/docker-compose.yml"
96
+ owner: "{{ ansible_user }}"
97
+ group: "{{ ansible_user }}"
98
+ mode: "0644"
99
+
100
+ - name: Replace GHCR image tag (staging uses :staging, prod uses :latest)
101
+ replace:
102
+ path: "{{ project_dir }}/docker-compose.yml"
103
+ regexp: "(ghcr.io/[^/]+/cybermem-[^:]+):latest"
104
+ replace: '\1:{{ ghcr_tag | default("latest") }}'
105
+ when: ghcr_tag is defined and ghcr_tag != 'latest'
106
+
107
+ - name: Sync monitoring configuration
43
108
  synchronize:
44
- src: "{{ playbook_dir }}/../../"
45
- dest: "{{ project_dir }}"
46
- rsync_opts:
47
- - "--exclude=.git"
48
- - "--exclude=node_modules"
49
- - "--exclude=.next"
50
- - "--exclude=dashboard/.next"
51
- - "--exclude=*.log"
52
- - "--exclude=tmp"
53
- - "--exclude=__pycache__"
54
-
55
- - name: Copy environment file
109
+ src: "../../monitoring"
110
+ dest: "{{ project_dir }}/"
111
+ recursive: true
112
+ delete: true
113
+
114
+ # NOTE: build_from_source removed - all builds happen on GH Actions now
115
+
116
+ - name: Patch Traefik configuration
117
+ replace:
118
+ path: "{{ project_dir }}/monitoring/traefik/traefik.yml"
119
+ regexp: 'address: ":8626"'
120
+ replace: 'address: ":{{ EFFECTIVE_TRAEFIK_PORT }}"'
121
+
122
+ - name: Ensure .env is present (template if missing)
56
123
  copy:
57
- src: "../../{{ env_file }}"
124
+ content: |
125
+ CYBERMEM_INSTANCE=rpi
126
+ CYBERMEM_ENV={{ cybermem_env | default('prod') }}
127
+ PROJECT_NAME={{ PROJECT_NAME | default('cybermem') }}
128
+ TRAEFIK_PORT={{ EFFECTIVE_TRAEFIK_PORT }}
129
+ CYBERMEM_TAILSCALE={{ CYBERMEM_TAILSCALE | default('false') }}
130
+ SECRETS_DIR={{ project_dir }}/secrets
131
+ DATA_DIR={{ project_dir }}/data
132
+ CYBERMEM_ENV_PATH={{ project_dir }}/.env
133
+ CYBERMEM_HOME={{ project_dir }}
134
+ # Add other envs as needed
58
135
  dest: "{{ project_dir }}/.env"
136
+ force: true
59
137
  owner: "{{ ansible_user }}"
60
138
  group: "{{ ansible_user }}"
61
139
  mode: "0600"
62
140
 
63
- - name: Pull and start services
64
- command: docker-compose -f docker-compose.prod.yml --profile ollama up -d --pull always
141
+ - name: Load global version from package.json
142
+ set_fact:
143
+ global_version: "{{ (lookup('file', playbook_dir + '/../../../../../package.json') | from_json).version }}"
144
+
145
+ - name: Pull images from GHCR
146
+ command: docker-compose -p {{ PROJECT_NAME | default('cybermem') }} pull
147
+ args:
148
+ chdir: "{{ project_dir }}"
149
+ register: pull_out
150
+ changed_when: "'Downloaded newer image' in pull_out.stdout or 'Downloaded newer image' in pull_out.stderr"
151
+ when: not (skip_pull | default(false) | bool)
152
+
153
+ - name: Stop staging containers (prevent port conflicts, NEVER touch prod)
154
+ shell: |
155
+ docker-compose -p cybermem-staging down --remove-orphans 2>/dev/null || true
156
+ args:
157
+ chdir: "{{ project_dir }}"
158
+ changed_when: false
159
+ ignore_errors: true
160
+ when: cybermem_env | default('prod') == 'staging'
161
+
162
+ - name: Start services
163
+ command: docker-compose -p {{ PROJECT_NAME | default('cybermem') }} up -d --remove-orphans
65
164
  args:
66
165
  chdir: "{{ project_dir }}"
67
- register: compose_output
166
+ register: up_out
167
+ changed_when: "'Creating' in up_out.stdout or 'Recreating' in up_out.stdout or 'Starting' in up_out.stdout or 'Creating' in up_out.stderr or 'Recreating' in up_out.stderr or 'Starting' in up_out.stderr"
168
+
169
+ - name: Install sqlite3 for token injection
170
+ apt:
171
+ name: sqlite3
172
+ state: present
173
+
174
+ - name: Inject Authentication Token
175
+ shell: |
176
+ sqlite3 {{ project_dir }}/data/openmemory.sqlite "CREATE TABLE IF NOT EXISTS access_keys (
177
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
178
+ key_hash TEXT NOT NULL,
179
+ name TEXT DEFAULT 'default',
180
+ user_id TEXT DEFAULT 'default',
181
+ created_at TEXT DEFAULT (datetime('now')),
182
+ last_used_at TEXT,
183
+ is_active INTEGER DEFAULT 1
184
+ );"
185
+ sqlite3 {{ project_dir }}/data/openmemory.sqlite "DELETE FROM access_keys WHERE name='{{ auth_token_name | default('ansible-generated') }}';"
186
+ sqlite3 {{ project_dir }}/data/openmemory.sqlite "INSERT INTO access_keys (id, key_hash, name, user_id) VALUES ('{{ auth_token_id }}', '{{ auth_token_hash }}', '{{ auth_token_name | default('ansible-generated') }}', 'admin');"
187
+ when: auth_token_hash is defined
188
+ ignore_errors: true
189
+
190
+ - name: Wait for Traefik to be ready
191
+ uri:
192
+ url: "http://localhost:{{ EFFECTIVE_TRAEFIK_PORT }}/health"
193
+ status_code: 200
194
+ register: traefik_ping
195
+ until: traefik_ping.status == 200
196
+ retries: 10
197
+ delay: 5
198
+
199
+ - name: Verify Overall System Health
200
+ uri:
201
+ url: "http://localhost:{{ EFFECTIVE_TRAEFIK_PORT }}/api/health"
202
+ return_content: true
203
+ register: health_status
204
+ until: "health_status.content is defined and (health_status.content | from_json).overall == 'ok'"
205
+ retries: 5
206
+ delay: 10
207
+ ignore_errors: true
208
+
209
+ - name: Configure Tailscale Funnel (after health check)
210
+ shell: |
211
+ # Reset any existing configuration for clean state
212
+ tailscale serve reset 2>/dev/null || true
213
+ tailscale funnel reset 2>/dev/null || true
214
+
215
+ # Configure port based on current environment only
216
+ if [ "{{ cybermem_env | default('prod') }}" = "staging" ]; then
217
+ tailscale funnel --bg --https=10000 http://127.0.0.1:{{ EFFECTIVE_TRAEFIK_PORT }}
218
+ else
219
+ tailscale funnel --bg --https=443 http://127.0.0.1:{{ EFFECTIVE_TRAEFIK_PORT }}
220
+ fi
221
+
222
+ tailscale funnel status
223
+ become: true
224
+ when: (CYBERMEM_TAILSCALE | default('false') | bool) and (health_status is defined and not health_status.failed and (health_status.content | from_json).overall == 'ok')
225
+ register: funnel_status
226
+ ignore_errors: true
227
+
228
+ - name: Display Tailscale Funnel Status
229
+ debug:
230
+ var: funnel_status.stdout_lines
231
+ when: funnel_status is defined and funnel_status.stdout_lines is defined
232
+
233
+ - name: Automated Rollback on Failure
234
+ block:
235
+ - name: Rollback message
236
+ debug:
237
+ msg: "Rollback logic triggered due to unhealthy state."
238
+
239
+ - name: Restart previous state containers
240
+ command: docker-compose -p {{ PROJECT_NAME | default('cybermem') }} up -d --remove-orphans
241
+ args:
242
+ chdir: "{{ project_dir }}"
243
+ ignore_errors: true
244
+ when: health_status.failed or (health_status.content | from_json).overall != 'ok'
68
245
 
69
- - name: Show deployment status
246
+ - name: Deployment Summary
70
247
  debug:
71
- var: compose_output.stdout_lines
248
+ msg: "CyberMem v{{ global_version }} successfully deployed. Health: {{ (health_status.content | from_json).overall | default('UNKNOWN') }}"
@@ -0,0 +1,44 @@
1
+ ---
2
+ - name: Reset CyberMem Database on Raspberry Pi
3
+ hosts: rpi
4
+ become: yes
5
+ vars:
6
+ project_dir: /home/{{ ansible_user }}/cybermem
7
+ db_path: "{{ project_dir }}/data/openmemory.sqlite"
8
+
9
+ tasks:
10
+ - name: Stop CyberMem services
11
+ command: docker-compose down
12
+ args:
13
+ chdir: "{{ project_dir }}"
14
+
15
+ - name: Wipe database file
16
+ file:
17
+ path: "{{ db_path }}"
18
+ state: absent
19
+
20
+ - name: Ensure data directory exists
21
+ file:
22
+ path: "{{ project_dir }}/data"
23
+ state: directory
24
+ owner: "{{ ansible_user }}"
25
+ group: "{{ ansible_user }}"
26
+ mode: "0755"
27
+
28
+ - name: Start CyberMem services
29
+ command: docker-compose up -d
30
+ args:
31
+ chdir: "{{ project_dir }}"
32
+
33
+ - name: Wait for Traefik to be ready
34
+ uri:
35
+ url: "http://localhost:8626/health"
36
+ status_code: 200
37
+ register: traefik_ping
38
+ until: traefik_ping.status == 200
39
+ retries: 10
40
+ delay: 5
41
+
42
+ - name: Database Reset Summary
43
+ debug:
44
+ msg: "CyberMem database at {{ db_path }} has been wiped and services are back online."
@@ -1,17 +1,9 @@
1
- # Builder stage for native modules
2
- FROM node:20-alpine AS builder
1
+ FROM node:20-alpine
3
2
  WORKDIR /app
4
- RUN apk add --no-cache python3 make g++
3
+ RUN apk add --no-cache libc6-compat
5
4
  COPY package.json ./
6
5
  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
13
6
  COPY server.js .
14
- COPY package.json .
15
7
 
16
8
  EXPOSE 3001
17
9
  CMD ["node", "server.js"]
@@ -3,7 +3,5 @@
3
3
  "version": "0.1.0",
4
4
  "description": "Auth sidecar for CyberMem",
5
5
  "main": "server.js",
6
- "dependencies": {
7
- "sqlite3": "5.1.7"
8
- }
6
+ "dependencies": {}
9
7
  }
@@ -2,98 +2,71 @@
2
2
  * CyberMem Auth Sidecar
3
3
  *
4
4
  * ForwardAuth service for Traefik that validates:
5
- * 1. Bearer tokens (sk-xxx) against SQLite access_keys table
5
+ * 1. Bearer tokens (sk-xxx) against Docker Secret file (SSoT)
6
6
  * 2. Local requests bypass (localhost, *.local domains)
7
7
  *
8
- * NO EXTERNAL DEPENDENCIES - uses built-in crypto and sqlite3.
8
+ * NO EXTERNAL DEPENDENCIES - uses built-in crypto and fs.
9
9
  */
10
10
 
11
11
  const http = require("http");
12
12
  const crypto = require("crypto");
13
13
  const path = require("path");
14
- const sqlite3 = require("sqlite3").verbose();
14
+ const fs = require("fs");
15
+ const SECRET_PATH = process.env.API_KEY_FILE || "/run/secrets/om_api_key";
16
+ let cachedToken = null;
15
17
 
16
18
  const PORT = process.env.PORT || 3001;
17
- const DB_PATH = process.env.OM_DB_PATH || "/data/openmemory.sqlite";
18
-
19
- // Ensure schema exists
20
- function initSchema() {
21
- const db = new sqlite3.Database(DB_PATH);
22
- db.serialize(() => {
23
- db.run(
24
- `
25
- CREATE TABLE IF NOT EXISTS access_keys (
26
- key_id TEXT PRIMARY KEY,
27
- key_hash TEXT UNIQUE NOT NULL,
28
- user_id TEXT NOT NULL,
29
- name TEXT,
30
- is_active INTEGER DEFAULT 1,
31
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
32
- )
33
- `,
34
- (err) => {
35
- if (err) console.error("SCHEMA ERROR:", err.message);
36
- else console.log("Schema verified for access_keys");
37
- db.close();
38
- },
39
- );
40
- });
41
- }
42
-
43
- initSchema();
44
-
45
- // Hash token using same PBKDF2 as CLI (for verification)
46
- function hashToken(token) {
47
- return new Promise((resolve, reject) => {
48
- const salt = crypto
49
- .createHash("sha256")
50
- .update("cybermem-salt-v1")
51
- .digest("hex")
52
- .slice(0, 16);
53
- crypto.pbkdf2(token, salt, 100000, 64, "sha512", (err, key) => {
54
- if (err) reject(err);
55
- else resolve(key.toString("hex"));
56
- });
57
- });
58
- }
59
19
 
60
- // Verify token against SQLite access_keys table
61
- async function verifyToken(token) {
20
+ // Load token from secret file once at startup (SSoT)
21
+ function loadSecret() {
62
22
  try {
63
- const db = new sqlite3.Database(DB_PATH);
64
-
65
- const tokenHash = await hashToken(token);
66
-
67
- return new Promise((resolve, reject) => {
68
- db.get(
69
- "SELECT user_id, name FROM access_keys WHERE key_hash = ? AND is_active = 1",
70
- [tokenHash],
71
- (err, row) => {
72
- db.close();
73
- if (err) {
74
- console.log("DB error:", err.message);
75
- resolve(null);
76
- } else if (row) {
77
- // Update last_used_at
78
- const updateDb = new sqlite3.Database(DB_PATH);
79
- updateDb.run(
80
- "UPDATE access_keys SET last_used_at = datetime('now') WHERE key_hash = ?",
81
- [tokenHash],
82
- );
83
- updateDb.close();
84
- resolve({ userId: row.user_id, name: row.name });
85
- } else {
86
- resolve(null);
87
- }
88
- },
23
+ // 1. Check environment variable first (Direct)
24
+ if (process.env.OM_API_KEY) {
25
+ cachedToken = process.env.OM_API_KEY.trim();
26
+ console.log("SSoT Token loaded from OM_API_KEY env");
27
+ return;
28
+ }
29
+
30
+ // 2. Check secret/env file (Mapped)
31
+ if (fs.existsSync(SECRET_PATH)) {
32
+ const content = fs.readFileSync(SECRET_PATH, "utf8").trim();
33
+
34
+ // Check if it's a shell-formatted env file (OM_API_KEY=sk-...)
35
+ const envMatch = content.match(/OM_API_KEY=["']?(sk-[a-zA-Z0-9]+)["']?/);
36
+ if (envMatch) {
37
+ cachedToken = envMatch[1];
38
+ console.log(`SSoT Token extracted from env file: ${SECRET_PATH}`);
39
+ } else {
40
+ // Assume raw token file
41
+ cachedToken = content;
42
+ console.log(`SSoT Token loaded from raw file: ${SECRET_PATH}`);
43
+ }
44
+ } else {
45
+ console.warn(
46
+ `SECRET WARNING: ${SECRET_PATH} not found. Remote auth will fail.`,
89
47
  );
90
- });
48
+ }
91
49
  } catch (err) {
92
- console.log("Token verification error:", err.message);
93
- return null;
50
+ console.error("SECRET LOAD ERROR:", err.message);
94
51
  }
95
52
  }
96
53
 
54
+ loadSecret();
55
+
56
+ // Verify token against SSoT (file-based)
57
+ async function verifyToken(token) {
58
+ if (!cachedToken) {
59
+ // Try one more time if not loaded (e.g. race condition/debug)
60
+ loadSecret();
61
+ }
62
+
63
+ if (cachedToken && token === cachedToken) {
64
+ return { userId: "admin", name: "SSoT-Admin" };
65
+ }
66
+
67
+ return null;
68
+ }
69
+
97
70
  // Check if request is from localhost or local network
98
71
  function isLocalRequest(req) {
99
72
  const forwarded = req.headers["x-forwarded-for"];
@@ -102,21 +75,41 @@ function isLocalRequest(req) {
102
75
  const ip =
103
76
  forwarded?.split(",")[0]?.trim() || realIp || req.socket.remoteAddress;
104
77
 
105
- // IP-based local check
78
+ // 1. First reject Tailscale/Remote requests (.ts.net, 100.x.x.x)
79
+ // CRITICAL: Tailscale requests (via Funnel) must NEVER be treated as local.
80
+ // If host contains .ts.net, it's external.
81
+ // NOTE: Legacy CYBERMEM_TAILSCALE env-flag logic was removed on purpose.
82
+ // We now auto-detect Tailscale by host/IP (".ts.net" / "100.x.x.x"),
83
+ // so .local bypass works regardless of CYBERMEM_TAILSCALE being set.
84
+ if (
85
+ host.includes(".ts.net") ||
86
+ (typeof ip === "string" && ip.startsWith("100."))
87
+ ) {
88
+ return false;
89
+ }
90
+
91
+ // 2. Allow .local (mDNS) bypass for RPi LAN access
92
+ const hostname = host.split(":")[0];
93
+ if (hostname.endsWith(".local")) {
94
+ return true;
95
+ }
96
+
97
+ // Host-based check REMOVED for security (CVE-2026-001)
98
+ // We only trust loopback IP if not on Tailscale.
99
+
100
+ // Allow localhost bypass ONLY for local Dev environment (Docker Desktop)
101
+ const isDev = process.env.CYBERMEM_INSTANCE === "local";
102
+ if (isDev && (host.startsWith("localhost") || host.startsWith("127.0.0.1"))) {
103
+ return true;
104
+ }
105
+
106
106
  const isLocalIp =
107
107
  ip === "127.0.0.1" ||
108
108
  ip === "::1" ||
109
109
  ip === "::ffff:127.0.0.1" ||
110
110
  ip === "localhost";
111
111
 
112
- // Host-based local check (raspberrypi.local, localhost, *.local)
113
- const isLocalHost =
114
- host.includes("localhost") ||
115
- host.includes("127.0.0.1") ||
116
- host.includes("raspberrypi.local") ||
117
- host.match(/\.local(:\d+)?$/);
118
-
119
- return isLocalIp || isLocalHost;
112
+ return isLocalIp;
120
113
  }
121
114
 
122
115
  // ForwardAuth handler
@@ -131,9 +124,47 @@ const server = http.createServer(async (req, res) => {
131
124
  const authHeader = req.headers["authorization"];
132
125
  const apiKeyHeader = req.headers["x-api-key"];
133
126
 
134
- // 1. Local bypass - no auth required for localhost
135
- if (isLocalRequest(req)) {
136
- console.log("Auth OK: Local bypass");
127
+ // 1. Resolve URI
128
+ const requestUri = req.headers["x-forwarded-uri"] || req.url || "/";
129
+ console.log(`[Auth-Sidecar] Processing ${req.method} ${requestUri}`);
130
+
131
+ // 2. Public Paths Bypass
132
+ // We allow Dashboard roots, login paths and essential assets to bypass SSoT token check
133
+ // The Dashboard itself handles its own session auth.
134
+ const prefixPublicPaths = [
135
+ "/auth",
136
+ "/api/auth",
137
+ "/api/health",
138
+ // "/api/metrics", // LEAK FIXED
139
+ // "/api/stats", // LEAK FIXED
140
+ "/api/settings",
141
+ "/_next",
142
+ "/favicon",
143
+ "/static",
144
+ "/public",
145
+ "/health",
146
+ "/login",
147
+ // "/metrics", // LEAK FIXED
148
+ "/clients.json",
149
+ ];
150
+
151
+ const exactPublicPaths = ["/"];
152
+
153
+ const isPublic =
154
+ exactPublicPaths.includes(requestUri) ||
155
+ prefixPublicPaths.some((p) => requestUri.startsWith(p));
156
+
157
+ if (isPublic) {
158
+ console.log(`[Auth-Sidecar] ✅ Public bypass: ${requestUri}`);
159
+ res.writeHead(200, { "X-Auth-Method": "public" });
160
+ res.end();
161
+ return;
162
+ }
163
+
164
+ // 3. Local bypass
165
+ const isK3d = process.env.CYBERMEM_INSTANCE === "k3d";
166
+ if (!isK3d && isLocalRequest(req)) {
167
+ console.log(`[Auth-Sidecar] ✅ Local bypass: ${requestUri}`);
137
168
  res.writeHead(200, {
138
169
  "X-Auth-Method": "local",
139
170
  "X-User-Id": "local",
@@ -142,32 +173,41 @@ const server = http.createServer(async (req, res) => {
142
173
  return;
143
174
  }
144
175
 
145
- // 2. Check Bearer token (sk-xxx format)
176
+ // 4. Token Auth Verification Helper
177
+ const verifyRequestToken = async (received) => {
178
+ if (!received) return null;
179
+ const result = await verifyToken(received);
180
+ if (!result) {
181
+ console.log(
182
+ `[Auth-Sidecar] DEBUG Mismatch: received [${received.substring(0, 5)}...] (len:${received.length}) vs cached [${cachedToken?.substring(0, 5)}...] (len:${cachedToken?.length})`,
183
+ );
184
+ }
185
+ return result;
186
+ };
187
+
146
188
  if (authHeader?.startsWith("Bearer ")) {
147
189
  const token = authHeader.substring(7);
148
-
149
- if (token.startsWith("sk-")) {
150
- const result = await verifyToken(token);
151
-
152
- if (result) {
153
- console.log(`Auth OK: Token (${result.name || result.userId})`);
154
- res.writeHead(200, {
155
- "X-User-Id": result.userId,
156
- "X-Auth-Method": "token",
157
- "X-Token-Name": result.name || "",
158
- });
159
- res.end();
160
- return;
161
- }
190
+ const result = await verifyRequestToken(token);
191
+ if (result) {
192
+ console.log(
193
+ `[Auth-Sidecar] ✅ Auth OK (Bearer): ${result.name || result.userId}`,
194
+ );
195
+ res.writeHead(200, {
196
+ "X-User-Id": result.userId,
197
+ "X-Auth-Method": "token",
198
+ "X-Token-Name": result.name || "",
199
+ });
200
+ res.end();
201
+ return;
162
202
  }
163
203
  }
164
204
 
165
- // 3. Check X-API-Key header (sk-xxx format)
166
205
  if (apiKeyHeader?.startsWith("sk-")) {
167
- const result = await verifyToken(apiKeyHeader);
168
-
206
+ const result = await verifyRequestToken(apiKeyHeader);
169
207
  if (result) {
170
- console.log(`Auth OK: API-Key Header (${result.name || result.userId})`);
208
+ console.log(
209
+ `[Auth-Sidecar] ✅ Auth OK (X-API-Key): ${result.name || result.userId}`,
210
+ );
171
211
  res.writeHead(200, {
172
212
  "X-User-Id": result.userId,
173
213
  "X-Auth-Method": "api-key",
@@ -178,8 +218,8 @@ const server = http.createServer(async (req, res) => {
178
218
  }
179
219
  }
180
220
 
181
- // 4. Unauthorized
182
- console.log("Auth FAILED: No valid token");
221
+ // 5. Unauthorized
222
+ console.log(`[Auth-Sidecar] ❌ Auth FAILED: ${requestUri}`);
183
223
  res.writeHead(401, { "Content-Type": "application/json" });
184
224
  res.end(
185
225
  JSON.stringify({
@@ -192,5 +232,4 @@ const server = http.createServer(async (req, res) => {
192
232
 
193
233
  server.listen(PORT, () => {
194
234
  console.log(`Auth sidecar (token-auth) listening on port ${PORT}`);
195
- console.log(`DB path: ${DB_PATH}`);
196
235
  });
@@ -0,0 +1,13 @@
1
+ # Patterns to ignore when building packages.
2
+ # This supports shell glob matching, same as .gitignore.
3
+ # ...
4
+ .DS_Store
5
+ *.swp
6
+ *.bak
7
+ *.tmp
8
+ *.orig
9
+ *~
10
+ .git
11
+ .gitignore
12
+ .idea
13
+ .vscode