fix: improve namespace-based multi-user

This commit is contained in:
weishu
2025-12-31 23:28:29 +08:00
parent e821458af8
commit 7ae7158127
5 changed files with 90 additions and 7 deletions
+9 -3
View File
@@ -211,6 +211,7 @@ export class Store {
}
private initSchema(): void {
// Step 1: Create tables and indexes that don't depend on new columns
this.db.exec(`
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
@@ -230,7 +231,6 @@ export class Store {
seq INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_sessions_tag ON sessions(tag);
CREATE INDEX IF NOT EXISTS idx_sessions_tag_namespace ON sessions(tag, namespace);
CREATE TABLE IF NOT EXISTS machines (
id TEXT PRIMARY KEY,
@@ -245,7 +245,6 @@ export class Store {
active_at INTEGER,
seq INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_machines_namespace ON machines(namespace);
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
@@ -268,9 +267,9 @@ export class Store {
UNIQUE(platform, platform_user_id)
);
CREATE INDEX IF NOT EXISTS idx_users_platform ON users(platform);
CREATE INDEX IF NOT EXISTS idx_users_platform_namespace ON users(platform, namespace);
`)
// Step 2: Migrate existing tables (add missing columns)
const sessionColumns = this.db.prepare('PRAGMA table_info(sessions)').all() as Array<{ name: string }>
const sessionColumnNames = new Set(sessionColumns.map((c) => c.name))
@@ -295,6 +294,13 @@ export class Store {
if (!userColumnNames.has('namespace')) {
this.db.exec("ALTER TABLE users ADD COLUMN namespace TEXT NOT NULL DEFAULT 'default'")
}
// Step 3: Create indexes that depend on namespace column (after migration)
this.db.exec(`
CREATE INDEX IF NOT EXISTS idx_sessions_tag_namespace ON sessions(tag, namespace);
CREATE INDEX IF NOT EXISTS idx_machines_namespace ON machines(namespace);
CREATE INDEX IF NOT EXISTS idx_users_platform_namespace ON users(platform, namespace);
`)
}
getOrCreateSession(tag: string, metadata: unknown, agentState: unknown, namespace: string): StoredSession {
+33 -4
View File
@@ -9,6 +9,7 @@ import { existsSync } from 'node:fs'
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
import { randomBytes } from 'node:crypto'
import { dirname, join } from 'node:path'
import { parseAccessToken } from '../utils/accessToken'
export interface Settings {
machineId?: string
@@ -53,6 +54,28 @@ function isWeakToken(token: string): boolean {
return weakPatterns.some(p => p.test(token))
}
type CliApiTokenSource = 'env' | 'file'
function normalizeCliApiToken(rawToken: string, source: CliApiTokenSource): { token: string; didStrip: boolean } {
const parsed = parseAccessToken(rawToken)
if (!parsed) {
if (rawToken.includes(':')) {
console.warn(`[WARN] CLI_API_TOKEN from ${source} contains ":" but is not a valid token. Server expects a base token without namespace.`)
}
return { token: rawToken, didStrip: false }
}
if (!rawToken.includes(':')) {
return { token: rawToken, didStrip: false }
}
console.warn(
`[WARN] CLI_API_TOKEN from ${source} includes namespace suffix "${parsed.namespace}". ` +
'Server expects the base token only; stripping the suffix.'
)
return { token: parsed.baseToken, didStrip: true }
}
/**
* Read settings from file, preserving all existing fields.
* Returns null if file exists but cannot be parsed (to avoid data loss).
@@ -99,18 +122,19 @@ export async function getOrCreateCliApiToken(dataDir: string): Promise<CliApiTok
// 1. Environment variable has highest priority (backward compatible)
const envToken = process.env.CLI_API_TOKEN
if (envToken) {
if (isWeakToken(envToken)) {
const normalized = normalizeCliApiToken(envToken, 'env')
if (isWeakToken(normalized.token)) {
console.warn('[WARN] CLI_API_TOKEN appears to be weak. Consider using a stronger secret.')
}
// Persist env token to file if not already saved (prevents token loss on env var issues)
const settings = await readSettings(settingsFile)
if (settings !== null && !settings.cliApiToken) {
settings.cliApiToken = envToken
settings.cliApiToken = normalized.token
await writeSettings(settingsFile, settings)
}
return { token: envToken, source: 'env', isNew: false, filePath: settingsFile }
return { token: normalized.token, source: 'env', isNew: false, filePath: settingsFile }
}
// 2. Read from settings file
@@ -124,7 +148,12 @@ export async function getOrCreateCliApiToken(dataDir: string): Promise<CliApiTok
}
if (settings.cliApiToken) {
return { token: settings.cliApiToken, source: 'file', isNew: false, filePath: settingsFile }
const normalized = normalizeCliApiToken(settings.cliApiToken, 'file')
if (normalized.didStrip) {
settings.cliApiToken = normalized.token
await writeSettings(settingsFile, settings)
}
return { token: normalized.token, source: 'file', isNew: false, filePath: settingsFile }
}
// 3. Generate new token and save