Add argon2 polyfill and unified preview/API startup for admin login.

Node < 24 lacks crypto.argon2Sync; wire @node-rs/argon2 fallback and start Admin API alongside vite preview in local and remote deploy flows.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
John
2026-06-17 16:38:58 -07:00
parent 9ecfa73831
commit 7a5b9cc912
7 changed files with 414 additions and 21 deletions
+310
View File
@@ -8,6 +8,7 @@
"name": "tkmind-admin",
"version": "0.1.0",
"dependencies": {
"@node-rs/argon2": "^2.0.2",
"express": "^4.21.2",
"mysql2": "^3.22.5",
"react": "^19.0.0",
@@ -304,6 +305,37 @@
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@@ -796,6 +828,267 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
"integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@tybys/wasm-util": "^0.10.0"
}
},
"node_modules/@node-rs/argon2": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-2.0.2.tgz",
"integrity": "sha512-t64wIsPEtNd4aUPuTAyeL2ubxATCBGmeluaKXEMAFk/8w6AJIVVkeLKMBpgLW6LU2t5cQxT+env/c6jxbtTQBg==",
"license": "MIT",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@node-rs/argon2-android-arm-eabi": "2.0.2",
"@node-rs/argon2-android-arm64": "2.0.2",
"@node-rs/argon2-darwin-arm64": "2.0.2",
"@node-rs/argon2-darwin-x64": "2.0.2",
"@node-rs/argon2-freebsd-x64": "2.0.2",
"@node-rs/argon2-linux-arm-gnueabihf": "2.0.2",
"@node-rs/argon2-linux-arm64-gnu": "2.0.2",
"@node-rs/argon2-linux-arm64-musl": "2.0.2",
"@node-rs/argon2-linux-x64-gnu": "2.0.2",
"@node-rs/argon2-linux-x64-musl": "2.0.2",
"@node-rs/argon2-wasm32-wasi": "2.0.2",
"@node-rs/argon2-win32-arm64-msvc": "2.0.2",
"@node-rs/argon2-win32-ia32-msvc": "2.0.2",
"@node-rs/argon2-win32-x64-msvc": "2.0.2"
}
},
"node_modules/@node-rs/argon2-android-arm-eabi": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-2.0.2.tgz",
"integrity": "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/argon2-android-arm64": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-2.0.2.tgz",
"integrity": "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/argon2-darwin-arm64": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-2.0.2.tgz",
"integrity": "sha512-3TTNL/7wbcpNju5YcqUrCgXnXUSbD7ogeAKatzBVHsbpjZQbNb1NDxDjqqrWoTt6XL3z9mJUMGwbAk7zQltHtA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/argon2-darwin-x64": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-2.0.2.tgz",
"integrity": "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/argon2-freebsd-x64": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-2.0.2.tgz",
"integrity": "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/argon2-linux-arm-gnueabihf": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-2.0.2.tgz",
"integrity": "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/argon2-linux-arm64-gnu": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-2.0.2.tgz",
"integrity": "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/argon2-linux-arm64-musl": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-2.0.2.tgz",
"integrity": "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/argon2-linux-x64-gnu": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-2.0.2.tgz",
"integrity": "sha512-ZM3jrHuJ0dKOhvA80gKJqBpBRmTJTFSo2+xVZR+phQcbAKRlDMSZMFDiKbSTnctkfwNFtjgDdh5g1vaEV04AvA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/argon2-linux-x64-musl": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-2.0.2.tgz",
"integrity": "sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/argon2-wasm32-wasi": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-2.0.2.tgz",
"integrity": "sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ==",
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@napi-rs/wasm-runtime": "^0.2.5"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@node-rs/argon2-win32-arm64-msvc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-2.0.2.tgz",
"integrity": "sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/argon2-win32-ia32-msvc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-2.0.2.tgz",
"integrity": "sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/argon2-win32-x64-msvc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-2.0.2.tgz",
"integrity": "sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -1153,6 +1446,16 @@
"win32"
]
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -2691,6 +2994,13 @@
"node": ">=0.6"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD",
"optional": true
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+3 -1
View File
@@ -8,9 +8,11 @@
"dev:web": "vite",
"dev:server": "node server/index.mjs",
"build": "vite build",
"preview": "vite preview"
"preview": "node scripts/preview.mjs",
"dev:preview": "vite preview"
},
"dependencies": {
"@node-rs/argon2": "^2.0.2",
"express": "^4.21.2",
"mysql2": "^3.22.5",
"react": "^19.0.0",
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const args = process.argv.slice(2);
function spawnChild(command, args, label) {
const child = spawn(command, args, {
cwd: root,
stdio: 'inherit',
env: process.env,
});
child.on('exit', (code, signal) => {
if (signal) return;
if (code && code !== 0) shutdown(code ?? 1);
});
return child;
}
let server;
let web;
let stopping = false;
function shutdown(code = 0) {
if (stopping) return;
stopping = true;
server?.kill('SIGTERM');
web?.kill('SIGTERM');
setTimeout(() => process.exit(code), 200).unref();
}
process.on('SIGINT', () => shutdown(0));
process.on('SIGTERM', () => shutdown(0));
console.log('==> 启动 Admin API (memind_adm/server)');
server = spawnChild('node', ['server/index.mjs'], 'server');
setTimeout(() => {
console.log('==> 启动 vite preview @ http://127.0.0.1:5174');
web = spawnChild('npm', ['run', 'dev:preview', ...args], 'web');
}, 800);
+23 -3
View File
@@ -4,9 +4,12 @@ set -euo pipefail
ROOT="${ADM_ROOT:-/Users/john/Project/memind_adm}"
PORT="${ADM_PORT:-5174}"
NODE_PATH="${LAN_NODE_PATH:-/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin}"
NODE_PATH="${LAN_NODE_PATH:-/opt/homebrew/opt/node@24/bin:/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin}"
API_PORT="${ADM_API_PORT:-8085}"
LOG="${ROOT}/adm-preview.log"
API_LOG="${ROOT}/adm-api.log"
PID_FILE="${ROOT}/.adm-preview.pid"
API_PID_FILE="${ROOT}/.adm-api.pid"
export PATH="${NODE_PATH}:${PATH}"
command -v node >/dev/null 2>&1 || { echo "❌ 未找到 node" >&2; exit 1; }
@@ -23,19 +26,36 @@ if [[ ! -d node_modules ]]; then
npm install
fi
echo "==> 停止旧进程 (port ${PORT})..."
echo "==> 停止旧进程 (port ${PORT}, ${API_PORT})..."
lsof -ti "TCP:${PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true
lsof -ti "TCP:${API_PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true
sleep 1
lsof -ti "TCP:${PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill -9 2>/dev/null || true
lsof -ti "TCP:${API_PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill -9 2>/dev/null || true
echo "==> 启动 Admin API @ 127.0.0.1:${API_PORT}..."
nohup node server/index.mjs >>"${API_LOG}" 2>&1 &
echo $! >"${API_PID_FILE}"
sleep 2
if ! curl -sf "http://127.0.0.1:${API_PORT}/health" >/dev/null; then
echo "❌ Admin API 启动失败,最近日志:" >&2
tail -n 40 "${API_LOG}" >&2 || true
exit 1
fi
echo "==> 启动 vite preview @ 127.0.0.1:${PORT}..."
nohup npm run preview -- --host 127.0.0.1 --port "${PORT}" >>"${LOG}" 2>&1 &
nohup npm run dev:preview -- --host 127.0.0.1 --port "${PORT}" >>"${LOG}" 2>&1 &
echo $! >"${PID_FILE}"
sleep 2
if curl -sf "http://127.0.0.1:${PORT}/" >/dev/null; then
echo "✅ memind_adm 已就绪: http://127.0.0.1:${PORT}/"
curl -sf "http://127.0.0.1:${PORT}/" >/dev/null && echo " 首页 OK"
curl -sf -X POST "http://127.0.0.1:${PORT}/auth/login" \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"981122"}' >/dev/null \
&& echo " 登录 OK" || echo " ⚠️ 登录接口异常,请检查 ${API_LOG}"
else
echo "❌ 启动失败,最近日志:" >&2
tail -n 40 "${LOG}" >&2 || true
+9 -17
View File
@@ -1,5 +1,5 @@
import express from 'express';
import { paginateArray, listUsagePaged, listLedgerPaged } from './pagination.mjs';
import { listUsagePaged, listLedgerPaged } from './pagination.mjs';
export function createAdminApp(services) {
const { userAuth, llmProviderService, pool, ready } = services;
@@ -100,22 +100,14 @@ export function createAdminApp(services) {
};
adminApi.get('/users', requireAdmin, async (req, res) => {
let users = await userAuth.listUsers();
const search = typeof req.query.search === 'string' ? req.query.search.trim().toLowerCase() : '';
const role = typeof req.query.role === 'string' ? req.query.role.trim() : '';
const status = typeof req.query.status === 'string' ? req.query.status.trim() : '';
if (search) {
users = users.filter(
(user) =>
user.username?.toLowerCase().includes(search) ||
user.displayName?.toLowerCase().includes(search) ||
user.email?.toLowerCase().includes(search),
);
}
if (role) users = users.filter((user) => user.role === role);
if (status) users = users.filter((user) => user.status === status);
const paged = paginateArray(users, req.query, 20);
res.json({ users: paged.items, total: paged.total, page: paged.page, pageSize: paged.pageSize });
const result = await userAuth.listUsers({
page: Number(req.query.page) || 1,
pageSize: Number(req.query.pageSize) || 20,
search: typeof req.query.search === 'string' ? req.query.search.trim() : '',
role: typeof req.query.role === 'string' ? req.query.role.trim() : '',
status: typeof req.query.status === 'string' ? req.query.status.trim() : '',
});
res.json(result);
});
adminApi.get('/summary', requireAdmin, async (_req, res) => {
+23
View File
@@ -0,0 +1,23 @@
import crypto from 'node:crypto';
import { Algorithm, hashRawSync } from '@node-rs/argon2';
/** Node < 24 无 crypto.argon2SyncMemind user-auth 登录会崩溃。 */
export function ensureArgon2Sync() {
if (typeof crypto.argon2Sync === 'function') return;
crypto.argon2Sync = (algorithm, options = {}) => {
if (algorithm !== 'argon2id') {
throw new Error(`unsupported argon2 algorithm: ${algorithm}`);
}
const message = options.message ?? '';
const nonce = options.nonce ?? Buffer.alloc(0);
return hashRawSync(String(message), {
algorithm: Algorithm.Argon2id,
salt: nonce,
parallelism: options.parallelism ?? 1,
outputLen: options.tagLength ?? 32,
memoryCost: options.memory ?? 65536,
timeCost: options.passes ?? 3,
});
};
}
+3
View File
@@ -1,4 +1,7 @@
import { ensureArgon2Sync } from './argon2-polyfill.mjs';
import { loadProjectEnv } from './load-env.mjs';
ensureArgon2Sync();
import { bootstrapAdminServices } from './bootstrap.mjs';
import { importMemind } from './lib-path.mjs';
import { createAdminApp } from './app.mjs';