@chrysb/alphaclaw 0.3.5-beta.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/alphaclaw.js +66 -32
- package/lib/public/assets/icons/google_icon.svg +8 -0
- package/lib/public/css/explorer.css +254 -6
- package/lib/public/js/app.js +165 -100
- package/lib/public/js/components/channels.js +1 -0
- package/lib/public/js/components/credentials-modal.js +36 -8
- package/lib/public/js/components/file-tree.js +267 -88
- package/lib/public/js/components/file-viewer/constants.js +6 -0
- package/lib/public/js/components/file-viewer/diff-viewer.js +46 -0
- package/lib/public/js/components/file-viewer/editor-surface.js +120 -0
- package/lib/public/js/components/file-viewer/frontmatter-panel.js +56 -0
- package/lib/public/js/components/file-viewer/index.js +202 -0
- package/lib/public/js/components/file-viewer/markdown-split-view.js +51 -0
- package/lib/public/js/components/file-viewer/media-preview.js +44 -0
- package/lib/public/js/components/file-viewer/scroll-sync.js +95 -0
- package/lib/public/js/components/file-viewer/sqlite-viewer.js +167 -0
- package/lib/public/js/components/file-viewer/status-banners.js +64 -0
- package/lib/public/js/components/file-viewer/storage.js +58 -0
- package/lib/public/js/components/file-viewer/toolbar.js +119 -0
- package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +93 -0
- package/lib/public/js/components/file-viewer/use-file-diff.js +60 -0
- package/lib/public/js/components/file-viewer/use-file-loader.js +312 -0
- package/lib/public/js/components/file-viewer/use-file-viewer-draft-sync.js +32 -0
- package/lib/public/js/components/file-viewer/use-file-viewer-hotkeys.js +25 -0
- package/lib/public/js/components/file-viewer/use-file-viewer.js +471 -0
- package/lib/public/js/components/file-viewer/utils.js +11 -0
- package/lib/public/js/components/gateway.js +83 -30
- package/lib/public/js/components/google/account-row.js +98 -0
- package/lib/public/js/components/google/add-account-modal.js +93 -0
- package/lib/public/js/components/google/index.js +439 -0
- package/lib/public/js/components/google/use-google-accounts.js +41 -0
- package/lib/public/js/components/icons.js +39 -0
- package/lib/public/js/components/sidebar-git-panel.js +115 -25
- package/lib/public/js/components/sidebar.js +91 -75
- package/lib/public/js/components/usage-tab.js +4 -1
- package/lib/public/js/components/watchdog-tab.js +6 -0
- package/lib/public/js/lib/api.js +88 -8
- package/lib/public/js/lib/browse-file-policies.js +52 -0
- package/lib/public/js/lib/syntax-highlighters/index.js +6 -5
- package/lib/public/shared/browse-file-policies.json +13 -0
- package/lib/scripts/git +40 -0
- package/lib/scripts/git-askpass +6 -0
- package/lib/server/constants.js +20 -0
- package/lib/server/google-state.js +187 -0
- package/lib/server/helpers.js +12 -4
- package/lib/server/onboarding/github.js +21 -2
- package/lib/server/onboarding/index.js +1 -3
- package/lib/server/onboarding/openclaw.js +3 -0
- package/lib/server/onboarding/workspace.js +40 -0
- package/lib/server/routes/browse/constants.js +51 -0
- package/lib/server/routes/browse/file-helpers.js +43 -0
- package/lib/server/routes/browse/git.js +131 -0
- package/lib/server/routes/browse/index.js +660 -0
- package/lib/server/routes/browse/path-utils.js +53 -0
- package/lib/server/routes/browse/sqlite.js +140 -0
- package/lib/server/routes/google.js +414 -213
- package/lib/server/routes/proxy.js +11 -5
- package/lib/setup/core-prompts/TOOLS.md +0 -4
- package/lib/setup/gitignore +3 -0
- package/lib/setup/hourly-git-sync.sh +28 -1
- package/package.json +1 -1
- package/lib/public/js/components/file-viewer.js +0 -1095
- package/lib/public/js/components/google.js +0 -228
- package/lib/server/routes/browse.js +0 -500
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
const {
|
|
2
|
+
kDefaultGoogleClient,
|
|
3
|
+
kDefaultGoogleScopes,
|
|
4
|
+
createGoogleAccountId,
|
|
5
|
+
readGoogleState,
|
|
6
|
+
writeGoogleState,
|
|
7
|
+
listGoogleAccounts,
|
|
8
|
+
getGoogleAccountById,
|
|
9
|
+
getGoogleAccountByEmailAndClient,
|
|
10
|
+
upsertGoogleAccount,
|
|
11
|
+
removeGoogleAccount,
|
|
12
|
+
} = require("../google-state");
|
|
13
|
+
const { syncBootstrapPromptFiles } = require("../onboarding/workspace");
|
|
14
|
+
|
|
15
|
+
const quoteShellArg = (value) => `"${String(value || "").replace(/(["\\$`])/g, "\\$1")}"`;
|
|
16
|
+
|
|
17
|
+
const parseJsonSafe = (raw, fallbackValue) => {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(String(raw || ""));
|
|
20
|
+
} catch {
|
|
21
|
+
return fallbackValue;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const uniqueServiceLabels = (scopes) =>
|
|
26
|
+
Array.from(
|
|
27
|
+
new Set(
|
|
28
|
+
(scopes || [])
|
|
29
|
+
.map((scope) => String(scope || "").split(":")[0])
|
|
30
|
+
.filter(Boolean),
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
|
|
1
34
|
const registerGoogleRoutes = ({
|
|
2
35
|
app,
|
|
3
36
|
fs,
|
|
@@ -9,15 +42,136 @@ const registerGoogleRoutes = ({
|
|
|
9
42
|
constants,
|
|
10
43
|
}) => {
|
|
11
44
|
const {
|
|
12
|
-
GOG_CREDENTIALS_PATH,
|
|
13
45
|
GOG_CONFIG_DIR,
|
|
14
46
|
GOG_STATE_PATH,
|
|
15
47
|
API_TEST_COMMANDS,
|
|
16
48
|
BASE_SCOPES,
|
|
17
49
|
SCOPE_MAP,
|
|
18
50
|
REVERSE_SCOPE_MAP,
|
|
51
|
+
kMaxGoogleAccounts,
|
|
52
|
+
gogClientCredentialsPath,
|
|
19
53
|
} = constants;
|
|
20
54
|
|
|
55
|
+
const readState = () => readGoogleState({ fs, statePath: GOG_STATE_PATH });
|
|
56
|
+
const saveState = (state) => writeGoogleState({ fs, statePath: GOG_STATE_PATH, state });
|
|
57
|
+
const syncBootstrapTools = (req) => {
|
|
58
|
+
try {
|
|
59
|
+
syncBootstrapPromptFiles({
|
|
60
|
+
fs,
|
|
61
|
+
workspaceDir: constants.WORKSPACE_DIR,
|
|
62
|
+
baseUrl: getBaseUrl(req),
|
|
63
|
+
});
|
|
64
|
+
} catch {}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const listAuthenticatedAccounts = async (state) => {
|
|
68
|
+
const configuredClients = new Set([kDefaultGoogleClient]);
|
|
69
|
+
listGoogleAccounts(state).forEach((account) => {
|
|
70
|
+
const client = String(account.client || kDefaultGoogleClient).trim() || kDefaultGoogleClient;
|
|
71
|
+
configuredClients.add(client);
|
|
72
|
+
});
|
|
73
|
+
const combined = [];
|
|
74
|
+
for (const client of configuredClients) {
|
|
75
|
+
const command =
|
|
76
|
+
client === kDefaultGoogleClient
|
|
77
|
+
? "auth list --json --check"
|
|
78
|
+
: `--client ${quoteShellArg(client)} auth list --json --check`;
|
|
79
|
+
const result = await gogCmd(command, { quiet: true });
|
|
80
|
+
if (!result.ok) continue;
|
|
81
|
+
const parsed = parseJsonSafe(result.stdout, { accounts: [] });
|
|
82
|
+
const accounts = Array.isArray(parsed?.accounts) ? parsed.accounts : [];
|
|
83
|
+
accounts.forEach((entry) => {
|
|
84
|
+
combined.push({
|
|
85
|
+
...entry,
|
|
86
|
+
client: String(entry.client || client || kDefaultGoogleClient).trim() || kDefaultGoogleClient,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return combined;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const accountIsAuthenticated = ({ account, authenticatedAccounts }) =>
|
|
94
|
+
authenticatedAccounts.some(
|
|
95
|
+
(entry) =>
|
|
96
|
+
String(entry.email || "").trim().toLowerCase() === String(account.email || "").trim().toLowerCase() &&
|
|
97
|
+
String(entry.client || kDefaultGoogleClient).trim() === String(account.client || kDefaultGoogleClient).trim() &&
|
|
98
|
+
(entry.valid !== false),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const getSelectedAccount = ({ state, accountId, fallbackToFirst = true }) => {
|
|
102
|
+
if (accountId) {
|
|
103
|
+
return getGoogleAccountById(state, accountId);
|
|
104
|
+
}
|
|
105
|
+
return fallbackToFirst ? listGoogleAccounts(state)[0] || null : null;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const clearStoredGoogleAuthForEmail = async ({
|
|
109
|
+
email,
|
|
110
|
+
preferredClient = kDefaultGoogleClient,
|
|
111
|
+
extraClients = [],
|
|
112
|
+
}) => {
|
|
113
|
+
const normalizedEmail = String(email || "").trim();
|
|
114
|
+
if (!normalizedEmail) return;
|
|
115
|
+
const clientCandidates = new Set([
|
|
116
|
+
kDefaultGoogleClient,
|
|
117
|
+
preferredClient,
|
|
118
|
+
...extraClients,
|
|
119
|
+
]);
|
|
120
|
+
for (const clientName of clientCandidates) {
|
|
121
|
+
const safeClientName =
|
|
122
|
+
String(clientName || "").trim() || kDefaultGoogleClient;
|
|
123
|
+
const clientArg =
|
|
124
|
+
safeClientName === kDefaultGoogleClient
|
|
125
|
+
? ""
|
|
126
|
+
: `--client ${quoteShellArg(safeClientName)} `;
|
|
127
|
+
await gogCmd(
|
|
128
|
+
`${clientArg}auth remove ${quoteShellArg(normalizedEmail)} --force`,
|
|
129
|
+
{ quiet: true },
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const ensureClientCredentials = ({ client, clientId, clientSecret, req }) => {
|
|
135
|
+
const credentialsPath = gogClientCredentialsPath(client);
|
|
136
|
+
fs.mkdirSync(GOG_CONFIG_DIR, { recursive: true });
|
|
137
|
+
const credentials = {
|
|
138
|
+
web: {
|
|
139
|
+
client_id: clientId,
|
|
140
|
+
client_secret: clientSecret,
|
|
141
|
+
auth_uri: "https://accounts.google.com/o/oauth2/auth",
|
|
142
|
+
token_uri: "https://oauth2.googleapis.com/token",
|
|
143
|
+
redirect_uris: [`${getBaseUrl(req)}/auth/google/callback`],
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
fs.writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2));
|
|
147
|
+
return credentialsPath;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
app.get("/api/google/accounts", async (req, res) => {
|
|
151
|
+
const state = readState();
|
|
152
|
+
const authenticatedAccounts = await listAuthenticatedAccounts(state);
|
|
153
|
+
const accounts = listGoogleAccounts(state).map((account) => {
|
|
154
|
+
const activeScopes = account.services || [];
|
|
155
|
+
const services = uniqueServiceLabels(activeScopes).join(", ");
|
|
156
|
+
const hasCredentials = fs.existsSync(gogClientCredentialsPath(account.client));
|
|
157
|
+
return {
|
|
158
|
+
...account,
|
|
159
|
+
services,
|
|
160
|
+
activeScopes,
|
|
161
|
+
hasCredentials,
|
|
162
|
+
authenticated:
|
|
163
|
+
hasCredentials &&
|
|
164
|
+
(Boolean(account.authenticated) || accountIsAuthenticated({ account, authenticatedAccounts })),
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
res.json({
|
|
168
|
+
ok: true,
|
|
169
|
+
hasCompanyCredentials: fs.existsSync(gogClientCredentialsPath(kDefaultGoogleClient)),
|
|
170
|
+
hasPersonalCredentials: fs.existsSync(gogClientCredentialsPath("personal")),
|
|
171
|
+
accounts,
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
21
175
|
app.get("/api/google/status", async (req, res) => {
|
|
22
176
|
if (!(await isGatewayRunning())) {
|
|
23
177
|
return res.json({
|
|
@@ -25,106 +179,171 @@ const registerGoogleRoutes = ({
|
|
|
25
179
|
authenticated: false,
|
|
26
180
|
email: "",
|
|
27
181
|
services: "",
|
|
182
|
+
activeScopes: [],
|
|
28
183
|
});
|
|
29
184
|
}
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
} catch {}
|
|
45
|
-
}
|
|
185
|
+
const state = readState();
|
|
186
|
+
const selected = getSelectedAccount({
|
|
187
|
+
state,
|
|
188
|
+
accountId: String(req.query.accountId || ""),
|
|
189
|
+
fallbackToFirst: true,
|
|
190
|
+
});
|
|
191
|
+
if (!selected) {
|
|
192
|
+
return res.json({
|
|
193
|
+
hasCredentials: false,
|
|
194
|
+
authenticated: false,
|
|
195
|
+
email: "",
|
|
196
|
+
services: "",
|
|
197
|
+
activeScopes: [],
|
|
198
|
+
});
|
|
46
199
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.filter((v, i, a) => a.indexOf(v) === i)
|
|
56
|
-
.join(", ");
|
|
57
|
-
} catch {}
|
|
58
|
-
|
|
59
|
-
const status = {
|
|
200
|
+
const authenticatedAccounts = await listAuthenticatedAccounts(state);
|
|
201
|
+
const activeScopes = selected.services || [];
|
|
202
|
+
const services = uniqueServiceLabels(activeScopes).join(", ");
|
|
203
|
+
const hasCredentials = fs.existsSync(gogClientCredentialsPath(selected.client));
|
|
204
|
+
res.json({
|
|
205
|
+
accountId: selected.id,
|
|
206
|
+
client: selected.client,
|
|
207
|
+
personal: selected.personal,
|
|
60
208
|
hasCredentials,
|
|
61
|
-
authenticated
|
|
62
|
-
|
|
209
|
+
authenticated:
|
|
210
|
+
hasCredentials &&
|
|
211
|
+
(Boolean(selected.authenticated) ||
|
|
212
|
+
accountIsAuthenticated({ account: selected, authenticatedAccounts })),
|
|
213
|
+
email: selected.email,
|
|
63
214
|
services,
|
|
64
215
|
activeScopes,
|
|
65
|
-
};
|
|
66
|
-
res.json(status);
|
|
216
|
+
});
|
|
67
217
|
});
|
|
68
218
|
|
|
69
219
|
app.post("/api/google/credentials", async (req, res) => {
|
|
70
|
-
const
|
|
220
|
+
const body = req.body || {};
|
|
221
|
+
const clientId = String(body.clientId || "").trim();
|
|
222
|
+
const clientSecret = String(body.clientSecret || "").trim();
|
|
223
|
+
const email = String(body.email || "").trim();
|
|
224
|
+
const accountId = String(body.accountId || "").trim();
|
|
225
|
+
const personal = Boolean(body.personal);
|
|
226
|
+
const client = String(body.client || (personal ? "personal" : kDefaultGoogleClient)).trim()
|
|
227
|
+
|| kDefaultGoogleClient;
|
|
71
228
|
if (!clientId || !clientSecret || !email) {
|
|
72
229
|
return res.json({ ok: false, error: "Missing fields" });
|
|
73
230
|
}
|
|
74
231
|
|
|
75
232
|
try {
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
233
|
+
const state = readState();
|
|
234
|
+
const existing = accountId ? getGoogleAccountById(state, accountId) : null;
|
|
235
|
+
const legacyClientsForEmail = listGoogleAccounts(state)
|
|
236
|
+
.filter(
|
|
237
|
+
(entry) =>
|
|
238
|
+
String(entry.email || "").trim().toLowerCase() ===
|
|
239
|
+
email.toLowerCase(),
|
|
240
|
+
)
|
|
241
|
+
.map((entry) => String(entry.client || kDefaultGoogleClient).trim());
|
|
242
|
+
await clearStoredGoogleAuthForEmail({
|
|
243
|
+
email,
|
|
244
|
+
preferredClient: client,
|
|
245
|
+
extraClients: [
|
|
246
|
+
...legacyClientsForEmail,
|
|
247
|
+
String(existing?.client || "").trim(),
|
|
248
|
+
],
|
|
249
|
+
});
|
|
250
|
+
const credentialsPath = ensureClientCredentials({
|
|
251
|
+
client,
|
|
252
|
+
clientId,
|
|
253
|
+
clientSecret,
|
|
254
|
+
req,
|
|
255
|
+
});
|
|
256
|
+
const command = client === kDefaultGoogleClient
|
|
257
|
+
? `auth credentials set ${quoteShellArg(credentialsPath)}`
|
|
258
|
+
: `--client ${quoteShellArg(client)} auth credentials set ${quoteShellArg(credentialsPath)}`;
|
|
259
|
+
const result = await gogCmd(command, { quiet: true });
|
|
260
|
+
if (!result.ok) {
|
|
261
|
+
throw new Error(result.stderr || "Failed to set Google client credentials");
|
|
262
|
+
}
|
|
90
263
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
264
|
+
const { state: nextState, account } = upsertGoogleAccount({
|
|
265
|
+
state,
|
|
266
|
+
maxAccounts: kMaxGoogleAccounts,
|
|
267
|
+
account: {
|
|
268
|
+
id: existing?.id || accountId || createGoogleAccountId(),
|
|
269
|
+
email,
|
|
270
|
+
personal,
|
|
271
|
+
client,
|
|
272
|
+
services: body.services || existing?.services || kDefaultGoogleScopes,
|
|
273
|
+
authenticated: false,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
saveState(nextState);
|
|
277
|
+
syncBootstrapTools(req);
|
|
100
278
|
|
|
101
|
-
res.json({ ok: true });
|
|
279
|
+
res.json({ ok: true, accountId: account.id, account });
|
|
102
280
|
} catch (err) {
|
|
103
281
|
console.error("[alphaclaw] Failed to save Google credentials:", err);
|
|
104
282
|
res.json({ ok: false, error: err.message });
|
|
105
283
|
}
|
|
106
284
|
});
|
|
107
285
|
|
|
108
|
-
app.
|
|
109
|
-
|
|
110
|
-
|
|
286
|
+
app.post("/api/google/accounts", (req, res) => {
|
|
287
|
+
const body = req.body || {};
|
|
288
|
+
const email = String(body.email || "").trim();
|
|
289
|
+
const accountId = String(body.accountId || "").trim();
|
|
290
|
+
const personal = Boolean(body.personal);
|
|
291
|
+
const client = String(body.client || (personal ? "personal" : kDefaultGoogleClient)).trim()
|
|
292
|
+
|| kDefaultGoogleClient;
|
|
293
|
+
if (!email) {
|
|
294
|
+
return res.json({ ok: false, error: "Missing fields" });
|
|
295
|
+
}
|
|
296
|
+
if (!fs.existsSync(gogClientCredentialsPath(client))) {
|
|
297
|
+
return res.json({
|
|
298
|
+
ok: false,
|
|
299
|
+
error: "Credentials missing for selected client. Save credentials first.",
|
|
300
|
+
});
|
|
301
|
+
}
|
|
111
302
|
try {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
303
|
+
const state = readState();
|
|
304
|
+
const existing = accountId ? getGoogleAccountById(state, accountId) : null;
|
|
305
|
+
const { state: nextState, account } = upsertGoogleAccount({
|
|
306
|
+
state,
|
|
307
|
+
maxAccounts: kMaxGoogleAccounts,
|
|
308
|
+
account: {
|
|
309
|
+
id: existing?.id || accountId || createGoogleAccountId(),
|
|
310
|
+
email,
|
|
311
|
+
personal,
|
|
312
|
+
client,
|
|
313
|
+
services: body.services || existing?.services || kDefaultGoogleScopes,
|
|
314
|
+
authenticated: Boolean(existing?.authenticated),
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
saveState(nextState);
|
|
318
|
+
syncBootstrapTools(req);
|
|
319
|
+
res.json({ ok: true, accountId: account.id, account });
|
|
320
|
+
} catch (err) {
|
|
321
|
+
res.json({ ok: false, error: err.message });
|
|
322
|
+
}
|
|
323
|
+
});
|
|
117
324
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
325
|
+
app.get("/api/google/check", async (req, res) => {
|
|
326
|
+
const state = readState();
|
|
327
|
+
const account = getSelectedAccount({
|
|
328
|
+
state,
|
|
329
|
+
accountId: String(req.query.accountId || ""),
|
|
330
|
+
fallbackToFirst: true,
|
|
331
|
+
});
|
|
332
|
+
if (!account) return res.json({ error: "No Google account configured" });
|
|
333
|
+
|
|
334
|
+
const enabledServices = uniqueServiceLabels(account.services || []);
|
|
121
335
|
const results = {};
|
|
122
|
-
|
|
123
336
|
for (const svc of enabledServices) {
|
|
124
337
|
const cmd = API_TEST_COMMANDS[svc];
|
|
125
338
|
if (!cmd) continue;
|
|
126
|
-
|
|
127
|
-
|
|
339
|
+
const clientArg =
|
|
340
|
+
account.client === kDefaultGoogleClient
|
|
341
|
+
? ""
|
|
342
|
+
: `--client ${quoteShellArg(account.client)} `;
|
|
343
|
+
const result = await gogCmd(
|
|
344
|
+
`${clientArg}${cmd} --account ${quoteShellArg(account.email)}`,
|
|
345
|
+
{ quiet: true },
|
|
346
|
+
);
|
|
128
347
|
const stderr = result.stderr || "";
|
|
129
348
|
if (stderr.includes("has not been used") || stderr.includes("is not enabled")) {
|
|
130
349
|
const projectMatch = stderr.match(/project=(\d+)/);
|
|
@@ -135,7 +354,6 @@ const registerGoogleRoutes = ({
|
|
|
135
354
|
} else if (result.ok || stderr.includes("not found") || stderr.includes("Not Found")) {
|
|
136
355
|
results[svc] = { status: "ok", enableUrl: getApiEnableUrl(svc) };
|
|
137
356
|
} else {
|
|
138
|
-
console.log(`[alphaclaw] API check ${svc} error: ${result.stderr?.slice(0, 300)}`);
|
|
139
357
|
results[svc] = {
|
|
140
358
|
status: "error",
|
|
141
359
|
message: result.stderr?.slice(0, 200),
|
|
@@ -143,58 +361,47 @@ const registerGoogleRoutes = ({
|
|
|
143
361
|
};
|
|
144
362
|
}
|
|
145
363
|
}
|
|
146
|
-
|
|
147
|
-
res.json({ email, results });
|
|
364
|
+
res.json({ accountId: account.id, email: account.email, results });
|
|
148
365
|
});
|
|
149
366
|
|
|
150
367
|
app.post("/api/google/disconnect", async (req, res) => {
|
|
368
|
+
const accountId = String(req.body?.accountId || "").trim();
|
|
369
|
+
const state = readState();
|
|
370
|
+
const account = getSelectedAccount({ state, accountId, fallbackToFirst: true });
|
|
371
|
+
if (!account) return res.json({ ok: true });
|
|
151
372
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
);
|
|
163
|
-
if (exportResult.ok) {
|
|
164
|
-
try {
|
|
165
|
-
const tokenData = JSON.parse(fs.readFileSync("/tmp/gog-revoke.json", "utf8"));
|
|
166
|
-
if (tokenData.refresh_token) {
|
|
167
|
-
await fetch(`https://oauth2.googleapis.com/revoke?token=${tokenData.refresh_token}`, {
|
|
168
|
-
method: "POST",
|
|
169
|
-
});
|
|
170
|
-
console.log(`[alphaclaw] Revoked Google token for ${email}`);
|
|
171
|
-
}
|
|
172
|
-
fs.unlinkSync("/tmp/gog-revoke.json");
|
|
173
|
-
} catch {}
|
|
174
|
-
}
|
|
175
|
-
await gogCmd(`auth remove ${email} --force`);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
for (const f of [GOG_STATE_PATH, GOG_CREDENTIALS_PATH]) {
|
|
373
|
+
const revokeFile = `/tmp/gog-revoke-${Date.now()}.json`;
|
|
374
|
+
const clientArg =
|
|
375
|
+
account.client === kDefaultGoogleClient
|
|
376
|
+
? ""
|
|
377
|
+
: `--client ${quoteShellArg(account.client)} `;
|
|
378
|
+
const exportResult = await gogCmd(
|
|
379
|
+
`${clientArg}auth tokens export ${quoteShellArg(account.email)} --out ${quoteShellArg(revokeFile)} --overwrite`,
|
|
380
|
+
{ quiet: true },
|
|
381
|
+
);
|
|
382
|
+
if (exportResult.ok && fs.existsSync(revokeFile)) {
|
|
179
383
|
try {
|
|
180
|
-
fs.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
384
|
+
const tokenData = parseJsonSafe(fs.readFileSync(revokeFile, "utf8"), {});
|
|
385
|
+
if (tokenData.refresh_token) {
|
|
386
|
+
await fetch(`https://oauth2.googleapis.com/revoke?token=${tokenData.refresh_token}`, {
|
|
387
|
+
method: "POST",
|
|
388
|
+
});
|
|
185
389
|
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const stateStillExists = fs.existsSync(GOG_STATE_PATH);
|
|
190
|
-
const credsStillExists = fs.existsSync(GOG_CREDENTIALS_PATH);
|
|
191
|
-
if (stateStillExists || credsStillExists) {
|
|
192
|
-
console.error(
|
|
193
|
-
`[alphaclaw] Files survived deletion! state=${stateStillExists} creds=${credsStillExists}`,
|
|
194
|
-
);
|
|
390
|
+
} catch {}
|
|
195
391
|
}
|
|
196
|
-
|
|
197
|
-
|
|
392
|
+
try {
|
|
393
|
+
fs.unlinkSync(revokeFile);
|
|
394
|
+
} catch {}
|
|
395
|
+
await gogCmd(
|
|
396
|
+
`${clientArg}auth remove ${quoteShellArg(account.email)} --force`,
|
|
397
|
+
{ quiet: true },
|
|
398
|
+
);
|
|
399
|
+
const { state: nextState } = removeGoogleAccount({
|
|
400
|
+
state,
|
|
401
|
+
accountId: account.id,
|
|
402
|
+
});
|
|
403
|
+
saveState(nextState);
|
|
404
|
+
syncBootstrapTools(req);
|
|
198
405
|
res.json({ ok: true });
|
|
199
406
|
} catch (err) {
|
|
200
407
|
console.error("[alphaclaw] Google disconnect error:", err);
|
|
@@ -203,31 +410,44 @@ const registerGoogleRoutes = ({
|
|
|
203
410
|
});
|
|
204
411
|
|
|
205
412
|
app.get("/auth/google/start", (req, res) => {
|
|
206
|
-
const
|
|
413
|
+
const state = readState();
|
|
414
|
+
const requestedAccountId = String(req.query.accountId || "").trim();
|
|
415
|
+
const requestedClient = String(req.query.client || "").trim();
|
|
416
|
+
let account = requestedAccountId
|
|
417
|
+
? getGoogleAccountById(state, requestedAccountId)
|
|
418
|
+
: null;
|
|
419
|
+
if (!account && req.query.email) {
|
|
420
|
+
account = getGoogleAccountByEmailAndClient(
|
|
421
|
+
state,
|
|
422
|
+
String(req.query.email || "").trim(),
|
|
423
|
+
requestedClient || kDefaultGoogleClient,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
const client = account?.client || requestedClient || kDefaultGoogleClient;
|
|
427
|
+
const email = account?.email || String(req.query.email || "").trim();
|
|
207
428
|
const services = (
|
|
208
429
|
req.query.services ||
|
|
209
|
-
"
|
|
430
|
+
(account?.services || kDefaultGoogleScopes).join(",")
|
|
210
431
|
)
|
|
211
432
|
.split(",")
|
|
433
|
+
.map((scope) => String(scope || "").trim())
|
|
212
434
|
.filter(Boolean);
|
|
213
|
-
|
|
214
435
|
try {
|
|
215
|
-
const { clientId } = readGoogleCredentials();
|
|
436
|
+
const { clientId } = readGoogleCredentials(client);
|
|
216
437
|
if (!clientId) throw new Error("No client_id found");
|
|
217
|
-
|
|
218
438
|
const scopes = [
|
|
219
439
|
...BASE_SCOPES,
|
|
220
|
-
...services.map((
|
|
440
|
+
...services.map((scope) => SCOPE_MAP[scope]).filter(Boolean),
|
|
221
441
|
].join(" ");
|
|
222
|
-
console.log(
|
|
223
|
-
`[alphaclaw] Google OAuth scopes: services=${services.join(",")} resolved=${scopes}`,
|
|
224
|
-
);
|
|
225
|
-
|
|
226
442
|
const redirectUri = `${getBaseUrl(req)}/auth/google/callback`;
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
443
|
+
const encodedState = Buffer.from(
|
|
444
|
+
JSON.stringify({
|
|
445
|
+
accountId: account?.id || requestedAccountId || "",
|
|
446
|
+
client,
|
|
447
|
+
email,
|
|
448
|
+
services,
|
|
449
|
+
}),
|
|
450
|
+
).toString("base64url");
|
|
231
451
|
const authUrl = new URL("https://accounts.google.com/o/oauth2/auth");
|
|
232
452
|
authUrl.searchParams.set("client_id", clientId);
|
|
233
453
|
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
@@ -235,31 +455,41 @@ const registerGoogleRoutes = ({
|
|
|
235
455
|
authUrl.searchParams.set("scope", scopes);
|
|
236
456
|
authUrl.searchParams.set("access_type", "offline");
|
|
237
457
|
authUrl.searchParams.set("prompt", "consent");
|
|
238
|
-
authUrl.searchParams.set("state",
|
|
458
|
+
authUrl.searchParams.set("state", encodedState);
|
|
239
459
|
if (email) authUrl.searchParams.set("login_hint", email);
|
|
240
|
-
|
|
241
460
|
res.redirect(authUrl.toString());
|
|
242
461
|
} catch (err) {
|
|
243
462
|
console.error("[alphaclaw] Failed to start Google auth:", err);
|
|
244
|
-
res.redirect(
|
|
463
|
+
res.redirect(`/setup?google=error&message=${encodeURIComponent(err.message)}`);
|
|
245
464
|
}
|
|
246
465
|
});
|
|
247
466
|
|
|
248
467
|
app.get("/auth/google/callback", async (req, res) => {
|
|
249
468
|
const { code, error, state } = req.query;
|
|
250
|
-
if (error) return res.redirect(
|
|
469
|
+
if (error) return res.redirect(`/setup?google=error&message=${encodeURIComponent(error)}`);
|
|
251
470
|
if (!code) return res.redirect("/setup?google=error&message=no_code");
|
|
252
471
|
|
|
253
472
|
try {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
473
|
+
const decodedState = parseJsonSafe(
|
|
474
|
+
Buffer.from(String(state || ""), "base64url").toString(),
|
|
475
|
+
{},
|
|
476
|
+
);
|
|
477
|
+
const accountId = String(decodedState.accountId || "").trim();
|
|
478
|
+
const requestedClient = String(decodedState.client || "").trim();
|
|
479
|
+
const stateData = readState();
|
|
480
|
+
const existingAccount = accountId
|
|
481
|
+
? getGoogleAccountById(stateData, accountId)
|
|
482
|
+
: getGoogleAccountByEmailAndClient(
|
|
483
|
+
stateData,
|
|
484
|
+
String(decodedState.email || "").trim(),
|
|
485
|
+
requestedClient || kDefaultGoogleClient,
|
|
486
|
+
);
|
|
487
|
+
const client = existingAccount?.client || requestedClient || kDefaultGoogleClient;
|
|
488
|
+
const { clientId, clientSecret } = readGoogleCredentials(client);
|
|
489
|
+
if (!clientId || !clientSecret) {
|
|
490
|
+
throw new Error(`Google credentials missing for client "${client}"`);
|
|
491
|
+
}
|
|
261
492
|
const redirectUri = `${getBaseUrl(req)}/auth/google/callback`;
|
|
262
|
-
|
|
263
493
|
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
|
|
264
494
|
method: "POST",
|
|
265
495
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
@@ -271,42 +501,25 @@ const registerGoogleRoutes = ({
|
|
|
271
501
|
grant_type: "authorization_code",
|
|
272
502
|
}),
|
|
273
503
|
});
|
|
274
|
-
|
|
275
504
|
const tokens = await tokenRes.json();
|
|
276
505
|
if (!tokenRes.ok || tokens.error) {
|
|
277
|
-
|
|
278
|
-
`[alphaclaw] Google token exchange failed: status=${tokenRes.status} error=${tokens.error} desc=${tokens.error_description}`,
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
if (tokens.error) {
|
|
282
|
-
throw new Error(`Google token error: ${tokens.error_description || tokens.error}`);
|
|
506
|
+
throw new Error(`Google token error: ${tokens.error_description || tokens.error || "exchange_failed"}`);
|
|
283
507
|
}
|
|
284
508
|
|
|
285
|
-
if (!tokens.refresh_token) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
hasExisting = stateData.authenticated;
|
|
290
|
-
} catch {}
|
|
291
|
-
|
|
292
|
-
if (hasExisting) {
|
|
293
|
-
console.log(
|
|
294
|
-
"[alphaclaw] No new refresh token (already authorized), keeping existing",
|
|
295
|
-
);
|
|
296
|
-
} else {
|
|
297
|
-
throw new Error(
|
|
298
|
-
"No refresh token received. Revoke app access at myaccount.google.com/permissions and retry.",
|
|
299
|
-
);
|
|
300
|
-
}
|
|
509
|
+
if (!tokens.refresh_token && !existingAccount?.authenticated) {
|
|
510
|
+
throw new Error(
|
|
511
|
+
"No refresh token received. Revoke app access at myaccount.google.com/permissions and retry.",
|
|
512
|
+
);
|
|
301
513
|
}
|
|
302
514
|
|
|
515
|
+
let email = String(existingAccount?.email || decodedState.email || "").trim();
|
|
303
516
|
if (!email && tokens.access_token) {
|
|
304
517
|
try {
|
|
305
518
|
const infoRes = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", {
|
|
306
519
|
headers: { Authorization: `Bearer ${tokens.access_token}` },
|
|
307
520
|
});
|
|
308
521
|
const info = await infoRes.json();
|
|
309
|
-
email = info.email ||
|
|
522
|
+
email = String(info.email || "").trim();
|
|
310
523
|
} catch {}
|
|
311
524
|
}
|
|
312
525
|
|
|
@@ -314,73 +527,61 @@ const registerGoogleRoutes = ({
|
|
|
314
527
|
const tokenFile = `/tmp/gog-token-${Date.now()}.json`;
|
|
315
528
|
const tokenData = {
|
|
316
529
|
email,
|
|
317
|
-
client
|
|
530
|
+
client,
|
|
318
531
|
created_at: new Date().toISOString(),
|
|
319
532
|
refresh_token: tokens.refresh_token,
|
|
320
533
|
};
|
|
321
534
|
fs.writeFileSync(tokenFile, JSON.stringify(tokenData, null, 2));
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (!result.ok) {
|
|
330
|
-
console.error("[alphaclaw] Token import failed, trying gog auth add --manual");
|
|
331
|
-
const keyringDir = `${GOG_CONFIG_DIR}/keyring`;
|
|
332
|
-
fs.mkdirSync(keyringDir, { recursive: true });
|
|
333
|
-
fs.writeFileSync(
|
|
334
|
-
`${keyringDir}/token-${email}.json`,
|
|
335
|
-
JSON.stringify(tokenData, null, 2),
|
|
336
|
-
);
|
|
337
|
-
console.log(
|
|
338
|
-
`[alphaclaw] Token written directly to keyring: ${keyringDir}/token-${email}.json`,
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
|
|
535
|
+
const importCmd =
|
|
536
|
+
client === kDefaultGoogleClient
|
|
537
|
+
? `auth tokens import ${quoteShellArg(tokenFile)}`
|
|
538
|
+
: `--client ${quoteShellArg(client)} auth tokens import ${quoteShellArg(tokenFile)}`;
|
|
539
|
+
const result = await gogCmd(importCmd, { quiet: true });
|
|
342
540
|
try {
|
|
343
541
|
fs.unlinkSync(tokenFile);
|
|
344
542
|
} catch {}
|
|
543
|
+
if (!result.ok) {
|
|
544
|
+
throw new Error(result.stderr || "Failed to import Google token");
|
|
545
|
+
}
|
|
345
546
|
}
|
|
346
547
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
services = decoded.services || [];
|
|
351
|
-
} catch {}
|
|
352
|
-
|
|
548
|
+
const requestedServices = Array.isArray(decodedState.services)
|
|
549
|
+
? decodedState.services
|
|
550
|
+
: [];
|
|
353
551
|
const grantedServices = tokens.scope
|
|
354
552
|
? tokens.scope
|
|
355
553
|
.split(" ")
|
|
356
|
-
.map((
|
|
554
|
+
.map((scope) => REVERSE_SCOPE_MAP[scope])
|
|
357
555
|
.filter(Boolean)
|
|
358
|
-
:
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
GOG_STATE_PATH,
|
|
365
|
-
JSON.stringify({
|
|
556
|
+
: requestedServices;
|
|
557
|
+
const { state: nextState, account } = upsertGoogleAccount({
|
|
558
|
+
state: stateData,
|
|
559
|
+
maxAccounts: kMaxGoogleAccounts,
|
|
560
|
+
account: {
|
|
561
|
+
id: existingAccount?.id || accountId || createGoogleAccountId(),
|
|
366
562
|
email,
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
services: grantedServices,
|
|
563
|
+
personal: Boolean(existingAccount?.personal),
|
|
564
|
+
client,
|
|
565
|
+
services: grantedServices.length ? grantedServices : requestedServices,
|
|
370
566
|
authenticated: true,
|
|
371
|
-
}
|
|
372
|
-
);
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
saveState(nextState);
|
|
570
|
+
syncBootstrapTools(req);
|
|
373
571
|
|
|
572
|
+
const safeEmail = String(email || "").replace(/'/g, "\\'");
|
|
573
|
+
const safeAccountId = String(account.id || "").replace(/'/g, "\\'");
|
|
374
574
|
res.send(`<!DOCTYPE html><html><body><script>
|
|
375
|
-
window.opener?.postMessage({ google: 'success', email: '${
|
|
575
|
+
window.opener?.postMessage({ google: 'success', accountId: '${safeAccountId}', email: '${safeEmail}' }, '*');
|
|
376
576
|
window.close();
|
|
377
577
|
</script><p>Google connected! You can close this window.</p></body></html>`);
|
|
378
578
|
} catch (err) {
|
|
379
579
|
console.error("[alphaclaw] Google OAuth callback error:", err);
|
|
580
|
+
const safeMessage = String(err.message || "unknown_error").replace(/'/g, "\\'");
|
|
380
581
|
res.send(`<!DOCTYPE html><html><body><script>
|
|
381
|
-
window.opener?.postMessage({ google: 'error', message: '${
|
|
582
|
+
window.opener?.postMessage({ google: 'error', message: '${safeMessage}' }, '*');
|
|
382
583
|
window.close();
|
|
383
|
-
</script><p>Error: ${
|
|
584
|
+
</script><p>Error: ${safeMessage}. You can close this window.</p></body></html>`);
|
|
384
585
|
}
|
|
385
586
|
});
|
|
386
587
|
};
|