@nextclaw/server 0.6.2 → 0.6.4

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
@@ -14,6 +14,10 @@ type ApiResponse<T> = {
14
14
  ok: false;
15
15
  error: ApiError;
16
16
  };
17
+ type AppMetaView = {
18
+ name: string;
19
+ productVersion: string;
20
+ };
17
21
  type ProviderConfigView = {
18
22
  displayName?: string;
19
23
  apiKeySet: boolean;
@@ -50,6 +54,31 @@ type ProviderConnectionTestResult = {
50
54
  latencyMs: number;
51
55
  message: string;
52
56
  };
57
+ type ProviderAuthStartResult = {
58
+ provider: string;
59
+ kind: "device_code";
60
+ sessionId: string;
61
+ verificationUri: string;
62
+ userCode: string;
63
+ expiresAt: string;
64
+ intervalMs: number;
65
+ note?: string;
66
+ };
67
+ type ProviderAuthPollRequest = {
68
+ sessionId: string;
69
+ };
70
+ type ProviderAuthPollResult = {
71
+ provider: string;
72
+ status: "pending" | "authorized" | "denied" | "expired" | "error";
73
+ message?: string;
74
+ nextPollMs?: number;
75
+ };
76
+ type ProviderAuthImportResult = {
77
+ provider: string;
78
+ status: "imported";
79
+ source: "cli";
80
+ expiresAt?: string;
81
+ };
53
82
  type AgentProfileView = {
54
83
  id: string;
55
84
  default?: boolean;
@@ -361,6 +390,20 @@ type ProviderSpecView = {
361
390
  isGateway?: boolean;
362
391
  isLocal?: boolean;
363
392
  defaultApiBase?: string;
393
+ logo?: string;
394
+ apiBaseHelp?: {
395
+ en?: string;
396
+ zh?: string;
397
+ };
398
+ auth?: {
399
+ kind: "device_code";
400
+ displayName?: string;
401
+ note?: {
402
+ en?: string;
403
+ zh?: string;
404
+ };
405
+ supportsCliImport?: boolean;
406
+ };
364
407
  defaultModels?: string[];
365
408
  supportsWireApi?: boolean;
366
409
  wireApiOptions?: Array<"auto" | "chat" | "responses">;
@@ -653,6 +696,7 @@ type UiServerOptions = {
653
696
  host: string;
654
697
  port: number;
655
698
  configPath: string;
699
+ productVersion?: string;
656
700
  corsOrigins?: string[] | "*";
657
701
  staticDir?: string;
658
702
  marketplace?: MarketplaceApiConfig;
@@ -670,6 +714,7 @@ declare function startUiServer(options: UiServerOptions): UiServerHandle;
670
714
 
671
715
  type UiRouterOptions = {
672
716
  configPath: string;
717
+ productVersion?: string;
673
718
  publish: (event: UiServerEvent) => void;
674
719
  marketplace?: MarketplaceApiConfig;
675
720
  cronService?: InstanceType<typeof NextclawCore.CronService>;
@@ -713,4 +758,4 @@ declare function deleteSession(configPath: string, key: string): boolean;
713
758
  declare function updateRuntime(configPath: string, patch: RuntimeConfigUpdate): Pick<ConfigView, "agents" | "bindings" | "session">;
714
759
  declare function updateSecrets(configPath: string, patch: SecretsConfigUpdate): SecretsView;
715
760
 
716
- export { type AgentBindingView, type AgentProfileView, type ApiError, type ApiResponse, type BindingPeerView, type ChannelSpecView, type ChatCapabilitiesView, type ChatRunListView, type ChatRunState, type ChatRunView, 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, 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 MarketplacePluginInstallRequest, type MarketplacePluginInstallResult, type MarketplacePluginManageAction, type MarketplacePluginManageRequest, type MarketplacePluginManageResult, type MarketplaceRecommendationView, type MarketplaceSkillContentView, type MarketplaceSkillInstallRequest, type MarketplaceSkillInstallResult, type MarketplaceSkillManageAction, type MarketplaceSkillManageRequest, type MarketplaceSkillManageResult, type MarketplaceSort, type ProviderConfigUpdate, type ProviderConfigView, type ProviderConnectionTestRequest, type ProviderConnectionTestResult, type ProviderCreateRequest, type ProviderCreateResult, type ProviderDeleteResult, type ProviderSpecView, type RuntimeConfigUpdate, 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, 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, updateSecrets };
761
+ export { type AgentBindingView, type AgentProfileView, type ApiError, type ApiResponse, type AppMetaView, type BindingPeerView, type ChannelSpecView, type ChatCapabilitiesView, type ChatRunListView, type ChatRunState, type ChatRunView, 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, 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 MarketplacePluginInstallRequest, type MarketplacePluginInstallResult, type MarketplacePluginManageAction, type MarketplacePluginManageRequest, type MarketplacePluginManageResult, type MarketplaceRecommendationView, type MarketplaceSkillContentView, type MarketplaceSkillInstallRequest, type MarketplaceSkillInstallResult, type MarketplaceSkillManageAction, type MarketplaceSkillManageRequest, type MarketplaceSkillManageResult, type MarketplaceSort, type ProviderAuthImportResult, type ProviderAuthPollRequest, type ProviderAuthPollResult, type ProviderAuthStartResult, type ProviderConfigUpdate, type ProviderConfigView, type ProviderConnectionTestRequest, type ProviderConnectionTestResult, type ProviderCreateRequest, type ProviderCreateResult, type ProviderDeleteResult, type ProviderSpecView, type RuntimeConfigUpdate, 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, 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, updateSecrets };
package/dist/index.js CHANGED
@@ -5,12 +5,12 @@ import { cors } from "hono/cors";
5
5
  import { serve } from "@hono/node-server";
6
6
  import { WebSocketServer, WebSocket } from "ws";
7
7
  import { existsSync, readFileSync } from "fs";
8
- import { readFile as readFile2, stat } from "fs/promises";
8
+ import { readFile as readFile3, stat } from "fs/promises";
9
9
  import { join } from "path";
10
10
 
11
11
  // src/ui/router.ts
12
12
  import { Hono } from "hono";
13
- import { readFile } from "fs/promises";
13
+ import { readFile as readFile2 } from "fs/promises";
14
14
  import * as NextclawCore from "@nextclaw/core";
15
15
  import { buildPluginStatusReport } from "@nextclaw/openclaw-compat";
16
16
 
@@ -21,9 +21,7 @@ import {
21
21
  ConfigSchema,
22
22
  probeFeishu,
23
23
  LiteLLMProvider,
24
- PROVIDERS,
25
24
  buildConfigSchema,
26
- findProviderByName,
27
25
  getProviderName,
28
26
  getPackageVersion,
29
27
  hasSecretRef,
@@ -31,22 +29,9 @@ import {
31
29
  SessionManager,
32
30
  getWorkspacePathFromConfig
33
31
  } from "@nextclaw/core";
32
+ import { findBuiltinProviderByName, listBuiltinProviders } from "@nextclaw/runtime";
34
33
  var MASK_MIN_LENGTH = 8;
35
34
  var EXTRA_SENSITIVE_PATH_PATTERNS = [/authorization/i, /cookie/i, /session/i, /bearer/i];
36
- var PROVIDER_TEST_MODEL_FALLBACKS = {
37
- nextclaw: "dashscope/qwen3.5-flash",
38
- openai: "gpt-5-mini",
39
- deepseek: "deepseek-chat",
40
- gemini: "gemini-3-flash-preview",
41
- zhipu: "glm-5",
42
- dashscope: "qwen3.5-flash",
43
- moonshot: "kimi-k2.5",
44
- minimax: "MiniMax-M2.5",
45
- groq: "llama-3.1-8b-instant",
46
- openrouter: "openai/gpt-5.3-codex",
47
- aihubmix: "gpt-5.3-codex",
48
- anthropic: "claude-opus-4-6"
49
- };
50
35
  var PREFERRED_PROVIDER_ORDER = [
51
36
  "nextclaw",
52
37
  "openai",
@@ -62,7 +47,8 @@ var PREFERRED_PROVIDER_ORDER = [
62
47
  var PREFERRED_PROVIDER_ORDER_INDEX = new Map(
63
48
  PREFERRED_PROVIDER_ORDER.map((name, index) => [name, index])
64
49
  );
65
- var BUILTIN_PROVIDER_NAMES = new Set(PROVIDERS.map((spec) => spec.name));
50
+ var BUILTIN_PROVIDERS = listBuiltinProviders();
51
+ var BUILTIN_PROVIDER_NAMES = new Set(BUILTIN_PROVIDERS.map((spec) => spec.name));
66
52
  var CUSTOM_PROVIDER_WIRE_API_OPTIONS = ["auto", "chat", "responses"];
67
53
  var CUSTOM_PROVIDER_PREFIX = "custom-";
68
54
  var PROVIDER_TEST_MAX_TOKENS = 16;
@@ -103,6 +89,33 @@ function findNextCustomProviderName(config) {
103
89
  }
104
90
  return `${CUSTOM_PROVIDER_PREFIX}${index}`;
105
91
  }
92
+ function createDefaultProviderConfig(defaultWireApi = "auto") {
93
+ return {
94
+ displayName: "",
95
+ apiKey: "",
96
+ apiBase: null,
97
+ extraHeaders: null,
98
+ wireApi: defaultWireApi,
99
+ models: []
100
+ };
101
+ }
102
+ function ensureProviderConfig(config, providerName) {
103
+ const providers = config.providers;
104
+ const existing = providers[providerName];
105
+ if (existing) {
106
+ return existing;
107
+ }
108
+ if (isCustomProviderName(providerName)) {
109
+ return null;
110
+ }
111
+ const spec = findBuiltinProviderByName(providerName);
112
+ if (!spec) {
113
+ return null;
114
+ }
115
+ const created = createDefaultProviderConfig(spec.defaultWireApi ?? "auto");
116
+ providers[providerName] = created;
117
+ return created;
118
+ }
106
119
  function clearSecretRefsByPrefix(config, pathPrefix) {
107
120
  for (const key of Object.keys(config.secrets.refs)) {
108
121
  if (key === pathPrefix || key.startsWith(`${pathPrefix}.`)) {
@@ -363,7 +376,7 @@ function buildConfigView(config) {
363
376
  const uiHints = buildUiHints(config);
364
377
  const providers = {};
365
378
  for (const [name, provider] of Object.entries(config.providers)) {
366
- const spec = findProviderByName(name);
379
+ const spec = findBuiltinProviderByName(name);
367
380
  providers[name] = toProviderView(config, provider, name, uiHints, spec);
368
381
  }
369
382
  return {
@@ -394,7 +407,7 @@ function clearSecretRef(config, path) {
394
407
  }
395
408
  function buildConfigMeta(config) {
396
409
  const configProviders = config.providers;
397
- const builtinProviders = PROVIDERS.map((spec) => {
410
+ const builtinProviders = BUILTIN_PROVIDERS.map((spec) => {
398
411
  const providerConfig = configProviders[spec.name];
399
412
  return {
400
413
  name: spec.name,
@@ -406,6 +419,14 @@ function buildConfigMeta(config) {
406
419
  isGateway: spec.isGateway,
407
420
  isLocal: spec.isLocal,
408
421
  defaultApiBase: spec.defaultApiBase,
422
+ logo: spec.logo,
423
+ apiBaseHelp: spec.apiBaseHelp,
424
+ auth: spec.auth ? {
425
+ kind: spec.auth.kind,
426
+ displayName: spec.auth.displayName,
427
+ note: spec.auth.note,
428
+ supportsCliImport: Boolean(spec.auth.cliCredential)
429
+ } : void 0,
409
430
  defaultModels: normalizeModelList(spec.defaultModels ?? []),
410
431
  supportsWireApi: spec.supportsWireApi,
411
432
  wireApiOptions: spec.wireApiOptions,
@@ -438,6 +459,9 @@ function buildConfigMeta(config) {
438
459
  isGateway: false,
439
460
  isLocal: false,
440
461
  defaultApiBase: void 0,
462
+ logo: void 0,
463
+ apiBaseHelp: void 0,
464
+ auth: void 0,
441
465
  defaultModels: [],
442
466
  supportsWireApi: true,
443
467
  wireApiOptions: CUSTOM_PROVIDER_WIRE_API_OPTIONS,
@@ -527,11 +551,11 @@ function updateModel(configPath, patch) {
527
551
  }
528
552
  function updateProvider(configPath, providerName, patch) {
529
553
  const config = loadConfigOrDefault(configPath);
530
- const provider = config.providers[providerName];
554
+ const provider = ensureProviderConfig(config, providerName);
531
555
  if (!provider) {
532
556
  return null;
533
557
  }
534
- const spec = findProviderByName(providerName);
558
+ const spec = findBuiltinProviderByName(providerName);
535
559
  const isCustom = isCustomProviderName(providerName);
536
560
  if (Object.prototype.hasOwnProperty.call(patch, "displayName") && isCustom) {
537
561
  provider.displayName = normalizeOptionalDisplayName(patch.displayName) ?? "";
@@ -653,7 +677,8 @@ function resolveTestModel(config, providerName, requestedModel, provider, spec)
653
677
  if (isCustomProviderName(providerName)) {
654
678
  return null;
655
679
  }
656
- return PROVIDER_TEST_MODEL_FALLBACKS[providerName] ?? defaultModel ?? null;
680
+ const specDefaultModel = normalizeModelList(spec?.defaultModels ?? [])[0] ?? null;
681
+ return specDefaultModel ?? defaultModel ?? null;
657
682
  }
658
683
  function stringifyError(error) {
659
684
  const raw = error instanceof Error ? error.message : String(error);
@@ -661,11 +686,11 @@ function stringifyError(error) {
661
686
  }
662
687
  async function testProviderConnection(configPath, providerName, patch) {
663
688
  const config = loadConfigOrDefault(configPath);
664
- const provider = config.providers[providerName];
689
+ const provider = ensureProviderConfig(config, providerName);
665
690
  if (!provider) {
666
691
  return null;
667
692
  }
668
- const spec = findProviderByName(providerName);
693
+ const spec = findBuiltinProviderByName(providerName);
669
694
  const hasApiKeyPatch = Object.prototype.hasOwnProperty.call(patch, "apiKey");
670
695
  const providedApiKey = normalizeOptionalString(patch.apiKey);
671
696
  const currentApiKey = normalizeOptionalString(provider.apiKey);
@@ -1003,7 +1028,336 @@ function updateSecrets(configPath, patch) {
1003
1028
  };
1004
1029
  }
1005
1030
 
1031
+ // src/ui/provider-auth.ts
1032
+ import { createHash, randomBytes, randomUUID } from "crypto";
1033
+ import { readFile } from "fs/promises";
1034
+ import { homedir } from "os";
1035
+ import { isAbsolute, resolve } from "path";
1036
+ import {
1037
+ ConfigSchema as ConfigSchema2,
1038
+ loadConfig as loadConfig2,
1039
+ saveConfig as saveConfig2
1040
+ } from "@nextclaw/core";
1041
+ import { findBuiltinProviderByName as findBuiltinProviderByName2 } from "@nextclaw/runtime";
1042
+ var authSessions = /* @__PURE__ */ new Map();
1043
+ var DEFAULT_AUTH_INTERVAL_MS = 2e3;
1044
+ var MAX_AUTH_INTERVAL_MS = 1e4;
1045
+ function normalizePositiveInt(value, fallback) {
1046
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
1047
+ return fallback;
1048
+ }
1049
+ return Math.floor(value);
1050
+ }
1051
+ function toBase64Url(buffer) {
1052
+ return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
1053
+ }
1054
+ function buildPkce() {
1055
+ const verifier = toBase64Url(randomBytes(48));
1056
+ const challenge = toBase64Url(createHash("sha256").update(verifier).digest());
1057
+ return { verifier, challenge };
1058
+ }
1059
+ function withTrailingSlash(value) {
1060
+ return value.endsWith("/") ? value : `${value}/`;
1061
+ }
1062
+ function cleanupExpiredAuthSessions(now = Date.now()) {
1063
+ for (const [sessionId, session] of authSessions.entries()) {
1064
+ if (session.expiresAtMs <= now) {
1065
+ authSessions.delete(sessionId);
1066
+ }
1067
+ }
1068
+ }
1069
+ function resolveDeviceCodeEndpoints(baseUrl, deviceCodePath, tokenPath) {
1070
+ const deviceCodeEndpoint = new URL(deviceCodePath, withTrailingSlash(baseUrl)).toString();
1071
+ const tokenEndpoint = new URL(tokenPath, withTrailingSlash(baseUrl)).toString();
1072
+ return { deviceCodeEndpoint, tokenEndpoint };
1073
+ }
1074
+ function resolveAuthNote(params) {
1075
+ return params.zh ?? params.en;
1076
+ }
1077
+ function resolveHomePath(inputPath) {
1078
+ const trimmed = inputPath.trim();
1079
+ if (!trimmed) {
1080
+ return trimmed;
1081
+ }
1082
+ if (trimmed === "~") {
1083
+ return homedir();
1084
+ }
1085
+ if (trimmed.startsWith("~/")) {
1086
+ return resolve(homedir(), trimmed.slice(2));
1087
+ }
1088
+ if (isAbsolute(trimmed)) {
1089
+ return trimmed;
1090
+ }
1091
+ return resolve(trimmed);
1092
+ }
1093
+ function normalizeExpiresAt(value) {
1094
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
1095
+ return Math.floor(value);
1096
+ }
1097
+ if (typeof value === "string" && value.trim()) {
1098
+ const asNumber = Number(value);
1099
+ if (Number.isFinite(asNumber) && asNumber > 0) {
1100
+ return Math.floor(asNumber);
1101
+ }
1102
+ const parsedTime = Date.parse(value);
1103
+ if (Number.isFinite(parsedTime) && parsedTime > 0) {
1104
+ return parsedTime;
1105
+ }
1106
+ }
1107
+ return null;
1108
+ }
1109
+ function readFieldAsString(source, fieldName) {
1110
+ if (!fieldName) {
1111
+ return null;
1112
+ }
1113
+ const rawValue = source[fieldName];
1114
+ if (typeof rawValue !== "string") {
1115
+ return null;
1116
+ }
1117
+ const trimmed = rawValue.trim();
1118
+ return trimmed.length > 0 ? trimmed : null;
1119
+ }
1120
+ function setProviderApiKey(params) {
1121
+ const config = loadConfig2(params.configPath);
1122
+ const providers = config.providers;
1123
+ if (!providers[params.provider]) {
1124
+ providers[params.provider] = {
1125
+ displayName: "",
1126
+ apiKey: "",
1127
+ apiBase: null,
1128
+ extraHeaders: null,
1129
+ wireApi: "auto",
1130
+ models: []
1131
+ };
1132
+ }
1133
+ const target = providers[params.provider];
1134
+ target.apiKey = params.accessToken;
1135
+ if (!target.apiBase && params.defaultApiBase) {
1136
+ target.apiBase = params.defaultApiBase;
1137
+ }
1138
+ const next = ConfigSchema2.parse(config);
1139
+ saveConfig2(next, params.configPath);
1140
+ }
1141
+ async function startProviderAuth(configPath, providerName) {
1142
+ cleanupExpiredAuthSessions();
1143
+ const spec = findBuiltinProviderByName2(providerName);
1144
+ if (!spec?.auth || spec.auth.kind !== "device_code") {
1145
+ return null;
1146
+ }
1147
+ const { deviceCodeEndpoint, tokenEndpoint } = resolveDeviceCodeEndpoints(
1148
+ spec.auth.baseUrl,
1149
+ spec.auth.deviceCodePath,
1150
+ spec.auth.tokenPath
1151
+ );
1152
+ const pkce = spec.auth.usePkce ? buildPkce() : null;
1153
+ const body = new URLSearchParams({
1154
+ client_id: spec.auth.clientId,
1155
+ scope: spec.auth.scope
1156
+ });
1157
+ if (pkce) {
1158
+ body.set("code_challenge", pkce.challenge);
1159
+ body.set("code_challenge_method", "S256");
1160
+ }
1161
+ const response = await fetch(deviceCodeEndpoint, {
1162
+ method: "POST",
1163
+ headers: {
1164
+ "Content-Type": "application/x-www-form-urlencoded",
1165
+ Accept: "application/json"
1166
+ },
1167
+ body
1168
+ });
1169
+ const payload = await response.json().catch(() => ({}));
1170
+ if (!response.ok) {
1171
+ const message = payload.error_description || payload.error || response.statusText || "device code auth failed";
1172
+ throw new Error(message);
1173
+ }
1174
+ const deviceCode = payload.device_code?.trim() ?? "";
1175
+ const userCode = payload.user_code?.trim() ?? "";
1176
+ const verificationUri = payload.verification_uri_complete?.trim() || payload.verification_uri?.trim() || "";
1177
+ if (!deviceCode || !userCode || !verificationUri) {
1178
+ throw new Error("provider auth payload is incomplete");
1179
+ }
1180
+ const intervalMs = normalizePositiveInt(payload.interval, DEFAULT_AUTH_INTERVAL_MS / 1e3) * 1e3;
1181
+ const expiresInSec = normalizePositiveInt(payload.expires_in, 600);
1182
+ const expiresAtMs = Date.now() + expiresInSec * 1e3;
1183
+ const sessionId = randomUUID();
1184
+ authSessions.set(sessionId, {
1185
+ sessionId,
1186
+ provider: providerName,
1187
+ configPath,
1188
+ deviceCode,
1189
+ codeVerifier: pkce?.verifier,
1190
+ tokenEndpoint,
1191
+ clientId: spec.auth.clientId,
1192
+ grantType: spec.auth.grantType,
1193
+ expiresAtMs,
1194
+ intervalMs
1195
+ });
1196
+ return {
1197
+ provider: providerName,
1198
+ kind: "device_code",
1199
+ sessionId,
1200
+ verificationUri,
1201
+ userCode,
1202
+ expiresAt: new Date(expiresAtMs).toISOString(),
1203
+ intervalMs,
1204
+ note: resolveAuthNote(spec.auth.note ?? {})
1205
+ };
1206
+ }
1207
+ async function pollProviderAuth(params) {
1208
+ cleanupExpiredAuthSessions();
1209
+ const session = authSessions.get(params.sessionId);
1210
+ if (!session || session.provider !== params.providerName || session.configPath !== params.configPath) {
1211
+ return null;
1212
+ }
1213
+ if (Date.now() >= session.expiresAtMs) {
1214
+ authSessions.delete(params.sessionId);
1215
+ return {
1216
+ provider: params.providerName,
1217
+ status: "expired",
1218
+ message: "authorization session expired"
1219
+ };
1220
+ }
1221
+ const body = new URLSearchParams({
1222
+ grant_type: session.grantType,
1223
+ client_id: session.clientId,
1224
+ device_code: session.deviceCode
1225
+ });
1226
+ if (session.codeVerifier) {
1227
+ body.set("code_verifier", session.codeVerifier);
1228
+ }
1229
+ const response = await fetch(session.tokenEndpoint, {
1230
+ method: "POST",
1231
+ headers: {
1232
+ "Content-Type": "application/x-www-form-urlencoded",
1233
+ Accept: "application/json"
1234
+ },
1235
+ body
1236
+ });
1237
+ const payload = await response.json().catch(() => ({}));
1238
+ if (!response.ok) {
1239
+ const errorCode = payload.error?.trim().toLowerCase();
1240
+ if (errorCode === "authorization_pending") {
1241
+ return {
1242
+ provider: params.providerName,
1243
+ status: "pending",
1244
+ nextPollMs: session.intervalMs
1245
+ };
1246
+ }
1247
+ if (errorCode === "slow_down") {
1248
+ const nextPollMs = Math.min(Math.floor(session.intervalMs * 1.5), MAX_AUTH_INTERVAL_MS);
1249
+ session.intervalMs = nextPollMs;
1250
+ authSessions.set(params.sessionId, session);
1251
+ return {
1252
+ provider: params.providerName,
1253
+ status: "pending",
1254
+ nextPollMs
1255
+ };
1256
+ }
1257
+ if (errorCode === "access_denied") {
1258
+ authSessions.delete(params.sessionId);
1259
+ return {
1260
+ provider: params.providerName,
1261
+ status: "denied",
1262
+ message: payload.error_description || "authorization denied"
1263
+ };
1264
+ }
1265
+ if (errorCode === "expired_token") {
1266
+ authSessions.delete(params.sessionId);
1267
+ return {
1268
+ provider: params.providerName,
1269
+ status: "expired",
1270
+ message: payload.error_description || "authorization session expired"
1271
+ };
1272
+ }
1273
+ return {
1274
+ provider: params.providerName,
1275
+ status: "error",
1276
+ message: payload.error_description || payload.error || response.statusText || "authorization failed"
1277
+ };
1278
+ }
1279
+ const accessToken = payload.access_token?.trim();
1280
+ if (!accessToken) {
1281
+ return {
1282
+ provider: params.providerName,
1283
+ status: "error",
1284
+ message: "provider token response missing access token"
1285
+ };
1286
+ }
1287
+ const spec = findBuiltinProviderByName2(params.providerName);
1288
+ setProviderApiKey({
1289
+ configPath: params.configPath,
1290
+ provider: params.providerName,
1291
+ accessToken,
1292
+ defaultApiBase: spec?.defaultApiBase
1293
+ });
1294
+ authSessions.delete(params.sessionId);
1295
+ return {
1296
+ provider: params.providerName,
1297
+ status: "authorized"
1298
+ };
1299
+ }
1300
+ async function importProviderAuthFromCli(configPath, providerName) {
1301
+ const spec = findBuiltinProviderByName2(providerName);
1302
+ if (!spec?.auth || spec.auth.kind !== "device_code" || !spec.auth.cliCredential) {
1303
+ return null;
1304
+ }
1305
+ const credentialPath = resolveHomePath(spec.auth.cliCredential.path);
1306
+ if (!credentialPath) {
1307
+ throw new Error("provider cli credential path is empty");
1308
+ }
1309
+ let rawContent = "";
1310
+ try {
1311
+ rawContent = await readFile(credentialPath, "utf8");
1312
+ } catch (error) {
1313
+ const message = error instanceof Error ? error.message : String(error);
1314
+ throw new Error(`failed to read CLI credential: ${message}`);
1315
+ }
1316
+ let payload;
1317
+ try {
1318
+ const parsed = JSON.parse(rawContent);
1319
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1320
+ throw new Error("credential payload is not an object");
1321
+ }
1322
+ payload = parsed;
1323
+ } catch (error) {
1324
+ const message = error instanceof Error ? error.message : String(error);
1325
+ throw new Error(`invalid CLI credential JSON: ${message}`);
1326
+ }
1327
+ const accessToken = readFieldAsString(payload, spec.auth.cliCredential.accessTokenField);
1328
+ if (!accessToken) {
1329
+ throw new Error(
1330
+ `CLI credential missing access token field: ${spec.auth.cliCredential.accessTokenField}`
1331
+ );
1332
+ }
1333
+ const expiresAtMs = normalizeExpiresAt(
1334
+ spec.auth.cliCredential.expiresAtField ? payload[spec.auth.cliCredential.expiresAtField] : void 0
1335
+ );
1336
+ if (typeof expiresAtMs === "number" && expiresAtMs <= Date.now()) {
1337
+ throw new Error("CLI credential has expired, please login again");
1338
+ }
1339
+ setProviderApiKey({
1340
+ configPath,
1341
+ provider: providerName,
1342
+ accessToken,
1343
+ defaultApiBase: spec.defaultApiBase
1344
+ });
1345
+ return {
1346
+ provider: providerName,
1347
+ status: "imported",
1348
+ source: "cli",
1349
+ expiresAt: expiresAtMs ? new Date(expiresAtMs).toISOString() : void 0
1350
+ };
1351
+ }
1352
+
1006
1353
  // src/ui/router.ts
1354
+ function buildAppMetaView(options) {
1355
+ const productVersion = options.productVersion?.trim();
1356
+ return {
1357
+ name: "NextClaw",
1358
+ productVersion: productVersion && productVersion.length > 0 ? productVersion : "0.0.0"
1359
+ };
1360
+ }
1007
1361
  var DEFAULT_MARKETPLACE_API_BASE = "https://marketplace-api.nextclaw.io";
1008
1362
  var NEXTCLAW_PLUGIN_NPM_PREFIX = "@nextclaw/channel-plugin-";
1009
1363
  var CLAWBAY_CHANNEL_PLUGIN_NPM_SPEC = "@clawbay/clawbay-channel";
@@ -1672,7 +2026,7 @@ async function loadLocalSkillMarkdown(options, skillName) {
1672
2026
  return null;
1673
2027
  }
1674
2028
  try {
1675
- const raw = await readFile(skillInfo.path, "utf-8");
2029
+ const raw = await readFile2(skillInfo.path, "utf-8");
1676
2030
  return {
1677
2031
  raw,
1678
2032
  source: skillInfo.source
@@ -2283,6 +2637,7 @@ function createUiRouter(options) {
2283
2637
  const marketplaceBaseUrl = normalizeMarketplaceBaseUrl(options);
2284
2638
  app.notFound((c) => c.json(err("NOT_FOUND", "endpoint not found"), 404));
2285
2639
  app.get("/api/health", (c) => c.json(ok({ status: "ok" })));
2640
+ app.get("/api/app/meta", (c) => c.json(ok(buildAppMetaView(options))));
2286
2641
  app.get("/api/config", (c) => {
2287
2642
  const config = loadConfigOrDefault(options.configPath);
2288
2643
  return c.json(ok(buildConfigView(config)));
@@ -2370,6 +2725,56 @@ function createUiRouter(options) {
2370
2725
  }
2371
2726
  return c.json(ok(result));
2372
2727
  });
2728
+ app.post("/api/config/providers/:provider/auth/start", async (c) => {
2729
+ const provider = c.req.param("provider");
2730
+ try {
2731
+ const result = await startProviderAuth(options.configPath, provider);
2732
+ if (!result) {
2733
+ return c.json(err("NOT_SUPPORTED", `provider auth is not supported: ${provider}`), 404);
2734
+ }
2735
+ return c.json(ok(result));
2736
+ } catch (error) {
2737
+ const message = error instanceof Error ? error.message : String(error);
2738
+ return c.json(err("AUTH_START_FAILED", message), 400);
2739
+ }
2740
+ });
2741
+ app.post("/api/config/providers/:provider/auth/poll", async (c) => {
2742
+ const provider = c.req.param("provider");
2743
+ const body = await readJson(c.req.raw);
2744
+ if (!body.ok) {
2745
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2746
+ }
2747
+ const sessionId = typeof body.data.sessionId === "string" ? body.data.sessionId.trim() : "";
2748
+ if (!sessionId) {
2749
+ return c.json(err("INVALID_BODY", "sessionId is required"), 400);
2750
+ }
2751
+ const result = await pollProviderAuth({
2752
+ configPath: options.configPath,
2753
+ providerName: provider,
2754
+ sessionId
2755
+ });
2756
+ if (!result) {
2757
+ return c.json(err("NOT_FOUND", "provider auth session not found"), 404);
2758
+ }
2759
+ if (result.status === "authorized") {
2760
+ options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
2761
+ }
2762
+ return c.json(ok(result));
2763
+ });
2764
+ app.post("/api/config/providers/:provider/auth/import-cli", async (c) => {
2765
+ const provider = c.req.param("provider");
2766
+ try {
2767
+ const result = await importProviderAuthFromCli(options.configPath, provider);
2768
+ if (!result) {
2769
+ return c.json(err("NOT_SUPPORTED", `provider cli auth import is not supported: ${provider}`), 404);
2770
+ }
2771
+ options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
2772
+ return c.json(ok(result));
2773
+ } catch (error) {
2774
+ const message = error instanceof Error ? error.message : String(error);
2775
+ return c.json(err("AUTH_IMPORT_FAILED", message), 400);
2776
+ }
2777
+ });
2373
2778
  app.put("/api/config/channels/:channel", async (c) => {
2374
2779
  const channel = c.req.param("channel");
2375
2780
  const body = await readJson(c.req.raw);
@@ -2997,6 +3402,7 @@ function startUiServer(options) {
2997
3402
  "/",
2998
3403
  createUiRouter({
2999
3404
  configPath: options.configPath,
3405
+ productVersion: options.productVersion,
3000
3406
  publish,
3001
3407
  marketplace: options.marketplace,
3002
3408
  cronService: options.cronService,
@@ -3013,7 +3419,7 @@ function startUiServer(options) {
3013
3419
  join,
3014
3420
  getContent: async (path) => {
3015
3421
  try {
3016
- return await readFile2(path);
3422
+ return await readFile3(path);
3017
3423
  } catch {
3018
3424
  return null;
3019
3425
  }
@@ -3052,9 +3458,9 @@ function startUiServer(options) {
3052
3458
  host: options.host,
3053
3459
  port: options.port,
3054
3460
  publish,
3055
- close: () => new Promise((resolve) => {
3461
+ close: () => new Promise((resolve2) => {
3056
3462
  wss.close(() => {
3057
- server.close(() => resolve());
3463
+ server.close(() => resolve2());
3058
3464
  });
3059
3465
  })
3060
3466
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/server",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "private": false,
5
5
  "description": "Nextclaw UI/API server.",
6
6
  "type": "module",
@@ -16,9 +16,10 @@
16
16
  "dependencies": {
17
17
  "@hono/node-server": "^1.13.3",
18
18
  "@nextclaw/openclaw-compat": "^0.2.0",
19
+ "@nextclaw/runtime": "^0.1.1",
19
20
  "hono": "^4.6.2",
20
21
  "ws": "^8.18.0",
21
- "@nextclaw/core": "^0.7.0"
22
+ "@nextclaw/core": "^0.7.1"
22
23
  },
23
24
  "devDependencies": {
24
25
  "@types/node": "^20.17.6",