@goodandready/dsh-voice 0.9.35 → 0.9.36

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/client.js CHANGED
@@ -618,61 +618,61 @@ window.__ModuleLoader__.load({
618
618
  // ------------------------------------------------------------------ css
619
619
  const CSS =
620
620
  '.dvo-btn{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;padding:0;box-sizing:border-box;user-select:none;-webkit-user-select:none;touch-action:manipulation}' +
621
- '.dvo-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}' +
622
- '.dvo-btn[data-err="1"]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}' +
623
- '.dvo-pill{display:flex;align-items:center;gap:10px;height:52px;border-radius:26px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);padding:0 14px;width:100%;max-width:720px;margin:0 auto;box-shadow:0 8px 24px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);box-sizing:border-box;user-select:none;-webkit-user-select:none}' +
624
- '.dvo-chev-icon{width:14px;height:14px;display:block;flex:none}' +
625
- '.dvo-pbtn{display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:50%;cursor:pointer;padding:0;flex:none;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);user-select:none;-webkit-user-select:none;touch-action:manipulation}' +
626
- '.dvo-pbtn:hover{background:var(--dsw-alias-bg-layer-2)}' +
627
- '.dvo-wave{flex:1;min-width:0;max-width:100%;height:40px;width:100%;color:var(--dsw-alias-label-primary)}' +
628
- '.dvo-status{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-secondary);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
629
- '.dvo-err{color:var(--dsw-alias-state-error-primary)}' +
630
- '.dvo-count{font-variant-numeric:tabular-nums;font-size:13px;color:var(--dsw-alias-label-secondary)}' +
631
- '.dvo-spin{animation:dvo-spin 1s linear infinite}' +
632
- '@keyframes dvo-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}' +
633
- '.dvo-btn-active{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}' +
634
- '.dvo-audio-wrap{display:flex;align-items:center;gap:8px;padding:2px 10px;background:var(--dsw-alias-bg-layer-2);border-radius:14px;border:1px solid var(--dsw-alias-border-l1)}' +
635
- '.dvo-audio-play{display:flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);border:0;cursor:pointer;padding:0}' +
636
- '.dvo-audio-play:hover{opacity:0.9}' +
637
- '.dvo-audio-time{font-size:12px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary)}' +
638
- '.dvo-dash{display:flex;flex-direction:column;gap:8px;margin-top:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px}' +
639
- '.dvo-dash-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:8px;margin-top:4px}' +
640
- '.dvo-dash-item{display:flex;flex-direction:column;gap:4px;padding:8px 10px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l1);border-radius:8px}' +
641
- '.dvo-dash-name{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
642
- '.dvo-dash-row{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary)}' +
643
- '.dvo-badge{display:inline-flex;align-items:center;font-size:11px;padding:1px 6px;border-radius:6px;font-weight:600}' +
644
- '.dvo-badge-fast{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-success-primary);border:1px solid var(--dsw-alias-state-success-primary)}' +
645
- '.dvo-badge-norm{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-warning-primary);border:1px solid var(--dsw-alias-state-warning-primary)}' +
646
- '.dvo-badge-slow{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-warning-primary);border:1px solid var(--dsw-alias-border-l2)}' +
647
- '.dvo-badge-err{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary)}' +
648
- '.dvo-badge-idle{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary)}' +
649
- '.dvo-mic-test{display:flex;align-items:center;gap:10px;margin-top:8px;padding:6px 12px;background:var(--dsw-alias-bg-layer-2);border-radius:8px;border:1px solid var(--dsw-alias-border-l1)}' +
650
- '.dvo-meter-bar{flex:1;height:8px;background:var(--dsw-alias-bg-layer-3);border-radius:4px;overflow:hidden;position:relative}' +
651
- '.dvo-meter-fill{height:100%;background:var(--dsw-alias-state-success-primary);border-radius:4px;transition:width 60ms ease-out}' +
652
- '.dvo-meter-gate{position:absolute;top:0;bottom:0;width:2px;background:var(--dsw-alias-state-warning-primary);box-shadow:0 0 4px var(--dsw-alias-state-warning-primary);z-index:2;pointer-events:none}' +
653
- '.dvo-silence-ring{width:34px;height:34px;transform:rotate(-90deg);flex:none}' +
654
- '.dvo-ring-bg{fill:none;stroke:var(--dsw-alias-border-l1);stroke-width:2.5}' +
655
- '.dvo-ring-fg{fill:none;stroke:var(--dsw-alias-state-info-primary);stroke-width:2.5;stroke-linecap:round;transition:stroke-dashoffset 100ms linear}' +
656
- '.dvo-ghost{font-style:italic;color:var(--dsw-alias-label-tertiary);margin-left:6px;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px}' +
657
- '.dvo-installer-box{display:flex;flex-direction:column;gap:8px;padding:10px 12px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;margin-top:8px}' +
658
- '.dvo-installer-row{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
659
- '.dvo-progress-bar{height:6px;width:100%;background:var(--dsw-alias-bg-layer-3);border-radius:3px;overflow:hidden}' +
660
- '.dvo-progress-fill{height:100%;background:var(--dsw-alias-state-info-primary);transition:width 150ms ease}' +
661
- '.dvo-jargon-table{width:100%;margin-top:6px;border-collapse:collapse;font-size:12px}' +
662
- '.dvo-jargon-table td{padding:4px 6px;border-bottom:1px solid var(--dsw-alias-border-l1)}' +
663
- '.dvo-pill-preview{display:flex;flex-direction:column;gap:8px;padding:10px 14px;border-radius:18px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);width:100%;max-width:720px;margin:0 auto;box-shadow:0 8px 24px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);box-sizing:border-box}' +
664
- '.dvo-preview-header{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%}' +
665
- '.dvo-preview-tabs{display:inline-flex;align-items:center;gap:4px;background:var(--dsw-alias-bg-layer-2);padding:2px;border-radius:12px;border:1px solid var(--dsw-alias-border-l1)}' +
666
- '.dvo-tab-btn{border:none;background:transparent;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:500;padding:3px 10px;border-radius:10px;cursor:pointer;transition:all 150ms ease}' +
667
- '.dvo-tab-btn:hover{color:var(--dsw-alias-label-primary)}' +
668
- '.dvo-tab-btn-active{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 1px 3px color-mix(in srgb, var(--dsw-alias-label-primary) 15%, transparent)}' +
669
- '.dvo-preview-body{display:flex;align-items:center;gap:8px;width:100%}' +
670
- '.dvo-preview-text{flex:1;font-size:13px;color:var(--dsw-alias-label-primary);line-height:1.4;word-break:break-word;max-height:80px;overflow-y:auto;padding:2px 0}' +
671
- '.dvo-preview-input{flex:1;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 8px;outline:none;font-family:inherit;width:100%}' +
672
- '.dvo-preview-input:focus{border-color:var(--dsw-alias-state-info-primary)}' +
673
- '.dvo-btn-send{display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:50%;cursor:pointer;padding:0;flex:none;box-sizing:border-box;border:none;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);transition:opacity 150ms ease}' +
674
- '.dvo-btn-send:hover{opacity:0.9}' +
675
- '.dvo-warn-banner{font-size:12px;color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-error-primary);padding:4px 8px;border-radius:6px;margin-top:4px}' + '.dvo-mode-pill-btn{display:inline-flex;align-items:center;justify-content:center;height:28px;padding:0 8px;font-size:11px;font-weight:600;border-radius:14px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);user-select:none;touch-action:manipulation;transition:all 150ms ease}' + '.dvo-mode-pill-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary)}' + '.dvo-mode-badge{font-size:11px;font-weight:600;padding:2px 6px;border-radius:6px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);margin-right:4px}' + '.dvo-macro-item{display:flex;flex-direction:column;gap:6px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;margin-top:6px}' + '.dvo-macro-actions{display:flex;justify-content:flex-end;gap:6px}' + '.dvo-fallback-badge{font-size:11px;padding:2px 6px;border-radius:4px;color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-warning-primary)}'
621
+ '.dvo-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}' +
622
+ '.dvo-btn[data-err="1"]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}' +
623
+ '.dvo-pill{display:flex;align-items:center;gap:10px;height:52px;border-radius:26px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);padding:0 14px;width:100%;max-width:720px;margin:0 auto;box-shadow:0 8px 24px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);box-sizing:border-box;user-select:none;-webkit-user-select:none}' +
624
+ '.dvo-chev-icon{width:14px;height:14px;display:block;flex:none}' +
625
+ '.dvo-pbtn{display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:50%;cursor:pointer;padding:0;flex:none;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);user-select:none;-webkit-user-select:none;touch-action:manipulation}' +
626
+ '.dvo-pbtn:hover{background:var(--dsw-alias-bg-layer-2)}' +
627
+ '.dvo-wave{flex:1;min-width:0;max-width:100%;height:40px;width:100%;color:var(--dsw-alias-label-primary)}' +
628
+ '.dvo-status{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-secondary);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
629
+ '.dvo-err{color:var(--dsw-alias-state-error-primary)}' +
630
+ '.dvo-count{font-variant-numeric:tabular-nums;font-size:13px;color:var(--dsw-alias-label-secondary)}' +
631
+ '.dvo-spin{animation:dvo-spin 1s linear infinite}' +
632
+ '@keyframes dvo-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}' +
633
+ '.dvo-btn-active{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}' +
634
+ '.dvo-audio-wrap{display:flex;align-items:center;gap:8px;padding:2px 10px;background:var(--dsw-alias-bg-layer-2);border-radius:14px;border:1px solid var(--dsw-alias-border-l1)}' +
635
+ '.dvo-audio-play{display:flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);border:0;cursor:pointer;padding:0}' +
636
+ '.dvo-audio-play:hover{opacity:0.9}' +
637
+ '.dvo-audio-time{font-size:12px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary)}' +
638
+ '.dvo-dash{display:flex;flex-direction:column;gap:8px;margin-top:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px}' +
639
+ '.dvo-dash-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:8px;margin-top:4px}' +
640
+ '.dvo-dash-item{display:flex;flex-direction:column;gap:4px;padding:8px 10px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l1);border-radius:8px}' +
641
+ '.dvo-dash-name{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
642
+ '.dvo-dash-row{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary)}' +
643
+ '.dvo-badge{display:inline-flex;align-items:center;font-size:11px;padding:1px 6px;border-radius:6px;font-weight:600}' +
644
+ '.dvo-badge-fast{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-success-primary);border:1px solid var(--dsw-alias-state-success-primary)}' +
645
+ '.dvo-badge-norm{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-warning-primary);border:1px solid var(--dsw-alias-state-warning-primary)}' +
646
+ '.dvo-badge-slow{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-warning-primary);border:1px solid var(--dsw-alias-border-l2)}' +
647
+ '.dvo-badge-err{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary)}' +
648
+ '.dvo-badge-idle{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary)}' +
649
+ '.dvo-mic-test{display:flex;align-items:center;gap:10px;margin-top:8px;padding:6px 12px;background:var(--dsw-alias-bg-layer-2);border-radius:8px;border:1px solid var(--dsw-alias-border-l1)}' +
650
+ '.dvo-meter-bar{flex:1;height:8px;background:var(--dsw-alias-bg-layer-3);border-radius:4px;overflow:hidden;position:relative}' +
651
+ '.dvo-meter-fill{height:100%;background:var(--dsw-alias-state-success-primary);border-radius:4px;transition:width 60ms ease-out}' +
652
+ '.dvo-meter-gate{position:absolute;top:0;bottom:0;width:2px;background:var(--dsw-alias-state-warning-primary);box-shadow:0 0 4px var(--dsw-alias-state-warning-primary);z-index:2;pointer-events:none}' +
653
+ '.dvo-silence-ring{width:34px;height:34px;transform:rotate(-90deg);flex:none}' +
654
+ '.dvo-ring-bg{fill:none;stroke:var(--dsw-alias-border-l1);stroke-width:2.5}' +
655
+ '.dvo-ring-fg{fill:none;stroke:var(--dsw-alias-state-info-primary);stroke-width:2.5;stroke-linecap:round;transition:stroke-dashoffset 100ms linear}' +
656
+ '.dvo-ghost{font-style:italic;color:var(--dsw-alias-label-tertiary);margin-left:6px;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px}' +
657
+ '.dvo-installer-box{display:flex;flex-direction:column;gap:8px;padding:10px 12px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;margin-top:8px}' +
658
+ '.dvo-installer-row{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
659
+ '.dvo-progress-bar{height:6px;width:100%;background:var(--dsw-alias-bg-layer-3);border-radius:3px;overflow:hidden}' +
660
+ '.dvo-progress-fill{height:100%;background:var(--dsw-alias-state-info-primary);transition:width 150ms ease}' +
661
+ '.dvo-jargon-table{width:100%;margin-top:6px;border-collapse:collapse;font-size:12px}' +
662
+ '.dvo-jargon-table td{padding:4px 6px;border-bottom:1px solid var(--dsw-alias-border-l1)}' +
663
+ '.dvo-pill-preview{display:flex;flex-direction:column;gap:8px;padding:10px 14px;border-radius:18px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);width:100%;max-width:720px;margin:0 auto;box-shadow:0 8px 24px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);box-sizing:border-box}' +
664
+ '.dvo-preview-header{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%}' +
665
+ '.dvo-preview-tabs{display:inline-flex;align-items:center;gap:4px;background:var(--dsw-alias-bg-layer-2);padding:2px;border-radius:12px;border:1px solid var(--dsw-alias-border-l1)}' +
666
+ '.dvo-tab-btn{border:none;background:transparent;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:500;padding:3px 10px;border-radius:10px;cursor:pointer;transition:all 150ms ease}' +
667
+ '.dvo-tab-btn:hover{color:var(--dsw-alias-label-primary)}' +
668
+ '.dvo-tab-btn-active{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 1px 3px color-mix(in srgb, var(--dsw-alias-label-primary) 15%, transparent)}' +
669
+ '.dvo-preview-body{display:flex;align-items:center;gap:8px;width:100%}' +
670
+ '.dvo-preview-text{flex:1;font-size:13px;color:var(--dsw-alias-label-primary);line-height:1.4;word-break:break-word;max-height:80px;overflow-y:auto;padding:2px 0}' +
671
+ '.dvo-preview-input{flex:1;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 8px;outline:none;font-family:inherit;width:100%}' +
672
+ '.dvo-preview-input:focus{border-color:var(--dsw-alias-state-info-primary)}' +
673
+ '.dvo-btn-send{display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:50%;cursor:pointer;padding:0;flex:none;box-sizing:border-box;border:none;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);transition:opacity 150ms ease}' +
674
+ '.dvo-btn-send:hover{opacity:0.9}' +
675
+ '.dvo-warn-banner{font-size:12px;color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-error-primary);padding:4px 8px;border-radius:6px;margin-top:4px}' + '.dvo-mode-pill-btn{display:inline-flex;align-items:center;justify-content:center;height:28px;padding:0 8px;font-size:11px;font-weight:600;border-radius:14px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);user-select:none;touch-action:manipulation;transition:all 150ms ease}' + '.dvo-mode-pill-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary)}' + '.dvo-mode-badge{font-size:11px;font-weight:600;padding:2px 6px;border-radius:6px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);margin-right:4px}' + '.dvo-macro-item{display:flex;flex-direction:column;gap:6px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;margin-top:6px}' + '.dvo-macro-actions{display:flex;justify-content:flex-end;gap:6px}' + '.dvo-fallback-badge{font-size:11px;padding:2px 6px;border-radius:4px;color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-warning-primary)}'
676
676
  const cssId = 'dsh-voice/client.module.css'
677
677
  if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-voice"][data-plugin-css="' + cssId + '"]')) {
678
678
  const tag = document.createElement('style')
@@ -935,7 +935,7 @@ window.__ModuleLoader__.load({
935
935
  try {
936
936
  res = await fetch('/dsh-voice/transcribe', {
937
937
  method: 'POST',
938
- headers: { 'content-type': 'application/json' },
938
+ headers: { 'content-type': 'application/json', ...(typeof window !== 'undefined' && window.__dshVoiceToken ? { 'x-dsh-voice-token': window.__dshVoiceToken } : {}) },
939
939
  body: JSON.stringify(payload),
940
940
  signal: controller ? controller.signal : undefined,
941
941
  })
@@ -1805,7 +1805,7 @@ voice._chainsPromise = null
1805
1805
  try {
1806
1806
  const pRes = await fetch('/dsh-voice/polish', {
1807
1807
  method: 'POST',
1808
- headers: { 'content-type': 'application/json' },
1808
+ headers: { 'content-type': 'application/json', ...(typeof window !== 'undefined' && window.__dshVoiceToken ? { 'x-dsh-voice-token': window.__dshVoiceToken } : {}) },
1809
1809
  body: JSON.stringify({ text }),
1810
1810
  })
1811
1811
  const pData = await pRes.json()
@@ -4299,6 +4299,7 @@ function MacrosSection(props) {
4299
4299
  setStatusLatency(Math.round(performance.now() - start))
4300
4300
  if (data && data.ok) {
4301
4301
  setStatusData(data)
4302
+ if (data.sessionToken && typeof window !== 'undefined') window.__dshVoiceToken = data.sessionToken
4302
4303
  if (data.webgpuModel && voice.settings) {
4303
4304
  voice.settings.webgpuModel = data.webgpuModel
4304
4305
  }
@@ -1,6 +1,6 @@
1
1
  // HTTP host routes registered on ctx.webServer.
2
2
  import os from 'node:os'
3
- import { readBody, isLoopback, isTrustedCaller } from './http-util.js'
3
+ import { readBody, isLoopback, isTrustedCaller, getVoiceSessionToken } from './http-util.js'
4
4
  import { resolveSensevoiceProvider } from './sensevoice-installer.js'
5
5
  import { applyVoiceMacros } from './macros.js'
6
6
  import { createSyntheticWav } from './wav.js'
@@ -45,6 +45,7 @@ export function registerHostRoutes(ctx, deps) {
45
45
  const sensevoiceError = sensevoiceOk ? null : getSensevoiceError()
46
46
  writeJson(res, 200, {
47
47
  ok: true,
48
+ sessionToken: getVoiceSessionToken(),
48
49
  whisperRunning: whisperOk,
49
50
  whisperError: whisperOk ? null : whisperError,
50
51
  sensevoiceRunning: sensevoiceOk,
@@ -154,7 +155,7 @@ export function registerHostRoutes(ctx, deps) {
154
155
  path: '/dsh-voice/benchmark',
155
156
  handler: async (req, res) => {
156
157
  if (req.method !== 'POST') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } }); return }
157
- if (!isTrustedCaller(req)) { writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden source origin' } }); return }
158
+ if (!isTrustedCaller(req, { loopbackOnly: true })) { writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden source origin' } }); return }
158
159
 
159
160
  const cfg = live()
160
161
  const effectiveProvider = resolveSensevoiceProvider(cfg)
package/lib/http-util.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // Pure HTTP helpers shared by host routes. No cordis, no network.
2
+ import { randomBytes, timingSafeEqual } from 'node:crypto'
2
3
 
3
4
  export class BodyTooLargeError extends Error {
4
5
  constructor(message = 'body too large') {
@@ -9,6 +10,29 @@ export class BodyTooLargeError extends Error {
9
10
  }
10
11
  }
11
12
 
13
+ let sessionSecret = ''
14
+ export function getVoiceSessionToken() {
15
+ if (!sessionSecret) {
16
+ sessionSecret = randomBytes(24).toString('hex')
17
+ }
18
+ return sessionSecret
19
+ }
20
+
21
+ export function setVoiceSessionToken(t) {
22
+ sessionSecret = String(t || '')
23
+ }
24
+
25
+ export function verifyVoiceSessionToken(token) {
26
+ if (!token || typeof token !== 'string') return false
27
+ const secret = getVoiceSessionToken()
28
+ if (token.length !== secret.length) return false
29
+ try {
30
+ return timingSafeEqual(Buffer.from(token), Buffer.from(secret))
31
+ } catch {
32
+ return false
33
+ }
34
+ }
35
+
12
36
  export function writeJson(res, code, body) {
13
37
  try {
14
38
  const headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }
@@ -77,12 +101,16 @@ export function isLoopback(addr) {
77
101
  }
78
102
 
79
103
  /**
80
- * Verify that the HTTP request originates from a trusted caller (loopback or same-origin).
81
- * Fail-closed security guard for state-changing or quota-consuming POST routes.
104
+ * Verify that the HTTP request originates from a trusted caller.
105
+ * Fail-closed security guard for state-changing or quota-consuming POST routes (#117, #173, #277).
106
+ * - Loopback callers (127.0.0.1, ::1) are trusted unless rejected by browser security headers.
107
+ * - Non-loopback LAN mutating/quota-consuming POST/PUT requests require valid session token (#277).
108
+ * - Routes marked loopbackOnly strictly reject non-loopback callers.
82
109
  * @param {import('node:http').IncomingMessage} req
110
+ * @param {{ loopbackOnly?: boolean }} [options]
83
111
  * @returns {boolean}
84
112
  */
85
- export function isTrustedCaller(req) {
113
+ export function isTrustedCaller(req, options = {}) {
86
114
  if (!req) return false
87
115
 
88
116
  // 1. Browser security headers: immediately reject cross-site and same-site requests (#117, #173)
@@ -113,7 +141,6 @@ export function isTrustedCaller(req) {
113
141
  }
114
142
 
115
143
  // 4. For state-changing or data-carrying requests (POST/PUT/PATCH), require application/json or octet-stream
116
- // Cross-origin simple form submissions cannot set application/json without a CORS preflight.
117
144
  const method = String(req.method || 'GET').toUpperCase()
118
145
  if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
119
146
  const contentType = String(req.headers?.['content-type'] || '').toLowerCase()
@@ -128,7 +155,20 @@ export function isTrustedCaller(req) {
128
155
  const remote = req.socket?.remoteAddress || req.connection?.remoteAddress || ''
129
156
  if (isLoopback(remote)) return true
130
157
 
131
- // 6. Non-loopback requests must be verified same-origin
158
+ // If route strictly requires loopback (e.g. host model installer or hardware benchmark)
159
+ if (options && options.loopbackOnly) return false
160
+
161
+ // 6. Non-loopback LAN requests: POST/PUT/PATCH quota-burning operations require session auth token (#277)
162
+ const isMutating = method === 'POST' || method === 'PUT' || method === 'PATCH'
163
+ if (isMutating) {
164
+ const incomingToken = String(req.headers?.['x-dsh-voice-token'] || req.headers?.['x-voice-token'] || '')
165
+ if (incomingToken && verifyVoiceSessionToken(incomingToken)) {
166
+ return true
167
+ }
168
+ return false
169
+ }
170
+
171
+ // 7. Non-state-changing GET requests from LAN: verify same-origin
132
172
  if (secSite === 'same-origin') return true
133
173
  if (origin) {
134
174
  try {
@@ -146,4 +186,4 @@ export function isTrustedCaller(req) {
146
186
  }
147
187
 
148
188
  return false
149
- }
189
+ }
package/lib/index.js CHANGED
@@ -114,7 +114,9 @@ export function apply(ctx, baseConfig) {
114
114
  ctx.inject(['settings'], (sctx) => {
115
115
  settingsService = sctx.settings
116
116
  try {
117
- scope = sctx.settings.register(NS, Config, { base: baseConfig })
117
+ if (typeof sctx.settings?.register === 'function') {
118
+ scope = sctx.settings.register(NS, Config, { base: baseConfig })
119
+ }
118
120
  if (scope && typeof scope.get === 'function') {
119
121
  getConfig = () => scope.get() ?? describeRow()?.config ?? readEntryConfig()
120
122
  } else {
@@ -133,16 +135,10 @@ export function apply(ctx, baseConfig) {
133
135
  triggerAutostart()
134
136
  })
135
137
 
136
- if (typeof ctx.on === 'function') {
137
- if (typeof ctx.effect === 'function') {
138
- ctx.effect(() => ctx.on('loader/volatile-update', () => triggerAutostart()), 'dsh-voice: volatile autostart')
139
- ctx.effect(() => ctx.on('settings/document-updated', () => triggerAutostart()), 'dsh-voice: settings autostart')
140
- ctx.effect(() => ctx.on('config', () => triggerAutostart()), 'dsh-voice: config autostart')
141
- } else {
142
- ctx.on('loader/volatile-update', () => triggerAutostart())
143
- ctx.on('settings/document-updated', () => triggerAutostart())
144
- ctx.on('config', () => triggerAutostart())
145
- }
138
+ if (typeof ctx.effect === 'function' && typeof ctx.on === 'function') {
139
+ ctx.effect(() => ctx.on('loader/volatile-update', () => triggerAutostart()), 'dsh-voice: volatile autostart')
140
+ ctx.effect(() => ctx.on('settings/document-updated', () => triggerAutostart()), 'dsh-voice: settings autostart')
141
+ ctx.effect(() => ctx.on('config', () => triggerAutostart()), 'dsh-voice: config autostart')
146
142
  }
147
143
 
148
144
  triggerAutostart()
@@ -35,8 +35,10 @@ export function createLocalDaemons({ live, ctx }) {
35
35
  try { child.kill() } catch { /* ignore */ }
36
36
  child = null
37
37
  }
38
+ const rawLang = cfg?.dictation?.language
39
+ const safeLang = isAutoLang(rawLang) ? 'auto' : String(rawLang || '').replace(/[^a-zA-Z0-9_-]/g, '') || 'auto'
38
40
  const command = `${JSON.stringify(cfg.whisperBin)} -m ${JSON.stringify(cfg.whisperModel)}`
39
- + ` --host 127.0.0.1 --port 8001 -t 8 -p 1 -l ${isAutoLang(cfg.dictation.language) ? 'auto' : cfg.dictation.language}`
41
+ + ` --host 127.0.0.1 --port 8001 -t 8 -p 1 -l ${JSON.stringify(safeLang)}`
40
42
  const specOpts = { command, onExpiry: 'none', timeoutMs: 60000, stdoutMaxBytes: 4 * 1024 * 1024 }
41
43
  const spec = typeof ctx.shell?.resolve === 'function' ? ctx.shell.resolve(specOpts) : specOpts
42
44
 
@@ -1,7 +1,8 @@
1
1
  // lib/sensevoice-installer.js — 1-Click installer for SenseVoice-Small & Whisper Large-v3-Turbo
2
2
  // Downloads pre-quantized ONNX models and configures autostart on the host via sherpa-onnx.
3
3
 
4
- import { createWriteStream, existsSync, readdirSync } from 'node:fs'
4
+ import { createWriteStream, createReadStream, existsSync, readdirSync } from 'node:fs'
5
+ import { createHash } from 'node:crypto'
5
6
  import { mkdir, unlink } from 'node:fs/promises'
6
7
  import { homedir } from 'node:os'
7
8
  import path from 'node:path'
@@ -20,6 +21,29 @@ export const WHISPER_TURBO_MODEL_URL =
20
21
  export const WHISPER_TURBO_MODEL_MIRROR =
21
22
  'https://huggingface.co/csukuangfj/sherpa-onnx-whisper-turbo/resolve/main/sherpa-onnx-whisper-turbo.tar.bz2'
22
23
 
24
+ export const MODEL_SHA256 = {
25
+ 'sensevoice': 'f6b2a72ebcb1ac7a764d4cfccd886e6bcb2a95c4657c2199d0ba95ed4b9ea71a',
26
+ 'whisper-turbo': 'b11acbbcd660b44a8e0df33724feb5aaa709cf65668f2823d59f656312544f22',
27
+ }
28
+
29
+ export function verifyArchiveSha256(filePath, expectedSha256) {
30
+ if (!expectedSha256) return Promise.resolve(true)
31
+ return new Promise((resolve, reject) => {
32
+ const hash = createHash('sha256')
33
+ const stream = createReadStream(filePath)
34
+ stream.on('data', (chunk) => hash.update(chunk))
35
+ stream.on('end', () => {
36
+ const digest = hash.digest('hex').toLowerCase()
37
+ if (digest === expectedSha256.toLowerCase()) {
38
+ resolve(true)
39
+ } else {
40
+ reject(new Error(`SHA-256 verification failed: expected ${expectedSha256}, got ${digest}`))
41
+ }
42
+ })
43
+ stream.on('error', reject)
44
+ })
45
+ }
46
+
23
47
  let installerState = {
24
48
  installing: false,
25
49
  progress: 0,
@@ -176,7 +200,7 @@ export async function downloadArchive(url, destPath, onProgress) {
176
200
 
177
201
  export function extractTarBz2(archivePath, targetDir) {
178
202
  return new Promise((resolve, reject) => {
179
- const child = spawn('tar', ['-xjf', archivePath, '-C', targetDir], { windowsHide: true })
203
+ const child = spawn('tar', ['--no-same-owner', '-xjf', archivePath, '-C', targetDir], { windowsHide: true })
180
204
  let stderr = ''
181
205
  child.stderr.on('data', (d) => { stderr += d })
182
206
  child.on('close', (code) => {
@@ -233,6 +257,13 @@ export async function startSensevoiceInstall(ctx, cfg, updateConfigFn, startSens
233
257
 
234
258
  if (!downloadOk) throw new Error(`Could not download ${targetModel} model from primary or mirror URL`)
235
259
 
260
+ installerState.step = 'verifying checksum'
261
+ installerState.progress = 92
262
+ const expectedHash = MODEL_SHA256[targetModel]
263
+ if (expectedHash) {
264
+ await verifyArchiveSha256(archivePath, expectedHash)
265
+ }
266
+
236
267
  installerState.step = 'extracting'
237
268
  installerState.progress = 95
238
269
  await extractTarBz2(archivePath, modelDir)
@@ -281,7 +312,7 @@ export function registerSensevoiceInstaller(ctx, { liveConfig, isAlive, updateCo
281
312
  kind: 'exact',
282
313
  path: '/dsh-voice/sensevoice-installer',
283
314
  handler: async (req, res) => {
284
- if (!isTrustedCaller(req)) {
315
+ if (!isTrustedCaller(req, { loopbackOnly: true })) {
285
316
  writeJson(res, 403, { ok: false, error: 'Forbidden: untrusted caller' })
286
317
  return
287
318
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-voice",
3
- "version": "0.9.35",
3
+ "version": "0.9.36",
4
4
  "description": "Voice input for DeepSeek Harness: dictation chunked by pauses and voice messages, each with its own provider fallback chain (Deepgram, Groq, HuggingFace, local whisper.cpp, plus any OpenAI-compatible API of your own).",
5
5
  "license": "MIT",
6
6
  "type": "module",