@seele00/seele 0.1.60
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/LEGAL.md +7 -0
- package/README.md +76 -0
- package/bin/seele-codex +58 -0
- package/codex/dist/app-server.js +63 -0
- package/codex/dist/errors.js +16 -0
- package/codex/dist/index.js +4 -0
- package/codex/dist/rpc-client.js +96 -0
- package/codex/dist/transports/stdio.js +81 -0
- package/codex/dist/types.js +1 -0
- package/codex/package.json +12 -0
- package/package.json +24 -0
- package/scripts/deploy/bootstrap.sh +318 -0
- package/scripts/deploy/npm/postinstall.mjs +10 -0
- package/scripts/deploy/prepare-proxy.sh +273 -0
- package/scripts/deploy/seele.env.example +75 -0
- package/scripts/deploy/start.sh +101 -0
- package/server/dist/auth/session.js +71 -0
- package/server/dist/auth/users.js +192 -0
- package/server/dist/codex-runtime/run-streamed-turn.js +587 -0
- package/server/dist/config/config.js +71 -0
- package/server/dist/contracts/artifact-rendering.js +14 -0
- package/server/dist/contracts/global-environment.js +23 -0
- package/server/dist/contracts/index.js +114 -0
- package/server/dist/contracts/managed-assets.js +13 -0
- package/server/dist/contracts/response-style.js +23 -0
- package/server/dist/contracts/task-clarification.js +13 -0
- package/server/dist/contracts/types.js +1 -0
- package/server/dist/contracts/wakeup.js +13 -0
- package/server/dist/contracts/workspace-boundary.js +20 -0
- package/server/dist/contracts/workspace-knowledge.js +48 -0
- package/server/dist/contracts/workspace-memory.js +32 -0
- package/server/dist/contracts/workspace-responsibility.js +23 -0
- package/server/dist/contracts/workspace-transcript.js +26 -0
- package/server/dist/conversations/knowledge.js +380 -0
- package/server/dist/conversations/store.js +2376 -0
- package/server/dist/doctor.js +105 -0
- package/server/dist/realtime/sse.js +12 -0
- package/server/dist/runtime-worker/client.js +405 -0
- package/server/dist/runtime-worker/process.js +216 -0
- package/server/dist/runtime-worker/protocol.js +1 -0
- package/server/dist/server.js +4570 -0
- package/server/dist/storage/sqlite.js +39 -0
- package/web/dist/assets/abnfDiagram-VRR7QNED-DevTexTl.js +1 -0
- package/web/dist/assets/arc-WlXuN6B_.js +1 -0
- package/web/dist/assets/architectureDiagram-ZJ3FMSHR-DJYRAqSp.js +36 -0
- package/web/dist/assets/bash-BELqPA7S.js +1 -0
- package/web/dist/assets/blockDiagram-677ZJIJ3-CsjO1oTv.js +132 -0
- package/web/dist/assets/c4Diagram-LMCZKHZV-DjUCYgVk.js +10 -0
- package/web/dist/assets/channel-DGnywxBx.js +1 -0
- package/web/dist/assets/chunk-2Q5K7J3B-By6S-6eo.js +1 -0
- package/web/dist/assets/chunk-32BRIVSS-Cl-hXbZD.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4--cWPnxlT.js +15 -0
- package/web/dist/assets/chunk-EX3LRPZG-PakDrWLX.js +231 -0
- package/web/dist/assets/chunk-JWPE2WC7-DMfuuvGl.js +1 -0
- package/web/dist/assets/chunk-MOJQB5TN-BHD_IRop.js +88 -0
- package/web/dist/assets/chunk-RYQCIY6F-T4jqSvsD.js +1 -0
- package/web/dist/assets/chunk-V7JOEXUC-C4paE6t8.js +206 -0
- package/web/dist/assets/chunk-VR4S4FIN-F5foapWH.js +1 -0
- package/web/dist/assets/chunk-XXDRQBXY-Dp4WIFPJ.js +1 -0
- package/web/dist/assets/classDiagram-OUVF2IWQ-DjHXtkKr.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-DjHXtkKr.js +1 -0
- package/web/dist/assets/core-CGjsG6g4.js +12 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-DcMU2oKv.js +1 -0
- package/web/dist/assets/css-CLj8gQPS.js +1 -0
- package/web/dist/assets/cynefin-VYW2F7L2-SiDUbAfJ.js +178 -0
- package/web/dist/assets/cynefinDiagram-TSTJHNR4-DhkNlW7i.js +62 -0
- package/web/dist/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/web/dist/assets/dagre-VKFMJZFB-Co--CY4-.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-FQU43EPY-BZs6G_Wi.js +3 -0
- package/web/dist/assets/diagram-G47NLZAW-C6SAe39v.js +24 -0
- package/web/dist/assets/diagram-NH7WQ7WH-D6EoRzQM.js +24 -0
- package/web/dist/assets/diagram-OA4YK3LP-kHe4KJ_b.js +30 -0
- package/web/dist/assets/diagram-WEI45ONY-BM-NnNEq.js +41 -0
- package/web/dist/assets/diff-D97Zzqfu.js +1 -0
- package/web/dist/assets/dockerfile-BcOcwvcX.js +1 -0
- package/web/dist/assets/ebnfDiagram-CCIWWBDH-BfwqR7DU.js +1 -0
- package/web/dist/assets/engine-javascript-DBd1bXLz.js +141 -0
- package/web/dist/assets/erDiagram-Q63AITRT-BwUkv7nL.js +85 -0
- package/web/dist/assets/flowDiagram-23GEKE2U-Cn0zH9bm.js +156 -0
- package/web/dist/assets/ganttDiagram-NO4QXBWP-BY_VLaEQ.js +292 -0
- package/web/dist/assets/gitGraphDiagram-IHSO6WYX-D0xpVnfm.js +106 -0
- package/web/dist/assets/github-light-DAi9KRSo.js +1 -0
- package/web/dist/assets/graph-C9eacEi8.js +1 -0
- package/web/dist/assets/html-pp8916En.js +1 -0
- package/web/dist/assets/index-Bl4JO4M7.css +1 -0
- package/web/dist/assets/index-D9Ao6WQl.js +111 -0
- package/web/dist/assets/infoDiagram-FWYZ7A6U-DnmnPGAH.js +2 -0
- package/web/dist/assets/init-Gi6I4Gst.js +1 -0
- package/web/dist/assets/ishikawaDiagram-FXEZZL3T-DTWLm3ZQ.js +70 -0
- package/web/dist/assets/java-CylS5w8V.js +1 -0
- package/web/dist/assets/javascript-wDzz0qaB.js +1 -0
- package/web/dist/assets/journeyDiagram-5HDEW3XC-RN13a5iK.js +139 -0
- package/web/dist/assets/json-Cp-IABpG.js +1 -0
- package/web/dist/assets/jsonc-Des-eS-w.js +1 -0
- package/web/dist/assets/jsx-g9-lgVsj.js +1 -0
- package/web/dist/assets/kanban-definition-HUTT4EX6-CvmlJTH9.js +89 -0
- package/web/dist/assets/katex-C5jXJg4s.js +257 -0
- package/web/dist/assets/layout-DEXfKzaS.js +1 -0
- package/web/dist/assets/linear-zHsglUI0.js +1 -0
- package/web/dist/assets/map-Czzmt4hB.js +1 -0
- package/web/dist/assets/markdown-Cvjx9yec.js +1 -0
- package/web/dist/assets/mermaid.core-Bfj9YagN.js +314 -0
- package/web/dist/assets/mindmap-definition-LN4V7U3C-CkcLeMWS.js +96 -0
- package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/web/dist/assets/pegDiagram-2B236MQR-eturQJg0.js +1 -0
- package/web/dist/assets/pieDiagram-ENE6RG2P-DJmmSZSO.js +39 -0
- package/web/dist/assets/python-B6aJPvgy.js +1 -0
- package/web/dist/assets/quadrantDiagram-ABIIQ3AL-Br5y_5ZO.js +7 -0
- package/web/dist/assets/railroadDiagram-RFXS5EU6-DwnwvUqm.js +1 -0
- package/web/dist/assets/requirementDiagram-TGXJPOKE-CMMKGrPQ.js +84 -0
- package/web/dist/assets/sankeyDiagram-HTMAVEWB-BKP-cLrV.js +40 -0
- package/web/dist/assets/sequenceDiagram-DBY2YBRQ-YgkgEj_6.js +162 -0
- package/web/dist/assets/sizeCapture-X5ZJPWSS-nkigHDeZ.js +1 -0
- package/web/dist/assets/sql-CRqJ_cUM.js +1 -0
- package/web/dist/assets/stateDiagram-2N3HPSRC-CEoMK2Kt.js +1 -0
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB--iEtUEGo.js +1 -0
- package/web/dist/assets/swimlanes-5IMT3BWC-Co9CmBP_.js +2 -0
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BzapPKvi.js +8 -0
- package/web/dist/assets/timeline-definition-FHXFAJF6-C5njALNw.js +120 -0
- package/web/dist/assets/toml-vGWfd6FD.js +1 -0
- package/web/dist/assets/tsx-COt5Ahok.js +1 -0
- package/web/dist/assets/typescript-BPQ3VLAy.js +1 -0
- package/web/dist/assets/vennDiagram-L72KCM5P-BpvLQRHS.js +34 -0
- package/web/dist/assets/wardleyDiagram-EHGQE667-CWINCOno.js +78 -0
- package/web/dist/assets/xml-sdJ4AIDG.js +1 -0
- package/web/dist/assets/xychartDiagram-FW5EYKEG-xDH3ABG4.js +7 -0
- package/web/dist/assets/yaml-Buea-lGh.js +1 -0
- package/web/dist/index.html +13 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
5
|
+
cd "$ROOT_DIR"
|
|
6
|
+
|
|
7
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
8
|
+
echo "node is required. Install Node.js 24 LTS before starting Seele." >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
node -e 'const major = Number(process.versions.node.split(".")[0]); if (major !== 24) { console.error(`Node.js 24 LTS is required. Current: ${process.version}`); process.exit(1); }'
|
|
12
|
+
|
|
13
|
+
ENV_FILE=""
|
|
14
|
+
|
|
15
|
+
while [ "$#" -gt 0 ]; do
|
|
16
|
+
case "$1" in
|
|
17
|
+
--env)
|
|
18
|
+
ENV_FILE="${2:-}"
|
|
19
|
+
shift 2
|
|
20
|
+
;;
|
|
21
|
+
-h|--help)
|
|
22
|
+
echo "Usage: ./scripts/deploy/start.sh [--env PATH]"
|
|
23
|
+
exit 0
|
|
24
|
+
;;
|
|
25
|
+
*)
|
|
26
|
+
echo "Unknown option: $1" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
;;
|
|
29
|
+
esac
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
if [ -n "$ENV_FILE" ]; then
|
|
33
|
+
if [ ! -f "$ENV_FILE" ]; then
|
|
34
|
+
echo "Environment file not found: $ENV_FILE" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
set -a
|
|
38
|
+
# shellcheck disable=SC1090
|
|
39
|
+
. "$ENV_FILE"
|
|
40
|
+
set +a
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if [ -z "${SEELE_ROOT:-}" ]; then
|
|
44
|
+
echo "SEELE_ROOT is required." >&2
|
|
45
|
+
echo "Set it in seele.env to the confirmed Seele root directory." >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
export SEELE_ROOT
|
|
49
|
+
RUNTIME_DIR="${SEELE_RUNTIME_DIR:-$SEELE_ROOT/.runtime}"
|
|
50
|
+
export SEELE_RUNTIME_DIR="$RUNTIME_DIR"
|
|
51
|
+
export CODEX_HOME="${CODEX_HOME:-$RUNTIME_DIR/codex-home}"
|
|
52
|
+
export CODEX_INSTALL_DIR="$RUNTIME_DIR/bin"
|
|
53
|
+
export SEELE_CODEX_HOST="${SEELE_CODEX_HOST:-0.0.0.0}"
|
|
54
|
+
export SEELE_CODEX_PORT="${SEELE_CODEX_PORT:-3000}"
|
|
55
|
+
export SEELE_CODEX_WORKSPACE_ROOT="${SEELE_CODEX_WORKSPACE_ROOT:-$SEELE_ROOT/workspaces}"
|
|
56
|
+
export SEELE_CODEX_WEB_DIST="${SEELE_CODEX_WEB_DIST:-$ROOT_DIR/web/dist}"
|
|
57
|
+
export PATH="$CODEX_INSTALL_DIR:$PATH"
|
|
58
|
+
|
|
59
|
+
export NO_PROXY="${NO_PROXY:-127.0.0.1,localhost,.svc,.cluster.local}"
|
|
60
|
+
export no_proxy="$NO_PROXY"
|
|
61
|
+
|
|
62
|
+
if [ ! -x "$CODEX_INSTALL_DIR/codex" ]; then
|
|
63
|
+
echo "Seele-managed Codex CLI was not found at $CODEX_INSTALL_DIR/codex." >&2
|
|
64
|
+
echo "Run ./scripts/deploy/bootstrap.sh before starting Seele." >&2
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
if [ ! -f "$ROOT_DIR/server/dist/server.js" ] || [ ! -f "$SEELE_CODEX_WEB_DIST/index.html" ]; then
|
|
69
|
+
echo "Build output is missing. Run ./scripts/deploy/bootstrap.sh first." >&2
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Prepare the local no-auth proxy entrypoint when an upstream proxy with
|
|
74
|
+
# credentials is configured. Codex's models-refresh / MCP HTTP clients send a
|
|
75
|
+
# bare HTTP CONNECT without proxy credentials; an authenticated upstream then
|
|
76
|
+
# replies 407 and resets the stream. prepare-proxy.sh runs a local no-auth
|
|
77
|
+
# listener (SEELE_PROXY) that forwards to the credentialed upstream
|
|
78
|
+
# (SEELE_UPSTREAM_PROXY). It is idempotent, so a restart re-syncs it.
|
|
79
|
+
# Skipped when SEELE_UPSTREAM_PROXY is unset (legacy single-proxy mode).
|
|
80
|
+
if [ -n "${SEELE_UPSTREAM_PROXY:-}" ]; then
|
|
81
|
+
echo "Preparing local no-auth proxy entrypoint (SEELE_UPSTREAM_PROXY is set)..."
|
|
82
|
+
PREPARE_PROXY="$ROOT_DIR/scripts/deploy/prepare-proxy.sh"
|
|
83
|
+
if [ ! -x "$PREPARE_PROXY" ]; then
|
|
84
|
+
echo "prepare-proxy.sh not found or not executable at $PREPARE_PROXY" >&2
|
|
85
|
+
exit 1
|
|
86
|
+
fi
|
|
87
|
+
if [ -n "$ENV_FILE" ]; then
|
|
88
|
+
"$PREPARE_PROXY" --env "$ENV_FILE"
|
|
89
|
+
else
|
|
90
|
+
"$PREPARE_PROXY"
|
|
91
|
+
fi
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
mkdir -p "$CODEX_HOME"
|
|
95
|
+
mkdir -p "$SEELE_CODEX_WORKSPACE_ROOT"
|
|
96
|
+
|
|
97
|
+
echo "Starting Seele Codex on http://$SEELE_CODEX_HOST:$SEELE_CODEX_PORT"
|
|
98
|
+
echo "Codex home: $CODEX_HOME"
|
|
99
|
+
echo "Workspace root: $SEELE_CODEX_WORKSPACE_ROOT"
|
|
100
|
+
|
|
101
|
+
exec node "$ROOT_DIR/server/dist/server.js"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
const COOKIE_NAME = "seele_session";
|
|
3
|
+
export function readSessionPayload(request, secret) {
|
|
4
|
+
const token = parseCookies(request.headers.cookie)[COOKIE_NAME];
|
|
5
|
+
if (!token) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const [encodedPayload, signature] = token.split(".");
|
|
9
|
+
if (!encodedPayload || !signature) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
if (!safeEqual(signature, sign(encodedPayload, secret))) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const payload = JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8"));
|
|
17
|
+
if (typeof payload.userId !== "string" ||
|
|
18
|
+
!Number.isInteger(payload.sessionVersion) ||
|
|
19
|
+
!Number.isFinite(payload.expiresAt) ||
|
|
20
|
+
payload.expiresAt <= Date.now()) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return payload;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function setSessionCookie(response, request, input) {
|
|
30
|
+
const payload = Buffer.from(JSON.stringify({
|
|
31
|
+
userId: input.userId,
|
|
32
|
+
sessionVersion: input.sessionVersion,
|
|
33
|
+
expiresAt: Date.now() + input.ttlSeconds * 1000,
|
|
34
|
+
})).toString("base64url");
|
|
35
|
+
const token = `${payload}.${sign(payload, input.secret)}`;
|
|
36
|
+
const secure = isSecureRequest(request) ? "; Secure" : "";
|
|
37
|
+
response.setHeader("Set-Cookie", `${COOKIE_NAME}=${token}; Path=${input.path}; HttpOnly; SameSite=Strict; Max-Age=${input.ttlSeconds}${secure}`);
|
|
38
|
+
}
|
|
39
|
+
export function clearSessionCookie(response, request, path) {
|
|
40
|
+
const secure = isSecureRequest(request) ? "; Secure" : "";
|
|
41
|
+
response.setHeader("Set-Cookie", `${COOKIE_NAME}=; Path=${path}; HttpOnly; SameSite=Strict; Max-Age=0${secure}`);
|
|
42
|
+
}
|
|
43
|
+
function sign(value, secret) {
|
|
44
|
+
return createHmac("sha256", secret).update(value).digest("base64url");
|
|
45
|
+
}
|
|
46
|
+
function safeEqual(left, right) {
|
|
47
|
+
const leftBuffer = Buffer.from(left);
|
|
48
|
+
const rightBuffer = Buffer.from(right);
|
|
49
|
+
return (leftBuffer.length === rightBuffer.length &&
|
|
50
|
+
timingSafeEqual(leftBuffer, rightBuffer));
|
|
51
|
+
}
|
|
52
|
+
function parseCookies(header) {
|
|
53
|
+
if (!header) {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
return Object.fromEntries(header.split(";").flatMap((part) => {
|
|
57
|
+
const separator = part.indexOf("=");
|
|
58
|
+
if (separator < 0) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
const key = part.slice(0, separator).trim();
|
|
62
|
+
const value = part.slice(separator + 1).trim();
|
|
63
|
+
return key ? [[key, value]] : [];
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
function isSecureRequest(request) {
|
|
67
|
+
const forwardedProto = request.headers["x-forwarded-proto"];
|
|
68
|
+
const protocol = Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto;
|
|
69
|
+
return (protocol?.split(",")[0]?.trim() === "https" ||
|
|
70
|
+
Boolean(request.socket.encrypted));
|
|
71
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { mkdirSync } from "node:fs";
|
|
4
|
+
import { openSqliteDatabase, } from "../storage/sqlite.js";
|
|
5
|
+
export class UserStore {
|
|
6
|
+
sessionSecret;
|
|
7
|
+
db;
|
|
8
|
+
constructor(databasePath, bootstrap) {
|
|
9
|
+
mkdirSync(path.dirname(databasePath), { recursive: true });
|
|
10
|
+
this.db = openSqliteDatabase(databasePath);
|
|
11
|
+
this.db.exec("pragma journal_mode = WAL");
|
|
12
|
+
this.initializeSchema();
|
|
13
|
+
this.sessionSecret = this.ensureSessionSecret();
|
|
14
|
+
if (this.countUsers() === 0 && bootstrap?.password) {
|
|
15
|
+
this.createUser({
|
|
16
|
+
username: bootstrap.username,
|
|
17
|
+
password: bootstrap.password,
|
|
18
|
+
role: "admin",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
get enabled() {
|
|
23
|
+
return this.countUsers() > 0;
|
|
24
|
+
}
|
|
25
|
+
listUsers() {
|
|
26
|
+
const rows = this.db
|
|
27
|
+
.prepare("select * from auth_users order by created_at asc")
|
|
28
|
+
.all();
|
|
29
|
+
return rows.map(userFromRow);
|
|
30
|
+
}
|
|
31
|
+
getUser(id) {
|
|
32
|
+
const row = this.db
|
|
33
|
+
.prepare("select * from auth_users where id = ?")
|
|
34
|
+
.get(id);
|
|
35
|
+
return row ? userFromRow(row) : null;
|
|
36
|
+
}
|
|
37
|
+
getUserByUsername(username) {
|
|
38
|
+
const row = this.db
|
|
39
|
+
.prepare("select * from auth_users where username = ? collate nocase")
|
|
40
|
+
.get(username.trim());
|
|
41
|
+
return row ? userFromRow(row) : null;
|
|
42
|
+
}
|
|
43
|
+
authenticate(username, password) {
|
|
44
|
+
const row = this.db
|
|
45
|
+
.prepare("select * from auth_users where username = ? collate nocase")
|
|
46
|
+
.get(username.trim());
|
|
47
|
+
if (!row || !row.active || !verifyPassword(password, row.password_hash)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return userFromRow(row);
|
|
51
|
+
}
|
|
52
|
+
createUser(input) {
|
|
53
|
+
const username = normalizeUsername(input.username);
|
|
54
|
+
validatePassword(input.password);
|
|
55
|
+
const now = new Date().toISOString();
|
|
56
|
+
const row = {
|
|
57
|
+
id: randomUUID(),
|
|
58
|
+
username,
|
|
59
|
+
password_hash: hashPassword(input.password),
|
|
60
|
+
role: input.role,
|
|
61
|
+
active: 1,
|
|
62
|
+
session_version: 1,
|
|
63
|
+
created_at: now,
|
|
64
|
+
updated_at: now,
|
|
65
|
+
};
|
|
66
|
+
this.db.prepare(`
|
|
67
|
+
insert into auth_users (
|
|
68
|
+
id, username, password_hash, role, active, session_version, created_at, updated_at
|
|
69
|
+
) values (
|
|
70
|
+
@id, @username, @password_hash, @role, @active, @session_version, @created_at, @updated_at
|
|
71
|
+
)
|
|
72
|
+
`).run(row);
|
|
73
|
+
return userFromRow(row);
|
|
74
|
+
}
|
|
75
|
+
updateUser(id, input) {
|
|
76
|
+
const current = this.db
|
|
77
|
+
.prepare("select * from auth_users where id = ?")
|
|
78
|
+
.get(id);
|
|
79
|
+
if (!current) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const username = input.username === undefined
|
|
83
|
+
? current.username
|
|
84
|
+
: normalizeUsername(input.username);
|
|
85
|
+
const passwordChanged = input.password !== undefined && input.password.length > 0;
|
|
86
|
+
if (passwordChanged) {
|
|
87
|
+
validatePassword(input.password);
|
|
88
|
+
}
|
|
89
|
+
const next = {
|
|
90
|
+
...current,
|
|
91
|
+
username,
|
|
92
|
+
password_hash: passwordChanged ? hashPassword(input.password) : current.password_hash,
|
|
93
|
+
role: input.role ?? current.role,
|
|
94
|
+
active: input.active === undefined ? current.active : input.active ? 1 : 0,
|
|
95
|
+
session_version: passwordChanged || input.active === false
|
|
96
|
+
? current.session_version + 1
|
|
97
|
+
: current.session_version,
|
|
98
|
+
updated_at: new Date().toISOString(),
|
|
99
|
+
};
|
|
100
|
+
this.db.prepare(`
|
|
101
|
+
update auth_users set
|
|
102
|
+
username = @username,
|
|
103
|
+
password_hash = @password_hash,
|
|
104
|
+
role = @role,
|
|
105
|
+
active = @active,
|
|
106
|
+
session_version = @session_version,
|
|
107
|
+
updated_at = @updated_at
|
|
108
|
+
where id = @id
|
|
109
|
+
`).run(next);
|
|
110
|
+
return userFromRow(next);
|
|
111
|
+
}
|
|
112
|
+
deleteUser(id) {
|
|
113
|
+
return this.db.prepare("delete from auth_users where id = ?").run(id).changes > 0;
|
|
114
|
+
}
|
|
115
|
+
countUsers() {
|
|
116
|
+
const row = this.db.prepare("select count(*) as count from auth_users").get();
|
|
117
|
+
return row.count;
|
|
118
|
+
}
|
|
119
|
+
initializeSchema() {
|
|
120
|
+
this.db.exec(`
|
|
121
|
+
create table if not exists auth_users (
|
|
122
|
+
id text primary key,
|
|
123
|
+
username text not null unique collate nocase,
|
|
124
|
+
password_hash text not null,
|
|
125
|
+
role text not null check (role in ('admin', 'user')),
|
|
126
|
+
active integer not null default 1,
|
|
127
|
+
session_version integer not null default 1,
|
|
128
|
+
created_at text not null,
|
|
129
|
+
updated_at text not null
|
|
130
|
+
);
|
|
131
|
+
create table if not exists auth_metadata (
|
|
132
|
+
key text primary key,
|
|
133
|
+
value text not null
|
|
134
|
+
);
|
|
135
|
+
`);
|
|
136
|
+
}
|
|
137
|
+
ensureSessionSecret() {
|
|
138
|
+
const existing = this.db
|
|
139
|
+
.prepare("select value from auth_metadata where key = 'session_secret'")
|
|
140
|
+
.get();
|
|
141
|
+
if (existing) {
|
|
142
|
+
return existing.value;
|
|
143
|
+
}
|
|
144
|
+
const value = randomBytes(32).toString("base64url");
|
|
145
|
+
this.db
|
|
146
|
+
.prepare("insert into auth_metadata (key, value) values ('session_secret', ?)")
|
|
147
|
+
.run(value);
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function userFromRow(row) {
|
|
152
|
+
return {
|
|
153
|
+
id: row.id,
|
|
154
|
+
username: row.username,
|
|
155
|
+
role: row.role,
|
|
156
|
+
active: row.active === 1,
|
|
157
|
+
sessionVersion: row.session_version,
|
|
158
|
+
createdAt: row.created_at,
|
|
159
|
+
updatedAt: row.updated_at,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function normalizeUsername(value) {
|
|
163
|
+
const username = value.trim();
|
|
164
|
+
if (!/^[A-Za-z0-9_.-]{2,64}$/.test(username)) {
|
|
165
|
+
throw new Error("invalid_username");
|
|
166
|
+
}
|
|
167
|
+
return username;
|
|
168
|
+
}
|
|
169
|
+
function validatePassword(value) {
|
|
170
|
+
if (value.length < 8 || value.length > 512) {
|
|
171
|
+
throw new Error("invalid_password");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function hashPassword(password) {
|
|
175
|
+
const salt = randomBytes(16);
|
|
176
|
+
const digest = scryptSync(password, salt, 64);
|
|
177
|
+
return `scrypt:${salt.toString("base64url")}:${digest.toString("base64url")}`;
|
|
178
|
+
}
|
|
179
|
+
function verifyPassword(password, stored) {
|
|
180
|
+
const [algorithm, encodedSalt, encodedDigest] = stored.split(":");
|
|
181
|
+
if (algorithm !== "scrypt" || !encodedSalt || !encodedDigest) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const expected = Buffer.from(encodedDigest, "base64url");
|
|
186
|
+
const actual = scryptSync(password, Buffer.from(encodedSalt, "base64url"), expected.length);
|
|
187
|
+
return expected.length === actual.length && timingSafeEqual(expected, actual);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|