dsh-ide-git 0.21.0 → 0.21.1

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/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-ide-git",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "main": "./src/index.js",
5
5
  "description": "IDE 级 Git 操作面板:分支树 + 提交图谱 + 变更/暂存/提交 + 提交详情。装了 dsh-better-sidebar 时挂成它的原生 tab(右侧栏 + 底部工作台);没装时直接挂 DSH 原生右侧栏,单独安装即可用。JetBrains 风格 Git 工具窗口 for DeepSeek Harness.",
6
6
  "engines": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ide-git",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "description": "DSH plugin: an IDE-grade Git tool window as a native dsh-better-sidebar tab — branch tree, commit graph, changes, commit details, JetBrains-style actions, in the right sidebar and the bottom panel. | DSH 插件:IDE 级 Git 工具窗口,以 dsh-better-sidebar 原生 Tab 挂载——分支树 / 提交图谱 / 变更与提交详情 / JetBrains 风格操作,右侧栏与底部面板双布局自适应。",
5
5
  "type": "module",
6
6
  "repository": {
package/src/client.js CHANGED
@@ -7425,6 +7425,19 @@ window.__ModuleLoader__.load({
7425
7425
  function isAuthRejection(status) {
7426
7426
  return status === 401 || status === 403
7427
7427
  }
7428
+
7429
+ /* Whether a proxy answer looks like OUR route (a JSON object). With no
7430
+ remote-web-ui installed the SPA static fallback serves /remote/<path> as
7431
+ '405 Method Not Allowed' with an EMPTY body (issue #20: on a plain-HTTP
7432
+ LAN page the panel surfaced a bare 'HTTP 405' instead of the real
7433
+ 403 refusal). A non-JSON retry therefore proves there is no proxy, and
7434
+ the original answer must be what the caller sees. Cloning leaves the
7435
+ caller's own body untouched. */
7436
+ async function answersJson(response) {
7437
+ let body = null
7438
+ try { body = await response.clone().json() } catch (error) { return false }
7439
+ return body !== null && typeof body === 'object'
7440
+ }
7428
7441
  /* ---- end remote channel fallback core ---- */
7429
7442
 
7430
7443
  /* Sticky remote-channel mode: once the proxy has answered, later calls go
@@ -7446,13 +7459,15 @@ window.__ModuleLoader__.load({
7446
7459
  } else {
7447
7460
  response = await fetch(direct, init)
7448
7461
  if (isAuthRejection(response.status) === true) {
7449
- /* Off the origin's trust path: try the pairing proxy once. With no
7450
- remote-web-ui installed the proxy path 404s — the retry then also
7451
- fails, and the ORIGINAL rejection is what the caller sees (a
7452
- proxy answer that is itself an auth rejection is discarded the
7453
- same way, so a genuine cross-site refusal can never be masked). */
7462
+ /* Off the origin's trust path: try the pairing proxy once. It has to
7463
+ answer like our route (a JSON body) to take over: with no
7464
+ remote-web-ui installed the SPA fallback serves the retry as 405
7465
+ with an empty body, and that non-answer must not outrank the
7466
+ ORIGINAL rejection (issue #20). A proxy answer that is itself an
7467
+ auth rejection is discarded the same way, so a genuine refusal can
7468
+ never be masked. */
7454
7469
  const retried = await fetch(remoteFallbackUrl(direct), init)
7455
- if (isAuthRejection(retried.status) !== true) {
7470
+ if (isAuthRejection(retried.status) !== true && (await answersJson(retried)) === true) {
7456
7471
  viaRemoteChannel = true
7457
7472
  response = retried
7458
7473
  }
package/src/index.js CHANGED
@@ -2086,6 +2086,61 @@ function headerOf(req, name) {
2086
2086
  return value
2087
2087
  }
2088
2088
 
2089
+ /* Normalize an authority ("host[:port]" or an absolute URL) to
2090
+ "hostname:port", filling the scheme default port when it is implicit.
2091
+ Returns '' for anything unparseable (including the "null" opaque origin). */
2092
+ function authorityKey(value, protocol) {
2093
+ const text = String(value === undefined || value === null ? '' : value).trim()
2094
+ if (text === '') return ''
2095
+ let hostname = text
2096
+ let port = ''
2097
+ let scheme = protocol
2098
+ if (text.includes('://') === true) {
2099
+ let parsed = null
2100
+ try { parsed = new URL(text) } catch (error) { parsed = null }
2101
+ if (parsed === null) return ''
2102
+ hostname = parsed.hostname
2103
+ port = parsed.port
2104
+ scheme = parsed.protocol
2105
+ } else {
2106
+ const bracketed = /^\[([^\]]+)\](?::(\d+))?$/.exec(text)
2107
+ if (bracketed !== null) {
2108
+ hostname = bracketed[1]
2109
+ port = bracketed[2] === undefined ? '' : bracketed[2]
2110
+ } else {
2111
+ const plain = /^([^:]+)(?::(\d+))?$/.exec(text)
2112
+ if (plain === null) return ''
2113
+ hostname = plain[1]
2114
+ port = plain[2] === undefined ? '' : plain[2]
2115
+ }
2116
+ }
2117
+ if (hostname === '') return ''
2118
+ if (port === '') port = scheme === 'https:' ? '443' : '80'
2119
+ return hostname.toLowerCase() + ':' + port
2120
+ }
2121
+
2122
+ /* Same-origin fallback for browsers that send no Sec-Fetch-* headers. A page
2123
+ served over plain HTTP from a non-loopback address is not a secure context,
2124
+ and the fetch-metadata headers are omitted there (issue #20: the git panel
2125
+ answered 403 on a LAN address). Origin and Referer are forbidden headers a
2126
+ page cannot forge — a cross-site request always carries the caller's own
2127
+ origin — so comparing either against the Host header restores the
2128
+ same-origin fence without trusting anything the request body claims. */
2129
+ function sameOriginOf(req, host) {
2130
+ const hostKey = authorityKey(host, 'http:')
2131
+ if (hostKey === '') return false
2132
+ /* A Host header without a port says nothing about the scheme, so either
2133
+ scheme default may be the one the browser used in Origin. */
2134
+ const candidates = new Set([hostKey])
2135
+ if (/:\d+$/.test(String(host).trim()) !== true) candidates.add(authorityKey(host, 'https:'))
2136
+ const matches = (value) => {
2137
+ const key = authorityKey(value, 'http:')
2138
+ return key !== '' && candidates.has(key)
2139
+ }
2140
+ if (matches(headerOf(req, 'origin')) === true) return true
2141
+ return matches(headerOf(req, 'referer'))
2142
+ }
2143
+
2089
2144
  /** Loopback authority, or a same-origin browser request (desktop shell / remote UI). */
2090
2145
  function isTrusted(req) {
2091
2146
  const host = headerOf(req, 'host')
@@ -2094,7 +2149,8 @@ function isTrusted(req) {
2094
2149
  if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '0.0.0.0') return true
2095
2150
  if (/^127\./.test(hostname)) return true
2096
2151
  const site = headerOf(req, 'sec-fetch-site')
2097
- return site === 'same-origin' || site === 'same-site'
2152
+ if (site === 'same-origin' || site === 'same-site') return true
2153
+ return sameOriginOf(req, host)
2098
2154
  }
2099
2155
 
2100
2156
  function sendJson(res, status, body) {
@@ -2136,7 +2192,13 @@ function readJsonBody(req, limit = 1 << 20) {
2136
2192
 
2137
2193
  async function handle(req, res) {
2138
2194
  if (!isTrusted(req)) {
2139
- sendJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden' } })
2195
+ sendJson(res, 403, {
2196
+ ok: false,
2197
+ error: {
2198
+ code: 'forbidden',
2199
+ message: 'forbidden: a non-loopback request must be same-origin (open this host from its own page) or come through the remote pairing proxy',
2200
+ },
2201
+ })
2140
2202
  return
2141
2203
  }
2142
2204
  if (req.method !== 'POST') {