@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 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 ConfigSchema.parse(merged);
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 loadConfig(configPath);
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 = ConfigSchema.parse(config);
794
- saveConfig(next, configPath);
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 = ConfigSchema.parse(config);
848
- saveConfig(next, configPath);
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 = ConfigSchema.parse(config);
882
- saveConfig(next, configPath);
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 = ConfigSchema.parse(config);
902
- saveConfig(next, configPath);
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 = ConfigSchema.parse(config);
922
- saveConfig(next, configPath);
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 = ConfigSchema.parse(config);
1073
- saveConfig(next, configPath);
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 = ConfigSchema.parse(config);
1360
- saveConfig(next, configPath);
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 = ConfigSchema.parse(config);
1395
- saveConfig(next, configPath);
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 ConfigSchema2,
2007
- loadConfig as loadConfig2,
2008
- saveConfig as saveConfig2
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(randomBytes(48));
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 = loadConfig2(params.configPath);
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 = ConfigSchema2.parse(config);
2221
- saveConfig2(next, params.configPath);
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(randomBytes(16));
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": randomUUID()
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 = randomUUID();
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
- app.use("/api/*", cors({ origin }));
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 wss = new WebSocketServer({
4183
- server,
4184
- path: "/ws"
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.6.13",
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.6",
22
- "@nextclaw/runtime": "0.1.6",
23
- "@nextclaw/core": "0.7.7"
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",