@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.
Files changed (68) hide show
  1. package/bin/alphaclaw.js +1 -31
  2. package/lib/public/assets/icons/google_icon.svg +8 -0
  3. package/lib/public/css/explorer.css +53 -0
  4. package/lib/public/css/shell.css +21 -19
  5. package/lib/public/css/theme.css +17 -0
  6. package/lib/public/js/app.js +205 -109
  7. package/lib/public/js/components/credentials-modal.js +36 -8
  8. package/lib/public/js/components/file-tree.js +212 -22
  9. package/lib/public/js/components/file-viewer/editor-surface.js +5 -0
  10. package/lib/public/js/components/file-viewer/index.js +47 -6
  11. package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -0
  12. package/lib/public/js/components/file-viewer/status-banners.js +11 -6
  13. package/lib/public/js/components/file-viewer/toolbar.js +56 -1
  14. package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +6 -0
  15. package/lib/public/js/components/file-viewer/use-file-diff.js +11 -0
  16. package/lib/public/js/components/file-viewer/use-file-loader.js +12 -2
  17. package/lib/public/js/components/file-viewer/use-file-viewer.js +142 -15
  18. package/lib/public/js/components/google/account-row.js +131 -0
  19. package/lib/public/js/components/google/add-account-modal.js +93 -0
  20. package/lib/public/js/components/google/gmail-setup-wizard.js +450 -0
  21. package/lib/public/js/components/google/gmail-watch-toggle.js +81 -0
  22. package/lib/public/js/components/google/index.js +553 -0
  23. package/lib/public/js/components/google/use-gmail-watch.js +140 -0
  24. package/lib/public/js/components/google/use-google-accounts.js +41 -0
  25. package/lib/public/js/components/icons.js +26 -0
  26. package/lib/public/js/components/scope-picker.js +1 -1
  27. package/lib/public/js/components/sidebar-git-panel.js +48 -20
  28. package/lib/public/js/components/sidebar.js +93 -75
  29. package/lib/public/js/components/toast.js +11 -7
  30. package/lib/public/js/components/usage-tab/constants.js +31 -0
  31. package/lib/public/js/components/usage-tab/formatters.js +24 -0
  32. package/lib/public/js/components/usage-tab/index.js +72 -0
  33. package/lib/public/js/components/usage-tab/overview-section.js +147 -0
  34. package/lib/public/js/components/usage-tab/sessions-section.js +175 -0
  35. package/lib/public/js/components/usage-tab/use-usage-tab.js +241 -0
  36. package/lib/public/js/components/webhooks.js +182 -129
  37. package/lib/public/js/lib/api.js +178 -9
  38. package/lib/public/js/lib/browse-file-policies.js +29 -11
  39. package/lib/public/js/lib/format.js +71 -0
  40. package/lib/public/js/lib/syntax-highlighters/index.js +6 -5
  41. package/lib/public/shared/browse-file-policies.json +13 -0
  42. package/lib/server/constants.js +47 -7
  43. package/lib/server/gmail-push.js +109 -0
  44. package/lib/server/gmail-serve.js +254 -0
  45. package/lib/server/gmail-watch.js +725 -0
  46. package/lib/server/google-state.js +317 -0
  47. package/lib/server/helpers.js +17 -11
  48. package/lib/server/internal-files-migration.js +31 -3
  49. package/lib/server/onboarding/github.js +21 -2
  50. package/lib/server/onboarding/index.js +1 -3
  51. package/lib/server/onboarding/openclaw.js +3 -0
  52. package/lib/server/onboarding/workspace.js +40 -0
  53. package/lib/server/routes/browse/index.js +90 -2
  54. package/lib/server/routes/gmail.js +128 -0
  55. package/lib/server/routes/google.js +433 -213
  56. package/lib/server/routes/system.js +107 -0
  57. package/lib/server/routes/usage.js +29 -2
  58. package/lib/server/routes/webhooks.js +52 -17
  59. package/lib/server/usage-db.js +283 -15
  60. package/lib/server/watchdog.js +66 -0
  61. package/lib/server/webhook-middleware.js +99 -1
  62. package/lib/server/webhooks.js +214 -65
  63. package/lib/server.js +27 -0
  64. package/lib/setup/gitignore +6 -0
  65. package/lib/setup/hourly-git-sync.sh +29 -2
  66. package/package.json +1 -1
  67. package/lib/public/js/components/google.js +0 -228
  68. package/lib/public/js/components/usage-tab.js +0 -531
