@yeaft/webchat-agent 1.0.188 → 1.0.190

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 (121) hide show
  1. package/cli.js +12 -0
  2. package/index.js +10 -2
  3. package/local-run.js +218 -0
  4. package/local-runtime/server/.env.example +54 -0
  5. package/local-runtime/server/api.js +111 -0
  6. package/local-runtime/server/auth/aad.js +235 -0
  7. package/local-runtime/server/auth/login.js +156 -0
  8. package/local-runtime/server/auth/oauth-flow.js +277 -0
  9. package/local-runtime/server/auth/password-reset.js +134 -0
  10. package/local-runtime/server/auth/providers/alipay.js +125 -0
  11. package/local-runtime/server/auth/providers/github.js +82 -0
  12. package/local-runtime/server/auth/providers/google.js +60 -0
  13. package/local-runtime/server/auth/providers/microsoft.js +71 -0
  14. package/local-runtime/server/auth/providers/types.js +57 -0
  15. package/local-runtime/server/auth/providers/wechat.js +68 -0
  16. package/local-runtime/server/auth/register.js +91 -0
  17. package/local-runtime/server/auth/session-store.js +57 -0
  18. package/local-runtime/server/auth/token.js +85 -0
  19. package/local-runtime/server/auth/totp-auth.js +133 -0
  20. package/local-runtime/server/auth/utils.js +42 -0
  21. package/local-runtime/server/auth.js +8 -0
  22. package/local-runtime/server/check-node-version.js +74 -0
  23. package/local-runtime/server/config.js +298 -0
  24. package/local-runtime/server/context.js +140 -0
  25. package/local-runtime/server/create-user.js +59 -0
  26. package/local-runtime/server/database.js +12 -0
  27. package/local-runtime/server/db/connection.js +963 -0
  28. package/local-runtime/server/db/expert-db.js +171 -0
  29. package/local-runtime/server/db/identity-db.js +92 -0
  30. package/local-runtime/server/db/invitation-db.js +38 -0
  31. package/local-runtime/server/db/message-db.js +257 -0
  32. package/local-runtime/server/db/session-db.js +118 -0
  33. package/local-runtime/server/db/user-db.js +165 -0
  34. package/local-runtime/server/db/user-stats-db.js +185 -0
  35. package/local-runtime/server/db/yeaft-session-db.js +258 -0
  36. package/local-runtime/server/email.js +96 -0
  37. package/local-runtime/server/encryption.js +105 -0
  38. package/local-runtime/server/handlers/agent-conversation.js +347 -0
  39. package/local-runtime/server/handlers/agent-file-terminal.js +99 -0
  40. package/local-runtime/server/handlers/agent-output.js +854 -0
  41. package/local-runtime/server/handlers/agent-sync.js +399 -0
  42. package/local-runtime/server/handlers/agent-work-center.js +27 -0
  43. package/local-runtime/server/handlers/client-conversation.js +1182 -0
  44. package/local-runtime/server/handlers/client-misc.js +254 -0
  45. package/local-runtime/server/handlers/client-work-center.js +269 -0
  46. package/local-runtime/server/handlers/client-workbench.js +146 -0
  47. package/local-runtime/server/handlers/session-pin-router.js +61 -0
  48. package/local-runtime/server/heartbeat-policy.js +46 -0
  49. package/local-runtime/server/index.js +275 -0
  50. package/local-runtime/server/package.json +55 -0
  51. package/local-runtime/server/perf-trace.js +154 -0
  52. package/local-runtime/server/proxy.js +273 -0
  53. package/local-runtime/server/routes/admin-routes.js +207 -0
  54. package/local-runtime/server/routes/auth-routes.js +322 -0
  55. package/local-runtime/server/routes/expert-routes.js +117 -0
  56. package/local-runtime/server/routes/invitation-routes.js +60 -0
  57. package/local-runtime/server/routes/session-routes.js +112 -0
  58. package/local-runtime/server/routes/upload-routes.js +109 -0
  59. package/local-runtime/server/routes/user-routes.js +241 -0
  60. package/local-runtime/server/totp.js +74 -0
  61. package/local-runtime/server/work-item-attachment-policy.js +56 -0
  62. package/local-runtime/server/ws-agent.js +319 -0
  63. package/local-runtime/server/ws-client.js +214 -0
  64. package/local-runtime/server/ws-utils.js +394 -0
  65. package/local-runtime/server/yeaft-asset-store.js +339 -0
  66. package/local-runtime/version.json +1 -0
  67. package/local-runtime/web/app.bundle.js +7673 -0
  68. package/local-runtime/web/app.bundle.js.gz +0 -0
  69. package/local-runtime/web/assets/avatars/README.md +34 -0
  70. package/local-runtime/web/assets/avatars/ada.svg +1 -0
  71. package/local-runtime/web/assets/avatars/alan.svg +1 -0
  72. package/local-runtime/web/assets/avatars/alice.svg +1 -0
  73. package/local-runtime/web/assets/avatars/anders.svg +1 -0
  74. package/local-runtime/web/assets/avatars/bezos.svg +1 -0
  75. package/local-runtime/web/assets/avatars/borges.svg +1 -0
  76. package/local-runtime/web/assets/avatars/buffett.svg +1 -0
  77. package/local-runtime/web/assets/avatars/clausewitz.svg +1 -0
  78. package/local-runtime/web/assets/avatars/dalio.svg +1 -0
  79. package/local-runtime/web/assets/avatars/dieter.svg +1 -0
  80. package/local-runtime/web/assets/avatars/drucker.svg +1 -0
  81. package/local-runtime/web/assets/avatars/einstein.svg +1 -0
  82. package/local-runtime/web/assets/avatars/grace.svg +1 -0
  83. package/local-runtime/web/assets/avatars/harari.svg +1 -0
  84. package/local-runtime/web/assets/avatars/jung.svg +1 -0
  85. package/local-runtime/web/assets/avatars/kahneman.svg +1 -0
  86. package/local-runtime/web/assets/avatars/ken.svg +1 -0
  87. package/local-runtime/web/assets/avatars/kongzi.svg +1 -0
  88. package/local-runtime/web/assets/avatars/kubrick.svg +1 -0
  89. package/local-runtime/web/assets/avatars/linus.svg +1 -0
  90. package/local-runtime/web/assets/avatars/luxun.svg +1 -0
  91. package/local-runtime/web/assets/avatars/margaret.svg +1 -0
  92. package/local-runtime/web/assets/avatars/martin.svg +1 -0
  93. package/local-runtime/web/assets/avatars/miyazaki.svg +1 -0
  94. package/local-runtime/web/assets/avatars/munger.svg +1 -0
  95. package/local-runtime/web/assets/avatars/nietzsche.svg +1 -0
  96. package/local-runtime/web/assets/avatars/norman.svg +1 -0
  97. package/local-runtime/web/assets/avatars/shannon.svg +1 -0
  98. package/local-runtime/web/assets/avatars/simaqian.svg +1 -0
  99. package/local-runtime/web/assets/avatars/socrates.svg +1 -0
  100. package/local-runtime/web/assets/avatars/steve.svg +1 -0
  101. package/local-runtime/web/assets/avatars/sudongpo.svg +1 -0
  102. package/local-runtime/web/assets/avatars/sunzi.svg +1 -0
  103. package/local-runtime/web/docx-preview.min.js +2 -0
  104. package/local-runtime/web/docx-preview.min.js.gz +0 -0
  105. package/local-runtime/web/html-to-image.min.js +2 -0
  106. package/local-runtime/web/html-to-image.min.js.gz +0 -0
  107. package/local-runtime/web/index.html +21 -0
  108. package/local-runtime/web/jszip.min.js +13 -0
  109. package/local-runtime/web/jszip.min.js.gz +0 -0
  110. package/local-runtime/web/mermaid.min.js +2843 -0
  111. package/local-runtime/web/mermaid.min.js.gz +0 -0
  112. package/local-runtime/web/msal-browser.min.js +69 -0
  113. package/local-runtime/web/msal-browser.min.js.gz +0 -0
  114. package/local-runtime/web/style.bundle.css +10 -0
  115. package/local-runtime/web/style.bundle.css.gz +0 -0
  116. package/local-runtime/web/vendor.bundle.js +1522 -0
  117. package/local-runtime/web/vendor.bundle.js.gz +0 -0
  118. package/local-runtime/web/xlsx.min.js +24 -0
  119. package/local-runtime/web/xlsx.min.js.gz +0 -0
  120. package/package.json +13 -3
  121. package/scripts/prepare-local-runtime.js +25 -0
