Skip to content

Commit a0b8a28

Browse files
authored
Merge pull request #3 from spencerkit/feature/0.2.1
Allow remote auth over HTTP
2 parents d5896e8 + fd2ae88 commit a0b8a28

15 files changed

Lines changed: 185 additions & 57 deletions

File tree

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
## 0.2.1
6+
7+
### Changed
8+
9+
- Remote public-mode access no longer hard-requires HTTPS. HTTP access is now allowed, while HTTPS remains recommended for public deployment.
10+
- The auth gate now shows the normal sign-in flow on remote HTTP hosts instead of blocking with an HTTPS requirement.
11+
- Deployment docs now document the HTTP/HTTPS tradeoff and the `Secure` cookie behavior more clearly.
12+
13+
### Added
14+
15+
- Release E2E coverage for remote HTTP sign-in.
16+
- Community support acknowledgement for LinuxDo in the Chinese homepage README.
17+
18+
## 0.2.0
19+
20+
### Added
21+
22+
- Initial local-server + web-ui workbench release for local folders and remote Git repositories.
23+
- Claude-based workspace flow with draft tasks, split panes, and PTY-style agent interaction.
24+
- Code browsing and editing with file tree, file search, Monaco preview/edit, and save.
25+
- Git workflow support with diff review, stage, unstage, discard, and commit actions.
26+
- Embedded multi-terminal workspace panel.
27+
- Public-mode auth with passphrase login, session cookies, root-path restrictions, and IP blocking.
28+
- npm CLI packaging, release verification, and cross-platform runtime publishing flow.

README.en.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ For a publicly reachable deployment, the current build now includes:
172172
- `HttpOnly` session cookie
173173
- a `24` hour IP block after `3` failed passphrase attempts within `10` minutes
174174
- server-side single-root restrictions via `root.path`
175-
- HTTPS-required login for non-local hosts
175+
- public access over HTTP or HTTPS, with HTTPS reverse proxy still recommended
176176

177177
Deployment details are documented here:
178178

@@ -217,6 +217,7 @@ The following should not be described as fully shipped user-facing functionality
217217

218218
Product docs:
219219

220+
- Changelog: `CHANGELOG.md`
220221
- Chinese PRD: `docs/PRD.md`
221222
- English PRD: `docs/PRD.en.md`
222223

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
Coder Studio 是一个本地优先的开发工作台,当前以本地 server + Web 界面形态运行,用于把仓库接入、Claude Agent 运行、代码浏览与编辑、Git 操作、内置终端放到同一个界面中。
66

