@chrysb/alphaclaw 0.1.20 → 0.1.21

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.
@@ -67,6 +67,10 @@
67
67
  </form>
68
68
  </div>
69
69
  <script>
70
+ try {
71
+ window.localStorage?.clear?.();
72
+ } catch {}
73
+
70
74
  const formEl = document.getElementById("login-form");
71
75
  const passwordEl = document.getElementById("password");
72
76
  const submitButtonEl = document.getElementById("submit-btn");
@@ -1,10 +1,44 @@
1
1
  const crypto = require("crypto");
2
- const path = require("path");
3
2
  const { kLoginCleanupIntervalMs } = require("../constants");
4
3
 
5
4
  const registerAuthRoutes = ({ app, loginThrottle }) => {
6
5
  const SETUP_PASSWORD = process.env.SETUP_PASSWORD || "";
7
- const kAuthTokens = new Set();
6
+ const kSessionTtlMs = 7 * 24 * 60 * 60 * 1000;
7
+
8
+ const signSessionPayload = (payload) =>
9
+ crypto.createHmac("sha256", SETUP_PASSWORD).update(payload).digest("base64url");
10
+
11
+ const createSessionToken = () => {
12
+ const now = Date.now();
13
+ const payload = Buffer.from(
14
+ JSON.stringify({
15
+ iat: now,
16
+ exp: now + kSessionTtlMs,
17
+ nonce: crypto.randomBytes(16).toString("hex"),
18
+ }),
19
+ ).toString("base64url");
20
+ const signature = signSessionPayload(payload);
21
+ return `${payload}.${signature}`;
22
+ };
23
+
24
+ const verifySessionToken = (token) => {
25
+ if (!SETUP_PASSWORD || !token || typeof token !== "string") return false;
26
+ const parts = token.split(".");
27
+ if (parts.length !== 2) return false;
28
+ const [payload, signature] = parts;
29
+ if (!payload || !signature) return false;
30
+ const expectedSignature = signSessionPayload(payload);
31
+ const expectedBuffer = Buffer.from(expectedSignature);
32
+ const signatureBuffer = Buffer.from(signature);
33
+ if (expectedBuffer.length !== signatureBuffer.length) return false;
34
+ if (!crypto.timingSafeEqual(expectedBuffer, signatureBuffer)) return false;
35
+ try {
36
+ const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
37
+ return Number.isFinite(parsed?.exp) && parsed.exp > Date.now();
38
+ } catch {
39
+ return false;
40
+ }
41
+ };
8
42
 
9
43
  const cookieParser = (req) => {
10
44
  const cookies = {};
@@ -43,12 +77,12 @@ const registerAuthRoutes = ({ app, loginThrottle }) => {
43
77
  return res.status(401).json({ ok: false, error: "Invalid credentials" });
44
78
  }
45
79
  loginThrottle.recordLoginSuccess(clientKey);
46
- const token = crypto.randomBytes(32).toString("hex");
47
- kAuthTokens.add(token);
80
+ const token = createSessionToken();
48
81
  res.cookie("setup_token", token, {
49
82
  httpOnly: true,
50
83
  sameSite: "lax",
51
84
  path: "/",
85
+ maxAge: kSessionTtlMs,
52
86
  });
53
87
  res.json({ ok: true });
54
88
  });
@@ -57,24 +91,42 @@ const registerAuthRoutes = ({ app, loginThrottle }) => {
57
91
  loginThrottle.cleanupLoginAttemptStates();
58
92
  }, kLoginCleanupIntervalMs).unref();
59
93
 