@@ -0,0 +1,133 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import { CONFIG, getUserByUsername, isEmailConfigured, updateUserTotp } from '../config.js';
3
+ import { sendVerificationCode } from '../email.js';
4
+ import { verifyTotpCode } from '../totp.js';
5
+ import { pendingVerifications, pendingTotpVerifications, pendingTotpSetup } from './session-store.js';
6
+ import { completeLogin } from './login.js';
7
+ import { generateVerificationCode, maskEmail } from './utils.js';
8
+
9
+ /**
10
+ * Verify TOTP code (for returning users with TOTP enabled)
11
+ */
12
+ export async function verifyTotpStep(tempToken, totpCode) {
13
+ const pending = pendingTotpVerifications.get(tempToken);
14
+
15
+ if (!pending) {
16
+ return { success: false, error: 'Invalid or expired TOTP verification token' };
17
+ }
18
+
19
+ if (Date.now() > pending.expiresAt) {
20
+ pendingTotpVerifications.delete(tempToken);
21
+ return { success: false, error: 'TOTP verification has expired' };
22
+ }
23
+
24
+ const user = getUserByUsername(pending.username);
25
+ if (!user || !user.totpSecret) {
26
+ pendingTotpVerifications.delete(tempToken);
27
+ return { success: false, error: 'User TOTP configuration not found' };
28
+ }
29
+
30
+ const isValid = verifyTotpCode(totpCode, user.totpSecret);
31
+ if (!isValid) {
32
+ return { success: false, error: 'Invalid TOTP code' };
33
+ }
34
+
35
+ pendingTotpVerifications.delete(tempToken);
36
+
37
+ // Check if email verification is needed
38
+ if (isEmailConfigured()) {
39
+ const code = generateVerificationCode();
40
+ const newTempToken = jwt.sign({ username: pending.username, type: 'temp' }, CONFIG.jwtSecret, { expiresIn: CONFIG.tempTokenExpiresIn });
41
+
42
+ pendingVerifications.set(newTempToken, {
43
+ username: pending.username,
44
+ code,
45
+ email: user.email,
46
+ sessionKey: pending.sessionKey,
47
+ role: pending.role,
48
+ expiresAt: Date.now() + CONFIG.emailCodeExpiresIn
49
+ });
50
+
51
+ try {
52
+ await sendVerificationCode(user.email, code, pending.username);
53
+ } catch (err) {
54
+ console.error('Failed to send verification email:', err.message);
55
+ pendingVerifications.delete(newTempToken);
56
+ return { success: false, error: 'Failed to send verification email' };
57
+ }
58
+
59
+ return {
60
+ success: true,
61
+ tempToken: newTempToken,
62
+ needEmailCode: true,
63
+ emailHint: maskEmail(user.email)
64
+ };
65
+ }
66
+
67
+ return completeLogin(pending.username, pending.sessionKey, pending.role);
68
+ }
69
+
70
+ /**
71
+ * Complete TOTP setup (for first-time TOTP configuration)
72
+ */
73
+ export async function completeTotpSetup(setupToken, totpCode) {
74
+ const pending = pendingTotpSetup.get(setupToken);
75
+
76
+ if (!pending) {
77
+ return { success: false, error: 'Invalid or expired TOTP setup token' };
78
+ }
79
+
80
+ if (Date.now() > pending.expiresAt) {
81
+ pendingTotpSetup.delete(setupToken);
82
+ return { success: false, error: 'TOTP setup has expired' };
83
+ }
84
+
85
+ const isValid = verifyTotpCode(totpCode, pending.secret);
86
+ if (!isValid) {
87
+ return { success: false, error: 'Invalid TOTP code. Please try again.' };
88
+ }
89
+
90
+ const saved = await updateUserTotp(pending.username, {
91
+ totpSecret: pending.secret,
92
+ totpEnabled: true
93
+ });
94
+
95
+ if (!saved) {
96
+ return { success: false, error: 'Failed to save TOTP settings' };
97
+ }
98
+
99
+ pendingTotpSetup.delete(setupToken);
100
+
101
+ const user = getUserByUsername(pending.username);
102
+
103
+ if (isEmailConfigured() && user) {
104
+ const code = generateVerificationCode();
105
+ const tempToken = jwt.sign({ username: pending.username, type: 'temp' }, CONFIG.jwtSecret, { expiresIn: CONFIG.tempTokenExpiresIn });
106
+
107
+ pendingVerifications.set(tempToken, {
108
+ username: pending.username,
109
+ code,
110
+ email: user.email,
111
+ sessionKey: pending.sessionKey,
112
+ role: pending.role,
113
+ expiresAt: Date.now() + CONFIG.emailCodeExpiresIn
114
+ });
115
+
116
+ try {
117
+ await sendVerificationCode(user.email, code, pending.username);
118
+ } catch (err) {
119
+ console.error('Failed to send verification email:', err.message);
120
+ pendingVerifications.delete(tempToken);
121
+ return { success: false, error: 'Failed to send verification email' };
122
+ }
123
+
124
+ return {
125
+ success: true,
126
+ tempToken,
127
+ needEmailCode: true,
128
+ emailHint: maskEmail(user.email)
129
+ };
130
+ }
131
+
132
+ return completeLogin(pending.username, pending.sessionKey, pending.role);
133
+ }
@@ -0,0 +1,42 @@
1
+ import { randomInt } from 'crypto';
2
+ import bcrypt from 'bcrypt';
3
+ import { CONFIG } from '../config.js';
4
+ import { generateSessionKey, encodeKey } from '../encryption.js';
5
+
6
+ /**
7
+ * Generate a random verification code
8
+ */
9
+ export function generateVerificationCode() {
10
+ const length = CONFIG.emailCodeLength;
11
+ let code = '';
12
+ for (let i = 0; i < length; i++) {
13
+ code += randomInt(10).toString();
14
+ }
15
+ return code;
16
+ }
17
+
18
+ /**
19
+ * Mask email for display (e.g., a***@example.com)
20
+ */
21
+ export function maskEmail(email) {
22
+ const [local, domain] = email.split('@');
23
+ if (local.length <= 2) {
24
+ return `${local[0]}***@${domain}`;
25
+ }
26
+ return `${local[0]}${'*'.repeat(Math.min(local.length - 1, 3))}@${domain}`;
27
+ }
28
+
29
+ /**
30
+ * Hash a password for storage
31
+ */
32
+ export async function hashPassword(password) {
33
+ return bcrypt.hash(password, 10);
34
+ }
35
+
36
+ /**
37
+ * Generate session key for skip-auth mode
38
+ */
39
+ export function generateSkipAuthSession() {
40
+ const sessionKey = generateSessionKey();
41
+ return { sessionKey, sessionKeyEncoded: encodeKey(sessionKey) };
42
+ }
@@ -0,0 +1,8 @@
1
+ // Re-export entry point — preserves all existing import paths.
2
+ // Actual implementations are in server/auth/*.js sub-modules.
3
+ export { loginStep1, loginStep2 } from './auth/login.js';
4
+ export { verifyTotpStep, completeTotpSetup } from './auth/totp-auth.js';
5
+ export { verifyToken, logout, maybeRenewToken } from './auth/token.js';
6
+ export { verifyAgent, register } from './auth/register.js';
7
+ export { hashPassword, generateSkipAuthSession } from './auth/utils.js';
8
+ export { loginWithAad } from './auth/aad.js';
@@ -0,0 +1,74 @@
1
+ /**
2
+ * check-node-version.js — Runtime guard for Node.js minimum version.
3
+ *
4
+ * The agent and server use `node:sqlite` (DatabaseSync) which is built-in
5
+ * to Node ≥ 22.5.0. On older Node the import fails with the cryptic
6
+ * `No such built-in module: node:sqlite`. We replace that with a clear
7
+ * actionable message and exit early.
8
+ *
9
+ * Why this exists even though `package.json` has `"engines": { "node": ">=22.5.0" }`:
10
+ * - npm only WARNS on engines mismatch by default (not strict).
11
+ * - `nvm use 22` selects whichever 22.x is already installed; if the
12
+ * user has 22.0–22.4, they pass the major check but fail node:sqlite.
13
+ * - Globally-installed CLIs (`@yeaft/webchat-agent`) bypass package.json
14
+ * engines entirely on the user's system.
15
+ *
16
+ * Call `assertNodeVersion()` at the very top of every entry point (before
17
+ * any import that transitively touches `node:sqlite`).
18
+ */
19
+
20
+ const MIN_MAJOR = 22;
21
+ const MIN_MINOR = 5;
22
+
23
+ /**
24
+ * Parse `process.version` ("v22.5.1") into [major, minor, patch].
25
+ * Returns null on unrecognized input rather than throwing — we never
26
+ * want this guard to break startup itself.
27
+ */
28
+ function parseNodeVersion(v) {
29
+ if (typeof v !== 'string') return null;
30
+ const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v);
31
+ if (!m) return null;
32
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
33
+ }
34
+
35
+ /**
36
+ * @returns {boolean} true if current Node satisfies the minimum.
37
+ */
38
+ export function isSupportedNodeVersion(versionString = process.version) {
39
+ const parsed = parseNodeVersion(versionString);
40
+ if (!parsed) return true; // unknown — don't block
41
+ const [major, minor] = parsed;
42
+ if (major > MIN_MAJOR) return true;
43
+ if (major < MIN_MAJOR) return false;
44
+ return minor >= MIN_MINOR;
45
+ }
46
+
47
+ /**
48
+ * Print a clear error and exit(1) if Node is too old.
49
+ *
50
+ * @param {{ component?: string }} [opts]
51
+ */
52
+ export function assertNodeVersion(opts = {}) {
53
+ if (isSupportedNodeVersion()) return;
54
+ const component = opts.component || 'yeaft';
55
+ const required = `${MIN_MAJOR}.${MIN_MINOR}.0`;
56
+ const lines = [
57
+ '',
58
+ `✖ ${component} requires Node.js ≥ ${required}`,
59
+ ` Current: ${process.version}`,
60
+ '',
61
+ ' This project uses the built-in `node:sqlite` module which was',
62
+ ` added in Node ${required}.`,
63
+ '',
64
+ ' Fix:',
65
+ ' # if you use nvm:',
66
+ ' nvm install 22 # installs the latest 22.x (currently > 22.5)',
67
+ ' nvm use 22',
68
+ '',
69
+ ' # or upgrade Node from https://nodejs.org/',
70
+ '',
71
+ ];
72
+ process.stderr.write(lines.join('\n'));
73
+ process.exit(1);
74
+ }
@@ -0,0 +1,298 @@
1
+ import 'dotenv/config';
2
+ import { readFileSync, existsSync, writeFileSync } from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+ import { userDb } from './database.js';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ /**
11
+ * Load users from environment variable or users.json file
12
+ * Format for AUTH_USERS: username:passwordHash:email
13
+ * TOTP settings are loaded from SQLite database
14
+ *
15
+ * On startup, users are migrated into SQLite with role='admin'.
16
+ * After migration, all runtime queries go through the database.
17
+ */
18
+ function loadUsers() {
19
+ const users = [];
20
+
21
+ // Try loading from environment variable first
22
+ if (process.env.AUTH_USERS) {
23
+ const userEntries = process.env.AUTH_USERS.split(',');
24
+ for (const entry of userEntries) {
25
+ const parts = entry.split(':');
26
+ const [username, passwordHash, email] = parts;
27
+ if (username && passwordHash && email) {
28
+ const trimmedUsername = username.trim();
29
+ // Load TOTP settings from database
30
+ const totpData = userDb.getTotp(trimmedUsername);
31
+ users.push({
32
+ username: trimmedUsername,
33
+ passwordHash: passwordHash.trim(),
34
+ email: email.trim(),
35
+ totpSecret: totpData?.totpSecret || null,
36
+ totpEnabled: totpData?.totpEnabled || false
37
+ });
38
+ }
39
+ }
40
+ }
41
+
42
+ // Try loading from users.json file (fallback for backward compatibility)
43
+ const usersFile = join(__dirname, 'users.json');
44
+ if (existsSync(usersFile)) {
45
+ try {
46
+ const fileContent = JSON.parse(readFileSync(usersFile, 'utf-8'));
47
+ if (Array.isArray(fileContent.users)) {
48
+ for (const u of fileContent.users) {
49
+ // Skip if user already loaded from env
50
+ if (users.find(existing => existing.username === u.username)) continue;
51
+ // Load TOTP settings from database
52
+ const totpData = userDb.getTotp(u.username);
53
+ users.push({
54
+ ...u,
55
+ totpSecret: totpData?.totpSecret || u.totpSecret || null,
56
+ totpEnabled: totpData?.totpEnabled || u.totpEnabled || false
57
+ });
58
+ }
59
+ }
60
+ } catch (err) {
61
+ console.error('Failed to load users.json:', err.message);
62
+ }
63
+ }
64
+
65
+ // Migrate all loaded users into SQLite (idempotent)
66
+ let migrated = 0;
67
+ for (const u of users) {
68
+ const result = userDb.migrateUser(u.username, u.passwordHash, u.email, 'admin');
69
+ if (result) migrated++;
70
+ }
71
+ if (migrated > 0) {
72
+ console.log(`[Migration] Synced ${migrated} users from AUTH_USERS/users.json to database`);
73
+ }
74
+
75
+ return users;
76
+ }
77
+
78
+ // Default secrets - these MUST be changed in production
79
+ const DEFAULT_JWT_SECRET = 'default-secret-change-in-production';
80
+ const DEFAULT_AGENT_SECRET = 'agent-shared-secret';
81
+
82
+ export const CONFIG = {
83
+ // Server settings
84
+ port: parseInt(process.env.PORT, 10) || 3456,
85
+ host: process.env.SERVER_HOST || undefined,
86
+
87
+ // Authentication
88
+ skipAuth: process.env.SKIP_AUTH === 'true',
89
+ users: loadUsers(),
90
+
91
+ // JWT settings
92
+ jwtSecret: process.env.JWT_SECRET || DEFAULT_JWT_SECRET,
93
+ jwtExpiresIn: process.env.JWT_EXPIRES_IN || '3d',
94
+ // Sliding renew: when an inbound request's token has less than this many ms
95
+ // remaining, requireAuth issues a fresh token via the X-New-Token response
96
+ // header. 1 day means an active user never sees an unexpected logout.
97
+ jwtRenewThresholdMs: parseInt(process.env.JWT_RENEW_THRESHOLD_MS, 10) || 24 * 60 * 60 * 1000,
98
+ tempTokenExpiresIn: process.env.TEMP_TOKEN_EXPIRES_IN || '10m',
99
+
100
+ // Email verification
101
+ emailCodeLength: parseInt(process.env.EMAIL_CODE_LENGTH, 10) || 6,
102
+ emailCodeExpiresIn: parseInt(process.env.EMAIL_CODE_EXPIRES_IN, 10) || 300000, // 5 minutes in ms
103
+
104
+ // SMTP settings
105
+ smtp: {
106
+ host: process.env.SMTP_HOST || '',
107
+ port: parseInt(process.env.SMTP_PORT, 10) || 587,
108
+ secure: process.env.SMTP_SECURE === 'true',
109
+ user: process.env.SMTP_USER || '',
110
+ pass: process.env.SMTP_PASS || '',
111
+ from: process.env.SMTP_FROM || 'WebChat <noreply@example.com>'
112
+ },
113
+
114
+ // Agent authentication (global fallback — per-user agent_secret is preferred)
115
+ agentSecret: process.env.AGENT_SECRET || DEFAULT_AGENT_SECRET,
116
+
117
+ // File upload settings
118
+ maxFileSize: parseInt(process.env.MAX_FILE_SIZE, 10) || 50 * 1024 * 1024, // 50MB
119
+ fileCleanupInterval: parseInt(process.env.FILE_CLEANUP_INTERVAL, 10) || 600000, // 10 minutes
120
+
121
+ // TOTP (Two-Factor Authentication) settings
122
+ totp: {
123
+ enabled: process.env.TOTP_ENABLED !== 'false', // Enable by default
124
+ issuer: process.env.TOTP_ISSUER || 'Claude Web Chat',
125
+ window: parseInt(process.env.TOTP_WINDOW, 10) || 1 // Allow 1 step before/after
126
+ },
127
+
128
+ // Azure AD (Microsoft Entra ID) SSO settings
129
+ aad: {
130
+ enabled: process.env.AAD_ENABLED === 'true',
131
+ clientId: process.env.AAD_CLIENT_ID || '',
132
+ tenantId: process.env.AAD_TENANT_ID || '',
133
+ autoCreateUser: process.env.AAD_AUTO_CREATE_USER !== 'false', // Auto-create local user on first AAD login
134
+ defaultRole: process.env.AAD_DEFAULT_ROLE || 'pro' // Default role for auto-created AAD users
135
+ },
136
+
137
+ // Multi-provider SSO settings (server-driven OAuth code flow)
138
+ // Each provider follows the same shape: { enabled, clientId/secret, callbackUrl, autoCreateUser, defaultRole }
139
+ // Microsoft re-uses the existing aad.* block above and is included here for the unified UI listing only.
140
+ sso: {
141
+ github: {
142
+ enabled: process.env.SSO_GITHUB_ENABLED === 'true',
143
+ clientId: process.env.SSO_GITHUB_CLIENT_ID || '',
144
+ clientSecret: process.env.SSO_GITHUB_CLIENT_SECRET || '',
145
+ callbackUrl: process.env.SSO_GITHUB_CALLBACK_URL || '',
146
+ autoCreateUser: process.env.SSO_GITHUB_AUTO_CREATE_USER !== 'false',
147
+ defaultRole: process.env.SSO_GITHUB_DEFAULT_ROLE || 'pro'
148
+ },
149
+ google: {
150
+ enabled: process.env.SSO_GOOGLE_ENABLED === 'true',
151
+ clientId: process.env.SSO_GOOGLE_CLIENT_ID || '',
152
+ clientSecret: process.env.SSO_GOOGLE_CLIENT_SECRET || '',
153
+ callbackUrl: process.env.SSO_GOOGLE_CALLBACK_URL || '',
154
+ autoCreateUser: process.env.SSO_GOOGLE_AUTO_CREATE_USER !== 'false',
155
+ defaultRole: process.env.SSO_GOOGLE_DEFAULT_ROLE || 'pro'
156
+ },
157
+ wechat: {
158
+ // PC web 扫码 (Open Platform 网页扫码) — open.weixin.qq.com/connect/qrconnect
159
+ enabled: process.env.SSO_WECHAT_ENABLED === 'true',
160
+ appId: process.env.SSO_WECHAT_APP_ID || '',
161
+ appSecret: process.env.SSO_WECHAT_APP_SECRET || '',
162
+ callbackUrl: process.env.SSO_WECHAT_CALLBACK_URL || '',
163
+ autoCreateUser: process.env.SSO_WECHAT_AUTO_CREATE_USER !== 'false',
164
+ defaultRole: process.env.SSO_WECHAT_DEFAULT_ROLE || 'pro'
165
+ },
166
+ alipay: {
167
+ // 支付宝网页授权 — oauth2/publicAppAuthorize.htm (RSA2 signing)
168
+ enabled: process.env.SSO_ALIPAY_ENABLED === 'true',
169
+ appId: process.env.SSO_ALIPAY_APP_ID || '',
170
+ privateKey: process.env.SSO_ALIPAY_PRIVATE_KEY || '',
171
+ alipayPublicKey: process.env.SSO_ALIPAY_PUBLIC_KEY || '',
172
+ callbackUrl: process.env.SSO_ALIPAY_CALLBACK_URL || '',
173
+ autoCreateUser: process.env.SSO_ALIPAY_AUTO_CREATE_USER !== 'false',
174
+ defaultRole: process.env.SSO_ALIPAY_DEFAULT_ROLE || 'pro'
175
+ }
176
+ }
177
+ };
178
+
179
+ /**
180
+ * Check if email is configured
181
+ */
182
+ export function isEmailConfigured() {
183
+ return !!(CONFIG.smtp.host && CONFIG.smtp.user && CONFIG.smtp.pass);
184
+ }
185
+
186
+ /**
187
+ * Get user by username — queries database first, falls back to CONFIG.users
188
+ * @param {string} username
189
+ * @returns {object|null} Normalized user object with { username, passwordHash, email, totpSecret, totpEnabled, role, id }
190
+ */
191
+ export function getUserByUsername(username) {
192
+ // Query database first (includes migrated, registered, and SSO-only users).
193
+ const dbUser = userDb.getByUsername(username);
194
+ if (dbUser) {
195
+ return {
196
+ username: dbUser.username,
197
+ passwordHash: dbUser.password_hash || null,
198
+ email: dbUser.email,
199
+ totpSecret: dbUser.totp_secret,
200
+ totpEnabled: !!dbUser.totp_enabled,
201
+ role: dbUser.role === 'admin' ? 'admin' : 'pro',
202
+ id: dbUser.id
203
+ };
204
+ }
205
+ // Fallback to CONFIG.users (only relevant before first migration)
206
+ return CONFIG.users.find(u => u.username === username) || null;
207
+ }
208
+
209
+ /**
210
+ * Update user TOTP settings in SQLite database
211
+ * @param {string} username
212
+ * @param {{totpSecret: string, totpEnabled: boolean}} data
213
+ * @returns {Promise<boolean>}
214
+ */
215
+ export async function updateUserTotp(username, data) {
216
+ try {
217
+ userDb.updateTotp(username, data.totpSecret, data.totpEnabled);
218
+
219
+ // Update in-memory user data (CONFIG.users) for backward compat
220
+ const user = CONFIG.users.find(u => u.username === username);
221
+ if (user) {
222
+ user.totpSecret = data.totpSecret;
223
+ user.totpEnabled = data.totpEnabled;
224
+ }
225
+
226
+ console.log(`TOTP settings saved to database for user: ${username}`);
227
+ return true;
228
+ } catch (err) {
229
+ console.error('Failed to save TOTP settings:', err.message);
230
+ return false;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Check if TOTP is globally enabled
236
+ * @returns {boolean}
237
+ */
238
+ export function isTotpEnabled() {
239
+ return CONFIG.totp?.enabled !== false;
240
+ }
241
+
242
+ /**
243
+ * Validate that required secrets are configured in production mode
244
+ * Throws an error if default secrets are used in production
245
+ */
246
+ export function validateProductionConfig() {
247
+ if (CONFIG.skipAuth) {
248
+ // Development mode - skip validation
249
+ return { valid: true };
250
+ }
251
+
252
+ const errors = [];
253
+ const warnings = [];
254
+
255
+ // Check JWT_SECRET
256
+ if (CONFIG.jwtSecret === DEFAULT_JWT_SECRET) {
257
+ errors.push('JWT_SECRET must be set to a secure value in production mode');
258
+ }
259
+
260
+ // Check that at least one user with a password exists (in DB or config)
261
+ // Only warn (don't block startup) — allows first-time setup via create-user.js
262
+ const dbUsers = userDb.getAll();
263
+ const hasUserWithPassword = dbUsers.some(u => u.password_hash) || CONFIG.users.length > 0;
264
+ if (!hasUserWithPassword) {
265
+ warnings.push('No users configured. Create one with: node server/create-user.js <username> <password> [email]');
266
+ }
267
+
268
+ if (errors.length > 0) {
269
+ return { valid: false, errors };
270
+ }
271
+
272
+ return { valid: true, warnings: warnings.length > 0 ? warnings : undefined };
273
+ }
274
+
275
+ /**
276
+ * Check if Azure AD SSO is configured
277
+ * @returns {boolean}
278
+ */
279
+ export function isAadEnabled() {
280
+ return CONFIG.aad?.enabled && !!CONFIG.aad.clientId && !!CONFIG.aad.tenantId;
281
+ }
282
+
283
+ /**
284
+ * Return the set of enabled SSO providers (excluding Microsoft, which is exposed via aadEnabled).
285
+ * Used by /api/auth/mode to drive the LoginPage button list.
286
+ * @returns {{ github: boolean, google: boolean, wechat: boolean, alipay: boolean }}
287
+ */
288
+ export function getEnabledSsoProviders() {
289
+ const sso = CONFIG.sso || {};
290
+ return {
291
+ github: !!(sso.github?.enabled && sso.github.clientId && sso.github.clientSecret && sso.github.callbackUrl),
292
+ google: !!(sso.google?.enabled && sso.google.clientId && sso.google.clientSecret && sso.google.callbackUrl),
293
+ wechat: !!(sso.wechat?.enabled && sso.wechat.appId && sso.wechat.appSecret && sso.wechat.callbackUrl),
294
+ alipay: !!(sso.alipay?.enabled && sso.alipay.appId && sso.alipay.privateKey && sso.alipay.callbackUrl)
295
+ };
296
+ }
297
+
298
+ export default CONFIG;
@@ -0,0 +1,140 @@
1
+ import { WebSocket } from 'ws';
2
+
3
+ // 存储所有连接的 agents
4
+ // agentId -> { ws, name, workDir, conversations: Map<convId, {workDir, claudeSessionId}>, sessionKey, isAlive, capabilities }
5
+ export const agents = new Map();
6
+
7
+ // 存储所有 web 客户端
8
+ // clientId -> { ws, authenticated, currentAgent, currentConversation, sessionKey, isAlive }
9
+ export const webClients = new Map();
10
+
11
+ // 临时文件存储: fileId -> { name, mimeType, buffer, uploadedAt, userId }
12
+ export const pendingFiles = new Map();
13
+
14
+ // Port proxy
15
+ export const pendingProxyRequests = new Map(); // requestId → { res, timeout, streaming }
16
+ export const proxyWsConnections = new Map(); // proxyWsId → { browserWs, agentId }
17
+
18
+ // Store pending agent connections (waiting for auth message)
19
+ // tempId -> { ws, agentId, agentName, workDir, timeout }
20
+ export const pendingAgentConnections = new Map();
21
+
22
+ // ★ Phase 3: Server-side message queues
23
+ // conversationId → [{id, prompt, workDir, userId, clientId, queuedAt, files}]
24
+ export const serverMessageQueues = new Map();
25
+
26
+ // ★ Phase 4: Directory listing cache
27
+ // key: `${agentId}:${normalizedDirPath}` → { entries, timestamp }
28
+ export const directoryCache = new Map();
29
+ export const DIR_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
30
+ export const DIR_CACHE_MAX_SIZE = 500;
31
+
32
+ // ★ Phase 5: File Tab state storage
33
+ // key: `${userId}:${agentId}` → { files: [{path}], activeIndex, timestamp }
34
+ export const userFileTabs = new Map();
35
+
36
+ // Preview file cache for binary file preview (Office/PDF/Image)
37
+ // fileId → { buffer, mimeType, filename, createdAt, token }
38
+ export const previewFiles = new Map();
39
+
40
+ // Admin dashboard usage stats.
41
+ // userId → { requests, bytesSent, bytesReceived, messages, sessions }
42
+ // `messages` is user turn count. bytesSent/bytesReceived are message traffic
43
+ // only; heartbeat/control frames are deliberately excluded.
44
+ export const userStatsDeltas = new Map();
45
+
46
+ const HEARTBEAT_MESSAGE_TYPES = new Set([
47
+ 'ping',
48
+ 'pong',
49
+ 'ping_session',
50
+ 'pong_session',
51
+ 'client_hello'
52
+ ]);
53
+
54
+ const OUTBOUND_MESSAGE_TRAFFIC_TYPES = new Set([
55
+ 'claude_output',
56
+ 'yeaft_output',
57
+ 'btw_stream',
58
+ 'btw_done',
59
+ 'btw_error',
60
+ 'context_usage',
61
+ 'ask_user_question'
62
+ ]);
63
+
64
+ /**
65
+ * Get or initialize a stats delta entry for a user.
66
+ */
67
+ function getOrCreateDelta(userId) {
68
+ let delta = userStatsDeltas.get(userId);
69
+ if (!delta) {
70
+ delta = {
71
+ requests: 0,
72
+ bytesSent: 0,
73
+ bytesReceived: 0,
74
+ messages: 0,
75
+ sessions: 0,
76
+ };
77
+ userStatsDeltas.set(userId, delta);
78
+ }
79
+ return delta;
80
+ }
81
+
82
+ export function isHeartbeatMessageType(type) {
83
+ return HEARTBEAT_MESSAGE_TYPES.has(type);
84
+ }
85
+
86
+ export function isOutboundMessageTraffic(type) {
87
+ return OUTBOUND_MESSAGE_TRAFFIC_TYPES.has(type);
88
+ }
89
+
90
+ /**
91
+ * Record a non-heartbeat WS request received from a user. This is kept for
92
+ * operator diagnostics; dashboard traffic comes from user-turn/message bytes.
93
+ */
94
+ export function trackRequest(userId, bytesReceived, messageType = '') {
95
+ if (!userId || isHeartbeatMessageType(messageType)) return;
96
+ const delta = getOrCreateDelta(userId);
97
+ delta.requests++;
98
+ }
99
+
100
+ /**
101
+ * Record outbound message bytes sent to a user via WS.
102
+ */
103
+ export function trackMessageBytesSent(userId, bytesSent, messageType = '') {
104
+ if (!userId || !bytesSent || !isOutboundMessageTraffic(messageType)) return;
105
+ const delta = getOrCreateDelta(userId);
106
+ delta.bytesSent += bytesSent;
107
+ }
108
+
109
+ /**
110
+ * Backward-compatible alias. Only message output frames are counted.
111
+ */
112
+ export function trackBytesSent(userId, bytesSent, messageType = '') {
113
+ trackMessageBytesSent(userId, bytesSent, messageType);
114
+ }
115
+
116
+ /**
117
+ * Record a user turn and the inbound message payload bytes for it.
118
+ */
119
+ export function trackUserTurn(userId, bytesReceived = 0) {
120
+ if (!userId) return;
121
+ const delta = getOrCreateDelta(userId);
122
+ delta.messages++;
123
+ delta.bytesReceived += Math.max(0, Number(bytesReceived) || 0);
124
+ }
125
+
126
+ /**
127
+ * Legacy name: a tracked "message" is now a user turn.
128
+ */
129
+ export function trackMessage(userId, bytesReceived = 0) {
130
+ trackUserTurn(userId, bytesReceived);
131
+ }
132
+
133
+ /**
134
+ * Record a new session created by a user.
135
+ */
136
+ export function trackSession(userId) {
137
+ if (!userId) return;
138
+ const delta = getOrCreateDelta(userId);
139
+ delta.sessions++;
140
+ }