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 +1 -1
- package/package.json +1 -1
- package/src/client.js +21 -6
- package/src/index.js +64 -2
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-ide-git",
|
|
3
|
-
"version": "0.21.
|
|
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.
|
|
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.
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
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
|
-
|
|
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, {
|
|
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') {
|