@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.
- package/README.en.md +58 -10
- package/README.md +58 -10
- package/client.js +1 -1
- package/index.js +127 -7
- package/model-config-routes.js +633 -0
- package/model-config-schema.js +132 -0
- package/model-smart-fill.js +2206 -0
- package/model-workbench-routes.js +1740 -0
- package/model-workbench-settings.js +389 -0
- package/model-workbench.js +1618 -0
- package/package.json +10 -2
- package/provider-availability.js +84 -0
- package/subagent-routes.js +7 -0
|
@@ -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
|
+
}
|