@chrysb/alphaclaw 0.3.5-beta.1 → 0.4.1-beta.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 +1 -31
- package/lib/public/assets/icons/google_icon.svg +8 -0
- package/lib/public/css/explorer.css +53 -0
- package/lib/public/css/shell.css +21 -19
- package/lib/public/css/theme.css +17 -0
- package/lib/public/js/app.js +205 -109
- package/lib/public/js/components/credentials-modal.js +36 -8
- package/lib/public/js/components/file-tree.js +212 -22
- package/lib/public/js/components/file-viewer/editor-surface.js +5 -0
- package/lib/public/js/components/file-viewer/index.js +47 -6
- package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -0
- package/lib/public/js/components/file-viewer/status-banners.js +11 -6
- package/lib/public/js/components/file-viewer/toolbar.js +56 -1
- package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +6 -0
- package/lib/public/js/components/file-viewer/use-file-diff.js +11 -0
- package/lib/public/js/components/file-viewer/use-file-loader.js +12 -2
- package/lib/public/js/components/file-viewer/use-file-viewer.js +142 -15
- package/lib/public/js/components/google/account-row.js +131 -0
- package/lib/public/js/components/google/add-account-modal.js +93 -0
- package/lib/public/js/components/google/gmail-setup-wizard.js +450 -0
- package/lib/public/js/components/google/gmail-watch-toggle.js +81 -0
- package/lib/public/js/components/google/index.js +553 -0
- package/lib/public/js/components/google/use-gmail-watch.js +140 -0
- package/lib/public/js/components/google/use-google-accounts.js +41 -0
- package/lib/public/js/components/icons.js +26 -0
- package/lib/public/js/components/scope-picker.js +1 -1
- package/lib/public/js/components/sidebar-git-panel.js +48 -20
- package/lib/public/js/components/sidebar.js +93 -75
- package/lib/public/js/components/toast.js +11 -7
- package/lib/public/js/components/usage-tab/constants.js +31 -0
- package/lib/public/js/components/usage-tab/formatters.js +24 -0
- package/lib/public/js/components/usage-tab/index.js +72 -0
- package/lib/public/js/components/usage-tab/overview-section.js +147 -0
- package/lib/public/js/components/usage-tab/sessions-section.js +175 -0
- package/lib/public/js/components/usage-tab/use-usage-tab.js +241 -0
- package/lib/public/js/components/webhooks.js +182 -129
- package/lib/public/js/lib/api.js +178 -9
- package/lib/public/js/lib/browse-file-policies.js +29 -11
- package/lib/public/js/lib/format.js +71 -0
- package/lib/public/js/lib/syntax-highlighters/index.js +6 -5
- package/lib/public/shared/browse-file-policies.json +13 -0
- package/lib/server/constants.js +47 -7
- package/lib/server/gmail-push.js +109 -0
- package/lib/server/gmail-serve.js +254 -0
- package/lib/server/gmail-watch.js +725 -0
- package/lib/server/google-state.js +317 -0
- package/lib/server/helpers.js +17 -11
- package/lib/server/internal-files-migration.js +31 -3
- 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/index.js +90 -2
- package/lib/server/routes/gmail.js +128 -0
- package/lib/server/routes/google.js +433 -213
- package/lib/server/routes/system.js +107 -0
- package/lib/server/routes/usage.js +29 -2
- package/lib/server/routes/webhooks.js +52 -17
- package/lib/server/usage-db.js +283 -15
- package/lib/server/watchdog.js +66 -0
- package/lib/server/webhook-middleware.js +99 -1
- package/lib/server/webhooks.js +214 -65
- package/lib/server.js +27 -0
- package/lib/setup/gitignore +6 -0
- package/lib/setup/hourly-git-sync.sh +29 -2
- package/package.json +1 -1
- package/lib/public/js/components/google.js +0 -228
- package/lib/public/js/components/usage-tab.js +0 -531
|
@@ -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,190 @@ 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
|
-
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
app.get("/api/google/credentials", (req, res) => {
|
|
220
|
+
const state = readState();
|
|
221
|
+
const accountId = String(req.query.accountId || "").trim();
|
|
222
|
+
const requestedClient = String(req.query.client || "").trim();
|
|
223
|
+
const account = accountId ? getGoogleAccountById(state, accountId) : null;
|
|
224
|
+
const client =
|
|
225
|
+
String(account?.client || requestedClient || kDefaultGoogleClient).trim()
|
|
226
|
+
|| kDefaultGoogleClient;
|
|
227
|
+
const credentials = readGoogleCredentials(client);
|
|
228
|
+
const hasCredentials = Boolean(credentials.clientId && credentials.clientSecret);
|
|
229
|
+
res.json({
|
|
230
|
+
ok: true,
|
|
231
|
+
client,
|
|
232
|
+
hasCredentials,
|
|
233
|
+
clientId: credentials.clientId || "",
|
|
234
|
+
clientSecret: credentials.clientSecret || "",
|
|
235
|
+
});
|
|
67
236
|
});
|
|
68
237
|
|
|
69
238
|
app.post("/api/google/credentials", async (req, res) => {
|
|
70
|
-
const
|
|
239
|
+
const body = req.body || {};
|
|
240
|
+
const clientId = String(body.clientId || "").trim();
|
|
241
|
+
const clientSecret = String(body.clientSecret || "").trim();
|
|
242
|
+
const email = String(body.email || "").trim();
|
|
243
|
+
const accountId = String(body.accountId || "").trim();
|
|
244
|
+
const personal = Boolean(body.personal);
|
|
245
|
+
const client = String(body.client || (personal ? "personal" : kDefaultGoogleClient)).trim()
|
|
246
|
+
|| kDefaultGoogleClient;
|
|
71
247
|
if (!clientId || !clientSecret || !email) {
|
|
72
248
|
return res.json({ ok: false, error: "Missing fields" });
|
|
73
249
|
}
|
|
74
250
|
|
|
75
251
|
try {
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
252
|
+
const state = readState();
|
|
253
|
+
const existing = accountId ? getGoogleAccountById(state, accountId) : null;
|
|
254
|
+
const legacyClientsForEmail = listGoogleAccounts(state)
|
|
255
|
+
.filter(
|
|
256
|
+
(entry) =>
|
|
257
|
+
String(entry.email || "").trim().toLowerCase() ===
|
|
258
|
+
email.toLowerCase(),
|
|
259
|
+
)
|
|
260
|
+
.map((entry) => String(entry.client || kDefaultGoogleClient).trim());
|
|
261
|
+
await clearStoredGoogleAuthForEmail({
|
|
262
|
+
email,
|
|
263
|
+
preferredClient: client,
|
|
264
|
+
extraClients: [
|
|
265
|
+
...legacyClientsForEmail,
|
|
266
|
+
String(existing?.client || "").trim(),
|
|
267
|
+
],
|
|
268
|
+
});
|
|
269
|
+
const credentialsPath = ensureClientCredentials({
|
|
270
|
+
client,
|
|
271
|
+
clientId,
|
|
272
|
+
clientSecret,
|
|
273
|
+
req,
|
|
274
|
+
});
|
|
275
|
+
const command = client === kDefaultGoogleClient
|
|
276
|
+
? `auth credentials set ${quoteShellArg(credentialsPath)}`
|
|
277
|
+
: `--client ${quoteShellArg(client)} auth credentials set ${quoteShellArg(credentialsPath)}`;
|
|
278
|
+
const result = await gogCmd(command, { quiet: true });
|
|
279
|
+
if (!result.ok) {
|
|
280
|
+
throw new Error(result.stderr || "Failed to set Google client credentials");
|
|
281
|
+
}
|
|
90
282
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
283
|
+
const { state: nextState, account } = upsertGoogleAccount({
|
|
284
|
+
state,
|
|
285
|
+
maxAccounts: kMaxGoogleAccounts,
|
|
286
|
+
account: {
|
|
287
|
+
id: existing?.id || accountId || createGoogleAccountId(),
|
|
288
|
+
email,
|
|
289
|
+
personal,
|
|
290
|
+
client,
|
|
291
|
+
services: body.services || existing?.services || kDefaultGoogleScopes,
|
|
292
|
+
authenticated: false,
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
saveState(nextState);
|
|
296
|
+
syncBootstrapTools(req);
|
|
100
297
|
|
|
101
|
-
res.json({ ok: true });
|
|
298
|
+
res.json({ ok: true, accountId: account.id, account });
|
|
102
299
|
} catch (err) {
|
|
103
300
|
console.error("[alphaclaw] Failed to save Google credentials:", err);
|
|
104
301
|
res.json({ ok: false, error: err.message });
|
|
105
302
|
}
|
|
106
303
|
});
|
|
107
304
|
|
|
108
|
-
app.
|
|
109
|
-
|
|
110
|
-
|
|
305
|
+
app.post("/api/google/accounts", (req, res) => {
|
|
306
|
+
const body = req.body || {};
|
|
307
|
+
const email = String(body.email || "").trim();
|
|
308
|
+
const accountId = String(body.accountId || "").trim();
|
|
309
|
+
const personal = Boolean(body.personal);
|
|
310
|
+
const client = String(body.client || (personal ? "personal" : kDefaultGoogleClient)).trim()
|
|
311
|
+
|| kDefaultGoogleClient;
|
|
312
|
+
if (!email) {
|
|
313
|
+
return res.json({ ok: false, error: "Missing fields" });
|
|
314
|
+
}
|
|
315
|
+
if (!fs.existsSync(gogClientCredentialsPath(client))) {
|
|
316
|
+
return res.json({
|
|
317
|
+
ok: false,
|
|
318
|
+
error: "Credentials missing for selected client. Save credentials first.",
|
|
319
|
+
});
|
|
320
|
+
}
|
|
111
321
|
try {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
322
|
+
const state = readState();
|
|
323
|
+
const existing = accountId ? getGoogleAccountById(state, accountId) : null;
|
|
324
|
+
const { state: nextState, account } = upsertGoogleAccount({
|
|
325
|
+
state,
|
|
326
|
+
maxAccounts: kMaxGoogleAccounts,
|
|
327
|
+
account: {
|
|
328
|
+
id: existing?.id || accountId || createGoogleAccountId(),
|
|
329
|
+
email,
|
|
330
|
+
personal,
|
|
331
|
+
client,
|
|
332
|
+
services: body.services || existing?.services || kDefaultGoogleScopes,
|
|
333
|
+
authenticated: Boolean(existing?.authenticated),
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
saveState(nextState);
|
|
337
|
+
syncBootstrapTools(req);
|
|
338
|
+
res.json({ ok: true, accountId: account.id, account });
|
|
339
|
+
} catch (err) {
|
|
340
|
+
res.json({ ok: false, error: err.message });
|
|
341
|
+
}
|
|
342
|
+
});
|
|
117
343
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
344
|
+
app.get("/api/google/check", async (req, res) => {
|
|
345
|
+
const state = readState();
|
|
346
|
+
const account = getSelectedAccount({
|
|
347
|
+
state,
|
|
348
|
+
accountId: String(req.query.accountId || ""),
|
|
349
|
+
fallbackToFirst: true,
|
|
350
|
+
});
|
|
351
|
+
if (!account) return res.json({ error: "No Google account configured" });
|
|
352
|
+
|
|
353
|
+
const enabledServices = uniqueServiceLabels(account.services || []);
|
|
121
354
|
const results = {};
|
|
122
|
-
|
|
123
355
|
for (const svc of enabledServices) {
|
|
124
356
|
const cmd = API_TEST_COMMANDS[svc];
|
|
125
357
|
if (!cmd) continue;
|
|
126
|
-
|
|
127
|
-
|
|
358
|
+
const clientArg =
|
|
359
|
+
account.client === kDefaultGoogleClient
|
|
360
|
+
? ""
|
|
361
|
+
: `--client ${quoteShellArg(account.client)} `;
|
|
362
|
+
const result = await gogCmd(
|
|
363
|
+
`${clientArg}${cmd} --account ${quoteShellArg(account.email)}`,
|
|
364
|
+
{ quiet: true },
|
|
365
|
+
);
|
|
128
366
|
const stderr = result.stderr || "";
|
|
129
367
|
if (stderr.includes("has not been used") || stderr.includes("is not enabled")) {
|
|
130
368
|
const projectMatch = stderr.match(/project=(\d+)/);
|
|
@@ -135,7 +373,6 @@ const registerGoogleRoutes = ({
|
|
|
135
373
|
} else if (result.ok || stderr.includes("not found") || stderr.includes("Not Found")) {
|
|
136
374
|
results[svc] = { status: "ok", enableUrl: getApiEnableUrl(svc) };
|
|
137
375
|
} else {
|
|
138
|
-
console.log(`[alphaclaw] API check ${svc} error: ${result.stderr?.slice(0, 300)}`);
|
|
139
376
|
results[svc] = {
|
|
140
377
|
status: "error",
|
|
141
378
|
message: result.stderr?.slice(0, 200),
|
|
@@ -143,58 +380,47 @@ const registerGoogleRoutes = ({
|
|
|
143
380
|
};
|
|
144
381
|
}
|
|
145
382
|
}
|
|
146
|
-
|
|
147
|
-
res.json({ email, results });
|
|
383
|
+
res.json({ accountId: account.id, email: account.email, results });
|
|
148
384
|
});
|
|
149
385
|
|
|
150
386
|
app.post("/api/google/disconnect", async (req, res) => {
|
|
387
|
+
const accountId = String(req.body?.accountId || "").trim();
|
|
388
|
+
const state = readState();
|
|
389
|
+
const account = getSelectedAccount({ state, accountId, fallbackToFirst: true });
|
|
390
|
+
if (!account) return res.json({ ok: true });
|
|
151
391
|
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]) {
|
|
392
|
+
const revokeFile = `/tmp/gog-revoke-${Date.now()}.json`;
|
|
393
|
+
const clientArg =
|
|
394
|
+
account.client === kDefaultGoogleClient
|
|
395
|
+
? ""
|
|
396
|
+
: `--client ${quoteShellArg(account.client)} `;
|
|
397
|
+
const exportResult = await gogCmd(
|
|
398
|
+
`${clientArg}auth tokens export ${quoteShellArg(account.email)} --out ${quoteShellArg(revokeFile)} --overwrite`,
|
|
399
|
+
{ quiet: true },
|
|
400
|
+
);
|
|
401
|
+
if (exportResult.ok && fs.existsSync(revokeFile)) {
|
|
179
402
|
try {
|
|
180
|
-
fs.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
403
|
+
const tokenData = parseJsonSafe(fs.readFileSync(revokeFile, "utf8"), {});
|
|
404
|
+
if (tokenData.refresh_token) {
|
|
405
|
+
await fetch(`https://oauth2.googleapis.com/revoke?token=${tokenData.refresh_token}`, {
|
|
406
|
+
method: "POST",
|
|
407
|
+
});
|
|
185
408
|
}
|
|
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
|
-
);
|
|
409
|
+
} catch {}
|
|
195
410
|
}
|
|
196
|
-
|
|
197
|
-
|
|
411
|
+
try {
|
|
412
|
+
fs.unlinkSync(revokeFile);
|
|
413
|
+
} catch {}
|
|
414
|
+
await gogCmd(
|
|
415
|
+
`${clientArg}auth remove ${quoteShellArg(account.email)} --force`,
|
|
416
|
+
{ quiet: true },
|
|
417
|
+
);
|
|
418
|
+
const { state: nextState } = removeGoogleAccount({
|
|
419
|
+
state,
|
|
420
|
+
accountId: account.id,
|
|
421
|
+
});
|
|
422
|
+
saveState(nextState);
|
|
423
|
+
syncBootstrapTools(req);
|
|
198
424
|
res.json({ ok: true });
|
|
199
425
|
} catch (err) {
|
|
200
426
|
console.error("[alphaclaw] Google disconnect error:", err);
|
|
@@ -203,31 +429,44 @@ const registerGoogleRoutes = ({
|
|
|
203
429
|
});
|
|
204
430
|
|
|
205
431
|
app.get("/auth/google/start", (req, res) => {
|
|
206
|
-
const
|
|
432
|
+
const state = readState();
|
|
433
|
+
const requestedAccountId = String(req.query.accountId || "").trim();
|
|
434
|
+
const requestedClient = String(req.query.client || "").trim();
|
|
435
|
+
let account = requestedAccountId
|
|
436
|
+
? getGoogleAccountById(state, requestedAccountId)
|
|
437
|
+
: null;
|
|
438
|
+
if (!account && req.query.email) {
|
|
439
|
+
account = getGoogleAccountByEmailAndClient(
|
|
440
|
+
state,
|
|
441
|
+
String(req.query.email || "").trim(),
|
|
442
|
+
requestedClient || kDefaultGoogleClient,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const client = account?.client || requestedClient || kDefaultGoogleClient;
|
|
446
|
+
const email = account?.email || String(req.query.email || "").trim();
|
|
207
447
|
const services = (
|
|
208
448
|
req.query.services ||
|
|
209
|
-
"
|
|
449
|
+
(account?.services || kDefaultGoogleScopes).join(",")
|
|
210
450
|
)
|
|
211
451
|
.split(",")
|
|
452
|
+
.map((scope) => String(scope || "").trim())
|
|
212
453
|
.filter(Boolean);
|
|
213
|
-
|
|
214
454
|
try {
|
|
215
|
-
const { clientId } = readGoogleCredentials();
|
|
455
|
+
const { clientId } = readGoogleCredentials(client);
|
|
216
456
|
if (!clientId) throw new Error("No client_id found");
|
|
217
|
-
|
|
218
457
|
const scopes = [
|
|
219
458
|
...BASE_SCOPES,
|
|
220
|
-
...services.map((
|
|
459
|
+
...services.map((scope) => SCOPE_MAP[scope]).filter(Boolean),
|
|
221
460
|
].join(" ");
|
|
222
|
-
console.log(
|
|
223
|
-
`[alphaclaw] Google OAuth scopes: services=${services.join(",")} resolved=${scopes}`,
|
|
224
|
-
);
|
|
225
|
-
|
|
226
461
|
const redirectUri = `${getBaseUrl(req)}/auth/google/callback`;
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
462
|
+
const encodedState = Buffer.from(
|
|
463
|
+
JSON.stringify({
|
|
464
|
+
accountId: account?.id || requestedAccountId || "",
|
|
465
|
+
client,
|
|
466
|
+
email,
|
|
467
|
+
services,
|
|
468
|
+
}),
|
|
469
|
+
).toString("base64url");
|
|
231
470
|
const authUrl = new URL("https://accounts.google.com/o/oauth2/auth");
|
|
232
471
|
authUrl.searchParams.set("client_id", clientId);
|
|
233
472
|
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
@@ -235,31 +474,41 @@ const registerGoogleRoutes = ({
|
|
|
235
474
|
authUrl.searchParams.set("scope", scopes);
|
|
236
475
|
authUrl.searchParams.set("access_type", "offline");
|
|
237
476
|
authUrl.searchParams.set("prompt", "consent");
|
|
238
|
-
authUrl.searchParams.set("state",
|
|
477
|
+
authUrl.searchParams.set("state", encodedState);
|
|
239
478
|
if (email) authUrl.searchParams.set("login_hint", email);
|
|
240
|
-
|
|
241
479
|
res.redirect(authUrl.toString());
|
|
242
480
|
} catch (err) {
|
|
243
481
|
console.error("[alphaclaw] Failed to start Google auth:", err);
|
|
244
|
-
res.redirect(
|
|
482
|
+
res.redirect(`/setup?google=error&message=${encodeURIComponent(err.message)}`);
|
|
245
483
|
}
|
|
246
484
|
});
|
|
247
485
|
|
|
248
486
|
app.get("/auth/google/callback", async (req, res) => {
|
|
249
487
|
const { code, error, state } = req.query;
|
|
250
|
-
if (error) return res.redirect(
|
|
488
|
+
if (error) return res.redirect(`/setup?google=error&message=${encodeURIComponent(error)}`);
|
|
251
489
|
if (!code) return res.redirect("/setup?google=error&message=no_code");
|
|
252
490
|
|
|
253
491
|
try {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
492
|
+
const decodedState = parseJsonSafe(
|
|
493
|
+
Buffer.from(String(state || ""), "base64url").toString(),
|
|
494
|
+
{},
|
|
495
|
+
);
|
|
496
|
+
const accountId = String(decodedState.accountId || "").trim();
|
|
497
|
+
const requestedClient = String(decodedState.client || "").trim();
|
|
498
|
+
const stateData = readState();
|
|
499
|
+
const existingAccount = accountId
|
|
500
|
+
? getGoogleAccountById(stateData, accountId)
|
|
501
|
+
: getGoogleAccountByEmailAndClient(
|
|
502
|
+
stateData,
|
|
503
|
+
String(decodedState.email || "").trim(),
|
|
504
|
+
requestedClient || kDefaultGoogleClient,
|
|
505
|
+
);
|
|
506
|
+
const client = existingAccount?.client || requestedClient || kDefaultGoogleClient;
|
|
507
|
+
const { clientId, clientSecret } = readGoogleCredentials(client);
|
|
508
|
+
if (!clientId || !clientSecret) {
|
|
509
|
+
throw new Error(`Google credentials missing for client "${client}"`);
|
|
510
|
+
}
|
|
261
511
|
const redirectUri = `${getBaseUrl(req)}/auth/google/callback`;
|
|
262
|
-
|
|
263
512
|
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
|
|
264
513
|
method: "POST",
|
|
265
514
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
@@ -271,42 +520,25 @@ const registerGoogleRoutes = ({
|
|
|
271
520
|
grant_type: "authorization_code",
|
|
272
521
|
}),
|
|
273
522
|
});
|
|
274
|
-
|
|
275
523
|
const tokens = await tokenRes.json();
|
|
276
524
|
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}`);
|
|
525
|
+
throw new Error(`Google token error: ${tokens.error_description || tokens.error || "exchange_failed"}`);
|
|
283
526
|
}
|
|
284
527
|
|
|
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
|
-
}
|
|
528
|
+
if (!tokens.refresh_token && !existingAccount?.authenticated) {
|
|
529
|
+
throw new Error(
|
|
530
|
+
"No refresh token received. Revoke app access at myaccount.google.com/permissions and retry.",
|
|
531
|
+
);
|
|
301
532
|
}
|
|
302
533
|
|
|
534
|
+
let email = String(existingAccount?.email || decodedState.email || "").trim();
|
|
303
535
|
if (!email && tokens.access_token) {
|
|
304
536
|
try {
|
|
305
537
|
const infoRes = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", {
|
|
306
538
|
headers: { Authorization: `Bearer ${tokens.access_token}` },
|
|
307
539
|
});
|
|
308
540
|
const info = await infoRes.json();
|
|
309
|
-
email = info.email ||
|
|
541
|
+
email = String(info.email || "").trim();
|
|
310
542
|
} catch {}
|
|
311
543
|
}
|
|
312
544
|
|
|
@@ -314,73 +546,61 @@ const registerGoogleRoutes = ({
|
|
|
314
546
|
const tokenFile = `/tmp/gog-token-${Date.now()}.json`;
|
|
315
547
|
const tokenData = {
|
|
316
548
|
email,
|
|
317
|
-
client
|
|
549
|
+
client,
|
|
318
550
|
created_at: new Date().toISOString(),
|
|
319
551
|
refresh_token: tokens.refresh_token,
|
|
320
552
|
};
|
|
321
553
|
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
|
-
|
|
554
|
+
const importCmd =
|
|
555
|
+
client === kDefaultGoogleClient
|
|
556
|
+
? `auth tokens import ${quoteShellArg(tokenFile)}`
|
|
557
|
+
: `--client ${quoteShellArg(client)} auth tokens import ${quoteShellArg(tokenFile)}`;
|
|
558
|
+
const result = await gogCmd(importCmd, { quiet: true });
|
|
342
559
|
try {
|
|
343
560
|
fs.unlinkSync(tokenFile);
|
|
344
561
|
} catch {}
|
|
562
|
+
if (!result.ok) {
|
|
563
|
+
throw new Error(result.stderr || "Failed to import Google token");
|
|
564
|
+
}
|
|
345
565
|
}
|
|
346
566
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
services = decoded.services || [];
|
|
351
|
-
} catch {}
|
|
352
|
-
|
|
567
|
+
const requestedServices = Array.isArray(decodedState.services)
|
|
568
|
+
? decodedState.services
|
|
569
|
+
: [];
|
|
353
570
|
const grantedServices = tokens.scope
|
|
354
571
|
? tokens.scope
|
|
355
572
|
.split(" ")
|
|
356
|
-
.map((
|
|
573
|
+
.map((scope) => REVERSE_SCOPE_MAP[scope])
|
|
357
574
|
.filter(Boolean)
|
|
358
|
-
:
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
GOG_STATE_PATH,
|
|
365
|
-
JSON.stringify({
|
|
575
|
+
: requestedServices;
|
|
576
|
+
const { state: nextState, account } = upsertGoogleAccount({
|
|
577
|
+
state: stateData,
|
|
578
|
+
maxAccounts: kMaxGoogleAccounts,
|
|
579
|
+
account: {
|
|
580
|
+
id: existingAccount?.id || accountId || createGoogleAccountId(),
|
|
366
581
|
email,
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
services: grantedServices,
|
|
582
|
+
personal: Boolean(existingAccount?.personal),
|
|
583
|
+
client,
|
|
584
|
+
services: grantedServices.length ? grantedServices : requestedServices,
|
|
370
585
|
authenticated: true,
|
|
371
|
-
}
|
|
372
|
-
);
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
saveState(nextState);
|
|
589
|
+
syncBootstrapTools(req);
|
|
373
590
|
|
|
591
|
+
const safeEmail = String(email || "").replace(/'/g, "\\'");
|
|
592
|
+
const safeAccountId = String(account.id || "").replace(/'/g, "\\'");
|
|
374
593
|
res.send(`<!DOCTYPE html><html><body><script>
|
|
375
|
-
window.opener?.postMessage({ google: 'success', email: '${
|
|
594
|
+
window.opener?.postMessage({ google: 'success', accountId: '${safeAccountId}', email: '${safeEmail}' }, '*');
|
|
376
595
|
window.close();
|
|
377
596
|
</script><p>Google connected! You can close this window.</p></body></html>`);
|
|
378
597
|
} catch (err) {
|
|
379
598
|
console.error("[alphaclaw] Google OAuth callback error:", err);
|
|
599
|
+
const safeMessage = String(err.message || "unknown_error").replace(/'/g, "\\'");
|
|
380
600
|
res.send(`<!DOCTYPE html><html><body><script>
|
|
381
|
-
window.opener?.postMessage({ google: 'error', message: '${
|
|
601
|
+
window.opener?.postMessage({ google: 'error', message: '${safeMessage}' }, '*');
|
|
382
602
|
window.close();
|
|
383
|
-
</script><p>Error: ${
|
|
603
|
+
</script><p>Error: ${safeMessage}. You can close this window.</p></body></html>`);
|
|
384
604
|
}
|
|
385
605
|
});
|
|
386
606
|
};
|