94
+ const isAuthorizedRequest = (req) => {
95
+ if (!SETUP_PASSWORD) return true;
96
+ const requestPath = req.path || "";
97
+ if (requestPath.startsWith("/auth/google/callback")) return true;
98
+ if (requestPath.startsWith("/auth/codex/callback")) return true;
99
+ const cookies = cookieParser(req);
100
+ const query = req.query || {};
101
+ const token = cookies.setup_token || query.token;
102
+ return verifySessionToken(token);
103
+ };
104
+
60
105
  const requireAuth = (req, res, next) => {
61
106
  if (!SETUP_PASSWORD) return next();
62
107
  if (req.path.startsWith("/auth/google/callback")) return next();
63
108
  if (req.path.startsWith("/auth/codex/callback")) return next();
64
- const cookies = cookieParser(req);
65
- const token = cookies.setup_token || req.query.token;
66
- if (token && kAuthTokens.has(token)) return next();
109
+ if (isAuthorizedRequest(req)) return next();
67
110
  if (req.path.startsWith("/api/")) {
68
111
  return res.status(401).json({ error: "Unauthorized" });
69
112
  }
70
- return res.sendFile(path.join(__dirname, "..", "..", "public", "login.html"));
113
+ return res.redirect("/login.html");
71
114
  };
72
115
 
116
+ app.get("/api/auth/status", (req, res) => {
117
+ res.json({ authEnabled: !!SETUP_PASSWORD });
118
+ });
119
+
120
+ app.post("/api/auth/logout", (req, res) => {
121
+ res.clearCookie("setup_token", { path: "/" });
122
+ res.json({ ok: true });
123
+ });
124
+
73
125
  app.use("/setup", requireAuth);
74
126
  app.use("/api", requireAuth);
75
127
  app.use("/auth", requireAuth);
76
128
 
77
- return { requireAuth };
129
+ return { requireAuth, isAuthorizedRequest };
78
130
  };
79
131
 
80
132
  module.exports = { registerAuthRoutes };
@@ -63,7 +63,6 @@ const registerGoogleRoutes = ({
63
63
  services,
64
64
  activeScopes,
65
65
  };
66
- console.log(`[alphaclaw] Google status: ${JSON.stringify(status)}`);
67
66
  res.json(status);
68
67
  });
69
68
 
