@shawnstack/quickforge 1.2.4 → 1.2.6
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 +6 -6
- package/dist/assets/{anthropic-BiTRBcug.js → anthropic-g5RJMS9O.js} +1 -1
- package/dist/assets/{azure-openai-responses-MuqXmHEI.js → azure-openai-responses-a7afzGwC.js} +1 -1
- package/dist/assets/{google-Bp1Z3Se-.js → google-DZsPNex3.js} +1 -1
- package/dist/assets/{google-gemini-cli-6iC3v7Mu.js → google-gemini-cli-DnAdZ-9e.js} +1 -1
- package/dist/assets/{google-vertex-DzmUuLVp.js → google-vertex-BDuS0bO0.js} +1 -1
- package/dist/assets/{icons-BzvJv-Bv.js → icons-DjhMV6OE.js} +1 -1
- package/dist/assets/index-B094j8RZ.css +3 -0
- package/dist/assets/{index-VdWqU8e1.js → index-CC71Wiy2.js} +660 -487
- package/dist/assets/{mistral-BSlX93lo.js → mistral-CuUpINR3.js} +1 -1
- package/dist/assets/{openai-codex-responses-CvN5Iwy4.js → openai-codex-responses-DchchAd8.js} +1 -1
- package/dist/assets/{openai-completions-CteXgyGA.js → openai-completions-CV15qkLX.js} +1 -1
- package/dist/assets/{openai-responses-Chg1ZGwU.js → openai-responses-BYlHDVWf.js} +1 -1
- package/dist/assets/{openai-responses-shared-C8jnJ317.js → openai-responses-shared-CIztTfIF.js} +1 -1
- package/dist/assets/{react-vendor-CdZo8gqc.js → react-vendor-BK8yG_FK.js} +1 -1
- package/dist/index.html +4 -4
- package/node_modules/protobufjs/dist/light/protobuf.js +36 -12
- package/node_modules/protobufjs/dist/light/protobuf.js.map +1 -1
- package/node_modules/protobufjs/dist/light/protobuf.min.js +3 -3
- package/node_modules/protobufjs/dist/light/protobuf.min.js.map +1 -1
- package/node_modules/protobufjs/dist/minimal/protobuf.js +2 -2
- package/node_modules/protobufjs/dist/minimal/protobuf.min.js +2 -2
- package/node_modules/protobufjs/dist/protobuf.js +71 -42
- package/node_modules/protobufjs/dist/protobuf.js.map +1 -1
- package/node_modules/protobufjs/dist/protobuf.min.js +3 -3
- package/node_modules/protobufjs/dist/protobuf.min.js.map +1 -1
- package/node_modules/protobufjs/index.d.ts +18 -5
- package/node_modules/protobufjs/package.json +1 -1
- package/node_modules/protobufjs/src/namespace.js +8 -4
- package/node_modules/protobufjs/src/parse.js +35 -30
- package/node_modules/protobufjs/src/root.js +4 -2
- package/node_modules/protobufjs/src/service.js +4 -2
- package/node_modules/protobufjs/src/type.js +4 -2
- package/node_modules/protobufjs/src/util.js +14 -0
- package/node_modules/ws/lib/sender.js +6 -1
- package/node_modules/ws/package.json +1 -1
- package/package.json +1 -1
- package/server/agent-manager.mjs +49 -6
- package/server/index.mjs +49 -4
- package/server/lan-access-store.mjs +215 -0
- package/server/routes/backup.mjs +184 -39
- package/server/routes/lan-access.mjs +201 -0
- package/server/share-store.mjs +7 -29
- package/server/system-prompt.mjs +2 -0
- package/server/tools/index.mjs +39 -13
- package/server/utils/password-auth.mjs +44 -0
- package/dist/assets/index-B9f6WrD6.css +0 -3
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { sendJson, readJsonBody } from '../utils/response.mjs'
|
|
2
|
+
import { getLanUrls } from '../utils/network.mjs'
|
|
3
|
+
import { logger } from '../utils/logger.mjs'
|
|
4
|
+
import {
|
|
5
|
+
issueLanAccessToken,
|
|
6
|
+
lanAccessCookieName,
|
|
7
|
+
readLanAccessStatus,
|
|
8
|
+
revokeLanAccessTokens,
|
|
9
|
+
updateLanAccessSettings,
|
|
10
|
+
} from '../lan-access-store.mjs'
|
|
11
|
+
|
|
12
|
+
const MAX_FAILED_ATTEMPTS = 5
|
|
13
|
+
const ATTEMPT_WINDOW_MS = 5 * 60 * 1000
|
|
14
|
+
const LOCK_MS = 5 * 60 * 1000
|
|
15
|
+
const attempts = new Map()
|
|
16
|
+
|
|
17
|
+
function remoteKey(req) {
|
|
18
|
+
return String(req.socket.remoteAddress || 'unknown')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function attemptState(req) {
|
|
22
|
+
const key = remoteKey(req)
|
|
23
|
+
const now = Date.now()
|
|
24
|
+
const state = attempts.get(key)
|
|
25
|
+
if (!state || state.resetAt <= now) {
|
|
26
|
+
const fresh = { count: 0, resetAt: now + ATTEMPT_WINDOW_MS, lockedUntil: 0 }
|
|
27
|
+
attempts.set(key, fresh)
|
|
28
|
+
return fresh
|
|
29
|
+
}
|
|
30
|
+
return state
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function assertNotLocked(req) {
|
|
34
|
+
const state = attemptState(req)
|
|
35
|
+
if (state.lockedUntil > Date.now()) {
|
|
36
|
+
const error = new Error('Too many failed attempts. Please try again later.')
|
|
37
|
+
error.statusCode = 429
|
|
38
|
+
throw error
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function recordFailure(req) {
|
|
43
|
+
const state = attemptState(req)
|
|
44
|
+
state.count += 1
|
|
45
|
+
if (state.count >= MAX_FAILED_ATTEMPTS) {
|
|
46
|
+
state.lockedUntil = Date.now() + LOCK_MS
|
|
47
|
+
state.count = 0
|
|
48
|
+
state.resetAt = Date.now() + ATTEMPT_WINDOW_MS
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function clearFailures(req) {
|
|
53
|
+
attempts.delete(remoteKey(req))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function setLanCookie(res, token, maxAge) {
|
|
57
|
+
const cookie = [
|
|
58
|
+
`${lanAccessCookieName()}=${encodeURIComponent(token)}`,
|
|
59
|
+
'HttpOnly',
|
|
60
|
+
'SameSite=Lax',
|
|
61
|
+
`Max-Age=${Math.max(1, Number(maxAge) || 1)}`,
|
|
62
|
+
'Path=/',
|
|
63
|
+
].join('; ')
|
|
64
|
+
res.setHeader('Set-Cookie', cookie)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function clearLanCookie(res) {
|
|
68
|
+
res.setHeader('Set-Cookie', `${lanAccessCookieName()}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function requireLocal(context) {
|
|
72
|
+
if (!context.isLocalRequest) {
|
|
73
|
+
const error = new Error('LAN access settings can only be changed from this machine.')
|
|
74
|
+
error.statusCode = 403
|
|
75
|
+
throw error
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function renderLanUnlockPage(res) {
|
|
80
|
+
const html = `<!doctype html>
|
|
81
|
+
<html lang="zh-CN">
|
|
82
|
+
<head>
|
|
83
|
+
<meta charset="utf-8" />
|
|
84
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
85
|
+
<title>QuickForge 局域网访问</title>
|
|
86
|
+
<style>
|
|
87
|
+
:root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
88
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #0f172a; color: #e5e7eb; }
|
|
89
|
+
main { width: min(420px, calc(100vw - 32px)); border: 1px solid rgba(148,163,184,.3); border-radius: 20px; padding: 28px; background: rgba(15,23,42,.92); box-shadow: 0 24px 80px rgba(0,0,0,.35); }
|
|
90
|
+
h1 { margin: 0 0 8px; font-size: 22px; }
|
|
91
|
+
p { margin: 0 0 18px; color: #94a3b8; line-height: 1.6; }
|
|
92
|
+
label { display: block; margin-bottom: 8px; font-size: 14px; font-weight: 600; }
|
|
93
|
+
input { width: 100%; box-sizing: border-box; height: 42px; border-radius: 10px; border: 1px solid #334155; background: #020617; color: #f8fafc; padding: 0 12px; font-size: 15px; }
|
|
94
|
+
button { width: 100%; height: 42px; margin-top: 14px; border: 0; border-radius: 10px; background: #2563eb; color: white; font-weight: 700; cursor: pointer; }
|
|
95
|
+
button:disabled { opacity: .6; cursor: default; }
|
|
96
|
+
.error { min-height: 20px; margin-top: 12px; color: #fca5a5; font-size: 13px; }
|
|
97
|
+
</style>
|
|
98
|
+
</head>
|
|
99
|
+
<body>
|
|
100
|
+
<main>
|
|
101
|
+
<h1>QuickForge 局域网访问</h1>
|
|
102
|
+
<p>请输入本机设置中配置的局域网访问密码。</p>
|
|
103
|
+
<label for="password">访问密码</label>
|
|
104
|
+
<input id="password" type="password" autocomplete="current-password" autofocus />
|
|
105
|
+
<button id="submit" type="button">进入 QuickForge</button>
|
|
106
|
+
<div id="error" class="error" role="alert"></div>
|
|
107
|
+
</main>
|
|
108
|
+
<script>
|
|
109
|
+
const password = document.getElementById('password')
|
|
110
|
+
const button = document.getElementById('submit')
|
|
111
|
+
const error = document.getElementById('error')
|
|
112
|
+
async function unlock() {
|
|
113
|
+
error.textContent = ''
|
|
114
|
+
button.disabled = true
|
|
115
|
+
try {
|
|
116
|
+
const response = await fetch('/api/lan-access/unlock', {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: { 'content-type': 'application/json' },
|
|
119
|
+
body: JSON.stringify({ password: password.value })
|
|
120
|
+
})
|
|
121
|
+
const payload = await response.json().catch(() => null)
|
|
122
|
+
if (!response.ok) throw new Error(payload && payload.error ? payload.error : '密码错误')
|
|
123
|
+
window.location.reload()
|
|
124
|
+
} catch (err) {
|
|
125
|
+
error.textContent = err instanceof Error ? err.message : '密码错误'
|
|
126
|
+
} finally {
|
|
127
|
+
button.disabled = false
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
button.addEventListener('click', unlock)
|
|
131
|
+
password.addEventListener('keydown', (event) => { if (event.key === 'Enter') unlock() })
|
|
132
|
+
</script>
|
|
133
|
+
</body>
|
|
134
|
+
</html>`
|
|
135
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' })
|
|
136
|
+
res.end(html)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function handleLanAccessApi(req, res, url, context = {}) {
|
|
140
|
+
const pathname = url.pathname
|
|
141
|
+
|
|
142
|
+
if (req.method === 'GET' && pathname === '/api/lan-access/status') {
|
|
143
|
+
const status = await readLanAccessStatus()
|
|
144
|
+
if (context.isLocalRequest) {
|
|
145
|
+
sendJson(res, 200, { ...status, lanUrls: getLanUrls(context.port) })
|
|
146
|
+
} else {
|
|
147
|
+
sendJson(res, 200, { enabled: status.enabled, requiresPassword: status.enabled && status.hasPassword })
|
|
148
|
+
}
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (req.method === 'PUT' && pathname === '/api/lan-access/settings') {
|
|
153
|
+
requireLocal(context)
|
|
154
|
+
const body = await readJsonBody(req)
|
|
155
|
+
const status = await updateLanAccessSettings({
|
|
156
|
+
enabled: Boolean(body?.enabled),
|
|
157
|
+
password: typeof body?.password === 'string' ? body.password : undefined,
|
|
158
|
+
sessionTtlHours: body?.sessionTtlHours,
|
|
159
|
+
})
|
|
160
|
+
logger.info('LAN access settings updated.', { enabled: status.enabled })
|
|
161
|
+
sendJson(res, 200, { ok: true, ...status, lanUrls: getLanUrls(context.port) })
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (req.method === 'POST' && pathname === '/api/lan-access/unlock') {
|
|
166
|
+
assertNotLocked(req)
|
|
167
|
+
const body = await readJsonBody(req, 1024)
|
|
168
|
+
try {
|
|
169
|
+
const result = await issueLanAccessToken(body?.password)
|
|
170
|
+
setLanCookie(res, result.token, result.maxAge)
|
|
171
|
+
clearFailures(req)
|
|
172
|
+
logger.info('LAN access unlock succeeded.', { remoteAddress: req.socket.remoteAddress })
|
|
173
|
+
sendJson(res, 200, { ok: true, expiresAt: result.expiresAt })
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (error?.statusCode === 401) {
|
|
176
|
+
recordFailure(req)
|
|
177
|
+
logger.warn('LAN access unlock failed.', { remoteAddress: req.socket.remoteAddress })
|
|
178
|
+
}
|
|
179
|
+
throw error
|
|
180
|
+
}
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (req.method === 'POST' && pathname === '/api/lan-access/logout') {
|
|
185
|
+
clearLanCookie(res)
|
|
186
|
+
sendJson(res, 200, { ok: true })
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (req.method === 'POST' && pathname === '/api/lan-access/revoke-all') {
|
|
191
|
+
requireLocal(context)
|
|
192
|
+
const status = await revokeLanAccessTokens()
|
|
193
|
+
logger.info('LAN access tokens revoked.')
|
|
194
|
+
sendJson(res, 200, { ok: true, ...status })
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const error = new Error('Not found')
|
|
199
|
+
error.statusCode = 404
|
|
200
|
+
throw error
|
|
201
|
+
}
|
package/server/share-store.mjs
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomBytes } from 'node:crypto'
|
|
2
2
|
import { promises as fs } from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
-
import { promisify } from 'node:util'
|
|
5
4
|
import { ensureStorage, storageDir } from './storage.mjs'
|
|
6
|
-
|
|
7
|
-
const scrypt = promisify(scryptCallback)
|
|
5
|
+
import { hashPassword, safeHashEqual, sha256Base64Url, verifyPassword } from './utils/password-auth.mjs'
|
|
8
6
|
const SHARE_ID_PREFIX = 'qfs_'
|
|
9
|
-
const PASSWORD_VERSION = 1
|
|
10
7
|
const SHARE_TOKEN_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000
|
|
11
8
|
const MAX_SHARE_TOKENS = 50
|
|
12
9
|
const SHARES_DIR = path.join(storageDir, 'shares')
|
|
@@ -98,32 +95,21 @@ export function generateSharePassword() {
|
|
|
98
95
|
return `${randomToken(4).slice(0, 6).toUpperCase()}-${randomToken(4).slice(0, 6).toUpperCase()}`
|
|
99
96
|
}
|
|
100
97
|
|
|
101
|
-
export async function hashSharePassword(password, salt
|
|
98
|
+
export async function hashSharePassword(password, salt) {
|
|
102
99
|
if (password === undefined || password === null || typeof password !== 'string') return {}
|
|
103
100
|
if (!password) return { passwordHash: undefined, passwordSalt: undefined, passwordVersion: undefined }
|
|
104
|
-
|
|
105
|
-
const derived = await scrypt(password, salt, 32)
|
|
106
|
-
return {
|
|
107
|
-
passwordHash: derived.toString('base64url'),
|
|
108
|
-
passwordSalt: salt,
|
|
109
|
-
passwordVersion: PASSWORD_VERSION,
|
|
110
|
-
}
|
|
101
|
+
return hashPassword(password, salt)
|
|
111
102
|
}
|
|
112
103
|
|
|
113
104
|
export async function verifySharePassword(record, password) {
|
|
114
105
|
if (!record?.passwordHash || !record?.passwordSalt) return !password
|
|
115
|
-
|
|
116
|
-
const { passwordHash } = await hashSharePassword(password, record.passwordSalt)
|
|
117
|
-
const expected = Buffer.from(record.passwordHash, 'base64url')
|
|
118
|
-
const actual = Buffer.from(passwordHash, 'base64url')
|
|
119
|
-
if (expected.length !== actual.length) return false
|
|
120
|
-
return timingSafeEqual(expected, actual)
|
|
106
|
+
return verifyPassword(record, password)
|
|
121
107
|
}
|
|
122
108
|
|
|
123
109
|
export function createShareToken(shareId) {
|
|
124
110
|
assertSafeShareId(shareId)
|
|
125
111
|
const secret = randomToken(32)
|
|
126
|
-
const secretHash =
|
|
112
|
+
const secretHash = sha256Base64Url(secret)
|
|
127
113
|
return {
|
|
128
114
|
token: `${shareId}.${secret}`,
|
|
129
115
|
tokenHash: secretHash,
|
|
@@ -140,19 +126,11 @@ function pruneShareTokens(tokens, now = Date.now()) {
|
|
|
140
126
|
.slice(-MAX_SHARE_TOKENS)
|
|
141
127
|
}
|
|
142
128
|
|
|
143
|
-
function safeHashEqual(expectedHash, actualHash) {
|
|
144
|
-
if (!expectedHash || !actualHash) return false
|
|
145
|
-
const expected = Buffer.from(expectedHash)
|
|
146
|
-
const actual = Buffer.from(actualHash)
|
|
147
|
-
if (expected.length !== actual.length) return false
|
|
148
|
-
return timingSafeEqual(expected, actual)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
129
|
export function verifyShareToken(record, token) {
|
|
152
130
|
if (!record || !token || typeof token !== 'string') return false
|
|
153
131
|
const [tokenShareId, secret] = token.split('.')
|
|
154
132
|
if (tokenShareId !== record.id || !secret) return false
|
|
155
|
-
const actualHash =
|
|
133
|
+
const actualHash = sha256Base64Url(secret)
|
|
156
134
|
const authVersion = record.authVersion || 1
|
|
157
135
|
const tokenRecords = pruneShareTokens(record.tokens)
|
|
158
136
|
|
package/server/system-prompt.mjs
CHANGED
|
@@ -6,6 +6,8 @@ For project tasks:
|
|
|
6
6
|
- Prefer dedicated workspace tools for reading, editing, and searching files.
|
|
7
7
|
- If dedicated tools are unavailable or insufficient, use the shell/command tool.
|
|
8
8
|
- Use Python through the shell for reliable scripting, data processing, or file transformations.
|
|
9
|
+
- When falling back to shell for file edits, do not create temporary helper scripts such as modify.py, patch.py, edit.js, or update.sh. Use inline shell commands only, such as python -c, python - <<'PY', node -e, sed, awk, cat > target <<'EOF', or git apply <<'PATCH'. Never write a helper script to disk just to execute it for code modification.
|
|
10
|
+
- If a file edit tool fails, re-read the relevant file context and retry the dedicated edit tool when practical before using shell fallback.
|
|
9
11
|
- Stay within the current workspace unless the user explicitly asks otherwise.
|
|
10
12
|
- Verify changes with relevant tests, build, lint, or targeted checks.
|
|
11
13
|
- If no suitable tool is available, say so clearly.`
|
package/server/tools/index.mjs
CHANGED
|
@@ -304,7 +304,22 @@ export async function toolReadSkillResource(params, context) {
|
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
// --- run_command ---
|
|
307
|
-
|
|
307
|
+
function formatCommandOutput(command, stdout, stderr, meta = {}) {
|
|
308
|
+
return [
|
|
309
|
+
`Command: ${command}`,
|
|
310
|
+
meta.running
|
|
311
|
+
? 'Status: running'
|
|
312
|
+
: `Exit code: ${meta.code ?? 'unknown'}${meta.signal ? `, signal: ${meta.signal}` : ''}${meta.timedOut ? ' (timed out)' : ''}`,
|
|
313
|
+
'',
|
|
314
|
+
'STDOUT:',
|
|
315
|
+
stdout || '(empty)',
|
|
316
|
+
'',
|
|
317
|
+
'STDERR:',
|
|
318
|
+
stderr || '(empty)',
|
|
319
|
+
].join('\n')
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export async function toolRunCommand(params, context, runtime = {}) {
|
|
308
323
|
const command = String(params?.command || '')
|
|
309
324
|
if (!command.trim()) {
|
|
310
325
|
const error = new Error('command is required')
|
|
@@ -315,8 +330,9 @@ export async function toolRunCommand(params, context) {
|
|
|
315
330
|
const timeoutMs = Math.min(10 * 60, Math.max(1, Number(params?.timeoutSeconds || 60))) * 1000
|
|
316
331
|
|
|
317
332
|
return new Promise((resolve) => {
|
|
333
|
+
const cwd = getToolWorkspaceRoot(context)
|
|
318
334
|
const child = spawn(command, {
|
|
319
|
-
cwd
|
|
335
|
+
cwd,
|
|
320
336
|
shell: true,
|
|
321
337
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
322
338
|
windowsHide: true,
|
|
@@ -325,6 +341,21 @@ export async function toolRunCommand(params, context) {
|
|
|
325
341
|
let stdout = ''
|
|
326
342
|
let stderr = ''
|
|
327
343
|
let timedOut = false
|
|
344
|
+
let updateTimer = null
|
|
345
|
+
let updatePending = false
|
|
346
|
+
const emitUpdate = () => {
|
|
347
|
+
updateTimer = null
|
|
348
|
+
if (!updatePending) return
|
|
349
|
+
updatePending = false
|
|
350
|
+
runtime.onUpdate?.({
|
|
351
|
+
content: [{ type: 'text', text: truncateText(formatCommandOutput(command, stdout, stderr, { running: true })) }],
|
|
352
|
+
details: { command, project: context?.project, cwd, running: true, stdout, stderr },
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
const scheduleUpdate = () => {
|
|
356
|
+
updatePending = true
|
|
357
|
+
if (!updateTimer) updateTimer = setTimeout(emitUpdate, 150)
|
|
358
|
+
}
|
|
328
359
|
const timer = setTimeout(() => {
|
|
329
360
|
timedOut = true
|
|
330
361
|
child.kill('SIGTERM')
|
|
@@ -332,26 +363,21 @@ export async function toolRunCommand(params, context) {
|
|
|
332
363
|
|
|
333
364
|
child.stdout.on('data', (chunk) => {
|
|
334
365
|
stdout = truncateText(stdout + chunk.toString())
|
|
366
|
+
scheduleUpdate()
|
|
335
367
|
})
|
|
336
368
|
child.stderr.on('data', (chunk) => {
|
|
337
369
|
stderr = truncateText(stderr + chunk.toString())
|
|
370
|
+
scheduleUpdate()
|
|
338
371
|
})
|
|
339
372
|
child.on('close', (code, signal) => {
|
|
340
373
|
clearTimeout(timer)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
'',
|
|
345
|
-
'STDOUT:',
|
|
346
|
-
stdout || '(empty)',
|
|
347
|
-
'',
|
|
348
|
-
'STDERR:',
|
|
349
|
-
stderr || '(empty)',
|
|
350
|
-
].join('\n')
|
|
351
|
-
resolve({ content: truncateText(content), details: { command, project: context?.project, cwd: getToolWorkspaceRoot(context), code, signal, timedOut } })
|
|
374
|
+
if (updateTimer) clearTimeout(updateTimer)
|
|
375
|
+
const content = formatCommandOutput(command, stdout, stderr, { code, signal, timedOut })
|
|
376
|
+
resolve({ content: truncateText(content), details: { command, project: context?.project, cwd, code, signal, timedOut } })
|
|
352
377
|
})
|
|
353
378
|
child.on('error', (err) => {
|
|
354
379
|
clearTimeout(timer)
|
|
380
|
+
if (updateTimer) clearTimeout(updateTimer)
|
|
355
381
|
resolve({
|
|
356
382
|
isError: true,
|
|
357
383
|
content: truncateText(`Error running command: ${err.message}`),
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createHash, randomBytes, scrypt as scryptCallback, timingSafeEqual } from 'node:crypto'
|
|
2
|
+
import { promisify } from 'node:util'
|
|
3
|
+
|
|
4
|
+
const scrypt = promisify(scryptCallback)
|
|
5
|
+
const PASSWORD_VERSION = 1
|
|
6
|
+
|
|
7
|
+
function randomToken(bytes = 24) {
|
|
8
|
+
return randomBytes(bytes).toString('base64url')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createRandomToken(bytes = 24) {
|
|
12
|
+
return randomToken(bytes)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function hashPassword(password, salt = randomToken(16)) {
|
|
16
|
+
if (typeof password !== 'string' || !password) return {}
|
|
17
|
+
const derived = await scrypt(password, salt, 32)
|
|
18
|
+
return {
|
|
19
|
+
passwordHash: derived.toString('base64url'),
|
|
20
|
+
passwordSalt: salt,
|
|
21
|
+
passwordVersion: PASSWORD_VERSION,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function verifyPassword(record, password) {
|
|
26
|
+
if (!record?.passwordHash || !record?.passwordSalt || typeof password !== 'string' || !password) return false
|
|
27
|
+
const { passwordHash } = await hashPassword(password, record.passwordSalt)
|
|
28
|
+
const expected = Buffer.from(record.passwordHash, 'base64url')
|
|
29
|
+
const actual = Buffer.from(passwordHash, 'base64url')
|
|
30
|
+
if (expected.length !== actual.length) return false
|
|
31
|
+
return timingSafeEqual(expected, actual)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function sha256Base64Url(value) {
|
|
35
|
+
return createHash('sha256').update(String(value)).digest('base64url')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function safeHashEqual(expectedHash, actualHash) {
|
|
39
|
+
if (!expectedHash || !actualHash) return false
|
|
40
|
+
const expected = Buffer.from(expectedHash)
|
|
41
|
+
const actual = Buffer.from(actualHash)
|
|
42
|
+
if (expected.length !== actual.length) return false
|
|
43
|
+
return timingSafeEqual(expected, actual)
|
|
44
|
+
}
|