@chrysb/alphaclaw 0.1.25 → 0.2.1

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.
@@ -3,6 +3,7 @@ import { useState, useEffect, useRef } from "https://esm.sh/preact/hooks";
3
3
  import htm from "https://esm.sh/htm";
4
4
  import {
5
5
  runOnboard,
6
+ verifyGithubOnboardingRepo,
6
7
  fetchModels,
7
8
  fetchCodexStatus,
8
9
  disconnectCodex,
@@ -58,6 +59,7 @@ export const Welcome = ({ onComplete }) => {
58
59
  const [codexAuthStarted, setCodexAuthStarted] = useState(false);
59
60
  const [codexAuthWaiting, setCodexAuthWaiting] = useState(false);
60
61
  const [loading, setLoading] = useState(false);
62
+ const [githubStepLoading, setGithubStepLoading] = useState(false);
61
63
  const [error, setError] = useState(null);
62
64
  const codexPopupPollRef = useRef(null);
63
65
 
@@ -360,8 +362,27 @@ export const Welcome = ({ onComplete }) => {
360
362
  setStep(kWelcomeGroups.length - 1);
361
363
  };
362
364
 
363
- const goNext = () => {
365
+ const goNext = async () => {
364
366
  if (!activeGroup || !currentGroupValid) return;
367
+ if (activeGroup.id === "github") {
368
+ setGithubStepLoading(true);
369
+ setError(null);
370
+ try {
371
+ const result = await verifyGithubOnboardingRepo(
372
+ vals.GITHUB_WORKSPACE_REPO,
373
+ vals.GITHUB_TOKEN,
374
+ );
375
+ if (!result?.ok) {
376
+ setError(result?.error || "GitHub verification failed");
377
+ return;
378
+ }
379
+ } catch (err) {
380
+ setError(err?.message || "GitHub verification failed");
381
+ return;
382
+ } finally {
383
+ setGithubStepLoading(false);
384
+ }
385
+ }
365
386
  setStep((prev) => Math.min(kWelcomeGroups.length - 1, prev + 1));
366
387
  };
367
388
 
@@ -438,6 +459,7 @@ export const Welcome = ({ onComplete }) => {
438
459
  goBack=${goBack}
439
460
  goNext=${goNext}
440
461
  loading=${loading}
462
+ githubStepLoading=${githubStepLoading}
441
463
  allValid=${allValid}
442
464
  handleSubmit=${handleSubmit}
443
465
  />
@@ -167,6 +167,15 @@ export async function runOnboard(vars, modelKey) {
167
167
  return res.json();
168
168
  }
169
169
 
170
+ export async function verifyGithubOnboardingRepo(repo, token) {
171
+ const res = await authFetch('/api/onboard/github/verify', {
172
+ method: 'POST',
173
+ headers: { 'Content-Type': 'application/json' },
174
+ body: JSON.stringify({ repo, token }),
175
+ });
176
+ return res.json();
177
+ }
178
+
170
179
  export const fetchModels = async () => {
171
180
  const res = await authFetch('/api/models');
172
181
  return res.json();
@@ -264,6 +264,7 @@ const SETUP_API_PREFIXES = [
264
264
  "/api/openclaw",
265
265
  "/api/devices",
266
266
  "/api/sync-cron",
267
+ "/api/telegram",
267
268
  ];
268
269
 
269
270
  module.exports = {
@@ -63,6 +63,8 @@ const normalizeIp = (ip) => String(ip || "").replace(/^::ffff:/, "");
63
63
 
64
64
  const isTruthyEnvFlag = (value) =>
65
65
  ["1", "true", "yes", "on"].includes(String(value || "").trim().toLowerCase());
66
+ const isDebugEnabled = () =>
67
+ isTruthyEnvFlag(process.env.ALPHACLAW_DEBUG) || isTruthyEnvFlag(process.env.DEBUG);
66
68
 
67
69
  const getClientKey = (req) =>
68
70
  normalizeIp(
@@ -172,6 +174,33 @@ const readGoogleCredentials = () => {
172
174
  }
173
175
  };
174
176
 
177
+ const kSecretKeyMatchers = [
178
+ /(?:^|_)TOKEN(?:$|_)/i,
179
+ /(?:^|_)API_KEY(?:$|_)/i,
180
+ /(?:^|_)PASSWORD(?:$|_)/i,
181
+ /(?:^|_)SECRET(?:$|_)/i,
182
+ /(?:^|_)PRIVATE_KEY(?:$|_)/i,
183
+ ];
184
+
185
+ const isSensitiveKey = (key) =>
186
+ kSecretKeyMatchers.some((matcher) => matcher.test(String(key || "")));
187
+
188
+ const buildSecretReplacements = (...sources) => {
189
+ const replacements = [];
190
+ const seen = new Set();
191
+ for (const source of sources) {
192
+ for (const [rawKey, rawValue] of Object.entries(source || {})) {
193
+ const key = String(rawKey || "").trim();
194
+ const value = String(rawValue || "");
195
+ if (!key || !value || !isSensitiveKey(key)) continue;
196
+ if (seen.has(value)) continue;
197
+ seen.add(value);
198
+ replacements.push([value, `\${${key}}`]);
199
+ }
200
+ }
201
+ return replacements.sort((a, b) => b[0].length - a[0].length);
202
+ };
203
+
175
204
  module.exports = {
176
205
  normalizeOpenclawVersion,
177
206
  compareVersionParts,
@@ -180,6 +209,7 @@ module.exports = {
180
209
  getCodexAccountId,
181
210
  normalizeIp,
182
211
  isTruthyEnvFlag,
212
+ isDebugEnabled,
183
213
  getClientKey,
184
214
  resolveGithubRepoUrl,
185
215
  createPkcePair,
@@ -189,4 +219,6 @@ module.exports = {
189
219
  getBaseUrl,
190
220
  getApiEnableUrl,
191
221
  readGoogleCredentials,
222
+ isSensitiveKey,
223
+ buildSecretReplacements,
192
224
  };
@@ -1,49 +1,114 @@
1
- const ensureGithubRepoAccessible = async ({ repoUrl, repoName, remoteUrl, githubToken }) => {
2
- void remoteUrl;
3
- const ghHeaders = {
4
- Authorization: `token ${githubToken}`,
5
- "User-Agent": "openclaw-railway",
6
- Accept: "application/vnd.github+json",
7
- };
1
+ const buildGithubHeaders = (githubToken) => ({
2
+ Authorization: `token ${githubToken}`,
3
+ "User-Agent": "openclaw-railway",
4
+ Accept: "application/vnd.github+json",
5
+ });
8
6
 
7
+ const parseGithubErrorMessage = async (response) => {
9
8
  try {
10
- const checkRes = await fetch(`https://api.github.com/repos/${repoUrl}`, {
9
+ const payload = await response.json();
10
+ if (typeof payload?.message === "string" && payload.message.trim()) {
11
+ return payload.message.trim();
12
+ }
13
+ } catch {}
14
+ return response.statusText || `HTTP ${response.status}`;
15
+ };
16
+
17
+ const verifyGithubRepoForOnboarding = async ({ repoUrl, githubToken }) => {
18
+ const ghHeaders = buildGithubHeaders(githubToken);
19
+ const [repoOwner] = String(repoUrl || "").split("/", 1);
20
+
21
+ try {
22
+ const userRes = await fetch("https://api.github.com/user", {
11
23
  headers: ghHeaders,
12
24
  });
25
+ if (!userRes.ok) {
26
+ const details = await parseGithubErrorMessage(userRes);
27
+ return {
28
+ ok: false,
29
+ status: 400,
30
+ error: `Cannot verify GitHub token: ${details}`,
31
+ };
32
+ }
33
+ const authedUser = await userRes.json().catch(() => ({}));
34
+ const authedLogin = String(authedUser?.login || "").trim();
35
+ if (
36
+ repoOwner &&
37
+ authedLogin &&
38
+ repoOwner.toLowerCase() !== authedLogin.toLowerCase()
39
+ ) {
40
+ return {
41
+ ok: false,
42
+ status: 400,
43
+ error: `Workspace repo owner must match your token user "${authedLogin}"`,
44
+ };
45
+ }
13
46
 
47
+ const checkRes = await fetch(`https://api.github.com/repos/${repoUrl}`, {
48
+ headers: ghHeaders,
49
+ });
14
50
  if (checkRes.status === 404) {
15
- console.log(`[onboard] Creating repo ${repoUrl}...`);
16
- const createRes = await fetch("https://api.github.com/user/repos", {
17
- method: "POST",
18
- headers: { ...ghHeaders, "Content-Type": "application/json" },
19
- body: JSON.stringify({
20
- name: repoName,
21
- private: true,
22
- auto_init: false,
23
- }),
24
- });
25
- if (!createRes.ok) {
26
- const err = await createRes.json().catch(() => ({}));
27
- return {
28
- ok: false,
29
- status: 400,
30
- error: `Failed to create repo: ${err.message || createRes.statusText}`,
31
- };
32
- }
33
- console.log(`[onboard] Repo ${repoUrl} created`);
34
51
  return { ok: true };
35
52
  }
53
+ if (checkRes.ok) {
54
+ return {
55
+ ok: false,
56
+ status: 400,
57
+ error: `Repository "${repoUrl}" already exists.`,
58
+ };
59
+ }
36
60
 
37
- if (checkRes.ok) return { ok: true };
38
-
61
+ const details = await parseGithubErrorMessage(checkRes);
39
62
  return {
40
63
  ok: false,
41
64
  status: 400,
42
- error: `Cannot access repo "${repoUrl}" — check your token has the "repo" scope`,
65
+ error: `Cannot verify repo "${repoUrl}": ${details}`,
43
66
  };
67
+ } catch (e) {
68
+ return {
69
+ ok: false,
70
+ status: 400,
71
+ error: `GitHub verification error: ${e.message}`,
72
+ };
73
+ }
74
+ };
75
+
76
+ const ensureGithubRepoAccessible = async ({
77
+ repoUrl,
78
+ repoName,
79
+ githubToken,
80
+ }) => {
81
+ const ghHeaders = buildGithubHeaders(githubToken);
82
+ const verification = await verifyGithubRepoForOnboarding({
83
+ repoUrl,
84
+ githubToken,
85
+ });
86
+ if (!verification.ok) return verification;
87
+
88
+ try {
89
+ console.log(`[onboard] Creating repo ${repoUrl}...`);
90
+ const createRes = await fetch("https://api.github.com/user/repos", {
91
+ method: "POST",
92
+ headers: { ...ghHeaders, "Content-Type": "application/json" },
93
+ body: JSON.stringify({
94
+ name: repoName,
95
+ private: true,
96
+ auto_init: false,
97
+ }),
98
+ });
99
+ if (!createRes.ok) {
100
+ const details = await parseGithubErrorMessage(createRes);
101
+ return {
102
+ ok: false,
103
+ status: 400,
104
+ error: `Failed to create repo: ${details}`,
105
+ };
106
+ }
107
+ console.log(`[onboard] Repo ${repoUrl} created`);
108
+ return { ok: true };
44
109
  } catch (e) {
45
110
  return { ok: false, status: 400, error: `GitHub error: ${e.message}` };
46
111
  }
47
112
  };
48
113
 
49
- module.exports = { ensureGithubRepoAccessible };
114
+ module.exports = { ensureGithubRepoAccessible, verifyGithubRepoForOnboarding };
@@ -1,11 +1,22 @@
1
1
  const path = require("path");
2
2
  const { kSetupDir, kRootDir } = require("../constants");
3
3
  const { validateOnboardingInput } = require("./validation");
4
- const { ensureGithubRepoAccessible } = require("./github");
5
- const { buildOnboardArgs, writeSanitizedOpenclawConfig } = require("./openclaw");
6
- const { installControlUiSkill, syncBootstrapPromptFiles } = require("./workspace");
7
- const { installHourlyGitSyncScript, installHourlyGitSyncCron } = require("./cron");
8
- const { isTruthyEnvFlag } = require("../helpers");
4
+ const {
5
+ ensureGithubRepoAccessible,
6
+ verifyGithubRepoForOnboarding,
7
+ } = require("./github");
8
+ const {
9
+ buildOnboardArgs,
10
+ writeSanitizedOpenclawConfig,
11
+ } = require("./openclaw");
12
+ const {
13
+ installControlUiSkill,
14
+ syncBootstrapPromptFiles,
15
+ } = require("./workspace");
16
+ const {
17
+ installHourlyGitSyncScript,
18
+ installHourlyGitSyncCron,
19
+ } = require("./cron");
9
20
 
10
21
  const createOnboardingService = ({
11
22
  fs,
@@ -23,6 +34,15 @@ const createOnboardingService = ({
23
34
  }) => {
24
35
  const { OPENCLAW_DIR, WORKSPACE_DIR } = constants;
25
36
 
37
+ const verifyGithubSetup = async ({
38
+ githubRepoInput,
39
+ githubToken,
40
+ resolveGithubRepoUrl,
41
+ }) => {
42
+ const repoUrl = resolveGithubRepoUrl(githubRepoInput);
43
+ return verifyGithubRepoForOnboarding({ repoUrl, githubToken });
44
+ };
45
+
26
46
  const completeOnboarding = async ({ req, vars, modelKey }) => {
27
47
  const validation = validateOnboardingInput({
28
48
  vars,
@@ -31,43 +51,62 @@ const createOnboardingService = ({
31
51
  hasCodexOauthProfile,
32
52
  });
33
53
  if (!validation.ok) {
34
- return { status: validation.status, body: { ok: false, error: validation.error } };
54
+ return {
55
+ status: validation.status,
56
+ body: { ok: false, error: validation.error },
57
+ };
35
58
  }
36
59
 
37
- const { varMap, githubToken, githubRepoInput, selectedProvider, hasCodexOauth } =
38
- validation.data;
60
+ const {
61
+ varMap,
62
+ githubToken,
63
+ githubRepoInput,
64
+ selectedProvider,
65
+ hasCodexOauth,
66
+ } = validation.data;
39
67
 
40
68
  const repoUrl = resolveGithubRepoUrl(githubRepoInput);
41
- const varsToSave = [...vars.filter((v) => v.value && v.key !== "GITHUB_WORKSPACE_REPO")];
69
+ const varsToSave = [
70
+ ...vars.filter((v) => v.value && v.key !== "GITHUB_WORKSPACE_REPO"),
71
+ ];
42
72
  varsToSave.push({ key: "GITHUB_WORKSPACE_REPO", value: repoUrl });
43
73
  writeEnvFile(varsToSave);
44
74
  reloadEnv();
45
75
 
46
- const remoteUrl = `https://${githubToken}@github.com/${repoUrl}.git`;
76
+ const remoteUrl = `https://github.com/${repoUrl}.git`;
47
77
  const [, repoName] = repoUrl.split("/");
48
78
  const repoCheck = await ensureGithubRepoAccessible({
49
79
  repoUrl,
50
80
  repoName,
51
- remoteUrl,
52
81
  githubToken,
53
82
  });
54
83
  if (!repoCheck.ok) {
55
- return { status: repoCheck.status, body: { ok: false, error: repoCheck.error } };
84
+ return {
85
+ status: repoCheck.status,
86
+ body: { ok: false, error: repoCheck.error },
87
+ };
56
88
  }
57
89
 
58
90
  fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
59
91
  fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
60
- syncBootstrapPromptFiles({ fs, workspaceDir: WORKSPACE_DIR, baseUrl: getBaseUrl(req) });
92
+ syncBootstrapPromptFiles({
93
+ fs,
94
+ workspaceDir: WORKSPACE_DIR,
95
+ baseUrl: getBaseUrl(req),
96
+ });
61
97
 
62
98
  if (!fs.existsSync(`${OPENCLAW_DIR}/.git`)) {
63
99
  await shellCmd(
64
- `cd ${OPENCLAW_DIR} && git init -b main && git remote add origin "${remoteUrl}" && git config user.email "agent@openclaw.ai" && git config user.name "OpenClaw Agent"`,
100
+ `cd ${OPENCLAW_DIR} && git init -b main && git remote add origin "${remoteUrl}" && git config user.email "agent@alphaclaw.md" && git config user.name "AlphaClaw Agent"`,
65
101
  );
66
102
  console.log("[onboard] Git initialized");
67
103
  }
68
104
 
69
105
  if (!fs.existsSync(`${OPENCLAW_DIR}/.gitignore`)) {
70
- fs.copyFileSync(path.join(kSetupDir, "gitignore"), `${OPENCLAW_DIR}/.gitignore`);
106
+ fs.copyFileSync(
107
+ path.join(kSetupDir, "gitignore"),
108
+ `${OPENCLAW_DIR}/.gitignore`,
109
+ );
71
110
  }
72
111
 
73
112
  const onboardArgs = buildOnboardArgs({
@@ -76,14 +115,17 @@ const createOnboardingService = ({
76
115
  hasCodexOauth,
77
116
  workspaceDir: WORKSPACE_DIR,
78
117
  });
79
- await shellCmd(`openclaw onboard ${onboardArgs.map((a) => `"${a}"`).join(" ")}`, {
80
- env: {
81
- ...process.env,
82
- OPENCLAW_HOME: kRootDir,
83
- OPENCLAW_CONFIG_PATH: `${OPENCLAW_DIR}/openclaw.json`,
118
+ await shellCmd(
119
+ `openclaw onboard ${onboardArgs.map((a) => `"${a}"`).join(" ")}`,
120
+ {
121
+ env: {
122
+ ...process.env,
123
+ OPENCLAW_HOME: kRootDir,
124
+ OPENCLAW_CONFIG_PATH: `${OPENCLAW_DIR}/openclaw.json`,
125
+ },
126
+ timeout: 120000,
84
127
  },
85
- timeout: 120000,
86
- });
128
+ );
87
129
  console.log("[onboard] Onboard complete");
88
130
 
89
131
  await shellCmd(`openclaw models set "${modelKey}"`, {
@@ -91,7 +133,9 @@ const createOnboardingService = ({
91
133
  timeout: 30000,
92
134
  }).catch((e) => {
93
135
  console.error("[onboard] Failed to set model:", e.message);
94
- throw new Error(`Onboarding completed but failed to set model "${modelKey}"`);
136
+ throw new Error(
137
+ `Onboarding completed but failed to set model "${modelKey}"`,
138
+ );
95
139
  });
96
140
 
97
141
  try {
@@ -101,24 +145,33 @@ const createOnboardingService = ({
101
145
  writeSanitizedOpenclawConfig({ fs, openclawDir: OPENCLAW_DIR, varMap });
102
146
  ensureGatewayProxyConfig(getBaseUrl(req));
103
147
 
104
- installControlUiSkill({ fs, openclawDir: OPENCLAW_DIR, baseUrl: getBaseUrl(req) });
105
-
106
- const shouldForcePush = isTruthyEnvFlag(process.env.OPENCLAW_DEBUG_FORCE_PUSH);
107
- const pushArgs = shouldForcePush ? "-u --force" : "-u";
108
- await shellCmd(
109
- `cd ${OPENCLAW_DIR} && git add -A && git commit -m "initial setup" && git push ${pushArgs} origin main`,
110
- { timeout: 30000 },
111
- ).catch((e) => console.error("[onboard] Git push error:", e.message));
112
- console.log("[onboard] Initial state committed and pushed");
148
+ installControlUiSkill({
149
+ fs,
150
+ openclawDir: OPENCLAW_DIR,
151
+ baseUrl: getBaseUrl(req),
152
+ });
113
153
 
114
154
  installHourlyGitSyncScript({ fs, openclawDir: OPENCLAW_DIR });
115
155
  await installHourlyGitSyncCron({ fs, openclawDir: OPENCLAW_DIR });
116
156
 
157
+ try {
158
+ await shellCmd(`alphaclaw git-sync -m "initial setup"`, {
159
+ timeout: 30000,
160
+ env: {
161
+ ...process.env,
162
+ GITHUB_TOKEN: githubToken,
163
+ },
164
+ });
165
+ console.log("[onboard] Initial state committed and pushed");
166
+ } catch (e) {
167
+ console.error("[onboard] Git push error:", e.message);
168
+ }
169
+
117
170
  startGateway();
118
171
  return { status: 200, body: { ok: true } };
119
172
  };
120
173
 
121
- return { completeOnboarding };
174
+ return { completeOnboarding, verifyGithubSetup };
122
175
  };
123
176
 
124
177
  module.exports = { createOnboardingService };
@@ -1,3 +1,5 @@
1
+ const { buildSecretReplacements } = require("../helpers");
2
+
1
3
  const buildOnboardArgs = ({ varMap, selectedProvider, hasCodexOauth, workspaceDir }) => {
2
4
  const onboardArgs = [
3
5
  "--non-interactive",
@@ -149,18 +151,9 @@ const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
149
151
  }
150
152
 
151
153
  let content = JSON.stringify(cfg, null, 2);
152
- const replacements = [
153
- [process.env.OPENCLAW_GATEWAY_TOKEN, "${OPENCLAW_GATEWAY_TOKEN}"],
154
- [varMap.ANTHROPIC_API_KEY, "${ANTHROPIC_API_KEY}"],
155
- [varMap.ANTHROPIC_TOKEN, "${ANTHROPIC_TOKEN}"],
156
- [varMap.TELEGRAM_BOT_TOKEN, "${TELEGRAM_BOT_TOKEN}"],
157
- [varMap.DISCORD_BOT_TOKEN, "${DISCORD_BOT_TOKEN}"],
158
- [varMap.OPENAI_API_KEY, "${OPENAI_API_KEY}"],
159
- [varMap.GEMINI_API_KEY, "${GEMINI_API_KEY}"],
160
- [varMap.BRAVE_API_KEY, "${BRAVE_API_KEY}"],
161
- ];
154
+ const replacements = buildSecretReplacements(varMap, process.env);
162
155
  for (const [secret, envRef] of replacements) {
163
- if (secret && secret.length > 8) {
156
+ if (secret) {
164
157
  content = content.split(secret).join(envRef);
165
158
  }
166
159
  }
@@ -1,5 +1,6 @@
1
1
  const path = require("path");
2
- const { kSetupDir } = require("../constants");
2
+ const { kSetupDir, OPENCLAW_DIR } = require("../constants");
3
+ const { renderTopicRegistryMarkdown } = require("../topic-registry");
3
4
 
4
5
  const resolveSetupUiUrl = (baseUrl) => {
5
6
  const normalizedBaseUrl = String(baseUrl || "").trim().replace(/\/+$/, "");
@@ -19,16 +20,38 @@ const resolveSetupUiUrl = (baseUrl) => {
19
20
  return "http://localhost:3000";
20
21
  };
21
22
 
23
+ // Single assembly point for TOOLS.md: template + topic registry.
24
+ // Idempotent — always rebuilds from source so deploys never clobber topic data.
25
+ const isTelegramWorkspaceEnabled = (fs) => {
26
+ try {
27
+ const configPath = `${OPENCLAW_DIR}/openclaw.json`;
28
+ const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
29
+ return Object.keys(cfg.channels?.telegram?.groups || {}).length > 0;
30
+ } catch {
31
+ return false;
32
+ }
33
+ };
34
+
22
35
  const syncBootstrapPromptFiles = ({ fs, workspaceDir, baseUrl }) => {
23
36
  try {
37
+ const setupUiUrl = resolveSetupUiUrl(baseUrl);
24
38
  const bootstrapDir = `${workspaceDir}/hooks/bootstrap`;
25
39
  fs.mkdirSync(bootstrapDir, { recursive: true });
26
40
  fs.copyFileSync(path.join(kSetupDir, "core-prompts", "AGENTS.md"), `${bootstrapDir}/AGENTS.md`);
41
+
27
42
  const toolsTemplate = fs.readFileSync(path.join(kSetupDir, "core-prompts", "TOOLS.md"), "utf8");
28
- const toolsContent = toolsTemplate.replace(
43
+ let toolsContent = toolsTemplate.replace(
29
44
  /\{\{SETUP_UI_URL\}\}/g,
30
- resolveSetupUiUrl(baseUrl),
45
+ setupUiUrl,
31
46
  );
47
+
48
+ const topicSection = renderTopicRegistryMarkdown({
49
+ includeSyncGuidance: isTelegramWorkspaceEnabled(fs),
50
+ });
51
+ if (topicSection) {
52
+ toolsContent += topicSection;
53
+ }
54
+
32
55
  fs.writeFileSync(`${bootstrapDir}/TOOLS.md`, toolsContent);
33
56
  console.log("[onboard] Bootstrap prompt files synced");
34
57
  } catch (e) {
@@ -2,7 +2,8 @@ const crypto = require("crypto");
2
2
  const { kLoginCleanupIntervalMs } = require("../constants");
3
3
 
4
4
  const registerAuthRoutes = ({ app, loginThrottle }) => {
5
- const SETUP_PASSWORD = process.env.SETUP_PASSWORD || "";
5
+ const SETUP_PASSWORD = String(process.env.SETUP_PASSWORD || "").trim();
6
+ const kAuthMisconfigured = !SETUP_PASSWORD;
6
7
  const kSessionTtlMs = 7 * 24 * 60 * 60 * 1000;
7
8
 
8
9
  const signSessionPayload = (payload) =>
@@ -50,7 +51,13 @@ const registerAuthRoutes = ({ app, loginThrottle }) => {
50
51
  };
51
52
 
52
53
  app.post("/api/auth/login", (req, res) => {
53
- if (!SETUP_PASSWORD) return res.json({ ok: true });
54
+ if (kAuthMisconfigured) {
55
+ return res.status(503).json({
56
+ ok: false,
57
+ error:
58
+ "Server misconfigured: SETUP_PASSWORD is missing. Set it in your deployment environment variables and restart.",
59
+ });
60
+ }
54
61
  const now = Date.now();
55
62
  const clientKey = loginThrottle.getClientKey(req);
56
63
  const state = loginThrottle.getOrCreateLoginAttemptState(clientKey, now);
@@ -92,18 +99,29 @@ const registerAuthRoutes = ({ app, loginThrottle }) => {
92
99
  }, kLoginCleanupIntervalMs).unref();
93
100
 
94
101
  const isAuthorizedRequest = (req) => {
95
- if (!SETUP_PASSWORD) return true;
102
+ if (kAuthMisconfigured) return false;
96
103
  const requestPath = req.path || "";
97
104
  if (requestPath.startsWith("/auth/google/callback")) return true;
98
105
  if (requestPath.startsWith("/auth/codex/callback")) return true;
99
106
  const cookies = cookieParser(req);
100
- const query = req.query || {};
101
- const token = cookies.setup_token || query.token;
107
+ const token = cookies.setup_token;
102
108
  return verifySessionToken(token);
103
109
  };
104
110
 
105
111
  const requireAuth = (req, res, next) => {
106
- if (!SETUP_PASSWORD) return next();
112
+ if (kAuthMisconfigured) {
113
+ if (req.path.startsWith("/api/")) {
114
+ return res.status(503).json({
115
+ error:
116
+ "Server misconfigured: SETUP_PASSWORD is missing. Set it in your deployment environment variables and restart.",
117
+ });
118
+ }
119
+ return res
120
+ .status(503)
121
+ .send(
122
+ "Setup auth is not configured. Set SETUP_PASSWORD in your deployment environment and restart.",
123
+ );
124
+ }
107
125
  if (req.path.startsWith("/auth/google/callback")) return next();
108
126
  if (req.path.startsWith("/auth/codex/callback")) return next();
109
127
  if (isAuthorizedRequest(req)) return next();
@@ -111,6 +111,42 @@ const registerOnboardingRoutes = ({
111
111
  res.status(500).json({ ok: false, error: sanitizeOnboardingError(err) });
112
112
  }
113
113
  });
114
+
115
+ app.post("/api/onboard/github/verify", async (req, res) => {
116
+ if (isOnboarded()) {
117
+ return res.json({ ok: false, error: "Already onboarded" });
118
+ }
119
+
120
+ try {
121
+ const githubRepoInput = String(req.body?.repo || "").trim();
122
+ const githubToken = String(req.body?.token || "").trim();
123
+ if (!githubRepoInput || !githubToken) {
124
+ return res
125
+ .status(400)
126
+ .json({
127
+ ok: false,
128
+ error: "GitHub token and workspace repo are required",
129
+ });
130
+ }
131
+
132
+ const result = await onboardingService.verifyGithubSetup({
133
+ githubRepoInput,
134
+ githubToken,
135
+ resolveGithubRepoUrl,
136
+ });
137
+ if (!result.ok) {
138
+ return res
139
+ .status(result.status || 400)
140
+ .json({ ok: false, error: result.error });
141
+ }
142
+ return res.json({ ok: true });
143
+ } catch (err) {
144
+ console.error("[onboard] GitHub verify error:", err);
145
+ return res
146
+ .status(500)
147
+ .json({ ok: false, error: sanitizeOnboardingError(err) });
148
+ }
149
+ });
114
150
  };
115
151
 
116
152
  module.exports = { registerOnboardingRoutes };
@@ -3,13 +3,13 @@ const registerProxyRoutes = ({ app, proxy, SETUP_API_PREFIXES, requireAuth }) =>
3
3
  req.url = "/";
4
4
  proxy.web(req, res);
5
5
  });
6
- app.all("/openclaw/*", requireAuth, (req, res) => {
6
+ app.all("/openclaw/*path", requireAuth, (req, res) => {
7
7
  req.url = req.url.replace(/^\/openclaw/, "");
8
8
  proxy.web(req, res);
9
9
  });
10
- app.all("/assets/*", requireAuth, (req, res) => proxy.web(req, res));
10
+ app.all("/assets/*path", requireAuth, (req, res) => proxy.web(req, res));
11
11
 
12
- app.all("/webhook/*", (req, res) => {
12
+ app.all("/webhook/*path", (req, res) => {
13
13
  if (!req.headers.authorization && req.query.token) {
14
14
  req.headers.authorization = `Bearer ${req.query.token}`;
15
15
  delete req.query.token;
@@ -20,7 +20,7 @@ const registerProxyRoutes = ({ app, proxy, SETUP_API_PREFIXES, requireAuth }) =>
20
20
  proxy.web(req, res);
21
21
  });
22
22
 
23
- app.all("/api/*", (req, res) => {
23
+ app.all("/api/*path", (req, res) => {
24
24
  if (SETUP_API_PREFIXES.some((p) => req.path.startsWith(p))) return;
25
25
  proxy.web(req, res);
26
26
  });