dsh-scnet 0.6.9 → 0.6.10
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/README.md +7 -9
- package/README_CN.md +6 -6
- package/connector/callback.mjs +155 -0
- package/connector/index.mjs +4 -0
- package/package.json +1 -1
- package/skills/scnet-hpc/VERSION +1 -1
package/README.md
CHANGED
|
@@ -4,17 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
DSH-SCNet (`dsh-scnet` on npm) is a community-maintained DSH bundle for operating Supercomputing Network (SCNet). It packages the source [`scnet-hpc`](https://github.com/lql341/scnet-hpc) Skill, profile-aware utilities, and bounded tools for SSH, Slurm, OpenAPI files, regional resources, and Notebook discovery.
|
|
6
6
|
|
|
7
|
-
Current release: **0.6.
|
|
7
|
+
Current release: **0.6.10**
|
|
8
8
|
|
|
9
9
|
<!-- scnet-release:start -->
|
|
10
|
-
## 0.6.
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
- First-use setup defaults to OpenAPI, retains existing backend preferences, and preserves
|
|
17
|
-
unrelated configuration fields when settings are updated.
|
|
10
|
+
## 0.6.10 highlights
|
|
11
|
+
|
|
12
|
+
- Coordinate the synchronized downstream DSH release with a protocol-neutral one-shot loopback
|
|
13
|
+
OAuth callback listener, fixed-path validation, timeout cleanup, and generic browser responses.
|
|
14
|
+
- Keep the canonical Skill endpoint-neutral; no SCNet issuer, endpoint, client ID, scope, or
|
|
15
|
+
credential is embedded in this release.
|
|
18
16
|
<!-- scnet-release:end -->
|
|
19
17
|
|
|
20
18
|
> This is an independent community project. It is compatible with DeepSeek Harness but is not an official DeepSeek product and does not imply endorsement, partnership, or authorization by DeepSeek.
|
package/README_CN.md
CHANGED
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
<p align="center">在 DeepSeek Harness 里操作超算互联网(SCNet):配置 SSH、管理 Slurm 作业、查询 OpenAPI 区域和文件、检查 Notebook 资源与实例,并保留海光 DCU/DTK 诊断能力。</p>
|
|
6
6
|
|
|
7
|
-
当前版本:**0.6.
|
|
7
|
+
当前版本:**0.6.10**
|
|
8
8
|
|
|
9
9
|
<!-- scnet-release:start -->
|
|
10
|
-
## 0.6.
|
|
10
|
+
## 0.6.10 更新
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
12
|
+
- 协调下游 DSH 发布协议无关的一次性 loopback OAuth 回调监听器,包含固定路径校验、
|
|
13
|
+
超时清理和不暴露回调细节的通用浏览器响应。
|
|
14
|
+
- canonical Skill 继续保持 endpoint 无关;本版本不嵌入 SCNet issuer、endpoint、client ID、
|
|
15
|
+
scope 或任何凭证。
|
|
16
16
|
<!-- scnet-release:end -->
|
|
17
17
|
|
|
18
18
|
> 本项目是独立维护的社区项目,与 DeepSeek Harness 兼容,但不是 DeepSeek 官方产品,也不表示 DeepSeek 对本项目提供背书、合作或授权。
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createServer } from 'node:http'
|
|
2
|
+
|
|
3
|
+
import { ConnectorError } from './errors.mjs'
|
|
4
|
+
|
|
5
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', '[::1]', 'localhost'])
|
|
6
|
+
const CALLBACK_TIMEOUT_MS = 120000
|
|
7
|
+
|
|
8
|
+
function requiredString(value, label) {
|
|
9
|
+
const normalized = String(value ?? '').trim()
|
|
10
|
+
if (!normalized) throw new TypeError(`${label} must be a non-empty string`)
|
|
11
|
+
return normalized
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseLoopbackRedirect(value) {
|
|
15
|
+
let redirect
|
|
16
|
+
try {
|
|
17
|
+
redirect = new URL(requiredString(value, 'redirectUri'))
|
|
18
|
+
} catch {
|
|
19
|
+
throw new TypeError('redirectUri must be a valid URL')
|
|
20
|
+
}
|
|
21
|
+
if (redirect.protocol !== 'http:' || !LOOPBACK_HOSTS.has(redirect.hostname)) {
|
|
22
|
+
throw new TypeError('redirectUri must use an http loopback address')
|
|
23
|
+
}
|
|
24
|
+
if (!redirect.pathname || redirect.pathname === '/') {
|
|
25
|
+
throw new TypeError('redirectUri must include a callback path')
|
|
26
|
+
}
|
|
27
|
+
if (redirect.username || redirect.password || redirect.hash) {
|
|
28
|
+
throw new TypeError('redirectUri must not include credentials or a fragment')
|
|
29
|
+
}
|
|
30
|
+
return redirect
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function responseBody(res, statusCode, body) {
|
|
34
|
+
const content = Buffer.from(body, 'utf8')
|
|
35
|
+
res.writeHead(statusCode, {
|
|
36
|
+
'content-type': 'text/html; charset=utf-8',
|
|
37
|
+
'content-length': content.byteLength,
|
|
38
|
+
connection: 'close',
|
|
39
|
+
})
|
|
40
|
+
res.end(content)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Start a one-shot local OAuth callback listener.
|
|
45
|
+
*
|
|
46
|
+
* The listener is protocol-neutral: the caller owns the authorization session
|
|
47
|
+
* and provides its callback consumer. Browser responses never contain callback
|
|
48
|
+
* parameters or private error details.
|
|
49
|
+
*/
|
|
50
|
+
export async function listenLoopbackCallback({
|
|
51
|
+
redirectUri,
|
|
52
|
+
consumeCallback,
|
|
53
|
+
timeoutMs = CALLBACK_TIMEOUT_MS,
|
|
54
|
+
} = {}) {
|
|
55
|
+
const requested = parseLoopbackRedirect(redirectUri)
|
|
56
|
+
if (typeof consumeCallback !== 'function') {
|
|
57
|
+
throw new TypeError('consumeCallback must be a function')
|
|
58
|
+
}
|
|
59
|
+
const timeout = Number(timeoutMs)
|
|
60
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
61
|
+
throw new TypeError('timeoutMs must be a positive number')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let settled = false
|
|
65
|
+
let timer
|
|
66
|
+
let actualRedirect
|
|
67
|
+
let resolveResult
|
|
68
|
+
let rejectResult
|
|
69
|
+
const result = new Promise((resolve, reject) => {
|
|
70
|
+
resolveResult = resolve
|
|
71
|
+
rejectResult = reject
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const server = createServer((req, res) => {
|
|
75
|
+
if (settled) {
|
|
76
|
+
responseBody(res, 410, '<!doctype html><title>Callback closed</title>')
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
const callbackUrl = actualRedirect && new URL(req.url || '/', actualRedirect.origin)
|
|
80
|
+
if (req.method !== 'GET' || !callbackUrl || callbackUrl.pathname !== requested.pathname) {
|
|
81
|
+
responseBody(res, 404, '<!doctype html><title>Not found</title>')
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
settle(callbackUrl.href, res)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
function closeServer() {
|
|
88
|
+
if (timer) clearTimeout(timer)
|
|
89
|
+
if (server.listening) server.close()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function settle(callbackUrl, res) {
|
|
93
|
+
if (settled) return
|
|
94
|
+
settled = true
|
|
95
|
+
try {
|
|
96
|
+
const value = consumeCallback(callbackUrl)
|
|
97
|
+
responseBody(res, 200, '<!doctype html><title>Authorization complete</title>')
|
|
98
|
+
resolveResult(value)
|
|
99
|
+
} catch (error) {
|
|
100
|
+
responseBody(res, 400, '<!doctype html><title>Authorization failed</title>')
|
|
101
|
+
rejectResult(
|
|
102
|
+
error instanceof ConnectorError
|
|
103
|
+
? error
|
|
104
|
+
: new ConnectorError('CALLBACK_MISMATCH', error?.message || 'OAuth callback rejected', {
|
|
105
|
+
operation: 'login',
|
|
106
|
+
}),
|
|
107
|
+
)
|
|
108
|
+
} finally {
|
|
109
|
+
closeServer()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await new Promise((resolve, reject) => {
|
|
114
|
+
server.once('error', reject)
|
|
115
|
+
server.listen({
|
|
116
|
+
host: requested.hostname,
|
|
117
|
+
port: requested.port,
|
|
118
|
+
exclusive: true,
|
|
119
|
+
}, resolve)
|
|
120
|
+
}).catch((error) => {
|
|
121
|
+
closeServer()
|
|
122
|
+
throw new ConnectorError('CALLBACK_MISMATCH', 'Unable to open the local OAuth callback listener', {
|
|
123
|
+
operation: 'login',
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const address = server.address()
|
|
128
|
+
if (!address || typeof address === 'string') {
|
|
129
|
+
closeServer()
|
|
130
|
+
throw new ConnectorError('CALLBACK_MISMATCH', 'Local OAuth callback listener returned no address', {
|
|
131
|
+
operation: 'login',
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
actualRedirect = new URL(requested.href)
|
|
135
|
+
actualRedirect.port = String(address.port)
|
|
136
|
+
|
|
137
|
+
timer = setTimeout(() => {
|
|
138
|
+
if (settled) return
|
|
139
|
+
settled = true
|
|
140
|
+
rejectResult(new ConnectorError('CALLBACK_MISMATCH', 'OAuth callback timed out', { operation: 'login' }))
|
|
141
|
+
closeServer()
|
|
142
|
+
}, timeout)
|
|
143
|
+
timer.unref?.()
|
|
144
|
+
|
|
145
|
+
return Object.freeze({
|
|
146
|
+
redirectUri: actualRedirect.href,
|
|
147
|
+
waitForCallback: () => result,
|
|
148
|
+
close() {
|
|
149
|
+
if (settled) return
|
|
150
|
+
settled = true
|
|
151
|
+
rejectResult(new ConnectorError('CALLBACK_MISMATCH', 'OAuth callback listener closed', { operation: 'login' }))
|
|
152
|
+
closeServer()
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
}
|
package/connector/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-scnet",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.10",
|
|
4
4
|
"description": "Community DeepSeek Harness bundle for SCNet SSH and OpenAPI jobs, files, Notebook resources, Slurm diagnostics, and Hygon DCU/DTK guidance.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
package/skills/scnet-hpc/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.6.
|
|
1
|
+
0.6.10
|