@nextclaw/server 0.6.13 → 0.7.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 +61 -1
- package/dist/index.js +485 -37
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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
3
|
import { Hono } from 'hono';
|
|
4
|
+
import { IncomingMessage } from 'node:http';
|
|
4
5
|
|
|
5
6
|
type ApiError = {
|
|
6
7
|
code: string;
|
|
@@ -133,6 +134,26 @@ type ProviderAuthImportResult = {
|
|
|
133
134
|
source: "cli";
|
|
134
135
|
expiresAt?: string;
|
|
135
136
|
};
|
|
137
|
+
type AuthStatusView = {
|
|
138
|
+
enabled: boolean;
|
|
139
|
+
configured: boolean;
|
|
140
|
+
authenticated: boolean;
|
|
141
|
+
username?: string;
|
|
142
|
+
};
|
|
143
|
+
type AuthSetupRequest = {
|
|
144
|
+
username: string;
|
|
145
|
+
password: string;
|
|
146
|
+
};
|
|
147
|
+
type AuthLoginRequest = {
|
|
148
|
+
username: string;
|
|
149
|
+
password: string;
|
|
150
|
+
};
|
|
151
|
+
type AuthPasswordUpdateRequest = {
|
|
152
|
+
password: string;
|
|
153
|
+
};
|
|
154
|
+
type AuthEnabledUpdateRequest = {
|
|
155
|
+
enabled: boolean;
|
|
156
|
+
};
|
|
136
157
|
type AgentProfileView = {
|
|
137
158
|
id: string;
|
|
138
159
|
default?: boolean;
|
|
@@ -820,6 +841,44 @@ type UiServerHandle = {
|
|
|
820
841
|
|
|
821
842
|
declare function startUiServer(options: UiServerOptions): UiServerHandle;
|
|
822
843
|
|
|
844
|
+
declare class UiAuthService {
|
|
845
|
+
private readonly configPath;
|
|
846
|
+
private readonly sessions;
|
|
847
|
+
constructor(configPath: string);
|
|
848
|
+
private loadCurrentConfig;
|
|
849
|
+
private saveCurrentConfig;
|
|
850
|
+
private readAuthConfig;
|
|
851
|
+
private isConfigured;
|
|
852
|
+
isProtectionEnabled(): boolean;
|
|
853
|
+
private getSessionIdFromCookieHeader;
|
|
854
|
+
private getValidSession;
|
|
855
|
+
isRequestAuthenticated(request: Request): boolean;
|
|
856
|
+
isSocketAuthenticated(request: IncomingMessage): boolean;
|
|
857
|
+
getStatus(request: Request): AuthStatusView;
|
|
858
|
+
private createSession;
|
|
859
|
+
private clearAllSessions;
|
|
860
|
+
private deleteRequestSession;
|
|
861
|
+
private buildLoginCookie;
|
|
862
|
+
buildLogoutCookie(request: Request): string;
|
|
863
|
+
setup(request: Request, payload: AuthSetupRequest): {
|
|
864
|
+
status: AuthStatusView;
|
|
865
|
+
cookie: string;
|
|
866
|
+
};
|
|
867
|
+
login(request: Request, payload: AuthLoginRequest): {
|
|
868
|
+
status: AuthStatusView;
|
|
869
|
+
cookie: string;
|
|
870
|
+
};
|
|
871
|
+
logout(request: Request): void;
|
|
872
|
+
updatePassword(request: Request, payload: AuthPasswordUpdateRequest): {
|
|
873
|
+
status: AuthStatusView;
|
|
874
|
+
cookie?: string;
|
|
875
|
+
};
|
|
876
|
+
updateEnabled(request: Request, payload: AuthEnabledUpdateRequest): {
|
|
877
|
+
status: AuthStatusView;
|
|
878
|
+
cookie?: string;
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
823
882
|
type UiRouterOptions = {
|
|
824
883
|
configPath: string;
|
|
825
884
|
productVersion?: string;
|
|
@@ -827,6 +886,7 @@ type UiRouterOptions = {
|
|
|
827
886
|
marketplace?: MarketplaceApiConfig;
|
|
828
887
|
cronService?: InstanceType<typeof NextclawCore.CronService>;
|
|
829
888
|
chatRuntime?: UiChatRuntime;
|
|
889
|
+
authService?: UiAuthService;
|
|
830
890
|
};
|
|
831
891
|
|
|
832
892
|
declare function createUiRouter(options: UiRouterOptions): Hono;
|
|
@@ -875,4 +935,4 @@ declare function deleteSession(configPath: string, key: string): boolean;
|
|
|
875
935
|
declare function updateRuntime(configPath: string, patch: RuntimeConfigUpdate): Pick<ConfigView, "agents" | "bindings" | "session">;
|
|
876
936
|
declare function updateSecrets(configPath: string, patch: SecretsConfigUpdate): SecretsView;
|
|
877
937
|
|
|
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 };
|
|
938
|
+
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 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,6 +8,316 @@ 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";
|
|
13
323
|
|
|
@@ -82,14 +392,115 @@ var AppRoutesController = class {
|
|
|
82
392
|
appMeta = (c) => c.json(ok(buildAppMetaView(this.options)));
|
|
83
393
|
};
|
|
84
394
|
|
|
395
|
+
// src/ui/router/auth.controller.ts
|
|
396
|
+
function isAuthenticationRequiredError(message) {
|
|
397
|
+
return message === "Authentication required.";
|
|
398
|
+
}
|
|
399
|
+
function isConflictError(message) {
|
|
400
|
+
return message.includes("already configured");
|
|
401
|
+
}
|
|
402
|
+
function setCookieHeader(c, cookie) {
|
|
403
|
+
if (!cookie) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
c.header("Set-Cookie", cookie);
|
|
407
|
+
}
|
|
408
|
+
var AuthRoutesController = class {
|
|
409
|
+
constructor(authService) {
|
|
410
|
+
this.authService = authService;
|
|
411
|
+
}
|
|
412
|
+
getStatus = (c) => {
|
|
413
|
+
return c.json(ok(this.authService.getStatus(c.req.raw)));
|
|
414
|
+
};
|
|
415
|
+
setup = async (c) => {
|
|
416
|
+
const body = await readJson(c.req.raw);
|
|
417
|
+
if (!body.ok) {
|
|
418
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
419
|
+
}
|
|
420
|
+
if (typeof body.data.username !== "string" || typeof body.data.password !== "string") {
|
|
421
|
+
return c.json(err("INVALID_BODY", "username and password are required"), 400);
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
const result = this.authService.setup(c.req.raw, body.data);
|
|
425
|
+
setCookieHeader(c, result.cookie);
|
|
426
|
+
return c.json(ok(result.status), 201);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
429
|
+
return c.json(err(isConflictError(message) ? "AUTH_ALREADY_CONFIGURED" : "INVALID_BODY", message), isConflictError(message) ? 409 : 400);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
login = async (c) => {
|
|
433
|
+
const body = await readJson(c.req.raw);
|
|
434
|
+
if (!body.ok) {
|
|
435
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
436
|
+
}
|
|
437
|
+
if (typeof body.data.username !== "string" || typeof body.data.password !== "string") {
|
|
438
|
+
return c.json(err("INVALID_BODY", "username and password are required"), 400);
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
const result = this.authService.login(c.req.raw, body.data);
|
|
442
|
+
setCookieHeader(c, result.cookie);
|
|
443
|
+
return c.json(ok(result.status));
|
|
444
|
+
} catch (error) {
|
|
445
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
446
|
+
const code = message === "Invalid username or password." ? "INVALID_CREDENTIALS" : "AUTH_NOT_ENABLED";
|
|
447
|
+
const status = message === "Invalid username or password." ? 401 : 400;
|
|
448
|
+
return c.json(err(code, message), status);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
logout = (c) => {
|
|
452
|
+
this.authService.logout(c.req.raw);
|
|
453
|
+
setCookieHeader(c, this.authService.buildLogoutCookie(c.req.raw));
|
|
454
|
+
return c.json(ok({ success: true }));
|
|
455
|
+
};
|
|
456
|
+
updatePassword = async (c) => {
|
|
457
|
+
const body = await readJson(c.req.raw);
|
|
458
|
+
if (!body.ok) {
|
|
459
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
460
|
+
}
|
|
461
|
+
if (typeof body.data.password !== "string") {
|
|
462
|
+
return c.json(err("INVALID_BODY", "password is required"), 400);
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
const result = this.authService.updatePassword(c.req.raw, body.data);
|
|
466
|
+
setCookieHeader(c, result.cookie);
|
|
467
|
+
return c.json(ok(result.status));
|
|
468
|
+
} catch (error) {
|
|
469
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
470
|
+
const status = isAuthenticationRequiredError(message) ? 401 : 400;
|
|
471
|
+
const code = isAuthenticationRequiredError(message) ? "UNAUTHORIZED" : "INVALID_BODY";
|
|
472
|
+
return c.json(err(code, message), status);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
updateEnabled = async (c) => {
|
|
476
|
+
const body = await readJson(c.req.raw);
|
|
477
|
+
if (!body.ok) {
|
|
478
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
479
|
+
}
|
|
480
|
+
if (typeof body.data.enabled !== "boolean") {
|
|
481
|
+
return c.json(err("INVALID_BODY", "enabled is required"), 400);
|
|
482
|
+
}
|
|
483
|
+
try {
|
|
484
|
+
const result = this.authService.updateEnabled(c.req.raw, body.data);
|
|
485
|
+
setCookieHeader(c, result.cookie);
|
|
486
|
+
return c.json(ok(result.status));
|
|
487
|
+
} catch (error) {
|
|
488
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
489
|
+
const status = isAuthenticationRequiredError(message) ? 401 : 400;
|
|
490
|
+
const code = isAuthenticationRequiredError(message) ? "UNAUTHORIZED" : "INVALID_BODY";
|
|
491
|
+
return c.json(err(code, message), status);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
};
|
|
495
|
+
|
|
85
496
|
// src/ui/router/chat.controller.ts
|
|
86
497
|
import * as NextclawCore2 from "@nextclaw/core";
|
|
87
498
|
|
|
88
499
|
// src/ui/config.ts
|
|
89
500
|
import {
|
|
90
|
-
loadConfig,
|
|
91
|
-
saveConfig,
|
|
92
|
-
ConfigSchema,
|
|
501
|
+
loadConfig as loadConfig2,
|
|
502
|
+
saveConfig as saveConfig2,
|
|
503
|
+
ConfigSchema as ConfigSchema2,
|
|
93
504
|
probeFeishu,
|
|
94
505
|
LiteLLMProvider,
|
|
95
506
|
buildConfigSchema,
|
|
@@ -432,7 +843,7 @@ function resolveRuntimeConfig(config, draftConfig) {
|
|
|
432
843
|
return config;
|
|
433
844
|
}
|
|
434
845
|
const merged = deepMerge(config, draftConfig);
|
|
435
|
-
return
|
|
846
|
+
return ConfigSchema2.parse(merged);
|
|
436
847
|
}
|
|
437
848
|
function getActionById(config, actionId) {
|
|
438
849
|
const actions = buildConfigSchemaView(config).actions;
|
|
@@ -783,15 +1194,15 @@ async function executeConfigAction(configPath, actionId, request) {
|
|
|
783
1194
|
};
|
|
784
1195
|
}
|
|
785
1196
|
function loadConfigOrDefault(configPath) {
|
|
786
|
-
return
|
|
1197
|
+
return loadConfig2(configPath);
|
|
787
1198
|
}
|
|
788
1199
|
function updateModel(configPath, patch) {
|
|
789
1200
|
const config = loadConfigOrDefault(configPath);
|
|
790
1201
|
if (typeof patch.model === "string") {
|
|
791
1202
|
config.agents.defaults.model = patch.model;
|
|
792
1203
|
}
|
|
793
|
-
const next =
|
|
794
|
-
|
|
1204
|
+
const next = ConfigSchema2.parse(config);
|
|
1205
|
+
saveConfig2(next, configPath);
|
|
795
1206
|
return buildConfigView(next);
|
|
796
1207
|
}
|
|
797
1208
|
function updateSearch(configPath, patch) {
|
|
@@ -844,8 +1255,8 @@ function updateSearch(configPath, patch) {
|
|
|
844
1255
|
config.search.providers.brave.baseUrl = normalizeOptionalString(bravePatch.baseUrl) ?? "https://api.search.brave.com/res/v1/web/search";
|
|
845
1256
|
}
|
|
846
1257
|
}
|
|
847
|
-
const next =
|
|
848
|
-
|
|
1258
|
+
const next = ConfigSchema2.parse(config);
|
|
1259
|
+
saveConfig2(next, configPath);
|
|
849
1260
|
return buildSearchView(next);
|
|
850
1261
|
}
|
|
851
1262
|
function updateProvider(configPath, providerName, patch) {
|
|
@@ -878,8 +1289,8 @@ function updateProvider(configPath, providerName, patch) {
|
|
|
878
1289
|
if (Object.prototype.hasOwnProperty.call(patch, "modelThinking")) {
|
|
879
1290
|
provider.modelThinking = normalizeModelThinkingConfig(patch.modelThinking ?? {});
|
|
880
1291
|
}
|
|
881
|
-
const next =
|
|
882
|
-
|
|
1292
|
+
const next = ConfigSchema2.parse(config);
|
|
1293
|
+
saveConfig2(next, configPath);
|
|
883
1294
|
const uiHints = buildUiHints(next);
|
|
884
1295
|
const updated = next.providers[providerName];
|
|
885
1296
|
return toProviderView(next, updated, providerName, uiHints, spec ?? void 0);
|
|
@@ -898,8 +1309,8 @@ function createCustomProvider(configPath, patch = {}) {
|
|
|
898
1309
|
models: normalizeModelList(patch.models ?? []),
|
|
899
1310
|
modelThinking: normalizeModelThinkingConfig(patch.modelThinking ?? {})
|
|
900
1311
|
};
|
|
901
|
-
const next =
|
|
902
|
-
|
|
1312
|
+
const next = ConfigSchema2.parse(config);
|
|
1313
|
+
saveConfig2(next, configPath);
|
|
903
1314
|
const uiHints = buildUiHints(next);
|
|
904
1315
|
const created = next.providers[providerName];
|
|
905
1316
|
return {
|
|
@@ -918,8 +1329,8 @@ function deleteCustomProvider(configPath, providerName) {
|
|
|
918
1329
|
}
|
|
919
1330
|
delete providers[providerName];
|
|
920
1331
|
clearSecretRefsByPrefix(config, `providers.${providerName}`);
|
|
921
|
-
const next =
|
|
922
|
-
|
|
1332
|
+
const next = ConfigSchema2.parse(config);
|
|
1333
|
+
saveConfig2(next, configPath);
|
|
923
1334
|
return true;
|
|
924
1335
|
}
|
|
925
1336
|
function normalizeOptionalString(value) {
|
|
@@ -1069,8 +1480,8 @@ function updateChannel(configPath, channelName, patch) {
|
|
|
1069
1480
|
}
|
|
1070
1481
|
}
|
|
1071
1482
|
config.channels[channelName] = { ...channel, ...patch };
|
|
1072
|
-
const next =
|
|
1073
|
-
|
|
1483
|
+
const next = ConfigSchema2.parse(config);
|
|
1484
|
+
saveConfig2(next, configPath);
|
|
1074
1485
|
const uiHints = buildUiHints(next);
|
|
1075
1486
|
return sanitizePublicConfigValue(
|
|
1076
1487
|
next.channels[channelName],
|
|
@@ -1356,8 +1767,8 @@ function updateRuntime(configPath, patch) {
|
|
|
1356
1767
|
agentToAgent: nextAgentToAgent
|
|
1357
1768
|
};
|
|
1358
1769
|
}
|
|
1359
|
-
const next =
|
|
1360
|
-
|
|
1770
|
+
const next = ConfigSchema2.parse(config);
|
|
1771
|
+
saveConfig2(next, configPath);
|
|
1361
1772
|
const view = buildConfigView(next);
|
|
1362
1773
|
return {
|
|
1363
1774
|
agents: view.agents,
|
|
@@ -1391,8 +1802,8 @@ function updateSecrets(configPath, patch) {
|
|
|
1391
1802
|
if (Object.prototype.hasOwnProperty.call(patch, "refs")) {
|
|
1392
1803
|
config.secrets.refs = patch.refs ?? {};
|
|
1393
1804
|
}
|
|
1394
|
-
const next =
|
|
1395
|
-
|
|
1805
|
+
const next = ConfigSchema2.parse(config);
|
|
1806
|
+
saveConfig2(next, configPath);
|
|
1396
1807
|
return {
|
|
1397
1808
|
enabled: next.secrets.enabled,
|
|
1398
1809
|
defaults: { ...next.secrets.defaults },
|
|
@@ -1998,14 +2409,14 @@ var ChatRoutesController = class {
|
|
|
1998
2409
|
};
|
|
1999
2410
|
|
|
2000
2411
|
// src/ui/provider-auth.ts
|
|
2001
|
-
import { createHash, randomBytes, randomUUID } from "crypto";
|
|
2412
|
+
import { createHash, randomBytes as randomBytes2, randomUUID as randomUUID2 } from "crypto";
|
|
2002
2413
|
import { readFile } from "fs/promises";
|
|
2003
2414
|
import { homedir } from "os";
|
|
2004
2415
|
import { isAbsolute, resolve } from "path";
|
|
2005
2416
|
import {
|
|
2006
|
-
ConfigSchema as
|
|
2007
|
-
loadConfig as
|
|
2008
|
-
saveConfig as
|
|
2417
|
+
ConfigSchema as ConfigSchema3,
|
|
2418
|
+
loadConfig as loadConfig3,
|
|
2419
|
+
saveConfig as saveConfig3
|
|
2009
2420
|
} from "@nextclaw/core";
|
|
2010
2421
|
var authSessions = /* @__PURE__ */ new Map();
|
|
2011
2422
|
var DEFAULT_AUTH_INTERVAL_MS = 2e3;
|
|
@@ -2026,7 +2437,7 @@ function toBase64Url(buffer) {
|
|
|
2026
2437
|
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
2027
2438
|
}
|
|
2028
2439
|
function buildPkce() {
|
|
2029
|
-
const verifier = toBase64Url(
|
|
2440
|
+
const verifier = toBase64Url(randomBytes2(48));
|
|
2030
2441
|
const challenge = toBase64Url(createHash("sha256").update(verifier).digest());
|
|
2031
2442
|
return { verifier, challenge };
|
|
2032
2443
|
}
|
|
@@ -2199,7 +2610,7 @@ function readFieldAsString(source, fieldName) {
|
|
|
2199
2610
|
return trimmed.length > 0 ? trimmed : null;
|
|
2200
2611
|
}
|
|
2201
2612
|
function setProviderApiKey(params) {
|
|
2202
|
-
const config =
|
|
2613
|
+
const config = loadConfig3(params.configPath);
|
|
2203
2614
|
const providers = config.providers;
|
|
2204
2615
|
if (!providers[params.provider]) {
|
|
2205
2616
|
providers[params.provider] = {
|
|
@@ -2217,8 +2628,8 @@ function setProviderApiKey(params) {
|
|
|
2217
2628
|
if (!target.apiBase && params.defaultApiBase) {
|
|
2218
2629
|
target.apiBase = params.defaultApiBase;
|
|
2219
2630
|
}
|
|
2220
|
-
const next =
|
|
2221
|
-
|
|
2631
|
+
const next = ConfigSchema3.parse(config);
|
|
2632
|
+
saveConfig3(next, params.configPath);
|
|
2222
2633
|
}
|
|
2223
2634
|
async function startProviderAuth(configPath, providerName, options) {
|
|
2224
2635
|
cleanupExpiredAuthSessions();
|
|
@@ -2243,7 +2654,7 @@ async function startProviderAuth(configPath, providerName, options) {
|
|
|
2243
2654
|
if (!pkce) {
|
|
2244
2655
|
throw new Error("MiniMax OAuth requires PKCE");
|
|
2245
2656
|
}
|
|
2246
|
-
const state = toBase64Url(
|
|
2657
|
+
const state = toBase64Url(randomBytes2(16));
|
|
2247
2658
|
const body = new URLSearchParams({
|
|
2248
2659
|
response_type: "code",
|
|
2249
2660
|
client_id: resolvedMethod.clientId,
|
|
@@ -2257,7 +2668,7 @@ async function startProviderAuth(configPath, providerName, options) {
|
|
|
2257
2668
|
headers: {
|
|
2258
2669
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
2259
2670
|
Accept: "application/json",
|
|
2260
|
-
"x-request-id":
|
|
2671
|
+
"x-request-id": randomUUID2()
|
|
2261
2672
|
},
|
|
2262
2673
|
body
|
|
2263
2674
|
});
|
|
@@ -2309,7 +2720,7 @@ async function startProviderAuth(configPath, providerName, options) {
|
|
|
2309
2720
|
const expiresInSec = normalizePositiveInt(payload.expires_in, 600);
|
|
2310
2721
|
expiresAtMs = Date.now() + expiresInSec * 1e3;
|
|
2311
2722
|
}
|
|
2312
|
-
const sessionId =
|
|
2723
|
+
const sessionId = randomUUID2();
|
|
2313
2724
|
authSessions.set(sessionId, {
|
|
2314
2725
|
sessionId,
|
|
2315
2726
|
provider: providerName,
|
|
@@ -4046,7 +4457,9 @@ var SessionRoutesController = class {
|
|
|
4046
4457
|
function createUiRouter(options) {
|
|
4047
4458
|
const app = new Hono();
|
|
4048
4459
|
const marketplaceBaseUrl = normalizeMarketplaceBaseUrl(options);
|
|
4460
|
+
const authService = options.authService ?? new UiAuthService(options.configPath);
|
|
4049
4461
|
const appController = new AppRoutesController(options);
|
|
4462
|
+
const authController = new AuthRoutesController(authService);
|
|
4050
4463
|
const configController = new ConfigRoutesController(options);
|
|
4051
4464
|
const chatController = new ChatRoutesController(options);
|
|
4052
4465
|
const sessionController = new SessionRoutesController(options);
|
|
@@ -4054,8 +4467,27 @@ function createUiRouter(options) {
|
|
|
4054
4467
|
const pluginMarketplaceController = new PluginMarketplaceController(options, marketplaceBaseUrl);
|
|
4055
4468
|
const skillMarketplaceController = new SkillMarketplaceController(options, marketplaceBaseUrl);
|
|
4056
4469
|
app.notFound((c) => c.json(err("NOT_FOUND", "endpoint not found"), 404));
|
|
4470
|
+
app.use("/api/*", async (c, next) => {
|
|
4471
|
+
const path = c.req.path;
|
|
4472
|
+
if (path === "/api/health" || path.startsWith("/api/auth/")) {
|
|
4473
|
+
await next();
|
|
4474
|
+
return;
|
|
4475
|
+
}
|
|
4476
|
+
if (!authService.isProtectionEnabled() || authService.isRequestAuthenticated(c.req.raw)) {
|
|
4477
|
+
await next();
|
|
4478
|
+
return;
|
|
4479
|
+
}
|
|
4480
|
+
c.status(401);
|
|
4481
|
+
return c.json(err("UNAUTHORIZED", "Authentication required."), 401);
|
|
4482
|
+
});
|
|
4057
4483
|
app.get("/api/health", appController.health);
|
|
4058
4484
|
app.get("/api/app/meta", appController.appMeta);
|
|
4485
|
+
app.get("/api/auth/status", authController.getStatus);
|
|
4486
|
+
app.post("/api/auth/setup", authController.setup);
|
|
4487
|
+
app.post("/api/auth/login", authController.login);
|
|
4488
|
+
app.post("/api/auth/logout", authController.logout);
|
|
4489
|
+
app.put("/api/auth/password", authController.updatePassword);
|
|
4490
|
+
app.put("/api/auth/enabled", authController.updateEnabled);
|
|
4059
4491
|
app.get("/api/config", configController.getConfig);
|
|
4060
4492
|
app.get("/api/config/meta", configController.getConfigMeta);
|
|
4061
4493
|
app.get("/api/config/schema", configController.getConfigSchema);
|
|
@@ -4121,7 +4553,8 @@ function startUiServer(options) {
|
|
|
4121
4553
|
const app = new Hono2();
|
|
4122
4554
|
app.use("/*", compress());
|
|
4123
4555
|
const origin = options.corsOrigins ?? DEFAULT_CORS_ORIGINS;
|
|
4124
|
-
|
|
4556
|
+
const authService = new UiAuthService(options.configPath);
|
|
4557
|
+
app.use("/api/*", cors({ origin, credentials: true }));
|
|
4125
4558
|
const clients = /* @__PURE__ */ new Set();
|
|
4126
4559
|
const publish = (event) => {
|
|
4127
4560
|
const payload = JSON.stringify(event);
|
|
@@ -4139,7 +4572,8 @@ function startUiServer(options) {
|
|
|
4139
4572
|
publish,
|
|
4140
4573
|
marketplace: options.marketplace,
|
|
4141
4574
|
cronService: options.cronService,
|
|
4142
|
-
chatRuntime: options.chatRuntime
|
|
4575
|
+
chatRuntime: options.chatRuntime,
|
|
4576
|
+
authService
|
|
4143
4577
|
})
|
|
4144
4578
|
);
|
|
4145
4579
|
const staticDir = options.staticDir;
|
|
@@ -4179,9 +4613,23 @@ function startUiServer(options) {
|
|
|
4179
4613
|
port: options.port,
|
|
4180
4614
|
hostname: options.host
|
|
4181
4615
|
});
|
|
4182
|
-
const
|
|
4183
|
-
|
|
4184
|
-
|
|
4616
|
+
const httpServer = server;
|
|
4617
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
4618
|
+
httpServer.on("upgrade", (request, socket, head) => {
|
|
4619
|
+
const host = request.headers.host ?? "127.0.0.1";
|
|
4620
|
+
const url = request.url ?? "/";
|
|
4621
|
+
const pathname = new URL(url, `http://${host}`).pathname;
|
|
4622
|
+
if (pathname !== "/ws") {
|
|
4623
|
+
return;
|
|
4624
|
+
}
|
|
4625
|
+
if (!authService.isSocketAuthenticated(request)) {
|
|
4626
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
4627
|
+
socket.destroy();
|
|
4628
|
+
return;
|
|
4629
|
+
}
|
|
4630
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
4631
|
+
wss.emit("connection", ws, request);
|
|
4632
|
+
});
|
|
4185
4633
|
});
|
|
4186
4634
|
wss.on("connection", (socket) => {
|
|
4187
4635
|
clients.add(socket);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Nextclaw UI/API server.",
|
|
6
6
|
"type": "module",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"@hono/node-server": "^1.13.3",
|
|
19
19
|
"hono": "^4.6.2",
|
|
20
20
|
"ws": "^8.18.0",
|
|
21
|
-
"@nextclaw/openclaw-compat": "0.2.
|
|
22
|
-
"@nextclaw/runtime": "0.1.
|
|
23
|
-
"@nextclaw/core": "0.
|
|
21
|
+
"@nextclaw/openclaw-compat": "0.2.7",
|
|
22
|
+
"@nextclaw/runtime": "0.1.7",
|
|
23
|
+
"@nextclaw/core": "0.8.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/node": "^20.17.6",
|