@delt/claude-alarm 0.9.0 → 0.9.1
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/README.md +29 -4
- package/dist/cli.js +156 -37
- package/dist/cli.js.map +1 -1
- package/dist/dashboard/index.html +26 -33
- package/dist/hub/server.js +137 -32
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +137 -32
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/dashboard/index.html +26 -33
package/dist/index.js
CHANGED
|
@@ -521,6 +521,7 @@ ${previewHtml}${truncNote}`;
|
|
|
521
521
|
}
|
|
522
522
|
async handleCallbackQuery(query) {
|
|
523
523
|
if (!query.data) return;
|
|
524
|
+
if (String(query.message?.chat.id) !== String(this.config.chatId)) return;
|
|
524
525
|
if (query.data.startsWith("sess:")) {
|
|
525
526
|
await this.handleSessionSelectCallback(query);
|
|
526
527
|
return;
|
|
@@ -784,8 +785,66 @@ function logStartup(context) {
|
|
|
784
785
|
`);
|
|
785
786
|
}
|
|
786
787
|
|
|
788
|
+
// src/hub/auth.ts
|
|
789
|
+
import crypto from "crypto";
|
|
790
|
+
var SESSION_COOKIE = "ca_session";
|
|
791
|
+
var COOKIE_MAX_AGE = 30 * 24 * 60 * 60;
|
|
792
|
+
function safeEqual(a, b) {
|
|
793
|
+
if (typeof a !== "string") return false;
|
|
794
|
+
const x = Buffer.from(a);
|
|
795
|
+
const y = Buffer.from(b);
|
|
796
|
+
return x.length === y.length && crypto.timingSafeEqual(x, y);
|
|
797
|
+
}
|
|
798
|
+
function dashboardCookieValue(token) {
|
|
799
|
+
return crypto.createHmac("sha256", token).update("claude-alarm-dashboard").digest("hex");
|
|
800
|
+
}
|
|
801
|
+
function parseCookies(header) {
|
|
802
|
+
const out = {};
|
|
803
|
+
for (const part of (header ?? "").split(";")) {
|
|
804
|
+
const i = part.indexOf("=");
|
|
805
|
+
if (i <= 0) continue;
|
|
806
|
+
const raw = part.slice(i + 1).trim();
|
|
807
|
+
let value = raw;
|
|
808
|
+
try {
|
|
809
|
+
value = decodeURIComponent(raw);
|
|
810
|
+
} catch {
|
|
811
|
+
}
|
|
812
|
+
out[part.slice(0, i).trim()] = value;
|
|
813
|
+
}
|
|
814
|
+
return out;
|
|
815
|
+
}
|
|
816
|
+
function sessionCookieHeader(token, secure) {
|
|
817
|
+
return `${SESSION_COOKIE}=${dashboardCookieValue(token)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${COOKIE_MAX_AGE}${secure ? "; Secure" : ""}`;
|
|
818
|
+
}
|
|
819
|
+
function isAuthorized(req, token, allowQueryToken) {
|
|
820
|
+
const auth = req.headers.authorization;
|
|
821
|
+
if (auth?.startsWith("Bearer ") && safeEqual(auth.slice(7), token)) return true;
|
|
822
|
+
if (safeEqual(parseCookies(req.headers.cookie)[SESSION_COOKIE], dashboardCookieValue(token))) return true;
|
|
823
|
+
if (allowQueryToken) {
|
|
824
|
+
const q = new URL(req.url ?? "/", "http://hub").searchParams.get("token");
|
|
825
|
+
if (safeEqual(q, token)) return true;
|
|
826
|
+
}
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
function isCrossOrigin(req) {
|
|
830
|
+
const origin = req.headers.origin;
|
|
831
|
+
if (!origin) return false;
|
|
832
|
+
try {
|
|
833
|
+
return new URL(origin).host !== req.headers.host;
|
|
834
|
+
} catch {
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
function isJsonRequest(req) {
|
|
839
|
+
return String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json");
|
|
840
|
+
}
|
|
841
|
+
function isSecureRequest(req) {
|
|
842
|
+
return req.headers["x-forwarded-proto"] === "https";
|
|
843
|
+
}
|
|
844
|
+
|
|
787
845
|
// src/hub/server.ts
|
|
788
846
|
var __dirname = path4.dirname(fileURLToPath(import.meta.url));
|
|
847
|
+
var MAX_WS_PAYLOAD = 16 * 1024 * 1024;
|
|
789
848
|
var HubServer = class {
|
|
790
849
|
httpServer;
|
|
791
850
|
wssChannel;
|
|
@@ -803,6 +862,7 @@ var HubServer = class {
|
|
|
803
862
|
heartbeatInterval;
|
|
804
863
|
channelAlive = /* @__PURE__ */ new Map();
|
|
805
864
|
// sessionId -> alive flag
|
|
865
|
+
socketOwners = /* @__PURE__ */ new WeakMap();
|
|
806
866
|
host;
|
|
807
867
|
port;
|
|
808
868
|
token;
|
|
@@ -825,22 +885,23 @@ var HubServer = class {
|
|
|
825
885
|
this.initTelegram(fullConfig.telegram);
|
|
826
886
|
}
|
|
827
887
|
this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
|
|
828
|
-
this.wssChannel = new WebSocketServer({ noServer: true });
|
|
888
|
+
this.wssChannel = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
829
889
|
this.wssChannel.on("connection", (ws, req) => this.handleChannelConnection(ws, req));
|
|
830
890
|
this.wssChannel.on("error", (err) => logger.warn(`Channel WebSocket server error: ${err.message}`));
|
|
831
|
-
this.wssDashboard = new WebSocketServer({ noServer: true });
|
|
891
|
+
this.wssDashboard = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD });
|
|
832
892
|
this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
|
|
833
893
|
this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
|
|
834
894
|
this.httpServer.on("upgrade", (req, socket, head) => {
|
|
835
895
|
socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
|
|
836
|
-
const url = new URL(req.url,
|
|
896
|
+
const url = new URL(req.url ?? "/", "http://hub");
|
|
837
897
|
const pathname = url.pathname;
|
|
838
|
-
if (
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
898
|
+
if (isCrossOrigin(req)) {
|
|
899
|
+
this.rejectUpgrade(socket, 403, "Forbidden", pathname, req);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (!this.authorized(req, true)) {
|
|
903
|
+
this.rejectUpgrade(socket, 401, "Unauthorized", pathname, req);
|
|
904
|
+
return;
|
|
844
905
|
}
|
|
845
906
|
if (pathname === WS_PATH_CHANNEL) {
|
|
846
907
|
this.wssChannel.handleUpgrade(req, socket, head, (ws) => {
|
|
@@ -892,31 +953,28 @@ var HubServer = class {
|
|
|
892
953
|
}
|
|
893
954
|
// --- HTTP Handler ---
|
|
894
955
|
handleHttp(req, res) {
|
|
895
|
-
const url = new URL(req.url,
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
899
|
-
}
|
|
900
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
901
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
902
|
-
if (req.method === "OPTIONS") {
|
|
903
|
-
res.writeHead(204);
|
|
904
|
-
res.end();
|
|
956
|
+
const url = new URL(req.url ?? "/", "http://hub");
|
|
957
|
+
if (isCrossOrigin(req)) {
|
|
958
|
+
this.jsonResponse(res, 403, { error: "Cross-origin request rejected" });
|
|
905
959
|
return;
|
|
906
960
|
}
|
|
907
|
-
if (
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
911
|
-
if (bearerToken !== this.token) {
|
|
912
|
-
this.jsonResponse(res, 401, { error: "Unauthorized" });
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
961
|
+
if (req.method === "POST" && !isJsonRequest(req)) {
|
|
962
|
+
this.jsonResponse(res, 415, { error: "Content-Type must be application/json" });
|
|
963
|
+
return;
|
|
916
964
|
}
|
|
917
965
|
if (url.pathname === "/" && req.method === "GET") {
|
|
918
|
-
this.serveDashboard(res);
|
|
919
|
-
|
|
966
|
+
this.serveDashboard(req, res, url);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
if (url.pathname === "/api/login" && req.method === "POST") {
|
|
970
|
+
this.handleLogin(req, res);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
if (!this.authorized(req, false)) {
|
|
974
|
+
this.jsonResponse(res, 401, { error: "Unauthorized" });
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
920
978
|
this.jsonResponse(res, 200, { sessions: this.sessions.getAll() });
|
|
921
979
|
} else if (url.pathname === "/api/status" && req.method === "GET") {
|
|
922
980
|
this.jsonResponse(res, 200, {
|
|
@@ -967,7 +1025,13 @@ var HubServer = class {
|
|
|
967
1025
|
this.jsonResponse(res, 404, { error: "Not found" });
|
|
968
1026
|
}
|
|
969
1027
|
}
|
|
970
|
-
serveDashboard(res) {
|
|
1028
|
+
serveDashboard(req, res, url) {
|
|
1029
|
+
const linkToken = url.searchParams.get("token");
|
|
1030
|
+
if (linkToken !== null && this.token && safeEqual(linkToken, this.token)) {
|
|
1031
|
+
res.writeHead(302, { Location: "/", "Set-Cookie": sessionCookieHeader(this.token, isSecureRequest(req)) });
|
|
1032
|
+
res.end();
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
971
1035
|
const candidates = [
|
|
972
1036
|
path4.join(__dirname, "..", "dashboard", "index.html"),
|
|
973
1037
|
// from dist/hub/
|
|
@@ -994,6 +1058,20 @@ var HubServer = class {
|
|
|
994
1058
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
995
1059
|
res.end("<html><body><h1>claude-alarm</h1><p>Dashboard HTML not found. Reinstall the package.</p></body></html>");
|
|
996
1060
|
}
|
|
1061
|
+
async handleLogin(req, res) {
|
|
1062
|
+
const body = await this.readBody(req);
|
|
1063
|
+
if (!this.token) {
|
|
1064
|
+
res.writeHead(204);
|
|
1065
|
+
res.end();
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (typeof body?.token !== "string" || !safeEqual(body.token, this.token)) {
|
|
1069
|
+
this.jsonResponse(res, 401, { error: "Unauthorized" });
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
res.writeHead(204, { "Set-Cookie": sessionCookieHeader(this.token, isSecureRequest(req)) });
|
|
1073
|
+
res.end();
|
|
1074
|
+
}
|
|
997
1075
|
async handleApiSend(req, res) {
|
|
998
1076
|
const body = await this.readBody(req);
|
|
999
1077
|
if (!body) {
|
|
@@ -1070,6 +1148,22 @@ var HubServer = class {
|
|
|
1070
1148
|
});
|
|
1071
1149
|
}
|
|
1072
1150
|
handleChannelMessage(ws, isLocal, msg) {
|
|
1151
|
+
if (msg.type === "register") {
|
|
1152
|
+
const id = msg.session.id;
|
|
1153
|
+
const owned = this.socketOwners.get(ws);
|
|
1154
|
+
if (owned && owned !== id) {
|
|
1155
|
+
logger.warn(`Rejected register for ${id}: connection already owns ${owned}`);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const holder = this.channelSockets.get(id);
|
|
1159
|
+
if (holder && holder !== ws) {
|
|
1160
|
+
this.socketOwners.delete(holder);
|
|
1161
|
+
holder.terminate();
|
|
1162
|
+
}
|
|
1163
|
+
this.socketOwners.set(ws, id);
|
|
1164
|
+
} else if ("sessionId" in msg && this.socketOwners.get(ws) !== msg.sessionId) {
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1073
1167
|
switch (msg.type) {
|
|
1074
1168
|
case "register": {
|
|
1075
1169
|
const session = msg.session;
|
|
@@ -1093,7 +1187,6 @@ var HubServer = class {
|
|
|
1093
1187
|
break;
|
|
1094
1188
|
}
|
|
1095
1189
|
case "peer_name": {
|
|
1096
|
-
if (this.channelSockets.get(msg.sessionId) !== ws) break;
|
|
1097
1190
|
const updated = this.sessions.setPeerName(msg.sessionId, msg.peerName);
|
|
1098
1191
|
if (updated) {
|
|
1099
1192
|
logger.info(`Peer name for ${msg.sessionId}: ${msg.peerName ?? "-"}`);
|
|
@@ -1421,6 +1514,18 @@ var HubServer = class {
|
|
|
1421
1514
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1422
1515
|
res.end(JSON.stringify(body));
|
|
1423
1516
|
}
|
|
1517
|
+
authorized(req, allowQueryToken) {
|
|
1518
|
+
return !this.token || isAuthorized(req, this.token, allowQueryToken);
|
|
1519
|
+
}
|
|
1520
|
+
rejectUpgrade(socket, status, text, pathname, req) {
|
|
1521
|
+
logger.warn(`Rejected ${pathname} upgrade (${status}) from ${req.socket.remoteAddress}`);
|
|
1522
|
+
socket.write(`HTTP/1.1 ${status} ${text}\r
|
|
1523
|
+
Connection: close\r
|
|
1524
|
+
Content-Length: 0\r
|
|
1525
|
+
\r
|
|
1526
|
+
`);
|
|
1527
|
+
socket.destroy();
|
|
1528
|
+
}
|
|
1424
1529
|
isLocalRequest(req) {
|
|
1425
1530
|
const addr = req.socket.remoteAddress;
|
|
1426
1531
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|