@nordbyte/nordrelay 0.8.0 → 0.8.2
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/.env.example +9 -0
- package/README.md +81 -1197
- package/dist/{access-control.js → access/access-control.js} +1 -1
- package/dist/{audit-log.js → access/audit-log.js} +2 -2
- package/dist/{session-locks.js → access/session-locks.js} +1 -1
- package/dist/{user-management.js → access/user-management.js} +1 -1
- package/dist/{claude-code-cli.js → agents/claude-code/claude-code-cli.js} +2 -2
- package/dist/{claude-code-session.js → agents/claude-code/claude-code-session.js} +1 -1
- package/dist/{codex-cli.js → agents/codex/codex-cli.js} +14 -5
- package/dist/{codex-session.js → agents/codex/codex-session.js} +2 -4
- package/dist/{hermes-cli.js → agents/hermes/hermes-cli.js} +2 -2
- package/dist/{hermes-launch.js → agents/hermes/hermes-launch.js} +1 -1
- package/dist/{hermes-session.js → agents/hermes/hermes-session.js} +1 -1
- package/dist/{openclaw-cli.js → agents/openclaw/openclaw-cli.js} +2 -2
- package/dist/{openclaw-launch.js → agents/openclaw/openclaw-launch.js} +1 -1
- package/dist/{openclaw-session.js → agents/openclaw/openclaw-session.js} +1 -1
- package/dist/{pi-cli.js → agents/pi/pi-cli.js} +2 -2
- package/dist/{pi-launch.js → agents/pi/pi-launch.js} +1 -1
- package/dist/{pi-session.js → agents/pi/pi-session.js} +1 -1
- package/dist/{adapter-conformance.js → agents/shared/adapter-conformance.js} +2 -2
- package/dist/{agent-activity.js → agents/shared/agent-activity.js} +5 -5
- package/dist/agents/shared/agent-auth-commands.js +30 -0
- package/dist/{agent-factory.js → agents/shared/agent-factory.js} +5 -5
- package/dist/{agent-feature-matrix.js → agents/shared/agent-feature-matrix.js} +2 -2
- package/dist/{agent-updates.js → agents/shared/agent-updates.js} +7 -7
- package/dist/{discord-artifacts.js → channels/discord/discord-artifacts.js} +4 -4
- package/dist/{discord-bot.js → channels/discord/discord-bot.js} +164 -424
- package/dist/{discord-channel-runtime.js → channels/discord/discord-channel-runtime.js} +2 -2
- package/dist/{discord-command-surface.js → channels/discord/discord-command-surface.js} +3 -3
- package/dist/{bot-rendering.js → channels/shared/bot-rendering.js} +6 -6
- package/dist/{channel-actions.js → channels/shared/channel-actions.js} +4 -4
- package/dist/channels/shared/channel-bridge-controller.js +69 -0
- package/dist/channels/shared/channel-cli-artifacts.js +51 -0
- package/dist/{channel-command-service.js → channels/shared/channel-command-service.js} +51 -28
- package/dist/channels/shared/channel-external-mirror-controller.js +193 -0
- package/dist/channels/shared/channel-external-monitor.js +52 -0
- package/dist/{channel-mirror-registry.js → channels/shared/channel-mirror-registry.js} +14 -6
- package/dist/{channel-peer-prompt.js → channels/shared/channel-peer-prompt.js} +3 -3
- package/dist/{channel-turn-service.js → channels/shared/channel-turn-service.js} +2 -2
- package/dist/{context-key.js → channels/shared/context-key.js} +1 -1
- package/dist/{session-format.js → channels/shared/session-format.js} +2 -2
- package/dist/{slack-artifacts.js → channels/slack/slack-artifacts.js} +4 -4
- package/dist/{slack-bot.js → channels/slack/slack-bot.js} +159 -294
- package/dist/{slack-channel-runtime.js → channels/slack/slack-channel-runtime.js} +2 -2
- package/dist/{slack-command-surface.js → channels/slack/slack-command-surface.js} +2 -2
- package/dist/{slack-diagnostics.js → channels/slack/slack-diagnostics.js} +2 -2
- package/dist/{bot-ui.js → channels/telegram/bot-ui.js} +1 -1
- package/dist/{bot.js → channels/telegram/bot.js} +178 -427
- package/dist/{telegram-access-commands.js → channels/telegram/telegram-access-commands.js} +3 -3
- package/dist/{telegram-access-middleware.js → channels/telegram/telegram-access-middleware.js} +4 -4
- package/dist/{telegram-agent-commands.js → channels/telegram/telegram-agent-commands.js} +9 -9
- package/dist/{telegram-artifact-commands.js → channels/telegram/telegram-artifact-commands.js} +4 -4
- package/dist/{telegram-channel-runtime.js → channels/telegram/telegram-channel-runtime.js} +2 -2
- package/dist/{telegram-command-menu.js → channels/telegram/telegram-command-menu.js} +1 -1
- package/dist/{telegram-diagnostics-command.js → channels/telegram/telegram-diagnostics-command.js} +7 -7
- package/dist/{telegram-general-commands.js → channels/telegram/telegram-general-commands.js} +4 -4
- package/dist/{telegram-operational-commands.js → channels/telegram/telegram-operational-commands.js} +5 -5
- package/dist/{telegram-output.js → channels/telegram/telegram-output.js} +2 -2
- package/dist/{telegram-preference-commands.js → channels/telegram/telegram-preference-commands.js} +3 -3
- package/dist/{telegram-queue-commands.js → channels/telegram/telegram-queue-commands.js} +6 -6
- package/dist/{telegram-support-command.js → channels/telegram/telegram-support-command.js} +4 -4
- package/dist/{telegram-update-commands.js → channels/telegram/telegram-update-commands.js} +5 -5
- package/dist/{config-metadata.js → core/config-metadata.js} +8 -0
- package/dist/{config.js → core/config.js} +11 -3
- package/dist/index.js +27 -23
- package/dist/{peer-client.js → peers/peer-client.js} +57 -1
- package/dist/peers/peer-discovery-jobs.js +206 -0
- package/dist/peers/peer-discovery.js +223 -0
- package/dist/peers/peer-health-monitor.js +49 -0
- package/dist/{peer-identity.js → peers/peer-identity.js} +50 -1
- package/dist/{peer-runtime-service.js → peers/peer-runtime-service.js} +29 -7
- package/dist/{peer-server.js → peers/peer-server.js} +23 -6
- package/dist/{peer-store.js → peers/peer-store.js} +84 -11
- package/dist/{peer-types.js → peers/peer-types.js} +9 -0
- package/dist/peers/peer-web-proxy-contract.js +127 -0
- package/dist/{metrics.js → runtime/metrics.js} +5 -3
- package/dist/{relay-artifact-service.js → runtime/relay-artifact-service.js} +1 -1
- package/dist/runtime/relay-auth-service.js +63 -0
- package/dist/runtime/relay-dashboard-service.js +139 -0
- package/dist/{relay-external-activity-monitor.js → runtime/relay-external-activity-monitor.js} +140 -53
- package/dist/runtime/relay-runtime-active-sessions.js +387 -0
- package/dist/runtime/relay-runtime-dashboard.js +201 -0
- package/dist/runtime/relay-runtime-prompt-queue-artifacts.js +307 -0
- package/dist/runtime/relay-runtime-sessions.js +623 -0
- package/dist/runtime/relay-runtime-types.js +1 -0
- package/dist/runtime/relay-runtime-updates-jobs.js +360 -0
- package/dist/runtime/relay-runtime.js +451 -0
- package/dist/runtime/runtime-cache.js +117 -0
- package/dist/{session-registry.js → state/session-registry.js} +3 -3
- package/dist/{operations.js → support/operations.js} +7 -7
- package/dist/{support-bundle.js → support/support-bundle.js} +1 -1
- package/dist/{web-api-contract.js → web/web-api-contract.js} +17 -3
- package/dist/web/web-api-types.js +1 -0
- package/dist/{web-dashboard-access-routes.js → web/web-dashboard-access-routes.js} +2 -2
- package/dist/{web-dashboard-assets.js → web/web-dashboard-assets.js} +24 -2
- package/dist/{web-dashboard-http.js → web/web-dashboard-http.js} +41 -5
- package/dist/{web-dashboard-pages.js → web/web-dashboard-pages.js} +37 -10
- package/dist/{web-dashboard-peer-routes.js → web/web-dashboard-peer-routes.js} +102 -7
- package/dist/web/web-dashboard-security.js +14 -0
- package/dist/{web-dashboard-session-routes.js → web/web-dashboard-session-routes.js} +12 -1
- package/dist/{web-dashboard.js → web/web-dashboard.js} +132 -48
- package/dist/web/web-performance.js +60 -0
- package/dist/web/web-rate-limit.js +19 -0
- package/dist/{web-state.js → web/web-state.js} +74 -5
- package/dist/webui-assets/dashboard.css +171 -10
- package/dist/webui-assets/dashboard.js +515 -48
- package/dist/webui-assets/favicon.ico +0 -0
- package/dist/webui-assets/favicon.png +0 -0
- package/dist/webui-assets/logo.png +0 -0
- package/package.json +4 -3
- package/plugins/nordrelay/scripts/nordrelay.mjs +17 -5
- package/{launchd/start.sh → scripts/launchd-start.sh} +1 -1
- package/dist/relay-runtime.js +0 -1916
- package/dist/runtime-cache.js +0 -57
- /package/dist/{user-management-crypto.js → access/user-management-crypto.js} +0 -0
- /package/dist/{user-management-normalize.js → access/user-management-normalize.js} +0 -0
- /package/dist/{user-management-types.js → access/user-management-types.js} +0 -0
- /package/dist/{claude-code-auth.js → agents/claude-code/claude-code-auth.js} +0 -0
- /package/dist/{claude-code-launch.js → agents/claude-code/claude-code-launch.js} +0 -0
- /package/dist/{claude-code-state.js → agents/claude-code/claude-code-state.js} +0 -0
- /package/dist/{codex-auth.js → agents/codex/codex-auth.js} +0 -0
- /package/dist/{codex-config.js → agents/codex/codex-config.js} +0 -0
- /package/dist/{codex-launch.js → agents/codex/codex-launch.js} +0 -0
- /package/dist/{codex-state.js → agents/codex/codex-state.js} +0 -0
- /package/dist/{hermes-api.js → agents/hermes/hermes-api.js} +0 -0
- /package/dist/{hermes-auth.js → agents/hermes/hermes-auth.js} +0 -0
- /package/dist/{hermes-state.js → agents/hermes/hermes-state.js} +0 -0
- /package/dist/{openclaw-auth.js → agents/openclaw/openclaw-auth.js} +0 -0
- /package/dist/{openclaw-gateway.js → agents/openclaw/openclaw-gateway.js} +0 -0
- /package/dist/{openclaw-state.js → agents/openclaw/openclaw-state.js} +0 -0
- /package/dist/{pi-auth.js → agents/pi/pi-auth.js} +0 -0
- /package/dist/{pi-rpc.js → agents/pi/pi-rpc.js} +0 -0
- /package/dist/{pi-state.js → agents/pi/pi-state.js} +0 -0
- /package/dist/{agent-adapter.js → agents/shared/agent-adapter.js} +0 -0
- /package/dist/{agent.js → agents/shared/agent.js} +0 -0
- /package/dist/{artifacts.js → artifacts/artifacts.js} +0 -0
- /package/dist/{attachments.js → artifacts/attachments.js} +0 -0
- /package/dist/{voice.js → artifacts/voice.js} +0 -0
- /package/dist/{discord-rate-limit.js → channels/discord/discord-rate-limit.js} +0 -0
- /package/dist/{channel-adapter.js → channels/shared/channel-adapter.js} +0 -0
- /package/dist/{relay-runtime-types.js → channels/shared/channel-bridge-state.js} +0 -0
- /package/dist/{channel-command-catalog.js → channels/shared/channel-command-catalog.js} +0 -0
- /package/dist/{channel-command-core.js → channels/shared/channel-command-core.js} +0 -0
- /package/dist/{channel-prompt-engine.js → channels/shared/channel-prompt-engine.js} +0 -0
- /package/dist/{channel-runtime.js → channels/shared/channel-runtime.js} +0 -0
- /package/dist/{channel-turn-lifecycle.js → channels/shared/channel-turn-lifecycle.js} +0 -0
- /package/dist/{slack-rate-limit.js → channels/slack/slack-rate-limit.js} +0 -0
- /package/dist/{telegram-command-types.js → channels/telegram/telegram-command-types.js} +0 -0
- /package/dist/{telegram-rate-limit.js → channels/telegram/telegram-rate-limit.js} +0 -0
- /package/dist/{activity-events.js → core/activity-events.js} +0 -0
- /package/dist/{error-messages.js → core/error-messages.js} +0 -0
- /package/dist/{format.js → core/format.js} +0 -0
- /package/dist/{logger.js → core/logger.js} +0 -0
- /package/dist/{redaction.js → core/redaction.js} +0 -0
- /package/dist/{settings-service.js → core/settings-service.js} +0 -0
- /package/dist/{settings-wizard-test.js → core/settings-wizard-test.js} +0 -0
- /package/dist/{workspace-policy.js → core/workspace-policy.js} +0 -0
- /package/dist/{peer-auth.js → peers/peer-auth.js} +0 -0
- /package/dist/{peer-context.js → peers/peer-context.js} +0 -0
- /package/dist/{peer-readiness.js → peers/peer-readiness.js} +0 -0
- /package/dist/{relay-queue-service.js → runtime/relay-queue-service.js} +0 -0
- /package/dist/{web-api-types.js → runtime/relay-runtime-delegate.js} +0 -0
- /package/dist/{relay-runtime-helpers.js → runtime/relay-runtime-helpers.js} +0 -0
- /package/dist/{remote-prompt.js → runtime/remote-prompt.js} +0 -0
- /package/dist/{bot-preferences.js → state/bot-preferences.js} +0 -0
- /package/dist/{job-store.js → state/job-store.js} +0 -0
- /package/dist/{persistence.js → state/persistence.js} +0 -0
- /package/dist/{prompt-store.js → state/prompt-store.js} +0 -0
- /package/dist/{state-backend.js → state/state-backend.js} +0 -0
- /package/dist/{zip-writer.js → support/zip-writer.js} +0 -0
- /package/dist/{web-dashboard-artifact-routes.js → web/web-dashboard-artifact-routes.js} +0 -0
- /package/dist/{web-dashboard-runtime-routes.js → web/web-dashboard-runtime-routes.js} +0 -0
- /package/dist/{web-dashboard-ui.js → web/web-dashboard-ui.js} +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { readJsonFileWithBackup, writeJsonFileAtomic } from "../state/persistence.js";
|
|
5
|
+
import { countDiscoveryTargets, discoverLanPeers } from "./peer-discovery.js";
|
|
6
|
+
const MAX_JOBS = 25;
|
|
7
|
+
const MAX_LOG_LINES = 300;
|
|
8
|
+
const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
|
|
9
|
+
export class PeerDiscoveryJobManager {
|
|
10
|
+
config;
|
|
11
|
+
jobs = new Map();
|
|
12
|
+
filePath;
|
|
13
|
+
constructor(config, home = process.env.NORDRELAY_HOME || DEFAULT_HOME) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.filePath = path.join(home, "peer-discovery-jobs.json");
|
|
16
|
+
this.load();
|
|
17
|
+
}
|
|
18
|
+
list() {
|
|
19
|
+
return [...this.jobs.values()].map((entry) => cloneJob(entry.snapshot))
|
|
20
|
+
.sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt));
|
|
21
|
+
}
|
|
22
|
+
get(id) {
|
|
23
|
+
const entry = this.jobs.get(id);
|
|
24
|
+
return entry ? cloneJob(entry.snapshot) : null;
|
|
25
|
+
}
|
|
26
|
+
log(id) {
|
|
27
|
+
return this.jobs.get(id)?.snapshot.log.join("\n") ?? "";
|
|
28
|
+
}
|
|
29
|
+
async start(input = {}) {
|
|
30
|
+
this.prune();
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const options = normalizeInput(this.config, input);
|
|
33
|
+
const id = randomUUID().replace(/-/g, "").slice(0, 12);
|
|
34
|
+
const snapshot = {
|
|
35
|
+
id,
|
|
36
|
+
status: "queued",
|
|
37
|
+
createdAt: new Date().toISOString(),
|
|
38
|
+
scanned: 0,
|
|
39
|
+
total: await countDiscoveryTargets(this.config, options).catch(() => 0),
|
|
40
|
+
candidates: [],
|
|
41
|
+
warnings: [],
|
|
42
|
+
log: [],
|
|
43
|
+
options,
|
|
44
|
+
};
|
|
45
|
+
const entry = { snapshot, controller };
|
|
46
|
+
this.jobs.set(id, entry);
|
|
47
|
+
this.append(entry, `Queued peer discovery job ${id}.`);
|
|
48
|
+
void this.run(entry).catch((error) => {
|
|
49
|
+
entry.snapshot.status = controller.signal.aborted ? "cancelled" : "failed";
|
|
50
|
+
entry.snapshot.error = error instanceof Error ? error.message : String(error);
|
|
51
|
+
entry.snapshot.completedAt = new Date().toISOString();
|
|
52
|
+
this.append(entry, entry.snapshot.error);
|
|
53
|
+
});
|
|
54
|
+
return cloneJob(snapshot);
|
|
55
|
+
}
|
|
56
|
+
cancel(id) {
|
|
57
|
+
const entry = this.jobs.get(id);
|
|
58
|
+
if (!entry)
|
|
59
|
+
return null;
|
|
60
|
+
if (entry.snapshot.status === "queued" || entry.snapshot.status === "running") {
|
|
61
|
+
entry.controller.abort();
|
|
62
|
+
entry.snapshot.status = "cancelled";
|
|
63
|
+
entry.snapshot.completedAt = new Date().toISOString();
|
|
64
|
+
this.append(entry, "Cancellation requested.");
|
|
65
|
+
}
|
|
66
|
+
return cloneJob(entry.snapshot);
|
|
67
|
+
}
|
|
68
|
+
async run(entry) {
|
|
69
|
+
entry.snapshot.status = "running";
|
|
70
|
+
entry.snapshot.startedAt = new Date().toISOString();
|
|
71
|
+
this.append(entry, `Scanning ${entry.snapshot.total} peer endpoint candidate(s).`);
|
|
72
|
+
const result = await discoverLanPeers(this.config, {
|
|
73
|
+
...entry.snapshot.options,
|
|
74
|
+
signal: entry.controller.signal,
|
|
75
|
+
onProgress: (progress) => {
|
|
76
|
+
entry.snapshot.scanned = progress.scanned;
|
|
77
|
+
if (progress.candidate) {
|
|
78
|
+
entry.snapshot.candidates = mergeCandidates(entry.snapshot.candidates, progress.candidate);
|
|
79
|
+
this.append(entry, `Found ${progress.candidate.name || progress.candidate.host} at ${progress.candidate.url}.`);
|
|
80
|
+
}
|
|
81
|
+
else if (progress.scanned % 25 === 0 || progress.scanned === entry.snapshot.total) {
|
|
82
|
+
this.append(entry, `Scanned ${progress.scanned}/${progress.total}.`);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
entry.snapshot.scanned = result.scanned;
|
|
87
|
+
entry.snapshot.candidates = result.candidates;
|
|
88
|
+
entry.snapshot.warnings = result.warnings;
|
|
89
|
+
entry.snapshot.status = entry.controller.signal.aborted ? "cancelled" : "completed";
|
|
90
|
+
entry.snapshot.completedAt = new Date().toISOString();
|
|
91
|
+
this.append(entry, `${entry.snapshot.status === "completed" ? "Completed" : "Cancelled"} with ${result.candidates.length} candidate(s).`);
|
|
92
|
+
for (const warning of result.warnings) {
|
|
93
|
+
this.append(entry, `Warning: ${warning}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
append(entry, line) {
|
|
97
|
+
entry.snapshot.log.push(`[${new Date().toLocaleString()}] ${line}`);
|
|
98
|
+
if (entry.snapshot.log.length > MAX_LOG_LINES) {
|
|
99
|
+
entry.snapshot.log.splice(0, entry.snapshot.log.length - MAX_LOG_LINES);
|
|
100
|
+
}
|
|
101
|
+
this.save();
|
|
102
|
+
}
|
|
103
|
+
prune() {
|
|
104
|
+
const completed = this.list()
|
|
105
|
+
.filter((job) => job.status !== "running" && job.status !== "queued")
|
|
106
|
+
.slice(MAX_JOBS);
|
|
107
|
+
for (const job of completed) {
|
|
108
|
+
this.jobs.delete(job.id);
|
|
109
|
+
}
|
|
110
|
+
this.save();
|
|
111
|
+
}
|
|
112
|
+
load() {
|
|
113
|
+
const result = readJsonFileWithBackup(this.filePath);
|
|
114
|
+
const jobs = Array.isArray(result.value?.jobs) ? result.value.jobs : [];
|
|
115
|
+
let changed = false;
|
|
116
|
+
for (const job of jobs) {
|
|
117
|
+
const snapshot = normalizePersistedJob(job);
|
|
118
|
+
if (!snapshot)
|
|
119
|
+
continue;
|
|
120
|
+
if (snapshot.status === "queued" || snapshot.status === "running") {
|
|
121
|
+
snapshot.status = "failed";
|
|
122
|
+
snapshot.completedAt = new Date().toISOString();
|
|
123
|
+
snapshot.error = "Discovery job was interrupted by a NordRelay restart.";
|
|
124
|
+
snapshot.log = [
|
|
125
|
+
...snapshot.log,
|
|
126
|
+
`[${new Date().toLocaleString()}] Discovery job was interrupted by a NordRelay restart.`,
|
|
127
|
+
].slice(-MAX_LOG_LINES);
|
|
128
|
+
changed = true;
|
|
129
|
+
}
|
|
130
|
+
this.jobs.set(snapshot.id, { snapshot, controller: new AbortController() });
|
|
131
|
+
}
|
|
132
|
+
this.prune();
|
|
133
|
+
if (changed) {
|
|
134
|
+
this.save();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
save() {
|
|
138
|
+
const jobs = this.list().slice(0, MAX_JOBS);
|
|
139
|
+
writeJsonFileAtomic(this.filePath, { version: 1, jobs });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function normalizePersistedJob(value) {
|
|
143
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
144
|
+
return null;
|
|
145
|
+
const record = value;
|
|
146
|
+
if (typeof record.id !== "string" || typeof record.createdAt !== "string")
|
|
147
|
+
return null;
|
|
148
|
+
const status = typeof record.status === "string" && ["queued", "running", "completed", "failed", "cancelled"].includes(record.status)
|
|
149
|
+
? record.status
|
|
150
|
+
: "failed";
|
|
151
|
+
const optionsRecord = record.options && typeof record.options === "object" && !Array.isArray(record.options)
|
|
152
|
+
? record.options
|
|
153
|
+
: {};
|
|
154
|
+
return {
|
|
155
|
+
id: record.id,
|
|
156
|
+
status,
|
|
157
|
+
createdAt: record.createdAt,
|
|
158
|
+
startedAt: typeof record.startedAt === "string" ? record.startedAt : undefined,
|
|
159
|
+
completedAt: typeof record.completedAt === "string" ? record.completedAt : undefined,
|
|
160
|
+
scanned: integerField(record.scanned),
|
|
161
|
+
total: integerField(record.total),
|
|
162
|
+
candidates: Array.isArray(record.candidates) ? record.candidates : [],
|
|
163
|
+
warnings: Array.isArray(record.warnings) ? record.warnings.filter((item) => typeof item === "string") : [],
|
|
164
|
+
log: Array.isArray(record.log) ? record.log.filter((item) => typeof item === "string").slice(-MAX_LOG_LINES) : [],
|
|
165
|
+
error: typeof record.error === "string" ? record.error : undefined,
|
|
166
|
+
options: {
|
|
167
|
+
targets: Array.isArray(optionsRecord.targets) ? optionsRecord.targets.filter((item) => typeof item === "string") : [],
|
|
168
|
+
timeoutMs: integerField(optionsRecord.timeoutMs),
|
|
169
|
+
concurrency: integerField(optionsRecord.concurrency),
|
|
170
|
+
maxHosts: integerField(optionsRecord.maxHosts),
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function integerField(value) {
|
|
175
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
176
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
177
|
+
}
|
|
178
|
+
function normalizeInput(config, input) {
|
|
179
|
+
return {
|
|
180
|
+
targets: (input.targets ?? []).map((target) => target.trim()).filter(Boolean),
|
|
181
|
+
timeoutMs: clampInteger(input.timeoutMs, config.peerDiscoveryTimeoutMs, 100, 30_000),
|
|
182
|
+
concurrency: clampInteger(input.concurrency, 32, 1, 128),
|
|
183
|
+
maxHosts: clampInteger(input.maxHosts, 512, 1, 65_536),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function clampInteger(value, fallback, min, max) {
|
|
187
|
+
const parsed = Number(value);
|
|
188
|
+
return Number.isInteger(parsed) ? Math.max(min, Math.min(max, parsed)) : fallback;
|
|
189
|
+
}
|
|
190
|
+
function mergeCandidates(existing, candidate) {
|
|
191
|
+
const byNode = new Map(existing.map((item) => [item.nodeId, item]));
|
|
192
|
+
byNode.set(candidate.nodeId, candidate);
|
|
193
|
+
return [...byNode.values()].sort((left, right) => (left.name || left.host).localeCompare(right.name || right.host));
|
|
194
|
+
}
|
|
195
|
+
function cloneJob(job) {
|
|
196
|
+
return {
|
|
197
|
+
...job,
|
|
198
|
+
candidates: job.candidates.map((candidate) => ({ ...candidate })),
|
|
199
|
+
warnings: [...job.warnings],
|
|
200
|
+
log: [...job.log],
|
|
201
|
+
options: {
|
|
202
|
+
...job.options,
|
|
203
|
+
targets: [...job.options.targets],
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import dns from "node:dns/promises";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { checkPeerIdentityEndpoint } from "./peer-client.js";
|
|
5
|
+
export async function discoverLanPeers(config, options = {}) {
|
|
6
|
+
const warnings = [];
|
|
7
|
+
const targets = await buildDiscoveryTargets(config, options.maxHosts ?? 512, warnings, options.targets ?? []);
|
|
8
|
+
const candidates = [];
|
|
9
|
+
const concurrency = Math.max(1, Math.min(options.concurrency ?? 32, 128));
|
|
10
|
+
let index = 0;
|
|
11
|
+
let scanned = 0;
|
|
12
|
+
async function worker() {
|
|
13
|
+
while (index < targets.length) {
|
|
14
|
+
if (options.signal?.aborted) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const target = targets[index++];
|
|
18
|
+
const startedAt = Date.now();
|
|
19
|
+
const probe = await checkPeerIdentityEndpoint(target.url, { timeoutMs: options.timeoutMs ?? config.peerDiscoveryTimeoutMs });
|
|
20
|
+
scanned += 1;
|
|
21
|
+
if (!probe.ok || !probe.identity) {
|
|
22
|
+
options.onProgress?.({ scanned, total: targets.length, target: target.url });
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const candidate = {
|
|
26
|
+
url: target.url,
|
|
27
|
+
host: target.host,
|
|
28
|
+
port: target.port,
|
|
29
|
+
scheme: target.scheme,
|
|
30
|
+
nodeId: probe.identity.nodeId,
|
|
31
|
+
name: probe.identity.name,
|
|
32
|
+
fingerprint: probe.identity.fingerprint,
|
|
33
|
+
tlsFingerprint: probe.tlsFingerprint,
|
|
34
|
+
latencyMs: probe.latencyMs ?? Date.now() - startedAt,
|
|
35
|
+
};
|
|
36
|
+
candidates.push(candidate);
|
|
37
|
+
options.onProgress?.({ scanned, total: targets.length, candidate, target: target.url });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, targets.length) }, () => worker()));
|
|
41
|
+
return {
|
|
42
|
+
scanned,
|
|
43
|
+
candidates: dedupeCandidates(candidates),
|
|
44
|
+
warnings: options.signal?.aborted ? [...warnings, "Discovery was cancelled."] : warnings,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export async function countDiscoveryTargets(config, options = {}) {
|
|
48
|
+
return (await buildDiscoveryTargets(config, options.maxHosts ?? 512, [], options.targets ?? [])).length;
|
|
49
|
+
}
|
|
50
|
+
async function buildDiscoveryTargets(config, maxHosts, warnings, requestedTargets) {
|
|
51
|
+
const schemes = config.peerTlsEnabled ? ["https"] : ["http", "https"];
|
|
52
|
+
const explicitTargets = await customDiscoveryTargets(requestedTargets, config.peerPort, schemes, maxHosts, warnings);
|
|
53
|
+
if (explicitTargets.length > 0) {
|
|
54
|
+
return dedupeTargets(explicitTargets);
|
|
55
|
+
}
|
|
56
|
+
const targets = [];
|
|
57
|
+
const hosts = localSubnetHosts(maxHosts, warnings);
|
|
58
|
+
const mdnsHosts = await mdnsCandidateHosts(warnings);
|
|
59
|
+
if (hosts.length === 0 && mdnsHosts.length === 0) {
|
|
60
|
+
warnings.push("No private IPv4 LAN interface was found for peer discovery.");
|
|
61
|
+
}
|
|
62
|
+
for (const host of [...hosts, ...mdnsHosts]) {
|
|
63
|
+
for (const scheme of schemes) {
|
|
64
|
+
targets.push({ host, scheme, port: config.peerPort, url: formatDiscoveryUrl(scheme, host, config.peerPort) });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return dedupeTargets(targets);
|
|
68
|
+
}
|
|
69
|
+
async function customDiscoveryTargets(requested, port, schemes, maxHosts, warnings) {
|
|
70
|
+
const targets = [];
|
|
71
|
+
for (const raw of requested.flatMap((value) => value.split(/[\n, ]/)).map((value) => value.trim()).filter(Boolean)) {
|
|
72
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
73
|
+
try {
|
|
74
|
+
const url = new URL(raw);
|
|
75
|
+
const scheme = url.protocol === "http:" ? "http" : "https";
|
|
76
|
+
const targetPort = Number(url.port || port);
|
|
77
|
+
targets.push({ host: url.hostname, scheme, port: targetPort, url: formatDiscoveryUrl(scheme, url.hostname, targetPort) });
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
warnings.push(`Ignored invalid discovery URL: ${raw}`);
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
for (const host of expandHostPattern(raw, maxHosts, warnings)) {
|
|
85
|
+
for (const scheme of schemes) {
|
|
86
|
+
targets.push({ host, scheme, port, url: formatDiscoveryUrl(scheme, host, port) });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return targets;
|
|
91
|
+
}
|
|
92
|
+
function expandHostPattern(raw, maxHosts, warnings) {
|
|
93
|
+
if (raw.includes("/")) {
|
|
94
|
+
return expandIpv4Cidr(raw, maxHosts, warnings);
|
|
95
|
+
}
|
|
96
|
+
const range = raw.match(/^(\d+\.\d+\.\d+\.)(\d+)-(\d+)$/);
|
|
97
|
+
if (range) {
|
|
98
|
+
const start = Number(range[2]);
|
|
99
|
+
const end = Number(range[3]);
|
|
100
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end > 255 || start > end) {
|
|
101
|
+
warnings.push(`Ignored invalid IPv4 range: ${raw}`);
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
return Array.from({ length: Math.min(maxHosts, end - start + 1) }, (_, index) => `${range[1]}${start + index}`);
|
|
105
|
+
}
|
|
106
|
+
if (net.isIP(raw) || /^[a-z0-9_.-]+$/i.test(raw)) {
|
|
107
|
+
return [raw];
|
|
108
|
+
}
|
|
109
|
+
warnings.push(`Ignored invalid discovery target: ${raw}`);
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
function expandIpv4Cidr(raw, maxHosts, warnings) {
|
|
113
|
+
const [address, prefixText] = raw.split("/");
|
|
114
|
+
const prefix = Number(prefixText);
|
|
115
|
+
if (net.isIP(address) !== 4 || !Number.isInteger(prefix) || prefix < 16 || prefix > 32) {
|
|
116
|
+
warnings.push(`Ignored unsupported discovery CIDR: ${raw}. Use IPv4 /16 through /32.`);
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const base = ipv4ToNumber(address);
|
|
120
|
+
const hostBits = 32 - prefix;
|
|
121
|
+
const mask = hostBits === 32 ? 0 : (0xffffffff << hostBits) >>> 0;
|
|
122
|
+
const network = base & mask;
|
|
123
|
+
const total = prefix === 32 ? 1 : Math.max(0, (2 ** hostBits) - 2);
|
|
124
|
+
const count = Math.min(total, maxHosts);
|
|
125
|
+
if (total > maxHosts) {
|
|
126
|
+
warnings.push(`CIDR ${raw} was limited to ${maxHosts} host candidates.`);
|
|
127
|
+
}
|
|
128
|
+
return Array.from({ length: count }, (_, index) => numberToIpv4(network + (prefix === 32 ? index : index + 1)));
|
|
129
|
+
}
|
|
130
|
+
async function mdnsCandidateHosts(warnings) {
|
|
131
|
+
const names = [`${os.hostname()}.local`, "nordrelay.local"];
|
|
132
|
+
const found = [];
|
|
133
|
+
for (const name of names) {
|
|
134
|
+
try {
|
|
135
|
+
await withTimeout(dns.lookup(name), 250);
|
|
136
|
+
found.push(name);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// mDNS support depends on the host resolver; absence is normal.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return found;
|
|
143
|
+
}
|
|
144
|
+
async function withTimeout(promise, timeoutMs) {
|
|
145
|
+
let timeout;
|
|
146
|
+
try {
|
|
147
|
+
return await Promise.race([
|
|
148
|
+
promise,
|
|
149
|
+
new Promise((_, reject) => {
|
|
150
|
+
timeout = setTimeout(() => reject(new Error("mDNS lookup timed out.")), timeoutMs);
|
|
151
|
+
timeout.unref?.();
|
|
152
|
+
}),
|
|
153
|
+
]);
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
if (timeout)
|
|
157
|
+
clearTimeout(timeout);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function formatDiscoveryUrl(scheme, host, port) {
|
|
161
|
+
const displayHost = net.isIP(host) === 6 && !host.startsWith("[") ? `[${host}]` : host;
|
|
162
|
+
return `${scheme}://${displayHost}:${port}`;
|
|
163
|
+
}
|
|
164
|
+
function dedupeTargets(targets) {
|
|
165
|
+
const seen = new Set();
|
|
166
|
+
return targets.filter((target) => {
|
|
167
|
+
const key = target.url.toLowerCase();
|
|
168
|
+
if (seen.has(key))
|
|
169
|
+
return false;
|
|
170
|
+
seen.add(key);
|
|
171
|
+
return true;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function localSubnetHosts(maxHosts, warnings) {
|
|
175
|
+
const interfaces = os.networkInterfaces();
|
|
176
|
+
const hosts = new Set();
|
|
177
|
+
for (const items of Object.values(interfaces)) {
|
|
178
|
+
for (const item of items ?? []) {
|
|
179
|
+
if (item.family !== "IPv4" || item.internal || !isPrivateIPv4(item.address)) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const parts = item.address.split(".").map(Number);
|
|
183
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part))) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const prefix = parts.slice(0, 3).join(".");
|
|
187
|
+
for (let last = 1; last <= 254; last += 1) {
|
|
188
|
+
const host = `${prefix}.${last}`;
|
|
189
|
+
if (host !== item.address) {
|
|
190
|
+
hosts.add(host);
|
|
191
|
+
}
|
|
192
|
+
if (hosts.size >= maxHosts) {
|
|
193
|
+
warnings.push(`LAN discovery was limited to ${maxHosts} host candidates.`);
|
|
194
|
+
return [...hosts];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return [...hosts];
|
|
200
|
+
}
|
|
201
|
+
function isPrivateIPv4(address) {
|
|
202
|
+
const [a, b] = address.split(".").map(Number);
|
|
203
|
+
return a === 10 ||
|
|
204
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
205
|
+
(a === 192 && b === 168) ||
|
|
206
|
+
(a === 169 && b === 254);
|
|
207
|
+
}
|
|
208
|
+
function ipv4ToNumber(address) {
|
|
209
|
+
return address.split(".").map(Number).reduce((sum, part) => ((sum << 8) + part) >>> 0, 0);
|
|
210
|
+
}
|
|
211
|
+
function numberToIpv4(value) {
|
|
212
|
+
return [24, 16, 8, 0].map((shift) => (value >>> shift) & 255).join(".");
|
|
213
|
+
}
|
|
214
|
+
function dedupeCandidates(candidates) {
|
|
215
|
+
const byNode = new Map();
|
|
216
|
+
for (const candidate of candidates) {
|
|
217
|
+
const existing = byNode.get(candidate.nodeId);
|
|
218
|
+
if (!existing || (candidate.latencyMs ?? Number.MAX_SAFE_INTEGER) < (existing.latencyMs ?? Number.MAX_SAFE_INTEGER)) {
|
|
219
|
+
byNode.set(candidate.nodeId, candidate);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return [...byNode.values()].sort((a, b) => (a.name || a.host).localeCompare(b.name || b.host));
|
|
223
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { RemoteRelayClient } from "./peer-client.js";
|
|
2
|
+
import { PeerStore } from "./peer-store.js";
|
|
3
|
+
export function startPeerHealthMonitor(options) {
|
|
4
|
+
const store = new PeerStore(options.home);
|
|
5
|
+
const client = new RemoteRelayClient(store);
|
|
6
|
+
let running = false;
|
|
7
|
+
let timer;
|
|
8
|
+
async function checkNow() {
|
|
9
|
+
if (running) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
running = true;
|
|
13
|
+
try {
|
|
14
|
+
const peers = store.list().filter((peer) => peer.enabled && peer.url);
|
|
15
|
+
await Promise.all(peers.map(async (peer) => {
|
|
16
|
+
try {
|
|
17
|
+
const startedAt = Date.now();
|
|
18
|
+
const result = await client.rpc(peer.id, "peer.ping");
|
|
19
|
+
const record = result && typeof result === "object" ? result : {};
|
|
20
|
+
store.markSeen(peer.id, {
|
|
21
|
+
latencyMs: Date.now() - startedAt,
|
|
22
|
+
remoteVersion: typeof record.version === "string" ? record.version : undefined,
|
|
23
|
+
remoteStatus: typeof record.status === "string" ? record.status : "online",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
store.markError(peer.id, error instanceof Error ? error.message : String(error));
|
|
28
|
+
}
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
running = false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (options.config.peerHealthCheckMs > 0) {
|
|
36
|
+
timer = setInterval(() => void checkNow().catch(() => { }), options.config.peerHealthCheckMs);
|
|
37
|
+
timer.unref?.();
|
|
38
|
+
setTimeout(() => void checkNow().catch(() => { }), 2_000).unref?.();
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
checkNow,
|
|
42
|
+
close() {
|
|
43
|
+
if (timer) {
|
|
44
|
+
clearInterval(timer);
|
|
45
|
+
timer = undefined;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -3,7 +3,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import selfsigned from "selfsigned";
|
|
6
|
-
import { readJsonFileWithBackup, writeJsonFileAtomic, writeTextFileAtomic } from "
|
|
6
|
+
import { readJsonFileWithBackup, writeJsonFileAtomic, writeTextFileAtomic } from "../state/persistence.js";
|
|
7
7
|
const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
|
|
8
8
|
export function loadOrCreatePeerIdentity(home = process.env.NORDRELAY_HOME || DEFAULT_HOME, name) {
|
|
9
9
|
const filePath = path.join(home, "identity.json");
|
|
@@ -48,6 +48,55 @@ export function loadOrCreatePeerIdentity(home = process.env.NORDRELAY_HOME || DE
|
|
|
48
48
|
privateKey: identity.privateKey,
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
|
+
export function exportPeerIdentityBackup(home = process.env.NORDRELAY_HOME || DEFAULT_HOME, name) {
|
|
52
|
+
const identity = loadOrCreatePeerIdentity(home, name);
|
|
53
|
+
const tls = existsSync(path.join(home, "tls", "peer.crt"))
|
|
54
|
+
? ensurePeerTlsFiles(home, identity.public)
|
|
55
|
+
: undefined;
|
|
56
|
+
return {
|
|
57
|
+
version: 1,
|
|
58
|
+
exportedAt: new Date().toISOString(),
|
|
59
|
+
identity: identity.public,
|
|
60
|
+
privateKey: identity.privateKey,
|
|
61
|
+
tlsFingerprint: tls?.fingerprint,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function restorePeerIdentityBackup(backup, home = process.env.NORDRELAY_HOME || DEFAULT_HOME) {
|
|
65
|
+
if (!backup || backup.version !== 1 || !backup.identity?.publicKey || !backup.privateKey) {
|
|
66
|
+
throw new Error("Invalid peer identity backup.");
|
|
67
|
+
}
|
|
68
|
+
const fingerprint = fingerprintForPublicKey(backup.identity.publicKey);
|
|
69
|
+
if (fingerprint !== backup.identity.fingerprint) {
|
|
70
|
+
throw new Error("Peer identity backup fingerprint does not match the public key.");
|
|
71
|
+
}
|
|
72
|
+
const probe = `nordrelay-identity-restore:${Date.now()}`;
|
|
73
|
+
const signature = signPeerPayload(backup.privateKey, probe);
|
|
74
|
+
if (!verifyPeerPayload(backup.identity.publicKey, probe, signature)) {
|
|
75
|
+
throw new Error("Peer identity backup private key does not match the public key.");
|
|
76
|
+
}
|
|
77
|
+
const payload = {
|
|
78
|
+
nodeId: backup.identity.nodeId,
|
|
79
|
+
name: backup.identity.name || defaultNodeName(),
|
|
80
|
+
publicKey: backup.identity.publicKey,
|
|
81
|
+
privateKey: backup.privateKey,
|
|
82
|
+
fingerprint,
|
|
83
|
+
createdAt: backup.identity.createdAt || new Date().toISOString(),
|
|
84
|
+
};
|
|
85
|
+
const filePath = path.join(home, "identity.json");
|
|
86
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
87
|
+
writeJsonFileAtomic(filePath, payload);
|
|
88
|
+
chmodSync(filePath, 0o600);
|
|
89
|
+
return {
|
|
90
|
+
public: {
|
|
91
|
+
nodeId: payload.nodeId,
|
|
92
|
+
name: payload.name,
|
|
93
|
+
publicKey: payload.publicKey,
|
|
94
|
+
fingerprint: payload.fingerprint,
|
|
95
|
+
createdAt: payload.createdAt,
|
|
96
|
+
},
|
|
97
|
+
privateKey: payload.privateKey,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
51
100
|
export function ensurePeerTlsFiles(home = process.env.NORDRELAY_HOME || DEFAULT_HOME, identity) {
|
|
52
101
|
const certDir = path.join(home, "tls");
|
|
53
102
|
const certPath = path.join(certDir, "peer.crt");
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { enabledAgents } from "
|
|
3
|
-
import { listAgentAdapterDescriptors } from "
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
2
|
+
import { enabledAgents } from "../agents/shared/agent-factory.js";
|
|
3
|
+
import { listAgentAdapterDescriptors } from "../agents/shared/agent-adapter.js";
|
|
4
|
+
import { buildAdapterConformanceMatrix } from "../agents/shared/adapter-conformance.js";
|
|
5
|
+
import { isAgentId } from "../agents/shared/agent.js";
|
|
6
|
+
import { permissionForWebRequest } from "../access/access-control.js";
|
|
7
|
+
import { listChannelDescriptors } from "../channels/shared/channel-adapter.js";
|
|
8
|
+
import { friendlyErrorText } from "../core/error-messages.js";
|
|
9
|
+
import { getPackageVersion } from "../support/operations.js";
|
|
9
10
|
import { checkPeerEndpoint } from "./peer-client.js";
|
|
10
11
|
export class PeerRuntimeService {
|
|
11
12
|
config;
|
|
@@ -147,6 +148,12 @@ export class PeerRuntimeService {
|
|
|
147
148
|
if (method === "GET" && path === "/api/adapters/health") {
|
|
148
149
|
return { adapters: (await runtime.adapterHealth()).filter((adapter) => this.canUseAgent(peer, adapter.id)) };
|
|
149
150
|
}
|
|
151
|
+
if (method === "GET" && path === "/api/adapters/conformance") {
|
|
152
|
+
return buildAdapterConformanceMatrix({
|
|
153
|
+
agents: listAgentAdapterDescriptors().filter((adapter) => this.canUseAgent(peer, adapter.id)),
|
|
154
|
+
channels: listChannelDescriptors(),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
150
157
|
if (method === "GET" && path === "/api/diagnostics")
|
|
151
158
|
return this.scopedDiagnostics(peer, await runtime.diagnostics());
|
|
152
159
|
if (method === "GET" && path === "/api/diagnostics/bundle") {
|
|
@@ -163,6 +170,13 @@ export class PeerRuntimeService {
|
|
|
163
170
|
this.assertAgentScope(peer, agentId);
|
|
164
171
|
return this.scopedControlOptions(peer, await runtime.controlOptions(agentId));
|
|
165
172
|
}
|
|
173
|
+
if (method === "GET" && path === "/api/locks")
|
|
174
|
+
return { locks: runtime.locks() };
|
|
175
|
+
if (method === "POST" && path === "/api/locks") {
|
|
176
|
+
return { lock: runtime.lockWebSession(stringValue(body.ownerName) || `Peer ${peer.name}`, remoteActor), locks: runtime.locks() };
|
|
177
|
+
}
|
|
178
|
+
if (method === "DELETE" && path === "/api/locks")
|
|
179
|
+
return runtime.unlockWebSession(remoteActor);
|
|
166
180
|
if (method === "GET" && path === "/api/auth/status") {
|
|
167
181
|
const agentId = parseAgentId(query.agent);
|
|
168
182
|
this.assertAgentScope(peer, agentId);
|
|
@@ -281,6 +295,14 @@ export class PeerRuntimeService {
|
|
|
281
295
|
await this.assertCurrentSessionScope(peer, runtime);
|
|
282
296
|
return { messages: await runtime.chatHistory(numberValue(query.limit, 200)) };
|
|
283
297
|
}
|
|
298
|
+
if (method === "GET" && path === "/api/chat/mirror") {
|
|
299
|
+
await this.assertCurrentSessionScope(peer, runtime);
|
|
300
|
+
return runtime.webMirrorPreference("");
|
|
301
|
+
}
|
|
302
|
+
if (method === "POST" && path === "/api/chat/mirror") {
|
|
303
|
+
await this.assertCurrentSessionScope(peer, runtime);
|
|
304
|
+
return runtime.webMirrorPreference(stringValue(body.argument) || stringValue(body.mode) || "", remoteActor);
|
|
305
|
+
}
|
|
284
306
|
if (method === "DELETE" && path === "/api/chat/history") {
|
|
285
307
|
await this.assertCurrentSessionScope(peer, runtime);
|
|
286
308
|
return runtime.clearChatHistory(remoteActor);
|