@@ -1,13 +1,13 @@
1
- const registerProxyRoutes = ({ app, proxy, SETUP_API_PREFIXES }) => {
2
- app.all("/openclaw", (req, res) => {
1
+ const registerProxyRoutes = ({ app, proxy, SETUP_API_PREFIXES, requireAuth }) => {
2
+ app.all("/openclaw", requireAuth, (req, res) => {
3
3
  req.url = "/";
4
4
  proxy.web(req, res);
5
5
  });
6
- app.all("/openclaw/*", (req, res) => {
6
+ app.all("/openclaw/*", requireAuth, (req, res) => {
7
7
  req.url = req.url.replace(/^\/openclaw/, "");
8
8
  proxy.web(req, res);
9
9
  });
10
- app.all("/assets/*", (req, res) => proxy.web(req, res));
10
+ app.all("/assets/*", requireAuth, (req, res) => proxy.web(req, res));
11
11
 
12
12
  app.all("/webhook/*", (req, res) => {
13
13
  if (!req.headers.authorization && req.query.token) {
@@ -18,7 +18,20 @@ const registerSystemRoutes = ({
18
18
  OPENCLAW_DIR,
19
19
  }) => {
20
20
  let envRestartPending = false;
21
- const kEnvVarsHiddenFromEnvarsTab = new Set(["GITHUB_WORKSPACE_REPO"]);
21
+ const kEnvVarsReservedForUserInput = new Set([
22
+ "GITHUB_WORKSPACE_REPO",
23
+ "GOG_KEYRING_PASSWORD",
24
+ "ALPHACLAW_ROOT_DIR",
25
+ "OPENCLAW_HOME",
26
+ "OPENCLAW_CONFIG_PATH",
27
+ "XDG_CONFIG_HOME",
28
+ ]);
29
+ const kReservedUserEnvVarKeys = Array.from(
30
+ new Set([...kSystemVars, ...kEnvVarsReservedForUserInput]),
31
+ );
32
+ const isReservedUserEnvVar = (key) =>
33
+ kSystemVars.has(key)
34
+ || kEnvVarsReservedForUserInput.has(key);
22
35
  const kSystemCronPath = "/etc/cron.d/openclaw-hourly-sync";
23
36
  const kSystemCronConfigPath = `${OPENCLAW_DIR}/cron/system-sync.json`;
24
37
  const kSystemCronScriptPath = `${OPENCLAW_DIR}/hourly-git-sync.sh`;
@@ -72,7 +85,7 @@ const registerSystemRoutes = ({
72
85
  const merged = [];
73
86
 
74
87
  for (const def of kKnownVars) {
75
- if (kEnvVarsHiddenFromEnvarsTab.has(def.key)) continue;
88
+ if (isReservedUserEnvVar(def.key)) continue;
76
89
  const fileEntry = fileVars.find((v) => v.key === def.key);
77
90
  const value = fileEntry?.value || "";
78
91
  merged.push({
@@ -87,7 +100,7 @@ const registerSystemRoutes = ({
87
100
  }
88
101
 
89
102
  for (const v of fileVars) {
90
- if (kKnownKeys.has(v.key) || kSystemVars.has(v.key)) continue;
103
+ if (kKnownKeys.has(v.key) || isReservedUserEnvVar(v.key)) continue;
91
104
  merged.push({
92
105
  key: v.key,
93
106
  value: v.value,
@@ -99,7 +112,11 @@ const registerSystemRoutes = ({
99
112
  });
100
113
  }
101
114
 
102
- res.json({ vars: merged, restartRequired: envRestartPending && isOnboarded() });
115
+ res.json({
116
+ vars: merged,
117
+ reservedKeys: kReservedUserEnvVarKeys,
118
+ restartRequired: envRestartPending && isOnboarded(),
119
+ });
103
120
  });
104
121
 
105
122
  app.put("/api/env", (req, res) => {
@@ -108,14 +125,22 @@ const registerSystemRoutes = ({
108
125
  return res.status(400).json({ ok: false, error: "Missing vars array" });
109
126
  }
110
127
 
111
- const filtered = vars.filter(
112
- (v) =>
113
- !kSystemVars.has(v.key) &&
114
- !kEnvVarsHiddenFromEnvarsTab.has(v.key),
115
- );
116
- const existingLockedVars = readEnvFile().filter((v) =>
117
- kEnvVarsHiddenFromEnvarsTab.has(v.key),
128
+ const blockedKeys = Array.from(
129
+ new Set(
130
+ vars
131
+ .map((v) => String(v?.key || "").trim())
132
+ .filter((key) => key && isReservedUserEnvVar(key)),
133
+ ),
118
134
  );
135
+ if (blockedKeys.length) {
136
+ return res.status(400).json({
137
+ ok: false,
138
+ error: `Reserved environment variables cannot be edited: ${blockedKeys.join(", ")}`,
139
+ });
140
+ }
141
+
142
+ const filtered = vars.filter((v) => !isReservedUserEnvVar(v.key));
143
+ const existingLockedVars = readEnvFile().filter((v) => isReservedUserEnvVar(v.key));
119
144
  const nextEnvVars = [...filtered, ...existingLockedVars];
120
145
  syncChannelConfig(nextEnvVars, "remove");
121
146
  writeEnvFile(nextEnvVars);
package/lib/server.js CHANGED
@@ -79,7 +79,10 @@ const openclawVersionService = createOpenclawVersionService({
79
79
  });
80
80
  const alphaclawVersionService = createAlphaclawVersionService();
81
81
 
82
- const { requireAuth } = registerAuthRoutes({ app, loginThrottle });
82
+ const { requireAuth, isAuthorizedRequest } = registerAuthRoutes({
83
+ app,
84
+ loginThrottle,
85
+ });
83
86
  app.use(express.static(path.join(__dirname, "public")));
84
87
 
85
88
  registerPageRoutes({ app, requireAuth, isGatewayRunning });
@@ -143,10 +146,25 @@ registerGoogleRoutes({
143
146
  getApiEnableUrl,
144
147
  constants,
145
148
  });
146
- registerProxyRoutes({ app, proxy, SETUP_API_PREFIXES });
149
+ registerProxyRoutes({ app, proxy, SETUP_API_PREFIXES, requireAuth });
147
150
 
148
151
  const server = http.createServer(app);
149
152
  server.on("upgrade", (req, socket, head) => {
153
+ const requestUrl = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
154
+ if (requestUrl.pathname.startsWith("/openclaw")) {
155
+ const upgradeReq = {
156
+ ...req,
157
+ path: requestUrl.pathname,
158
+ query: Object.fromEntries(requestUrl.searchParams.entries()),
159
+ };
160
+ if (!isAuthorizedRequest(upgradeReq)) {
161
+ socket.write(
162
+ "HTTP/1.1 401 Unauthorized\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nUnauthorized",
163
+ );
164
+ socket.destroy();
165
+ return;
166
+ }
167
+ }
150
168
  proxy.ws(req, socket, head);
151
169
  });
152
170
 
@@ -10,6 +10,17 @@ Always explain:
10
10
 
11
11
  Then WAIT for the user's approval.
12
12
 
13
+ ### Plan Before You Build
14
+
15
+ Before diving into implementation, share your plan when the work is **significant**. Significance isn't about line count — a single high-impact change can be just as significant as a multi-step refactor. Ask yourself:
16
+
17
+ - Could this break existing behavior or introduce subtle bugs?
18
+ - Does it touch critical paths, shared state, or external integrations?
19
+ - Are there multiple valid approaches worth weighing?
20
+ - Would reverting this be painful?
21
+
22
+ If any of these apply, outline your approach first — what you intend to do, in what order, and any trade-offs you see — then **wait for the user's sign-off** before proceeding. For straightforward, low-risk tasks, just get it done.
23
+
13
24
  ### Show Your Work (IMPORTANT)
14
25
 
15
26
  Mandatory: Anytime you add, edit, or remove files/resources, end your message with a **Changes committed** summary.
@@ -1,3 +1,25 @@
1
+ ## AlphaClaw Harness
2
+
3
+ AlphaClaw is the setup and management harness that runs alongside OpenClaw. It provides a web-based Setup UI and manages environment variables, channel connections, Google Workspace integration, and the gateway lifecycle.
4
+
5
+ AlphaClaw UI: `{{SETUP_UI_URL}}`
6
+
7
+ ### Tabs
8
+
9
+ | Tab | URL | What it manages |
10
+ | ------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
11
+ | General | `{{SETUP_UI_URL}}#general` | Gateway status & restart, channel health (Telegram/Discord), pending pairings, Google Workspace connection, repo auto-sync schedule, OpenClaw dashboard |
12
+ | Models | `{{SETUP_UI_URL}}#models` | AI provider credentials (Anthropic, OpenAI, Google), Codex OAuth |
13
+ | Envars | `{{SETUP_UI_URL}}#envars` | View/edit/add environment variables (saved to `/data/.env`), gateway restart to apply changes |
14
+
15
+ ### Environment variables
16
+
17
+ Changes to env vars are made through the **Envars** tab (`{{SETUP_UI_URL}}#envars`). After saving, a gateway restart may be required to pick up the changes — the UI prompts for this automatically. Do not edit `/data/.env` directly; use the Setup UI so changes are validated and the gateway restart is handled.
18
+
19
+ ### Google Workspace
20
+
21
+ Google Workspace is connected via the **General** tab (`{{SETUP_UI_URL}}#general`). The user provides OAuth client credentials from Google Cloud Console, then authorizes access to the services they need (Gmail, Calendar, Drive, Sheets, Docs, Tasks, Contacts, Meet).
22
+
1
23
  ## Git Discipline
2
24
 
3
25
  **Commit and push after every set of changes.** Your entire .openclaw directory (config, cron, workspace) is version controlled. This is how your work survives container restarts.
@@ -9,10 +31,6 @@ cd /data/.openclaw && git add -A && git commit -m "description" && git push
9
31
  Never force push. Always pull before pushing if there might be remote changes.
10
32
  After pushing, include a link to the commit using the abbreviated hash: [abc1234](https://github.com/owner/repo/commit/abc1234) format. No backticks.
11
33
 
12
- ## Setup UI
13
-
14
- Web-based setup UI URL: `{{SETUP_UI_URL}}`
15
-
16
34
  ## Telegram Formatting
17
35
 
18
36
  - **Links:** Use markdown syntax `[text](URL)` — HTML `<a href>` does NOT render
@@ -13,25 +13,25 @@ There is a web-based Setup UI at `{{BASE_URL}}`. The **user** manages runtime co
13
13
 
14
14
  When the user needs to add a new API key, token, or any env var:
15
15
 
16
- > You can add that in your Setup UI → **Envars** tab: {{BASE_URL}}
16
+ > You can add that in your Setup UI → **Envars** tab: {{BASE_URL}}#envars
17
17
 
18
18
  ### Connecting a new channel (Telegram, Discord)
19
19
 
20
- > Add your bot token in the Setup UI → **Envars** tab, then approve the pairing request in the **General** tab.
20
+ > Add your bot token in the Setup UI → **Envars** tab ({{BASE_URL}}#envars), then approve the pairing request in the **General** tab ({{BASE_URL}}#general).
21
21
 
22
22
  ### Approving or rejecting pairings
23
23
 
24
24
  When a user asks about pairing their Telegram or Discord account:
25
25
 
26
- > Open the Setup UI → **General** tab. Pending pairing requests appear automatically — click **Approve** or **Reject**.
26
+ > Open the Setup UI → **General** tab ({{BASE_URL}}#general). Pending pairing requests appear automatically — click **Approve** or **Reject**.
27
27
 
28
28
  ### Connecting OpenAI Codex OAuth
29
29
 
30
- > Connect or reconnect Codex OAuth from the Setup UI → **Models** tab. Click **Connect Codex OAuth** and follow the popup flow.
30
+ > Connect or reconnect Codex OAuth from the Setup UI → **Models** tab ({{BASE_URL}}#models). Click **Connect Codex OAuth** and follow the popup flow.
31
31
 
32
32
  ### Connecting Google Workspace
33
33
 
34
- > Set up Google Workspace from the Setup UI → **General** tab (Google section). You'll need your OAuth client credentials from Google Cloud Console.
34
+ > Set up Google Workspace from the Setup UI → **General** tab ({{BASE_URL}}#general, Google section). You'll need your OAuth client credentials from Google Cloud Console.
35
35
 
36
36
  Supported Google services (user selects which to enable during OAuth):
37
37
 
@@ -65,6 +65,6 @@ Config lives at `/data/.openclaw/gogcli/`.
65
65
 
66
66
  This is a reference so you know what's available — not an invitation to call these endpoints.
67
67
 
68
- - **General tab**: Gateway status/restart, OpenClaw version + update, channel health, pending pairings, Google Workspace
69
- - **Models tab**: Primary model selection, provider credentials, Codex OAuth
70
- - **Envars tab**: View/edit/add environment variables, save to `/data/.env`
68
+ - **General tab** (`{{BASE_URL}}#general`): Gateway status/restart, OpenClaw version + update, channel health, pending pairings, Google Workspace
69
+ - **Models tab** (`{{BASE_URL}}#models`): Primary model selection, provider credentials, Codex OAuth
70
+ - **Envars tab** (`{{BASE_URL}}#envars`): View/edit/add environment variables, save to `/data/.env`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },