refactor: drop PAM auth and require password for funnel
This commit is contained in:
@@ -13,6 +13,8 @@
|
|||||||
- Telegram/WhatsApp: native replies now target the original inbound message; reply context is appended to `Body` and captured in `ReplyTo*` fields. (Thanks @joshp123 for the PR and follow-up question.)
|
- Telegram/WhatsApp: native replies now target the original inbound message; reply context is appended to `Body` and captured in `ReplyTo*` fields. (Thanks @joshp123 for the PR and follow-up question.)
|
||||||
- WhatsApp web creds persistence hardened; credentials are restored before auth checks and QR login auto-restarts if it stalls.
|
- WhatsApp web creds persistence hardened; credentials are restored before auth checks and QR login auto-restarts if it stalls.
|
||||||
- Group chats now honor `inbound.groupChat.requireMention=false` as the default activation when no per-group override exists.
|
- Group chats now honor `inbound.groupChat.requireMention=false` as the default activation when no per-group override exists.
|
||||||
|
- Gateway auth no longer supports PAM/system mode; use token or shared password.
|
||||||
|
- Tailscale Funnel now requires password auth (no token-only public exposure).
|
||||||
- Canvas defaults/A2UI auto-nav aligned; debug status overlay centered; redundant await removed in `CanvasManager`.
|
- Canvas defaults/A2UI auto-nav aligned; debug status overlay centered; redundant await removed in `CanvasManager`.
|
||||||
- Gateway launchd loop fixed by removing redundant `kickstart -k`.
|
- Gateway launchd loop fixed by removing redundant `kickstart -k`.
|
||||||
- CLI now hints when Peekaboo is unauthorized.
|
- CLI now hints when Peekaboo is unauthorized.
|
||||||
|
|||||||
@@ -27,20 +27,6 @@ private enum GatewayTailscaleMode: String, CaseIterable, Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum GatewayAuthMode: String, CaseIterable, Identifiable {
|
|
||||||
case system
|
|
||||||
case password
|
|
||||||
|
|
||||||
var id: String { self.rawValue }
|
|
||||||
|
|
||||||
var label: String {
|
|
||||||
switch self {
|
|
||||||
case .system: "System password"
|
|
||||||
case .password: "Shared password"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TailscaleIntegrationSection: View {
|
struct TailscaleIntegrationSection: View {
|
||||||
let connectionMode: AppState.ConnectionMode
|
let connectionMode: AppState.ConnectionMode
|
||||||
let isPaused: Bool
|
let isPaused: Bool
|
||||||
@@ -50,8 +36,6 @@ struct TailscaleIntegrationSection: View {
|
|||||||
@State private var hasLoaded = false
|
@State private var hasLoaded = false
|
||||||
@State private var tailscaleMode: GatewayTailscaleMode = .off
|
@State private var tailscaleMode: GatewayTailscaleMode = .off
|
||||||
@State private var requireCredentialsForServe = false
|
@State private var requireCredentialsForServe = false
|
||||||
@State private var authMode: GatewayAuthMode = .system
|
|
||||||
@State private var username: String = ""
|
|
||||||
@State private var password: String = ""
|
@State private var password: String = ""
|
||||||
@State private var statusMessage: String?
|
@State private var statusMessage: String?
|
||||||
@State private var validationMessage: String?
|
@State private var validationMessage: String?
|
||||||
@@ -115,9 +99,6 @@ struct TailscaleIntegrationSection: View {
|
|||||||
.onChange(of: self.requireCredentialsForServe) { _, _ in
|
.onChange(of: self.requireCredentialsForServe) { _, _ in
|
||||||
self.applySettings()
|
self.applySettings()
|
||||||
}
|
}
|
||||||
.onChange(of: self.authMode) { _, _ in
|
|
||||||
self.applySettings()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var statusRow: some View {
|
private var statusRow: some View {
|
||||||
@@ -210,7 +191,6 @@ struct TailscaleIntegrationSection: View {
|
|||||||
Toggle("Require credentials", isOn: self.$requireCredentialsForServe)
|
Toggle("Require credentials", isOn: self.$requireCredentialsForServe)
|
||||||
.toggleStyle(.checkbox)
|
.toggleStyle(.checkbox)
|
||||||
if self.requireCredentialsForServe {
|
if self.requireCredentialsForServe {
|
||||||
self.authModePicker
|
|
||||||
self.authFields
|
self.authFields
|
||||||
} else {
|
} else {
|
||||||
Text("Serve uses Tailscale identity headers; no password required.")
|
Text("Serve uses Tailscale identity headers; no password required.")
|
||||||
@@ -225,39 +205,22 @@ struct TailscaleIntegrationSection: View {
|
|||||||
Text("Funnel requires authentication.")
|
Text("Funnel requires authentication.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
self.authModePicker
|
|
||||||
self.authFields
|
self.authFields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var authModePicker: some View {
|
|
||||||
Picker("Auth", selection: self.$authMode) {
|
|
||||||
ForEach(GatewayAuthMode.allCases) { mode in
|
|
||||||
Text(mode.label).tag(mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pickerStyle(.segmented)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var authFields: some View {
|
private var authFields: some View {
|
||||||
if self.authMode == .system {
|
SecureField("Password", text: self.$password)
|
||||||
TextField("Username (optional)", text: self.$username)
|
.textFieldStyle(.roundedBorder)
|
||||||
.textFieldStyle(.roundedBorder)
|
.frame(maxWidth: 240)
|
||||||
.frame(maxWidth: 240)
|
.onSubmit { self.applySettings() }
|
||||||
.onSubmit { self.applySettings() }
|
Text("Stored in ~/.clawdis/clawdis.json. Prefer CLAWDIS_GATEWAY_PASSWORD for production.")
|
||||||
} else {
|
.font(.caption)
|
||||||
SecureField("Password", text: self.$password)
|
.foregroundStyle(.secondary)
|
||||||
.textFieldStyle(.roundedBorder)
|
Button("Update password") { self.applySettings() }
|
||||||
.frame(maxWidth: 240)
|
.buttonStyle(.bordered)
|
||||||
.onSubmit { self.applySettings() }
|
.controlSize(.small)
|
||||||
Text("Stored in ~/.clawdis/clawdis.json. Prefer CLAWDIS_GATEWAY_PASSWORD for production.")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Button("Update password") { self.applySettings() }
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.small)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadConfig() {
|
private func loadConfig() {
|
||||||
@@ -270,17 +233,10 @@ struct TailscaleIntegrationSection: View {
|
|||||||
let authModeRaw = auth["mode"] as? String
|
let authModeRaw = auth["mode"] as? String
|
||||||
let allowTailscale = auth["allowTailscale"] as? Bool
|
let allowTailscale = auth["allowTailscale"] as? Bool
|
||||||
|
|
||||||
if let authModeRaw, authModeRaw == "password" {
|
|
||||||
self.authMode = .password
|
|
||||||
} else {
|
|
||||||
self.authMode = .system
|
|
||||||
}
|
|
||||||
|
|
||||||
self.username = auth["username"] as? String ?? ""
|
|
||||||
self.password = auth["password"] as? String ?? ""
|
self.password = auth["password"] as? String ?? ""
|
||||||
|
|
||||||
if self.tailscaleMode == .serve {
|
if self.tailscaleMode == .serve {
|
||||||
let usesExplicitAuth = authModeRaw == "password" || authModeRaw == "system"
|
let usesExplicitAuth = authModeRaw == "password"
|
||||||
if let allowTailscale, allowTailscale == false {
|
if let allowTailscale, allowTailscale == false {
|
||||||
self.requireCredentialsForServe = true
|
self.requireCredentialsForServe = true
|
||||||
} else {
|
} else {
|
||||||
@@ -299,7 +255,7 @@ struct TailscaleIntegrationSection: View {
|
|||||||
let trimmedPassword = self.password.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmedPassword = self.password.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let requiresPassword = self.tailscaleMode == .funnel
|
let requiresPassword = self.tailscaleMode == .funnel
|
||||||
|| (self.tailscaleMode == .serve && self.requireCredentialsForServe)
|
|| (self.tailscaleMode == .serve && self.requireCredentialsForServe)
|
||||||
if requiresPassword, self.authMode == .password, trimmedPassword.isEmpty {
|
if requiresPassword, trimmedPassword.isEmpty {
|
||||||
self.validationMessage = "Password required for this mode."
|
self.validationMessage = "Password required for this mode."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -320,22 +276,10 @@ struct TailscaleIntegrationSection: View {
|
|||||||
auth["allowTailscale"] = true
|
auth["allowTailscale"] = true
|
||||||
auth.removeValue(forKey: "mode")
|
auth.removeValue(forKey: "mode")
|
||||||
auth.removeValue(forKey: "password")
|
auth.removeValue(forKey: "password")
|
||||||
auth.removeValue(forKey: "username")
|
|
||||||
} else {
|
} else {
|
||||||
auth["allowTailscale"] = false
|
auth["allowTailscale"] = false
|
||||||
auth["mode"] = self.authMode.rawValue
|
auth["mode"] = "password"
|
||||||
if self.authMode == .password {
|
auth["password"] = trimmedPassword
|
||||||
auth["password"] = trimmedPassword
|
|
||||||
auth.removeValue(forKey: "username")
|
|
||||||
} else {
|
|
||||||
let trimmedUsername = self.username.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
if trimmedUsername.isEmpty {
|
|
||||||
auth.removeValue(forKey: "username")
|
|
||||||
} else {
|
|
||||||
auth["username"] = trimmedUsername
|
|
||||||
}
|
|
||||||
auth.removeValue(forKey: "password")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if auth.isEmpty {
|
if auth.isEmpty {
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ Defaults:
|
|||||||
mode: "local", // or "remote"
|
mode: "local", // or "remote"
|
||||||
bind: "loopback",
|
bind: "loopback",
|
||||||
// controlUi: { enabled: true }
|
// controlUi: { enabled: true }
|
||||||
// auth: { mode: "token" | "password" | "system" }
|
// auth: { mode: "token" | "password" }
|
||||||
// tailscale: { mode: "off" | "serve" | "funnel" }
|
// tailscale: { mode: "off" | "serve" | "funnel" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -291,10 +291,9 @@ Notes:
|
|||||||
- `clawdis gateway` refuses to start unless `gateway.mode` is set to `local` (or you pass the override flag).
|
- `clawdis gateway` refuses to start unless `gateway.mode` is set to `local` (or you pass the override flag).
|
||||||
|
|
||||||
Auth and Tailscale:
|
Auth and Tailscale:
|
||||||
- `gateway.auth.mode` sets the handshake requirements (`token`, `password`, or `system`/PAM).
|
- `gateway.auth.mode` sets the handshake requirements (`token` or `password`).
|
||||||
- When `gateway.auth.mode` is set, only that method is accepted (plus optional Tailscale headers).
|
- When `gateway.auth.mode` is set, only that method is accepted (plus optional Tailscale headers).
|
||||||
- `gateway.auth.password` can be set here, or via `CLAWDIS_GATEWAY_PASSWORD` (recommended).
|
- `gateway.auth.password` can be set here, or via `CLAWDIS_GATEWAY_PASSWORD` (recommended).
|
||||||
- `gateway.auth.username` defaults to the current OS user; override with `CLAWDIS_GATEWAY_USERNAME`.
|
|
||||||
- `gateway.auth.allowTailscale` controls whether Tailscale identity headers can satisfy auth.
|
- `gateway.auth.allowTailscale` controls whether Tailscale identity headers can satisfy auth.
|
||||||
- `gateway.tailscale.mode: "serve"` uses Tailscale Serve (tailnet only, loopback bind).
|
- `gateway.tailscale.mode: "serve"` uses Tailscale Serve (tailnet only, loopback bind).
|
||||||
- `gateway.tailscale.mode: "funnel"` exposes the dashboard publicly; requires auth.
|
- `gateway.tailscale.mode: "funnel"` exposes the dashboard publicly; requires auth.
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ It speaks **directly to the Gateway WebSocket** on the same port.
|
|||||||
|
|
||||||
Auth is supplied during the WebSocket handshake via:
|
Auth is supplied during the WebSocket handshake via:
|
||||||
- `connect.params.auth.token`
|
- `connect.params.auth.token`
|
||||||
- `connect.params.auth.password` (optional `username` for system/PAM)
|
- `connect.params.auth.password`
|
||||||
The dashboard settings panel lets you store a token and optional username; passwords are not persisted.
|
The dashboard settings panel lets you store a token; passwords are not persisted.
|
||||||
|
|
||||||
## What it can do (today)
|
## What it can do (today)
|
||||||
- Chat with the model via Gateway WS (`chat.history`, `chat.send`, `chat.abort`)
|
- Chat with the model via Gateway WS (`chat.history`, `chat.send`, `chat.abort`)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Tailscale provides HTTPS, routing, and (for Serve) identity headers.
|
|||||||
## Modes
|
## Modes
|
||||||
|
|
||||||
- `serve`: Tailnet-only HTTPS via `tailscale serve`. The gateway stays on `127.0.0.1`.
|
- `serve`: Tailnet-only HTTPS via `tailscale serve`. The gateway stays on `127.0.0.1`.
|
||||||
- `funnel`: Public HTTPS via `tailscale funnel`. Requires auth.
|
- `funnel`: Public HTTPS via `tailscale funnel`. Requires a shared password.
|
||||||
- `off`: Default (no Tailscale automation).
|
- `off`: Default (no Tailscale automation).
|
||||||
|
|
||||||
## Auth
|
## Auth
|
||||||
@@ -22,10 +22,9 @@ Set `gateway.auth.mode` to control the handshake:
|
|||||||
|
|
||||||
- `token` (default when `CLAWDIS_GATEWAY_TOKEN` is set)
|
- `token` (default when `CLAWDIS_GATEWAY_TOKEN` is set)
|
||||||
- `password` (shared secret via `CLAWDIS_GATEWAY_PASSWORD` or config)
|
- `password` (shared secret via `CLAWDIS_GATEWAY_PASSWORD` or config)
|
||||||
- `system` (PAM, validates your OS password)
|
|
||||||
|
|
||||||
When `tailscale.mode = "serve"`, the gateway trusts Tailscale identity headers by
|
When `tailscale.mode = "serve"`, the gateway trusts Tailscale identity headers by
|
||||||
default unless you force `gateway.auth.mode` to `password`/`system` or set
|
default unless you force `gateway.auth.mode` to `password` or set
|
||||||
`gateway.auth.allowTailscale: false`.
|
`gateway.auth.allowTailscale: false`.
|
||||||
|
|
||||||
## Config examples
|
## Config examples
|
||||||
@@ -43,20 +42,6 @@ default unless you force `gateway.auth.mode` to `password`/`system` or set
|
|||||||
|
|
||||||
Open: `https://<magicdns>/ui/`
|
Open: `https://<magicdns>/ui/`
|
||||||
|
|
||||||
### Public internet (Funnel + system password)
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
gateway: {
|
|
||||||
bind: "loopback",
|
|
||||||
tailscale: { mode: "funnel" },
|
|
||||||
auth: { mode: "system" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Open: `https://<magicdns>/ui/` (public)
|
|
||||||
|
|
||||||
### Public internet (Funnel + shared password)
|
### Public internet (Funnel + shared password)
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
@@ -75,13 +60,12 @@ Prefer `CLAWDIS_GATEWAY_PASSWORD` over committing a password to disk.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdis gateway --tailscale serve
|
clawdis gateway --tailscale serve
|
||||||
clawdis gateway --tailscale funnel --auth system
|
clawdis gateway --tailscale funnel --auth password
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Tailscale Serve/Funnel requires the `tailscale` CLI to be installed and logged in.
|
- Tailscale Serve/Funnel requires the `tailscale` CLI to be installed and logged in.
|
||||||
- System auth uses the optional `authenticate-pam` native module; install if missing.
|
- `tailscale.mode: "funnel"` refuses to start unless auth mode is `password` to avoid public exposure.
|
||||||
- `tailscale.mode: "funnel"` refuses to start without auth to avoid public exposure.
|
|
||||||
- Set `gateway.tailscale.resetOnExit` if you want Clawdis to undo `tailscale serve`
|
- Set `gateway.tailscale.resetOnExit` if you want Clawdis to undo `tailscale serve`
|
||||||
or `tailscale funnel` configuration on shutdown.
|
or `tailscale funnel` configuration on shutdown.
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ Open:
|
|||||||
gateway: {
|
gateway: {
|
||||||
bind: "loopback",
|
bind: "loopback",
|
||||||
tailscale: { mode: "funnel" },
|
tailscale: { mode: "funnel" },
|
||||||
auth: { mode: "system" } // or "password" with CLAWDIS_GATEWAY_PASSWORD
|
auth: { mode: "password" } // or CLAWDIS_GATEWAY_PASSWORD
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -94,9 +94,9 @@ Open:
|
|||||||
## Security notes
|
## Security notes
|
||||||
|
|
||||||
- Binding the Gateway to a non-loopback address **requires** auth (`CLAWDIS_GATEWAY_TOKEN` or `gateway.auth`).
|
- Binding the Gateway to a non-loopback address **requires** auth (`CLAWDIS_GATEWAY_TOKEN` or `gateway.auth`).
|
||||||
- `gateway.auth.mode: "system"` uses PAM to verify your OS password.
|
|
||||||
- The UI sends `connect.params.auth.token` or `connect.params.auth.password`.
|
- The UI sends `connect.params.auth.token` or `connect.params.auth.password`.
|
||||||
- Use `gateway.auth.allowTailscale: false` to require explicit credentials even in Serve mode.
|
- Use `gateway.auth.allowTailscale: false` to require explicit credentials even in Serve mode.
|
||||||
|
- `gateway.tailscale.mode: "funnel"` requires `gateway.auth.mode: "password"` (shared password).
|
||||||
|
|
||||||
## Building the UI
|
## Building the UI
|
||||||
|
|
||||||
|
|||||||
@@ -94,9 +94,6 @@
|
|||||||
"ws": "^8.18.3",
|
"ws": "^8.18.3",
|
||||||
"zod": "^4.2.1"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
|
||||||
"authenticate-pam": "^1.0.5"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.3.10",
|
"@biomejs/biome": "^2.3.10",
|
||||||
"@lit-labs/signals": "^0.1.3",
|
"@lit-labs/signals": "^0.1.3",
|
||||||
|
|||||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@@ -171,10 +171,6 @@ importers:
|
|||||||
wireit:
|
wireit:
|
||||||
specifier: ^0.14.12
|
specifier: ^0.14.12
|
||||||
version: 0.14.12
|
version: 0.14.12
|
||||||
optionalDependencies:
|
|
||||||
authenticate-pam:
|
|
||||||
specifier: ^1.0.5
|
|
||||||
version: 1.0.5
|
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -1242,9 +1238,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-En9AY6EG1qYqEy5L/quryzbA4akBpJrnBZNxeKTqGHC2xT9Qc4aZ8b7CcbOMFTTc/MGdoNyp+SN4zInZNKxMYA==}
|
resolution: {integrity: sha512-En9AY6EG1qYqEy5L/quryzbA4akBpJrnBZNxeKTqGHC2xT9Qc4aZ8b7CcbOMFTTc/MGdoNyp+SN4zInZNKxMYA==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
authenticate-pam@1.0.5:
|
|
||||||
resolution: {integrity: sha512-zaPml3/19Sa3XLewuOoUNsxwnNz13mTNoO4Q09vr93cjTrH0dwXOU49Bcetk/XWl22bw9zO9WovSKkddGvBEsQ==}
|
|
||||||
|
|
||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
@@ -1990,9 +1983,6 @@ packages:
|
|||||||
mz@2.7.0:
|
mz@2.7.0:
|
||||||
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
||||||
|
|
||||||
nan@2.24.0:
|
|
||||||
resolution: {integrity: sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==}
|
|
||||||
|
|
||||||
nanoid@3.3.11:
|
nanoid@3.3.11:
|
||||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
@@ -3629,11 +3619,6 @@ snapshots:
|
|||||||
audio-type@2.2.1:
|
audio-type@2.2.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
authenticate-pam@1.0.5:
|
|
||||||
dependencies:
|
|
||||||
nan: 2.24.0
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
balanced-match@3.0.1: {}
|
balanced-match@3.0.1: {}
|
||||||
@@ -4421,9 +4406,6 @@ snapshots:
|
|||||||
object-assign: 4.1.1
|
object-assign: 4.1.1
|
||||||
thenify-all: 1.6.0
|
thenify-all: 1.6.0
|
||||||
|
|
||||||
nan@2.24.0:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
negotiator@1.0.0: {}
|
negotiator@1.0.0: {}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { forceFreePortAndWait } from "./ports.js";
|
|||||||
type GatewayRpcOpts = {
|
type GatewayRpcOpts = {
|
||||||
url?: string;
|
url?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
username?: string;
|
|
||||||
password?: string;
|
password?: string;
|
||||||
timeout?: string;
|
timeout?: string;
|
||||||
expectFinal?: boolean;
|
expectFinal?: boolean;
|
||||||
@@ -30,8 +29,7 @@ const gatewayCallOpts = (cmd: Command) =>
|
|||||||
cmd
|
cmd
|
||||||
.option("--url <url>", "Gateway WebSocket URL", "ws://127.0.0.1:18789")
|
.option("--url <url>", "Gateway WebSocket URL", "ws://127.0.0.1:18789")
|
||||||
.option("--token <token>", "Gateway token (if required)")
|
.option("--token <token>", "Gateway token (if required)")
|
||||||
.option("--username <username>", "Gateway username (system auth)")
|
.option("--password <password>", "Gateway password (password auth)")
|
||||||
.option("--password <password>", "Gateway password (password/system auth)")
|
|
||||||
.option("--timeout <ms>", "Timeout in ms", "10000")
|
.option("--timeout <ms>", "Timeout in ms", "10000")
|
||||||
.option("--expect-final", "Wait for final response (agent)", false);
|
.option("--expect-final", "Wait for final response (agent)", false);
|
||||||
|
|
||||||
@@ -43,7 +41,6 @@ const callGatewayCli = async (
|
|||||||
callGateway({
|
callGateway({
|
||||||
url: opts.url,
|
url: opts.url,
|
||||||
token: opts.token,
|
token: opts.token,
|
||||||
username: opts.username,
|
|
||||||
password: opts.password,
|
password: opts.password,
|
||||||
method,
|
method,
|
||||||
params,
|
params,
|
||||||
@@ -66,9 +63,8 @@ export function registerGatewayCli(program: Command) {
|
|||||||
"--token <token>",
|
"--token <token>",
|
||||||
"Shared token required in connect.params.auth.token (default: CLAWDIS_GATEWAY_TOKEN env if set)",
|
"Shared token required in connect.params.auth.token (default: CLAWDIS_GATEWAY_TOKEN env if set)",
|
||||||
)
|
)
|
||||||
.option("--auth <mode>", 'Gateway auth mode ("token"|"password"|"system")')
|
.option("--auth <mode>", 'Gateway auth mode ("token"|"password")')
|
||||||
.option("--password <password>", "Password for auth mode=password")
|
.option("--password <password>", "Password for auth mode=password")
|
||||||
.option("--username <username>", "Default username for system auth")
|
|
||||||
.option(
|
.option(
|
||||||
"--tailscale <mode>",
|
"--tailscale <mode>",
|
||||||
'Tailscale exposure mode ("off"|"serve"|"funnel")',
|
'Tailscale exposure mode ("off"|"serve"|"funnel")',
|
||||||
@@ -121,13 +117,12 @@ export function registerGatewayCli(program: Command) {
|
|||||||
const authModeRaw = opts.auth ? String(opts.auth) : undefined;
|
const authModeRaw = opts.auth ? String(opts.auth) : undefined;
|
||||||
const authMode =
|
const authMode =
|
||||||
authModeRaw === "token" ||
|
authModeRaw === "token" ||
|
||||||
authModeRaw === "password" ||
|
authModeRaw === "password"
|
||||||
authModeRaw === "system"
|
|
||||||
? authModeRaw
|
? authModeRaw
|
||||||
: null;
|
: null;
|
||||||
if (authModeRaw && !authMode) {
|
if (authModeRaw && !authMode) {
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
'Invalid --auth (use "token", "password", or "system")',
|
'Invalid --auth (use "token" or "password")',
|
||||||
);
|
);
|
||||||
defaultRuntime.exit(1);
|
defaultRuntime.exit(1);
|
||||||
return;
|
return;
|
||||||
@@ -205,11 +200,10 @@ export function registerGatewayCli(program: Command) {
|
|||||||
server = await startGatewayServer(port, {
|
server = await startGatewayServer(port, {
|
||||||
bind,
|
bind,
|
||||||
auth:
|
auth:
|
||||||
authMode || opts.password || opts.username || authModeRaw
|
authMode || opts.password || authModeRaw
|
||||||
? {
|
? {
|
||||||
mode: authMode ?? undefined,
|
mode: authMode ?? undefined,
|
||||||
password: opts.password ? String(opts.password) : undefined,
|
password: opts.password ? String(opts.password) : undefined,
|
||||||
username: opts.username ? String(opts.username) : undefined,
|
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
tailscale:
|
tailscale:
|
||||||
@@ -245,9 +239,8 @@ export function registerGatewayCli(program: Command) {
|
|||||||
"--token <token>",
|
"--token <token>",
|
||||||
"Shared token required in connect.params.auth.token (default: CLAWDIS_GATEWAY_TOKEN env if set)",
|
"Shared token required in connect.params.auth.token (default: CLAWDIS_GATEWAY_TOKEN env if set)",
|
||||||
)
|
)
|
||||||
.option("--auth <mode>", 'Gateway auth mode ("token"|"password"|"system")')
|
.option("--auth <mode>", 'Gateway auth mode ("token"|"password")')
|
||||||
.option("--password <password>", "Password for auth mode=password")
|
.option("--password <password>", "Password for auth mode=password")
|
||||||
.option("--username <username>", "Default username for system auth")
|
|
||||||
.option(
|
.option(
|
||||||
"--tailscale <mode>",
|
"--tailscale <mode>",
|
||||||
'Tailscale exposure mode ("off"|"serve"|"funnel")',
|
'Tailscale exposure mode ("off"|"serve"|"funnel")',
|
||||||
@@ -342,13 +335,12 @@ export function registerGatewayCli(program: Command) {
|
|||||||
const authModeRaw = opts.auth ? String(opts.auth) : undefined;
|
const authModeRaw = opts.auth ? String(opts.auth) : undefined;
|
||||||
const authMode =
|
const authMode =
|
||||||
authModeRaw === "token" ||
|
authModeRaw === "token" ||
|
||||||
authModeRaw === "password" ||
|
authModeRaw === "password"
|
||||||
authModeRaw === "system"
|
|
||||||
? authModeRaw
|
? authModeRaw
|
||||||
: null;
|
: null;
|
||||||
if (authModeRaw && !authMode) {
|
if (authModeRaw && !authMode) {
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
'Invalid --auth (use "token", "password", or "system")',
|
'Invalid --auth (use "token" or "password")',
|
||||||
);
|
);
|
||||||
defaultRuntime.exit(1);
|
defaultRuntime.exit(1);
|
||||||
return;
|
return;
|
||||||
@@ -443,11 +435,10 @@ export function registerGatewayCli(program: Command) {
|
|||||||
server = await startGatewayServer(port, {
|
server = await startGatewayServer(port, {
|
||||||
bind,
|
bind,
|
||||||
auth:
|
auth:
|
||||||
authMode || opts.password || opts.username || authModeRaw
|
authMode || opts.password || authModeRaw
|
||||||
? {
|
? {
|
||||||
mode: authMode ?? undefined,
|
mode: authMode ?? undefined,
|
||||||
password: opts.password ? String(opts.password) : undefined,
|
password: opts.password ? String(opts.password) : undefined,
|
||||||
username: opts.username ? String(opts.username) : undefined,
|
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
tailscale:
|
tailscale:
|
||||||
|
|||||||
@@ -115,13 +115,11 @@ export type GatewayControlUiConfig = {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GatewayAuthMode = "token" | "password" | "system";
|
export type GatewayAuthMode = "token" | "password";
|
||||||
|
|
||||||
export type GatewayAuthConfig = {
|
export type GatewayAuthConfig = {
|
||||||
/** Authentication mode for Gateway connections. Defaults to token when set. */
|
/** Authentication mode for Gateway connections. Defaults to token when set. */
|
||||||
mode?: GatewayAuthMode;
|
mode?: GatewayAuthMode;
|
||||||
/** Username for system auth (PAM). Defaults to current user. */
|
|
||||||
username?: string;
|
|
||||||
/** Shared password for password mode (consider env instead). */
|
/** Shared password for password mode (consider env instead). */
|
||||||
password?: string;
|
password?: string;
|
||||||
/** Allow Tailscale identity headers when serve mode is enabled. */
|
/** Allow Tailscale identity headers when serve mode is enabled. */
|
||||||
@@ -522,10 +520,8 @@ const ClawdisSchema = z.object({
|
|||||||
.union([
|
.union([
|
||||||
z.literal("token"),
|
z.literal("token"),
|
||||||
z.literal("password"),
|
z.literal("password"),
|
||||||
z.literal("system"),
|
|
||||||
])
|
])
|
||||||
.optional(),
|
.optional(),
|
||||||
username: z.string().optional(),
|
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
allowTailscale: z.boolean().optional(),
|
allowTailscale: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,29 +1,23 @@
|
|||||||
import { timingSafeEqual } from "node:crypto";
|
import { timingSafeEqual } from "node:crypto";
|
||||||
import type { IncomingMessage } from "node:http";
|
import type { IncomingMessage } from "node:http";
|
||||||
import os from "node:os";
|
export type ResolvedGatewayAuthMode = "none" | "token" | "password";
|
||||||
|
|
||||||
import { type PamAvailability, verifyPamCredentials } from "../infra/pam.js";
|
|
||||||
|
|
||||||
export type ResolvedGatewayAuthMode = "none" | "token" | "password" | "system";
|
|
||||||
|
|
||||||
export type ResolvedGatewayAuth = {
|
export type ResolvedGatewayAuth = {
|
||||||
mode: ResolvedGatewayAuthMode;
|
mode: ResolvedGatewayAuthMode;
|
||||||
token?: string;
|
token?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
username?: string;
|
|
||||||
allowTailscale: boolean;
|
allowTailscale: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GatewayAuthResult = {
|
export type GatewayAuthResult = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
method?: "none" | "token" | "password" | "system" | "tailscale";
|
method?: "none" | "token" | "password" | "tailscale";
|
||||||
user?: string;
|
user?: string;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ConnectAuth = {
|
type ConnectAuth = {
|
||||||
token?: string;
|
token?: string;
|
||||||
username?: string;
|
|
||||||
password?: string;
|
password?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,10 +98,7 @@ function isTailscaleProxyRequest(req?: IncomingMessage): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertGatewayAuthConfigured(
|
export function assertGatewayAuthConfigured(auth: ResolvedGatewayAuth): void {
|
||||||
auth: ResolvedGatewayAuth,
|
|
||||||
pam: PamAvailability,
|
|
||||||
): void {
|
|
||||||
if (auth.mode === "token" && !auth.token) {
|
if (auth.mode === "token" && !auth.token) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"gateway auth mode is token, but CLAWDIS_GATEWAY_TOKEN is not set",
|
"gateway auth mode is token, but CLAWDIS_GATEWAY_TOKEN is not set",
|
||||||
@@ -118,13 +109,6 @@ export function assertGatewayAuthConfigured(
|
|||||||
"gateway auth mode is password, but no password was configured",
|
"gateway auth mode is password, but no password was configured",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (auth.mode === "system" && !pam.available) {
|
|
||||||
throw new Error(
|
|
||||||
`gateway auth mode is system, but PAM auth is unavailable${
|
|
||||||
pam.error ? `: ${pam.error}` : ""
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function authorizeGatewayConnect(params: {
|
export async function authorizeGatewayConnect(params: {
|
||||||
@@ -170,21 +154,6 @@ export async function authorizeGatewayConnect(params: {
|
|||||||
return { ok: true, method: "password" };
|
return { ok: true, method: "password" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth.mode === "system") {
|
|
||||||
const password = connectAuth?.password;
|
|
||||||
if (!password) return { ok: false, reason: "unauthorized" };
|
|
||||||
const username = (
|
|
||||||
connectAuth?.username ??
|
|
||||||
auth.username ??
|
|
||||||
os.userInfo().username
|
|
||||||
).trim();
|
|
||||||
if (!username) return { ok: false, reason: "unauthorized" };
|
|
||||||
const ok = await verifyPamCredentials(username, password);
|
|
||||||
return ok
|
|
||||||
? { ok: true, method: "system", user: username }
|
|
||||||
: { ok: false, reason: "unauthorized" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auth.allowTailscale) {
|
if (auth.allowTailscale) {
|
||||||
const tailscaleUser = getTailscaleUser(req);
|
const tailscaleUser = getTailscaleUser(req);
|
||||||
if (tailscaleUser && isTailscaleProxyRequest(req)) {
|
if (tailscaleUser && isTailscaleProxyRequest(req)) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { PROTOCOL_VERSION } from "./protocol/index.js";
|
|||||||
export type CallGatewayOptions = {
|
export type CallGatewayOptions = {
|
||||||
url?: string;
|
url?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
username?: string;
|
|
||||||
password?: string;
|
password?: string;
|
||||||
method: string;
|
method: string;
|
||||||
params?: unknown;
|
params?: unknown;
|
||||||
@@ -37,7 +36,6 @@ export async function callGateway<T = unknown>(
|
|||||||
const client = new GatewayClient({
|
const client = new GatewayClient({
|
||||||
url: opts.url,
|
url: opts.url,
|
||||||
token: opts.token,
|
token: opts.token,
|
||||||
username: opts.username,
|
|
||||||
password: opts.password,
|
password: opts.password,
|
||||||
instanceId: opts.instanceId ?? randomUUID(),
|
instanceId: opts.instanceId ?? randomUUID(),
|
||||||
clientName: opts.clientName ?? "cli",
|
clientName: opts.clientName ?? "cli",
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ type Pending = {
|
|||||||
export type GatewayClientOptions = {
|
export type GatewayClientOptions = {
|
||||||
url?: string; // ws://127.0.0.1:18789
|
url?: string; // ws://127.0.0.1:18789
|
||||||
token?: string;
|
token?: string;
|
||||||
username?: string;
|
|
||||||
password?: string;
|
password?: string;
|
||||||
instanceId?: string;
|
instanceId?: string;
|
||||||
clientName?: string;
|
clientName?: string;
|
||||||
@@ -86,10 +85,9 @@ export class GatewayClient {
|
|||||||
|
|
||||||
private sendConnect() {
|
private sendConnect() {
|
||||||
const auth =
|
const auth =
|
||||||
this.opts.token || this.opts.password || this.opts.username
|
this.opts.token || this.opts.password
|
||||||
? {
|
? {
|
||||||
token: this.opts.token,
|
token: this.opts.token,
|
||||||
username: this.opts.username,
|
|
||||||
password: this.opts.password,
|
password: this.opts.password,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ export const ConnectParamsSchema = Type.Object(
|
|||||||
Type.Object(
|
Type.Object(
|
||||||
{
|
{
|
||||||
token: Type.Optional(Type.String()),
|
token: Type.Optional(Type.String()),
|
||||||
username: Type.Optional(Type.String()),
|
|
||||||
password: Type.Optional(Type.String()),
|
password: Type.Optional(Type.String()),
|
||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
|
|||||||
@@ -338,7 +338,6 @@ async function connectReq(
|
|||||||
ws: WebSocket,
|
ws: WebSocket,
|
||||||
opts?: {
|
opts?: {
|
||||||
token?: string;
|
token?: string;
|
||||||
username?: string;
|
|
||||||
password?: string;
|
password?: string;
|
||||||
minProtocol?: number;
|
minProtocol?: number;
|
||||||
maxProtocol?: number;
|
maxProtocol?: number;
|
||||||
@@ -368,10 +367,9 @@ async function connectReq(
|
|||||||
},
|
},
|
||||||
caps: [],
|
caps: [],
|
||||||
auth:
|
auth:
|
||||||
opts?.token || opts?.password || opts?.username
|
opts?.token || opts?.password
|
||||||
? {
|
? {
|
||||||
token: opts?.token,
|
token: opts?.token,
|
||||||
username: opts?.username,
|
|
||||||
password: opts?.password,
|
password: opts?.password,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ import {
|
|||||||
requestNodePairing,
|
requestNodePairing,
|
||||||
verifyNodeToken,
|
verifyNodeToken,
|
||||||
} from "../infra/node-pairing.js";
|
} from "../infra/node-pairing.js";
|
||||||
import { getPamAvailability } from "../infra/pam.js";
|
|
||||||
import { ensureClawdisCliOnPath } from "../infra/path-env.js";
|
import { ensureClawdisCliOnPath } from "../infra/path-env.js";
|
||||||
import {
|
import {
|
||||||
enqueueSystemEvent,
|
enqueueSystemEvent,
|
||||||
@@ -1211,30 +1210,24 @@ export async function startGatewayServer(
|
|||||||
const token = getGatewayToken();
|
const token = getGatewayToken();
|
||||||
const password =
|
const password =
|
||||||
authConfig.password ?? process.env.CLAWDIS_GATEWAY_PASSWORD ?? undefined;
|
authConfig.password ?? process.env.CLAWDIS_GATEWAY_PASSWORD ?? undefined;
|
||||||
const username =
|
|
||||||
authConfig.username ?? process.env.CLAWDIS_GATEWAY_USERNAME ?? undefined;
|
|
||||||
const authMode: ResolvedGatewayAuth["mode"] =
|
const authMode: ResolvedGatewayAuth["mode"] =
|
||||||
authConfig.mode ?? (password ? "password" : token ? "token" : "none");
|
authConfig.mode ?? (password ? "password" : token ? "token" : "none");
|
||||||
const allowTailscale =
|
const allowTailscale =
|
||||||
authConfig.allowTailscale ??
|
authConfig.allowTailscale ??
|
||||||
(tailscaleMode === "serve" &&
|
(tailscaleMode === "serve" && authMode !== "password");
|
||||||
authMode !== "password" &&
|
|
||||||
authMode !== "system");
|
|
||||||
const resolvedAuth: ResolvedGatewayAuth = {
|
const resolvedAuth: ResolvedGatewayAuth = {
|
||||||
mode: authMode,
|
mode: authMode,
|
||||||
token,
|
token,
|
||||||
password,
|
password,
|
||||||
username,
|
|
||||||
allowTailscale,
|
allowTailscale,
|
||||||
};
|
};
|
||||||
const canvasHostEnabled =
|
const canvasHostEnabled =
|
||||||
process.env.CLAWDIS_SKIP_CANVAS_HOST !== "1" &&
|
process.env.CLAWDIS_SKIP_CANVAS_HOST !== "1" &&
|
||||||
cfgAtStart.canvasHost?.enabled !== false;
|
cfgAtStart.canvasHost?.enabled !== false;
|
||||||
const pamAvailability = await getPamAvailability();
|
assertGatewayAuthConfigured(resolvedAuth);
|
||||||
assertGatewayAuthConfigured(resolvedAuth, pamAvailability);
|
if (tailscaleMode === "funnel" && authMode !== "password") {
|
||||||
if (tailscaleMode === "funnel" && authMode === "none") {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"tailscale funnel requires gateway auth (set gateway.auth or CLAWDIS_GATEWAY_TOKEN)",
|
"tailscale funnel requires gateway auth mode=password (set gateway.auth.password or CLAWDIS_GATEWAY_PASSWORD)",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (tailscaleMode !== "off" && !isLoopbackHost(bindHost)) {
|
if (tailscaleMode !== "off" && !isLoopbackHost(bindHost)) {
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
type PamAuthenticate = (
|
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
callback: (err: Error | null) => void,
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
let pamAuth: PamAuthenticate | null | undefined;
|
|
||||||
let pamError: string | null = null;
|
|
||||||
|
|
||||||
async function loadPam(): Promise<void> {
|
|
||||||
if (pamAuth !== undefined) return;
|
|
||||||
try {
|
|
||||||
// Vite/Vitest: avoid static analysis/bundling for optional native deps.
|
|
||||||
const pkgName = "authenticate-pam";
|
|
||||||
const mod = (await import(pkgName)) as
|
|
||||||
| { authenticate?: PamAuthenticate; default?: PamAuthenticate }
|
|
||||||
| PamAuthenticate;
|
|
||||||
const candidate =
|
|
||||||
typeof mod === "function"
|
|
||||||
? mod
|
|
||||||
: typeof (mod as { authenticate?: PamAuthenticate }).authenticate ===
|
|
||||||
"function"
|
|
||||||
? (mod as { authenticate: PamAuthenticate }).authenticate
|
|
||||||
: typeof (mod as { default?: PamAuthenticate }).default === "function"
|
|
||||||
? (mod as { default: PamAuthenticate }).default
|
|
||||||
: null;
|
|
||||||
if (!candidate) {
|
|
||||||
throw new Error(
|
|
||||||
"authenticate-pam did not export an authenticate function",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
pamAuth = candidate;
|
|
||||||
} catch (err) {
|
|
||||||
pamAuth = null;
|
|
||||||
pamError = err instanceof Error ? err.message : String(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PamAvailability = {
|
|
||||||
available: boolean;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getPamAvailability(): Promise<PamAvailability> {
|
|
||||||
await loadPam();
|
|
||||||
return pamAuth
|
|
||||||
? { available: true }
|
|
||||||
: { available: false, error: pamError ?? undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function verifyPamCredentials(
|
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
await loadPam();
|
|
||||||
const auth = pamAuth;
|
|
||||||
if (!auth) return false;
|
|
||||||
return await new Promise<boolean>((resolve) => {
|
|
||||||
auth(username, password, (err) => resolve(!err));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -48,7 +48,7 @@ export type EventLogEntry = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type AppViewState = {
|
export type AppViewState = {
|
||||||
settings: { gatewayUrl: string; token: string; username: string; sessionKey: string };
|
settings: { gatewayUrl: string; token: string; sessionKey: string };
|
||||||
password: string;
|
password: string;
|
||||||
tab: Tab;
|
tab: Tab;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
|
|||||||
@@ -188,9 +188,6 @@ export class ClawdisApp extends LitElement {
|
|||||||
this.client = new GatewayBrowserClient({
|
this.client = new GatewayBrowserClient({
|
||||||
url: this.settings.gatewayUrl,
|
url: this.settings.gatewayUrl,
|
||||||
token: this.settings.token.trim() ? this.settings.token : undefined,
|
token: this.settings.token.trim() ? this.settings.token : undefined,
|
||||||
username: this.settings.username.trim()
|
|
||||||
? this.settings.username.trim()
|
|
||||||
: undefined,
|
|
||||||
password: this.password.trim() ? this.password : undefined,
|
password: this.password.trim() ? this.password : undefined,
|
||||||
clientName: "clawdis-control-ui",
|
clientName: "clawdis-control-ui",
|
||||||
mode: "webchat",
|
mode: "webchat",
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ type Pending = {
|
|||||||
export type GatewayBrowserClientOptions = {
|
export type GatewayBrowserClientOptions = {
|
||||||
url: string;
|
url: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
username?: string;
|
|
||||||
password?: string;
|
password?: string;
|
||||||
clientName?: string;
|
clientName?: string;
|
||||||
clientVersion?: string;
|
clientVersion?: string;
|
||||||
@@ -99,10 +98,9 @@ export class GatewayBrowserClient {
|
|||||||
|
|
||||||
private sendConnect() {
|
private sendConnect() {
|
||||||
const auth =
|
const auth =
|
||||||
this.opts.token || this.opts.password || this.opts.username
|
this.opts.token || this.opts.password
|
||||||
? {
|
? {
|
||||||
token: this.opts.token,
|
token: this.opts.token,
|
||||||
username: this.opts.username,
|
|
||||||
password: this.opts.password,
|
password: this.opts.password,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ const KEY = "clawdis.control.settings.v1";
|
|||||||
export type UiSettings = {
|
export type UiSettings = {
|
||||||
gatewayUrl: string;
|
gatewayUrl: string;
|
||||||
token: string;
|
token: string;
|
||||||
username: string;
|
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -16,7 +15,6 @@ export function loadSettings(): UiSettings {
|
|||||||
const defaults: UiSettings = {
|
const defaults: UiSettings = {
|
||||||
gatewayUrl: defaultUrl,
|
gatewayUrl: defaultUrl,
|
||||||
token: "",
|
token: "",
|
||||||
username: "",
|
|
||||||
sessionKey: "main",
|
sessionKey: "main",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,8 +28,6 @@ export function loadSettings(): UiSettings {
|
|||||||
? parsed.gatewayUrl.trim()
|
? parsed.gatewayUrl.trim()
|
||||||
: defaults.gatewayUrl,
|
: defaults.gatewayUrl,
|
||||||
token: typeof parsed.token === "string" ? parsed.token : defaults.token,
|
token: typeof parsed.token === "string" ? parsed.token : defaults.token,
|
||||||
username:
|
|
||||||
typeof parsed.username === "string" ? parsed.username : defaults.username,
|
|
||||||
sessionKey:
|
sessionKey:
|
||||||
typeof parsed.sessionKey === "string" && parsed.sessionKey.trim()
|
typeof parsed.sessionKey === "string" && parsed.sessionKey.trim()
|
||||||
? parsed.sessionKey.trim()
|
? parsed.sessionKey.trim()
|
||||||
|
|||||||
@@ -59,17 +59,6 @@ export function renderOverview(props: OverviewProps) {
|
|||||||
placeholder="CLAWDIS_GATEWAY_TOKEN"
|
placeholder="CLAWDIS_GATEWAY_TOKEN"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
|
||||||
<span>Username (system auth)</span>
|
|
||||||
<input
|
|
||||||
.value=${props.settings.username}
|
|
||||||
@input=${(e: Event) => {
|
|
||||||
const v = (e.target as HTMLInputElement).value;
|
|
||||||
props.onSettingsChange({ ...props.settings, username: v });
|
|
||||||
}}
|
|
||||||
placeholder="optional"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Password (not stored)</span>
|
<span>Password (not stored)</span>
|
||||||
<input
|
<input
|
||||||
|
|||||||
Reference in New Issue
Block a user