@neuralnomads/codenomad-dev 0.10.3-dev-20260213-ba418a85 → 0.10.3-dev-20260213-e9f281a6
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/package.json +1 -1
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/assets/{main-CSlDZj4f.js → main-crtt5pqm.js} +82 -80
- package/public/index.html +1 -1
- package/public/sw.js +1 -1
- package/public/ui-version.json +1 -1
- package/dist/integrations/github/bot-signature.js +0 -11
- package/dist/integrations/github/git-ops.js +0 -133
- package/dist/integrations/github/github-types.js +0 -1
- package/dist/integrations/github/job-runner.js +0 -608
- package/dist/integrations/github/octokit.js +0 -58
- package/dist/integrations/github/sanitize-webhook.js +0 -42
- package/dist/integrations/github/webhook-verify.js +0 -21
- package/dist/integrations/github/workspace-context.js +0 -10
- package/dist/integrations/github/worktree-context.js +0 -15
- package/dist/opencode/request-context.js +0 -39
- package/dist/opencode/worktree-directory.js +0 -42
- package/dist/opencode-config-template/README.md +0 -32
- package/dist/opencode-config-template/opencode.jsonc +0 -3
- package/dist/opencode-config-template/plugin/codenomad.ts +0 -40
- package/dist/opencode-config-template/plugin/lib/background-process.ts +0 -160
- package/dist/opencode-config-template/plugin/lib/client.ts +0 -165
- package/dist/server/routes/github-plugin.js +0 -215
- package/dist/server/routes/github-webhook.js +0 -32
- package/scripts/copy-auth-pages.mjs +0 -22
- package/scripts/copy-opencode-config.mjs +0 -61
- package/scripts/copy-ui-dist.mjs +0 -21
- package/src/api-types.ts +0 -326
- package/src/auth/auth-store.ts +0 -175
- package/src/auth/http-auth.ts +0 -38
- package/src/auth/manager.ts +0 -163
- package/src/auth/password-hash.ts +0 -49
- package/src/auth/session-manager.ts +0 -23
- package/src/auth/token-manager.ts +0 -32
- package/src/background-processes/manager.ts +0 -519
- package/src/bin.ts +0 -29
- package/src/config/binaries.ts +0 -192
- package/src/config/location.ts +0 -78
- package/src/config/schema.ts +0 -104
- package/src/config/store.ts +0 -244
- package/src/events/bus.ts +0 -45
- package/src/filesystem/__tests__/search-cache.test.ts +0 -61
- package/src/filesystem/browser.ts +0 -353
- package/src/filesystem/search-cache.ts +0 -66
- package/src/filesystem/search.ts +0 -184
- package/src/index.ts +0 -540
- package/src/launcher.ts +0 -177
- package/src/loader.ts +0 -21
- package/src/logger.ts +0 -133
- package/src/opencode-config.ts +0 -31
- package/src/plugins/channel.ts +0 -55
- package/src/plugins/handlers.ts +0 -36
- package/src/releases/dev-release-monitor.ts +0 -118
- package/src/releases/release-monitor.ts +0 -149
- package/src/server/http-server.ts +0 -693
- package/src/server/network-addresses.ts +0 -75
- package/src/server/routes/auth-pages/login.html +0 -134
- package/src/server/routes/auth-pages/token.html +0 -93
- package/src/server/routes/auth.ts +0 -164
- package/src/server/routes/background-processes.ts +0 -85
- package/src/server/routes/config.ts +0 -76
- package/src/server/routes/events.ts +0 -61
- package/src/server/routes/filesystem.ts +0 -54
- package/src/server/routes/meta.ts +0 -58
- package/src/server/routes/plugin.ts +0 -75
- package/src/server/routes/storage.ts +0 -66
- package/src/server/routes/workspaces.ts +0 -113
- package/src/server/routes/worktrees.ts +0 -195
- package/src/server/tls.ts +0 -283
- package/src/storage/instance-store.ts +0 -64
- package/src/ui/__tests__/remote-ui.test.ts +0 -58
- package/src/ui/remote-ui.ts +0 -571
- package/src/workspaces/git-worktrees.ts +0 -241
- package/src/workspaces/instance-events.ts +0 -226
- package/src/workspaces/manager.ts +0 -493
- package/src/workspaces/opencode-auth.ts +0 -22
- package/src/workspaces/runtime.ts +0 -428
- package/src/workspaces/worktree-map.ts +0 -129
- package/tsconfig.json +0 -17
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import os from "os"
|
|
2
|
-
import type { NetworkAddress } from "../api-types"
|
|
3
|
-
|
|
4
|
-
export function resolveNetworkAddresses(args: {
|
|
5
|
-
host: string
|
|
6
|
-
protocol: "http" | "https"
|
|
7
|
-
port: number
|
|
8
|
-
}): NetworkAddress[] {
|
|
9
|
-
const { host, protocol, port } = args
|
|
10
|
-
const interfaces = os.networkInterfaces()
|
|
11
|
-
const seen = new Set<string>()
|
|
12
|
-
const results: NetworkAddress[] = []
|
|
13
|
-
|
|
14
|
-
const addAddress = (ip: string, scope: NetworkAddress["scope"]) => {
|
|
15
|
-
if (!ip || ip === "0.0.0.0") return
|
|
16
|
-
const key = `ipv4-${ip}`
|
|
17
|
-
if (seen.has(key)) return
|
|
18
|
-
seen.add(key)
|
|
19
|
-
results.push({ ip, family: "ipv4", scope, remoteUrl: `${protocol}://${ip}:${port}` })
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const normalizeFamily = (value: string | number) => {
|
|
23
|
-
if (typeof value === "string") {
|
|
24
|
-
const lowered = value.toLowerCase()
|
|
25
|
-
if (lowered === "ipv4") {
|
|
26
|
-
return "ipv4" as const
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (value === 4) return "ipv4" as const
|
|
30
|
-
return null
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (host === "0.0.0.0") {
|
|
34
|
-
// Enumerate system interfaces (IPv4 only)
|
|
35
|
-
for (const entries of Object.values(interfaces)) {
|
|
36
|
-
if (!entries) continue
|
|
37
|
-
for (const entry of entries) {
|
|
38
|
-
const family = normalizeFamily(entry.family)
|
|
39
|
-
if (!family) continue
|
|
40
|
-
if (!entry.address || entry.address === "0.0.0.0") continue
|
|
41
|
-
const scope: NetworkAddress["scope"] = entry.internal ? "loopback" : "external"
|
|
42
|
-
addAddress(entry.address, scope)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Always include loopback address
|
|
48
|
-
addAddress("127.0.0.1", "loopback")
|
|
49
|
-
|
|
50
|
-
// Include explicitly configured host if it was IPv4
|
|
51
|
-
if (isIPv4Address(host) && host !== "0.0.0.0") {
|
|
52
|
-
const isLoopback = host.startsWith("127.")
|
|
53
|
-
addAddress(host, isLoopback ? "loopback" : "external")
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const scopeWeight: Record<NetworkAddress["scope"], number> = { external: 0, internal: 1, loopback: 2 }
|
|
57
|
-
|
|
58
|
-
return results.sort((a, b) => {
|
|
59
|
-
const scopeDelta = scopeWeight[a.scope] - scopeWeight[b.scope]
|
|
60
|
-
if (scopeDelta !== 0) return scopeDelta
|
|
61
|
-
return a.ip.localeCompare(b.ip)
|
|
62
|
-
})
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function isIPv4Address(value: string | undefined): value is string {
|
|
66
|
-
if (!value) return false
|
|
67
|
-
const parts = value.split(".")
|
|
68
|
-
if (parts.length !== 4) return false
|
|
69
|
-
return parts.every((part) => {
|
|
70
|
-
if (part.length === 0 || part.length > 3) return false
|
|
71
|
-
if (!/^[0-9]+$/.test(part)) return false
|
|
72
|
-
const num = Number(part)
|
|
73
|
-
return Number.isInteger(num) && num >= 0 && num <= 255
|
|
74
|
-
})
|
|
75
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
<title>CodeNomad Login</title>
|
|
7
|
-
<style>
|
|
8
|
-
body {
|
|
9
|
-
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
|
|
10
|
-
background: #0b0b0f;
|
|
11
|
-
color: #fff;
|
|
12
|
-
display: flex;
|
|
13
|
-
align-items: center;
|
|
14
|
-
justify-content: center;
|
|
15
|
-
height: 100vh;
|
|
16
|
-
margin: 0;
|
|
17
|
-
}
|
|
18
|
-
.card {
|
|
19
|
-
width: 420px;
|
|
20
|
-
max-width: calc(100vw - 32px);
|
|
21
|
-
background: #14141c;
|
|
22
|
-
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
23
|
-
border-radius: 14px;
|
|
24
|
-
padding: 24px;
|
|
25
|
-
}
|
|
26
|
-
h1 {
|
|
27
|
-
font-size: 18px;
|
|
28
|
-
margin: 0 0 12px;
|
|
29
|
-
}
|
|
30
|
-
p {
|
|
31
|
-
margin: 0 0 18px;
|
|
32
|
-
color: rgba(255, 255, 255, 0.7);
|
|
33
|
-
font-size: 13px;
|
|
34
|
-
line-height: 1.4;
|
|
35
|
-
}
|
|
36
|
-
label {
|
|
37
|
-
display: block;
|
|
38
|
-
font-size: 12px;
|
|
39
|
-
margin: 10px 0 6px;
|
|
40
|
-
color: rgba(255, 255, 255, 0.75);
|
|
41
|
-
}
|
|
42
|
-
input {
|
|
43
|
-
width: 100%;
|
|
44
|
-
box-sizing: border-box;
|
|
45
|
-
padding: 10px 12px;
|
|
46
|
-
border-radius: 10px;
|
|
47
|
-
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
48
|
-
background: #0f0f16;
|
|
49
|
-
color: #fff;
|
|
50
|
-
}
|
|
51
|
-
button {
|
|
52
|
-
width: 100%;
|
|
53
|
-
margin-top: 14px;
|
|
54
|
-
padding: 10px 12px;
|
|
55
|
-
border-radius: 10px;
|
|
56
|
-
border: 0;
|
|
57
|
-
background: #4c6fff;
|
|
58
|
-
color: #fff;
|
|
59
|
-
font-weight: 600;
|
|
60
|
-
cursor: pointer;
|
|
61
|
-
}
|
|
62
|
-
.error {
|
|
63
|
-
margin-top: 12px;
|
|
64
|
-
color: #ff6b6b;
|
|
65
|
-
font-size: 13px;
|
|
66
|
-
}
|
|
67
|
-
</style>
|
|
68
|
-
</head>
|
|
69
|
-
<body>
|
|
70
|
-
<div class="card">
|
|
71
|
-
<h1>Sign in</h1>
|
|
72
|
-
<p>This CodeNomad server is protected. Enter your credentials to continue.</p>
|
|
73
|
-
|
|
74
|
-
<label for="username">Username</label>
|
|
75
|
-
<input id="username" autocomplete="username" placeholder="{{DEFAULT_USERNAME}}" value="" />
|
|
76
|
-
|
|
77
|
-
<label for="password">Password</label>
|
|
78
|
-
<input id="password" type="password" autocomplete="current-password" value="" />
|
|
79
|
-
|
|
80
|
-
<button id="submit" type="button">Continue</button>
|
|
81
|
-
<div id="error" class="error" style="display: none"></div>
|
|
82
|
-
</div>
|
|
83
|
-
|
|
84
|
-
<script>
|
|
85
|
-
const $ = (id) => document.getElementById(id)
|
|
86
|
-
const errorEl = $("error")
|
|
87
|
-
const showError = (msg) => {
|
|
88
|
-
errorEl.textContent = msg
|
|
89
|
-
errorEl.style.display = "block"
|
|
90
|
-
}
|
|
91
|
-
const hideError = () => {
|
|
92
|
-
errorEl.textContent = ""
|
|
93
|
-
errorEl.style.display = "none"
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function submit() {
|
|
97
|
-
hideError()
|
|
98
|
-
const username = $("username").value.trim()
|
|
99
|
-
const password = $("password").value
|
|
100
|
-
if (!username || !password) {
|
|
101
|
-
showError("Username and password are required.")
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
try {
|
|
105
|
-
const res = await fetch("/api/auth/login", {
|
|
106
|
-
method: "POST",
|
|
107
|
-
headers: { "Content-Type": "application/json" },
|
|
108
|
-
body: JSON.stringify({ username, password }),
|
|
109
|
-
credentials: "include",
|
|
110
|
-
})
|
|
111
|
-
if (!res.ok) {
|
|
112
|
-
let message = ""
|
|
113
|
-
try {
|
|
114
|
-
const json = await res.json()
|
|
115
|
-
message = json && json.error ? String(json.error) : ""
|
|
116
|
-
} catch {
|
|
117
|
-
message = ""
|
|
118
|
-
}
|
|
119
|
-
showError(message || `Login failed (${res.status})`)
|
|
120
|
-
return
|
|
121
|
-
}
|
|
122
|
-
window.location.href = "/"
|
|
123
|
-
} catch (e) {
|
|
124
|
-
showError(e && e.message ? e.message : String(e))
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
$("submit").addEventListener("click", submit)
|
|
129
|
-
$("password").addEventListener("keydown", (e) => {
|
|
130
|
-
if (e.key === "Enter") submit()
|
|
131
|
-
})
|
|
132
|
-
</script>
|
|
133
|
-
</body>
|
|
134
|
-
</html>
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
<title>CodeNomad</title>
|
|
7
|
-
<style>
|
|
8
|
-
body {
|
|
9
|
-
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
|
|
10
|
-
background: #0b0b0f;
|
|
11
|
-
color: #fff;
|
|
12
|
-
display: flex;
|
|
13
|
-
align-items: center;
|
|
14
|
-
justify-content: center;
|
|
15
|
-
height: 100vh;
|
|
16
|
-
margin: 0;
|
|
17
|
-
}
|
|
18
|
-
.card {
|
|
19
|
-
width: 420px;
|
|
20
|
-
max-width: calc(100vw - 32px);
|
|
21
|
-
background: #14141c;
|
|
22
|
-
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
23
|
-
border-radius: 14px;
|
|
24
|
-
padding: 24px;
|
|
25
|
-
}
|
|
26
|
-
h1 {
|
|
27
|
-
font-size: 18px;
|
|
28
|
-
margin: 0 0 12px;
|
|
29
|
-
}
|
|
30
|
-
p {
|
|
31
|
-
margin: 0;
|
|
32
|
-
color: rgba(255, 255, 255, 0.7);
|
|
33
|
-
font-size: 13px;
|
|
34
|
-
line-height: 1.4;
|
|
35
|
-
}
|
|
36
|
-
.error {
|
|
37
|
-
margin-top: 12px;
|
|
38
|
-
color: #ff6b6b;
|
|
39
|
-
font-size: 13px;
|
|
40
|
-
}
|
|
41
|
-
</style>
|
|
42
|
-
</head>
|
|
43
|
-
<body>
|
|
44
|
-
<div class="card">
|
|
45
|
-
<h1>Connecting…</h1>
|
|
46
|
-
<p>Finalizing local authentication.</p>
|
|
47
|
-
<div id="error" class="error" style="display: none"></div>
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
<script>
|
|
51
|
-
const token = (location.hash || "").replace(/^#/, "").trim()
|
|
52
|
-
const errorEl = document.getElementById("error")
|
|
53
|
-
const showError = (msg) => {
|
|
54
|
-
errorEl.textContent = msg
|
|
55
|
-
errorEl.style.display = "block"
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function run() {
|
|
59
|
-
if (!token) {
|
|
60
|
-
showError("Missing bootstrap token.")
|
|
61
|
-
return
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
const res = await fetch("/api/auth/token", {
|
|
66
|
-
method: "POST",
|
|
67
|
-
headers: { "Content-Type": "application/json" },
|
|
68
|
-
body: JSON.stringify({ token }),
|
|
69
|
-
credentials: "include",
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
if (!res.ok) {
|
|
73
|
-
let message = ""
|
|
74
|
-
try {
|
|
75
|
-
const json = await res.json()
|
|
76
|
-
message = json && json.error ? String(json.error) : ""
|
|
77
|
-
} catch {
|
|
78
|
-
message = ""
|
|
79
|
-
}
|
|
80
|
-
showError(message || `Token exchange failed (${res.status})`)
|
|
81
|
-
return
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
window.location.replace("/")
|
|
85
|
-
} catch (e) {
|
|
86
|
-
showError(e && e.message ? e.message : String(e))
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
run()
|
|
91
|
-
</script>
|
|
92
|
-
</body>
|
|
93
|
-
</html>
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import type { FastifyInstance } from "fastify"
|
|
2
|
-
import fs from "fs"
|
|
3
|
-
import { z } from "zod"
|
|
4
|
-
import type { AuthManager } from "../../auth/manager"
|
|
5
|
-
import { isLoopbackAddress } from "../../auth/http-auth"
|
|
6
|
-
|
|
7
|
-
interface RouteDeps {
|
|
8
|
-
authManager: AuthManager
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const LoginSchema = z.object({
|
|
12
|
-
username: z.string().min(1),
|
|
13
|
-
password: z.string().min(1),
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
const TokenSchema = z.object({
|
|
17
|
-
token: z.string().min(1),
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
const PasswordSchema = z.object({
|
|
21
|
-
password: z.string().min(8),
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
const LOGIN_TEMPLATE_URL = new URL("./auth-pages/login.html", import.meta.url)
|
|
25
|
-
const TOKEN_TEMPLATE_URL = new URL("./auth-pages/token.html", import.meta.url)
|
|
26
|
-
|
|
27
|
-
let cachedLoginTemplate: string | null = null
|
|
28
|
-
let cachedTokenTemplate: string | null = null
|
|
29
|
-
|
|
30
|
-
function readTemplate(url: URL, cache: string | null): string {
|
|
31
|
-
if (cache) return cache
|
|
32
|
-
const content = fs.readFileSync(url, "utf-8")
|
|
33
|
-
return content
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function getLoginHtml(defaultUsername: string): string {
|
|
37
|
-
if (!cachedLoginTemplate) {
|
|
38
|
-
cachedLoginTemplate = readTemplate(LOGIN_TEMPLATE_URL, null)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const escapedUsername = escapeHtml(defaultUsername)
|
|
42
|
-
return cachedLoginTemplate.replace(/\{\{DEFAULT_USERNAME\}\}/g, escapedUsername)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function getTokenHtml(): string {
|
|
46
|
-
if (!cachedTokenTemplate) {
|
|
47
|
-
cachedTokenTemplate = readTemplate(TOKEN_TEMPLATE_URL, null)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return cachedTokenTemplate
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function registerAuthRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|
54
|
-
app.get("/login", async (_request, reply) => {
|
|
55
|
-
const status = deps.authManager.getStatus()
|
|
56
|
-
reply.type("text/html").send(getLoginHtml(status.username))
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
app.get("/auth/token", async (request, reply) => {
|
|
60
|
-
if (!deps.authManager.isTokenBootstrapEnabled()) {
|
|
61
|
-
reply.code(404).send({ error: "Not found" })
|
|
62
|
-
return
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (!isLoopbackAddress(request.socket.remoteAddress)) {
|
|
66
|
-
reply.code(404).send({ error: "Not found" })
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
reply.type("text/html").send(getTokenHtml())
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
app.get("/api/auth/status", async (request, reply) => {
|
|
74
|
-
const session = deps.authManager.getSessionFromRequest(request)
|
|
75
|
-
if (!session) {
|
|
76
|
-
reply.send({ authenticated: false })
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
reply.send({ authenticated: true, ...deps.authManager.getStatus() })
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
app.post("/api/auth/login", async (request, reply) => {
|
|
83
|
-
const body = LoginSchema.parse(request.body ?? {})
|
|
84
|
-
const ok = deps.authManager.validateLogin(body.username, body.password)
|
|
85
|
-
if (!ok) {
|
|
86
|
-
reply.code(401).send({ error: "Invalid credentials" })
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const session = deps.authManager.createSession(body.username)
|
|
91
|
-
deps.authManager.setSessionCookieWithOptions(reply, session.id, { secure: isSecureRequest(request) })
|
|
92
|
-
reply.send({ ok: true })
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
app.post("/api/auth/token", async (request, reply) => {
|
|
96
|
-
if (!deps.authManager.isTokenBootstrapEnabled()) {
|
|
97
|
-
reply.code(404).send({ error: "Not found" })
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!isLoopbackAddress(request.socket.remoteAddress)) {
|
|
102
|
-
reply.code(404).send({ error: "Not found" })
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const body = TokenSchema.parse(request.body ?? {})
|
|
107
|
-
const ok = deps.authManager.consumeBootstrapToken(body.token)
|
|
108
|
-
if (!ok) {
|
|
109
|
-
reply.code(401).send({ error: "Invalid token" })
|
|
110
|
-
return
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const username = deps.authManager.getStatus().username
|
|
114
|
-
const session = deps.authManager.createSession(username)
|
|
115
|
-
deps.authManager.setSessionCookieWithOptions(reply, session.id, { secure: isSecureRequest(request) })
|
|
116
|
-
reply.send({ ok: true })
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
app.post("/api/auth/logout", async (request, reply) => {
|
|
120
|
-
deps.authManager.clearSessionCookieWithOptions(reply, { secure: isSecureRequest(request) })
|
|
121
|
-
reply.send({ ok: true })
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
app.post("/api/auth/password", async (request, reply) => {
|
|
125
|
-
const session = deps.authManager.getSessionFromRequest(request)
|
|
126
|
-
if (!session) {
|
|
127
|
-
reply.code(401).send({ error: "Unauthorized" })
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const body = PasswordSchema.parse(request.body ?? {})
|
|
132
|
-
try {
|
|
133
|
-
const status = deps.authManager.setPassword(body.password)
|
|
134
|
-
reply.send({ ok: true, ...status })
|
|
135
|
-
} catch (error) {
|
|
136
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
137
|
-
reply.code(409).type("text/plain").send(message)
|
|
138
|
-
}
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function isSecureRequest(request: any) {
|
|
143
|
-
if (request.protocol === "https") {
|
|
144
|
-
return true
|
|
145
|
-
}
|
|
146
|
-
return Boolean(request.raw?.socket && request.raw.socket.encrypted)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function escapeHtml(value: string) {
|
|
150
|
-
return value.replace(/[&<>"]/g, (char) => {
|
|
151
|
-
switch (char) {
|
|
152
|
-
case "&":
|
|
153
|
-
return "&"
|
|
154
|
-
case "<":
|
|
155
|
-
return "<"
|
|
156
|
-
case ">":
|
|
157
|
-
return ">"
|
|
158
|
-
case '"':
|
|
159
|
-
return """
|
|
160
|
-
default:
|
|
161
|
-
return char
|
|
162
|
-
}
|
|
163
|
-
})
|
|
164
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { FastifyInstance } from "fastify"
|
|
2
|
-
import { z } from "zod"
|
|
3
|
-
import type { BackgroundProcessManager } from "../../background-processes/manager"
|
|
4
|
-
|
|
5
|
-
interface RouteDeps {
|
|
6
|
-
backgroundProcessManager: BackgroundProcessManager
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const StartSchema = z.object({
|
|
10
|
-
title: z.string().trim().min(1),
|
|
11
|
-
command: z.string().trim().min(1),
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
const OutputQuerySchema = z.object({
|
|
15
|
-
method: z.enum(["full", "tail", "head", "grep"]).optional(),
|
|
16
|
-
mode: z.enum(["full", "tail", "head", "grep"]).optional(),
|
|
17
|
-
pattern: z.string().optional(),
|
|
18
|
-
lines: z.coerce.number().int().positive().max(2000).optional(),
|
|
19
|
-
maxBytes: z.coerce.number().int().positive().optional(),
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
export function registerBackgroundProcessRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|
23
|
-
app.get<{ Params: { id: string } }>("/workspaces/:id/plugin/background-processes", async (request) => {
|
|
24
|
-
const processes = await deps.backgroundProcessManager.list(request.params.id)
|
|
25
|
-
return { processes }
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
app.post<{ Params: { id: string } }>("/workspaces/:id/plugin/background-processes", async (request, reply) => {
|
|
29
|
-
const payload = StartSchema.parse(request.body ?? {})
|
|
30
|
-
const process = await deps.backgroundProcessManager.start(request.params.id, payload.title, payload.command)
|
|
31
|
-
reply.code(201)
|
|
32
|
-
return process
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
app.post<{ Params: { id: string; processId: string } }>(
|
|
36
|
-
"/workspaces/:id/plugin/background-processes/:processId/stop",
|
|
37
|
-
async (request, reply) => {
|
|
38
|
-
const process = await deps.backgroundProcessManager.stop(request.params.id, request.params.processId)
|
|
39
|
-
if (!process) {
|
|
40
|
-
reply.code(404)
|
|
41
|
-
return { error: "Process not found" }
|
|
42
|
-
}
|
|
43
|
-
return process
|
|
44
|
-
},
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
app.post<{ Params: { id: string; processId: string } }>(
|
|
48
|
-
"/workspaces/:id/plugin/background-processes/:processId/terminate",
|
|
49
|
-
async (request, reply) => {
|
|
50
|
-
await deps.backgroundProcessManager.terminate(request.params.id, request.params.processId)
|
|
51
|
-
reply.code(204)
|
|
52
|
-
return undefined
|
|
53
|
-
},
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
app.get<{ Params: { id: string; processId: string } }>(
|
|
57
|
-
"/workspaces/:id/plugin/background-processes/:processId/output",
|
|
58
|
-
async (request, reply) => {
|
|
59
|
-
const query = OutputQuerySchema.parse(request.query ?? {})
|
|
60
|
-
const method = query.method ?? query.mode
|
|
61
|
-
if (method === "grep" && !query.pattern) {
|
|
62
|
-
reply.code(400)
|
|
63
|
-
return { error: "Pattern is required for grep output" }
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
return await deps.backgroundProcessManager.readOutput(request.params.id, request.params.processId, {
|
|
67
|
-
method,
|
|
68
|
-
pattern: query.pattern,
|
|
69
|
-
lines: query.lines,
|
|
70
|
-
maxBytes: query.maxBytes,
|
|
71
|
-
})
|
|
72
|
-
} catch (error) {
|
|
73
|
-
reply.code(400)
|
|
74
|
-
return { error: error instanceof Error ? error.message : "Invalid output request" }
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
app.get<{ Params: { id: string; processId: string } }>(
|
|
80
|
-
"/workspaces/:id/plugin/background-processes/:processId/stream",
|
|
81
|
-
async (request, reply) => {
|
|
82
|
-
await deps.backgroundProcessManager.streamOutput(request.params.id, request.params.processId, reply)
|
|
83
|
-
},
|
|
84
|
-
)
|
|
85
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { FastifyInstance } from "fastify"
|
|
2
|
-
import { z } from "zod"
|
|
3
|
-
import { ConfigStore } from "../../config/store"
|
|
4
|
-
import { BinaryRegistry } from "../../config/binaries"
|
|
5
|
-
|
|
6
|
-
interface RouteDeps {
|
|
7
|
-
configStore: ConfigStore
|
|
8
|
-
binaryRegistry: BinaryRegistry
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const BinaryCreateSchema = z.object({
|
|
12
|
-
path: z.string(),
|
|
13
|
-
label: z.string().optional(),
|
|
14
|
-
makeDefault: z.boolean().optional(),
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
const BinaryUpdateSchema = z.object({
|
|
18
|
-
label: z.string().optional(),
|
|
19
|
-
makeDefault: z.boolean().optional(),
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
const BinaryValidateSchema = z.object({
|
|
23
|
-
path: z.string(),
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
export function registerConfigRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|
27
|
-
app.get("/api/config/app", async () => deps.configStore.get())
|
|
28
|
-
|
|
29
|
-
app.put("/api/config/app", async (request, reply) => {
|
|
30
|
-
// Backwards compatible: treat PUT as a merge-patch update.
|
|
31
|
-
try {
|
|
32
|
-
deps.configStore.mergePatch(request.body ?? {})
|
|
33
|
-
return deps.configStore.get()
|
|
34
|
-
} catch (error) {
|
|
35
|
-
reply.code(400)
|
|
36
|
-
return { error: error instanceof Error ? error.message : "Invalid config patch" }
|
|
37
|
-
}
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
app.patch("/api/config/app", async (request, reply) => {
|
|
41
|
-
try {
|
|
42
|
-
deps.configStore.mergePatch(request.body ?? {})
|
|
43
|
-
return deps.configStore.get()
|
|
44
|
-
} catch (error) {
|
|
45
|
-
reply.code(400)
|
|
46
|
-
return { error: error instanceof Error ? error.message : "Invalid config patch" }
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
app.get("/api/config/binaries", async () => {
|
|
51
|
-
return { binaries: deps.binaryRegistry.list() }
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
app.post("/api/config/binaries", async (request, reply) => {
|
|
55
|
-
const body = BinaryCreateSchema.parse(request.body ?? {})
|
|
56
|
-
const binary = deps.binaryRegistry.create(body)
|
|
57
|
-
reply.code(201)
|
|
58
|
-
return { binary }
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
app.patch<{ Params: { id: string } }>("/api/config/binaries/:id", async (request) => {
|
|
62
|
-
const body = BinaryUpdateSchema.parse(request.body ?? {})
|
|
63
|
-
const binary = deps.binaryRegistry.update(request.params.id, body)
|
|
64
|
-
return { binary }
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
app.delete<{ Params: { id: string } }>("/api/config/binaries/:id", async (request, reply) => {
|
|
68
|
-
deps.binaryRegistry.remove(request.params.id)
|
|
69
|
-
reply.code(204)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
app.post("/api/config/binaries/validate", async (request) => {
|
|
73
|
-
const body = BinaryValidateSchema.parse(request.body ?? {})
|
|
74
|
-
return deps.binaryRegistry.validatePath(body.path)
|
|
75
|
-
})
|
|
76
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { FastifyInstance } from "fastify"
|
|
2
|
-
import { EventBus } from "../../events/bus"
|
|
3
|
-
import { WorkspaceEventPayload } from "../../api-types"
|
|
4
|
-
import { Logger } from "../../logger"
|
|
5
|
-
|
|
6
|
-
interface RouteDeps {
|
|
7
|
-
eventBus: EventBus
|
|
8
|
-
registerClient: (cleanup: () => void) => () => void
|
|
9
|
-
logger: Logger
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
let nextClientId = 0
|
|
13
|
-
|
|
14
|
-
export function registerEventRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|
15
|
-
app.get("/api/events", (request, reply) => {
|
|
16
|
-
const clientId = ++nextClientId
|
|
17
|
-
deps.logger.debug({ clientId }, "SSE client connected")
|
|
18
|
-
|
|
19
|
-
const origin = request.headers.origin ?? "*"
|
|
20
|
-
reply.raw.setHeader("Access-Control-Allow-Origin", origin)
|
|
21
|
-
reply.raw.setHeader("Access-Control-Allow-Credentials", "true")
|
|
22
|
-
reply.raw.setHeader("Content-Type", "text/event-stream")
|
|
23
|
-
reply.raw.setHeader("Cache-Control", "no-cache")
|
|
24
|
-
reply.raw.setHeader("Connection", "keep-alive")
|
|
25
|
-
reply.raw.flushHeaders?.()
|
|
26
|
-
reply.hijack()
|
|
27
|
-
|
|
28
|
-
const send = (event: WorkspaceEventPayload) => {
|
|
29
|
-
deps.logger.debug({ clientId, type: event.type }, "SSE event dispatched")
|
|
30
|
-
if (deps.logger.isLevelEnabled("trace")) {
|
|
31
|
-
deps.logger.trace({ clientId, event }, "SSE event payload")
|
|
32
|
-
}
|
|
33
|
-
reply.raw.write(`data: ${JSON.stringify(event)}\n\n`)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const unsubscribe = deps.eventBus.onEvent(send)
|
|
37
|
-
const heartbeat = setInterval(() => {
|
|
38
|
-
reply.raw.write(`:hb ${Date.now()}\n\n`)
|
|
39
|
-
}, 15000)
|
|
40
|
-
|
|
41
|
-
let closed = false
|
|
42
|
-
const close = () => {
|
|
43
|
-
if (closed) return
|
|
44
|
-
closed = true
|
|
45
|
-
clearInterval(heartbeat)
|
|
46
|
-
unsubscribe()
|
|
47
|
-
reply.raw.end?.()
|
|
48
|
-
deps.logger.debug({ clientId }, "SSE client disconnected")
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const unregister = deps.registerClient(close)
|
|
52
|
-
|
|
53
|
-
const handleClose = () => {
|
|
54
|
-
close()
|
|
55
|
-
unregister()
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
request.raw.on("close", handleClose)
|
|
59
|
-
request.raw.on("error", handleClose)
|
|
60
|
-
})
|
|
61
|
-
}
|