@goodandready/dsh-model-sync 0.5.4 → 0.5.6
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/lib/adapter-registry.js +22 -6
- package/lib/client.js +14 -0
- package/lib/diff-notifier.js +27 -1
- package/lib/http.js +33 -5
- package/lib/index.js +19 -6
- package/lib/inventory.js +3 -2
- package/lib/synchronizer-helpers.js +16 -5
- package/lib/synchronizer.js +14 -2
- package/lib/updater.js +0 -1
- package/package.json +1 -1
package/lib/adapter-registry.js
CHANGED
|
@@ -12,7 +12,7 @@ const PROVIDER_ADAPTERS = Object.freeze({
|
|
|
12
12
|
clinebot: {
|
|
13
13
|
endpoint: 'https://api.cline.bot/api/v1/users/me/plan',
|
|
14
14
|
auth: 'bearer',
|
|
15
|
-
|
|
15
|
+
parse: 'clinebot-plan',
|
|
16
16
|
defaultModels: [
|
|
17
17
|
'cline-pass/deepseek-v4-flash',
|
|
18
18
|
'cline-pass/deepseek-v4-pro',
|
|
@@ -99,12 +99,13 @@ function mappedRows(provider, rows, descriptor) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
function parseRows(provider, payload, descriptor) {
|
|
102
|
-
|
|
102
|
+
const parserKind = descriptor.parse || descriptor.parser
|
|
103
|
+
if (parserKind === 'clinebot-plan') {
|
|
103
104
|
const plan = payload?.data?.plan || payload?.data || payload?.plan || payload
|
|
104
105
|
const included = plan?.features?.included || plan?.includedModels || []
|
|
105
106
|
return parseClinePassModels(included)
|
|
106
107
|
}
|
|
107
|
-
if (
|
|
108
|
+
if (parserKind === 'google') {
|
|
108
109
|
const rows = Array.isArray(payload?.models) ? payload.models : []
|
|
109
110
|
return normalizeModels(provider, rows.map((row) => ({
|
|
110
111
|
...row,
|
|
@@ -136,6 +137,24 @@ export function normalizeAdapterConfig(config, profile = {}) {
|
|
|
136
137
|
if (endpoint.protocol === 'http:' && !privateHttpHost(endpoint.hostname)) {
|
|
137
138
|
throw Object.assign(new Error('adapter endpoint must use https outside loopback'), { code: 'ADAPTER_CONFIG_INVALID' })
|
|
138
139
|
}
|
|
140
|
+
if (profile?.provider && PROVIDER_ADAPTERS[profile.provider]) {
|
|
141
|
+
const defaultEndpoint = PROVIDER_ADAPTERS[profile.provider].endpoint
|
|
142
|
+
let defaultUrl
|
|
143
|
+
try { defaultUrl = new URL(defaultEndpoint) } catch (_err) { defaultUrl = null }
|
|
144
|
+
if (defaultUrl) {
|
|
145
|
+
const knownHost = defaultUrl.hostname.toLowerCase()
|
|
146
|
+
const targetHost = endpoint.hostname.toLowerCase()
|
|
147
|
+
const isDomainMatch = targetHost === knownHost || targetHost.endsWith('.' + knownHost.replace(/^api\./, ''))
|
|
148
|
+
const isLoopback = privateHttpHost(targetHost)
|
|
149
|
+
const isExplicitlyAllowed = source.allowCustomHost === true || source.allowCustomEndpoint === true
|
|
150
|
+
if (!isDomainMatch && !isLoopback && !isExplicitlyAllowed) {
|
|
151
|
+
throw Object.assign(
|
|
152
|
+
new Error(`adapter endpoint host "${endpoint.hostname}" does not match known provider domain for "${profile.provider}"`),
|
|
153
|
+
{ code: 'ADAPTER_HOST_MISMATCH' }
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
139
158
|
const auth = source.auth === undefined ? 'bearer' : String(source.auth)
|
|
140
159
|
if (!SAFE_AUTH.has(auth)) throw Object.assign(new Error('adapter auth mode is unsupported'), { code: 'ADAPTER_CONFIG_INVALID' })
|
|
141
160
|
const parse = source.parser === undefined ? 'openai' : String(source.parser)
|
|
@@ -224,9 +243,6 @@ async function fetchSpecific(profile, descriptor, options) {
|
|
|
224
243
|
return rows
|
|
225
244
|
}
|
|
226
245
|
} catch (error) {
|
|
227
|
-
if (descriptor.defaultModels && Array.isArray(descriptor.defaultModels) && descriptor.defaultModels.length > 0) {
|
|
228
|
-
return normalizeModels(profile.provider, descriptor.defaultModels.map((id) => ({ id })))
|
|
229
|
-
}
|
|
230
246
|
throw error
|
|
231
247
|
}
|
|
232
248
|
if (descriptor.defaultModels && Array.isArray(descriptor.defaultModels) && descriptor.defaultModels.length > 0) {
|
package/lib/client.js
CHANGED
|
@@ -785,6 +785,20 @@ window.__ModuleLoader__.load({
|
|
|
785
785
|
.finally(() => setBusy(false))
|
|
786
786
|
}
|
|
787
787
|
|
|
788
|
+
const resolveSmartAlias = (aliasName) => {
|
|
789
|
+
setResolvingKey(aliasName)
|
|
790
|
+
fetch(`${API}/aliases/resolve?alias=${encodeURIComponent(aliasName)}`)
|
|
791
|
+
.then(async (r) => {
|
|
792
|
+
const res = await r.json()
|
|
793
|
+
if (!r.ok) throw new Error(res.error?.message ?? `HTTP ${r.status}`)
|
|
794
|
+
setResolvedAliases((prev) => ({ ...prev, [aliasName]: res }))
|
|
795
|
+
})
|
|
796
|
+
.catch((e) => {
|
|
797
|
+
setResolvedAliases((prev) => ({ ...prev, [aliasName]: { error: String(e?.message ?? e) } }))
|
|
798
|
+
})
|
|
799
|
+
.finally(() => setResolvingKey(''))
|
|
800
|
+
}
|
|
801
|
+
|
|
788
802
|
const load = () => {
|
|
789
803
|
if (typeof document !== 'undefined' && document.hidden) return
|
|
790
804
|
setError('')
|
package/lib/diff-notifier.js
CHANGED
|
@@ -114,6 +114,20 @@ export async function sendWebhookNotification(url, secret, payload, { fetchImpl
|
|
|
114
114
|
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
115
115
|
return { ok: false, error: "URL must use http or https protocol" }
|
|
116
116
|
}
|
|
117
|
+
const hostname = parsedUrl.hostname.toLowerCase().replace(/^\[|\]$/g, '')
|
|
118
|
+
if (
|
|
119
|
+
hostname === '169.254.169.254' ||
|
|
120
|
+
hostname.startsWith('169.254.') ||
|
|
121
|
+
hostname === 'metadata.google.internal' ||
|
|
122
|
+
hostname === '0.0.0.0' ||
|
|
123
|
+
hostname === '255.255.255.255'
|
|
124
|
+
) {
|
|
125
|
+
return { ok: false, error: "webhook url targets a forbidden metadata or link-local address" }
|
|
126
|
+
}
|
|
127
|
+
const isLoopback = hostname === '127.0.0.1' || hostname === 'localhost' || hostname === '::1' || hostname.startsWith('127.') || hostname.startsWith('::ffff:127.')
|
|
128
|
+
if (parsedUrl.protocol === "http:" && !isLoopback) {
|
|
129
|
+
return { ok: false, error: "insecure http webhook url is only permitted on loopback" }
|
|
130
|
+
}
|
|
117
131
|
} catch (err) {
|
|
118
132
|
return { ok: false, error: "Invalid URL: " + (err?.message || String(err)) }
|
|
119
133
|
}
|
|
@@ -181,9 +195,21 @@ export async function dispatchDiffNotifications(ctx, config, results, { fetchImp
|
|
|
181
195
|
|
|
182
196
|
let webhookResult = null
|
|
183
197
|
if (config?.diffWebhookUrl && typeof config.diffWebhookUrl === "string" && config.diffWebhookUrl.trim()) {
|
|
198
|
+
let secret = config.diffWebhookSecret || ''
|
|
199
|
+
const secretRef = config.diffWebhookSecretRef || (typeof config.diffWebhookSecret === 'string' && /^[A-Za-z_][A-Za-z0-9_]*$/.test(config.diffWebhookSecret) ? config.diffWebhookSecret : '')
|
|
200
|
+
if (secretRef) {
|
|
201
|
+
try {
|
|
202
|
+
const res = await (ctx?.credentials?.resolve ? ctx.credentials.resolve(secretRef) : ctx?.get?.('credentials')?.resolve?.(secretRef))
|
|
203
|
+
if (res) secret = typeof res === 'string' ? res : res?.value || secret
|
|
204
|
+
} catch (err) {
|
|
205
|
+
if (typeof ctx?.logger?.debug === 'function') {
|
|
206
|
+
ctx.logger.debug('[dsh-model-sync] webhook secret resolution fallback:', err)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
184
210
|
webhookResult = await sendWebhookNotification(
|
|
185
211
|
config.diffWebhookUrl,
|
|
186
|
-
|
|
212
|
+
secret,
|
|
187
213
|
{
|
|
188
214
|
event: "model-sync.diff",
|
|
189
215
|
...events,
|
package/lib/http.js
CHANGED
|
@@ -2,8 +2,31 @@ import { validateAliasName } from './alias-resolver.js'
|
|
|
2
2
|
function normalizeWebhookTestRequest(body) {
|
|
3
3
|
if (!body || typeof body !== "object") throw Object.assign(new Error("body is required"), { code: "INVALID_REQUEST" })
|
|
4
4
|
if (typeof body.url !== "string" || !body.url.trim()) throw Object.assign(new Error("url is required"), { code: "INVALID_REQUEST" })
|
|
5
|
+
let parsed
|
|
6
|
+
try {
|
|
7
|
+
parsed = new URL(body.url.trim())
|
|
8
|
+
} catch (err) {
|
|
9
|
+
throw Object.assign(new Error("Invalid URL: " + (err?.message || String(err))), { code: "INVALID_REQUEST" })
|
|
10
|
+
}
|
|
11
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
12
|
+
throw Object.assign(new Error("URL must use http or https protocol"), { code: "INVALID_REQUEST" })
|
|
13
|
+
}
|
|
14
|
+
const hostname = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, '')
|
|
15
|
+
const isLoopback = hostname === '127.0.0.1' || hostname === 'localhost' || hostname === '::1' || hostname.startsWith('127.') || hostname.startsWith('::ffff:127.')
|
|
16
|
+
if (parsed.protocol === "http:" && !isLoopback) {
|
|
17
|
+
throw Object.assign(new Error("insecure http webhook url is only permitted on loopback"), { code: "INVALID_REQUEST" })
|
|
18
|
+
}
|
|
19
|
+
if (
|
|
20
|
+
hostname === '169.254.169.254' ||
|
|
21
|
+
hostname.startsWith('169.254.') ||
|
|
22
|
+
hostname === 'metadata.google.internal' ||
|
|
23
|
+
hostname === '0.0.0.0' ||
|
|
24
|
+
hostname === '255.255.255.255'
|
|
25
|
+
) {
|
|
26
|
+
throw Object.assign(new Error("webhook url targets a forbidden metadata or link-local address"), { code: "INVALID_REQUEST" })
|
|
27
|
+
}
|
|
5
28
|
return {
|
|
6
|
-
url:
|
|
29
|
+
url: parsed.toString(),
|
|
7
30
|
secret: typeof body.secret === "string" ? body.secret.trim() : "",
|
|
8
31
|
}
|
|
9
32
|
}
|
|
@@ -28,7 +51,9 @@ export function isLoopbackAddress(ip) {
|
|
|
28
51
|
export function trusted(req, { requireLoopback = false } = {}) {
|
|
29
52
|
const remoteAddress = req?.socket?.remoteAddress
|
|
30
53
|
if (requireLoopback || req?.method === 'POST') {
|
|
31
|
-
if (!isLoopbackAddress(remoteAddress)) return false
|
|
54
|
+
if (remoteAddress && !isLoopbackAddress(remoteAddress)) return false
|
|
55
|
+
if (!remoteAddress && req?.socket) return false
|
|
56
|
+
if (!remoteAddress && !isLoopbackAddress(req?.headers?.host?.split(':')[0])) return false
|
|
32
57
|
}
|
|
33
58
|
const fetchSite = req.headers?.['sec-fetch-site']
|
|
34
59
|
if (fetchSite === 'cross-site') return false
|
|
@@ -59,8 +84,10 @@ export function trusted(req, { requireLoopback = false } = {}) {
|
|
|
59
84
|
if (fetchSite === 'same-origin' || fetchSite === 'same-site') return true
|
|
60
85
|
// If sec-fetch-mode is present but origin/referer missing on a cross-site request, reject
|
|
61
86
|
if (req.headers?.['sec-fetch-mode'] && !host) return false
|
|
62
|
-
// For
|
|
63
|
-
if (req?.method === 'POST' && !origin && !referer
|
|
87
|
+
// For requests requiring loopback or POST without any browser origin/referer headers, require explicit loopback
|
|
88
|
+
if ((requireLoopback || req?.method === 'POST') && !origin && !referer) {
|
|
89
|
+
if (!isLoopbackAddress(remoteAddress ?? (req?.headers?.host ? req.headers.host.split(':')[0] : ''))) return false
|
|
90
|
+
}
|
|
64
91
|
return true
|
|
65
92
|
}
|
|
66
93
|
|
|
@@ -247,7 +274,8 @@ export function registerHttpApi(ctx, synchronizer, { getSchedulerStatus = () =>
|
|
|
247
274
|
return []
|
|
248
275
|
}
|
|
249
276
|
const makeHandler = (route) => async (req, res) => {
|
|
250
|
-
|
|
277
|
+
const isPublicStatus = route === 'status' && req.method === 'GET'
|
|
278
|
+
if (!trusted(req, { requireLoopback: !isPublicStatus })) return json(res, 403, { error: { code: 'forbidden', message: 'dsh-model-sync: cross-origin or non-loopback request rejected' } })
|
|
251
279
|
if (route === 'status') {
|
|
252
280
|
if (req.method !== 'GET') return json(res, 405, { error: { code: 'method', message: 'GET only' } })
|
|
253
281
|
const payload = { ...synchronizer.status(), scheduler: getSchedulerStatus(), providers: synchronizer.listProviders() }
|
package/lib/index.js
CHANGED
|
@@ -27,7 +27,8 @@ export const Config = Schema.object({
|
|
|
27
27
|
historyLimit: Schema.number().default(50).volatile(),
|
|
28
28
|
notificationLimit: Schema.number().default(50).volatile(),
|
|
29
29
|
diffWebhookUrl: Schema.string().default().volatile(),
|
|
30
|
-
|
|
30
|
+
diffWebhookSecretRef: Schema.string().role('credential-ref').default('').volatile(),
|
|
31
|
+
diffWebhookSecret: Schema.string().role('credential-ref').default('').volatile(),
|
|
31
32
|
notifyOnModelAdded: Schema.boolean().default(true).volatile(),
|
|
32
33
|
notifyOnModelDeprecated: Schema.boolean().default(true).volatile(),
|
|
33
34
|
notifyOnPriceChanged: Schema.boolean().default(true).volatile(),
|
|
@@ -123,7 +124,16 @@ export function plainConfig(value) {
|
|
|
123
124
|
|
|
124
125
|
export function apply(ctx, config) {
|
|
125
126
|
const baseConfig = structuredClone(plainConfig(config || {}))
|
|
126
|
-
const resolveConfig = (value) =>
|
|
127
|
+
const resolveConfig = (value) => {
|
|
128
|
+
try {
|
|
129
|
+
return plainConfig(Config(plainConfig(value) || {}))
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (typeof ctx?.logger?.warn === "function") {
|
|
132
|
+
ctx.logger.warn("[dsh-model-sync] invalid config payload, fallback to current config:", err?.message || err)
|
|
133
|
+
}
|
|
134
|
+
return liveConfig ?? baseConfig
|
|
135
|
+
}
|
|
136
|
+
}
|
|
127
137
|
let liveConfig = resolveConfig(baseConfig)
|
|
128
138
|
let reconfigure = () => {}
|
|
129
139
|
let saveConfigImpl = async (patch) => {
|
|
@@ -165,12 +175,16 @@ export function apply(ctx, config) {
|
|
|
165
175
|
reconfigure = () => scheduler.reconfigure()
|
|
166
176
|
scheduler.start()
|
|
167
177
|
|
|
168
|
-
|
|
169
|
-
ctx.on
|
|
178
|
+
ctx.effect(() => {
|
|
179
|
+
if (typeof ctx.on !== 'function') return () => {}
|
|
180
|
+
const dispose = ctx.on('loader/volatile-update', () => {
|
|
170
181
|
liveConfig = resolveConfig(config)
|
|
171
182
|
reconfigure()
|
|
172
183
|
})
|
|
173
|
-
|
|
184
|
+
return () => {
|
|
185
|
+
if (typeof dispose === 'function') dispose()
|
|
186
|
+
}
|
|
187
|
+
}, 'dsh-model-sync: volatile-update listener')
|
|
174
188
|
|
|
175
189
|
// settings.register was removed before 0.1.7-rc.2. The host hands the profile entry
|
|
176
190
|
// config to apply() directly, and liveConfig above is derived from it; the
|
|
@@ -189,7 +203,6 @@ export function apply(ctx, config) {
|
|
|
189
203
|
manifestUrl: new URL('../package.json', import.meta.url),
|
|
190
204
|
}), 'dsh-model-sync: plugin updater')
|
|
191
205
|
ctx.effect(() => {
|
|
192
|
-
if (!liveConfig.enabled) return () => {}
|
|
193
206
|
ctx.provide('modelSync', service)
|
|
194
207
|
ctx.provide('modelSyncRunner', synchronizer)
|
|
195
208
|
return () => {}
|
package/lib/inventory.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeModels } from './models.js'
|
|
2
|
+
import { sectionOf } from './synchronizer-helpers.js'
|
|
2
3
|
|
|
3
4
|
export const API_KEY_PROVIDER_IDS = Object.freeze([
|
|
4
5
|
'anthropic',
|
|
@@ -42,7 +43,7 @@ const API_KEY_PROVIDER_SET = new Set(API_KEY_PROVIDER_IDS)
|
|
|
42
43
|
|
|
43
44
|
function settingsProfiles(settings) {
|
|
44
45
|
try {
|
|
45
|
-
return settings
|
|
46
|
+
return sectionOf(settings, 'llm-pi-ai')?.providers ?? {}
|
|
46
47
|
} catch {
|
|
47
48
|
return {}
|
|
48
49
|
}
|
|
@@ -66,7 +67,7 @@ export function listApiKeyProviders(llm, settings) {
|
|
|
66
67
|
for (const entry of directory) {
|
|
67
68
|
let profile = profiles[entry.provider]
|
|
68
69
|
if (!profile && (entry.provider === 'commandcode' || entry.settingsNs === 'llm-commandcode')) {
|
|
69
|
-
const cc = settings
|
|
70
|
+
const cc = sectionOf(settings, 'llm-commandcode')
|
|
70
71
|
profile = {
|
|
71
72
|
apiKeyEnv: cc?.apiKeyEnv ?? 'COMMANDCODE_API_KEY',
|
|
72
73
|
credentialRef: cc?.apiKeyEnv ?? 'COMMANDCODE_API_KEY',
|
|
@@ -55,12 +55,23 @@ export function descriptorOf(settings, ns) {
|
|
|
55
55
|
catch { return undefined }
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
export function sectionOf(settings, ns) {
|
|
59
|
+
try {
|
|
60
|
+
const desc = descriptorOf(settings, ns)
|
|
61
|
+
if (desc && desc.value !== undefined) return desc.value
|
|
62
|
+
if (typeof settings?.get === 'function') return settings.get(ns)
|
|
63
|
+
return undefined
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
export function profileOf(settings, provider) {
|
|
59
70
|
try {
|
|
60
|
-
const piai = settings
|
|
71
|
+
const piai = sectionOf(settings, PIAI_NS)?.providers?.[provider]
|
|
61
72
|
if (piai) return piai
|
|
62
73
|
if (provider === 'commandcode') {
|
|
63
|
-
const cc = settings
|
|
74
|
+
const cc = sectionOf(settings, 'llm-commandcode')
|
|
64
75
|
return {
|
|
65
76
|
apiKeyEnv: cc?.apiKeyEnv ?? 'COMMANDCODE_API_KEY',
|
|
66
77
|
credentialRef: cc?.apiKeyEnv ?? 'COMMANDCODE_API_KEY',
|
|
@@ -97,7 +108,7 @@ export function adapterErrorType(error) {
|
|
|
97
108
|
export async function replaceWithRetry(settings, ns, patchFn, attempts = 3) {
|
|
98
109
|
let lastError
|
|
99
110
|
for (let i = 0; i < attempts; i++) {
|
|
100
|
-
const section = settings
|
|
111
|
+
const section = sectionOf(settings, ns)
|
|
101
112
|
const descriptor = descriptorOf(settings, ns)
|
|
102
113
|
if (!section || !descriptor || typeof settings.replace !== 'function') {
|
|
103
114
|
throw Object.assign(new Error(`${ns} settings are not writable`), { code: 'SETTINGS_UNAVAILABLE' })
|
|
@@ -120,7 +131,7 @@ export async function replaceWithRetry(settings, ns, patchFn, attempts = 3) {
|
|
|
120
131
|
|
|
121
132
|
export function rotationRefs(settings, provider) {
|
|
122
133
|
try {
|
|
123
|
-
const rows = settings
|
|
134
|
+
const rows = sectionOf(settings, 'dsh-key-rotation')?.providers
|
|
124
135
|
if (!Array.isArray(rows)) return []
|
|
125
136
|
return rows.filter((row) => row?.provider === provider).flatMap((row) => Array.isArray(row.keys) ? row.keys : [])
|
|
126
137
|
} catch {
|
|
@@ -149,7 +160,7 @@ export async function performHistoryRollback({ ctx, config, providers = [], hist
|
|
|
149
160
|
if (!snapshot || !Array.isArray(snapshot.before)) throw Object.assign(new Error(`history snapshot not found: ${provider}`), { code: 'HISTORY_NOT_FOUND' })
|
|
150
161
|
if (!providers.some((row) => row.provider === provider && row.configured)) throw Object.assign(new Error(`provider is not configured: ${provider}`), { code: 'PROVIDER_NOT_CONFIGURED' })
|
|
151
162
|
const settings = settingsOf(ctx)
|
|
152
|
-
const section = settings
|
|
163
|
+
const section = sectionOf(settings, PIAI_NS)
|
|
153
164
|
const descriptor = descriptorOf(settings, PIAI_NS)
|
|
154
165
|
if (!section || !descriptor || typeof settings.replace !== 'function') {
|
|
155
166
|
throw Object.assign(new Error('llm-pi-ai settings are not writable'), { code: 'SETTINGS_UNAVAILABLE' })
|
package/lib/synchronizer.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
settingsOf,
|
|
19
19
|
llmOf,
|
|
20
20
|
descriptorOf,
|
|
21
|
+
sectionOf,
|
|
21
22
|
profileOf,
|
|
22
23
|
errorMessage,
|
|
23
24
|
diagnosticMessage,
|
|
@@ -488,7 +489,7 @@ export function createModelSynchronizer(ctx, { getConfig = () => ({}), saveConfi
|
|
|
488
489
|
if (typeof saveConfig !== 'function') throw Object.assign(new Error('dsh-model-sync settings are not writable'), { code: 'CONFIG_UNAVAILABLE' })
|
|
489
490
|
|
|
490
491
|
const settings = settingsOf(ctx)
|
|
491
|
-
const section = settings
|
|
492
|
+
const section = sectionOf(settings, PIAI_NS)
|
|
492
493
|
const descriptor = descriptorOf(settings, PIAI_NS)
|
|
493
494
|
const current = normalizeModels(provider, profileOf(settings, provider).models ?? row.models)
|
|
494
495
|
const nextModels = selectedModels.length
|
|
@@ -553,7 +554,18 @@ export function createModelSynchronizer(ctx, { getConfig = () => ({}), saveConfi
|
|
|
553
554
|
notifications: notificationLedger,
|
|
554
555
|
updateNotification: updateNotificationState,
|
|
555
556
|
rollbackHistory,
|
|
556
|
-
testWebhook: async ({ url, secret }) =>
|
|
557
|
+
testWebhook: async ({ url, secret }) => {
|
|
558
|
+
let resolvedSecret = secret || ''
|
|
559
|
+
if (typeof secret === 'string' && /^[A-Za-z_][A-Za-z0-9_]*$/.test(secret)) {
|
|
560
|
+
try {
|
|
561
|
+
const res = await (ctx?.credentials?.resolve ? ctx.credentials.resolve(secret) : ctx?.get?.('credentials')?.resolve?.(secret))
|
|
562
|
+
if (res) resolvedSecret = typeof res === 'string' ? res : res?.value || resolvedSecret
|
|
563
|
+
} catch (err) {
|
|
564
|
+
ctx?.logger?.debug?.('[dsh-model-sync] secret ref resolution fallback:', err)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return sendWebhookNotification(url, resolvedSecret, { event: 'model-sync.test', timestamp: Date.now(), test: true })
|
|
568
|
+
},
|
|
557
569
|
status: () => ({ running: Boolean(running), lastRun, lastHealth, lastReport: structuredClone(lastReport), notifications: notificationLedger(), history: history(), policies: listPolicies(), lifecycle: structuredClone(getConfig()?.modelLifecycle ?? {}), aliases: structuredClone(getConfig()?.aliases ?? {}) }),
|
|
558
570
|
})
|
|
559
571
|
}
|
package/lib/updater.js
CHANGED
|
@@ -237,7 +237,6 @@ export async function installExact(target, packageSpec, options = {}) {
|
|
|
237
237
|
'--profile',
|
|
238
238
|
target.profileName,
|
|
239
239
|
'add',
|
|
240
|
-
'--config.minimumReleaseAge=0',
|
|
241
240
|
packageSpec,
|
|
242
241
|
`--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
|
|
243
242
|
],
|