@@ -1,5 +1,24 @@
1
+ const kClientTimeZoneHeader = "x-client-timezone";
2
+
3
+ const getBrowserTimeZone = () => {
4
+ try {
5
+ return Intl?.DateTimeFormat?.().resolvedOptions?.().timeZone || "";
6
+ } catch {
7
+ return "";
8
+ }
9
+ };
10
+
1
11
  export const authFetch = async (url, opts = {}) => {
2
- const res = await fetch(url, opts);
12
+ const nextOptions = { ...opts };
13
+ const headers = new Headers(opts?.headers || {});
14
+ if (!headers.has(kClientTimeZoneHeader)) {
15
+ const browserTimeZone = getBrowserTimeZone();
16
+ if (browserTimeZone) {
17
+ headers.set(kClientTimeZoneHeader, browserTimeZone);
18
+ }
19
+ }
20
+ nextOptions.headers = headers;
21
+ const res = await fetch(url, nextOptions);
3
22
  if (res.status === 401) {
4
23
  try {
5
24
  window.localStorage?.clear?.();
@@ -38,30 +57,162 @@ export async function rejectPairing(id, channel) {
38
57
  return res.json();
39
58
  }
40
59
 
41
- export async function fetchGoogleStatus() {
42
- const res = await authFetch('/api/google/status');
60
+ export async function fetchGoogleAccounts() {
61
+ const res = await authFetch('/api/google/accounts');
62
+ return res.json();
63
+ }
64
+
65
+ export async function fetchGoogleStatus(accountId = "") {
66
+ const params = new URLSearchParams();
67
+ if (accountId) params.set("accountId", String(accountId));
68
+ const suffix = params.toString() ? `?${params.toString()}` : "";
69
+ const res = await authFetch(`/api/google/status${suffix}`);
70
+ return res.json();
71
+ }
72
+
73
+ export async function fetchGoogleCredentials({
74
+ accountId = "",
75
+ client = "",
76
+ } = {}) {
77
+ const params = new URLSearchParams();
78
+ if (accountId) params.set("accountId", String(accountId));
79
+ if (client) params.set("client", String(client));
80
+ const suffix = params.toString() ? `?${params.toString()}` : "";
81
+ const res = await authFetch(`/api/google/credentials${suffix}`);
43
82
  return res.json();
44
83
  }
45
84
 
46
- export async function checkGoogleApis() {
47
- const res = await authFetch('/api/google/check');
85
+ export async function checkGoogleApis(accountId = "") {
86
+ const params = new URLSearchParams();
87
+ if (accountId) params.set("accountId", String(accountId));
88
+ const suffix = params.toString() ? `?${params.toString()}` : "";
89
+ const res = await authFetch(`/api/google/check${suffix}`);
48
90
  return res.json();
49
91
  }
50
92
 
51
- export async function saveGoogleCredentials(clientId, clientSecret, email) {
93
+ export async function saveGoogleCredentials({
94
+ clientId,
95
+ clientSecret,
96
+ email,
97
+ services = [],
98
+ client = "default",
99
+ personal = false,
100
+ accountId = "",
101
+ }) {
52
102
  const res = await authFetch('/api/google/credentials', {
53
103
  method: 'POST',
54
104
  headers: { 'Content-Type': 'application/json' },
55
- body: JSON.stringify({ clientId, clientSecret, email }),
105
+ body: JSON.stringify({
106
+ clientId,
107
+ clientSecret,
108
+ email,
109
+ services,
110
+ client,
111
+ personal,
112
+ accountId,
113
+ }),
56
114
  });
57
115
  return res.json();
58
116
  }
59
117
 
60
- export async function disconnectGoogle() {
61
- const res = await authFetch('/api/google/disconnect', { method: 'POST' });
118
+ export async function saveGoogleAccount({
119
+ email,
120
+ services = [],
121
+ client = "default",
122
+ personal = false,
123
+ accountId = "",
124
+ }) {
125
+ const res = await authFetch('/api/google/accounts', {
126
+ method: 'POST',
127
+ headers: { 'Content-Type': 'application/json' },
128
+ body: JSON.stringify({ email, services, client, personal, accountId }),
129
+ });
62
130
  return res.json();
63
131
  }
64
132
 
133
+ export async function disconnectGoogle(accountId = "") {
134
+ const res = await authFetch('/api/google/disconnect', {
135
+ method: 'POST',
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body: JSON.stringify({ accountId }),
138
+ });
139
+ return res.json();
140
+ }
141
+
142
+ export const fetchGmailConfig = async () => {
143
+ const res = await authFetch("/api/gmail/config");
144
+ return parseJsonOrThrow(res, "Could not load Gmail watch config");
145
+ };
146
+
147
+ export const saveGmailConfig = async ({
148
+ client = "default",
149
+ topicPath = "",
150
+ projectId = "",
151
+ regeneratePushToken = false,
152
+ } = {}) => {
153
+ const res = await authFetch("/api/gmail/config", {
154
+ method: "POST",
155
+ headers: { "Content-Type": "application/json" },
156
+ body: JSON.stringify({
157
+ client,
158
+ topicPath,
159
+ projectId,
160
+ regeneratePushToken,
161
+ }),
162
+ });
163
+ return parseJsonOrThrow(res, "Could not save Gmail watch config");
164
+ };
165
+
166
+ export const startGmailWatch = async (accountId) => {
167
+ const res = await authFetch("/api/gmail/watch/start", {
168
+ method: "POST",
169
+ headers: { "Content-Type": "application/json" },
170
+ body: JSON.stringify({ accountId: String(accountId || "") }),
171
+ });
172
+ return parseJsonOrThrow(res, "Could not start Gmail watch");
173
+ };
174
+
175
+ export const stopGmailWatch = async (accountId) => {
176
+ const res = await authFetch("/api/gmail/watch/stop", {
177
+ method: "POST",
178
+ headers: { "Content-Type": "application/json" },
179
+ body: JSON.stringify({ accountId: String(accountId || "") }),
180
+ });
181
+ return parseJsonOrThrow(res, "Could not stop Gmail watch");
182
+ };
183
+
184
+ export const renewGmailWatch = async ({
185
+ accountId = "",
186
+ force = true,
187
+ } = {}) => {
188
+ const res = await authFetch("/api/gmail/watch/renew", {
189
+ method: "POST",
190
+ headers: { "Content-Type": "application/json" },
191
+ body: JSON.stringify({ accountId: String(accountId || ""), force: Boolean(force) }),
192
+ });
193
+ return parseJsonOrThrow(res, "Could not renew Gmail watch");
194
+ };
195
+
196
+ export const fetchAgentSessions = async () => {
197
+ const res = await authFetch("/api/agent/sessions");
198
+ return parseJsonOrThrow(res, "Could not load agent sessions");
199
+ };
200
+
201
+ export const sendAgentMessage = async ({
202
+ message = "",
203
+ sessionKey = "",
204
+ } = {}) => {
205
+ const res = await authFetch("/api/agent/message", {
206
+ method: "POST",
207
+ headers: { "Content-Type": "application/json" },
208
+ body: JSON.stringify({
209
+ message: String(message || ""),
210
+ sessionKey: String(sessionKey || ""),
211
+ }),
212
+ });
213
+ return parseJsonOrThrow(res, "Could not send message to agent");
214
+ };
215
+
65
216
  export async function restartGateway() {
66
217
  const res = await authFetch('/api/gateway/restart', { method: 'POST' });
67
218
  return parseJsonOrThrow(res, 'Could not restart gateway');
@@ -389,6 +540,24 @@ export const saveFileContent = async (filePath, content) => {
389
540
  return parseJsonOrThrow(res, 'Could not save file');
390
541
  };
391
542
 
543
+ export const deleteBrowseFile = async (filePath) => {
544
+ const res = await authFetch('/api/browse/delete', {
545
+ method: 'DELETE',
546
+ headers: { 'Content-Type': 'application/json' },
547
+ body: JSON.stringify({ path: String(filePath || '') }),
548
+ });
549
+ return parseJsonOrThrow(res, 'Could not delete file');
550
+ };
551
+
552
+ export const restoreBrowseFile = async (filePath) => {
553
+ const res = await authFetch('/api/browse/restore', {
554
+ method: 'POST',
555
+ headers: { 'Content-Type': 'application/json' },
556
+ body: JSON.stringify({ path: String(filePath || '') }),
557
+ });
558
+ return parseJsonOrThrow(res, 'Could not restore file');
559
+ };
560
+
392
561
  export const fetchBrowseGitSummary = async () => {
393
562
  const res = await authFetch('/api/browse/git-summary');
394
563
  return parseJsonOrThrow(res, 'Could not load git summary');
@@ -1,15 +1,33 @@
1
- export const kProtectedBrowsePaths = new Set([
2
- "openclaw.json",
3
- "devices/paired.json",
4
- ]);
1
+ const kBrowseFilePoliciesUrl = new URL(
2
+ "../../shared/browse-file-policies.json",
3
+ import.meta.url,
4
+ );
5
5
 
6
- export const kLockedBrowsePaths = new Set([
7
- "hooks/bootstrap/agents.md",
8
- "hooks/bootstrap/tools.md",
9
- "skills/control-ui/skill.md",
10
- ".alphaclaw/hourly-git-sync.sh",
11
- ".alphaclaw/.cli-device-auto-approved",
12
- ]);
6
+ let kBrowseFilePolicies = {
7
+ protectedPaths: [],
8
+ lockedPaths: [],
9
+ };
10
+ try {
11
+ const policyResponse = await fetch(kBrowseFilePoliciesUrl);
12
+ if (policyResponse.ok) {
13
+ const policyJson = await policyResponse.json();
14
+ if (policyJson && typeof policyJson === "object") {
15
+ kBrowseFilePolicies = policyJson;
16
+ }
17
+ }
18
+ } catch {}
19
+
20
+ export const kProtectedBrowsePaths = new Set(
21
+ Array.isArray(kBrowseFilePolicies?.protectedPaths)
22
+ ? kBrowseFilePolicies.protectedPaths
23
+ : [],
24
+ );
25
+
26
+ export const kLockedBrowsePaths = new Set(
27
+ Array.isArray(kBrowseFilePolicies?.lockedPaths)
28
+ ? kBrowseFilePolicies.lockedPaths
29
+ : [],
30
+ );
13
31
 
14
32
  export const normalizeBrowsePolicyPath = (inputPath) =>
15
33
  String(inputPath || "")
@@ -0,0 +1,71 @@
1
+ const kIntegerFormatter = new Intl.NumberFormat("en-US");
2
+ const kUsdFormatter = new Intl.NumberFormat("en-US", {
3
+ style: "currency",
4
+ currency: "USD",
5
+ minimumFractionDigits: 2,
6
+ maximumFractionDigits: 3,
7
+ });
8
+
9
+ const toDateValue = (
10
+ value,
11
+ { valueIsUnixSeconds = false, valueIsEpochMs = false } = {},
12
+ ) => {
13
+ if (value == null || value === "") return null;
14
+ if (value instanceof Date) return value;
15
+ if (valueIsUnixSeconds) return new Date(Number(value) * 1000);
16
+ if (valueIsEpochMs) return new Date(Number(value));
17
+ return new Date(value);
18
+ };
19
+
20
+ const isSameDay = (left, right) =>
21
+ left.getFullYear() === right.getFullYear() &&
22
+ left.getMonth() === right.getMonth() &&
23
+ left.getDate() === right.getDate();
24
+
25
+ export const formatInteger = (value) =>
26
+ kIntegerFormatter.format(Number(value || 0));
27
+
28
+ export const formatUsd = (value) => kUsdFormatter.format(Number(value || 0));
29
+
30
+ export const formatLocaleDateTime = (
31
+ value,
32
+ { fallback = "—", valueIsUnixSeconds = false, valueIsEpochMs = false } = {},
33
+ ) => {
34
+ try {
35
+ const dateValue = toDateValue(value, { valueIsUnixSeconds, valueIsEpochMs });
36
+ if (!dateValue || Number.isNaN(dateValue.getTime())) return fallback;
37
+ return dateValue.toLocaleString();
38
+ } catch {
39
+ return fallback;
40
+ }
41
+ };
42
+
43
+ export const formatLocaleDateTimeWithTodayTime = (
44
+ value,
45
+ {
46
+ fallback = "—",
47
+ valueIsUnixSeconds = false,
48
+ valueIsEpochMs = false,
49
+ } = {},
50
+ ) => {
51
+ try {
52
+ const dateValue = toDateValue(value, { valueIsUnixSeconds, valueIsEpochMs });
53
+ if (!dateValue || Number.isNaN(dateValue.getTime())) return fallback;
54
+ return isSameDay(dateValue, new Date())
55
+ ? dateValue.toLocaleTimeString()
56
+ : dateValue.toLocaleString();
57
+ } catch {
58
+ return fallback;
59
+ }
60
+ };
61
+
62
+ export const formatDurationCompactMs = (value) => {
63
+ const ms = Number(value || 0);
64
+ if (!Number.isFinite(ms) || ms <= 0) return "0s";
65
+ if (ms < 1000) return `${Math.round(ms)}ms`;
66
+ const totalSeconds = Math.round(ms / 1000);
67
+ const minutes = Math.floor(totalSeconds / 60);
68
+ const seconds = totalSeconds % 60;
69
+ if (minutes <= 0) return `${seconds}s`;
70
+ return `${minutes}m ${seconds}s`;
71
+ };
@@ -8,11 +8,12 @@ import { escapeHtml, toLineObjects } from "./utils.js";
8
8
 
9
9
  export const getFileSyntaxKind = (filePath) => {
10
10
  const normalizedPath = String(filePath || "").toLowerCase();
11
- if (/\.(md|markdown|mdx)$/i.test(normalizedPath)) return "markdown";
12
- if (/\.(json|jsonl)$/i.test(normalizedPath)) return "json";
13
- if (/\.(html|htm)$/i.test(normalizedPath)) return "html";
14
- if (/\.(js|mjs|cjs)$/i.test(normalizedPath)) return "javascript";
15
- if (/\.(css|scss)$/i.test(normalizedPath)) return "css";
11
+ const pathWithoutBakSuffix = normalizedPath.replace(/(\.bak)+$/i, "");
12
+ if (/\.(md|markdown|mdx)$/i.test(pathWithoutBakSuffix)) return "markdown";
13
+ if (/\.(json|jsonl)$/i.test(pathWithoutBakSuffix)) return "json";
14
+ if (/\.(html|htm)$/i.test(pathWithoutBakSuffix)) return "html";
15
+ if (/\.(js|mjs|cjs)$/i.test(pathWithoutBakSuffix)) return "javascript";
16
+ if (/\.(css|scss)$/i.test(pathWithoutBakSuffix)) return "css";
16
17
  return "plain";
17
18
  };
18
19
 
@@ -0,0 +1,13 @@
1
+ {
2
+ "protectedPaths": [
3
+ "openclaw.json",
4
+ "devices/paired.json"
5
+ ],
6
+ "lockedPaths": [
7
+ "hooks/bootstrap/agents.md",
8
+ "hooks/bootstrap/tools.md",
9
+ "skills/control-ui/skill.md",
10
+ ".alphaclaw/hourly-git-sync.sh",
11
+ ".alphaclaw/.cli-device-auto-approved"
12
+ ]
13
+ }
@@ -1,5 +1,6 @@
1
1
  const os = require("os");
2
2
  const path = require("path");
3
+ const kBrowseFilePolicies = require("../public/shared/browse-file-policies.json");
3
4
 
4
5
  const parsePositiveIntEnv = (value, fallbackValue) => {
5
6
  const parsed = Number.parseInt(String(value || ""), 10);
@@ -119,6 +120,12 @@ const kMaxPayloadBytes = parsePositiveIntEnv(process.env.WEBHOOK_LOG_MAX_BYTES,
119
120
  const kWebhookPruneDays = parsePositiveIntEnv(process.env.WEBHOOK_LOG_RETENTION_DAYS, 30);
120
121
  const kWatchdogCheckIntervalMs =
121
122
  parsePositiveIntEnv(process.env.WATCHDOG_CHECK_INTERVAL, 120) * 1000;
123
+ const kWatchdogDegradedCheckIntervalMs =
124
+ parsePositiveIntEnv(process.env.WATCHDOG_DEGRADED_CHECK_INTERVAL, 5) * 1000;
125
+ const kWatchdogStartupFailureThreshold = parsePositiveIntEnv(
126
+ process.env.WATCHDOG_STARTUP_FAILURE_THRESHOLD,
127
+ 3,
128
+ );
122
129
  const kWatchdogMaxRepairAttempts = parsePositiveIntEnv(
123
130
  process.env.WATCHDOG_MAX_REPAIR_ATTEMPTS,
124
131
  2,
@@ -140,6 +147,7 @@ const kLogMaxBytes = parsePositiveIntEnv(
140
147
 
141
148
  const kSystemVars = new Set([
142
149
  "WEBHOOK_TOKEN",
150
+ "OPENCLAW_HOOKS_TOKEN",
143
151
  "OPENCLAW_GATEWAY_TOKEN",
144
152
  "SETUP_PASSWORD",
145
153
  "PORT",
@@ -258,6 +266,25 @@ const GOG_CONFIG_DIR = path.join(OPENCLAW_DIR, "gogcli");
258
266
  const GOG_CREDENTIALS_PATH = path.join(GOG_CONFIG_DIR, "credentials.json");
259
267
  const GOG_STATE_PATH = path.join(GOG_CONFIG_DIR, "state.json");
260
268
  const GOG_KEYRING_PASSWORD = process.env.GOG_KEYRING_PASSWORD || "alphaclaw";
269
+ const kMaxGoogleAccounts = 5;
270
+ const kGmailServeBasePort = parsePositiveIntEnv(
271
+ process.env.GMAIL_SERVE_BASE_PORT,
272
+ 18801,
273
+ );
274
+ const kGmailWatchRenewalIntervalMs =
275
+ parsePositiveIntEnv(process.env.GMAIL_WATCH_RENEWAL_INTERVAL_SECONDS, 6 * 60 * 60) *
276
+ 1000;
277
+ const kGmailWatchRenewalThresholdMs =
278
+ parsePositiveIntEnv(process.env.GMAIL_WATCH_RENEWAL_THRESHOLD_SECONDS, 24 * 60 * 60) *
279
+ 1000;
280
+ const kGmailMaxBodyBytes = parsePositiveIntEnv(
281
+ process.env.GMAIL_WATCH_MAX_BODY_BYTES,
282
+ 20000,
283
+ );
284
+ const gogClientCredentialsPath = (clientName = "default") =>
285
+ clientName === "default"
286
+ ? GOG_CREDENTIALS_PATH
287
+ : path.join(GOG_CONFIG_DIR, `credentials-${clientName}.json`);
261
288
 
262
289
  const API_TEST_COMMANDS = {
263
290
  gmail: "gmail labels list",
@@ -274,13 +301,16 @@ const kChannelDefs = {
274
301
  telegram: { envKey: "TELEGRAM_BOT_TOKEN" },
275
302
  discord: { envKey: "DISCORD_BOT_TOKEN" },
276
303
  };
277
- const kLockedBrowsePaths = new Set([
278
- "hooks/bootstrap/agents.md",
279
- "hooks/bootstrap/tools.md",
280
- "skills/control-ui/skill.md",
281
- ".alphaclaw/hourly-git-sync.sh",
282
- ".alphaclaw/.cli-device-auto-approved",
283
- ]);
304
+ const kProtectedBrowsePaths = new Set(
305
+ Array.isArray(kBrowseFilePolicies?.protectedPaths)
306
+ ? kBrowseFilePolicies.protectedPaths
307
+ : [],
308
+ );
309
+ const kLockedBrowsePaths = new Set(
310
+ Array.isArray(kBrowseFilePolicies?.lockedPaths)
311
+ ? kBrowseFilePolicies.lockedPaths
312
+ : [],
313
+ );
284
314
 
285
315
  const SETUP_API_PREFIXES = [
286
316
  "/api/status",
@@ -299,6 +329,7 @@ const SETUP_API_PREFIXES = [
299
329
  "/api/sync-cron",
300
330
  "/api/telegram",
301
331
  "/api/webhooks",
332
+ "/api/gmail",
302
333
  "/api/watchdog",
303
334
  "/api/usage",
304
335
  ];
@@ -342,6 +373,8 @@ module.exports = {
342
373
  kMaxPayloadBytes,
343
374
  kWebhookPruneDays,
344
375
  kWatchdogCheckIntervalMs,
376
+ kWatchdogDegradedCheckIntervalMs,
377
+ kWatchdogStartupFailureThreshold,
345
378
  kWatchdogMaxRepairAttempts,
346
379
  kWatchdogCrashLoopWindowMs,
347
380
  kWatchdogCrashLoopThreshold,
@@ -350,6 +383,7 @@ module.exports = {
350
383
  kSystemVars,
351
384
  kKnownVars,
352
385
  kKnownKeys,
386
+ kProtectedBrowsePaths,
353
387
  kLockedBrowsePaths,
354
388
  SCOPE_MAP,
355
389
  REVERSE_SCOPE_MAP,
@@ -358,6 +392,12 @@ module.exports = {
358
392
  GOG_CREDENTIALS_PATH,
359
393
  GOG_STATE_PATH,
360
394
  GOG_KEYRING_PASSWORD,
395
+ kMaxGoogleAccounts,
396
+ kGmailServeBasePort,
397
+ kGmailWatchRenewalIntervalMs,
398
+ kGmailWatchRenewalThresholdMs,
399
+ kGmailMaxBodyBytes,
400
+ gogClientCredentialsPath,
361
401
  API_TEST_COMMANDS,
362
402
  kChannelDefs,
363
403
  SETUP_API_PREFIXES,
@@ -0,0 +1,109 @@
1
+ const http = require("http");
2
+
3
+ const extractBodyBuffer = (body) => {
4
+ if (Buffer.isBuffer(body)) return body;
5
+ if (typeof body === "string") return Buffer.from(body, "utf8");
6
+ if (body && typeof body === "object") {
7
+ return Buffer.from(JSON.stringify(body), "utf8");
8
+ }
9
+ return Buffer.alloc(0);
10
+ };
11
+
12
+ const parsePushEnvelope = (bodyBuffer) => {
13
+ const parsed = JSON.parse(String(bodyBuffer || Buffer.alloc(0)).toString("utf8"));
14
+ const encodedData = String(parsed?.message?.data || "");
15
+ const decodedData = encodedData
16
+ ? JSON.parse(Buffer.from(encodedData, "base64").toString("utf8"))
17
+ : {};
18
+ return {
19
+ envelope: parsed || {},
20
+ payload: decodedData || {},
21
+ };
22
+ };
23
+
24
+ const proxyPushToServe = async ({
25
+ port,
26
+ bodyBuffer,
27
+ headers,
28
+ }) =>
29
+ await new Promise((resolve, reject) => {
30
+ const request = http.request(
31
+ {
32
+ hostname: "127.0.0.1",
33
+ port,
34
+ method: "POST",
35
+ path: "/",
36
+ headers: {
37
+ "content-type": headers["content-type"] || "application/json",
38
+ "content-length": String(bodyBuffer.length),
39
+ },
40
+ },
41
+ (response) => {
42
+ const chunks = [];
43
+ response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
44
+ response.on("end", () => {
45
+ resolve({
46
+ statusCode: response.statusCode || 200,
47
+ body: Buffer.concat(chunks).toString("utf8"),
48
+ });
49
+ });
50
+ },
51
+ );
52
+ request.on("error", reject);
53
+ if (bodyBuffer.length) request.write(bodyBuffer);
54
+ request.end();
55
+ });
56
+
57
+ const createGmailPushHandler = ({
58
+ resolvePushToken,
59
+ resolveTargetByEmail,
60
+ markPushReceived,
61
+ }) =>
62
+ async (req, res) => {
63
+ try {
64
+ const expectedToken = String(resolvePushToken?.() || "").trim();
65
+ const receivedToken = String(req.query?.token || "").trim();
66
+ if (!expectedToken || !receivedToken || expectedToken !== receivedToken) {
67
+ return res.status(401).json({ ok: false, error: "Invalid push token" });
68
+ }
69
+
70
+ const bodyBuffer = extractBodyBuffer(req.body);
71
+ const { payload } = parsePushEnvelope(bodyBuffer);
72
+ const email = String(payload?.emailAddress || "").trim().toLowerCase();
73
+ if (!email) {
74
+ return res.status(200).json({ ok: true, ignored: true, reason: "missing_email" });
75
+ }
76
+
77
+ const target = resolveTargetByEmail?.(email);
78
+ if (!target?.port) {
79
+ return res.status(200).json({ ok: true, ignored: true, reason: "watch_not_enabled" });
80
+ }
81
+
82
+ try {
83
+ const proxied = await proxyPushToServe({
84
+ port: target.port,
85
+ bodyBuffer,
86
+ headers: req.headers || {},
87
+ });
88
+ await markPushReceived?.({
89
+ accountId: target.accountId,
90
+ at: Date.now(),
91
+ });
92
+ return res
93
+ .status(proxied.statusCode)
94
+ .send(proxied.body || "");
95
+ } catch (err) {
96
+ console.error(
97
+ `[alphaclaw] Gmail push proxy error for ${email}: ${err.message || "unknown"}`,
98
+ );
99
+ return res.status(200).json({ ok: true, ignored: true, reason: "proxy_error" });
100
+ }
101
+ } catch (err) {
102
+ console.error("[alphaclaw] Gmail push handler error:", err);
103
+ return res.status(200).json({ ok: true, ignored: true, reason: "handler_error" });
104
+ }
105
+ };
106
+
107
+ module.exports = {
108
+ createGmailPushHandler,
109
+ };