7+
## 社区支持
8+
9+
感谢 LinuxDo 各位佬的支持!欢迎大家加入 [LinuxDo](https://linux.do/),各种技术交流、AI 前沿资讯、AI 经验分享,尽在 LinuxDo!
10+
711
## 项目是什么
812

913
这个项目当前的产品形态不是“通用 AI 平台”,而是一个围绕真实 Git 仓库工作的本地工作台;默认通过本地 server 暴露界面与 API。
@@ -171,7 +175,7 @@ pnpm build:cli
171175
- `HttpOnly` session cookie
172176
- 同一 IP `10` 分钟内 `3` 次口令错误后封禁 `24` 小时
173177
- 基于 `root.path` 的服务端单根目录白名单
174-
- 对外访问时要求通过 HTTPS 反向代理提交口令
178+
- 支持通过 HTTP 或 HTTPS 对外访问,推荐在公网入口前使用 HTTPS 反向代理
175179

176180
部署细节请看:
177181

@@ -216,6 +220,7 @@ pnpm build:cli
216220

217221
用户文档:
218222

223+
- 更新日志:`CHANGELOG.md`
219224
- 中文 PRD:`docs/PRD.md`
220225
- English PRD: `docs/PRD.en.md`
221226

apps/server/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "coder-studio"
3-
version = "0.2.0"
3+
version = "0.2.1"
44
edition = "2021"
55

66
[dependencies]

apps/server/src/auth/mod.rs

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,6 @@ pub(crate) fn login(
479479
if !request.public_mode {
480480
return Ok((status_response(&auth.file, &request, true), String::new()));
481481
}
482-
ensure_transport_allowed(&request)?;
483482
if !password_configured(&auth.file) {
484483
return Err(AuthFailure::new(
485484
StatusCode::SERVICE_UNAVAILABLE,
@@ -599,7 +598,6 @@ pub(crate) fn require_session(
599598
});
600599
}
601600

602-
ensure_transport_allowed(&request)?;
603601
let token = read_cookie_token(headers).ok_or_else(|| {
604602
AuthFailure::new(StatusCode::UNAUTHORIZED, "session_missing").clear_cookie()
605603
})?;
@@ -969,24 +967,14 @@ fn status_response(
969967
authenticated,
970968
password_configured: password_configured(file),
971969
local_host: request.is_local_host,
972-
secure_transport_required: request.public_mode && !request.is_local_host,
970+
secure_transport_required: false,
973971
secure_transport_ok: request.is_local_host || request.is_secure_transport,
974972
session_idle_minutes: file.session_idle_minutes,
975973
session_max_hours: file.session_max_hours,
976974
allowed_roots: file.allowed_roots.clone(),
977975
}
978976
}
979977

980-
fn ensure_transport_allowed(request: &RequestContext) -> Result<(), AuthFailure> {
981-
if request.public_mode && !request.is_local_host && !request.is_secure_transport {
982-
return Err(AuthFailure::new(
983-
StatusCode::BAD_REQUEST,
984-
"secure_transport_required",
985-
));
986-
}
987-
Ok(())
988-
}
989-
990978
fn request_context(
991979
headers: &HeaderMap,
992980
client_addr: SocketAddr,
@@ -1416,4 +1404,57 @@ mod tests {
14161404
assert_eq!(effective_root_path(&file), None);
14171405
assert!(file.allowed_roots.is_empty());
14181406
}
1407+
1408+
#[test]
1409+
fn insecure_remote_public_mode_is_allowed_in_status_response() {
1410+
let file = AuthFile {
1411+
version: 1,
1412+
public_mode: true,
1413+
password: "demo-passphrase".to_string(),
1414+
root_path: "/srv/coder-studio".to_string(),
1415+
allowed_roots: vec!["/srv/coder-studio".to_string()],
1416+
bind_host: DEFAULT_BIND_HOST.to_string(),
1417+
bind_port: DEFAULT_BIND_PORT,
1418+
session_idle_minutes: DEFAULT_SESSION_IDLE_MINUTES,
1419+
session_max_hours: DEFAULT_SESSION_MAX_HOURS,
1420+
sessions: Vec::new(),
1421+
};
1422+
let request = RequestContext {
1423+
ip: "203.0.113.10".to_string(),
1424+
user_agent: "test".to_string(),
1425+
is_local_host: false,
1426+
is_secure_transport: false,
1427+
public_mode: true,
1428+
};
1429+
1430+
let status = status_response(&file, &request, false);
1431+
1432+
assert!(status.public_mode);
1433+
assert!(!status.authenticated);
1434+
assert!(!status.local_host);
1435+
assert!(!status.secure_transport_required);
1436+
assert!(!status.secure_transport_ok);
1437+
}
1438+
1439+
#[test]
1440+
fn remote_http_sessions_use_non_secure_cookies() {
1441+
let expires_at_ms = now_epoch_ms() + 60_000;
1442+
let insecure_request = RequestContext {
1443+
ip: "203.0.113.10".to_string(),
1444+
user_agent: "test".to_string(),
1445+
is_local_host: false,
1446+
is_secure_transport: false,
1447+
public_mode: true,
1448+
};
1449+
let secure_request = RequestContext {
1450+
is_secure_transport: true,
1451+
..insecure_request.clone()
1452+
};
1453+
1454+
let insecure_cookie = build_session_cookie("token", expires_at_ms, &insecure_request);
1455+
let secure_cookie = build_session_cookie("token", expires_at_ms, &secure_request);
1456+
1457+
assert!(!insecure_cookie.contains("; Secure"));
1458+
assert!(secure_cookie.contains("; Secure"));
1459+
}
14191460
}

apps/web/src/components/AuthGate.tsx

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ type AuthGateProps = {
2222
type AuthViewMode =
2323
| "sign-in"
2424
| "not-configured"
25-
| "transport-required"
2625
| "blocked"
2726
| "unavailable";
2827

@@ -310,16 +309,13 @@ export default function AuthGate({ locale, onSelectLocale, children }: AuthGateP
310309
}
311310

312311
const blockedLabel = formatBlockedUntil(blockedUntil, locale);
313-
const securityBlocked = status.secure_transport_required && !status.secure_transport_ok;
314312
const viewMode: AuthViewMode = !status.password_configured || errorCode === "auth_not_configured"
315313
? "not-configured"
316-
: securityBlocked
317-
? "transport-required"
318-
: errorCode === "ip_blocked"
319-
? "blocked"
320-
: errorCode === "auth_unavailable"
321-
? "unavailable"
322-
: "sign-in";
314+
: errorCode === "ip_blocked"
315+
? "blocked"
316+
: errorCode === "auth_unavailable"
317+
? "unavailable"
318+
: "sign-in";
323319

324320
const inlineMessage =
325321
errorCode === "invalid_credentials"
@@ -330,9 +326,7 @@ export default function AuthGate({ locale, onSelectLocale, children }: AuthGateP
330326

331327
const helperText = viewMode === "sign-in"
332328
? t("authAllowedRootsHint")
333-
: viewMode === "transport-required"
334-
? t("authSecureTransportRequired")
335-
: viewMode === "blocked"
329+
: viewMode === "blocked"
336330
? t("authBlockedUntil", { time: blockedLabel || "—" })
337331
: viewMode === "unavailable"
338332
? t("authUnavailable")
@@ -354,11 +348,6 @@ export default function AuthGate({ locale, onSelectLocale, children }: AuthGateP
354348
statusDescription = t("authNotConfiguredDescription");
355349
statusTone = "warning";
356350
showRetry = true;
357-
} else if (viewMode === "transport-required") {
358-
statusTitle = t("authTransportRequiredTitle");
359-
statusDescription = t("authTransportRequiredDescription");
360-
statusTone = "warning";
361-
showRetry = true;
362351
} else if (viewMode === "blocked") {
363352
statusTitle = t("authBlockedTitle");
364353
statusDescription = t("authBlockedDescription", { time: blockedLabel || "—" });
@@ -416,7 +405,6 @@ export default function AuthGate({ locale, onSelectLocale, children }: AuthGateP
416405
<div className="auth-card-bar">
417406
<div className="auth-badge-stack">
418407
<span className="section-kicker">{t("authPublicModeBadge")}</span>
419-
{viewMode === "transport-required" ? <span className="auth-state-pill warning">HTTPS</span> : null}
420408
{viewMode === "not-configured" ? <span className="auth-state-pill warning">auth.json</span> : null}
421409
{viewMode === "blocked" ? <span className="auth-state-pill danger">IP</span> : null}
422410
</div>

docs/deployment/README.en.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ The current code supports:
1313
- a `24` hour IP ban after `3` failed passphrase attempts within `10` minutes
1414
- server-side single-root restrictions through `rootPath`
1515
- authentication on both `HTTP RPC` and `WebSocket`
16-
- HTTPS-required passphrase submission for non-local hosts
16+
- passphrase submission over either HTTP or HTTPS for non-local hosts
1717
- local access on `localhost`, `127.0.0.1`, and `::1` defaults to non-public mode
1818
- local access with `?auth=force` explicitly forces public mode
1919

@@ -28,7 +28,7 @@ Recommended structure:
2828
Why this is recommended:
2929

3030
- the application process does not terminate TLS itself
31-
- non-local login requires secure transport
31+
- HTTPS avoids sending the passphrase and session cookie over a clear-text network path
3232
- a reverse proxy is the right place for certificates, domains, and public ingress
3333

3434
## Configuration File
@@ -91,13 +91,13 @@ Recommended production values:
9191
That means:
9292

9393
- the app only listens locally
94-
- all public traffic must come through an HTTPS reverse proxy
94+
- public traffic is easier to control through a reverse proxy
9595

9696
If you explicitly want the process to listen beyond loopback, you can set:
9797

9898
- `bindHost`: `0.0.0.0`
9999

100-
But even then, public access should still sit behind an HTTPS reverse proxy because the app does not provide TLS directly.
100+
But even then, public access should still sit behind an HTTPS reverse proxy because the app does not provide TLS directly, and plain HTTP would expose the passphrase plus a non-`Secure` session cookie on the wire.
101101

102102
## Reverse Proxy Requirements
103103

@@ -108,7 +108,7 @@ Your proxy should forward:
108108
- `X-Forwarded-For`
109109
- `X-Forwarded-Proto`
110110

111-
`X-Forwarded-Proto=https` is especially important because the app uses it to decide whether secure transport is in place.
111+
If the proxy terminates HTTPS, keep forwarding `X-Forwarded-Proto=https` so the runtime can mark the returned session cookie as `Secure`.
112112

113113
## Caddy Example
114114

@@ -150,7 +150,7 @@ server {
150150
3. Set the access passphrase
151151
4. Override `rootPath`, `bindHost`, or `bindPort` only if you need non-default values
152152
5. Start or restart the app
153-
6. Configure an HTTPS reverse proxy to `bindHost:bindPort`
153+
6. Configure a reverse proxy to `bindHost:bindPort` as needed, with HTTPS still recommended for public ingress
154154
7. Open your domain and verify the login screen appears first
155155

156156
Minimal setup:

docs/deployment/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
- 同一 IP 在 `10` 分钟内连续 `3` 次口令错误后,封禁 `24` 小时
1414
- `rootPath` 服务端单根目录白名单
1515
- `HTTP RPC``WebSocket` 同时鉴权
16-
- 非本地访问时要求通过 HTTPS 提交口令
16+
- 非本地访问时允许通过 HTTP 或 HTTPS 提交口令
1717
- `localhost` / `127.0.0.1` / `::1` 访问时默认走非 public mode
1818
- 本地访问时如果 URL 带 `?auth=force`,则强制走 public mode
1919

@@ -28,7 +28,7 @@
2828
推荐这样做的原因:
2929

3030
- 应用进程本身不做 TLS 终止
31-
- 对外登录默认要求安全传输
31+
- HTTPS 入口可以避免口令和 session cookie 以明文经过网络
3232
- 反向代理更适合处理证书、域名和公开入口
3333

3434
## 配置文件
@@ -91,13 +91,13 @@
9191
这表示:
9292

9393
- 应用只监听本机
94-
- 对外流量必须先经过 HTTPS 反向代理
94+
- 对外流量更适合先经过反向代理
9595

9696
如果你明确知道自己在做什么,也可以改成:
9797

9898
- `bindHost`: `0.0.0.0`
9999

100-
但即使这样,公网访问也仍然建议放在 HTTPS 反向代理后面,因为应用本身不直接提供 TLS。
100+
但即使这样,公网访问也仍然建议放在 HTTPS 反向代理后面,因为应用本身不直接提供 TLS,而且 HTTP 会让口令和非 `Secure` cookie 暴露在明文链路上
101101

102102
## 反向代理要求
103103

@@ -108,7 +108,7 @@
108108
- `X-Forwarded-For`
109109
- `X-Forwarded-Proto`
110110

111-
其中 `X-Forwarded-Proto=https` 很关键,因为应用会用它判断当前是否为安全传输
111+
如果代理层已经做了 HTTPS,建议继续透传 `X-Forwarded-Proto=https`,这样运行时返回的 session cookie 会自动带上 `Secure` 属性
112112

113113
## Caddy 示例
114114

@@ -150,7 +150,7 @@ server {
150150
3. 设置访问口令
151151
4. 如有需要,再覆盖默认的 `rootPath``bindHost``bindPort`
152152
5. 启动或重启应用
153-
6. 配置 HTTPS 反向代理到 `bindHost:bindPort`
153+
6. 按需要配置反向代理到 `bindHost:bindPort`,公网入口推荐使用 HTTPS
154154
7. 打开你的域名,确认先出现登录页
155155

156156
最小可用配置:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "coder-studio-workspace",
33
"private": true,
4-
"version": "0.2.0",
4+
"version": "0.2.1",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

packages/cli/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@spencer-kit/coder-studio",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"type": "module",
55
"description": "CLI runtime manager for Coder Studio.",
66
"bin": {
@@ -12,10 +12,10 @@
1212
"README.md"
1313
],
1414
"optionalDependencies": {
15-
"@spencer-kit/coder-studio-linux-x64": "0.2.0",
16-
"@spencer-kit/coder-studio-darwin-arm64": "0.2.0",
17-
"@spencer-kit/coder-studio-darwin-x64": "0.2.0",
18-
"@spencer-kit/coder-studio-win32-x64": "0.2.0"
15+
"@spencer-kit/coder-studio-linux-x64": "0.2.1",
16+
"@spencer-kit/coder-studio-darwin-arm64": "0.2.1",
17+
"@spencer-kit/coder-studio-darwin-x64": "0.2.1",
18+
"@spencer-kit/coder-studio-win32-x64": "0.2.1"
1919
},
2020
"publishConfig": {
2121
"access": "public"

0 commit comments

Comments
 (0)