srift-transfer 4.0.0 → 4.1.0
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 +33 -20
- package/dist/daemon.js +349 -158
- package/dist/index.js +650 -426
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -1184,12 +1184,13 @@ function withQuery(url, q) {
|
|
|
1184
1184
|
return `${url}${url.includes("?") ? "&" : "?"}${q}`;
|
|
1185
1185
|
}
|
|
1186
1186
|
function explainStatus(status, body) {
|
|
1187
|
-
|
|
1187
|
+
let msg = body.replace(/^SRIFT:\s*/, "").trim();
|
|
1188
|
+
if (/^\s*<(!doctype|html)/i.test(msg)) msg = status >= 500 ? "The SRIFT server or a proxy in front of it is temporarily unavailable \u2014 retry shortly" : "";
|
|
1189
|
+
if (status === 424 || status === 502 && /sender error: (the shared file|cannot read the shared file)/i.test(msg)) return new GetError(msg || "The sender can no longer serve this file.", "ESENDERFILE", status);
|
|
1188
1190
|
if (status === 404) return new GetError(msg || "Link not found \u2014 it expired, was revoked, or the sender went offline.", "ENOTFOUND_LINK", 404);
|
|
1189
1191
|
if (status === 410) return new GetError(msg || "Link expired or reached its download limit.", "EGONE", 410);
|
|
1190
1192
|
if (status === 503) return new GetError(msg || "Sender is offline \u2014 their daemon must be running for relay links.", "EOFFLINE", 503);
|
|
1191
1193
|
if (status === 409) return new GetError(msg || "The sender is still uploading \u2014 retry shortly.", "ENOTREADY", 409);
|
|
1192
|
-
if (status === 502 && /sender error: (the shared file|cannot read the shared file)/i.test(msg)) return new GetError(msg, "ESENDERFILE", 502);
|
|
1193
1194
|
return new GetError(`${msg || "Download failed"} (HTTP ${status})`, "EHTTP", status);
|
|
1194
1195
|
}
|
|
1195
1196
|
async function readSmallBody(res, max = 4096) {
|
|
@@ -1376,7 +1377,7 @@ async function getLink(input, opts = {}) {
|
|
|
1376
1377
|
}
|
|
1377
1378
|
const serverName = filenameFromDisposition(head["content-disposition"]) || "download.bin";
|
|
1378
1379
|
const mode = head["x-srift-mode"] ? String(head["x-srift-mode"]) : null;
|
|
1379
|
-
const total = parseInt(String(head["content-length"] || ""), 10) || 0;
|
|
1380
|
+
const total = parseInt(String(head["x-srift-size"] || head["content-length"] || ""), 10) || 0;
|
|
1380
1381
|
let received = 0;
|
|
1381
1382
|
let lastPrint = 0;
|
|
1382
1383
|
const progress = (n) => {
|
|
@@ -1699,10 +1700,10 @@ var init_skills_data = __esm({
|
|
|
1699
1700
|
"../../lib/mcp/skills-data.mjs"() {
|
|
1700
1701
|
"use strict";
|
|
1701
1702
|
SOURCES = [
|
|
1702
|
-
{ name: "encrypted-chat", content: "---\nname: encrypted-chat\ndescription: Send and read end-to-end encrypted chat messages and manage in-session file transfers within an active SRIFT session\n---\n\n# encrypted-chat\n\nSend and read end-to-end encrypted chat messages within an active SRIFT session, plus manage in-session file transfers, via the local MCP daemon.\n\n## Requirements\n\nAn active SRIFT session (see the `session-management` skill to open or join one first). No special credentials required.\n\n## Instructions\n\nAvailable MCP tools (see `/.well-known/mcp/server-card.json` for full schemas):\n\n- `srift_send_chat` \u2014 send an E2EE chat message to peers in the current session.\n- `srift_chat_history` \u2014 read decrypted chat history for the current session.\n- `srift_send_file` \u2014 offer a file to peers already inside the session (in-session transfer, distinct from `quick-share`'s standalone link flow).\n- `srift_accept_transfer` \u2014 accept and download an inbound file offer.\n- `srift_list_transfers` \u2014 list all active and recent transfers with progress.\n- `srift_read_state` \u2014 read the raw `.srift-state.json` snapshot for debugging/observability.\n\
|
|
1703
|
+
{ name: "encrypted-chat", content: "---\nname: encrypted-chat\ndescription: Send and read end-to-end encrypted chat messages and manage in-session file transfers within an active SRIFT session\n---\n\n# encrypted-chat\n\nSend and read end-to-end encrypted chat messages within an active SRIFT session, plus manage in-session file transfers, via the local MCP daemon.\n\n## Requirements\n\nAn active SRIFT session (see the `session-management` skill to open or join one first). No special credentials required.\n\n## Instructions\n\nAvailable MCP tools (see `/.well-known/mcp/server-card.json` for full schemas):\n\n- `srift_send_chat` \u2014 send an E2EE chat message to peers in the current session.\n- `srift_chat_history` \u2014 read decrypted chat history for the current session.\n- `srift_send_file` \u2014 offer a file to peers already inside the session (in-session transfer, distinct from `quick-share`'s standalone link flow).\n- `srift_accept_transfer` \u2014 accept and download an inbound file offer.\n- `srift_list_transfers` \u2014 list all active and recent transfers with progress.\n- `srift_read_state` \u2014 read the raw `.srift-state.json` snapshot for debugging/observability.\n\nYour local daemon (or the browser) encrypts messages and file chunks with AES-256-GCM before they leave the device, using keys derived from the session (plus the optional `roomSecret`); the relay server only forwards ciphertext and never receives the key. Without a `roomSecret` the key is derivable from the session ID (which the server sees); set one on both sides to make it participant-only.\n" },
|
|
1703
1704
|
{ name: "get-company-info", content: "---\nname: get-company-info\ndescription: Retrieve overview and core metadata for SRIFT and SRIPTO Corporation\n---\n\n# get-company-info\n\nRetrieve high-level overview and specifications about SRIFT and SRIPTO Corporation.\n\n## Requirements\n\nNo special credentials required.\n\n## Instructions\n\nRead the home page or `llms.txt` to find details about Srift (secure P2P transport), LetMeTeach, SRIX, and A\u039EI.\n" },
|
|
1704
1705
|
{ name: "quick-share", content: "---\nname: quick-share\ndescription: Zero-install file delivery: seed a file and get a public download URL, no session setup required on the recipient's end\n---\n\n# quick-share\n\nZero-install file delivery via SRIFT's local MCP server. Returns a public `https://srift.app/d/<token>` relay link that any recipient can open in a browser \u2014 no SRIFT install, no signup, no session-join UI required on their end. The bytes stream from the sender's machine through the srift.app relay on demand (pass-through, `Cache-Control: no-store`); nothing is stored on any server, so the link works while the sender's daemon (or process) runs.\n\n## Requirements\n\nNo credentials. The SRIFT local daemon binds to `127.0.0.1:3822` with zero authentication. Only outbound HTTPS/WSS on port 443 is needed.\n\n## Instructions\n\n1. Call the MCP tool `srift_quick_share` with `{ filePath: \"/absolute/path/to/file\" }` (single file) or `{ filePaths: [...] }` (up to 500 files/folders). Optional params: `bundle` (default: true = one .tar.gz link; false = one link per path, created in parallel), `bundleName`, `exclude` (string[] for glob patterns to skip), `sessionName`, `maxDownloads`, `ttlMs`, `encrypt`, `password`.\n2. The tool returns a `downloadUrl` (e.g. `https://srift.app/d/<token>`) when bundled, or `{ links: [], errors: [] }` when separate. With `encrypt: true` (or a `password`) each link is end-to-end encrypted with AES-256-GCM: the key rides in the `#k=` URL fragment, is never sent to a server, and the browser decrypts locally. Without it, links are protected by TLS in transit only \u2014 use `encrypt` for anything sensitive.\n3. Hand the URL(s) to the recipient. In order of preference, they can run:\n - `srift get <url> [<url> ...]` (or `npx -y srift-transfer get <url>`; proxy-aware, resumable, decrypts `#k=` links, supports multiple URLs with `--concurrency N`, works where curl is broken)\n - `wget --content-disposition <url>`\n - PowerShell `iwr <url> -OutFile <name>`\n - `curl -OJ <url>`\n - or just open it in any browser.\n4. Keep the sending side running until the recipient has downloaded. From a shell, `srift quick-share <file> [<file> ...] --wait` blocks until then (`--wait-timeout 30m` exits with code 3 on timeout); `--keep-alive` serves until the TTL expires.\n\n## Sandboxed and datacenter agents\n\nIf the background daemon cannot start (the sandbox forbids local servers, blocks detached spawns or loopback, or the port is taken), `srift mcp` and `srift quick-share` host the daemon inside their own process: no local port, outbound HTTPS/WSS on 443 only. The MCP server switches automatically, so this tool keeps working. From a shell, run quick-share in the background (it exits after the download) or with `--wait`; `--foreground` forces in-process serving. Blocked UDP never breaks links. Run `srift doctor` (or the `srift_net_diagnose` tool) to diagnose connectivity; `HTTPS_PROXY`/`NO_PROXY` and `NODE_EXTRA_CA_CERTS` are honoured.\n\nSee `/openapi.json` and `/.well-known/mcp/server-card.json` for the full tool contract.\n" },
|
|
1705
|
-
{ name: "session-management", content: '---\nname: session-management\ndescription: Create, join, monitor, and control SRIFT end-to-end encrypted P2P sessions via the local MCP daemon\n---\n\n# session-management\n\nCreate, join, monitor, and control SRIFT end-to-end encrypted P2P sessions ("rooms") via the local MCP daemon. Covers the full session lifecycle used for in-session file transfer and encrypted chat.\n\n## Requirements\n\nNo special credentials required. The SRIFT local daemon binds to `127.0.0.1:3822` and requires zero authentication.\n\n## Instructions\n\nAvailable MCP tools (see `/.well-known/mcp/server-card.json` for full schemas):\n\n- `srift_start_session` \u2014 open a new E2EE room as host.\n- `srift_join_session` \u2014 join an existing room by its 7-character session ID.\n- `srift_session_status` \u2014 read current session, role, connected peers, and pending join requests.\n- `srift_approve_join` / `srift_reject_join` \u2014 host-only: approve or reject a guest\'s join request.\n- `srift_kick_user` \u2014 host-only:
|
|
1706
|
+
{ name: "session-management", content: '---\nname: session-management\ndescription: Create, join, monitor, and control SRIFT end-to-end encrypted P2P sessions via the local MCP daemon\n---\n\n# session-management\n\nCreate, join, monitor, and control SRIFT end-to-end encrypted P2P sessions ("rooms") via the local MCP daemon. Covers the full session lifecycle used for in-session file transfer and encrypted chat.\n\n## Requirements\n\nNo special credentials required. The SRIFT local daemon binds to `127.0.0.1:3822` and requires zero authentication.\n\n## Instructions\n\nAvailable MCP tools (see `/.well-known/mcp/server-card.json` for full schemas):\n\n- `srift_start_session` \u2014 open a new E2EE room as host.\n- `srift_join_session` \u2014 join an existing room by its 7-character session ID.\n- `srift_session_status` \u2014 read current session, role, connected peers, and pending join requests.\n- `srift_approve_join` / `srift_reject_join` \u2014 host-only: approve or reject a guest\'s join request.\n- `srift_kick_user` \u2014 host-only: remove a peer. Get their `userId` from `srift_session_status` \u2192 `participants`.\n- `srift_close_session` \u2014 tear down the session and flush all encryption keys.\n\nSessions are ephemeral: session metadata and chat ciphertext are kept server-side only until the session is deleted (host close or 7 days of inactivity), and files are never stored. Keys are derived locally via PBKDF2-SHA256 from the session ID (plus the optional `roomSecret`) and are never sent to the server; without a `roomSecret` the key is derivable from the session ID, which the server sees, so pass one to `srift_start_session` / `srift_join_session` to make it participant-only.\n' }
|
|
1706
1707
|
];
|
|
1707
1708
|
MCP_SKILLS = SOURCES.map(({ name, content }) => {
|
|
1708
1709
|
const uri = `skill://${name}/SKILL.md`;
|
|
@@ -1961,7 +1962,7 @@ var init_core = __esm({
|
|
|
1961
1962
|
type: "object",
|
|
1962
1963
|
properties: {
|
|
1963
1964
|
sessionName: { type: "string", description: 'Human-readable session name (optional, e.g. "Project Collab")' },
|
|
1964
|
-
roomSecret: { type: "string", description: "Optional shared secret
|
|
1965
|
+
roomSecret: { type: "string", description: "Optional shared secret mixed into key derivation. Never sent to server. Without it the key is derivable from the session ID (which the server sees); set it to make the key participant-only." }
|
|
1965
1966
|
}
|
|
1966
1967
|
},
|
|
1967
1968
|
outputSchema: {
|
|
@@ -2012,15 +2013,37 @@ var init_core = __esm({
|
|
|
2012
2013
|
{
|
|
2013
2014
|
name: "srift_session_status",
|
|
2014
2015
|
title: "Get Session Status",
|
|
2015
|
-
description: "Get current session details: id, role (host/guest), connection state, pending join requests,
|
|
2016
|
+
description: "Get current session details: session { id, role (host/guest), connection state, your userId }, pending join requests (approve with srift_approve_join), and members with their userIds (remove one with srift_kick_user).",
|
|
2016
2017
|
inputSchema: { type: "object", properties: {} },
|
|
2017
2018
|
outputSchema: {
|
|
2018
2019
|
type: "object",
|
|
2019
2020
|
properties: {
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2021
|
+
session: {
|
|
2022
|
+
type: "object",
|
|
2023
|
+
description: "The active session (id is null when there is none)",
|
|
2024
|
+
properties: {
|
|
2025
|
+
id: { type: ["string", "null"], description: "Active session ID" },
|
|
2026
|
+
name: { type: ["string", "null"], description: "Session name" },
|
|
2027
|
+
role: { type: ["string", "null"], description: '"host", "guest", or null' },
|
|
2028
|
+
isConnected: { type: "boolean", description: "True if connected to the signaling server" },
|
|
2029
|
+
userId: { type: ["string", "null"], description: "Your user id in the session" },
|
|
2030
|
+
status: { type: "string", description: 'Guest only: "pending_approval" or "approved" (hosted endpoint)' }
|
|
2031
|
+
}
|
|
2032
|
+
},
|
|
2033
|
+
peerCount: { type: "number", description: "Number of connected sockets in the room (hosted endpoint)" },
|
|
2034
|
+
participants: {
|
|
2035
|
+
type: "array",
|
|
2036
|
+
description: "Session members (local daemon): pass userId to srift_kick_user to remove one",
|
|
2037
|
+
items: {
|
|
2038
|
+
type: "object",
|
|
2039
|
+
properties: {
|
|
2040
|
+
userId: { type: "string" },
|
|
2041
|
+
username: { type: "string" },
|
|
2042
|
+
isHost: { type: "boolean" },
|
|
2043
|
+
online: { type: "boolean" }
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
},
|
|
2024
2047
|
pendingJoins: {
|
|
2025
2048
|
type: "array",
|
|
2026
2049
|
description: "List of pending guest join requests waiting for host approval",
|
|
@@ -2114,7 +2137,7 @@ var init_core = __esm({
|
|
|
2114
2137
|
{
|
|
2115
2138
|
name: "srift_kick_user",
|
|
2116
2139
|
title: "Kick Peer",
|
|
2117
|
-
description: "Host-only. Disconnect and
|
|
2140
|
+
description: "Host-only. Disconnect and remove a peer from the active room. Get the peer's userId from srift_session_status (participants).",
|
|
2118
2141
|
inputSchema: {
|
|
2119
2142
|
type: "object",
|
|
2120
2143
|
properties: {
|
|
@@ -2382,6 +2405,8 @@ var init_core = __esm({
|
|
|
2382
2405
|
type: "object",
|
|
2383
2406
|
properties: {
|
|
2384
2407
|
ok: { type: "boolean", description: "True if the diagnostic ran successfully (independent of what it found)" },
|
|
2408
|
+
base: { type: "string", description: "The SRIFT server that was probed" },
|
|
2409
|
+
checkedAt: { type: "string", description: "When the probe ran (ISO 8601); cached results show an older time" },
|
|
2385
2410
|
verdict: { type: "string", description: '"ok", "degraded", or "blocked"' },
|
|
2386
2411
|
rungs: {
|
|
2387
2412
|
type: "array",
|
|
@@ -2527,8 +2552,13 @@ session-management, and get-company-info, \`skills/get\` to fetch one by URI, an
|
|
|
2527
2552
|
## Security model
|
|
2528
2553
|
- AES-256-GCM, 12-byte random IV per message.
|
|
2529
2554
|
- PBKDF2-SHA256, 100,000 iterations.
|
|
2530
|
-
- All keys derive locally from sessionId (+ optional roomSecret).
|
|
2531
|
-
|
|
2555
|
+
- All keys derive locally from sessionId (+ optional roomSecret). The key is never sent to the
|
|
2556
|
+
server, but the server sees the sessionId: without a roomSecret the key is derivable from it.
|
|
2557
|
+
Pass roomSecret to srift_start_session / srift_join_session to make the key participant-only.
|
|
2558
|
+
- Quick-share links are end-to-end encrypted only with encrypt: true (key in the #k= fragment);
|
|
2559
|
+
otherwise TLS in transit only, zero retention.
|
|
2560
|
+
- The CLI daemon moves file chunks over the AES-256-GCM WebSocket relay (browsers use WebRTC for
|
|
2561
|
+
files >5 MB, with the relay as fallback).
|
|
2532
2562
|
|
|
2533
2563
|
## Local daemon vs hosted MCP
|
|
2534
2564
|
If you are talking to the hosted endpoint (https://srift.app/mcp) instead of a local
|
|
@@ -2545,6 +2575,8 @@ for full file-transfer + chat capability.
|
|
|
2545
2575
|
|
|
2546
2576
|
// ../../cli/mcp.ts
|
|
2547
2577
|
import http3 from "http";
|
|
2578
|
+
import fs6 from "fs";
|
|
2579
|
+
import path5 from "path";
|
|
2548
2580
|
function callDaemon(endpoint, method, body) {
|
|
2549
2581
|
const emb = embeddedCall(method, endpoint, body);
|
|
2550
2582
|
if (emb) return emb.then((r) => {
|
|
@@ -2595,43 +2627,60 @@ async function callTool(name, args) {
|
|
|
2595
2627
|
sessionName: args.sessionName,
|
|
2596
2628
|
roomSecret: args.roomSecret
|
|
2597
2629
|
});
|
|
2598
|
-
|
|
2630
|
+
const joinUrl = res.joinUrl || `https://srift.app/join-session?id=${res.sessionId}`;
|
|
2631
|
+
return {
|
|
2632
|
+
text: `Session created.
|
|
2599
2633
|
Session ID: ${res.sessionId}
|
|
2600
|
-
Share URL:
|
|
2601
|
-
You are the HOST. Approve incoming joins with srift_approve_join
|
|
2634
|
+
Share URL: ${joinUrl}
|
|
2635
|
+
You are the HOST. Approve incoming joins with srift_approve_join.`,
|
|
2636
|
+
structured: { sessionId: res.sessionId, url: joinUrl, role: "host" }
|
|
2637
|
+
};
|
|
2602
2638
|
}
|
|
2603
2639
|
case "srift_join_session": {
|
|
2604
2640
|
const res = await callDaemon("/session/join", "POST", args);
|
|
2605
|
-
return
|
|
2641
|
+
return {
|
|
2642
|
+
text: `Join requested for session ${args.sessionId}. Status: ${res.success ? "awaiting host approval" : "failed"}.`,
|
|
2643
|
+
structured: { sessionId: args.sessionId, status: res.success ? "awaiting_approval" : "failed" }
|
|
2644
|
+
};
|
|
2606
2645
|
}
|
|
2607
2646
|
case "srift_session_status": {
|
|
2608
2647
|
const res = await callDaemon("/status", "GET");
|
|
2609
|
-
return JSON.stringify({ session: res.session, pendingJoins: res.pendingJoins }, null, 2);
|
|
2648
|
+
return JSON.stringify({ session: res.session, pendingJoins: res.pendingJoins, participants: res.participants || [] }, null, 2);
|
|
2610
2649
|
}
|
|
2611
2650
|
case "srift_close_session":
|
|
2612
2651
|
await callDaemon("/session/close", "POST");
|
|
2613
|
-
return "Session closed. Keys flushed.";
|
|
2652
|
+
return { text: "Session closed. Keys flushed.", structured: { success: true } };
|
|
2614
2653
|
case "srift_approve_join":
|
|
2615
2654
|
await callDaemon("/session/approve", "POST", args);
|
|
2616
|
-
return `Approved ${args.tempUserId}
|
|
2655
|
+
return { text: `Approved ${args.tempUserId}.`, structured: { success: true, tempUserId: args.tempUserId } };
|
|
2617
2656
|
case "srift_reject_join":
|
|
2618
2657
|
await callDaemon("/session/reject", "POST", args);
|
|
2619
|
-
return `Rejected ${args.tempUserId}
|
|
2658
|
+
return { text: `Rejected ${args.tempUserId}.`, structured: { success: true, tempUserId: args.tempUserId } };
|
|
2620
2659
|
case "srift_kick_user":
|
|
2621
2660
|
await callDaemon("/session/kick", "POST", args);
|
|
2622
|
-
return `Kicked ${args.userId}
|
|
2661
|
+
return { text: `Kicked ${args.userId}.`, structured: { success: true, userId: args.userId } };
|
|
2623
2662
|
case "srift_send_file": {
|
|
2624
2663
|
const res = await callDaemon("/send", "POST", args);
|
|
2625
|
-
return
|
|
2664
|
+
return {
|
|
2665
|
+
text: `File offered.
|
|
2626
2666
|
File ID: ${res.fileId}
|
|
2627
|
-
The peer must call srift_accept_transfer with this fileId
|
|
2667
|
+
The peer must call srift_accept_transfer with this fileId.`,
|
|
2668
|
+
structured: { fileId: res.fileId, fileName: path5.basename(String(args.filePath || "")), fileSize: (() => {
|
|
2669
|
+
try {
|
|
2670
|
+
return fs6.statSync(args.filePath).size;
|
|
2671
|
+
} catch {
|
|
2672
|
+
return void 0;
|
|
2673
|
+
}
|
|
2674
|
+
})() }
|
|
2675
|
+
};
|
|
2628
2676
|
}
|
|
2629
2677
|
case "srift_accept_transfer":
|
|
2630
2678
|
await callDaemon("/receive", "POST", args);
|
|
2631
|
-
return `Transfer ${args.fileId} accepted. Downloading\u2026
|
|
2679
|
+
return { text: `Transfer ${args.fileId} accepted. Downloading\u2026`, structured: { success: true, fileId: args.fileId } };
|
|
2632
2680
|
case "srift_list_transfers": {
|
|
2633
2681
|
const res = await callDaemon("/status", "GET");
|
|
2634
|
-
|
|
2682
|
+
const transfers = res.activeTransfers || [];
|
|
2683
|
+
return { text: JSON.stringify(transfers, null, 2), structured: { transfers } };
|
|
2635
2684
|
}
|
|
2636
2685
|
case "srift_quick_share": {
|
|
2637
2686
|
const multi = Array.isArray(args?.filePaths) && args.filePaths.length > 0;
|
|
@@ -2725,10 +2774,11 @@ The peer must call srift_accept_transfer with this fileId.`;
|
|
|
2725
2774
|
}
|
|
2726
2775
|
case "srift_send_chat":
|
|
2727
2776
|
await callDaemon("/chat/send", "POST", args);
|
|
2728
|
-
return `Sent (E2EE): "${args.message}"
|
|
2777
|
+
return { text: `Sent (E2EE): "${args.message}"`, structured: { success: true, timestamp: (/* @__PURE__ */ new Date()).toISOString() } };
|
|
2729
2778
|
case "srift_chat_history": {
|
|
2730
2779
|
const res = await callDaemon("/chat/history", "GET");
|
|
2731
|
-
|
|
2780
|
+
const messages = Array.isArray(res) ? res : res?.messages || [];
|
|
2781
|
+
return { text: JSON.stringify(res, null, 2), structured: { messages } };
|
|
2732
2782
|
}
|
|
2733
2783
|
case "srift_read_state": {
|
|
2734
2784
|
const res = await callDaemon("/state", "GET");
|
|
@@ -2774,7 +2824,7 @@ var init_mcp = __esm({
|
|
|
2774
2824
|
SERVER_INFO = {
|
|
2775
2825
|
name: "srift-mcp-server",
|
|
2776
2826
|
title: "SRIFT Secure P2P File Transfer",
|
|
2777
|
-
version: "4.
|
|
2827
|
+
version: "4.1.0"
|
|
2778
2828
|
};
|
|
2779
2829
|
localDaemonBackend = { callTool, readResource };
|
|
2780
2830
|
handleMcpMessage = createMcpHandler({
|
|
@@ -2786,8 +2836,8 @@ var init_mcp = __esm({
|
|
|
2786
2836
|
});
|
|
2787
2837
|
|
|
2788
2838
|
// ../../cli/pack.ts
|
|
2789
|
-
import
|
|
2790
|
-
import
|
|
2839
|
+
import fs7 from "fs";
|
|
2840
|
+
import path6 from "path";
|
|
2791
2841
|
import zlib from "zlib";
|
|
2792
2842
|
import { pipeline } from "stream/promises";
|
|
2793
2843
|
import { Readable } from "stream";
|
|
@@ -2811,16 +2861,16 @@ function normFileMode(m) {
|
|
|
2811
2861
|
}
|
|
2812
2862
|
function listFiles(root, excludes = DEFAULT_EXCLUDES) {
|
|
2813
2863
|
const pats = excludes.map(globToRegExp);
|
|
2814
|
-
const excluded = (rel) => pats.some((p) => p.test(rel) || p.test(
|
|
2864
|
+
const excluded = (rel) => pats.some((p) => p.test(rel) || p.test(path6.posix.basename(rel)));
|
|
2815
2865
|
const out = [];
|
|
2816
2866
|
const walk = (dirAbs, dirRel) => {
|
|
2817
|
-
const ents =
|
|
2867
|
+
const ents = fs7.readdirSync(dirAbs, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
2818
2868
|
for (const e of ents) {
|
|
2819
2869
|
const rel = dirRel ? `${dirRel}/${e.name}` : e.name;
|
|
2820
2870
|
if (excluded(rel)) continue;
|
|
2821
|
-
const abs =
|
|
2871
|
+
const abs = path6.join(dirAbs, e.name);
|
|
2822
2872
|
if (e.isSymbolicLink()) continue;
|
|
2823
|
-
const st =
|
|
2873
|
+
const st = fs7.lstatSync(abs);
|
|
2824
2874
|
if (e.isDirectory()) {
|
|
2825
2875
|
out.push({ abs, rel, size: 0, mode: 493, mtime: Math.floor(st.mtimeMs / 1e3), dir: true });
|
|
2826
2876
|
walk(abs, rel);
|
|
@@ -2882,7 +2932,7 @@ async function* tarStream(entries, prefix) {
|
|
|
2882
2932
|
yield* entryHeaders(e, prefix);
|
|
2883
2933
|
if (e.dir) continue;
|
|
2884
2934
|
let written = 0;
|
|
2885
|
-
const stream =
|
|
2935
|
+
const stream = fs7.createReadStream(e.abs);
|
|
2886
2936
|
stream.on("error", () => {
|
|
2887
2937
|
});
|
|
2888
2938
|
try {
|
|
@@ -2903,17 +2953,17 @@ async function* tarStream(entries, prefix) {
|
|
|
2903
2953
|
yield Buffer.alloc(BLOCK * 2);
|
|
2904
2954
|
}
|
|
2905
2955
|
async function packDirectory(dir, outFile, excludes = DEFAULT_EXCLUDES) {
|
|
2906
|
-
const root =
|
|
2956
|
+
const root = path6.resolve(dir);
|
|
2907
2957
|
const entries = listFiles(root, excludes);
|
|
2908
2958
|
const files = entries.filter((e) => !e.dir);
|
|
2909
2959
|
if (!files.length) throw new Error(`Nothing to share in ${root} (empty, or everything was excluded)`);
|
|
2910
|
-
const prefix =
|
|
2911
|
-
await pipeline(Readable.from(tarStream(entries, prefix)), zlib.createGzip({ level: 6 }),
|
|
2960
|
+
const prefix = path6.basename(root) || "folder";
|
|
2961
|
+
await pipeline(Readable.from(tarStream(entries, prefix)), zlib.createGzip({ level: 6 }), fs7.createWriteStream(outFile, { mode: 384 }));
|
|
2912
2962
|
return { files: files.length, bytes: files.reduce((a, e) => a + e.size, 0) };
|
|
2913
2963
|
}
|
|
2914
2964
|
function readable(p) {
|
|
2915
2965
|
try {
|
|
2916
|
-
|
|
2966
|
+
fs7.accessSync(p, fs7.constants.R_OK);
|
|
2917
2967
|
return null;
|
|
2918
2968
|
} catch (e) {
|
|
2919
2969
|
return e?.code || "unreadable";
|
|
@@ -2927,7 +2977,7 @@ function listPaths(paths, excludes = DEFAULT_EXCLUDES, skipped = []) {
|
|
|
2927
2977
|
used.add(name.toLowerCase());
|
|
2928
2978
|
return name;
|
|
2929
2979
|
}
|
|
2930
|
-
const ext =
|
|
2980
|
+
const ext = path6.extname(name);
|
|
2931
2981
|
const stem = name.slice(0, name.length - ext.length);
|
|
2932
2982
|
for (let i = 2; ; i++) {
|
|
2933
2983
|
const c = `${stem} (${i})${ext}`;
|
|
@@ -2938,10 +2988,10 @@ function listPaths(paths, excludes = DEFAULT_EXCLUDES, skipped = []) {
|
|
|
2938
2988
|
}
|
|
2939
2989
|
};
|
|
2940
2990
|
for (const p of paths) {
|
|
2941
|
-
const abs =
|
|
2991
|
+
const abs = path6.resolve(p);
|
|
2942
2992
|
let st;
|
|
2943
2993
|
try {
|
|
2944
|
-
st =
|
|
2994
|
+
st = fs7.statSync(abs);
|
|
2945
2995
|
} catch (e) {
|
|
2946
2996
|
skipped.push({ path: abs, reason: e?.code === "ENOENT" ? "not found" : e?.code || "stat failed" });
|
|
2947
2997
|
continue;
|
|
@@ -2954,7 +3004,7 @@ function listPaths(paths, excludes = DEFAULT_EXCLUDES, skipped = []) {
|
|
|
2954
3004
|
skipped.push({ path: abs, reason: e?.code || "unreadable folder" });
|
|
2955
3005
|
continue;
|
|
2956
3006
|
}
|
|
2957
|
-
const name = unique(
|
|
3007
|
+
const name = unique(path6.basename(abs) || "item");
|
|
2958
3008
|
out.push({ abs, rel: name, size: 0, mode: 493, mtime: Math.floor(st.mtimeMs / 1e3), dir: true });
|
|
2959
3009
|
for (const e of inner) {
|
|
2960
3010
|
const why = e.dir ? null : readable(e.abs);
|
|
@@ -2970,7 +3020,7 @@ function listPaths(paths, excludes = DEFAULT_EXCLUDES, skipped = []) {
|
|
|
2970
3020
|
skipped.push({ path: abs, reason: why });
|
|
2971
3021
|
continue;
|
|
2972
3022
|
}
|
|
2973
|
-
out.push({ abs, rel: unique(
|
|
3023
|
+
out.push({ abs, rel: unique(path6.basename(abs) || "item"), size: st.size, mode: normFileMode(st.mode), mtime: Math.floor(st.mtimeMs / 1e3), dir: false });
|
|
2974
3024
|
} else {
|
|
2975
3025
|
skipped.push({ path: abs, reason: "not a regular file or folder" });
|
|
2976
3026
|
}
|
|
@@ -2987,7 +3037,7 @@ async function packPaths(paths, outFile, prefix, excludes = DEFAULT_EXCLUDES) {
|
|
|
2987
3037
|
throw new Error(`Nothing to share${skipped.length ? ` \u2014 skipped: ${skipped.map((x) => `${x.path} (${x.reason})`).join(", ")}` : " (empty folders, or everything was excluded)"}`);
|
|
2988
3038
|
}
|
|
2989
3039
|
try {
|
|
2990
|
-
await pipeline(Readable.from(tarStream(entries, prefix)), zlib.createGzip({ level: 6 }),
|
|
3040
|
+
await pipeline(Readable.from(tarStream(entries, prefix)), zlib.createGzip({ level: 6 }), fs7.createWriteStream(outFile, { mode: 384 }));
|
|
2991
3041
|
return { files: files.length, bytes: files.reduce((a, e) => a + e.size, 0), skipped };
|
|
2992
3042
|
} catch (err) {
|
|
2993
3043
|
const bad = err?.packEntry;
|
|
@@ -3028,10 +3078,10 @@ import express from "express";
|
|
|
3028
3078
|
import cors from "cors";
|
|
3029
3079
|
import { WebSocket as WebSocket2 } from "ws";
|
|
3030
3080
|
import { webcrypto } from "crypto";
|
|
3031
|
-
import
|
|
3081
|
+
import fs8 from "fs";
|
|
3032
3082
|
import http4 from "http";
|
|
3033
3083
|
import { Duplex } from "stream";
|
|
3034
|
-
import
|
|
3084
|
+
import path7 from "path";
|
|
3035
3085
|
import os4 from "os";
|
|
3036
3086
|
import { v4 as uuidv4 } from "uuid";
|
|
3037
3087
|
import dotenv from "dotenv";
|
|
@@ -3162,6 +3212,46 @@ async function decrypt(encB64) {
|
|
|
3162
3212
|
const decrypted = await crypto5.subtle.decrypt({ name: "AES-GCM", iv: iv2 }, encryptionKey, ciphertext);
|
|
3163
3213
|
return new TextDecoder().decode(decrypted);
|
|
3164
3214
|
}
|
|
3215
|
+
function relayKey() {
|
|
3216
|
+
if (!session.id) return Promise.reject(new Error("No active session"));
|
|
3217
|
+
if (relayKeyCache?.sessionId !== session.id) relayKeyCache = { sessionId: session.id, key: deriveKey(session.id) };
|
|
3218
|
+
return relayKeyCache.key;
|
|
3219
|
+
}
|
|
3220
|
+
function relayAad(fileId, chunkIndex, totalChunks) {
|
|
3221
|
+
return new TextEncoder().encode(`${RELAY_ENC}|${fileId}|${chunkIndex}|${totalChunks}`);
|
|
3222
|
+
}
|
|
3223
|
+
async function sealRelayChunk(plain, fileId, chunkIndex, totalChunks) {
|
|
3224
|
+
const iv2 = crypto5.getRandomValues(new Uint8Array(IV_LEN));
|
|
3225
|
+
const ct = new Uint8Array(await crypto5.subtle.encrypt(
|
|
3226
|
+
{ name: "AES-GCM", iv: iv2, additionalData: relayAad(fileId, chunkIndex, totalChunks) },
|
|
3227
|
+
await relayKey(),
|
|
3228
|
+
plain
|
|
3229
|
+
));
|
|
3230
|
+
const out = new Uint8Array(IV_LEN + ct.length);
|
|
3231
|
+
out.set(iv2);
|
|
3232
|
+
out.set(ct, IV_LEN);
|
|
3233
|
+
return Array.from(out);
|
|
3234
|
+
}
|
|
3235
|
+
async function openRelayChunk(p) {
|
|
3236
|
+
if (p.enc === void 0 || p.enc === null) {
|
|
3237
|
+
if (typeof p.chunk === "string") return Buffer.from(p.chunk, "base64");
|
|
3238
|
+
if (Array.isArray(p.chunk)) return Buffer.from(p.chunk);
|
|
3239
|
+
throw new Error("relay chunk is neither base64 nor a byte array");
|
|
3240
|
+
}
|
|
3241
|
+
if (p.enc !== RELAY_ENC) throw new Error(`unsupported relay encryption scheme: ${String(p.enc)}`);
|
|
3242
|
+
if (!Array.isArray(p.chunk)) throw new Error("sealed relay chunk is not a byte array");
|
|
3243
|
+
const data = Uint8Array.from(p.chunk);
|
|
3244
|
+
if (data.length < IV_LEN + 16) throw new Error("sealed relay chunk too short");
|
|
3245
|
+
try {
|
|
3246
|
+
return Buffer.from(await crypto5.subtle.decrypt(
|
|
3247
|
+
{ name: "AES-GCM", iv: data.slice(0, IV_LEN), additionalData: relayAad(p.fileId, p.chunkIndex, p.totalChunks) },
|
|
3248
|
+
await relayKey(),
|
|
3249
|
+
data.slice(IV_LEN)
|
|
3250
|
+
));
|
|
3251
|
+
} catch {
|
|
3252
|
+
throw new Error(`relay chunk ${p.chunkIndex} of ${p.fileId} failed authentication (wrong session key or tampered data)`);
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3165
3255
|
function getTrackers(sessionId) {
|
|
3166
3256
|
const list = [];
|
|
3167
3257
|
try {
|
|
@@ -3200,13 +3290,13 @@ function writeStateFile() {
|
|
|
3200
3290
|
})),
|
|
3201
3291
|
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
3202
3292
|
};
|
|
3203
|
-
|
|
3293
|
+
fs8.writeFileSync(STATE_PATH, JSON.stringify(stateData, null, 2), "utf-8");
|
|
3204
3294
|
}
|
|
3205
3295
|
function pruneStaleStateOnStartup() {
|
|
3206
|
-
if (!
|
|
3296
|
+
if (!fs8.existsSync(STATE_PATH)) return;
|
|
3207
3297
|
let prunedCount = 0;
|
|
3208
3298
|
try {
|
|
3209
|
-
const raw =
|
|
3299
|
+
const raw = fs8.readFileSync(STATE_PATH, "utf-8");
|
|
3210
3300
|
const parsed = JSON.parse(raw);
|
|
3211
3301
|
const now = Date.now();
|
|
3212
3302
|
const lastUpdatedMs = parsed?.lastUpdated ? Date.parse(parsed.lastUpdated) : NaN;
|
|
@@ -3225,7 +3315,7 @@ function pruneStaleStateOnStartup() {
|
|
|
3225
3315
|
if (sessionStale || freshTransfers.length !== transfers.length) {
|
|
3226
3316
|
parsed.activeTransfers = freshTransfers;
|
|
3227
3317
|
parsed.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
3228
|
-
|
|
3318
|
+
fs8.writeFileSync(STATE_PATH, JSON.stringify(parsed, null, 2), "utf-8");
|
|
3229
3319
|
}
|
|
3230
3320
|
if (prunedCount > 0) {
|
|
3231
3321
|
console.log(
|
|
@@ -3235,7 +3325,7 @@ function pruneStaleStateOnStartup() {
|
|
|
3235
3325
|
} catch (err) {
|
|
3236
3326
|
console.warn("[DAEMON] Stale state file was unreadable, removing it:", err?.message || err);
|
|
3237
3327
|
try {
|
|
3238
|
-
|
|
3328
|
+
fs8.unlinkSync(STATE_PATH);
|
|
3239
3329
|
} catch {
|
|
3240
3330
|
}
|
|
3241
3331
|
}
|
|
@@ -3256,8 +3346,11 @@ function connectWebSocket(wsTargetUrl) {
|
|
|
3256
3346
|
}
|
|
3257
3347
|
csrfToken = null;
|
|
3258
3348
|
console.log(`[DAEMON] Connecting to Signaler WS: ${wsTargetUrl}`);
|
|
3259
|
-
|
|
3260
|
-
wsConn
|
|
3349
|
+
const sock = new WebSocket2(wsTargetUrl, { agent: agentFor(wsTargetUrl) });
|
|
3350
|
+
wsConn = sock;
|
|
3351
|
+
const current = () => wsConn === sock;
|
|
3352
|
+
sock.on("open", () => {
|
|
3353
|
+
if (!current()) return;
|
|
3261
3354
|
console.log("[DAEMON] WebSocket connection open");
|
|
3262
3355
|
session.isConnected = true;
|
|
3263
3356
|
writeStateFile();
|
|
@@ -3295,7 +3388,8 @@ function connectWebSocket(wsTargetUrl) {
|
|
|
3295
3388
|
}
|
|
3296
3389
|
}, 3e4);
|
|
3297
3390
|
});
|
|
3298
|
-
|
|
3391
|
+
sock.on("message", async (dataStr) => {
|
|
3392
|
+
if (!current()) return;
|
|
3299
3393
|
try {
|
|
3300
3394
|
const msg = JSON.parse(dataStr);
|
|
3301
3395
|
const { type, payload } = msg;
|
|
@@ -3328,6 +3422,30 @@ function connectWebSocket(wsTargetUrl) {
|
|
|
3328
3422
|
case "heartbeat_ack":
|
|
3329
3423
|
lastHeartbeatAckAt = Date.now();
|
|
3330
3424
|
break;
|
|
3425
|
+
case "user_list": {
|
|
3426
|
+
const users = Array.isArray(payload?.users) ? payload.users : [];
|
|
3427
|
+
participants = users.map((u) => ({ userId: String(u.id), username: String(u.username || ""), isHost: !!u.is_host, online: !!u.isOnline }));
|
|
3428
|
+
broadcastSSE("participants", { participants });
|
|
3429
|
+
if (payload?.messageId) {
|
|
3430
|
+
try {
|
|
3431
|
+
sock.send(JSON.stringify({ type: "user_list_ack", payload: { messageId: payload.messageId } }));
|
|
3432
|
+
} catch {
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
break;
|
|
3436
|
+
}
|
|
3437
|
+
case "kicked_from_session":
|
|
3438
|
+
console.log(`[DAEMON] Kicked from session: ${payload?.reason || ""}`);
|
|
3439
|
+
sessionTerminated = true;
|
|
3440
|
+
terminationReason = payload?.reason || "You were removed from the session by the host";
|
|
3441
|
+
session.isConnected = false;
|
|
3442
|
+
session.userId = null;
|
|
3443
|
+
csrfToken = null;
|
|
3444
|
+
pendingJoins = [];
|
|
3445
|
+
participants = [];
|
|
3446
|
+
writeStateFile();
|
|
3447
|
+
broadcastSSE("session_terminated", { reason: terminationReason });
|
|
3448
|
+
break;
|
|
3331
3449
|
case "session_deleted":
|
|
3332
3450
|
console.log(`[DAEMON] Session deleted by host: ${payload.reason || ""}`);
|
|
3333
3451
|
sessionTerminated = true;
|
|
@@ -3335,6 +3453,7 @@ function connectWebSocket(wsTargetUrl) {
|
|
|
3335
3453
|
session = { id: null, name: null, role: null, isConnected: false, userId: null };
|
|
3336
3454
|
activeTransfers = [];
|
|
3337
3455
|
pendingJoins = [];
|
|
3456
|
+
participants = [];
|
|
3338
3457
|
encryptionKey = null;
|
|
3339
3458
|
writeStateFile();
|
|
3340
3459
|
broadcastSSE("session_terminated", { reason: terminationReason });
|
|
@@ -3351,6 +3470,7 @@ function connectWebSocket(wsTargetUrl) {
|
|
|
3351
3470
|
session.isConnected = false;
|
|
3352
3471
|
session.userId = null;
|
|
3353
3472
|
pendingJoins = [];
|
|
3473
|
+
participants = [];
|
|
3354
3474
|
writeStateFile();
|
|
3355
3475
|
broadcastSSE("session_terminated", { reason: terminationReason });
|
|
3356
3476
|
}
|
|
@@ -3405,14 +3525,41 @@ function connectWebSocket(wsTargetUrl) {
|
|
|
3405
3525
|
break;
|
|
3406
3526
|
case "file_accept":
|
|
3407
3527
|
console.log(`[DAEMON] File offer accepted by peer! ID: ${payload.fileId}`);
|
|
3528
|
+
{
|
|
3529
|
+
const acceptedTx = activeTransfers.find((t) => t.fileId === payload.fileId);
|
|
3530
|
+
if (acceptedTx) acceptedTx.peerRelayEnc = payload.relayEnc === RELAY_ENC ? RELAY_ENC : void 0;
|
|
3531
|
+
}
|
|
3408
3532
|
startUpload(payload.fileId, payload.userId);
|
|
3409
3533
|
break;
|
|
3410
3534
|
case "webtorrent_info_hash":
|
|
3411
3535
|
handleWebTorrentInfoHash(payload);
|
|
3412
3536
|
break;
|
|
3413
3537
|
case "file_chunk":
|
|
3414
|
-
handleIncomingChunk(payload)
|
|
3538
|
+
handleIncomingChunk(payload).catch((e) => {
|
|
3539
|
+
console.error(`[DAEMON] Relay chunk rejected: ${e?.message || e}`);
|
|
3540
|
+
const bad = activeTransfers.find((t) => t.fileId === payload?.fileId);
|
|
3541
|
+
if (bad) {
|
|
3542
|
+
bad.status = "error";
|
|
3543
|
+
writeStateFile();
|
|
3544
|
+
}
|
|
3545
|
+
try {
|
|
3546
|
+
sock.send(JSON.stringify({ type: "file_cancel_by_receiver", payload: { fileId: payload?.fileId } }));
|
|
3547
|
+
} catch {
|
|
3548
|
+
}
|
|
3549
|
+
});
|
|
3550
|
+
break;
|
|
3551
|
+
case "file_cancelled_by_receiver":
|
|
3552
|
+
case "file_cancelled_by_sender":
|
|
3553
|
+
case "file_cancelled_all": {
|
|
3554
|
+
const gone = activeTransfers.find((t) => t.fileId === payload?.fileId);
|
|
3555
|
+
activeUploads.delete(payload?.fileId);
|
|
3556
|
+
if (gone && gone.status !== "completed") {
|
|
3557
|
+
gone.status = "cancelled";
|
|
3558
|
+
writeStateFile();
|
|
3559
|
+
broadcastSSE("transfer_progress", { fileId: gone.fileId, fileName: gone.name, size: gone.size, bytesTransferred: gone.bytesTransferred, progress: gone.progress, status: "cancelled" });
|
|
3560
|
+
}
|
|
3415
3561
|
break;
|
|
3562
|
+
}
|
|
3416
3563
|
case "file_chunk_ack":
|
|
3417
3564
|
handleChunkAck(payload);
|
|
3418
3565
|
break;
|
|
@@ -3424,6 +3571,7 @@ function connectWebSocket(wsTargetUrl) {
|
|
|
3424
3571
|
if (entry) {
|
|
3425
3572
|
entry.token = payload.token;
|
|
3426
3573
|
entry.downloadUrl = payload.downloadUrl;
|
|
3574
|
+
if (typeof payload.claim === "string") entry.claim = payload.claim;
|
|
3427
3575
|
pubsharesByToken.set(payload.token, entry);
|
|
3428
3576
|
const resolver = pubshareRegResolvers.get(payload.fileId);
|
|
3429
3577
|
if (resolver) {
|
|
@@ -3457,7 +3605,8 @@ function connectWebSocket(wsTargetUrl) {
|
|
|
3457
3605
|
console.error("[DAEMON] Error processing WS message:", err);
|
|
3458
3606
|
}
|
|
3459
3607
|
});
|
|
3460
|
-
|
|
3608
|
+
sock.on("close", () => {
|
|
3609
|
+
if (!current()) return;
|
|
3461
3610
|
console.log("[DAEMON] WebSocket connection closed");
|
|
3462
3611
|
session.isConnected = false;
|
|
3463
3612
|
csrfToken = null;
|
|
@@ -3477,7 +3626,8 @@ function connectWebSocket(wsTargetUrl) {
|
|
|
3477
3626
|
}
|
|
3478
3627
|
}, 3e3);
|
|
3479
3628
|
});
|
|
3480
|
-
|
|
3629
|
+
sock.on("error", (err) => {
|
|
3630
|
+
if (!current()) return;
|
|
3481
3631
|
console.error("[DAEMON] WebSocket error:", err);
|
|
3482
3632
|
});
|
|
3483
3633
|
}
|
|
@@ -3615,7 +3765,7 @@ async function startWebSocketUpload(fileId, targetUserId) {
|
|
|
3615
3765
|
const tx = activeTransfers.find((t) => t.fileId === fileId);
|
|
3616
3766
|
if (!tx || !tx.filePath) return;
|
|
3617
3767
|
const chunkSize = 64 * 1024;
|
|
3618
|
-
const stat =
|
|
3768
|
+
const stat = fs8.statSync(tx.filePath);
|
|
3619
3769
|
const totalChunks = Math.max(1, Math.ceil(stat.size / chunkSize));
|
|
3620
3770
|
tx.status = "uploading";
|
|
3621
3771
|
tx.startTime = Date.now();
|
|
@@ -3633,6 +3783,17 @@ async function startWebSocketUpload(fileId, targetUserId) {
|
|
|
3633
3783
|
sendNextChunk(fileId);
|
|
3634
3784
|
}
|
|
3635
3785
|
function sendNextChunk(fileId) {
|
|
3786
|
+
sendNextChunkAsync(fileId).catch((e) => {
|
|
3787
|
+
console.error(`[DAEMON] Relay upload failed for ${fileId}: ${e?.message || e}`);
|
|
3788
|
+
activeUploads.delete(fileId);
|
|
3789
|
+
const failed = activeTransfers.find((t) => t.fileId === fileId);
|
|
3790
|
+
if (failed) {
|
|
3791
|
+
failed.status = "error";
|
|
3792
|
+
writeStateFile();
|
|
3793
|
+
}
|
|
3794
|
+
});
|
|
3795
|
+
}
|
|
3796
|
+
async function sendNextChunkAsync(fileId) {
|
|
3636
3797
|
const upload = activeUploads.get(fileId);
|
|
3637
3798
|
if (!upload) return;
|
|
3638
3799
|
const tx = activeTransfers.find((t) => t.fileId === fileId);
|
|
@@ -3652,18 +3813,18 @@ function sendNextChunk(fileId) {
|
|
|
3652
3813
|
return;
|
|
3653
3814
|
}
|
|
3654
3815
|
const start = upload.currentChunk * upload.chunkSize;
|
|
3655
|
-
const stat =
|
|
3816
|
+
const stat = fs8.statSync(upload.filePath);
|
|
3656
3817
|
const end = Math.min(start + upload.chunkSize, stat.size);
|
|
3657
3818
|
const buffer = Buffer.alloc(end - start);
|
|
3658
|
-
const fd =
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
const
|
|
3819
|
+
const fd = fs8.openSync(upload.filePath, "r");
|
|
3820
|
+
fs8.readSync(fd, buffer, 0, end - start, start);
|
|
3821
|
+
fs8.closeSync(fd);
|
|
3822
|
+
const frame = tx.peerRelayEnc === RELAY_ENC ? { chunk: await sealRelayChunk(buffer, fileId, upload.currentChunk, upload.totalChunks), enc: RELAY_ENC } : { chunk: buffer.toString("base64") };
|
|
3662
3823
|
wsConn?.send(JSON.stringify({
|
|
3663
3824
|
type: "file_chunk",
|
|
3664
3825
|
payload: {
|
|
3665
3826
|
fileId,
|
|
3666
|
-
|
|
3827
|
+
...frame,
|
|
3667
3828
|
chunkIndex: upload.currentChunk,
|
|
3668
3829
|
totalChunks: upload.totalChunks,
|
|
3669
3830
|
targetUserId: upload.targetUserId
|
|
@@ -3697,18 +3858,21 @@ function handleChunkAck(payload) {
|
|
|
3697
3858
|
sendNextChunk(fileId);
|
|
3698
3859
|
}
|
|
3699
3860
|
}
|
|
3700
|
-
function handleIncomingChunk(payload) {
|
|
3701
|
-
const { fileId,
|
|
3861
|
+
async function handleIncomingChunk(payload) {
|
|
3862
|
+
const { fileId, chunkIndex, totalChunks, from } = payload;
|
|
3702
3863
|
const tx = activeTransfers.find((t) => t.fileId === fileId);
|
|
3703
3864
|
if (!tx) return;
|
|
3865
|
+
const mode = payload.enc === void 0 || payload.enc === null ? "plain" : "sealed";
|
|
3866
|
+
if (tx.relayMode && tx.relayMode !== mode) throw new Error(`transfer ${fileId} mixed sealed and plaintext chunks`);
|
|
3867
|
+
tx.relayMode = mode;
|
|
3868
|
+
const chunkBuffer = await openRelayChunk(payload);
|
|
3704
3869
|
tx.status = "downloading";
|
|
3705
3870
|
tx.totalChunks = totalChunks;
|
|
3706
3871
|
tx.peerId = from;
|
|
3707
|
-
const chunkBuffer = Buffer.from(chunk, "base64");
|
|
3708
3872
|
tx.bytesTransferred += chunkBuffer.length;
|
|
3709
3873
|
tx.chunksCount++;
|
|
3710
|
-
const chunkPath =
|
|
3711
|
-
|
|
3874
|
+
const chunkPath = path7.join(TEMP_DIR, `${fileId}_chunk_${chunkIndex}`);
|
|
3875
|
+
fs8.writeFileSync(chunkPath, chunkBuffer);
|
|
3712
3876
|
tx.progress = tx.bytesTransferred / tx.size * 100;
|
|
3713
3877
|
const elapsed = (Date.now() - tx.startTime) / 1e3;
|
|
3714
3878
|
if (elapsed > 0) {
|
|
@@ -3736,10 +3900,10 @@ function handleFileComplete(payload) {
|
|
|
3736
3900
|
const tx = activeTransfers.find((t) => t.fileId === fileId);
|
|
3737
3901
|
if (!tx) return;
|
|
3738
3902
|
const destDir = tx.saveDir || process.cwd();
|
|
3739
|
-
const destPath =
|
|
3903
|
+
const destPath = path7.join(destDir, tx.name);
|
|
3740
3904
|
try {
|
|
3741
|
-
if (!
|
|
3742
|
-
|
|
3905
|
+
if (!fs8.existsSync(destDir)) {
|
|
3906
|
+
fs8.mkdirSync(destDir, { recursive: true });
|
|
3743
3907
|
}
|
|
3744
3908
|
} catch (err) {
|
|
3745
3909
|
console.error(`[DAEMON] Failed to create destination directory ${destDir}:`, err);
|
|
@@ -3747,7 +3911,7 @@ function handleFileComplete(payload) {
|
|
|
3747
3911
|
writeStateFile();
|
|
3748
3912
|
return;
|
|
3749
3913
|
}
|
|
3750
|
-
const writeStream =
|
|
3914
|
+
const writeStream = fs8.createWriteStream(destPath);
|
|
3751
3915
|
writeStream.on("error", (err) => {
|
|
3752
3916
|
console.error(`[DAEMON] Write stream error for ${destPath}:`, err);
|
|
3753
3917
|
tx.status = "error";
|
|
@@ -3782,12 +3946,12 @@ function handleFileComplete(payload) {
|
|
|
3782
3946
|
});
|
|
3783
3947
|
});
|
|
3784
3948
|
for (let i = 0; i < tx.totalChunks; i++) {
|
|
3785
|
-
const chunkPath =
|
|
3786
|
-
if (
|
|
3949
|
+
const chunkPath = path7.join(TEMP_DIR, `${fileId}_chunk_${i}`);
|
|
3950
|
+
if (fs8.existsSync(chunkPath)) {
|
|
3787
3951
|
try {
|
|
3788
|
-
const chunkBuf =
|
|
3952
|
+
const chunkBuf = fs8.readFileSync(chunkPath);
|
|
3789
3953
|
writeStream.write(chunkBuf);
|
|
3790
|
-
|
|
3954
|
+
fs8.unlinkSync(chunkPath);
|
|
3791
3955
|
} catch (err) {
|
|
3792
3956
|
console.error(`[DAEMON] Error reading/writing chunk ${i}:`, err);
|
|
3793
3957
|
writeStream.emit("error", err);
|
|
@@ -3797,6 +3961,20 @@ function handleFileComplete(payload) {
|
|
|
3797
3961
|
}
|
|
3798
3962
|
writeStream.end();
|
|
3799
3963
|
}
|
|
3964
|
+
async function waitSignalReady(maxMs = 8e3) {
|
|
3965
|
+
if (!session.id) return { status: 409, body: { success: false, error: "No active session. Call /session/start first." } };
|
|
3966
|
+
if (sessionTerminated) return { status: 410, body: { success: false, error: terminationReason || "The session ended." } };
|
|
3967
|
+
if (session.role === "guest" && !session.userId) {
|
|
3968
|
+
return { status: 409, body: { success: false, error: "Waiting for the host to approve your join request.", retryAfterMs: 2e3 } };
|
|
3969
|
+
}
|
|
3970
|
+
const ready = () => !!encryptionKey && !!csrfToken && wsConn?.readyState === WebSocket2.OPEN;
|
|
3971
|
+
for (let waited = 0; waited < maxMs && !ready() && session.id; waited += 100) {
|
|
3972
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
3973
|
+
}
|
|
3974
|
+
if (ready()) return null;
|
|
3975
|
+
if (!encryptionKey) return { status: 409, body: { success: false, error: "Encryption key not ready. Session still initialising." } };
|
|
3976
|
+
return { status: 503, body: { success: false, error: "Signaling connection not ready (WebSocket is connecting). Retry in 1\u20132 seconds.", retryAfterMs: 1500 } };
|
|
3977
|
+
}
|
|
3800
3978
|
async function registerPubshare(fileId, absPath, filename, size, mime, opts = {}, timeoutMs = 5e3) {
|
|
3801
3979
|
if (!wsConn || wsConn.readyState !== WebSocket2.OPEN) {
|
|
3802
3980
|
throw new Error("Signaling WebSocket not connected");
|
|
@@ -3819,7 +3997,8 @@ async function registerPubshare(fileId, absPath, filename, size, mime, opts = {}
|
|
|
3819
3997
|
linkKey: opts.linkKey,
|
|
3820
3998
|
tempFile: opts.tempFile,
|
|
3821
3999
|
displayName: opts.displayName || filename,
|
|
3822
|
-
prefixLen: opts.prefixLen
|
|
4000
|
+
prefixLen: opts.prefixLen,
|
|
4001
|
+
claim: opts.preferClaim
|
|
3823
4002
|
};
|
|
3824
4003
|
pubshares.set(fileId, entry);
|
|
3825
4004
|
const ackP = new Promise((resolve, reject) => {
|
|
@@ -3844,6 +4023,7 @@ async function registerPubshare(fileId, absPath, filename, size, mime, opts = {}
|
|
|
3844
4023
|
maxDownloads,
|
|
3845
4024
|
expiresAt,
|
|
3846
4025
|
preferToken: opts.preferToken || void 0,
|
|
4026
|
+
preferClaim: opts.preferToken && opts.preferClaim ? opts.preferClaim : void 0,
|
|
3847
4027
|
encrypted: opts.encrypted ? true : void 0,
|
|
3848
4028
|
prefixLen: opts.encrypted && opts.prefixLen ? opts.prefixLen : void 0
|
|
3849
4029
|
}
|
|
@@ -3860,7 +4040,7 @@ async function reregisterPubsharesAfterReconnect() {
|
|
|
3860
4040
|
pubsharesByToken.delete(entry.token);
|
|
3861
4041
|
if (entry.tempFile) {
|
|
3862
4042
|
try {
|
|
3863
|
-
|
|
4043
|
+
fs8.unlinkSync(entry.tempFile);
|
|
3864
4044
|
} catch {
|
|
3865
4045
|
}
|
|
3866
4046
|
}
|
|
@@ -3871,7 +4051,7 @@ async function reregisterPubsharesAfterReconnect() {
|
|
|
3871
4051
|
pubsharesByToken.delete(entry.token);
|
|
3872
4052
|
if (entry.tempFile) {
|
|
3873
4053
|
try {
|
|
3874
|
-
|
|
4054
|
+
fs8.unlinkSync(entry.tempFile);
|
|
3875
4055
|
} catch {
|
|
3876
4056
|
}
|
|
3877
4057
|
}
|
|
@@ -3890,6 +4070,7 @@ async function reregisterPubsharesAfterReconnect() {
|
|
|
3890
4070
|
maxDownloads: remainingMax,
|
|
3891
4071
|
ttlMs: remainingTtl,
|
|
3892
4072
|
preferToken: entry.token,
|
|
4073
|
+
preferClaim: entry.claim,
|
|
3893
4074
|
encrypted: entry.encrypted,
|
|
3894
4075
|
linkKey: entry.linkKey,
|
|
3895
4076
|
tempFile: entry.tempFile,
|
|
@@ -3915,16 +4096,16 @@ async function handlePubsharePull(payload) {
|
|
|
3915
4096
|
}
|
|
3916
4097
|
let fd;
|
|
3917
4098
|
try {
|
|
3918
|
-
fd =
|
|
4099
|
+
fd = fs8.openSync(entry.filePath, "r");
|
|
3919
4100
|
} catch (e) {
|
|
3920
4101
|
throw new Error(e?.code === "ENOENT" ? "the shared file no longer exists on the sender" : `cannot read the shared file (${e?.code || e?.message})`);
|
|
3921
4102
|
}
|
|
3922
4103
|
try {
|
|
3923
|
-
const size =
|
|
4104
|
+
const size = fs8.fstatSync(fd).size;
|
|
3924
4105
|
if (size !== entry.size) throw new Error("the shared file changed after the link was created");
|
|
3925
4106
|
} catch (e) {
|
|
3926
4107
|
try {
|
|
3927
|
-
|
|
4108
|
+
fs8.closeSync(fd);
|
|
3928
4109
|
} catch {
|
|
3929
4110
|
}
|
|
3930
4111
|
throw e;
|
|
@@ -3944,7 +4125,7 @@ async function handlePubsharePull(payload) {
|
|
|
3944
4125
|
}
|
|
3945
4126
|
const len = Math.min(CHUNK, end - pos + 1);
|
|
3946
4127
|
const buf = Buffer.alloc(len);
|
|
3947
|
-
const got =
|
|
4128
|
+
const got = fs8.readSync(fd, buf, 0, len, pos);
|
|
3948
4129
|
if (got !== len) throw new Error("the shared file changed while it was being sent");
|
|
3949
4130
|
const dataB64 = buf.toString("base64");
|
|
3950
4131
|
wsConn.send(JSON.stringify({
|
|
@@ -3968,7 +4149,7 @@ async function handlePubsharePull(payload) {
|
|
|
3968
4149
|
}
|
|
3969
4150
|
} finally {
|
|
3970
4151
|
try {
|
|
3971
|
-
|
|
4152
|
+
fs8.closeSync(fd);
|
|
3972
4153
|
} catch {
|
|
3973
4154
|
}
|
|
3974
4155
|
activePulls.delete(requestId);
|
|
@@ -4021,7 +4202,7 @@ async function ensureHostSessionOnce(sessionName) {
|
|
|
4021
4202
|
}
|
|
4022
4203
|
async function shareViaRelay(absPath, stat, reqBody, encrypt2) {
|
|
4023
4204
|
await ensureHostSession(reqBody.sessionName);
|
|
4024
|
-
const filename = reqBody.name ||
|
|
4205
|
+
const filename = reqBody.name || path7.basename(absPath);
|
|
4025
4206
|
const fileId = `cli_file_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
4026
4207
|
let servePath = absPath;
|
|
4027
4208
|
let serveSize = stat.size;
|
|
@@ -4031,13 +4212,13 @@ async function shareViaRelay(absPath, stat, reqBody, encrypt2) {
|
|
|
4031
4212
|
if (encrypt2) {
|
|
4032
4213
|
const key = newLinkKey();
|
|
4033
4214
|
linkKey = b64url(key);
|
|
4034
|
-
tempFile =
|
|
4215
|
+
tempFile = path7.join(TEMP_DIR, `${fileId}.sre1`);
|
|
4035
4216
|
const meta = { name: filename, size: stat.size, mime: "application/octet-stream" };
|
|
4036
|
-
const fd =
|
|
4217
|
+
const fd = fs8.openSync(tempFile, "w", 384);
|
|
4037
4218
|
try {
|
|
4038
|
-
for await (const part of encryptFile(absPath, key, meta, { password: reqBody.password }))
|
|
4219
|
+
for await (const part of encryptFile(absPath, key, meta, { password: reqBody.password })) fs8.writeSync(fd, part);
|
|
4039
4220
|
} finally {
|
|
4040
|
-
|
|
4221
|
+
fs8.closeSync(fd);
|
|
4041
4222
|
}
|
|
4042
4223
|
servePath = tempFile;
|
|
4043
4224
|
serveSize = encryptedSize(stat.size, meta);
|
|
@@ -4069,7 +4250,7 @@ async function shareViaRelay(absPath, stat, reqBody, encrypt2) {
|
|
|
4069
4250
|
if (reqBody._ownedPath) {
|
|
4070
4251
|
if (encrypt2) {
|
|
4071
4252
|
try {
|
|
4072
|
-
|
|
4253
|
+
fs8.unlinkSync(reqBody._ownedPath);
|
|
4073
4254
|
} catch {
|
|
4074
4255
|
}
|
|
4075
4256
|
} else tempFile = reqBody._ownedPath;
|
|
@@ -4108,7 +4289,7 @@ async function shareViaRelay(absPath, stat, reqBody, encrypt2) {
|
|
|
4108
4289
|
} catch (e) {
|
|
4109
4290
|
if (tempFile) {
|
|
4110
4291
|
try {
|
|
4111
|
-
|
|
4292
|
+
fs8.unlinkSync(tempFile);
|
|
4112
4293
|
} catch {
|
|
4113
4294
|
}
|
|
4114
4295
|
}
|
|
@@ -4140,26 +4321,26 @@ function safeArchiveBase(name, fallback) {
|
|
|
4140
4321
|
async function createShare(reqBody) {
|
|
4141
4322
|
if (!reqBody?.filePath) throw Object.assign(new Error("filePath (or filePaths) required"), { status: 400 });
|
|
4142
4323
|
assertRelayMode(reqBody);
|
|
4143
|
-
const absPath =
|
|
4324
|
+
const absPath = path7.resolve(reqBody.filePath);
|
|
4144
4325
|
let stat;
|
|
4145
4326
|
try {
|
|
4146
|
-
stat =
|
|
4327
|
+
stat = fs8.statSync(absPath);
|
|
4147
4328
|
} catch {
|
|
4148
4329
|
throw Object.assign(new Error(`File not found: ${absPath}`), { status: 404 });
|
|
4149
4330
|
}
|
|
4150
4331
|
if (stat.isDirectory()) {
|
|
4151
|
-
const base = safeArchiveBase(
|
|
4152
|
-
const out =
|
|
4332
|
+
const base = safeArchiveBase(path7.basename(absPath), "folder");
|
|
4333
|
+
const out = path7.join(TEMP_DIR, `${uuidv4()}-${base}.tar.gz`);
|
|
4153
4334
|
try {
|
|
4154
4335
|
await packDirectory(absPath, out, excludesFor(reqBody));
|
|
4155
4336
|
} catch (e) {
|
|
4156
4337
|
try {
|
|
4157
|
-
|
|
4338
|
+
fs8.unlinkSync(out);
|
|
4158
4339
|
} catch {
|
|
4159
4340
|
}
|
|
4160
4341
|
throw Object.assign(new Error(e?.message || String(e)), { status: 400 });
|
|
4161
4342
|
}
|
|
4162
|
-
return shareViaRelay(out,
|
|
4343
|
+
return shareViaRelay(out, fs8.statSync(out), { ...reqBody, name: reqBody.name || `${base}.tar.gz`, _ownedPath: out }, shareEncrypt(reqBody));
|
|
4163
4344
|
}
|
|
4164
4345
|
if (!stat.isFile()) throw Object.assign(new Error(`Not a regular file or folder: ${absPath}`), { status: 400 });
|
|
4165
4346
|
return shareViaRelay(absPath, stat, reqBody, shareEncrypt(reqBody));
|
|
@@ -4167,26 +4348,26 @@ async function createShare(reqBody) {
|
|
|
4167
4348
|
async function createMultiShare(reqBody) {
|
|
4168
4349
|
assertRelayMode(reqBody);
|
|
4169
4350
|
const raw = Array.isArray(reqBody.filePaths) ? reqBody.filePaths : [];
|
|
4170
|
-
const paths = Array.from(new Set(raw.filter((p) => typeof p === "string" && p.trim()).map((p) =>
|
|
4351
|
+
const paths = Array.from(new Set(raw.filter((p) => typeof p === "string" && p.trim()).map((p) => path7.resolve(p))));
|
|
4171
4352
|
if (!paths.length) throw Object.assign(new Error("filePaths must be a non-empty array of paths"), { status: 400 });
|
|
4172
4353
|
if (paths.length > MAX_MULTI_PATHS) throw Object.assign(new Error(`Too many paths (${paths.length}); max ${MAX_MULTI_PATHS}. Share a folder instead.`), { status: 400 });
|
|
4173
|
-
const missing = paths.filter((p) => !
|
|
4354
|
+
const missing = paths.filter((p) => !fs8.existsSync(p));
|
|
4174
4355
|
const bundle = reqBody.bundle !== false;
|
|
4175
4356
|
if (bundle) {
|
|
4176
4357
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[-:T]/g, "");
|
|
4177
4358
|
const base = safeArchiveBase(reqBody.bundleName, `srift-${paths.length}-items-${stamp}`);
|
|
4178
|
-
const out =
|
|
4359
|
+
const out = path7.join(TEMP_DIR, `${uuidv4()}-${base}.tar.gz`);
|
|
4179
4360
|
let packed;
|
|
4180
4361
|
try {
|
|
4181
4362
|
packed = await packPaths(paths, out, base, excludesFor(reqBody));
|
|
4182
4363
|
} catch (e) {
|
|
4183
4364
|
try {
|
|
4184
|
-
|
|
4365
|
+
fs8.unlinkSync(out);
|
|
4185
4366
|
} catch {
|
|
4186
4367
|
}
|
|
4187
4368
|
throw Object.assign(new Error(e?.message || String(e)), { status: 400 });
|
|
4188
4369
|
}
|
|
4189
|
-
const r = await shareViaRelay(out,
|
|
4370
|
+
const r = await shareViaRelay(out, fs8.statSync(out), { ...reqBody, name: `${base}.tar.gz`, _ownedPath: out }, shareEncrypt(reqBody));
|
|
4190
4371
|
if (packed.skipped.length) console.warn(`[DAEMON] bundle skipped ${packed.skipped.length} item(s): ${packed.skipped.map((x) => x.path).join(", ")}`);
|
|
4191
4372
|
return { ...r, bundle: true, files: packed.files, bytes: packed.bytes, paths: paths.length, skipped: packed.skipped };
|
|
4192
4373
|
}
|
|
@@ -4255,7 +4436,7 @@ function embeddedDispatch(method, pathname, body) {
|
|
|
4255
4436
|
}
|
|
4256
4437
|
});
|
|
4257
4438
|
}
|
|
4258
|
-
var _WebTorrentCtor, _webTorrentLoadError, SRIFT_HOME, EMBEDDED, STATE_PATH, logPath, logStream, logMessage, fmtLogArg, crypto5, PORT, DEFAULT_SIGNALER_URL, activeSignalerUrl, lastDiag, session, activeTransfers, chatHistory, pendingJoins, sseClients, wsConn, wsUrl, csrfToken, heartbeatInterval, lastHeartbeatAckAt, encryptionKey, sessionTerminated, terminationReason, activeTorrents, wtClient, pubshares, pubsharesByToken, activePulls, pubshareRegResolvers, TEMP_DIR, KDF_ITERATIONS, IV_LEN, STATE_STALE_MS, activeUploads, app, LOCAL_HOSTS, DAEMON_START_TIME, PACKAGE_VERSION, hostSessionInflight, MAX_MULTI_PATHS, MULTI_CONCURRENCY, embeddedReady, httpServer;
|
|
4439
|
+
var _WebTorrentCtor, _webTorrentLoadError, SRIFT_HOME, EMBEDDED, STATE_PATH, logPath, logStream, logMessage, fmtLogArg, crypto5, PORT, DEFAULT_SIGNALER_URL, activeSignalerUrl, lastDiag, session, activeTransfers, chatHistory, pendingJoins, participants, sseClients, wsConn, wsUrl, csrfToken, heartbeatInterval, lastHeartbeatAckAt, encryptionKey, sessionTerminated, terminationReason, activeTorrents, wtClient, pubshares, pubsharesByToken, activePulls, pubshareRegResolvers, TEMP_DIR, KDF_ITERATIONS, IV_LEN, RELAY_ENC, relayKeyCache, STATE_STALE_MS, activeUploads, app, LOCAL_HOSTS, DAEMON_START_TIME, PACKAGE_VERSION, hostSessionInflight, MAX_MULTI_PATHS, MULTI_CONCURRENCY, embeddedReady, httpServer;
|
|
4259
4440
|
var init_daemon = __esm({
|
|
4260
4441
|
"../../cli/daemon.ts"() {
|
|
4261
4442
|
init_mcp();
|
|
@@ -4266,25 +4447,25 @@ var init_daemon = __esm({
|
|
|
4266
4447
|
init_pack();
|
|
4267
4448
|
_WebTorrentCtor = null;
|
|
4268
4449
|
_webTorrentLoadError = null;
|
|
4269
|
-
SRIFT_HOME =
|
|
4450
|
+
SRIFT_HOME = path7.join(os4.homedir(), ".srift");
|
|
4270
4451
|
try {
|
|
4271
|
-
|
|
4452
|
+
fs8.mkdirSync(SRIFT_HOME, { recursive: true, mode: 448 });
|
|
4272
4453
|
try {
|
|
4273
|
-
|
|
4454
|
+
fs8.chmodSync(SRIFT_HOME, 448);
|
|
4274
4455
|
} catch {
|
|
4275
4456
|
}
|
|
4276
4457
|
} catch (e) {
|
|
4277
4458
|
}
|
|
4278
4459
|
EMBEDDED = process.env.SRIFT_DAEMON_EMBEDDED === "1";
|
|
4279
|
-
STATE_PATH =
|
|
4280
|
-
logPath =
|
|
4460
|
+
STATE_PATH = path7.join(SRIFT_HOME, EMBEDDED ? "state-embedded.json" : "state.json");
|
|
4461
|
+
logPath = path7.join(SRIFT_HOME, "daemon.log");
|
|
4281
4462
|
try {
|
|
4282
|
-
if (
|
|
4463
|
+
if (fs8.statSync(logPath).size > 5 * 1024 * 1024) fs8.renameSync(logPath, `${logPath}.1`);
|
|
4283
4464
|
} catch {
|
|
4284
4465
|
}
|
|
4285
|
-
logStream =
|
|
4466
|
+
logStream = fs8.createWriteStream(logPath, { flags: "a", mode: 384 });
|
|
4286
4467
|
try {
|
|
4287
|
-
|
|
4468
|
+
fs8.chmodSync(logPath, 384);
|
|
4288
4469
|
} catch {
|
|
4289
4470
|
}
|
|
4290
4471
|
logMessage = (level, message) => {
|
|
@@ -4331,6 +4512,7 @@ var init_daemon = __esm({
|
|
|
4331
4512
|
activeTransfers = [];
|
|
4332
4513
|
chatHistory = [];
|
|
4333
4514
|
pendingJoins = [];
|
|
4515
|
+
participants = [];
|
|
4334
4516
|
sseClients = [];
|
|
4335
4517
|
wsConn = null;
|
|
4336
4518
|
wsUrl = null;
|
|
@@ -4346,15 +4528,15 @@ var init_daemon = __esm({
|
|
|
4346
4528
|
pubsharesByToken = /* @__PURE__ */ new Map();
|
|
4347
4529
|
activePulls = /* @__PURE__ */ new Map();
|
|
4348
4530
|
pubshareRegResolvers = /* @__PURE__ */ new Map();
|
|
4349
|
-
TEMP_DIR =
|
|
4350
|
-
if (!
|
|
4351
|
-
|
|
4531
|
+
TEMP_DIR = path7.join(SRIFT_HOME, "tmp");
|
|
4532
|
+
if (!fs8.existsSync(TEMP_DIR)) {
|
|
4533
|
+
fs8.mkdirSync(TEMP_DIR, { recursive: true, mode: 448 });
|
|
4352
4534
|
}
|
|
4353
4535
|
try {
|
|
4354
|
-
for (const f of
|
|
4536
|
+
for (const f of fs8.readdirSync(TEMP_DIR)) {
|
|
4355
4537
|
if (f.endsWith(".sre1")) {
|
|
4356
4538
|
try {
|
|
4357
|
-
|
|
4539
|
+
fs8.unlinkSync(path7.join(TEMP_DIR, f));
|
|
4358
4540
|
} catch {
|
|
4359
4541
|
}
|
|
4360
4542
|
}
|
|
@@ -4363,6 +4545,8 @@ var init_daemon = __esm({
|
|
|
4363
4545
|
}
|
|
4364
4546
|
KDF_ITERATIONS = 1e5;
|
|
4365
4547
|
IV_LEN = 12;
|
|
4548
|
+
RELAY_ENC = "sgcm1";
|
|
4549
|
+
relayKeyCache = null;
|
|
4366
4550
|
STATE_STALE_MS = 24 * 60 * 60 * 1e3;
|
|
4367
4551
|
pruneStaleStateOnStartup();
|
|
4368
4552
|
activeUploads = /* @__PURE__ */ new Map();
|
|
@@ -4381,7 +4565,7 @@ var init_daemon = __esm({
|
|
|
4381
4565
|
app.use(cors({ origin: "*", maxAge: 600 }));
|
|
4382
4566
|
app.use(express.json());
|
|
4383
4567
|
DAEMON_START_TIME = Date.now();
|
|
4384
|
-
PACKAGE_VERSION = "4.
|
|
4568
|
+
PACKAGE_VERSION = "4.1.0";
|
|
4385
4569
|
app.get("/health", (req, res) => {
|
|
4386
4570
|
res.json({
|
|
4387
4571
|
ok: true,
|
|
@@ -4405,6 +4589,7 @@ var init_daemon = __esm({
|
|
|
4405
4589
|
},
|
|
4406
4590
|
activeTransfers,
|
|
4407
4591
|
pendingJoins,
|
|
4592
|
+
participants,
|
|
4408
4593
|
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
4409
4594
|
});
|
|
4410
4595
|
});
|
|
@@ -4432,7 +4617,7 @@ var init_daemon = __esm({
|
|
|
4432
4617
|
wsUrl = data.wsUrl || `${activeSignalerUrl.replace(/^http/, "ws")}/ws`;
|
|
4433
4618
|
if (wsUrl) connectWebSocket(wsUrl);
|
|
4434
4619
|
writeStateFile();
|
|
4435
|
-
res.json({ success: true, sessionId: data.sessionId });
|
|
4620
|
+
res.json({ success: true, sessionId: data.sessionId, joinUrl: `${activeSignalerUrl.replace(/\/+$/, "")}/join-session?id=${data.sessionId}` });
|
|
4436
4621
|
} catch (err) {
|
|
4437
4622
|
console.error("[DAEMON] Failed to start session:", err);
|
|
4438
4623
|
res.status(500).json({ success: false, error: err.message });
|
|
@@ -4477,7 +4662,7 @@ var init_daemon = __esm({
|
|
|
4477
4662
|
res.status(500).json({ success: false, error: err.message });
|
|
4478
4663
|
}
|
|
4479
4664
|
});
|
|
4480
|
-
app.post("/session/approve", (req, res) => {
|
|
4665
|
+
app.post("/session/approve", async (req, res) => {
|
|
4481
4666
|
const { tempUserId } = req.body;
|
|
4482
4667
|
if (!tempUserId) {
|
|
4483
4668
|
return res.status(400).json({ success: false, error: "tempUserId required" });
|
|
@@ -4495,7 +4680,13 @@ var init_daemon = __esm({
|
|
|
4495
4680
|
payload: { sessionId: session.id, tempUserId }
|
|
4496
4681
|
}));
|
|
4497
4682
|
pendingJoins = pendingJoins.filter((j) => j.tempUserId !== tempUserId);
|
|
4498
|
-
|
|
4683
|
+
const before = new Set(participants.filter((p) => p.online).map((p) => p.userId));
|
|
4684
|
+
const joined = () => participants.find((p) => p.online && !p.isHost && p.username === pending.username && !before.has(p.userId));
|
|
4685
|
+
for (let waited = 0; waited < 8e3 && !joined() && session.id; waited += 100) {
|
|
4686
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
4687
|
+
}
|
|
4688
|
+
const member = joined();
|
|
4689
|
+
res.json({ success: true, joined: !!member, userId: member?.userId ?? null, username: pending.username });
|
|
4499
4690
|
});
|
|
4500
4691
|
app.post("/session/reject", (req, res) => {
|
|
4501
4692
|
const { tempUserId, reason } = req.body;
|
|
@@ -4572,13 +4763,14 @@ var init_daemon = __esm({
|
|
|
4572
4763
|
activeTransfers = [];
|
|
4573
4764
|
chatHistory = [];
|
|
4574
4765
|
pendingJoins = [];
|
|
4766
|
+
participants = [];
|
|
4575
4767
|
csrfToken = null;
|
|
4576
4768
|
encryptionKey = null;
|
|
4577
4769
|
try {
|
|
4578
|
-
if (
|
|
4579
|
-
for (const f of
|
|
4770
|
+
if (fs8.existsSync(TEMP_DIR)) {
|
|
4771
|
+
for (const f of fs8.readdirSync(TEMP_DIR)) {
|
|
4580
4772
|
try {
|
|
4581
|
-
|
|
4773
|
+
fs8.unlinkSync(path7.join(TEMP_DIR, f));
|
|
4582
4774
|
} catch {
|
|
4583
4775
|
}
|
|
4584
4776
|
}
|
|
@@ -4594,14 +4786,16 @@ var init_daemon = __esm({
|
|
|
4594
4786
|
if (!filePath) {
|
|
4595
4787
|
return res.status(400).json({ success: false, error: "filePath is required" });
|
|
4596
4788
|
}
|
|
4597
|
-
const absPath =
|
|
4598
|
-
if (!
|
|
4789
|
+
const absPath = path7.resolve(filePath);
|
|
4790
|
+
if (!fs8.existsSync(absPath)) {
|
|
4599
4791
|
return res.status(404).json({ success: false, error: `File not found: ${absPath}` });
|
|
4600
4792
|
}
|
|
4601
|
-
const stat =
|
|
4602
|
-
const filename =
|
|
4793
|
+
const stat = fs8.statSync(absPath);
|
|
4794
|
+
const filename = path7.basename(absPath);
|
|
4603
4795
|
const fileId = `cli_file_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
4604
4796
|
const targetProtocol = webTorrentAllowed() && (protocol === "webtorrent" || stat.size > 10 * 1024 * 1024 && protocol !== "websocket") ? "webtorrent" : "websocket";
|
|
4797
|
+
const notReady = await waitSignalReady();
|
|
4798
|
+
if (notReady) return res.status(notReady.status).json(notReady.body);
|
|
4605
4799
|
const newTx = {
|
|
4606
4800
|
fileId,
|
|
4607
4801
|
name: filename,
|
|
@@ -4647,13 +4841,14 @@ var init_daemon = __esm({
|
|
|
4647
4841
|
if (!tx) {
|
|
4648
4842
|
return res.status(404).json({ success: false, error: "No offer found for this fileId" });
|
|
4649
4843
|
}
|
|
4650
|
-
tx.saveDir = saveDir ?
|
|
4844
|
+
tx.saveDir = saveDir ? path7.resolve(saveDir) : process.cwd();
|
|
4651
4845
|
tx.status = "downloading";
|
|
4652
4846
|
tx.startTime = Date.now();
|
|
4653
4847
|
console.log(`[DAEMON] Accepting file offer ${fileId}, saving to ${tx.saveDir}`);
|
|
4654
4848
|
wsConn?.send(JSON.stringify({
|
|
4655
4849
|
type: "file_accept",
|
|
4656
|
-
|
|
4850
|
+
// relayEnc: this daemon decrypts sealed relay chunks (see openRelayChunk)
|
|
4851
|
+
payload: { fileId, relayEnc: RELAY_ENC }
|
|
4657
4852
|
}));
|
|
4658
4853
|
res.json({ success: true });
|
|
4659
4854
|
});
|
|
@@ -4666,16 +4861,8 @@ var init_daemon = __esm({
|
|
|
4666
4861
|
if (!session.id) {
|
|
4667
4862
|
return res.status(409).json({ success: false, error: "No active session. Call /session/start first." });
|
|
4668
4863
|
}
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
}
|
|
4672
|
-
if (!wsConn || wsConn.readyState !== 1) {
|
|
4673
|
-
return res.status(503).json({
|
|
4674
|
-
success: false,
|
|
4675
|
-
error: "Signaling connection not ready (WebSocket is connecting). Retry in 1\u20132 seconds.",
|
|
4676
|
-
retryAfterMs: 1500
|
|
4677
|
-
});
|
|
4678
|
-
}
|
|
4864
|
+
const notReady = await waitSignalReady();
|
|
4865
|
+
if (notReady) return res.status(notReady.status).json(notReady.body);
|
|
4679
4866
|
const encContent = await encrypt(message);
|
|
4680
4867
|
wsConn.send(JSON.stringify({
|
|
4681
4868
|
type: "chat",
|
|
@@ -4698,8 +4885,8 @@ var init_daemon = __esm({
|
|
|
4698
4885
|
});
|
|
4699
4886
|
app.get("/state", (req, res) => {
|
|
4700
4887
|
try {
|
|
4701
|
-
if (
|
|
4702
|
-
const raw =
|
|
4888
|
+
if (fs8.existsSync(STATE_PATH)) {
|
|
4889
|
+
const raw = fs8.readFileSync(STATE_PATH, "utf-8");
|
|
4703
4890
|
res.type("application/json").send(raw);
|
|
4704
4891
|
} else {
|
|
4705
4892
|
res.json({ session: { id: null, isConnected: false }, activeTransfers: [], lastUpdated: null });
|
|
@@ -4779,7 +4966,7 @@ var init_daemon = __esm({
|
|
|
4779
4966
|
pubshares.delete(entry.fileId);
|
|
4780
4967
|
if (entry.tempFile) {
|
|
4781
4968
|
try {
|
|
4782
|
-
|
|
4969
|
+
fs8.unlinkSync(entry.tempFile);
|
|
4783
4970
|
} catch {
|
|
4784
4971
|
}
|
|
4785
4972
|
}
|
|
@@ -4891,7 +5078,7 @@ data: /mcp/messages
|
|
|
4891
5078
|
servers: [{ url: `http://127.0.0.1:${PORT}` }],
|
|
4892
5079
|
paths: {
|
|
4893
5080
|
"/health": { get: { summary: "Liveness probe", description: "Returns {ok,version,uptime_ms,mcp,webrtc,webtorrent}", responses: { "200": { description: "OK" } } } },
|
|
4894
|
-
"/status": { get: { summary: "Get session
|
|
5081
|
+
"/status": { get: { summary: "Get session, transfers, pending joins and participants (members with userId, for /session/kick)", responses: { "200": { description: "OK" } } } },
|
|
4895
5082
|
"/state": { get: { summary: "Workspace state snapshot (.srift-state.json)", responses: { "200": { description: "OK" } } } },
|
|
4896
5083
|
"/transfers": { get: { summary: "Live transfer list with speed and ETA. Optional ?fileId= to filter.", responses: { "200": { description: "OK" } } } },
|
|
4897
5084
|
"/transfers/{fileId}": { get: { summary: "Per-transfer drill-down", parameters: [{ name: "fileId", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "OK" }, "404": { description: "Transfer not found" } } } },
|
|
@@ -4899,9 +5086,11 @@ data: /mcp/messages
|
|
|
4899
5086
|
"/metrics": { get: { summary: "Prometheus-format counters (no auth)", responses: { "200": { description: "text/plain Prometheus format" } } } },
|
|
4900
5087
|
"/logs": { get: { summary: "NDJSON daemon log tail. ?lines=N (default 100)", responses: { "200": { description: "application/x-ndjson" } } } },
|
|
4901
5088
|
"/reset": { post: { summary: "Wipe all state and flush encryption keys", responses: { "200": { description: "{ok:true}" } } } },
|
|
4902
|
-
"/
|
|
5089
|
+
"/v1/diag": { get: { summary: "Network diagnosis (same as `srift doctor --json`); ?fresh=1 re-probes", responses: { "200": { description: "Diagnosis report" } } } },
|
|
5090
|
+
"/daemon/stop": { post: { summary: "Stop this daemon", responses: { "200": { description: "OK" } } } },
|
|
5091
|
+
"/session/start": { post: { summary: "Create new session (becomes host); returns sessionId and joinUrl", requestBody: { content: { "application/json": { schema: { type: "object", properties: { sessionName: { type: "string" }, roomSecret: { type: "string" } } } } } }, responses: { "200": { description: "{success:true,sessionId}" } } } },
|
|
4903
5092
|
"/session/join": { post: { summary: "Join existing session", requestBody: { content: { "application/json": { schema: { type: "object", required: ["sessionId"], properties: { sessionId: { type: "string" }, username: { type: "string" }, roomSecret: { type: "string" } } } } } }, responses: { "200": { description: "OK" }, "400": { description: "Missing sessionId" }, "404": { description: "Session not found" } } } },
|
|
4904
|
-
"/session/approve": { post: { summary: "Host approves a pending join request", requestBody: { content: { "application/json": { schema: { type: "object", required: ["tempUserId"], properties: { tempUserId: { type: "string" } } } } } }, responses: { "200": { description: "OK" }, "400": { description: "tempUserId required" }, "403": { description: "Not host" }, "404": { description: "No such pending request" } } } },
|
|
5093
|
+
"/session/approve": { post: { summary: "Host approves a pending join request; returns {success, joined, userId} once the guest is in the room (max 8 s)", requestBody: { content: { "application/json": { schema: { type: "object", required: ["tempUserId"], properties: { tempUserId: { type: "string" } } } } } }, responses: { "200": { description: "OK" }, "400": { description: "tempUserId required" }, "403": { description: "Not host" }, "404": { description: "No such pending request" } } } },
|
|
4905
5094
|
"/session/reject": { post: { summary: "Host rejects a pending join request", requestBody: { content: { "application/json": { schema: { type: "object", required: ["tempUserId"], properties: { tempUserId: { type: "string" }, reason: { type: "string" } } } } } }, responses: { "200": { description: "OK" }, "400": { description: "tempUserId required" }, "403": { description: "Not host" } } } },
|
|
4906
5095
|
"/session/kick": { post: { summary: "Host kicks a peer", requestBody: { content: { "application/json": { schema: { type: "object", required: ["userId"], properties: { userId: { type: "string" } } } } } }, responses: { "200": { description: "OK" }, "400": { description: "userId required" }, "403": { description: "Not host" } } } },
|
|
4907
5096
|
"/session/close": { post: { summary: "Tear down the session and flush keys", responses: { "200": { description: "OK" } } } },
|
|
@@ -4982,8 +5171,8 @@ data: /mcp/messages
|
|
|
4982
5171
|
app.get("/logs", (req, res) => {
|
|
4983
5172
|
const lines = parseInt(req.query.lines || "100", 10);
|
|
4984
5173
|
try {
|
|
4985
|
-
if (!
|
|
4986
|
-
const content =
|
|
5174
|
+
if (!fs8.existsSync(logPath)) return res.json([]);
|
|
5175
|
+
const content = fs8.readFileSync(logPath, "utf-8");
|
|
4987
5176
|
const all = content.split("\n").filter(Boolean);
|
|
4988
5177
|
const tail = all.slice(-Math.min(lines, all.length));
|
|
4989
5178
|
res.set("Content-Type", "application/x-ndjson");
|
|
@@ -5018,15 +5207,16 @@ data: /mcp/messages
|
|
|
5018
5207
|
activeTransfers = [];
|
|
5019
5208
|
chatHistory = [];
|
|
5020
5209
|
pendingJoins = [];
|
|
5210
|
+
participants = [];
|
|
5021
5211
|
csrfToken = null;
|
|
5022
5212
|
encryptionKey = null;
|
|
5023
5213
|
wsUrl = null;
|
|
5024
5214
|
let cleanedChunks = 0;
|
|
5025
5215
|
try {
|
|
5026
|
-
if (
|
|
5027
|
-
for (const f of
|
|
5216
|
+
if (fs8.existsSync(TEMP_DIR)) {
|
|
5217
|
+
for (const f of fs8.readdirSync(TEMP_DIR)) {
|
|
5028
5218
|
try {
|
|
5029
|
-
|
|
5219
|
+
fs8.unlinkSync(path7.join(TEMP_DIR, f));
|
|
5030
5220
|
cleanedChunks++;
|
|
5031
5221
|
} catch {
|
|
5032
5222
|
}
|
|
@@ -5088,16 +5278,17 @@ data: /mcp/messages
|
|
|
5088
5278
|
activeTransfers = [];
|
|
5089
5279
|
chatHistory = [];
|
|
5090
5280
|
pendingJoins = [];
|
|
5281
|
+
participants = [];
|
|
5091
5282
|
encryptionKey = null;
|
|
5092
5283
|
wsUrl = null;
|
|
5093
5284
|
writeStateFile();
|
|
5094
5285
|
} catch {
|
|
5095
5286
|
}
|
|
5096
5287
|
try {
|
|
5097
|
-
if (
|
|
5098
|
-
for (const f of
|
|
5288
|
+
if (fs8.existsSync(TEMP_DIR)) {
|
|
5289
|
+
for (const f of fs8.readdirSync(TEMP_DIR)) {
|
|
5099
5290
|
try {
|
|
5100
|
-
|
|
5291
|
+
fs8.unlinkSync(path7.join(TEMP_DIR, f));
|
|
5101
5292
|
} catch {
|
|
5102
5293
|
}
|
|
5103
5294
|
}
|
|
@@ -5112,7 +5303,7 @@ data: /mcp/messages
|
|
|
5112
5303
|
process.on("exit", () => {
|
|
5113
5304
|
for (const e of pubshares.values()) if (e.tempFile) {
|
|
5114
5305
|
try {
|
|
5115
|
-
|
|
5306
|
+
fs8.unlinkSync(e.tempFile);
|
|
5116
5307
|
} catch {
|
|
5117
5308
|
}
|
|
5118
5309
|
}
|