@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 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.4
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 && _ctx.configForms) {
105
+ if (!scopeRef.current && _ctx) {
106
106
  try {
107
- scopeRef.current = _ctx.configForms.get(NS);
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?.get?.() || navigator.language || 'en').startsWith('zh') ? 'zh' : 'en';
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
- (async () => {
134
- try {
135
- const snap = await scope.get();
136
- if (cancelled) return;
137
- if (snap && typeof snap === 'object' && 'status' in 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: snap.mode || DEFAULTS.mode,
151
- providerId: snap.providerId || DEFAULTS.providerId,
152
- modelId: snap.modelId || DEFAULTS.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('ready');
149
+ setStatus(snap.status);
157
150
  }
158
- } catch {
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
- return () => { cancelled = true; };
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 ctx = props && props.ctx;
532
- const lang = (ctx && ctx.locale && typeof ctx.locale.get === 'function' ? ctx.locale.get() : null) || 'en';
533
- const dict = String(lang).startsWith('zh') ? zh : en;
534
- const tt = typeof props.t === 'function' ? props.t : (k) => dict[k] || en[k] || 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
- try {
548
- ctx.locale.register(NS, { en, zh });
549
- } catch (err) {
550
- /* Locale namespace already registered or unavailable */
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
- export const Config = Schema.object({
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
- if (scanBuffer.length > SCAN_BUFFER_SAFE) {
72
- const safe = scanBuffer.slice(0, scanBuffer.length - SCAN_BUFFER_SAFE);
73
- scanBuffer = scanBuffer.slice(scanBuffer.length - SCAN_BUFFER_SAFE);
74
- countTags(safe);
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
- // Register settings scope inside ctx.effect
102
- if (typeof ctx.inject === 'function') {
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
- try {
105
- const scope = sctx.settings.register(NS, Config, { base: initialConfig });
106
- getConfig = () => Config(scope?.get() ?? initialConfig) ?? {};
107
- if (typeof scope?.watch === 'function') {
108
- sctx.effect(() => scope.watch(() => {
109
- activeConfig = getConfig();
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
- return (url.protocol === 'http:' || url.protocol === 'https:')
36
- && isLoopback(url.hostname) && url.host === host;
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.method !== 'POST') {
225
+ if (!checkRequestMethod(req, 'POST')) {
222
226
  res.writeHead(405, { allow: 'GET, HEAD, POST' });
223
227
  res.end();
224
228
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-dsml-artifact-guard",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Sanitizes leaked DeepSeek DSML closing tags from model text streams",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",