@goodandready/dsh-subscriptions 0.7.11 → 0.7.13
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 +143 -54
- package/lib/http.js +2 -5
- package/lib/mask.js +1 -0
- package/lib/ollama-health.js +25 -0
- package/lib/routes/oauth.js +17 -0
- package/lib/routes/sources.js +2 -2
- package/lib/routes/status.js +5 -3
- package/lib/routes/vault.js +7 -3
- package/lib/vault-snapshots.js +2 -0
- package/lib/vault-sync.js +43 -6
- package/lib/vendors/grok.js +1 -0
- package/lib/vendors/index.js +1 -0
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -390,6 +390,47 @@ window.__ModuleLoader__.load({
|
|
|
390
390
|
"cachePinnedBadge": "Cache Pinned",
|
|
391
391
|
"reasoningEffortTitle": "Reasoning Effort Control",
|
|
392
392
|
"reasoningEffortDesc": "Configure reasoning depth for models with extended thinking capabilities.",
|
|
393
|
+
"quotaWidget": "Quota widget",
|
|
394
|
+
"quotaWidgetOff": "Off",
|
|
395
|
+
"quotaWidgetDocked": "Docked",
|
|
396
|
+
"quotaWidgetFloating": "Floating (draggable)",
|
|
397
|
+
"quotaWidgetHint": "Drag/dock.",
|
|
398
|
+
"webhookUrl": "Webhook URL",
|
|
399
|
+
"webhookUrlHint": "Signed target.",
|
|
400
|
+
"webhookSecret": "Webhook secret",
|
|
401
|
+
"webhookFormat": "Webhook format",
|
|
402
|
+
"webhookFormatNative": "Native",
|
|
403
|
+
"webhookFormatNotify": "dsh-plugin-notify compatible",
|
|
404
|
+
"digestIntervalHours": "Digest interval (hours)",
|
|
405
|
+
"digestHint": "0 = off.",
|
|
406
|
+
"sendTest": "Send test",
|
|
407
|
+
"delivered": "delivered",
|
|
408
|
+
"vaultSnapshots": "Vault snapshots",
|
|
409
|
+
"vaultSnapshotsHint": "Auto pre-pull.",
|
|
410
|
+
"createSnapshot": "Create snapshot",
|
|
411
|
+
"restoreSnapshot": "Restore",
|
|
412
|
+
"noSnapshots": "No snapshots yet.",
|
|
413
|
+
"created": "created",
|
|
414
|
+
"restoredSlots": "restored slots:",
|
|
415
|
+
"providerPacks": "Provider packs",
|
|
416
|
+
"providerPacksHint": "Add provider JSON.",
|
|
417
|
+
"noPacks": "No packs yet.",
|
|
418
|
+
"validateAndSave": "Validate & save",
|
|
419
|
+
"exportPacks": "Export",
|
|
420
|
+
"exported": "exported",
|
|
421
|
+
"saved": "saved",
|
|
422
|
+
"sources": "Sources",
|
|
423
|
+
"sourcesHint": "OAuth → key → Ollama.",
|
|
424
|
+
"keyRotationOffline": "key-rotation not installed/offline",
|
|
425
|
+
"keyRotationHealth": "key-rotation health",
|
|
426
|
+
"apiKeySource": "API key",
|
|
427
|
+
"oauthSource": "OAuth",
|
|
428
|
+
"ollamaSource": "Ollama",
|
|
429
|
+
"sourceNone": "none",
|
|
430
|
+
"openSettings": "Open settings",
|
|
431
|
+
"sourceAvailable": "available",
|
|
432
|
+
"sourceUnavailable": "unavailable",
|
|
433
|
+
"cooldownLabel": "cooldown",
|
|
393
434
|
"reasoningLevelOff": "Off"
|
|
394
435
|
};
|
|
395
436
|
|
|
@@ -704,6 +745,47 @@ window.__ModuleLoader__.load({
|
|
|
704
745
|
"cachePinnedBadge": "缓存粘性",
|
|
705
746
|
"reasoningEffortTitle": "推理强度控制",
|
|
706
747
|
"reasoningEffortDesc": "为具备扩展思考能力的模型配置推理深度与思考预算。",
|
|
748
|
+
"quotaWidget": "额度组件",
|
|
749
|
+
"quotaWidgetOff": "关闭",
|
|
750
|
+
"quotaWidgetDocked": "停靠",
|
|
751
|
+
"quotaWidgetFloating": "浮动(可拖动)",
|
|
752
|
+
"quotaWidgetHint": "拖动/停靠",
|
|
753
|
+
"webhookUrl": "Webhook 地址",
|
|
754
|
+
"webhookUrlHint": "签名目标。",
|
|
755
|
+
"webhookSecret": "Webhook 密钥",
|
|
756
|
+
"webhookFormat": "Webhook 格式",
|
|
757
|
+
"webhookFormatNative": "原生",
|
|
758
|
+
"webhookFormatNotify": "兼容 dsh-plugin-notify",
|
|
759
|
+
"digestIntervalHours": "摘要间隔(小时)",
|
|
760
|
+
"digestHint": "0 关闭。",
|
|
761
|
+
"sendTest": "发送测试",
|
|
762
|
+
"delivered": "已送达",
|
|
763
|
+
"vaultSnapshots": "Vault 快照",
|
|
764
|
+
"vaultSnapshotsHint": "自动预拉取",
|
|
765
|
+
"createSnapshot": "创建快照",
|
|
766
|
+
"restoreSnapshot": "恢复",
|
|
767
|
+
"noSnapshots": "尚无快照。",
|
|
768
|
+
"created": "已创建",
|
|
769
|
+
"restoredSlots": "已恢复 slots:",
|
|
770
|
+
"providerPacks": "供应商包",
|
|
771
|
+
"providerPacksHint": "JSON 添加",
|
|
772
|
+
"noPacks": "尚无供应商包。",
|
|
773
|
+
"validateAndSave": "验证并保存",
|
|
774
|
+
"exportPacks": "导出",
|
|
775
|
+
"exported": "已导出",
|
|
776
|
+
"saved": "已保存",
|
|
777
|
+
"sources": "来源",
|
|
778
|
+
"sourcesHint": "订阅→密钥→Ollama",
|
|
779
|
+
"keyRotationOffline": "key-rotation 未安装或离线",
|
|
780
|
+
"keyRotationHealth": "key-rotation 健康度",
|
|
781
|
+
"apiKeySource": "API 密钥",
|
|
782
|
+
"oauthSource": "OAuth",
|
|
783
|
+
"ollamaSource": "Ollama",
|
|
784
|
+
"sourceNone": "无",
|
|
785
|
+
"openSettings": "打开设置",
|
|
786
|
+
"sourceAvailable": "可用",
|
|
787
|
+
"sourceUnavailable": "不可用",
|
|
788
|
+
"cooldownLabel": "冷却",
|
|
707
789
|
"reasoningLevelOff": "关闭"
|
|
708
790
|
};
|
|
709
791
|
// #51: live countdown to the quota window reset (account.quota.resetAt).
|
|
@@ -855,6 +937,7 @@ window.__ModuleLoader__.load({
|
|
|
855
937
|
// The translator comes from the slot because its registration declares locale.
|
|
856
938
|
const t = (props && props.t) || ((key) => key)
|
|
857
939
|
const [draft, setDraft] = React.useState(null)
|
|
940
|
+
const [newProviders, setNewProviders] = React.useState([])
|
|
858
941
|
const [accounts, setAccounts] = React.useState([])
|
|
859
942
|
const [providers, setProviders] = React.useState([])
|
|
860
943
|
const [modelsCatalog, setModelsCatalog] = React.useState([])
|
|
@@ -1560,6 +1643,7 @@ window.__ModuleLoader__.load({
|
|
|
1560
1643
|
const used = slots.filter((s) => s.provider === provider).map((s) => s.index)
|
|
1561
1644
|
let index = 1
|
|
1562
1645
|
while (used.indexOf(index) >= 0) index++
|
|
1646
|
+
setNewProviders((prev) => prev.includes(provider) ? prev : prev.concat([provider]))
|
|
1563
1647
|
setSlots((prev) => prev.concat([{ provider: provider, index: index, label: '' }]))
|
|
1564
1648
|
}
|
|
1565
1649
|
|
|
@@ -1829,21 +1913,21 @@ window.__ModuleLoader__.load({
|
|
|
1829
1913
|
),
|
|
1830
1914
|
),
|
|
1831
1915
|
h('div', { className: 'dsub-sub' }, t('composerQuotaDisplayHint')), h('div', { className: 'dsub-row' },
|
|
1832
|
-
h('span', { className: 'dsub-h' }, '
|
|
1916
|
+
h('span', { className: 'dsub-h' }, t('quotaWidget')),
|
|
1833
1917
|
h('select', {
|
|
1834
1918
|
className: 'dsub-select',
|
|
1835
1919
|
value: draft.quotaWidget || 'docked',
|
|
1836
1920
|
onChange: (e) => setDraft((d) => Object.assign({}, d, { quotaWidget: e.target.value })),
|
|
1837
1921
|
},
|
|
1838
|
-
h('option', { value: 'off' }, '
|
|
1839
|
-
h('option', { value: 'docked' }, '
|
|
1840
|
-
h('option', { value: 'floating' },
|
|
1922
|
+
h('option', { value: 'off' }, t('quotaWidgetOff')),
|
|
1923
|
+
h('option', { value: 'docked' }, t('quotaWidgetDocked')),
|
|
1924
|
+
h('option', { value: 'floating' }, t('quotaWidgetFloating')),
|
|
1841
1925
|
),
|
|
1842
1926
|
),
|
|
1843
|
-
h('div', { className: 'dsub-sub' }, '
|
|
1927
|
+
h('div', { className: 'dsub-sub' }, t('quotaWidgetHint')),
|
|
1844
1928
|
h(SourcesPanel, { t }),
|
|
1845
|
-
h(ProviderPacksPanel),
|
|
1846
|
-
h(VaultSnapshotsPanel),
|
|
1929
|
+
h(ProviderPacksPanel, { t }),
|
|
1930
|
+
h(VaultSnapshotsPanel, { t }),
|
|
1847
1931
|
|
|
1848
1932
|
h('div', { className: 'dsub-row' },
|
|
1849
1933
|
h('span', { className: 'dsub-h' }, t('composerQuota')),
|
|
@@ -1875,7 +1959,7 @@ window.__ModuleLoader__.load({
|
|
|
1875
1959
|
h('div', { className: 'dsub-sub' },
|
|
1876
1960
|
t('expiryNotifyDaysHint')),
|
|
1877
1961
|
h('div', { className: 'dsub-row' },
|
|
1878
|
-
h('span', { className: 'dsub-h' }, '
|
|
1962
|
+
h('span', { className: 'dsub-h' }, t('webhookUrl')),
|
|
1879
1963
|
h('input', {
|
|
1880
1964
|
type: 'url',
|
|
1881
1965
|
className: 'dsub-inp',
|
|
@@ -1884,20 +1968,20 @@ window.__ModuleLoader__.load({
|
|
|
1884
1968
|
onChange: (e) => setDraft((d) => Object.assign({}, d, { webhookUrl: e.target.value })),
|
|
1885
1969
|
}),
|
|
1886
1970
|
),
|
|
1887
|
-
h('div', { className: 'dsub-sub' }, '
|
|
1971
|
+
h('div', { className: 'dsub-sub' }, t('webhookUrlHint')),
|
|
1888
1972
|
h('div', { className: 'dsub-row' },
|
|
1889
|
-
h('span', { className: 'dsub-h' }, '
|
|
1973
|
+
h('span', { className: 'dsub-h' }, t('webhookFormat')),
|
|
1890
1974
|
h('select', {
|
|
1891
1975
|
className: 'dsub-select',
|
|
1892
1976
|
value: draft.webhookFormat || 'native',
|
|
1893
1977
|
onChange: (e) => setDraft((d) => Object.assign({}, d, { webhookFormat: e.target.value })),
|
|
1894
1978
|
},
|
|
1895
|
-
h('option', { value: 'native' }, '
|
|
1896
|
-
h('option', { value: 'notify' }, '
|
|
1979
|
+
h('option', { value: 'native' }, t('webhookFormatNative')),
|
|
1980
|
+
h('option', { value: 'notify' }, t('webhookFormatNotify')),
|
|
1897
1981
|
),
|
|
1898
1982
|
),
|
|
1899
1983
|
h('div', { className: 'dsub-row' },
|
|
1900
|
-
h('span', { className: 'dsub-h' }, '
|
|
1984
|
+
h('span', { className: 'dsub-h' }, t('webhookSecret')),
|
|
1901
1985
|
h('input', {
|
|
1902
1986
|
type: 'password',
|
|
1903
1987
|
className: 'dsub-inp',
|
|
@@ -1907,7 +1991,7 @@ window.__ModuleLoader__.load({
|
|
|
1907
1991
|
}),
|
|
1908
1992
|
),
|
|
1909
1993
|
h('div', { className: 'dsub-row' },
|
|
1910
|
-
h('span', { className: 'dsub-h' },
|
|
1994
|
+
h('span', { className: 'dsub-h' }, t('digestIntervalHours')),
|
|
1911
1995
|
h('input', {
|
|
1912
1996
|
type: 'number',
|
|
1913
1997
|
className: 'dsub-inp',
|
|
@@ -1918,8 +2002,8 @@ window.__ModuleLoader__.load({
|
|
|
1918
2002
|
onChange: (e) => setDraft((d) => Object.assign({}, d, { digestIntervalHours: Number(e.target.value) || 0 })),
|
|
1919
2003
|
}),
|
|
1920
2004
|
),
|
|
1921
|
-
h('div', { className: 'dsub-sub' }, '
|
|
1922
|
-
h(WebhookTestButton),
|
|
2005
|
+
h('div', { className: 'dsub-sub' }, t('digestHint')),
|
|
2006
|
+
h(WebhookTestButton, { t }),
|
|
1923
2007
|
),
|
|
1924
2008
|
h('details', { className: 'dsub-group', open: true },
|
|
1925
2009
|
h('summary', { className: 'dsub-groupHead' }, '🚦 ' + t('groupQueue')),
|
|
@@ -2060,19 +2144,19 @@ window.__ModuleLoader__.load({
|
|
|
2060
2144
|
h('div', { className: 'dsub-sub', style: { marginTop: '4px' } }, t('modelsCatalogDesc')),
|
|
2061
2145
|
h('div', { className: 'dsub-model-grid' },
|
|
2062
2146
|
(Array.isArray(modelsCatalog) && modelsCatalog.length ? modelsCatalog : [
|
|
2063
|
-
{ id: 'gpt-4o', contextLabel: '128k', capabilities: ['vision'], pool: { status: '
|
|
2064
|
-
{ id: 'o1 / o3-mini', contextLabel: '200k', capabilities: ['reasoning'], pool: { status: '
|
|
2065
|
-
{ id: 'claude-3-7-sonnet', contextLabel: '200k', capabilities: ['reasoning'], pool: { status: '
|
|
2066
|
-
{ id: 'claude-3-5-sonnet', contextLabel: '200k', capabilities: ['coding'], pool: { status: '
|
|
2067
|
-
{ id: 'grok-beta', contextLabel: '128k', capabilities: ['fast'], pool: { status: '
|
|
2068
|
-
{ id: 'gemini-2.5-pro', contextLabel: '1M', capabilities: ['vision'], pool: { status: '
|
|
2069
|
-
{ id: 'kimi-k1.5', contextLabel: '128k', capabilities: ['math'], pool: { status: '
|
|
2070
|
-
{ id: 'glm-4-plus', contextLabel: '128k', capabilities: ['chinese'], pool: { status: '
|
|
2147
|
+
{ id: 'gpt-4o', contextLabel: '128k', capabilities: ['vision'], pool: { status: t('sourceAvailable') } },
|
|
2148
|
+
{ id: 'o1 / o3-mini', contextLabel: '200k', capabilities: ['reasoning'], pool: { status: t('sourceAvailable') } },
|
|
2149
|
+
{ id: 'claude-3-7-sonnet', contextLabel: '200k', capabilities: ['reasoning'], pool: { status: t('sourceAvailable') } },
|
|
2150
|
+
{ id: 'claude-3-5-sonnet', contextLabel: '200k', capabilities: ['coding'], pool: { status: t('sourceAvailable') } },
|
|
2151
|
+
{ id: 'grok-beta', contextLabel: '128k', capabilities: ['fast'], pool: { status: t('sourceAvailable') } },
|
|
2152
|
+
{ id: 'gemini-2.5-pro', contextLabel: '1M', capabilities: ['vision'], pool: { status: t('sourceAvailable') } },
|
|
2153
|
+
{ id: 'kimi-k1.5', contextLabel: '128k', capabilities: ['math'], pool: { status: t('sourceAvailable') } },
|
|
2154
|
+
{ id: 'glm-4-plus', contextLabel: '128k', capabilities: ['chinese'], pool: { status: t('sourceAvailable') } },
|
|
2071
2155
|
]).map((m, idx) => {
|
|
2072
2156
|
const poolStatus = m.pool ? m.pool.status : 'unconfigured'
|
|
2073
2157
|
const statusClass = 'dsub-model-status-' + poolStatus
|
|
2074
2158
|
let poolLabel = ''
|
|
2075
|
-
if (poolStatus === '
|
|
2159
|
+
if (poolStatus === t('sourceAvailable')) {
|
|
2076
2160
|
poolLabel = '🟢 ' + (m.pool && m.pool.remainingPercent != null ? (m.pool.remainingPercent + '%') : t('modelPoolAvailable'))
|
|
2077
2161
|
} else if (poolStatus === 'degraded') {
|
|
2078
2162
|
poolLabel = '🟡 ' + t('modelPoolDegraded') + (m.pool ? ' (' + m.pool.healthyAccounts + '/' + m.pool.totalAccounts + ')' : '')
|
|
@@ -2460,7 +2544,8 @@ window.__ModuleLoader__.load({
|
|
|
2460
2544
|
|| slot.label || slot.proxyUrl || slot.expiresAt
|
|
2461
2545
|
|| (Array.isArray(slot.tags) && slot.tags.length) || slot.vipOnly || slot.isPro || slot.weight)
|
|
2462
2546
|
}
|
|
2463
|
-
const visibleNames = names.filter((prov) =>
|
|
2547
|
+
const visibleNames = names.filter((prov) => newProviders.includes(prov.id)
|
|
2548
|
+
|| slots.some((slot) => slot.provider === prov.id && configuredSlot(slot)))
|
|
2464
2549
|
const hiddenNames = names.filter((prov) => visibleNames.indexOf(prov) < 0)
|
|
2465
2550
|
return h(React.Fragment, null,
|
|
2466
2551
|
visibleNames.map((prov) => {
|
|
@@ -3703,7 +3788,8 @@ window.__ModuleLoader__.load({
|
|
|
3703
3788
|
)
|
|
3704
3789
|
}
|
|
3705
3790
|
|
|
3706
|
-
function WebhookTestButton() {
|
|
3791
|
+
function WebhookTestButton(props) {
|
|
3792
|
+
const t = (props && props.t) || ((k) => k)
|
|
3707
3793
|
const [state, setState] = React.useState({ busy: false, message: '', error: null })
|
|
3708
3794
|
const send = () => {
|
|
3709
3795
|
setState({ busy: true, message: '', error: null })
|
|
@@ -3711,19 +3797,20 @@ window.__ModuleLoader__.load({
|
|
|
3711
3797
|
.then(async (res) => {
|
|
3712
3798
|
const data = await res.json().catch(() => ({}))
|
|
3713
3799
|
if (!res.ok || !data.delivered) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
3714
|
-
setState({ busy: false, message: 'delivered', error: null })
|
|
3800
|
+
setState({ busy: false, message: t('delivered'), error: null })
|
|
3715
3801
|
})
|
|
3716
3802
|
.catch((err) => setState({ busy: false, message: '', error: String(err && err.message || err) }))
|
|
3717
3803
|
}
|
|
3718
3804
|
return h('div', { className: 'dsub-row' },
|
|
3719
|
-
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: send }, state.busy ? '…' : '
|
|
3805
|
+
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: send }, state.busy ? '…' : t('sendTest')),
|
|
3720
3806
|
state.message ? h('span', { className: 'dsub-ok' }, state.message) : null,
|
|
3721
3807
|
state.error ? h('span', { className: 'dsub-bad' }, state.error) : null,
|
|
3722
3808
|
)
|
|
3723
3809
|
}
|
|
3724
3810
|
|
|
3725
3811
|
// #736: vault snapshot list with create/restore. Payload stays server-side.
|
|
3726
|
-
function VaultSnapshotsPanel() {
|
|
3812
|
+
function VaultSnapshotsPanel(props) {
|
|
3813
|
+
const t = (props && props.t) || ((k) => k)
|
|
3727
3814
|
const [state, setState] = React.useState({ snapshots: [], stats: null, message: '', error: null, busy: false })
|
|
3728
3815
|
const pull = () => {
|
|
3729
3816
|
fetch('/dsh-subscriptions/vault/snapshots', { cache: 'no-store' })
|
|
@@ -3744,31 +3831,32 @@ window.__ModuleLoader__.load({
|
|
|
3744
3831
|
const create = () => {
|
|
3745
3832
|
setState((st) => Object.assign({}, st, { busy: true, message: '', error: null }))
|
|
3746
3833
|
call('/dsh-subscriptions/vault/snapshots', { label: 'manual' })
|
|
3747
|
-
.then((data) => { setState((st) => Object.assign({}, st, { busy: false, message: 'created ' + data.snapshot.id })); pull() })
|
|
3834
|
+
.then((data) => { setState((st) => Object.assign({}, st, { busy: false, message: t('created') + ' ' + data.snapshot.id })); pull() })
|
|
3748
3835
|
.catch((err) => setState((st) => Object.assign({}, st, { busy: false, error: String(err && err.message || err) })))
|
|
3749
3836
|
}
|
|
3750
3837
|
const restore = (id) => {
|
|
3751
3838
|
setState((st) => Object.assign({}, st, { busy: true, message: '', error: null }))
|
|
3752
3839
|
call('/dsh-subscriptions/vault/snapshots/restore', { id })
|
|
3753
|
-
.then((data) => { setState((st) => Object.assign({}, st, { busy: false, message: '
|
|
3840
|
+
.then((data) => { setState((st) => Object.assign({}, st, { busy: false, message: t('restoredSlots') + ' ' + data.slotCount })); pull() })
|
|
3754
3841
|
.catch((err) => setState((st) => Object.assign({}, st, { busy: false, error: String(err && err.message || err) })))
|
|
3755
3842
|
}
|
|
3756
3843
|
return h('div', { className: 'dsub-section-card' },
|
|
3757
|
-
h('div', { className: 'dsub-section-title' }, '
|
|
3758
|
-
h('div', { className: 'dsub-section-desc' }, '
|
|
3844
|
+
h('div', { className: 'dsub-section-title' }, t('vaultSnapshots'), h('button', { type: 'button', className: 'dsub-mini', onClick: create, disabled: state.busy }, state.busy ? '…' : t('createSnapshot'))),
|
|
3845
|
+
h('div', { className: 'dsub-section-desc' }, t('vaultSnapshotsHint')),
|
|
3759
3846
|
state.stats ? h('div', { className: 'dsub-dim' }, state.stats.count + ' snapshots · ' + state.stats.bytes + ' bytes · ' + state.stats.path) : null,
|
|
3760
3847
|
state.snapshots.length ? state.snapshots.map((snap) => h('div', { className: 'dsub-row', key: snap.id },
|
|
3761
3848
|
h('span', { className: 'dsub-h' }, snap.id),
|
|
3762
3849
|
h('span', { className: 'dsub-dim' }, (snap.label || '-') + ' · ' + snap.slotCount + ' slots · ' + snap.createdAt),
|
|
3763
|
-
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: () => restore(snap.id) }, '
|
|
3764
|
-
)) : h('div', { className: 'dsub-dim' }, '
|
|
3850
|
+
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: () => restore(snap.id) }, t('restoreSnapshot')),
|
|
3851
|
+
)) : h('div', { className: 'dsub-dim' }, t('noSnapshots')),
|
|
3765
3852
|
state.message ? h('div', { className: 'dsub-ok' }, state.message) : null,
|
|
3766
3853
|
state.error ? h('div', { className: 'dsub-bad' }, state.error) : null,
|
|
3767
3854
|
)
|
|
3768
3855
|
}
|
|
3769
3856
|
|
|
3770
3857
|
// #724: provider pack manager. Displays ids/models only; headers are masked.
|
|
3771
|
-
function ProviderPacksPanel() {
|
|
3858
|
+
function ProviderPacksPanel(props) {
|
|
3859
|
+
const t = (props && props.t) || ((k) => k)
|
|
3772
3860
|
const [state, setState] = React.useState({ packs: [], text: '', message: '', error: null, busy: false })
|
|
3773
3861
|
const publicPack = (pack) => {
|
|
3774
3862
|
const copy = Object.assign({}, pack)
|
|
@@ -3812,18 +3900,18 @@ window.__ModuleLoader__.load({
|
|
|
3812
3900
|
body: JSON.stringify({ config: Object.assign({}, cfg.config, { providerPacks: packs }), revision: cfg.revision }),
|
|
3813
3901
|
})
|
|
3814
3902
|
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
3815
|
-
setState((st) => Object.assign({}, st, { packs, busy: false, message: 'saved ' + checked.id, text: '' }))
|
|
3903
|
+
setState((st) => Object.assign({}, st, { packs, busy: false, message: t('saved') + ' ' + checked.id, text: '' }))
|
|
3816
3904
|
})
|
|
3817
3905
|
.catch((err) => setState((st) => Object.assign({}, st, { busy: false, error: String(err && err.message || err) })))
|
|
3818
3906
|
}
|
|
3819
|
-
const exportPacks = () => setState((st) => Object.assign({}, st, { text: JSON.stringify(st.packs.map(publicPack), null, 2), message: 'exported' }))
|
|
3907
|
+
const exportPacks = () => setState((st) => Object.assign({}, st, { text: JSON.stringify(st.packs.map(publicPack), null, 2), message: t('exported') }))
|
|
3820
3908
|
return h('div', { className: 'dsub-section-card' },
|
|
3821
|
-
h('div', { className: 'dsub-section-title' }, '
|
|
3822
|
-
h('div', { className: 'dsub-section-desc' }, '
|
|
3909
|
+
h('div', { className: 'dsub-section-title' }, t('providerPacks'), h('button', { type: 'button', className: 'dsub-mini', onClick: pull }, '↻')),
|
|
3910
|
+
h('div', { className: 'dsub-section-desc' }, t('providerPacksHint')),
|
|
3823
3911
|
state.packs.length ? state.packs.map((pack) => h('div', { className: 'dsub-row', key: pack.id },
|
|
3824
3912
|
h('span', { className: 'dsub-h' }, pack.id),
|
|
3825
3913
|
h('span', { className: 'dsub-dim' }, (pack.displayName || '') + ' · ' + ((pack.models || []).length) + ' models'),
|
|
3826
|
-
)) : h('div', { className: 'dsub-dim' }, '
|
|
3914
|
+
)) : h('div', { className: 'dsub-dim' }, t('noPacks')),
|
|
3827
3915
|
h('textarea', {
|
|
3828
3916
|
className: 'dsub-inp',
|
|
3829
3917
|
style: { width: '100%', minHeight: 120, fontFamily: 'ui-monospace,monospace', padding: 8 },
|
|
@@ -3832,8 +3920,8 @@ window.__ModuleLoader__.load({
|
|
|
3832
3920
|
onChange: (e) => setState((st) => Object.assign({}, st, { text: e.target.value, message: '', error: null })),
|
|
3833
3921
|
}),
|
|
3834
3922
|
h('div', { className: 'dsub-row' },
|
|
3835
|
-
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: save }, state.busy ? '…' : '
|
|
3836
|
-
h('button', { type: 'button', className: 'dsub-mini', onClick: exportPacks }, '
|
|
3923
|
+
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: save }, state.busy ? '…' : t('validateAndSave')),
|
|
3924
|
+
h('button', { type: 'button', className: 'dsub-mini', onClick: exportPacks }, t('exportPacks')),
|
|
3837
3925
|
),
|
|
3838
3926
|
state.message ? h('div', { className: 'dsub-ok' }, state.message) : null,
|
|
3839
3927
|
state.error ? h('div', { className: 'dsub-bad' }, state.error) : null,
|
|
@@ -3841,7 +3929,8 @@ window.__ModuleLoader__.load({
|
|
|
3841
3929
|
}
|
|
3842
3930
|
|
|
3843
3931
|
// #713: read-only source chain panel. Never renders key values.
|
|
3844
|
-
function SourcesPanel() {
|
|
3932
|
+
function SourcesPanel(props) {
|
|
3933
|
+
const t = (props && props.t) || ((k) => k)
|
|
3845
3934
|
const [state, setState] = React.useState({ loading: true, data: null, error: null })
|
|
3846
3935
|
const pull = () => {
|
|
3847
3936
|
fetch('/dsh-subscriptions/sources', { cache: 'no-store' })
|
|
@@ -3854,16 +3943,16 @@ window.__ModuleLoader__.load({
|
|
|
3854
3943
|
const id = setInterval(pull, 30 * 1000)
|
|
3855
3944
|
return () => clearInterval(id)
|
|
3856
3945
|
}, [])
|
|
3857
|
-
const sourceLabel = (source) => source === 'api-key' ? '
|
|
3946
|
+
const sourceLabel = (source) => source === 'api-key' ? t('apiKeySource') : (source === 'oauth' ? t('oauthSource') : (source === 'ollama' ? t('ollamaSource') : t('sourceNone')))
|
|
3858
3947
|
return h('div', { className: 'dsub-section-card' },
|
|
3859
|
-
h('div', { className: 'dsub-section-title' }, '
|
|
3860
|
-
h('div', { className: 'dsub-section-desc' }, '
|
|
3948
|
+
h('div', { className: 'dsub-section-title' }, t('sources'), h('button', { type: 'button', className: 'dsub-mini', onClick: pull }, state.loading ? '…' : '↻')),
|
|
3949
|
+
h('div', { className: 'dsub-section-desc' }, t('sourcesHint')),
|
|
3861
3950
|
state.error ? h('div', { className: 'dsub-bad' }, state.error) : null,
|
|
3862
3951
|
state.data && state.data.keyRotation && !state.data.keyRotation.available
|
|
3863
|
-
? h('div', { className: 'dsub-dim' }, '
|
|
3952
|
+
? h('div', { className: 'dsub-dim' }, t('keyRotationOffline') + (state.data.keyRotation.reason ? ': ' + state.data.keyRotation.reason : ''))
|
|
3864
3953
|
: null,
|
|
3865
3954
|
state.data && state.data.keyRotation && state.data.keyRotation.available
|
|
3866
|
-
? h('div', { className: 'dsub-dim' }, '
|
|
3955
|
+
? h('div', { className: 'dsub-dim' }, t('keyRotationHealth') + ': ' + state.data.keyRotation.healthyKeys + '/' + state.data.keyRotation.totalKeys + ' healthy keys')
|
|
3867
3956
|
: null,
|
|
3868
3957
|
(state.data && state.data.providers ? state.data.providers : []).map((row) => h('div', { className: 'dsub-row', key: row.provider },
|
|
3869
3958
|
h('span', { className: 'dsub-h' }, row.provider),
|
|
@@ -3993,16 +4082,16 @@ window.__ModuleLoader__.load({
|
|
|
3993
4082
|
},
|
|
3994
4083
|
h('div', { className: 'dsub-h' }, String(state.active.provider || '').toUpperCase() + ' sources'),
|
|
3995
4084
|
(state.candidates || []).map((c, i) => {
|
|
3996
|
-
const label = c.source === 'api-key' ? '
|
|
4085
|
+
const label = c.source === 'api-key' ? t('apiKeySource') : (c.source === 'oauth' ? t('oauthSource') : t('ollamaSource'))
|
|
3997
4086
|
const cd = c.detail && Array.isArray(c.detail.keys) ? Math.max(0, ...c.detail.keys.map((k) => Number(k.cooldownMsLeft) || 0)) : 0
|
|
3998
4087
|
return h('div', { className: 'dsub-row', key: i },
|
|
3999
4088
|
h('span', { className: 'dsub-h' }, label),
|
|
4000
|
-
h('span', { className: c.available ? 'dsub-ok' : 'dsub-dim' }, c.available ? '
|
|
4001
|
-
cd > 0 ? h('span', { className: 'dsub-dim' }, '
|
|
4089
|
+
h('span', { className: c.available ? 'dsub-ok' : 'dsub-dim' }, c.available ? t('sourceAvailable') : t('sourceUnavailable')),
|
|
4090
|
+
cd > 0 ? h('span', { className: 'dsub-dim' }, t('cooldownLabel') + ' ' + Math.ceil(cd / 1000) + 's') : null,
|
|
4002
4091
|
c.detail && c.detail.healthyKeys != null ? h('span', { className: 'dsub-dim' }, c.detail.healthyKeys + '/' + c.detail.totalKeys + ' keys') : null,
|
|
4003
4092
|
)
|
|
4004
4093
|
}),
|
|
4005
|
-
h('button', { type: 'button', className: 'dsub-mini', onClick: () => { setOpen(false); openSettings() } }, '
|
|
4094
|
+
h('button', { type: 'button', className: 'dsub-mini', onClick: () => { setOpen(false); openSettings() } }, t('openSettings')),
|
|
4006
4095
|
) : null
|
|
4007
4096
|
return h(React.Fragment, null, h('div', {
|
|
4008
4097
|
className: 'dsub-qw' + (docked ? ' dsub-qwDocked' : '') + tone,
|
package/lib/http.js
CHANGED
|
@@ -104,14 +104,11 @@ function configuredTrustedHosts(targetCtx) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* @returns {boolean}/**
|
|
107
|
+
* Reject cross-site and untrusted writes and sensitive reads.
|
|
108
|
+
* Fail-closed: must be loopback, same-origin/same-site, or authentic host credentials (#374).
|
|
110
109
|
* @param {import('node:http').IncomingMessage} request
|
|
111
110
|
* @param {any} [targetCtx]
|
|
112
111
|
* @returns {boolean}
|
|
113
|
-
* Reject cross-site and untrusted writes and sensitive reads.
|
|
114
|
-
* Fail-closed: must be loopback, same-origin/same-site, or authentic host credentials (#374).
|
|
115
112
|
*/
|
|
116
113
|
export function isTrustedSettingsRequest(request, targetCtx) {
|
|
117
114
|
const headers = request?.headers || {}
|
package/lib/mask.js
CHANGED
|
@@ -20,6 +20,7 @@ export function maskText(value) {
|
|
|
20
20
|
return String(value || '').replace(EMAIL_RE, (m) => maskEmail(m))
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** @type {Array<[RegExp, string]>} */
|
|
23
24
|
const SECRET_PATTERNS = [
|
|
24
25
|
[/Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, 'Bearer [REDACTED]'],
|
|
25
26
|
[/\b(?:sk|ghp|gho|ghu|ghs|ghr|github_pat|xox[baprs])[-_][A-Za-z0-9._-]{8,}/g, '[REDACTED]'],
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// #755: health must probe the actual Ollama endpoint, not the fallback flag.
|
|
2
|
+
let cache = { base: '', at: 0, value: false }
|
|
3
|
+
|
|
4
|
+
export function clearOllamaHealthCache() {
|
|
5
|
+
cache = { base: '', at: 0, value: false }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function probeOllama(baseUrl, { fetchImpl = fetch, timeoutMs = 800, ttlMs = 10000, now = Date.now } = {}) {
|
|
9
|
+
if (!baseUrl) return false
|
|
10
|
+
const base = String(baseUrl).replace(/\/$/, '')
|
|
11
|
+
if (cache.base === base && (now() - cache.at) < ttlMs) return cache.value
|
|
12
|
+
let value = false
|
|
13
|
+
try {
|
|
14
|
+
const init = { method: 'GET' }
|
|
15
|
+
if (timeoutMs > 0 && typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
16
|
+
init.signal = AbortSignal.timeout(timeoutMs)
|
|
17
|
+
}
|
|
18
|
+
const res = await fetchImpl(base + '/api/tags', init)
|
|
19
|
+
value = Boolean(res && res.ok)
|
|
20
|
+
} catch {
|
|
21
|
+
value = false
|
|
22
|
+
}
|
|
23
|
+
cache = { base, at: now(), value }
|
|
24
|
+
return value
|
|
25
|
+
}
|
package/lib/routes/oauth.js
CHANGED
|
@@ -292,10 +292,27 @@ export function registerOauthRoutes(ctx, state) {
|
|
|
292
292
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
293
293
|
return
|
|
294
294
|
}
|
|
295
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
296
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin or loopback only' } })
|
|
297
|
+
return
|
|
298
|
+
}
|
|
295
299
|
const q = queryOf(req)
|
|
296
300
|
const provider = q.get('provider') || ''
|
|
297
301
|
const redirectUri = q.get('redirectUri') || ''
|
|
298
302
|
const authUrl = q.get('authUrl') || ''
|
|
303
|
+
for (const [name, value] of [['redirectUri', redirectUri], ['authUrl', authUrl]]) {
|
|
304
|
+
if (!value) continue
|
|
305
|
+
try {
|
|
306
|
+
const parsed = new URL(value)
|
|
307
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
308
|
+
writeJson(res, 400, { ok: false, error: { code: 'url', message: name + ' must use http(s)' } })
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
writeJson(res, 400, { ok: false, error: { code: 'url', message: name + ' is invalid' } })
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
}
|
|
299
316
|
const host = (req.headers && req.headers.host) || ''
|
|
300
317
|
const user = process.env.USER || process.env.USERNAME || 'user'
|
|
301
318
|
const info = getHeadlessAuthInfo({ provider, redirectUri, authUrl, host, user })
|
package/lib/routes/sources.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { isTrustedSettingsRequest, safeJsonHandler, writeJson } from '../http.js'
|
|
2
2
|
import { buildSourceChain, readKeyRotationStatus, subscriptionCandidate } from '../key-rotation-bridge.js'
|
|
3
3
|
|
|
4
|
-
export async function collectSourceChain(state, port) {
|
|
4
|
+
export async function collectSourceChain(state, port, { accounts } = {}) {
|
|
5
5
|
const { accountsView, live } = state
|
|
6
6
|
let subscriptions = []
|
|
7
7
|
try {
|
|
8
|
-
const rows = await accountsView()
|
|
8
|
+
const rows = accounts || await accountsView()
|
|
9
9
|
subscriptions = (rows || []).map(subscriptionCandidate)
|
|
10
10
|
} catch { /* keep the chain available */ }
|
|
11
11
|
const keyRotation = port
|
package/lib/routes/status.js
CHANGED
|
@@ -27,6 +27,7 @@ import { PROVIDERS, displayName, droppedCredentialRefs, isProvider, oauthRef } f
|
|
|
27
27
|
import { getVendor } from '../vendors/index.js'
|
|
28
28
|
import { defaultErrorBuffer } from '../error-buffer.js'
|
|
29
29
|
import { assessHealth } from '../health-report.js'
|
|
30
|
+
import { probeOllama } from '../ollama-health.js'
|
|
30
31
|
import { defaultSnapshotStore } from '../vault-snapshots.js'
|
|
31
32
|
import { collectSourceChain } from './sources.js'
|
|
32
33
|
|
|
@@ -85,14 +86,15 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
85
86
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
86
87
|
return
|
|
87
88
|
}
|
|
88
|
-
const
|
|
89
|
-
const
|
|
89
|
+
const logged = typeof store.loggedInProviders === 'function' ? await store.loggedInProviders().catch(() => []) : []
|
|
90
|
+
const accounts = logged.map((provider) => ({ provider, configured: true }))
|
|
91
|
+
const sources = await collectSourceChain(state, req.socket && req.socket.localPort, { accounts })
|
|
90
92
|
const health = assessHealth({
|
|
91
93
|
accounts,
|
|
92
94
|
sources,
|
|
93
95
|
snapshots: defaultSnapshotStore.stats(),
|
|
94
96
|
webhookUrl: live().webhookUrl,
|
|
95
|
-
ollama: live().
|
|
97
|
+
ollama: await probeOllama(live().ollamaBaseUrl),
|
|
96
98
|
})
|
|
97
99
|
writeJson(res, 200, { ok: true, status: health.status, version: pkgVersion || 'unknown', checks: health.checks })
|
|
98
100
|
}),
|
package/lib/routes/vault.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Config, plainConfig, revisionOf } from '../config-schema.js'
|
|
2
2
|
import { isTrustedSettingsRequest, readBody, safeJsonHandler, writeJson } from '../http.js'
|
|
3
|
-
import { capturePrePullSnapshot,
|
|
3
|
+
import { capturePrePullSnapshot, restoreSnapshotWithConfig, resolveVaultKey } from '../vault-sync.js'
|
|
4
4
|
import { defaultSnapshotStore } from '../vault-snapshots.js'
|
|
5
5
|
|
|
6
6
|
export function registerVaultRoutes(ctx, state) {
|
|
@@ -33,7 +33,11 @@ export function registerVaultRoutes(ctx, state) {
|
|
|
33
33
|
return
|
|
34
34
|
}
|
|
35
35
|
try {
|
|
36
|
-
const
|
|
36
|
+
const key = resolveVaultKey(live(), passphrase)
|
|
37
|
+
const snapshot = capturePrePullSnapshot(live().slots || [], key.passphrase, String(body.label || 'manual'), {
|
|
38
|
+
keyMode: key.keyMode,
|
|
39
|
+
deviceId: key.deviceId,
|
|
40
|
+
})
|
|
37
41
|
writeJson(res, 200, { ok: true, snapshot })
|
|
38
42
|
} catch (err) {
|
|
39
43
|
writeJson(res, 400, { ok: false, error: { code: 'vault', message: String(err && err.message || err) } })
|
|
@@ -74,7 +78,7 @@ export function registerVaultRoutes(ctx, state) {
|
|
|
74
78
|
return
|
|
75
79
|
}
|
|
76
80
|
try {
|
|
77
|
-
const restored =
|
|
81
|
+
const restored = restoreSnapshotWithConfig(snapshot, live())
|
|
78
82
|
const cfg = live()
|
|
79
83
|
const slots = stripLegacySlots(restored.slots || [])
|
|
80
84
|
await api.replace(plainConfig(Config({ ...cfg, slots })), revisionOf(api))
|
package/lib/vault-snapshots.js
CHANGED
package/lib/vault-sync.js
CHANGED
|
@@ -269,14 +269,44 @@ export function mergeVaultSlots(localSlots, remoteSlots) {
|
|
|
269
269
|
return { slots: out, conflicts }
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
export function createVaultSnapshot({ slots, blobs = {} }, passphrase, label = '') {
|
|
272
|
+
export function createVaultSnapshot({ slots, blobs = {} }, passphrase, label = '', meta = {}) {
|
|
273
273
|
if (!passphrase) throw new Error('passphrase is required for a vault snapshot')
|
|
274
274
|
const { vault, slotCount, blobCount } = exportVault({ slots: slots || [], blobs }, passphrase)
|
|
275
|
-
return { version: 2, label: String(label || ''), createdAt: new Date().toISOString(), vault, slotCount, blobCount }
|
|
275
|
+
return { version: 2, label: String(label || ''), createdAt: new Date().toISOString(), vault, slotCount, blobCount, ...meta }
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
export function capturePrePullSnapshot(slots, passphrase, label = 'pre-pull') {
|
|
279
|
-
return defaultSnapshotStore.add(createVaultSnapshot({ slots: slots || [], blobs: {} }, passphrase, label))
|
|
278
|
+
export function capturePrePullSnapshot(slots, passphrase, label = 'pre-pull', meta = {}) {
|
|
279
|
+
return defaultSnapshotStore.add(createVaultSnapshot({ slots: slots || [], blobs: {} }, passphrase, label, meta))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function resolveVaultKey(cfg, passphrase) {
|
|
283
|
+
const perDevice = cfg && cfg.vaultPerDeviceKeys !== false
|
|
284
|
+
const device = resolveDeviceIdentity({ id: cfg && cfg.vaultDeviceId, label: cfg && cfg.vaultDeviceLabel })
|
|
285
|
+
return {
|
|
286
|
+
passphrase: perDevice ? devicePassphrase(passphrase, device.id) : passphrase,
|
|
287
|
+
keyMode: perDevice ? 'device' : 'base',
|
|
288
|
+
deviceId: perDevice ? device.id : '',
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function restoreSnapshotWithConfig(snapshot, cfg) {
|
|
293
|
+
const base = String((cfg && cfg.vaultSyncPassphrase) || '').trim()
|
|
294
|
+
if (!base) throw new Error('vaultSyncPassphrase is required to restore a snapshot')
|
|
295
|
+
const attempts = []
|
|
296
|
+
if (snapshot && snapshot.keyMode === 'device') {
|
|
297
|
+
attempts.push(devicePassphrase(base, snapshot.deviceId || resolveVaultKey(cfg, base).deviceId))
|
|
298
|
+
} else if (snapshot && snapshot.keyMode === 'base') {
|
|
299
|
+
attempts.push(base)
|
|
300
|
+
} else {
|
|
301
|
+
// Snapshots written before #749/#754 have no keyMode. Try the legacy
|
|
302
|
+
// base key first, then the current device key.
|
|
303
|
+
attempts.push(base, resolveVaultKey(cfg, base).passphrase)
|
|
304
|
+
}
|
|
305
|
+
let lastError = null
|
|
306
|
+
for (const pass of attempts) {
|
|
307
|
+
try { return restoreVaultSnapshot(snapshot, pass) } catch (err) { lastError = err }
|
|
308
|
+
}
|
|
309
|
+
throw lastError || new Error('unable to restore snapshot')
|
|
280
310
|
}
|
|
281
311
|
|
|
282
312
|
export function restoreVaultSnapshot(snapshot, passphrase) {
|
|
@@ -311,7 +341,7 @@ export async function syncVaultPush({ live, store, fetchFn = fetch }) {
|
|
|
311
341
|
|
|
312
342
|
const device = resolveDeviceIdentity({ id: cfg.vaultDeviceId, label: cfg.vaultDeviceLabel })
|
|
313
343
|
const perDevice = cfg.vaultPerDeviceKeys !== false
|
|
314
|
-
const activePass =
|
|
344
|
+
const activePass = resolveVaultKey(cfg, passphrase).passphrase
|
|
315
345
|
const { vault, slotCount, blobCount } = exportVault({ slots, blobs }, activePass)
|
|
316
346
|
const backend = cfg.vaultSyncBackend || 'webdav'
|
|
317
347
|
|
|
@@ -384,6 +414,7 @@ export async function syncVaultPull({ live, store, settingsApi, syncAdapter, ref
|
|
|
384
414
|
const device = resolveDeviceIdentity({ id: cfg.vaultDeviceId, label: cfg.vaultDeviceLabel })
|
|
385
415
|
const perDevice = cfg.vaultPerDeviceKeys !== false
|
|
386
416
|
let activePass = perDevice ? devicePassphrase(passphrase, device.id) : passphrase
|
|
417
|
+
let snapshotKeyMode = perDevice ? 'device' : 'base'
|
|
387
418
|
let usedLegacy = false
|
|
388
419
|
const load = async (deviceMode) => {
|
|
389
420
|
if (backend === 'webdav') {
|
|
@@ -423,10 +454,16 @@ export async function syncVaultPull({ live, store, settingsApi, syncAdapter, ref
|
|
|
423
454
|
if (!perDevice) throw err
|
|
424
455
|
encryptedData = await load(false)
|
|
425
456
|
activePass = passphrase
|
|
457
|
+
snapshotKeyMode = 'base'
|
|
426
458
|
usedLegacy = true
|
|
427
459
|
}
|
|
428
460
|
|
|
429
|
-
try {
|
|
461
|
+
try {
|
|
462
|
+
capturePrePullSnapshot(cfg.slots || [], activePass, 'pre-pull', {
|
|
463
|
+
keyMode: snapshotKeyMode,
|
|
464
|
+
deviceId: snapshotKeyMode === 'device' ? device.id : '',
|
|
465
|
+
})
|
|
466
|
+
} catch { /* snapshot is best-effort */ }
|
|
430
467
|
const result = importVault(encryptedData, activePass)
|
|
431
468
|
const providerFilter = new Set((cfg.vaultSyncProviders || []).map((p) => String(p)))
|
|
432
469
|
const remoteSlots = providerFilter.size
|
package/lib/vendors/grok.js
CHANGED
|
@@ -132,6 +132,7 @@ function streamHeaders(blob, config, extra) {
|
|
|
132
132
|
hostname = ''
|
|
133
133
|
}
|
|
134
134
|
if (!isTrustedAuthHost(hostname)) {
|
|
135
|
+
/** @type {Error & { code?: string }} */
|
|
135
136
|
const err = new Error(`Untrusted Grok baseUrl host: ${hostname || base}`)
|
|
136
137
|
err.code = 'GROK_UNTRUSTED_HOST'
|
|
137
138
|
throw err
|
package/lib/vendors/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.13",
|
|
4
4
|
"description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"url": "https://github.com/GooDAnDReaDY/dsh-subscriptions/issues"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
-
"test": "eslint lib/ test/ && node --import ./test/_setup.mjs --test test/*.test.mjs",
|
|
42
|
+
"test": "eslint lib/ test/ && npm run typecheck && node --import ./test/_setup.mjs --test test/*.test.mjs",
|
|
43
43
|
"typecheck": "tsc -p tsconfig.checkjs.json"
|
|
44
44
|
},
|
|
45
45
|
"dsh": {
|