@goodandready/dsh-dsml-artifact-guard 0.2.4 → 0.2.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/CHANGELOG.md +15 -1
- package/lib/client.js +70 -35
- package/lib/index.js +44 -17
- package/lib/updater.js +8 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
## 0.2.
|
|
10
|
+
## [0.2.6] - 2026-09-25
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **SettingsForms Migration (DSH 0.1.7)**: marked Config schema as volatile and configured settings policy (`auto: false`) to suppress generic auto-generated form duplicates, while resolving row ID `dsh-dsml-artifact-guard` via client `configForms` with live subscription support (#17).
|
|
14
|
+
- **Client Locale Lifecycle**: wrapped client locale registration in `ctx.effect` to ensure clean namespace disposal on HMR and avoid collision on module reload (#44).
|
|
15
|
+
|
|
16
|
+
## [0.2.5] - 2026-09-24
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **Preflight & Locale Contract**: eliminated nonexistent `locale.get()` call in `lib/client.js`, using injected `props.t` with canonical `ctx.locale.bind(NS)` fallback (#36).
|
|
20
|
+
- **Streaming Buffer & Tag Balancing**: replaced fixed 64-character buffer slicing with delimiter-based tag boundary scanning (`<` and `>`), ensuring DSML parameter tags of arbitrary length preserve open/close tag balance without premature truncation (#37).
|
|
21
|
+
- **One-Click Updater LAN Support**: updated `isTrustedUpdateRequest` to allow same-origin requests matching Host/Origin, enabling one-click updates from LAN and reverse-proxy Web UIs while retaining strict loopback verification for local endpoints (#38).
|
|
22
|
+
- **Dead Code Elimination**: connected `checkRequestMethod` helper to updater POST handler (#39).
|
|
23
|
+
|
|
24
|
+
## [0.2.4] - 2026-09-22
|
|
11
25
|
|
|
12
26
|
### Fixed
|
|
13
27
|
- The settings card no longer waits for the removed `settingsScope` service. It uses `configForms` on current DeepSeek Harness (#41).
|
package/lib/client.js
CHANGED
|
@@ -102,16 +102,20 @@ window.__ModuleLoader__.load({
|
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
const scopeRef = React.useRef(null);
|
|
105
|
-
if (!scopeRef.current && _ctx
|
|
105
|
+
if (!scopeRef.current && _ctx) {
|
|
106
106
|
try {
|
|
107
|
-
|
|
107
|
+
if (_ctx.configForms && typeof _ctx.configForms.get === 'function') {
|
|
108
|
+
scopeRef.current = _ctx.configForms.get(ROW_ID) || _ctx.configForms.get(NS);
|
|
109
|
+
} else if (_ctx.settingsScope && typeof _ctx.settingsScope.bind === 'function') {
|
|
110
|
+
scopeRef.current = _ctx.settingsScope.bind({ namespace: NS });
|
|
111
|
+
}
|
|
108
112
|
} catch (e) {
|
|
109
113
|
scopeRef.current = null;
|
|
110
114
|
}
|
|
111
115
|
}
|
|
112
116
|
const scope = scopeRef.current;
|
|
113
117
|
|
|
114
|
-
const currentLang = (_ctx?.locale?.
|
|
118
|
+
const currentLang = ((typeof _ctx?.locale?.getLocale === 'function' ? _ctx.locale.getLocale() : null) || (typeof navigator !== 'undefined' && navigator.language) || 'en').startsWith('zh') ? 'zh' : 'en';
|
|
115
119
|
const dict = currentLang === 'zh' ? zh : en;
|
|
116
120
|
const tt = (k, params) => {
|
|
117
121
|
let str = (typeof t === 'function' ? t(k) : null) || dict[k] || en[k] || k;
|
|
@@ -123,43 +127,67 @@ window.__ModuleLoader__.load({
|
|
|
123
127
|
return str;
|
|
124
128
|
};
|
|
125
129
|
|
|
126
|
-
// Load settings from snapshot
|
|
130
|
+
// Load settings from snapshot and subscribe to live changes
|
|
127
131
|
React.useEffect(() => {
|
|
128
132
|
if (!scope) {
|
|
129
133
|
setStatus('unavailable');
|
|
130
134
|
return;
|
|
131
135
|
}
|
|
132
136
|
let cancelled = false;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (snap
|
|
138
|
-
if (snap.status === 'ready' && snap.value) {
|
|
139
|
-
setDraft({
|
|
140
|
-
mode: snap.value.mode || DEFAULTS.mode,
|
|
141
|
-
providerId: snap.value.providerId || DEFAULTS.providerId,
|
|
142
|
-
modelId: snap.value.modelId || DEFAULTS.modelId,
|
|
143
|
-
});
|
|
144
|
-
setStatus('ready');
|
|
145
|
-
} else {
|
|
146
|
-
setStatus(snap.status);
|
|
147
|
-
}
|
|
148
|
-
} else if (snap && typeof snap === 'object') {
|
|
137
|
+
const syncSnapshot = (snap) => {
|
|
138
|
+
if (!snap || cancelled) return;
|
|
139
|
+
const view = snap.view || snap.value || snap;
|
|
140
|
+
if (typeof snap === 'object' && 'status' in snap) {
|
|
141
|
+
if (snap.status === 'ready' && view) {
|
|
149
142
|
setDraft({
|
|
150
|
-
mode:
|
|
151
|
-
providerId:
|
|
152
|
-
modelId:
|
|
143
|
+
mode: view.mode || DEFAULTS.mode,
|
|
144
|
+
providerId: view.providerId || DEFAULTS.providerId,
|
|
145
|
+
modelId: view.modelId || DEFAULTS.modelId,
|
|
153
146
|
});
|
|
154
147
|
setStatus('ready');
|
|
155
148
|
} else {
|
|
156
|
-
setStatus(
|
|
149
|
+
setStatus(snap.status);
|
|
157
150
|
}
|
|
158
|
-
}
|
|
151
|
+
} else if (typeof view === 'object') {
|
|
152
|
+
setDraft({
|
|
153
|
+
mode: view.mode || DEFAULTS.mode,
|
|
154
|
+
providerId: view.providerId || DEFAULTS.providerId,
|
|
155
|
+
modelId: view.modelId || DEFAULTS.modelId,
|
|
156
|
+
});
|
|
157
|
+
setStatus('ready');
|
|
158
|
+
} else {
|
|
159
|
+
setStatus('ready');
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
(async () => {
|
|
164
|
+
try {
|
|
165
|
+
const snap = typeof scope.getSnapshot === 'function' ? scope.getSnapshot() : (typeof scope.get === 'function' ? await scope.get() : null);
|
|
166
|
+
syncSnapshot(snap);
|
|
167
|
+
} catch (err) {
|
|
159
168
|
if (!cancelled) setStatus('unavailable');
|
|
160
169
|
}
|
|
161
170
|
})();
|
|
162
|
-
|
|
171
|
+
|
|
172
|
+
let unsubscribe;
|
|
173
|
+
if (typeof scope.subscribe === 'function') {
|
|
174
|
+
try {
|
|
175
|
+
unsubscribe = scope.subscribe(() => {
|
|
176
|
+
try {
|
|
177
|
+
syncSnapshot(typeof scope.getSnapshot === 'function' ? scope.getSnapshot() : null);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
/* Snapshot update read failed */
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
} catch (err) {
|
|
183
|
+
/* Subscription to scope updates unavailable */
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return () => {
|
|
188
|
+
cancelled = true;
|
|
189
|
+
if (typeof unsubscribe === 'function') unsubscribe();
|
|
190
|
+
};
|
|
163
191
|
}, [scope]);
|
|
164
192
|
|
|
165
193
|
// Check updater status on mount
|
|
@@ -179,7 +207,7 @@ window.__ModuleLoader__.load({
|
|
|
179
207
|
latestVersion: data.latestVersion || '',
|
|
180
208
|
updateAvailable: !!data.updateAvailable,
|
|
181
209
|
}));
|
|
182
|
-
} catch {
|
|
210
|
+
} catch (err) {
|
|
183
211
|
if (!cancelled) {
|
|
184
212
|
setUpdateState(s => ({ ...s, checking: false, error: tt('checkUpdateFailed') }));
|
|
185
213
|
}
|
|
@@ -528,10 +556,10 @@ window.__ModuleLoader__.load({
|
|
|
528
556
|
// title) and view:'page' (the form, bare). The older seat keeps the card.
|
|
529
557
|
function PluginItem(props) {
|
|
530
558
|
if (props && props.view === 'summary') {
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const tt = typeof props.t === 'function' ? props.t : (k) =>
|
|
559
|
+
const tBind = (props && props.ctx && typeof props.ctx.locale?.bind === 'function')
|
|
560
|
+
? props.ctx.locale.bind(NS)
|
|
561
|
+
: null;
|
|
562
|
+
const tt = typeof props.t === 'function' ? props.t : (typeof tBind === 'function' ? tBind : (k) => en[k] || k);
|
|
535
563
|
return React.createElement('div', {
|
|
536
564
|
'data-dsh-plugin': NS,
|
|
537
565
|
className: 'dsh-dsml-artifact-guard-sub',
|
|
@@ -544,10 +572,17 @@ window.__ModuleLoader__.load({
|
|
|
544
572
|
exports.inject = ['slots', 'configForms', 'locale'];
|
|
545
573
|
exports.apply = function apply(ctx) {
|
|
546
574
|
if (typeof ctx.locale?.register === 'function') {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
575
|
+
const registerLocale = () => {
|
|
576
|
+
try {
|
|
577
|
+
return ctx.locale.register(NS, { en, zh });
|
|
578
|
+
} catch (err) {
|
|
579
|
+
/* Locale namespace already registered or unavailable */
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
if (typeof ctx.effect === 'function') {
|
|
583
|
+
ctx.effect(registerLocale, 'dsh-dsml-artifact-guard: locale');
|
|
584
|
+
} else {
|
|
585
|
+
registerLocale();
|
|
551
586
|
}
|
|
552
587
|
}
|
|
553
588
|
|
package/lib/index.js
CHANGED
|
@@ -5,17 +5,20 @@ const TAIL = /(?:<\/|DSML|parameter>\s*)?(?:<\/|DSML|invoke>\s*)?<\/|D
|
|
|
5
5
|
const OPEN_TAG = /<|DSML|(?:tool_calls|invoke|parameter)[^>]*>/gu;
|
|
6
6
|
const CLOSE_TAG = /<\/|DSML|(?:tool_calls|invoke|parameter)>/gu;
|
|
7
7
|
const KEEP = 96;
|
|
8
|
-
const SCAN_BUFFER_SAFE = 64;
|
|
9
8
|
|
|
10
9
|
export const name = '@goodandready/dsh-dsml-artifact-guard';
|
|
11
10
|
export const inject = ['llm', 'settings', 'webServer'];
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
const rawConfig = Schema.object({
|
|
14
13
|
mode: Schema.string().default('sanitize'),
|
|
15
14
|
providerId: Schema.string().default('opencode-go'),
|
|
16
15
|
modelId: Schema.string().default('deepseek-v4-flash'),
|
|
17
16
|
});
|
|
18
17
|
|
|
18
|
+
export const Config = typeof rawConfig.volatile === 'function'
|
|
19
|
+
? rawConfig.volatile()
|
|
20
|
+
: rawConfig.extra('volatile', true);
|
|
21
|
+
|
|
19
22
|
export function matchesScope(o, c) {
|
|
20
23
|
const provider = o?.providerId ?? o?.provider?.id ?? o?.provider;
|
|
21
24
|
const model = o?.modelId ?? o?.model?.id ?? o?.model?.name ?? o?.model;
|
|
@@ -68,10 +71,22 @@ export async function* sanitizeDsmlArtifacts(s, { mode = 'sanitize', onArtifact
|
|
|
68
71
|
p += c.text;
|
|
69
72
|
|
|
70
73
|
scanBuffer += c.text;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
const lastOpen = scanBuffer.lastIndexOf('<');
|
|
75
|
+
const lastClose = scanBuffer.lastIndexOf('>');
|
|
76
|
+
if (lastOpen > lastClose) {
|
|
77
|
+
if (lastOpen > 0) {
|
|
78
|
+
const safe = scanBuffer.slice(0, lastOpen);
|
|
79
|
+
scanBuffer = scanBuffer.slice(lastOpen);
|
|
80
|
+
countTags(safe);
|
|
81
|
+
}
|
|
82
|
+
if (scanBuffer.length > 2048) {
|
|
83
|
+
const cut = scanBuffer.length - 1024;
|
|
84
|
+
countTags(scanBuffer.slice(0, cut));
|
|
85
|
+
scanBuffer = scanBuffer.slice(cut);
|
|
86
|
+
}
|
|
87
|
+
} else if (scanBuffer.length > 0) {
|
|
88
|
+
countTags(scanBuffer);
|
|
89
|
+
scanBuffer = '';
|
|
75
90
|
}
|
|
76
91
|
|
|
77
92
|
if (p.length > KEEP) {
|
|
@@ -98,19 +113,31 @@ export function apply(ctx, initialConfig = {}) {
|
|
|
98
113
|
|
|
99
114
|
let getConfig = () => activeConfig;
|
|
100
115
|
|
|
101
|
-
//
|
|
102
|
-
if (typeof ctx.
|
|
116
|
+
// Live config subscription from the plugin fiber
|
|
117
|
+
if (typeof ctx.on === 'function') {
|
|
118
|
+
ctx.effect(() => ctx.on('config', (nextConfig) => {
|
|
119
|
+
activeConfig = Config(nextConfig) ?? {};
|
|
120
|
+
}), 'dsh-dsml-artifact-guard: live config');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// DSH 0.1.7 SettingsForms: suppress generic duplicate form, custom card mounts in plugins seat
|
|
124
|
+
if (ctx.settings && typeof ctx.settings.configure === 'function') {
|
|
125
|
+
ctx.effect(() => ctx.settings.configure({ auto: false }), 'dsh-dsml-artifact-guard: settings policy');
|
|
126
|
+
} else if (typeof ctx.inject === 'function') {
|
|
127
|
+
// Legacy settings service fallback
|
|
103
128
|
ctx.inject(['settings'], (sctx) => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
129
|
+
if (typeof sctx.settings?.register === 'function') {
|
|
130
|
+
try {
|
|
131
|
+
const scope = sctx.settings.register(NS, Config, { base: initialConfig });
|
|
132
|
+
getConfig = () => Config(scope?.get() ?? initialConfig) ?? {};
|
|
133
|
+
if (typeof scope?.watch === 'function') {
|
|
134
|
+
sctx.effect(() => scope.watch(() => {
|
|
135
|
+
activeConfig = getConfig();
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
/* Legacy settings service registration unavailable */
|
|
111
140
|
}
|
|
112
|
-
} catch (err) {
|
|
113
|
-
ctx.logger?.warn?.(`[dsh-dsml-artifact-guard] settings registration skipped: ${String(err)}`);
|
|
114
141
|
}
|
|
115
142
|
});
|
|
116
143
|
}
|
package/lib/updater.js
CHANGED
|
@@ -4,6 +4,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { basename, dirname, isAbsolute, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { checkRequestMethod } from './index.js';
|
|
7
8
|
|
|
8
9
|
const UPDATE_HEADER = 'x-dsh-plugin-update';
|
|
9
10
|
const UPDATE_TIMEOUT_MS = 10 * 60000;
|
|
@@ -24,7 +25,6 @@ export function isLoopback(value) {
|
|
|
24
25
|
|
|
25
26
|
export function isTrustedUpdateRequest(request) {
|
|
26
27
|
if (header(request, UPDATE_HEADER) !== '1') return false;
|
|
27
|
-
if (!isLoopback(request.socket?.remoteAddress)) return false;
|
|
28
28
|
const site = header(request, 'sec-fetch-site');
|
|
29
29
|
if (site !== undefined && site !== 'same-origin') return false;
|
|
30
30
|
const origin = header(request, 'origin');
|
|
@@ -32,8 +32,12 @@ export function isTrustedUpdateRequest(request) {
|
|
|
32
32
|
if (origin === undefined || host === undefined) return false;
|
|
33
33
|
try {
|
|
34
34
|
const url = new URL(origin);
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
|
|
36
|
+
if (url.host.toLowerCase() !== host.toLowerCase()) return false;
|
|
37
|
+
if (isLoopback(url.hostname)) {
|
|
38
|
+
return isLoopback(request.socket?.remoteAddress);
|
|
39
|
+
}
|
|
40
|
+
return site === 'same-origin';
|
|
37
41
|
} catch {
|
|
38
42
|
return false;
|
|
39
43
|
}
|
|
@@ -218,7 +222,7 @@ export function registerPluginUpdater(ctx, options) {
|
|
|
218
222
|
res.end(req.method === 'HEAD' ? undefined : JSON.stringify(payload));
|
|
219
223
|
return;
|
|
220
224
|
}
|
|
221
|
-
if (req
|
|
225
|
+
if (!checkRequestMethod(req, 'POST')) {
|
|
222
226
|
res.writeHead(405, { allow: 'GET, HEAD, POST' });
|
|
223
227
|
res.end();
|
|
224
228
|
return;
|