@goodandready/dsh-clinebot 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/lib/client.js +415 -137
- package/lib/cline-client.js +20 -1
- package/lib/index.js +1 -3
- package/lib/models.js +10 -3
- package/lib/provider-sync.js +56 -20
- package/lib/routes/accounts.js +170 -2
- package/lib/routes/auth.js +15 -1
- package/lib/routes/models.js +1 -1
- package/lib/routes/settings.js +35 -3
- package/lib/slash-command.js +128 -64
- package/package.json +1 -1
package/lib/cline-client.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
-
CLINE_MODELS,
|
|
10
9
|
DEFAULT_MODEL_ID,
|
|
11
10
|
PROVIDER_ID,
|
|
12
11
|
PROVIDER_DISPLAY_NAME,
|
|
@@ -90,6 +89,26 @@ export async function saveCredentialKey(ctx, apiKeyEnv, apiKey) {
|
|
|
90
89
|
return { ok: true, envName: name }
|
|
91
90
|
}
|
|
92
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Delete API key from DSH credentials service.
|
|
94
|
+
*/
|
|
95
|
+
export async function deleteCredentialKey(ctx, apiKeyEnv) {
|
|
96
|
+
const name = String(apiKeyEnv || '').trim()
|
|
97
|
+
if (!name) return { ok: false }
|
|
98
|
+
const credentials = ctx?.credentials || ctx?.get?.('credentials')
|
|
99
|
+
if (!credentials) return { ok: false }
|
|
100
|
+
try {
|
|
101
|
+
const ref = await toCredentialRef(name)
|
|
102
|
+
if (typeof credentials.unset === 'function') {
|
|
103
|
+
await credentials.unset(ref)
|
|
104
|
+
return { ok: true, envName: name }
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.warn('[dsh-clinebot] Failed unsetting credential:', err)
|
|
108
|
+
}
|
|
109
|
+
return { ok: false }
|
|
110
|
+
}
|
|
111
|
+
|
|
93
112
|
function abortAfter(ms) {
|
|
94
113
|
const ac = new AbortController()
|
|
95
114
|
const timer = setTimeout(() => ac.abort(), Math.max(1, Number(ms) || DEFAULT_TIMEOUT_MS))
|
package/lib/index.js
CHANGED
|
@@ -40,9 +40,7 @@ export function apply(ctx, config) {
|
|
|
40
40
|
try {
|
|
41
41
|
const pub = publicConfig(cfg)
|
|
42
42
|
if (!pub.enabled) {
|
|
43
|
-
|
|
44
|
-
await removePiAiProvider(ctx)
|
|
45
|
-
}
|
|
43
|
+
await removePiAiProvider(ctx)
|
|
46
44
|
return
|
|
47
45
|
}
|
|
48
46
|
const activeKey = await resolveActiveAccountKey(ctx, cfg)
|
package/lib/models.js
CHANGED
|
@@ -168,12 +168,12 @@ export function parsePlanIncludedModels(includedInput) {
|
|
|
168
168
|
const clean = includedText
|
|
169
169
|
.replace(/^includes\s+/i, '')
|
|
170
170
|
.replace(/\band\b/gi, ',')
|
|
171
|
-
.replace(
|
|
171
|
+
.replace(/[.;]+$/g, '')
|
|
172
172
|
.trim()
|
|
173
173
|
|
|
174
174
|
const parts = clean
|
|
175
175
|
.split(',')
|
|
176
|
-
.map((p) => p.trim())
|
|
176
|
+
.map((p) => p.trim().replace(/[.;]+$/g, '').trim())
|
|
177
177
|
.filter(Boolean)
|
|
178
178
|
|
|
179
179
|
const normalize = (s) =>
|
|
@@ -193,7 +193,14 @@ export function parsePlanIncludedModels(includedInput) {
|
|
|
193
193
|
matched.push(found)
|
|
194
194
|
} else {
|
|
195
195
|
// Dynamic fallback for newly introduced models mentioned in plan
|
|
196
|
-
const idPart = name
|
|
196
|
+
const idPart = name
|
|
197
|
+
.toLowerCase()
|
|
198
|
+
.trim()
|
|
199
|
+
.replace(/^cline-pass\//, '')
|
|
200
|
+
.replace(/qwen\s+([0-9])/i, 'qwen$1')
|
|
201
|
+
.replace(/glm\s+([0-9])/i, 'glm-$1')
|
|
202
|
+
.replace(/[\s_]+/g, '-')
|
|
203
|
+
.replace(/-+/g, '-')
|
|
197
204
|
const isVision = /(vision|vl|multimodal|omni|image)/i.test(name)
|
|
198
205
|
const hasReasoning = /(reason|think|r1|pro|flash|max|plus|k3|m3|glm|qwen|mimo)/i.test(name)
|
|
199
206
|
matched.push({
|
package/lib/provider-sync.js
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
buildPiAiProvider,
|
|
18
18
|
sessionStats,
|
|
19
19
|
getLastRotation,
|
|
20
|
+
usageCache,
|
|
21
|
+
DEFAULT_API_KEY_ENV,
|
|
20
22
|
} from './cline-client.js'
|
|
21
23
|
|
|
22
24
|
export function resolvePathWithHome(p) {
|
|
@@ -30,27 +32,48 @@ export function resolvePathWithHome(p) {
|
|
|
30
32
|
|
|
31
33
|
export async function checkRegisteredInPiAi(ctx) {
|
|
32
34
|
const settings = ctx?.get?.('settings')
|
|
33
|
-
if (!settings?.get) return false
|
|
34
35
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
if (typeof settings?.describe === 'function') {
|
|
37
|
+
const descriptors = settings.describe()
|
|
38
|
+
const piAiDesc = Array.isArray(descriptors) ? descriptors.find((d) => d?.ns === LLM_PI_AI_NS) : null
|
|
39
|
+
if (piAiDesc?.value?.providers?.[PROVIDER_ID]) return true
|
|
40
|
+
}
|
|
41
|
+
if (typeof settings?.get === 'function') {
|
|
42
|
+
const piAi = settings.get(LLM_PI_AI_NS)
|
|
43
|
+
if (piAi?.providers?.[PROVIDER_ID]) return true
|
|
44
|
+
}
|
|
45
|
+
const llm = ctx?.get?.('llm')
|
|
46
|
+
if (typeof llm?.listProviders === 'function') {
|
|
47
|
+
const list = await llm.listProviders()
|
|
48
|
+
if (Array.isArray(list) && list.some((p) => (p?.id || p) === PROVIDER_ID)) {
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
37
52
|
} catch {
|
|
38
53
|
return false
|
|
39
54
|
}
|
|
55
|
+
return false
|
|
40
56
|
}
|
|
41
57
|
|
|
42
58
|
export async function resolveActiveAccountKey(ctx, cfg, pool = null) {
|
|
43
59
|
const accountPool = pool || await resolveAccountPool(ctx, cfg)
|
|
44
60
|
const configured = accountPool.filter((acc) => acc.present && acc.value)
|
|
45
61
|
if (!configured.length) {
|
|
46
|
-
|
|
62
|
+
const fallbackEnv = publicConfig(cfg).apiKeyEnv || DEFAULT_API_KEY_ENV
|
|
63
|
+
return { envName: fallbackEnv, apiKeyEnv: fallbackEnv, value: '', source: 'none', id: 'default' }
|
|
47
64
|
}
|
|
48
65
|
const pub = publicConfig(cfg)
|
|
66
|
+
let chosen = configured[0]
|
|
49
67
|
if (pub.activeAccount) {
|
|
50
68
|
const pinned = configured.find((acc) => acc.apiKeyEnv === pub.activeAccount || acc.id === pub.activeAccount)
|
|
51
|
-
if (pinned)
|
|
69
|
+
if (pinned) chosen = pinned
|
|
70
|
+
}
|
|
71
|
+
const keyEnv = chosen.apiKeyEnv || chosen.envName || DEFAULT_API_KEY_ENV
|
|
72
|
+
return {
|
|
73
|
+
...chosen,
|
|
74
|
+
envName: keyEnv,
|
|
75
|
+
apiKeyEnv: keyEnv,
|
|
52
76
|
}
|
|
53
|
-
return configured[0]
|
|
54
77
|
}
|
|
55
78
|
|
|
56
79
|
export async function buildStatus(ctx, cfg) {
|
|
@@ -101,14 +124,20 @@ export async function buildStatus(ctx, cfg) {
|
|
|
101
124
|
present: !!(activeAcc.value || key.value),
|
|
102
125
|
source: activeAcc.source || key.source,
|
|
103
126
|
},
|
|
104
|
-
accounts: pool.map((acc) =>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
127
|
+
accounts: pool.map((acc) => {
|
|
128
|
+
const cacheKey = `cline:usage:${(acc.value || '').slice(-8)}`
|
|
129
|
+
const cached = usageCache.get(cacheKey)?.data
|
|
130
|
+
const pct = cached?.windows?.fiveHour?.percentUsed
|
|
131
|
+
return {
|
|
132
|
+
id: acc.id,
|
|
133
|
+
label: acc.label,
|
|
134
|
+
apiKeyEnv: acc.apiKeyEnv,
|
|
135
|
+
present: acc.present,
|
|
136
|
+
source: acc.source,
|
|
137
|
+
isPinned: acc.isPinned,
|
|
138
|
+
percentUsed: typeof pct === 'number' ? pct : null,
|
|
139
|
+
}
|
|
140
|
+
}),
|
|
112
141
|
activeAccount: activeAcc.apiKeyEnv,
|
|
113
142
|
health,
|
|
114
143
|
usage: publicUsage(usage),
|
|
@@ -158,12 +187,19 @@ export async function removePiAiProvider(ctx) {
|
|
|
158
187
|
throw new Error('DSH settings service unavailable')
|
|
159
188
|
}
|
|
160
189
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
190
|
+
try {
|
|
191
|
+
await settings.mutate(LLM_PI_AI_NS, [
|
|
192
|
+
{
|
|
193
|
+
op: 'remove',
|
|
194
|
+
path: ['providers', PROVIDER_ID],
|
|
195
|
+
},
|
|
196
|
+
])
|
|
197
|
+
} catch (err) {
|
|
198
|
+
const msg = String(err?.message || err).toLowerCase()
|
|
199
|
+
if (!msg.includes('not found') && !msg.includes('absent') && !msg.includes('enoent') && !msg.includes('does not exist')) {
|
|
200
|
+
throw err
|
|
201
|
+
}
|
|
202
|
+
}
|
|
167
203
|
return { ok: true }
|
|
168
204
|
}
|
|
169
205
|
|
package/lib/routes/accounts.js
CHANGED
|
@@ -1,10 +1,178 @@
|
|
|
1
1
|
import { writeJson, readBody } from '../http.js'
|
|
2
|
-
import {
|
|
2
|
+
import { assertTrustedSettingsRequest } from '../access.js'
|
|
3
3
|
import { publicConfig, plainConfig, Config } from '../config.js'
|
|
4
4
|
import { upsertPiAiProvider, removePiAiProvider } from '../provider-sync.js'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
clearUsageCache,
|
|
7
|
+
clearProbeCache,
|
|
8
|
+
saveCredentialKey,
|
|
9
|
+
deleteCredentialKey,
|
|
10
|
+
smokeChat,
|
|
11
|
+
DEFAULT_API_KEY_ENV,
|
|
12
|
+
} from '../cline-client.js'
|
|
6
13
|
|
|
7
14
|
export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProviderState }) {
|
|
15
|
+
// POST /dsh-clinebot/accounts — add or update account in pool
|
|
16
|
+
ctx.effect(() => ctx.webServer.register({
|
|
17
|
+
kind: 'exact',
|
|
18
|
+
path: '/dsh-clinebot/accounts',
|
|
19
|
+
handler: async (req, res) => {
|
|
20
|
+
if (req.method === 'DELETE') {
|
|
21
|
+
return handleDeleteAccount(req, res)
|
|
22
|
+
}
|
|
23
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST or DELETE only' })
|
|
24
|
+
if (!assertTrustedSettingsRequest(req, res)) return
|
|
25
|
+
const settingsApi = getSettingsApi()
|
|
26
|
+
if (!settingsApi || typeof settingsApi.replace !== 'function') {
|
|
27
|
+
return writeJson(res, 503, { ok: false, error: 'Settings service unavailable or read-only' })
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const bodyBuf = await readBody(req)
|
|
31
|
+
let body = {}
|
|
32
|
+
try { body = JSON.parse(bodyBuf.toString('utf8')) } catch { body = {} }
|
|
33
|
+
|
|
34
|
+
const apiKey = String(body.apiKey || '').trim()
|
|
35
|
+
if (!apiKey) {
|
|
36
|
+
return writeJson(res, 400, { ok: false, error: 'API key is required' })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const curr = plainConfig(live())
|
|
40
|
+
const existingAccounts = Array.isArray(curr.accounts) ? [...curr.accounts] : []
|
|
41
|
+
|
|
42
|
+
let targetEnv = String(body.apiKeyEnv || '').trim()
|
|
43
|
+
if (!targetEnv) {
|
|
44
|
+
let maxN = 1
|
|
45
|
+
for (const acc of existingAccounts) {
|
|
46
|
+
const m = String(acc?.apiKeyEnv || '').match(/^CLINEBOT_API_KEY_(\d+)$/)
|
|
47
|
+
if (m) {
|
|
48
|
+
const num = parseInt(m[1], 10)
|
|
49
|
+
if (num >= maxN) maxN = num + 1
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (curr.apiKeyEnv && curr.apiKeyEnv.startsWith('CLINEBOT_API_KEY_')) {
|
|
53
|
+
const m = curr.apiKeyEnv.match(/^CLINEBOT_API_KEY_(\d+)$/)
|
|
54
|
+
if (m) {
|
|
55
|
+
const num = parseInt(m[1], 10)
|
|
56
|
+
if (num >= maxN) maxN = num + 1
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
targetEnv = `CLINEBOT_API_KEY_${maxN}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!/^CLINEBOT_API_KEY(_[A-Z0-9]+)?$/.test(targetEnv)) {
|
|
63
|
+
return writeJson(res, 400, {
|
|
64
|
+
ok: false,
|
|
65
|
+
error: `Disallowed apiKeyEnv: "${targetEnv}". Must match ^CLINEBOT_API_KEY(_[A-Z0-9]+)?$`,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const label = String(body.label || '').trim() || targetEnv
|
|
70
|
+
|
|
71
|
+
await saveCredentialKey(ctx, targetEnv, apiKey)
|
|
72
|
+
|
|
73
|
+
const idx = existingAccounts.findIndex((a) => a && a.apiKeyEnv === targetEnv)
|
|
74
|
+
const accountEntry = { label, apiKeyEnv: targetEnv }
|
|
75
|
+
if (idx >= 0) {
|
|
76
|
+
existingAccounts[idx] = accountEntry
|
|
77
|
+
} else {
|
|
78
|
+
existingAccounts.push(accountEntry)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const next = Config({ ...curr, accounts: existingAccounts })
|
|
82
|
+
await settingsApi.replace(next)
|
|
83
|
+
await syncProviderState(next)
|
|
84
|
+
|
|
85
|
+
let validation = { ok: false, error: 'Skipped validation: baseUrl is not https' }
|
|
86
|
+
try {
|
|
87
|
+
const parsed = new URL(curr.baseUrl)
|
|
88
|
+
if (parsed.protocol === 'https:') {
|
|
89
|
+
validation = await smokeChat(curr.baseUrl, apiKey, {
|
|
90
|
+
model: curr.defaultModel,
|
|
91
|
+
timeoutMs: 15000,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
validation = { ok: false, error: 'Invalid baseUrl' }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
clearUsageCache()
|
|
99
|
+
clearProbeCache()
|
|
100
|
+
|
|
101
|
+
writeJson(res, 200, {
|
|
102
|
+
ok: true,
|
|
103
|
+
account: accountEntry,
|
|
104
|
+
validated: validation.ok,
|
|
105
|
+
latencyMs: validation.latencyMs,
|
|
106
|
+
validationError: validation.ok ? null : validation.error,
|
|
107
|
+
})
|
|
108
|
+
} catch (err) {
|
|
109
|
+
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
}), 'dsh-clinebot: /accounts')
|
|
113
|
+
|
|
114
|
+
async function handleDeleteAccount(req, res, targetEnvFromUrl = '') {
|
|
115
|
+
if (!assertTrustedSettingsRequest(req, res)) return
|
|
116
|
+
const settingsApi = getSettingsApi()
|
|
117
|
+
if (!settingsApi || typeof settingsApi.replace !== 'function') {
|
|
118
|
+
return writeJson(res, 503, { ok: false, error: 'Settings service unavailable or read-only' })
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
let body = {}
|
|
122
|
+
if (req.method === 'POST' || req.method === 'DELETE') {
|
|
123
|
+
try {
|
|
124
|
+
const bodyBuf = await readBody(req)
|
|
125
|
+
body = JSON.parse(bodyBuf.toString('utf8'))
|
|
126
|
+
} catch {
|
|
127
|
+
body = {}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const targetEnv = String(targetEnvFromUrl || body.apiKeyEnv || '').trim()
|
|
131
|
+
if (!targetEnv) {
|
|
132
|
+
return writeJson(res, 400, { ok: false, error: 'apiKeyEnv is required' })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const curr = plainConfig(live())
|
|
136
|
+
if (targetEnv === curr.apiKeyEnv || targetEnv === DEFAULT_API_KEY_ENV) {
|
|
137
|
+
return writeJson(res, 400, { ok: false, error: 'Cannot delete the primary account' })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const existingAccounts = Array.isArray(curr.accounts) ? curr.accounts : []
|
|
141
|
+
const nextAccounts = existingAccounts.filter((a) => a && a.apiKeyEnv !== targetEnv)
|
|
142
|
+
let activeAccount = curr.activeAccount
|
|
143
|
+
if (activeAccount === targetEnv) {
|
|
144
|
+
activeAccount = ''
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const next = Config({ ...curr, accounts: nextAccounts, activeAccount })
|
|
148
|
+
await settingsApi.replace(next)
|
|
149
|
+
await syncProviderState(next)
|
|
150
|
+
|
|
151
|
+
if (body.deleteSecret) {
|
|
152
|
+
await deleteCredentialKey(ctx, targetEnv)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
clearUsageCache()
|
|
156
|
+
clearProbeCache()
|
|
157
|
+
|
|
158
|
+
writeJson(res, 200, { ok: true, removed: targetEnv })
|
|
159
|
+
} catch (err) {
|
|
160
|
+
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// POST /dsh-clinebot/accounts/delete — delete account endpoint
|
|
165
|
+
ctx.effect(() => ctx.webServer.register({
|
|
166
|
+
kind: 'exact',
|
|
167
|
+
path: '/dsh-clinebot/accounts/delete',
|
|
168
|
+
handler: async (req, res) => {
|
|
169
|
+
if (req.method !== 'POST' && req.method !== 'DELETE') {
|
|
170
|
+
return writeJson(res, 405, { ok: false, error: 'POST or DELETE only' })
|
|
171
|
+
}
|
|
172
|
+
return handleDeleteAccount(req, res)
|
|
173
|
+
},
|
|
174
|
+
}), 'dsh-clinebot: /accounts/delete')
|
|
175
|
+
|
|
8
176
|
// POST /dsh-clinebot/accounts/active — switch or pin active account
|
|
9
177
|
ctx.effect(() => ctx.webServer.register({
|
|
10
178
|
kind: 'exact',
|
package/lib/routes/auth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeJson, readBody } from '../http.js'
|
|
2
|
-
import {
|
|
2
|
+
import { assertTrustedSettingsRequest } from '../access.js'
|
|
3
3
|
import { publicConfig } from '../config.js'
|
|
4
4
|
import { resolveActiveAccountKey } from '../provider-sync.js'
|
|
5
5
|
import {
|
|
@@ -29,6 +29,20 @@ export function registerAuthRoutes(ctx, { live, getSettingsApi, syncProviderStat
|
|
|
29
29
|
const pub = publicConfig(live())
|
|
30
30
|
const base = normalizeBaseUrl(pub.baseUrl)
|
|
31
31
|
|
|
32
|
+
let parsedBase
|
|
33
|
+
try {
|
|
34
|
+
parsedBase = new URL(base)
|
|
35
|
+
} catch {
|
|
36
|
+
return writeJson(res, 400, { ok: false, valid: false, error: 'Invalid baseUrl' })
|
|
37
|
+
}
|
|
38
|
+
if (parsedBase.protocol !== 'https:' && parsedBase.hostname !== 'localhost' && parsedBase.hostname !== '127.0.0.1') {
|
|
39
|
+
return writeJson(res, 400, {
|
|
40
|
+
ok: false,
|
|
41
|
+
valid: false,
|
|
42
|
+
error: 'Insecure baseUrl: key verification requires https://',
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
const ac = new AbortController()
|
|
33
47
|
const timer = setTimeout(() => ac.abort(), 6000)
|
|
34
48
|
try {
|
package/lib/routes/models.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeJson, readBody } from '../http.js'
|
|
2
|
-
import {
|
|
2
|
+
import { assertTrustedSettingsRequest } from '../access.js'
|
|
3
3
|
import { publicConfig, plainConfig, Config } from '../config.js'
|
|
4
4
|
import { resolveActiveAccountKey, resolvePathWithHome } from '../provider-sync.js'
|
|
5
5
|
import { getAllModels, saveModelsDiskCache } from '../models.js'
|
package/lib/routes/settings.js
CHANGED
|
@@ -64,6 +64,30 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
64
64
|
return writeJson(res, 400, { ok: false, error: `unknown config field: ${k}` })
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
if (rawPayload.apiKeyEnv && !/^CLINEBOT_API_KEY(_[A-Z0-9]+)?$/.test(String(rawPayload.apiKeyEnv))) {
|
|
68
|
+
return writeJson(res, 400, { ok: false, error: `Disallowed apiKeyEnv: "${rawPayload.apiKeyEnv}". Must match ^CLINEBOT_API_KEY(_[A-Z0-9]+)?$` })
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(rawPayload.accounts)) {
|
|
71
|
+
for (const acc of rawPayload.accounts) {
|
|
72
|
+
if (!acc || typeof acc !== 'object') {
|
|
73
|
+
return writeJson(res, 400, { ok: false, error: 'Each account must be an object' })
|
|
74
|
+
}
|
|
75
|
+
const env = String(acc.apiKeyEnv || '').trim()
|
|
76
|
+
if (!/^CLINEBOT_API_KEY(_[A-Z0-9]+)?$/.test(env)) {
|
|
77
|
+
return writeJson(res, 400, { ok: false, error: `Disallowed account apiKeyEnv: "${env}". Must match ^CLINEBOT_API_KEY(_[A-Z0-9]+)?$` })
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (rawPayload.baseUrl) {
|
|
82
|
+
try {
|
|
83
|
+
const parsed = new URL(rawPayload.baseUrl)
|
|
84
|
+
if (parsed.protocol !== 'https:' && parsed.hostname !== 'localhost' && parsed.hostname !== '127.0.0.1') {
|
|
85
|
+
return writeJson(res, 400, { ok: false, error: 'Insecure baseUrl: remote endpoint must use https://' })
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
return writeJson(res, 400, { ok: false, error: 'Invalid baseUrl' })
|
|
89
|
+
}
|
|
90
|
+
}
|
|
67
91
|
try {
|
|
68
92
|
const base = plainConfig(live()) || {}
|
|
69
93
|
delete base.enabledModels
|
|
@@ -99,13 +123,21 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
99
123
|
const targetEnvName = String(body.apiKeyEnv || pub.apiKeyEnv || DEFAULT_API_KEY_ENV).trim()
|
|
100
124
|
|
|
101
125
|
const isDefaultPattern = /^CLINEBOT_API_KEY(_[A-Z0-9]+)?$/.test(targetEnvName)
|
|
102
|
-
|
|
126
|
+
if (!isDefaultPattern) {
|
|
127
|
+
return writeJson(res, 400, {
|
|
128
|
+
ok: false,
|
|
129
|
+
error: `Disallowed apiKeyEnv: "${targetEnvName}". Must match ^CLINEBOT_API_KEY(_[A-Z0-9]+)?$.`,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const isConfiguredEnv = targetEnvName === DEFAULT_API_KEY_ENV ||
|
|
134
|
+
targetEnvName === pub.apiKeyEnv ||
|
|
103
135
|
(Array.isArray(pub.accounts) && pub.accounts.some(acc => acc && acc.apiKeyEnv === targetEnvName))
|
|
104
136
|
|
|
105
|
-
if (!
|
|
137
|
+
if (!isConfiguredEnv) {
|
|
106
138
|
return writeJson(res, 400, {
|
|
107
139
|
ok: false,
|
|
108
|
-
error: `Disallowed apiKeyEnv: "${targetEnvName}". Must
|
|
140
|
+
error: `Disallowed apiKeyEnv: "${targetEnvName}". Must be configured in settings accounts pool.`,
|
|
109
141
|
})
|
|
110
142
|
}
|
|
111
143
|
|