@gehennawu/dsh-service 2.0.7 → 2.0.8

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.
@@ -0,0 +1,389 @@
1
+ // Versioned model configuration access. Display forms are never a write baseline.
2
+ //
3
+ // Two host generations, one facade:
4
+ // * legacy (settings.get exists, <=0.1.6): the namespace is the fixed string `llm-pi-ai`,
5
+ // `settings.get(ns)` is the raw section, `settings.describe()` may expose a revision.
6
+ // * modern (>=0.1.7, no settings.get): `configEditor.configuration()` resolves the
7
+ // `@deepseek-ai/dsh-llm-pi-ai` Profile entry. `entry.options.id` is the *dynamic* namespace
8
+ // (the Profile patch id, not the fixed `llm-pi-ai`), `entry.options.config` is the complete
9
+ // raw merged config (the only safe write baseline), `override` is the explicit layer,
10
+ // `inherited` the inherited layer, and `settings.describe().value` the effective projection
11
+ // (bundle defaults merged in) which must NEVER be written back.
12
+ //
13
+ // Verified against the installed 0.2.1-alpha.2 host:
14
+ // dsh-settings/lib/index.js:413-537 describe() revision + mutate(ns, ops, expected) inside the
15
+ // write queue (mismatch throws SettingsConflictError); unset
16
+ // re-materializes the inherited value.
17
+ // dsh-config-editor/lib/index.js:30-135 configuration()/entries() (duplicate ids are dropped by
18
+ // entries(), unique inclusion-owned ids only) and edit(),
19
+ // which re-checks entry identity inside the file lock and
20
+ // serializes any `{__jsExpr}` node back to a YAML `!js` tag.
21
+ // dsh-llm-pi-ai/lib/index.js:1050-1080 provider schema: headers is a free string dict.
22
+ //
23
+ // Safety posture: raw/explicit/inherited/effective never share one surface; the owner (package +
24
+ // entry id) and revision are captured synchronously before any await; duplicate/inactive/invalid
25
+ // owners and expression-backed configuration at any depth fail closed; display DTOs redact
26
+ // sensitive headers/secrets; an opaque fingerprint binds the complete raw config to the owner
27
+ // identity so a baseline can be re-checked after asynchronous work.
28
+
29
+ import { createHash } from 'node:crypto'
30
+
31
+ const PACKAGE = '@deepseek-ai/dsh-llm-pi-ai'
32
+ const NS = 'llm-pi-ai'
33
+ const EXPRESSION_KEY = '__jsExpr'
34
+
35
+ /** Value substituted for sensitive headers/fields in display DTOs. */
36
+ export const SENSITIVE_MASK = '[redacted]'
37
+
38
+ const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
39
+ const clone = (value) => structuredClone(value)
40
+
41
+ /** Detached copy of host-provided config; non-cloneable values are unusable as a baseline. */
42
+ function cloneConfig(value) {
43
+ try { return structuredClone(value) } catch (_) { throw fail('model-config-invalid-config') }
44
+ }
45
+
46
+ /** Coded, fail-closed error. The message is the code so callers can match on either. */
47
+ function fail(code) {
48
+ const error = new Error(code)
49
+ error.code = code
50
+ return error
51
+ }
52
+
53
+ // ─────────────────────────────────────────────────────────────────────────────
54
+ // Expression-backed configuration (fail closed)
55
+ // ─────────────────────────────────────────────────────────────────────────────
56
+
57
+ /** config-editor serializes a YAML `!js` node back to `{ __jsExpr: "<source>" }`. */
58
+ function hasExpression(value, seen = new Set()) {
59
+ if (Array.isArray(value)) {
60
+ if (seen.has(value)) return false
61
+ seen.add(value)
62
+ const found = value.some((item) => hasExpression(item, seen))
63
+ seen.delete(value)
64
+ return found
65
+ }
66
+ if (!object(value)) return false
67
+ if (seen.has(value)) return false
68
+ seen.add(value)
69
+ let found = Object.hasOwn(value, EXPRESSION_KEY)
70
+ if (!found) for (const child of Object.values(value)) if (hasExpression(child, seen)) { found = true; break }
71
+ seen.delete(value)
72
+ return found
73
+ }
74
+
75
+ /** Any expression anywhere (root, providers, a route, a model entry) forbids a literal rewrite. */
76
+ function assertLiteral(config) {
77
+ if (hasExpression(config)) throw fail('model-config-expression')
78
+ }
79
+
80
+ // ─────────────────────────────────────────────────────────────────────────────
81
+ // Sensitive-value redaction for display interfaces
82
+ // ─────────────────────────────────────────────────────────────────────────────
83
+
84
+ const normalizeKey = (key) => key.toLowerCase().replace(/[^a-z0-9]/g, '')
85
+
86
+ const SENSITIVE_HEADER_KEYS = new Set([
87
+ 'authorization', 'proxyauthorization', 'cookie', 'setcookie', 'xapikey', 'apikey',
88
+ 'xauthtoken', 'authtoken', 'xaccesstoken', 'accesstoken', 'xgoogapikey',
89
+ 'xamzsecuritytoken', 'xcsrftoken', 'xsrftoken',
90
+ ])
91
+
92
+ /** Header names are matched case-insensitively and separator-insensitively, plus common suffixes. */
93
+ export function isSensitiveHeaderName(name) {
94
+ if (typeof name !== 'string' || name === '') return false
95
+ const normalized = normalizeKey(name)
96
+ if (SENSITIVE_HEADER_KEYS.has(normalized)) return true
97
+ return normalized.endsWith('authorization') || normalized.endsWith('apikey') ||
98
+ normalized.endsWith('token') || normalized.endsWith('cookie') || normalized.includes('secret')
99
+ }
100
+
101
+ const SENSITIVE_FIELD_KEYS = new Set([
102
+ 'apikey', 'apikeys', 'token', 'tokens', 'secret', 'secrets', 'password', 'passwd',
103
+ 'authorization', 'cookie', 'credential', 'credentials', 'privatekey', 'accesskey',
104
+ 'clientsecret', 'apitoken', 'authtoken', 'bearer', 'sessionkey',
105
+ ])
106
+
107
+ /**
108
+ * `apiKeyEnv` is intentionally NOT masked: it is the *name* of a host credential record, not a
109
+ * secret value. Only names that denote a secret themselves are masked.
110
+ */
111
+ function isSensitiveFieldName(name) {
112
+ if (typeof name !== 'string' || name === '') return false
113
+ return SENSITIVE_FIELD_KEYS.has(normalizeKey(name))
114
+ }
115
+
116
+ /** Return a detached copy with sensitive header/field values masked. Keys are always preserved. */
117
+ export function redactSensitive(value, inHeaders = false) {
118
+ if (Array.isArray(value)) return value.map((item) => redactSensitive(item, inHeaders))
119
+ if (!object(value)) return value
120
+ const out = {}
121
+ for (const [key, child] of Object.entries(value)) {
122
+ if (inHeaders) {
123
+ out[key] = isSensitiveHeaderName(key) ? SENSITIVE_MASK : redactSensitive(child, false)
124
+ continue
125
+ }
126
+ if (isSensitiveFieldName(key)) { out[key] = SENSITIVE_MASK; continue }
127
+ out[key] = redactSensitive(child, normalizeKey(key) === 'headers')
128
+ }
129
+ return out
130
+ }
131
+
132
+ // ─────────────────────────────────────────────────────────────────────────────
133
+ // Opaque baseline fingerprint (complete raw config + owner identity)
134
+ // ─────────────────────────────────────────────────────────────────────────────
135
+
136
+ const UNDEFINED_TAG = '\u0000__undef__'
137
+
138
+ /** Deterministic, type-aware serialization; unknown value types fail closed. */
139
+ function canonicalString(value, seen = new Set()) {
140
+ if (value === undefined) return UNDEFINED_TAG
141
+ if (value === null) return 'null'
142
+ const type = typeof value
143
+ if (type === 'string') return JSON.stringify(value)
144
+ if (type === 'boolean' || type === 'bigint') return `${type}:${String(value)}`
145
+ if (type === 'number') return `number:${String(value)}`
146
+ if (type !== 'object') throw fail('model-config-invalid-config')
147
+ if (seen.has(value)) throw fail('model-config-invalid-config')
148
+ seen.add(value)
149
+ let out
150
+ if (Array.isArray(value)) {
151
+ out = `[${value.map((item) => canonicalString(item, seen)).join(',')}]`
152
+ } else {
153
+ out = `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalString(value[key], seen)}`).join(',')}}`
154
+ }
155
+ seen.delete(value)
156
+ return out
157
+ }
158
+
159
+ /** sha256 over the owner identity plus the complete raw config; opaque and order-independent. */
160
+ function baselineFingerprint(identity, raw) {
161
+ const payload = canonicalString({
162
+ package: identity.package,
163
+ entryId: identity.entryId,
164
+ entryName: identity.entryName ?? null,
165
+ mode: identity.mode,
166
+ raw,
167
+ })
168
+ return `sha256:${createHash('sha256').update(payload).digest('hex')}`
169
+ }
170
+
171
+ // ─────────────────────────────────────────────────────────────────────────────
172
+ // Owner resolution
173
+ // ─────────────────────────────────────────────────────────────────────────────
174
+
175
+ function ownerEntry(row) {
176
+ return row?.entry
177
+ }
178
+
179
+ function isPiAiEntry(entry) {
180
+ return entry?.options?.name === PACKAGE
181
+ }
182
+
183
+ /**
184
+ * Modern owner: exactly one active, inclusion-owned entry named PACKAGE with a non-empty string id.
185
+ * A duplicate id makes configEditor.entries() drop both rows, so the raw Loader list is consulted
186
+ * (when reachable) to report the ambiguity instead of a generic unavailability.
187
+ */
188
+ function resolveModernOwner(ctx, editor) {
189
+ if (typeof editor?.configuration !== 'function') throw fail('model-config-unavailable')
190
+ let rows
191
+ try { rows = editor.configuration() } catch (_) { throw fail('model-config-unavailable') }
192
+ if (!Array.isArray(rows)) throw fail('model-config-unavailable')
193
+
194
+ const owners = rows.filter((row) => isPiAiEntry(ownerEntry(row)))
195
+ const loader = typeof ctx?.get === 'function' ? ctx.get('loader') : undefined
196
+ if (loader !== undefined && typeof loader.entries === 'function') {
197
+ let named
198
+ try { named = [...loader.entries()].filter(isPiAiEntry) } catch (_) { named = undefined }
199
+ if (Array.isArray(named)) {
200
+ const ids = named.map((entry) => entry?.options?.id)
201
+ const duplicated = ids.some((id, index) => ids.indexOf(id) !== index)
202
+ if (duplicated || named.length > 1) throw fail('model-config-ambiguous')
203
+ }
204
+ }
205
+ if (owners.length > 1) throw fail('model-config-ambiguous')
206
+ if (owners.length === 0) throw fail('model-config-unavailable')
207
+
208
+ const row = owners[0]
209
+ const entry = ownerEntry(row)
210
+ const ns = entry?.options?.id
211
+ if (typeof ns !== 'string' || ns === '' || ns.length > 128) throw fail('model-config-invalid-owner')
212
+ if (entry.fiber !== undefined && entry.fiber.state !== 2) throw fail('model-config-inactive')
213
+ return { entry, ns, row }
214
+ }
215
+
216
+ /** Re-resolve the modern owner and require the exact same Loader entry instance and id. */
217
+ function assertSameModernOwner(ctx, editor, captured) {
218
+ let rows
219
+ try { rows = editor.configuration() } catch (_) { throw fail('model-config-owner-changed') }
220
+ if (!Array.isArray(rows)) throw fail('model-config-owner-changed')
221
+ const owners = rows.filter((row) => isPiAiEntry(ownerEntry(row)))
222
+ if (owners.length !== 1) throw fail('model-config-owner-changed')
223
+ const entry = ownerEntry(owners[0])
224
+ if (entry !== captured.entry || entry?.options?.id !== captured.ns) throw fail('model-config-owner-changed')
225
+ if (entry.fiber !== undefined && entry.fiber.state !== 2) throw fail('model-config-inactive')
226
+ }
227
+
228
+ // ─────────────────────────────────────────────────────────────────────────────
229
+ // Facade
230
+ // ─────────────────────────────────────────────────────────────────────────────
231
+
232
+ function facade({
233
+ settings, ns, mode, entryName, raw, explicit, inherited, effective, revision,
234
+ verifyOwner,
235
+ }) {
236
+ const revisionTracked = Number.isSafeInteger(revision)
237
+ const identity = Object.freeze({ mode, package: PACKAGE, entryId: ns, entryName: entryName ?? null })
238
+ const fingerprint = baselineFingerprint(identity, raw)
239
+
240
+ const access = {
241
+ namespace: ns,
242
+ revision,
243
+ revisionTracked,
244
+ identity,
245
+ get owner() { return { ...identity } },
246
+
247
+ // ── separated layers (detached snapshots; raw is the write baseline) ──
248
+ raw: () => clone(raw),
249
+ explicit: () => clone(explicit),
250
+ inherited: () => clone(inherited),
251
+ effective: () => effective === undefined ? undefined : clone(effective),
252
+
253
+ // ── display DTO: the only interface a UI/route should project, secrets masked ──
254
+ display: () => ({
255
+ namespace: ns,
256
+ mode,
257
+ revision,
258
+ revisionTracked,
259
+ raw: redactSensitive(raw),
260
+ explicit: redactSensitive(explicit),
261
+ inherited: redactSensitive(inherited),
262
+ effective: effective === undefined ? undefined : redactSensitive(effective),
263
+ }),
264
+
265
+ // ── opaque baseline ──
266
+ fingerprint: () => fingerprint,
267
+ baseline: () => ({
268
+ ...identity,
269
+ revision: revisionTracked ? revision : null,
270
+ revisionTracked,
271
+ fingerprint,
272
+ }),
273
+
274
+ // Legacy facade shape consumed by model-workbench-routes.js: `get`/`describe`/`mutate`
275
+ // always speak the fixed `llm-pi-ai` namespace, while `namespace`/`identity` expose the
276
+ // resolved host entry id.
277
+ get: (key) => key === NS ? clone(raw) : undefined,
278
+ describe: () => [{ ns: NS, revision }],
279
+ inheritedRoute: (route) => object(inherited.providers) && Object.hasOwn(inherited.providers, route),
280
+
281
+ /** Re-check this baseline still belongs to the captured owner (safe after async work). */
282
+ assertOwner: () => { verifyOwner(); return { ...identity } },
283
+
284
+ /**
285
+ * Carry the captured revision by default, including creation and compensation.
286
+ * Re-reading the revision after credential/sidecar I/O would reopen the TOCTOU gap.
287
+ */
288
+ mutate: (key, ops, expected = revision) => {
289
+ if (key !== NS) throw fail('model-config-unavailable')
290
+ if (typeof settings.mutate !== 'function') throw fail('model-config-unavailable')
291
+ if (!revisionTracked) {
292
+ // A host that advertises a revision parameter but exposes none cannot be written safely:
293
+ // reject instead of silently degrading to an unguarded write.
294
+ if (settings.mutate.length >= 3) throw fail('model-config-revision-unavailable')
295
+ // Documented pre-revision host: keep compatibility; the profile fingerprint gate upstream
296
+ // still guards the content window.
297
+ return settings.mutate(ns, ops)
298
+ }
299
+ verifyOwner()
300
+ return settings.mutate(ns, ops, expected)
301
+ },
302
+ }
303
+ return access
304
+ }
305
+
306
+ // ─────────────────────────────────────────────────────────────────────────────
307
+ // Entry point
308
+ // ─────────────────────────────────────────────────────────────────────────────
309
+
310
+ /** Capture one request's raw configuration, identity and revision before any awaits.
311
+ * <=0.1.6 uses namespaces; >=0.1.7 uses addressable Profile entries. Ambiguous, inactive or
312
+ * expression-backed owners fail closed instead of accidentally modifying another adapter.
313
+ */
314
+ export function modelSettingsFor(ctx) {
315
+ const settings = ctx.get('settings')
316
+ if (settings === undefined) throw fail('model-config-unavailable')
317
+
318
+ // ── legacy namespace host ──
319
+ if (typeof settings.get === 'function') {
320
+ let section
321
+ try { section = settings.get(NS) } catch (_) { throw fail('model-config-unavailable') }
322
+ if (section !== undefined && !object(section)) throw fail('model-config-invalid-section')
323
+ const raw = cloneConfig(section ?? { providers: {} })
324
+ assertLiteral(raw)
325
+
326
+ let revision
327
+ let effective
328
+ const describe = typeof settings.describe === 'function' ? settings.describe : undefined
329
+ if (describe !== undefined) {
330
+ let descriptors
331
+ try { descriptors = describe.call(settings) } catch (_) { descriptors = undefined }
332
+ const row = Array.isArray(descriptors) ? descriptors.find((item) => item?.ns === NS) : undefined
333
+ if (row !== undefined) {
334
+ if (Number.isSafeInteger(row.revision)) revision = row.revision
335
+ if (row.value !== undefined) effective = cloneConfig(row.value)
336
+ }
337
+ }
338
+ if (effective !== undefined) assertLiteral(effective)
339
+
340
+ return facade({
341
+ settings,
342
+ ns: NS,
343
+ mode: 'legacy',
344
+ entryName: PACKAGE,
345
+ raw,
346
+ explicit: raw,
347
+ inherited: {},
348
+ effective,
349
+ revision,
350
+ verifyOwner: () => {},
351
+ })
352
+ }
353
+
354
+ // ── modern Profile entry host ──
355
+ const editor = ctx.get('configEditor')
356
+ if (typeof editor?.configuration !== 'function' || typeof settings.describe !== 'function') {
357
+ throw fail('model-config-unavailable')
358
+ }
359
+ const { entry, ns, row } = resolveModernOwner(ctx, editor)
360
+
361
+ let descriptors
362
+ try { descriptors = settings.describe() } catch (_) { throw fail('model-config-unavailable') }
363
+ if (!Array.isArray(descriptors)) throw fail('model-config-unavailable')
364
+ const descriptor = descriptors.find((item) => item.ns === ns)
365
+ if (descriptor === undefined || !Number.isSafeInteger(descriptor.revision)) throw fail('model-config-unavailable')
366
+
367
+ const raw = cloneConfig(entry.options.config ?? {})
368
+ const explicit = cloneConfig(row.override ?? {})
369
+ const inherited = cloneConfig(row.inherited ?? {})
370
+ assertLiteral(raw)
371
+ assertLiteral(explicit)
372
+ assertLiteral(inherited)
373
+ const effective = descriptor.value === undefined ? undefined : cloneConfig(descriptor.value)
374
+ if (effective !== undefined) assertLiteral(effective)
375
+
376
+ const captured = { entry, ns }
377
+ return facade({
378
+ settings,
379
+ ns,
380
+ mode: 'modern',
381
+ entryName: entry.options.name,
382
+ raw,
383
+ explicit,
384
+ inherited,
385
+ effective,
386
+ revision: descriptor.revision,
387
+ verifyOwner: () => assertSameModernOwner(ctx, editor, captured),
388
+ })
389
+ }