@nextclaw/server 0.6.13 → 0.8.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/dist/index.d.ts +80 -1
- package/dist/index.js +579 -38
- package/package.json +6 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as NextclawCore from '@nextclaw/core';
|
|
2
2
|
import { ThinkingLevel, CronService, Config, ConfigActionExecuteRequest as ConfigActionExecuteRequest$1, ConfigActionExecuteResult as ConfigActionExecuteResult$1 } from '@nextclaw/core';
|
|
3
|
+
import { NcpAgentClientEndpoint, NcpSessionApi, NcpSessionSummary, NcpMessage } from '@nextclaw/ncp';
|
|
4
|
+
import { NcpHttpAgentStreamProvider } from '@nextclaw/ncp-http-agent-server';
|
|
3
5
|
import { Hono } from 'hono';
|
|
6
|
+
import { IncomingMessage } from 'node:http';
|
|
4
7
|
|
|
5
8
|
type ApiError = {
|
|
6
9
|
code: string;
|
|
@@ -133,6 +136,26 @@ type ProviderAuthImportResult = {
|
|
|
133
136
|
source: "cli";
|
|
134
137
|
expiresAt?: string;
|
|
135
138
|
};
|
|
139
|
+
type AuthStatusView = {
|
|
140
|
+
enabled: boolean;
|
|
141
|
+
configured: boolean;
|
|
142
|
+
authenticated: boolean;
|
|
143
|
+
username?: string;
|
|
144
|
+
};
|
|
145
|
+
type AuthSetupRequest = {
|
|
146
|
+
username: string;
|
|
147
|
+
password: string;
|
|
148
|
+
};
|
|
149
|
+
type AuthLoginRequest = {
|
|
150
|
+
username: string;
|
|
151
|
+
password: string;
|
|
152
|
+
};
|
|
153
|
+
type AuthPasswordUpdateRequest = {
|
|
154
|
+
password: string;
|
|
155
|
+
};
|
|
156
|
+
type AuthEnabledUpdateRequest = {
|
|
157
|
+
enabled: boolean;
|
|
158
|
+
};
|
|
136
159
|
type AgentProfileView = {
|
|
137
160
|
id: string;
|
|
138
161
|
default?: boolean;
|
|
@@ -429,6 +452,21 @@ type UiChatRuntime = {
|
|
|
429
452
|
listSessionTypes?: () => Promise<ChatSessionTypesView> | ChatSessionTypesView;
|
|
430
453
|
stopTurn?: (params: ChatTurnStopRequest) => Promise<ChatTurnStopResult> | ChatTurnStopResult;
|
|
431
454
|
};
|
|
455
|
+
type UiNcpSessionListView = {
|
|
456
|
+
sessions: NcpSessionSummary[];
|
|
457
|
+
total: number;
|
|
458
|
+
};
|
|
459
|
+
type UiNcpSessionMessagesView = {
|
|
460
|
+
sessionId: string;
|
|
461
|
+
messages: NcpMessage[];
|
|
462
|
+
total: number;
|
|
463
|
+
};
|
|
464
|
+
type UiNcpAgent = {
|
|
465
|
+
agentClientEndpoint: NcpAgentClientEndpoint;
|
|
466
|
+
streamProvider?: NcpHttpAgentStreamProvider;
|
|
467
|
+
sessionApi?: NcpSessionApi;
|
|
468
|
+
basePath?: string;
|
|
469
|
+
};
|
|
432
470
|
type ConfigView = {
|
|
433
471
|
agents: {
|
|
434
472
|
defaults: {
|
|
@@ -810,6 +848,7 @@ type UiServerOptions = {
|
|
|
810
848
|
marketplace?: MarketplaceApiConfig;
|
|
811
849
|
cronService?: CronService;
|
|
812
850
|
chatRuntime?: UiChatRuntime;
|
|
851
|
+
ncpAgent?: UiNcpAgent;
|
|
813
852
|
};
|
|
814
853
|
type UiServerHandle = {
|
|
815
854
|
host: string;
|
|
@@ -820,6 +859,44 @@ type UiServerHandle = {
|
|
|
820
859
|
|
|
821
860
|
declare function startUiServer(options: UiServerOptions): UiServerHandle;
|
|
822
861
|
|
|
862
|
+
declare class UiAuthService {
|
|
863
|
+
private readonly configPath;
|
|
864
|
+
private readonly sessions;
|
|
865
|
+
constructor(configPath: string);
|
|
866
|
+
private loadCurrentConfig;
|
|
867
|
+
private saveCurrentConfig;
|
|
868
|
+
private readAuthConfig;
|
|
869
|
+
private isConfigured;
|
|
870
|
+
isProtectionEnabled(): boolean;
|
|
871
|
+
private getSessionIdFromCookieHeader;
|
|
872
|
+
private getValidSession;
|
|
873
|
+
isRequestAuthenticated(request: Request): boolean;
|
|
874
|
+
isSocketAuthenticated(request: IncomingMessage): boolean;
|
|
875
|
+
getStatus(request: Request): AuthStatusView;
|
|
876
|
+
private createSession;
|
|
877
|
+
private clearAllSessions;
|
|
878
|
+
private deleteRequestSession;
|
|
879
|
+
private buildLoginCookie;
|
|
880
|
+
buildLogoutCookie(request: Request): string;
|
|
881
|
+
setup(request: Request, payload: AuthSetupRequest): {
|
|
882
|
+
status: AuthStatusView;
|
|
883
|
+
cookie: string;
|
|
884
|
+
};
|
|
885
|
+
login(request: Request, payload: AuthLoginRequest): {
|
|
886
|
+
status: AuthStatusView;
|
|
887
|
+
cookie: string;
|
|
888
|
+
};
|
|
889
|
+
logout(request: Request): void;
|
|
890
|
+
updatePassword(request: Request, payload: AuthPasswordUpdateRequest): {
|
|
891
|
+
status: AuthStatusView;
|
|
892
|
+
cookie?: string;
|
|
893
|
+
};
|
|
894
|
+
updateEnabled(request: Request, payload: AuthEnabledUpdateRequest): {
|
|
895
|
+
status: AuthStatusView;
|
|
896
|
+
cookie?: string;
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
823
900
|
type UiRouterOptions = {
|
|
824
901
|
configPath: string;
|
|
825
902
|
productVersion?: string;
|
|
@@ -827,6 +904,8 @@ type UiRouterOptions = {
|
|
|
827
904
|
marketplace?: MarketplaceApiConfig;
|
|
828
905
|
cronService?: InstanceType<typeof NextclawCore.CronService>;
|
|
829
906
|
chatRuntime?: UiChatRuntime;
|
|
907
|
+
ncpAgent?: UiNcpAgent;
|
|
908
|
+
authService?: UiAuthService;
|
|
830
909
|
};
|
|
831
910
|
|
|
832
911
|
declare function createUiRouter(options: UiRouterOptions): Hono;
|
|
@@ -875,4 +954,4 @@ declare function deleteSession(configPath: string, key: string): boolean;
|
|
|
875
954
|
declare function updateRuntime(configPath: string, patch: RuntimeConfigUpdate): Pick<ConfigView, "agents" | "bindings" | "session">;
|
|
876
955
|
declare function updateSecrets(configPath: string, patch: SecretsConfigUpdate): SecretsView;
|
|
877
956
|
|
|
878
|
-
export { type AgentBindingView, type AgentProfileView, type ApiError, type ApiResponse, type AppMetaView, type BindingPeerView, type BochaFreshnessValue, type ChannelSpecView, type ChatCapabilitiesView, type ChatCommandOptionView, type ChatCommandView, type ChatCommandsView, type ChatRunListView, type ChatRunState, type ChatRunView, type ChatSessionTypeOptionView, type ChatSessionTypesView, type ChatTurnRequest, type ChatTurnResult, type ChatTurnStopRequest, type ChatTurnStopResult, type ChatTurnStreamEvent, type ChatTurnView, type ConfigActionExecuteRequest, type ConfigActionExecuteResult, type ConfigActionManifest, type ConfigActionType, type ConfigMetaView, type ConfigSchemaResponse, type ConfigUiHint, type ConfigUiHints, type ConfigView, type CronActionResult, type CronEnableRequest, type CronJobStateView, type CronJobView, type CronListView, type CronPayloadView, type CronRunRequest, type CronScheduleView, DEFAULT_SESSION_TYPE, type MarketplaceApiConfig, type MarketplaceInstallKind, type MarketplaceInstallSkillParams, type MarketplaceInstallSpec, type MarketplaceInstalledRecord, type MarketplaceInstalledView, type MarketplaceInstaller, type MarketplaceItemSummary, type MarketplaceItemType, type MarketplaceItemView, type MarketplaceListView, type MarketplaceLocalizedTextMap, type MarketplacePluginContentView, type MarketplacePluginInstallKind, type MarketplacePluginInstallRequest, type MarketplacePluginInstallResult, type MarketplacePluginManageAction, type MarketplacePluginManageRequest, type MarketplacePluginManageResult, type MarketplaceRecommendationView, type MarketplaceSkillContentView, type MarketplaceSkillInstallKind, type MarketplaceSkillInstallRequest, type MarketplaceSkillInstallResult, type MarketplaceSkillManageAction, type MarketplaceSkillManageRequest, type MarketplaceSkillManageResult, type MarketplaceSort, type ProviderAuthImportResult, type ProviderAuthPollRequest, type ProviderAuthPollResult, type ProviderAuthStartRequest, type ProviderAuthStartResult, type ProviderConfigUpdate, type ProviderConfigView, type ProviderConnectionTestRequest, type ProviderConnectionTestResult, type ProviderCreateRequest, type ProviderCreateResult, type ProviderDeleteResult, type ProviderSpecView, type RuntimeConfigUpdate, type SearchConfigUpdate, type SearchConfigView, type SearchProviderConfigView, type SearchProviderName, type SearchProviderSpecView, type SecretProviderEnvView, type SecretProviderExecView, type SecretProviderFileView, type SecretProviderView, type SecretRefView, type SecretSourceView, type SecretsConfigUpdate, type SecretsView, type SessionConfigView, type SessionEntryView, type SessionEventView, type SessionHistoryView, type SessionMessageView, type SessionPatchUpdate, SessionPatchValidationError, type SessionsListView, type UiChatRuntime, type UiServerEvent, type UiServerHandle, type UiServerOptions, buildConfigMeta, buildConfigSchemaView, buildConfigView, createCustomProvider, createUiRouter, deleteCustomProvider, deleteSession, executeConfigAction, getSessionHistory, listSessions, loadConfigOrDefault, patchSession, startUiServer, testProviderConnection, updateChannel, updateModel, updateProvider, updateRuntime, updateSearch, updateSecrets };
|
|
957
|
+
export { type AgentBindingView, type AgentProfileView, type ApiError, type ApiResponse, type AppMetaView, type AuthEnabledUpdateRequest, type AuthLoginRequest, type AuthPasswordUpdateRequest, type AuthSetupRequest, type AuthStatusView, type BindingPeerView, type BochaFreshnessValue, type ChannelSpecView, type ChatCapabilitiesView, type ChatCommandOptionView, type ChatCommandView, type ChatCommandsView, type ChatRunListView, type ChatRunState, type ChatRunView, type ChatSessionTypeOptionView, type ChatSessionTypesView, type ChatTurnRequest, type ChatTurnResult, type ChatTurnStopRequest, type ChatTurnStopResult, type ChatTurnStreamEvent, type ChatTurnView, type ConfigActionExecuteRequest, type ConfigActionExecuteResult, type ConfigActionManifest, type ConfigActionType, type ConfigMetaView, type ConfigSchemaResponse, type ConfigUiHint, type ConfigUiHints, type ConfigView, type CronActionResult, type CronEnableRequest, type CronJobStateView, type CronJobView, type CronListView, type CronPayloadView, type CronRunRequest, type CronScheduleView, DEFAULT_SESSION_TYPE, type MarketplaceApiConfig, type MarketplaceInstallKind, type MarketplaceInstallSkillParams, type MarketplaceInstallSpec, type MarketplaceInstalledRecord, type MarketplaceInstalledView, type MarketplaceInstaller, type MarketplaceItemSummary, type MarketplaceItemType, type MarketplaceItemView, type MarketplaceListView, type MarketplaceLocalizedTextMap, type MarketplacePluginContentView, type MarketplacePluginInstallKind, type MarketplacePluginInstallRequest, type MarketplacePluginInstallResult, type MarketplacePluginManageAction, type MarketplacePluginManageRequest, type MarketplacePluginManageResult, type MarketplaceRecommendationView, type MarketplaceSkillContentView, type MarketplaceSkillInstallKind, type MarketplaceSkillInstallRequest, type MarketplaceSkillInstallResult, type MarketplaceSkillManageAction, type MarketplaceSkillManageRequest, type MarketplaceSkillManageResult, type MarketplaceSort, type ProviderAuthImportResult, type ProviderAuthPollRequest, type ProviderAuthPollResult, type ProviderAuthStartRequest, type ProviderAuthStartResult, type ProviderConfigUpdate, type ProviderConfigView, type ProviderConnectionTestRequest, type ProviderConnectionTestResult, type ProviderCreateRequest, type ProviderCreateResult, type ProviderDeleteResult, type ProviderSpecView, type RuntimeConfigUpdate, type SearchConfigUpdate, type SearchConfigView, type SearchProviderConfigView, type SearchProviderName, type SearchProviderSpecView, type SecretProviderEnvView, type SecretProviderExecView, type SecretProviderFileView, type SecretProviderView, type SecretRefView, type SecretSourceView, type SecretsConfigUpdate, type SecretsView, type SessionConfigView, type SessionEntryView, type SessionEventView, type SessionHistoryView, type SessionMessageView, type SessionPatchUpdate, SessionPatchValidationError, type SessionsListView, type UiChatRuntime, type UiNcpAgent, type UiNcpSessionListView, type UiNcpSessionMessagesView, type UiServerEvent, type UiServerHandle, type UiServerOptions, buildConfigMeta, buildConfigSchemaView, buildConfigView, createCustomProvider, createUiRouter, deleteCustomProvider, deleteSession, executeConfigAction, getSessionHistory, listSessions, loadConfigOrDefault, patchSession, startUiServer, testProviderConnection, updateChannel, updateModel, updateProvider, updateRuntime, updateSearch, updateSecrets };
|
package/dist/index.js
CHANGED
|
@@ -8,8 +8,319 @@ import { existsSync, readFileSync } from "fs";
|
|
|
8
8
|
import { readFile as readFile2, stat } from "fs/promises";
|
|
9
9
|
import { join } from "path";
|
|
10
10
|
|
|
11
|
+
// src/ui/auth.service.ts
|
|
12
|
+
import { ConfigSchema, loadConfig, saveConfig } from "@nextclaw/core";
|
|
13
|
+
import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "crypto";
|
|
14
|
+
var SESSION_COOKIE_NAME = "nextclaw_ui_session";
|
|
15
|
+
var PASSWORD_MIN_LENGTH = 8;
|
|
16
|
+
function normalizeUsername(value) {
|
|
17
|
+
return value.trim();
|
|
18
|
+
}
|
|
19
|
+
function parseCookieHeader(rawHeader) {
|
|
20
|
+
if (!rawHeader) {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
const cookies = {};
|
|
24
|
+
for (const chunk of rawHeader.split(";")) {
|
|
25
|
+
const [rawKey, ...rawValue] = chunk.split("=");
|
|
26
|
+
const key = rawKey?.trim();
|
|
27
|
+
if (!key) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
cookies[key] = decodeURIComponent(rawValue.join("=").trim());
|
|
31
|
+
}
|
|
32
|
+
return cookies;
|
|
33
|
+
}
|
|
34
|
+
function buildSetCookie(params) {
|
|
35
|
+
const parts = [
|
|
36
|
+
`${SESSION_COOKIE_NAME}=${encodeURIComponent(params.value)}`,
|
|
37
|
+
"Path=/",
|
|
38
|
+
"HttpOnly",
|
|
39
|
+
"SameSite=Lax"
|
|
40
|
+
];
|
|
41
|
+
if (params.secure) {
|
|
42
|
+
parts.push("Secure");
|
|
43
|
+
}
|
|
44
|
+
if (typeof params.maxAgeSeconds === "number") {
|
|
45
|
+
parts.push(`Max-Age=${Math.max(0, Math.trunc(params.maxAgeSeconds))}`);
|
|
46
|
+
}
|
|
47
|
+
if (params.expires) {
|
|
48
|
+
parts.push(`Expires=${params.expires}`);
|
|
49
|
+
}
|
|
50
|
+
return parts.join("; ");
|
|
51
|
+
}
|
|
52
|
+
function resolveSecureRequest(url, protocolHint) {
|
|
53
|
+
if (protocolHint?.trim().toLowerCase() === "https") {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return new URL(url).protocol === "https:";
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function hashPassword(password, salt) {
|
|
63
|
+
return scryptSync(password, salt, 64).toString("hex");
|
|
64
|
+
}
|
|
65
|
+
function verifyPassword(password, expectedHash, salt) {
|
|
66
|
+
const actualHashBuffer = Buffer.from(hashPassword(password, salt), "hex");
|
|
67
|
+
const expectedHashBuffer = Buffer.from(expectedHash, "hex");
|
|
68
|
+
if (actualHashBuffer.length !== expectedHashBuffer.length) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return timingSafeEqual(actualHashBuffer, expectedHashBuffer);
|
|
72
|
+
}
|
|
73
|
+
function createPasswordRecord(password) {
|
|
74
|
+
const passwordSalt = randomBytes(16).toString("hex");
|
|
75
|
+
return {
|
|
76
|
+
passwordHash: hashPassword(password, passwordSalt),
|
|
77
|
+
passwordSalt
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function validateUsernameAndPassword(username, password) {
|
|
81
|
+
if (!username) {
|
|
82
|
+
throw new Error("Username is required.");
|
|
83
|
+
}
|
|
84
|
+
if (password.trim().length < PASSWORD_MIN_LENGTH) {
|
|
85
|
+
throw new Error(`Password must be at least ${PASSWORD_MIN_LENGTH} characters.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
var UiAuthService = class {
|
|
89
|
+
constructor(configPath) {
|
|
90
|
+
this.configPath = configPath;
|
|
91
|
+
}
|
|
92
|
+
sessions = /* @__PURE__ */ new Map();
|
|
93
|
+
loadCurrentConfig() {
|
|
94
|
+
return loadConfig(this.configPath);
|
|
95
|
+
}
|
|
96
|
+
saveCurrentConfig(config) {
|
|
97
|
+
saveConfig(ConfigSchema.parse(config), this.configPath);
|
|
98
|
+
}
|
|
99
|
+
readAuthConfig() {
|
|
100
|
+
return this.loadCurrentConfig().ui.auth;
|
|
101
|
+
}
|
|
102
|
+
isConfigured(auth) {
|
|
103
|
+
return Boolean(
|
|
104
|
+
normalizeUsername(auth.username).length > 0 && auth.passwordHash.trim().length > 0 && auth.passwordSalt.trim().length > 0
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
isProtectionEnabled() {
|
|
108
|
+
const auth = this.readAuthConfig();
|
|
109
|
+
return Boolean(auth.enabled && this.isConfigured(auth));
|
|
110
|
+
}
|
|
111
|
+
getSessionIdFromCookieHeader(rawCookieHeader) {
|
|
112
|
+
const cookies = parseCookieHeader(rawCookieHeader);
|
|
113
|
+
const sessionId = cookies[SESSION_COOKIE_NAME];
|
|
114
|
+
return sessionId?.trim() ? sessionId.trim() : null;
|
|
115
|
+
}
|
|
116
|
+
getValidSession(sessionId, username) {
|
|
117
|
+
if (!sessionId) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const session = this.sessions.get(sessionId);
|
|
121
|
+
if (!session || session.username !== username) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return session;
|
|
125
|
+
}
|
|
126
|
+
isRequestAuthenticated(request) {
|
|
127
|
+
const auth = this.readAuthConfig();
|
|
128
|
+
if (!auth.enabled || !this.isConfigured(auth)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
const username = normalizeUsername(auth.username);
|
|
132
|
+
const sessionId = this.getSessionIdFromCookieHeader(request.headers.get("cookie"));
|
|
133
|
+
return Boolean(this.getValidSession(sessionId, username));
|
|
134
|
+
}
|
|
135
|
+
isSocketAuthenticated(request) {
|
|
136
|
+
const auth = this.readAuthConfig();
|
|
137
|
+
if (!auth.enabled || !this.isConfigured(auth)) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
const username = normalizeUsername(auth.username);
|
|
141
|
+
const rawCookieHeader = Array.isArray(request.headers.cookie) ? request.headers.cookie.join("; ") : request.headers.cookie;
|
|
142
|
+
const sessionId = this.getSessionIdFromCookieHeader(rawCookieHeader);
|
|
143
|
+
return Boolean(this.getValidSession(sessionId, username));
|
|
144
|
+
}
|
|
145
|
+
getStatus(request) {
|
|
146
|
+
const auth = this.readAuthConfig();
|
|
147
|
+
const configured = this.isConfigured(auth);
|
|
148
|
+
const enabled = Boolean(auth.enabled && configured);
|
|
149
|
+
const username = configured ? normalizeUsername(auth.username) : void 0;
|
|
150
|
+
return {
|
|
151
|
+
enabled,
|
|
152
|
+
configured,
|
|
153
|
+
authenticated: enabled ? this.isRequestAuthenticated(request) : false,
|
|
154
|
+
...username ? { username } : {}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
createSession(username) {
|
|
158
|
+
const sessionId = randomUUID();
|
|
159
|
+
this.sessions.set(sessionId, {
|
|
160
|
+
sessionId,
|
|
161
|
+
username,
|
|
162
|
+
createdAt: Date.now()
|
|
163
|
+
});
|
|
164
|
+
return sessionId;
|
|
165
|
+
}
|
|
166
|
+
clearAllSessions() {
|
|
167
|
+
this.sessions.clear();
|
|
168
|
+
}
|
|
169
|
+
deleteRequestSession(request) {
|
|
170
|
+
const sessionId = this.getSessionIdFromCookieHeader(request.headers.get("cookie"));
|
|
171
|
+
if (!sessionId) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
this.sessions.delete(sessionId);
|
|
175
|
+
}
|
|
176
|
+
buildLoginCookie(request, sessionId) {
|
|
177
|
+
return buildSetCookie({
|
|
178
|
+
value: sessionId,
|
|
179
|
+
secure: resolveSecureRequest(request.url, request.headers.get("x-forwarded-proto"))
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
buildLogoutCookie(request) {
|
|
183
|
+
return buildSetCookie({
|
|
184
|
+
value: "",
|
|
185
|
+
secure: resolveSecureRequest(request.url, request.headers.get("x-forwarded-proto")),
|
|
186
|
+
maxAgeSeconds: 0,
|
|
187
|
+
expires: (/* @__PURE__ */ new Date(0)).toUTCString()
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
setup(request, payload) {
|
|
191
|
+
const config = this.loadCurrentConfig();
|
|
192
|
+
const currentAuth = config.ui.auth;
|
|
193
|
+
if (this.isConfigured(currentAuth)) {
|
|
194
|
+
throw new Error("UI authentication is already configured.");
|
|
195
|
+
}
|
|
196
|
+
const username = normalizeUsername(payload.username);
|
|
197
|
+
const password = payload.password;
|
|
198
|
+
validateUsernameAndPassword(username, password);
|
|
199
|
+
const nextPassword = createPasswordRecord(password);
|
|
200
|
+
config.ui.auth = {
|
|
201
|
+
enabled: true,
|
|
202
|
+
username,
|
|
203
|
+
...nextPassword
|
|
204
|
+
};
|
|
205
|
+
this.saveCurrentConfig(config);
|
|
206
|
+
this.clearAllSessions();
|
|
207
|
+
const sessionId = this.createSession(username);
|
|
208
|
+
return {
|
|
209
|
+
status: {
|
|
210
|
+
enabled: true,
|
|
211
|
+
configured: true,
|
|
212
|
+
authenticated: true,
|
|
213
|
+
username
|
|
214
|
+
},
|
|
215
|
+
cookie: this.buildLoginCookie(request, sessionId)
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
login(request, payload) {
|
|
219
|
+
const auth = this.readAuthConfig();
|
|
220
|
+
if (!auth.enabled || !this.isConfigured(auth)) {
|
|
221
|
+
throw new Error("UI authentication is not enabled.");
|
|
222
|
+
}
|
|
223
|
+
const username = normalizeUsername(payload.username);
|
|
224
|
+
if (username !== normalizeUsername(auth.username) || !verifyPassword(payload.password, auth.passwordHash, auth.passwordSalt)) {
|
|
225
|
+
throw new Error("Invalid username or password.");
|
|
226
|
+
}
|
|
227
|
+
const sessionId = this.createSession(username);
|
|
228
|
+
return {
|
|
229
|
+
status: {
|
|
230
|
+
enabled: true,
|
|
231
|
+
configured: true,
|
|
232
|
+
authenticated: true,
|
|
233
|
+
username
|
|
234
|
+
},
|
|
235
|
+
cookie: this.buildLoginCookie(request, sessionId)
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
logout(request) {
|
|
239
|
+
this.deleteRequestSession(request);
|
|
240
|
+
}
|
|
241
|
+
updatePassword(request, payload) {
|
|
242
|
+
const config = this.loadCurrentConfig();
|
|
243
|
+
const auth = config.ui.auth;
|
|
244
|
+
if (!this.isConfigured(auth)) {
|
|
245
|
+
throw new Error("UI authentication is not configured.");
|
|
246
|
+
}
|
|
247
|
+
if (auth.enabled && !this.isRequestAuthenticated(request)) {
|
|
248
|
+
throw new Error("Authentication required.");
|
|
249
|
+
}
|
|
250
|
+
validateUsernameAndPassword(normalizeUsername(auth.username), payload.password);
|
|
251
|
+
const nextPassword = createPasswordRecord(payload.password);
|
|
252
|
+
config.ui.auth = {
|
|
253
|
+
...auth,
|
|
254
|
+
...nextPassword
|
|
255
|
+
};
|
|
256
|
+
this.saveCurrentConfig(config);
|
|
257
|
+
this.clearAllSessions();
|
|
258
|
+
if (!auth.enabled) {
|
|
259
|
+
return {
|
|
260
|
+
status: {
|
|
261
|
+
enabled: false,
|
|
262
|
+
configured: true,
|
|
263
|
+
authenticated: false,
|
|
264
|
+
username: normalizeUsername(auth.username)
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const sessionId = this.createSession(normalizeUsername(auth.username));
|
|
269
|
+
return {
|
|
270
|
+
status: {
|
|
271
|
+
enabled: true,
|
|
272
|
+
configured: true,
|
|
273
|
+
authenticated: true,
|
|
274
|
+
username: normalizeUsername(auth.username)
|
|
275
|
+
},
|
|
276
|
+
cookie: this.buildLoginCookie(request, sessionId)
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
updateEnabled(request, payload) {
|
|
280
|
+
const config = this.loadCurrentConfig();
|
|
281
|
+
const auth = config.ui.auth;
|
|
282
|
+
const configured = this.isConfigured(auth);
|
|
283
|
+
const currentlyEnabled = Boolean(auth.enabled && configured);
|
|
284
|
+
if (currentlyEnabled && !this.isRequestAuthenticated(request)) {
|
|
285
|
+
throw new Error("Authentication required.");
|
|
286
|
+
}
|
|
287
|
+
if (payload.enabled && !configured) {
|
|
288
|
+
throw new Error("UI authentication must be configured before it can be enabled.");
|
|
289
|
+
}
|
|
290
|
+
config.ui.auth = {
|
|
291
|
+
...auth,
|
|
292
|
+
enabled: Boolean(payload.enabled)
|
|
293
|
+
};
|
|
294
|
+
this.saveCurrentConfig(config);
|
|
295
|
+
if (!payload.enabled) {
|
|
296
|
+
this.clearAllSessions();
|
|
297
|
+
return {
|
|
298
|
+
status: {
|
|
299
|
+
enabled: false,
|
|
300
|
+
configured,
|
|
301
|
+
authenticated: false,
|
|
302
|
+
...configured ? { username: normalizeUsername(auth.username) } : {}
|
|
303
|
+
},
|
|
304
|
+
cookie: this.buildLogoutCookie(request)
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const username = normalizeUsername(auth.username);
|
|
308
|
+
const sessionId = this.createSession(username);
|
|
309
|
+
return {
|
|
310
|
+
status: {
|
|
311
|
+
enabled: true,
|
|
312
|
+
configured: true,
|
|
313
|
+
authenticated: true,
|
|
314
|
+
username
|
|
315
|
+
},
|
|
316
|
+
cookie: this.buildLoginCookie(request, sessionId)
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
11
321
|
// src/ui/router.ts
|
|
12
322
|
import { Hono } from "hono";
|
|
323
|
+
import { mountNcpHttpAgentRoutes } from "@nextclaw/ncp-http-agent-server";
|
|
13
324
|
|
|
14
325
|
// src/ui/router/response.ts
|
|
15
326
|
function ok(data) {
|
|
@@ -82,14 +393,115 @@ var AppRoutesController = class {
|
|
|
82
393
|
appMeta = (c) => c.json(ok(buildAppMetaView(this.options)));
|
|
83
394
|
};
|
|
84
395
|
|
|
396
|
+
// src/ui/router/auth.controller.ts
|
|
397
|
+
function isAuthenticationRequiredError(message) {
|
|
398
|
+
return message === "Authentication required.";
|
|
399
|
+
}
|
|
400
|
+
function isConflictError(message) {
|
|
401
|
+
return message.includes("already configured");
|
|
402
|
+
}
|
|
403
|
+
function setCookieHeader(c, cookie) {
|
|
404
|
+
if (!cookie) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
c.header("Set-Cookie", cookie);
|
|
408
|
+
}
|
|
409
|
+
var AuthRoutesController = class {
|
|
410
|
+
constructor(authService) {
|
|
411
|
+
this.authService = authService;
|
|
412
|
+
}
|
|
413
|
+
getStatus = (c) => {
|
|
414
|
+
return c.json(ok(this.authService.getStatus(c.req.raw)));
|
|
415
|
+
};
|
|
416
|
+
setup = async (c) => {
|
|
417
|
+
const body = await readJson(c.req.raw);
|
|
418
|
+
if (!body.ok) {
|
|
419
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
420
|
+
}
|
|
421
|
+
if (typeof body.data.username !== "string" || typeof body.data.password !== "string") {
|
|
422
|
+
return c.json(err("INVALID_BODY", "username and password are required"), 400);
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
const result = this.authService.setup(c.req.raw, body.data);
|
|
426
|
+
setCookieHeader(c, result.cookie);
|
|
427
|
+
return c.json(ok(result.status), 201);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
430
|
+
return c.json(err(isConflictError(message) ? "AUTH_ALREADY_CONFIGURED" : "INVALID_BODY", message), isConflictError(message) ? 409 : 400);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
login = async (c) => {
|
|
434
|
+
const body = await readJson(c.req.raw);
|
|
435
|
+
if (!body.ok) {
|
|
436
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
437
|
+
}
|
|
438
|
+
if (typeof body.data.username !== "string" || typeof body.data.password !== "string") {
|
|
439
|
+
return c.json(err("INVALID_BODY", "username and password are required"), 400);
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
const result = this.authService.login(c.req.raw, body.data);
|
|
443
|
+
setCookieHeader(c, result.cookie);
|
|
444
|
+
return c.json(ok(result.status));
|
|
445
|
+
} catch (error) {
|
|
446
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
447
|
+
const code = message === "Invalid username or password." ? "INVALID_CREDENTIALS" : "AUTH_NOT_ENABLED";
|
|
448
|
+
const status = message === "Invalid username or password." ? 401 : 400;
|
|
449
|
+
return c.json(err(code, message), status);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
logout = (c) => {
|
|
453
|
+
this.authService.logout(c.req.raw);
|
|
454
|
+
setCookieHeader(c, this.authService.buildLogoutCookie(c.req.raw));
|
|
455
|
+
return c.json(ok({ success: true }));
|
|
456
|
+
};
|
|
457
|
+
updatePassword = async (c) => {
|
|
458
|
+
const body = await readJson(c.req.raw);
|
|
459
|
+
if (!body.ok) {
|
|
460
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
461
|
+
}
|
|
462
|
+
if (typeof body.data.password !== "string") {
|
|
463
|
+
return c.json(err("INVALID_BODY", "password is required"), 400);
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
const result = this.authService.updatePassword(c.req.raw, body.data);
|
|
467
|
+
setCookieHeader(c, result.cookie);
|
|
468
|
+
return c.json(ok(result.status));
|
|
469
|
+
} catch (error) {
|
|
470
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
471
|
+
const status = isAuthenticationRequiredError(message) ? 401 : 400;
|
|
472
|
+
const code = isAuthenticationRequiredError(message) ? "UNAUTHORIZED" : "INVALID_BODY";
|
|
473
|
+
return c.json(err(code, message), status);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
updateEnabled = async (c) => {
|
|
477
|
+
const body = await readJson(c.req.raw);
|
|
478
|
+
if (!body.ok) {
|
|
479
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
480
|
+
}
|
|
481
|
+
if (typeof body.data.enabled !== "boolean") {
|
|
482
|
+
return c.json(err("INVALID_BODY", "enabled is required"), 400);
|
|
483
|
+
}
|
|
484
|
+
try {
|
|
485
|
+
const result = this.authService.updateEnabled(c.req.raw, body.data);
|
|
486
|
+
setCookieHeader(c, result.cookie);
|
|
487
|
+
return c.json(ok(result.status));
|
|
488
|
+
} catch (error) {
|
|
489
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
490
|
+
const status = isAuthenticationRequiredError(message) ? 401 : 400;
|
|
491
|
+
const code = isAuthenticationRequiredError(message) ? "UNAUTHORIZED" : "INVALID_BODY";
|
|
492
|
+
return c.json(err(code, message), status);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
};
|
|
496
|
+
|
|
85
497
|
// src/ui/router/chat.controller.ts
|
|
86
498
|
import * as NextclawCore2 from "@nextclaw/core";
|
|
87
499
|
|
|
88
500
|
// src/ui/config.ts
|
|
89
501
|
import {
|
|
90
|
-
loadConfig,
|
|
91
|
-
saveConfig,
|
|
92
|
-
ConfigSchema,
|
|
502
|
+
loadConfig as loadConfig2,
|
|
503
|
+
saveConfig as saveConfig2,
|
|
504
|
+
ConfigSchema as ConfigSchema2,
|
|
93
505
|
probeFeishu,
|
|
94
506
|
LiteLLMProvider,
|
|
95
507
|
buildConfigSchema,
|
|
@@ -432,7 +844,7 @@ function resolveRuntimeConfig(config, draftConfig) {
|
|
|
432
844
|
return config;
|
|
433
845
|
}
|
|
434
846
|
const merged = deepMerge(config, draftConfig);
|
|
435
|
-
return
|
|
847
|
+
return ConfigSchema2.parse(merged);
|
|
436
848
|
}
|
|
437
849
|
function getActionById(config, actionId) {
|
|
438
850
|
const actions = buildConfigSchemaView(config).actions;
|
|
@@ -783,15 +1195,15 @@ async function executeConfigAction(configPath, actionId, request) {
|
|
|
783
1195
|
};
|
|
784
1196
|
}
|
|
785
1197
|
function loadConfigOrDefault(configPath) {
|
|
786
|
-
return
|
|
1198
|
+
return loadConfig2(configPath);
|
|
787
1199
|
}
|
|
788
1200
|
function updateModel(configPath, patch) {
|
|
789
1201
|
const config = loadConfigOrDefault(configPath);
|
|
790
1202
|
if (typeof patch.model === "string") {
|
|
791
1203
|
config.agents.defaults.model = patch.model;
|
|
792
1204
|
}
|
|
793
|
-
const next =
|
|
794
|
-
|
|
1205
|
+
const next = ConfigSchema2.parse(config);
|
|
1206
|
+
saveConfig2(next, configPath);
|
|
795
1207
|
return buildConfigView(next);
|
|
796
1208
|
}
|
|
797
1209
|
function updateSearch(configPath, patch) {
|
|
@@ -844,8 +1256,8 @@ function updateSearch(configPath, patch) {
|
|
|
844
1256
|
config.search.providers.brave.baseUrl = normalizeOptionalString(bravePatch.baseUrl) ?? "https://api.search.brave.com/res/v1/web/search";
|
|
845
1257
|
}
|
|
846
1258
|
}
|
|
847
|
-
const next =
|
|
848
|
-
|
|
1259
|
+
const next = ConfigSchema2.parse(config);
|
|
1260
|
+
saveConfig2(next, configPath);
|
|
849
1261
|
return buildSearchView(next);
|
|
850
1262
|
}
|
|
851
1263
|
function updateProvider(configPath, providerName, patch) {
|
|
@@ -878,8 +1290,8 @@ function updateProvider(configPath, providerName, patch) {
|
|
|
878
1290
|
if (Object.prototype.hasOwnProperty.call(patch, "modelThinking")) {
|
|
879
1291
|
provider.modelThinking = normalizeModelThinkingConfig(patch.modelThinking ?? {});
|
|
880
1292
|
}
|
|
881
|
-
const next =
|
|
882
|
-
|
|
1293
|
+
const next = ConfigSchema2.parse(config);
|
|
1294
|
+
saveConfig2(next, configPath);
|
|
883
1295
|
const uiHints = buildUiHints(next);
|
|
884
1296
|
const updated = next.providers[providerName];
|
|
885
1297
|
return toProviderView(next, updated, providerName, uiHints, spec ?? void 0);
|
|
@@ -898,8 +1310,8 @@ function createCustomProvider(configPath, patch = {}) {
|
|
|
898
1310
|
models: normalizeModelList(patch.models ?? []),
|
|
899
1311
|
modelThinking: normalizeModelThinkingConfig(patch.modelThinking ?? {})
|
|
900
1312
|
};
|
|
901
|
-
const next =
|
|
902
|
-
|
|
1313
|
+
const next = ConfigSchema2.parse(config);
|
|
1314
|
+
saveConfig2(next, configPath);
|
|
903
1315
|
const uiHints = buildUiHints(next);
|
|
904
1316
|
const created = next.providers[providerName];
|
|
905
1317
|
return {
|
|
@@ -918,8 +1330,8 @@ function deleteCustomProvider(configPath, providerName) {
|
|
|
918
1330
|
}
|
|
919
1331
|
delete providers[providerName];
|
|
920
1332
|
clearSecretRefsByPrefix(config, `providers.${providerName}`);
|
|
921
|
-
const next =
|
|
922
|
-
|
|
1333
|
+
const next = ConfigSchema2.parse(config);
|
|
1334
|
+
saveConfig2(next, configPath);
|
|
923
1335
|
return true;
|
|
924
1336
|
}
|
|
925
1337
|
function normalizeOptionalString(value) {
|
|
@@ -1069,8 +1481,8 @@ function updateChannel(configPath, channelName, patch) {
|
|
|
1069
1481
|
}
|
|
1070
1482
|
}
|
|
1071
1483
|
config.channels[channelName] = { ...channel, ...patch };
|
|
1072
|
-
const next =
|
|
1073
|
-
|
|
1484
|
+
const next = ConfigSchema2.parse(config);
|
|
1485
|
+
saveConfig2(next, configPath);
|
|
1074
1486
|
const uiHints = buildUiHints(next);
|
|
1075
1487
|
return sanitizePublicConfigValue(
|
|
1076
1488
|
next.channels[channelName],
|
|
@@ -1356,8 +1768,8 @@ function updateRuntime(configPath, patch) {
|
|
|
1356
1768
|
agentToAgent: nextAgentToAgent
|
|
1357
1769
|
};
|
|
1358
1770
|
}
|
|
1359
|
-
const next =
|
|
1360
|
-
|
|
1771
|
+
const next = ConfigSchema2.parse(config);
|
|
1772
|
+
saveConfig2(next, configPath);
|
|
1361
1773
|
const view = buildConfigView(next);
|
|
1362
1774
|
return {
|
|
1363
1775
|
agents: view.agents,
|
|
@@ -1391,8 +1803,8 @@ function updateSecrets(configPath, patch) {
|
|
|
1391
1803
|
if (Object.prototype.hasOwnProperty.call(patch, "refs")) {
|
|
1392
1804
|
config.secrets.refs = patch.refs ?? {};
|
|
1393
1805
|
}
|
|
1394
|
-
const next =
|
|
1395
|
-
|
|
1806
|
+
const next = ConfigSchema2.parse(config);
|
|
1807
|
+
saveConfig2(next, configPath);
|
|
1396
1808
|
return {
|
|
1397
1809
|
enabled: next.secrets.enabled,
|
|
1398
1810
|
defaults: { ...next.secrets.defaults },
|
|
@@ -1998,14 +2410,14 @@ var ChatRoutesController = class {
|
|
|
1998
2410
|
};
|
|
1999
2411
|
|
|
2000
2412
|
// src/ui/provider-auth.ts
|
|
2001
|
-
import { createHash, randomBytes, randomUUID } from "crypto";
|
|
2413
|
+
import { createHash, randomBytes as randomBytes2, randomUUID as randomUUID2 } from "crypto";
|
|
2002
2414
|
import { readFile } from "fs/promises";
|
|
2003
2415
|
import { homedir } from "os";
|
|
2004
2416
|
import { isAbsolute, resolve } from "path";
|
|
2005
2417
|
import {
|
|
2006
|
-
ConfigSchema as
|
|
2007
|
-
loadConfig as
|
|
2008
|
-
saveConfig as
|
|
2418
|
+
ConfigSchema as ConfigSchema3,
|
|
2419
|
+
loadConfig as loadConfig3,
|
|
2420
|
+
saveConfig as saveConfig3
|
|
2009
2421
|
} from "@nextclaw/core";
|
|
2010
2422
|
var authSessions = /* @__PURE__ */ new Map();
|
|
2011
2423
|
var DEFAULT_AUTH_INTERVAL_MS = 2e3;
|
|
@@ -2026,7 +2438,7 @@ function toBase64Url(buffer) {
|
|
|
2026
2438
|
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
2027
2439
|
}
|
|
2028
2440
|
function buildPkce() {
|
|
2029
|
-
const verifier = toBase64Url(
|
|
2441
|
+
const verifier = toBase64Url(randomBytes2(48));
|
|
2030
2442
|
const challenge = toBase64Url(createHash("sha256").update(verifier).digest());
|
|
2031
2443
|
return { verifier, challenge };
|
|
2032
2444
|
}
|
|
@@ -2199,7 +2611,7 @@ function readFieldAsString(source, fieldName) {
|
|
|
2199
2611
|
return trimmed.length > 0 ? trimmed : null;
|
|
2200
2612
|
}
|
|
2201
2613
|
function setProviderApiKey(params) {
|
|
2202
|
-
const config =
|
|
2614
|
+
const config = loadConfig3(params.configPath);
|
|
2203
2615
|
const providers = config.providers;
|
|
2204
2616
|
if (!providers[params.provider]) {
|
|
2205
2617
|
providers[params.provider] = {
|
|
@@ -2217,8 +2629,8 @@ function setProviderApiKey(params) {
|
|
|
2217
2629
|
if (!target.apiBase && params.defaultApiBase) {
|
|
2218
2630
|
target.apiBase = params.defaultApiBase;
|
|
2219
2631
|
}
|
|
2220
|
-
const next =
|
|
2221
|
-
|
|
2632
|
+
const next = ConfigSchema3.parse(config);
|
|
2633
|
+
saveConfig3(next, params.configPath);
|
|
2222
2634
|
}
|
|
2223
2635
|
async function startProviderAuth(configPath, providerName, options) {
|
|
2224
2636
|
cleanupExpiredAuthSessions();
|
|
@@ -2243,7 +2655,7 @@ async function startProviderAuth(configPath, providerName, options) {
|
|
|
2243
2655
|
if (!pkce) {
|
|
2244
2656
|
throw new Error("MiniMax OAuth requires PKCE");
|
|
2245
2657
|
}
|
|
2246
|
-
const state = toBase64Url(
|
|
2658
|
+
const state = toBase64Url(randomBytes2(16));
|
|
2247
2659
|
const body = new URLSearchParams({
|
|
2248
2660
|
response_type: "code",
|
|
2249
2661
|
client_id: resolvedMethod.clientId,
|
|
@@ -2257,7 +2669,7 @@ async function startProviderAuth(configPath, providerName, options) {
|
|
|
2257
2669
|
headers: {
|
|
2258
2670
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
2259
2671
|
Accept: "application/json",
|
|
2260
|
-
"x-request-id":
|
|
2672
|
+
"x-request-id": randomUUID2()
|
|
2261
2673
|
},
|
|
2262
2674
|
body
|
|
2263
2675
|
});
|
|
@@ -2309,7 +2721,7 @@ async function startProviderAuth(configPath, providerName, options) {
|
|
|
2309
2721
|
const expiresInSec = normalizePositiveInt(payload.expires_in, 600);
|
|
2310
2722
|
expiresAtMs = Date.now() + expiresInSec * 1e3;
|
|
2311
2723
|
}
|
|
2312
|
-
const sessionId =
|
|
2724
|
+
const sessionId = randomUUID2();
|
|
2313
2725
|
authSessions.set(sessionId, {
|
|
2314
2726
|
sessionId,
|
|
2315
2727
|
provider: providerName,
|
|
@@ -2856,6 +3268,83 @@ var CronRoutesController = class {
|
|
|
2856
3268
|
};
|
|
2857
3269
|
};
|
|
2858
3270
|
|
|
3271
|
+
// src/ui/router/ncp-session.controller.ts
|
|
3272
|
+
function readPositiveInt(value) {
|
|
3273
|
+
if (typeof value !== "string") {
|
|
3274
|
+
return void 0;
|
|
3275
|
+
}
|
|
3276
|
+
const parsed = Number.parseInt(value, 10);
|
|
3277
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
3278
|
+
return void 0;
|
|
3279
|
+
}
|
|
3280
|
+
return parsed;
|
|
3281
|
+
}
|
|
3282
|
+
var NcpSessionRoutesController = class {
|
|
3283
|
+
constructor(options) {
|
|
3284
|
+
this.options = options;
|
|
3285
|
+
}
|
|
3286
|
+
listSessions = async (c) => {
|
|
3287
|
+
const sessionApi = this.options.ncpAgent?.sessionApi;
|
|
3288
|
+
if (!sessionApi) {
|
|
3289
|
+
return c.json(err("NOT_AVAILABLE", "ncp session api unavailable"), 503);
|
|
3290
|
+
}
|
|
3291
|
+
const sessions = await sessionApi.listSessions({
|
|
3292
|
+
limit: readPositiveInt(c.req.query("limit"))
|
|
3293
|
+
});
|
|
3294
|
+
const payload = {
|
|
3295
|
+
sessions,
|
|
3296
|
+
total: sessions.length
|
|
3297
|
+
};
|
|
3298
|
+
return c.json(ok(payload));
|
|
3299
|
+
};
|
|
3300
|
+
getSession = async (c) => {
|
|
3301
|
+
const sessionApi = this.options.ncpAgent?.sessionApi;
|
|
3302
|
+
if (!sessionApi) {
|
|
3303
|
+
return c.json(err("NOT_AVAILABLE", "ncp session api unavailable"), 503);
|
|
3304
|
+
}
|
|
3305
|
+
const sessionId = decodeURIComponent(c.req.param("sessionId"));
|
|
3306
|
+
const session = await sessionApi.getSession(sessionId);
|
|
3307
|
+
if (!session) {
|
|
3308
|
+
return c.json(err("NOT_FOUND", `ncp session not found: ${sessionId}`), 404);
|
|
3309
|
+
}
|
|
3310
|
+
return c.json(ok(session));
|
|
3311
|
+
};
|
|
3312
|
+
listSessionMessages = async (c) => {
|
|
3313
|
+
const sessionApi = this.options.ncpAgent?.sessionApi;
|
|
3314
|
+
if (!sessionApi) {
|
|
3315
|
+
return c.json(err("NOT_AVAILABLE", "ncp session api unavailable"), 503);
|
|
3316
|
+
}
|
|
3317
|
+
const sessionId = decodeURIComponent(c.req.param("sessionId"));
|
|
3318
|
+
const session = await sessionApi.getSession(sessionId);
|
|
3319
|
+
if (!session) {
|
|
3320
|
+
return c.json(err("NOT_FOUND", `ncp session not found: ${sessionId}`), 404);
|
|
3321
|
+
}
|
|
3322
|
+
const messages = await sessionApi.listSessionMessages(sessionId, {
|
|
3323
|
+
limit: readPositiveInt(c.req.query("limit"))
|
|
3324
|
+
});
|
|
3325
|
+
const payload = {
|
|
3326
|
+
sessionId,
|
|
3327
|
+
messages,
|
|
3328
|
+
total: messages.length
|
|
3329
|
+
};
|
|
3330
|
+
return c.json(ok(payload));
|
|
3331
|
+
};
|
|
3332
|
+
deleteSession = async (c) => {
|
|
3333
|
+
const sessionApi = this.options.ncpAgent?.sessionApi;
|
|
3334
|
+
if (!sessionApi) {
|
|
3335
|
+
return c.json(err("NOT_AVAILABLE", "ncp session api unavailable"), 503);
|
|
3336
|
+
}
|
|
3337
|
+
const sessionId = decodeURIComponent(c.req.param("sessionId"));
|
|
3338
|
+
const existing = await sessionApi.getSession(sessionId);
|
|
3339
|
+
if (!existing) {
|
|
3340
|
+
return c.json(err("NOT_FOUND", `ncp session not found: ${sessionId}`), 404);
|
|
3341
|
+
}
|
|
3342
|
+
await sessionApi.deleteSession(sessionId);
|
|
3343
|
+
this.options.publish({ type: "config.updated", payload: { path: "session" } });
|
|
3344
|
+
return c.json(ok({ deleted: true, sessionId }));
|
|
3345
|
+
};
|
|
3346
|
+
};
|
|
3347
|
+
|
|
2859
3348
|
// src/ui/router/marketplace/constants.ts
|
|
2860
3349
|
var DEFAULT_MARKETPLACE_API_BASE = "https://marketplace-api.nextclaw.io";
|
|
2861
3350
|
var NEXTCLAW_PLUGIN_NPM_PREFIX = "@nextclaw/channel-plugin-";
|
|
@@ -4046,16 +4535,38 @@ var SessionRoutesController = class {
|
|
|
4046
4535
|
function createUiRouter(options) {
|
|
4047
4536
|
const app = new Hono();
|
|
4048
4537
|
const marketplaceBaseUrl = normalizeMarketplaceBaseUrl(options);
|
|
4538
|
+
const authService = options.authService ?? new UiAuthService(options.configPath);
|
|
4049
4539
|
const appController = new AppRoutesController(options);
|
|
4540
|
+
const authController = new AuthRoutesController(authService);
|
|
4050
4541
|
const configController = new ConfigRoutesController(options);
|
|
4051
4542
|
const chatController = new ChatRoutesController(options);
|
|
4052
4543
|
const sessionController = new SessionRoutesController(options);
|
|
4053
4544
|
const cronController = new CronRoutesController(options);
|
|
4545
|
+
const ncpSessionController = new NcpSessionRoutesController(options);
|
|
4054
4546
|
const pluginMarketplaceController = new PluginMarketplaceController(options, marketplaceBaseUrl);
|
|
4055
4547
|
const skillMarketplaceController = new SkillMarketplaceController(options, marketplaceBaseUrl);
|
|
4056
4548
|
app.notFound((c) => c.json(err("NOT_FOUND", "endpoint not found"), 404));
|
|
4549
|
+
app.use("/api/*", async (c, next) => {
|
|
4550
|
+
const path = c.req.path;
|
|
4551
|
+
if (path === "/api/health" || path.startsWith("/api/auth/")) {
|
|
4552
|
+
await next();
|
|
4553
|
+
return;
|
|
4554
|
+
}
|
|
4555
|
+
if (!authService.isProtectionEnabled() || authService.isRequestAuthenticated(c.req.raw)) {
|
|
4556
|
+
await next();
|
|
4557
|
+
return;
|
|
4558
|
+
}
|
|
4559
|
+
c.status(401);
|
|
4560
|
+
return c.json(err("UNAUTHORIZED", "Authentication required."), 401);
|
|
4561
|
+
});
|
|
4057
4562
|
app.get("/api/health", appController.health);
|
|
4058
4563
|
app.get("/api/app/meta", appController.appMeta);
|
|
4564
|
+
app.get("/api/auth/status", authController.getStatus);
|
|
4565
|
+
app.post("/api/auth/setup", authController.setup);
|
|
4566
|
+
app.post("/api/auth/login", authController.login);
|
|
4567
|
+
app.post("/api/auth/logout", authController.logout);
|
|
4568
|
+
app.put("/api/auth/password", authController.updatePassword);
|
|
4569
|
+
app.put("/api/auth/enabled", authController.updateEnabled);
|
|
4059
4570
|
app.get("/api/config", configController.getConfig);
|
|
4060
4571
|
app.get("/api/config/meta", configController.getConfigMeta);
|
|
4061
4572
|
app.get("/api/config/schema", configController.getConfigSchema);
|
|
@@ -4085,6 +4596,17 @@ function createUiRouter(options) {
|
|
|
4085
4596
|
app.get("/api/sessions/:key/history", sessionController.getSessionHistory);
|
|
4086
4597
|
app.put("/api/sessions/:key", sessionController.patchSession);
|
|
4087
4598
|
app.delete("/api/sessions/:key", sessionController.deleteSession);
|
|
4599
|
+
if (options.ncpAgent) {
|
|
4600
|
+
mountNcpHttpAgentRoutes(app, {
|
|
4601
|
+
basePath: options.ncpAgent.basePath ?? "/api/ncp/agent",
|
|
4602
|
+
agentClientEndpoint: options.ncpAgent.agentClientEndpoint,
|
|
4603
|
+
streamProvider: options.ncpAgent.streamProvider
|
|
4604
|
+
});
|
|
4605
|
+
app.get("/api/ncp/sessions", ncpSessionController.listSessions);
|
|
4606
|
+
app.get("/api/ncp/sessions/:sessionId", ncpSessionController.getSession);
|
|
4607
|
+
app.get("/api/ncp/sessions/:sessionId/messages", ncpSessionController.listSessionMessages);
|
|
4608
|
+
app.delete("/api/ncp/sessions/:sessionId", ncpSessionController.deleteSession);
|
|
4609
|
+
}
|
|
4088
4610
|
app.get("/api/cron", cronController.listJobs);
|
|
4089
4611
|
app.delete("/api/cron/:id", cronController.deleteJob);
|
|
4090
4612
|
app.put("/api/cron/:id/enable", cronController.enableJob);
|
|
@@ -4121,7 +4643,8 @@ function startUiServer(options) {
|
|
|
4121
4643
|
const app = new Hono2();
|
|
4122
4644
|
app.use("/*", compress());
|
|
4123
4645
|
const origin = options.corsOrigins ?? DEFAULT_CORS_ORIGINS;
|
|
4124
|
-
|
|
4646
|
+
const authService = new UiAuthService(options.configPath);
|
|
4647
|
+
app.use("/api/*", cors({ origin, credentials: true }));
|
|
4125
4648
|
const clients = /* @__PURE__ */ new Set();
|
|
4126
4649
|
const publish = (event) => {
|
|
4127
4650
|
const payload = JSON.stringify(event);
|
|
@@ -4139,7 +4662,9 @@ function startUiServer(options) {
|
|
|
4139
4662
|
publish,
|
|
4140
4663
|
marketplace: options.marketplace,
|
|
4141
4664
|
cronService: options.cronService,
|
|
4142
|
-
chatRuntime: options.chatRuntime
|
|
4665
|
+
chatRuntime: options.chatRuntime,
|
|
4666
|
+
ncpAgent: options.ncpAgent,
|
|
4667
|
+
authService
|
|
4143
4668
|
})
|
|
4144
4669
|
);
|
|
4145
4670
|
const staticDir = options.staticDir;
|
|
@@ -4179,9 +4704,23 @@ function startUiServer(options) {
|
|
|
4179
4704
|
port: options.port,
|
|
4180
4705
|
hostname: options.host
|
|
4181
4706
|
});
|
|
4182
|
-
const
|
|
4183
|
-
|
|
4184
|
-
|
|
4707
|
+
const httpServer = server;
|
|
4708
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
4709
|
+
httpServer.on("upgrade", (request, socket, head) => {
|
|
4710
|
+
const host = request.headers.host ?? "127.0.0.1";
|
|
4711
|
+
const url = request.url ?? "/";
|
|
4712
|
+
const pathname = new URL(url, `http://${host}`).pathname;
|
|
4713
|
+
if (pathname !== "/ws") {
|
|
4714
|
+
return;
|
|
4715
|
+
}
|
|
4716
|
+
if (!authService.isSocketAuthenticated(request)) {
|
|
4717
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
4718
|
+
socket.destroy();
|
|
4719
|
+
return;
|
|
4720
|
+
}
|
|
4721
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
4722
|
+
wss.emit("connection", ws, request);
|
|
4723
|
+
});
|
|
4185
4724
|
});
|
|
4186
4725
|
wss.on("connection", (socket) => {
|
|
4187
4726
|
clients.add(socket);
|
|
@@ -4193,7 +4732,9 @@ function startUiServer(options) {
|
|
|
4193
4732
|
publish,
|
|
4194
4733
|
close: () => new Promise((resolve2) => {
|
|
4195
4734
|
wss.close(() => {
|
|
4196
|
-
server.close(() =>
|
|
4735
|
+
server.close(() => {
|
|
4736
|
+
Promise.resolve(options.ncpAgent?.agentClientEndpoint.stop()).catch(() => void 0).finally(() => resolve2());
|
|
4737
|
+
});
|
|
4197
4738
|
});
|
|
4198
4739
|
})
|
|
4199
4740
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Nextclaw UI/API server.",
|
|
6
6
|
"type": "module",
|
|
@@ -18,9 +18,11 @@
|
|
|
18
18
|
"@hono/node-server": "^1.13.3",
|
|
19
19
|
"hono": "^4.6.2",
|
|
20
20
|
"ws": "^8.18.0",
|
|
21
|
-
"@nextclaw/
|
|
22
|
-
"@nextclaw/
|
|
23
|
-
"@nextclaw/
|
|
21
|
+
"@nextclaw/ncp-http-agent-server": "0.3.0",
|
|
22
|
+
"@nextclaw/openclaw-compat": "0.3.0",
|
|
23
|
+
"@nextclaw/ncp": "0.3.0",
|
|
24
|
+
"@nextclaw/runtime": "0.2.0",
|
|
25
|
+
"@nextclaw/core": "0.9.0"
|
|
24
26
|
},
|
|
25
27
|
"devDependencies": {
|
|
26
28
|
"@types/node": "^20.17.6",
|