@alfe.ai/openclaw-google-chat 0.0.28 → 0.0.30

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/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # `@alfe.ai/openclaw-google-chat`
2
+
3
+ OpenClaw service plugin that polls direct-message spaces for the connected
4
+ Google account, dispatches inbound messages through OpenClaw, and sends the
5
+ agent's replies back through the Google Chat API.
6
+
7
+ The poller uses adaptive per-space intervals plus a discovery loop. Google API
8
+ calls have deadlines, response and pagination limits, runtime shape checks, and
9
+ shared rate-limit backoff. An inbound message is checkpointed only after the
10
+ dispatch pipeline accepts it; failures remain behind the timestamp cursor for
11
+ retry in order.
12
+
13
+ Poll state is stored atomically at `~/.alfe/state/google-chat-poller.json` with
14
+ owner-only permissions. OAuth credentials remain in memory and are never
15
+ written into this state file.
16
+
17
+ ## Development
18
+
19
+ ```bash
20
+ pnpm --filter @alfe.ai/openclaw-google-chat test
21
+ pnpm --filter @alfe.ai/openclaw-google-chat lint
22
+ pnpm --filter @alfe.ai/openclaw-google-chat typecheck
23
+ pnpm --filter @alfe.ai/openclaw-google-chat build
24
+ ```
25
+
26
+ See [DEVELOPING.md](./DEVELOPING.md) and the repository root
27
+ `DEVELOPING.md` before changing lifecycle, delivery, or provider behavior.
@@ -1,6 +1,16 @@
1
1
  //#region src/gchat-token.ts
2
2
  const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
3
3
  const REFRESH_MARGIN_MS = 300 * 1e3;
4
+ const TOKEN_REQUEST_TIMEOUT_MS = 2e4;
5
+ const MAX_TOKEN_RESPONSE_BYTES = 65536;
6
+ var GoogleTokenError = class extends Error {
7
+ constructor(message, status, code) {
8
+ super(message);
9
+ this.status = status;
10
+ this.code = code;
11
+ this.name = "GoogleTokenError";
12
+ }
13
+ };
4
14
  var TokenManager = class {
5
15
  refreshToken;
6
16
  clientId;
@@ -9,6 +19,7 @@ var TokenManager = class {
9
19
  cached = null;
10
20
  refreshPromise = null;
11
21
  constructor(init, log) {
22
+ if (!init.refreshToken.trim() || !init.clientId.trim() || !init.clientSecret.trim()) throw new Error("Google OAuth credentials must be non-empty");
12
23
  this.refreshToken = init.refreshToken;
13
24
  this.clientId = init.clientId;
14
25
  this.clientSecret = init.clientSecret;
@@ -34,6 +45,7 @@ var TokenManager = class {
34
45
  const response = await fetch(GOOGLE_TOKEN_URL, {
35
46
  method: "POST",
36
47
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
48
+ signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS),
37
49
  body: new URLSearchParams({
38
50
  client_id: this.clientId,
39
51
  client_secret: this.clientSecret,
@@ -41,19 +53,57 @@ var TokenManager = class {
41
53
  grant_type: "refresh_token"
42
54
  })
43
55
  });
56
+ const responseBody = await readTokenResponse(response);
44
57
  if (!response.ok) {
45
- const body = await response.text().catch(() => "");
46
- this.log.error(`Token refresh failed (${String(response.status)}): ${body}`);
47
- throw new Error(`Google token refresh failed: ${String(response.status)}`);
58
+ const oauthCode = readOAuthErrorCode(responseBody);
59
+ this.log.error(`Token refresh failed (${String(response.status)}${oauthCode ? `, ${oauthCode}` : ""})`);
60
+ throw new GoogleTokenError(`Google token refresh failed: ${String(response.status)}${oauthCode ? ` (${oauthCode})` : ""}`, response.status, oauthCode);
48
61
  }
49
- const data = await response.json();
62
+ if (!isTokenResponse(responseBody)) throw new GoogleTokenError("Google token refresh returned an invalid response");
50
63
  this.cached = {
51
- accessToken: data.access_token,
52
- expiresAt: Date.now() + data.expires_in * 1e3
64
+ accessToken: responseBody.access_token,
65
+ expiresAt: Date.now() + responseBody.expires_in * 1e3
53
66
  };
54
67
  this.log.debug("Google access token refreshed");
55
68
  return this.cached.accessToken;
56
69
  }
57
70
  };
71
+ function isTokenResponse(value) {
72
+ return typeof value === "object" && value !== null && "access_token" in value && typeof value.access_token === "string" && value.access_token.length > 0 && "expires_in" in value && typeof value.expires_in === "number" && Number.isFinite(value.expires_in) && value.expires_in > 0;
73
+ }
74
+ async function readTokenResponse(response) {
75
+ const declaredLength = response.headers.get("content-length");
76
+ if (declaredLength) {
77
+ const length = Number(declaredLength);
78
+ if (!Number.isFinite(length) || length < 0 || length > MAX_TOKEN_RESPONSE_BYTES) {
79
+ await response.body?.cancel().catch(() => void 0);
80
+ throw new GoogleTokenError("Google token response exceeded the size limit", response.status);
81
+ }
82
+ }
83
+ if (!response.body) return void 0;
84
+ const reader = response.body.getReader();
85
+ const decoder = new TextDecoder();
86
+ let total = 0;
87
+ let raw = "";
88
+ let chunk = await reader.read();
89
+ while (!chunk.done) {
90
+ total += chunk.value.byteLength;
91
+ if (total > MAX_TOKEN_RESPONSE_BYTES) {
92
+ await reader.cancel().catch(() => void 0);
93
+ throw new GoogleTokenError("Google token response exceeded the size limit", response.status);
94
+ }
95
+ raw += decoder.decode(chunk.value, { stream: true });
96
+ chunk = await reader.read();
97
+ }
98
+ raw += decoder.decode();
99
+ try {
100
+ return JSON.parse(raw);
101
+ } catch {
102
+ return;
103
+ }
104
+ }
105
+ function readOAuthErrorCode(data) {
106
+ if (typeof data === "object" && data !== null && "error" in data && typeof data.error === "string" && /^[a-z0-9_]{1,64}$/i.test(data.error)) return data.error;
107
+ }
58
108
  //#endregion
59
109
  exports.TokenManager = TokenManager;
@@ -1,6 +1,16 @@
1
1
  //#region src/gchat-token.ts
2
2
  const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
3
3
  const REFRESH_MARGIN_MS = 300 * 1e3;
4
+ const TOKEN_REQUEST_TIMEOUT_MS = 2e4;
5
+ const MAX_TOKEN_RESPONSE_BYTES = 65536;
6
+ var GoogleTokenError = class extends Error {
7
+ constructor(message, status, code) {
8
+ super(message);
9
+ this.status = status;
10
+ this.code = code;
11
+ this.name = "GoogleTokenError";
12
+ }
13
+ };
4
14
  var TokenManager = class {
5
15
  refreshToken;
6
16
  clientId;
@@ -9,6 +19,7 @@ var TokenManager = class {
9
19
  cached = null;
10
20
  refreshPromise = null;
11
21
  constructor(init, log) {
22
+ if (!init.refreshToken.trim() || !init.clientId.trim() || !init.clientSecret.trim()) throw new Error("Google OAuth credentials must be non-empty");
12
23
  this.refreshToken = init.refreshToken;
13
24
  this.clientId = init.clientId;
14
25
  this.clientSecret = init.clientSecret;
@@ -34,6 +45,7 @@ var TokenManager = class {
34
45
  const response = await fetch(GOOGLE_TOKEN_URL, {
35
46
  method: "POST",
36
47
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
48
+ signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS),
37
49
  body: new URLSearchParams({
38
50
  client_id: this.clientId,
39
51
  client_secret: this.clientSecret,
@@ -41,19 +53,57 @@ var TokenManager = class {
41
53
  grant_type: "refresh_token"
42
54
  })
43
55
  });
56
+ const responseBody = await readTokenResponse(response);
44
57
  if (!response.ok) {
45
- const body = await response.text().catch(() => "");
46
- this.log.error(`Token refresh failed (${String(response.status)}): ${body}`);
47
- throw new Error(`Google token refresh failed: ${String(response.status)}`);
58
+ const oauthCode = readOAuthErrorCode(responseBody);
59
+ this.log.error(`Token refresh failed (${String(response.status)}${oauthCode ? `, ${oauthCode}` : ""})`);
60
+ throw new GoogleTokenError(`Google token refresh failed: ${String(response.status)}${oauthCode ? ` (${oauthCode})` : ""}`, response.status, oauthCode);
48
61
  }
49
- const data = await response.json();
62
+ if (!isTokenResponse(responseBody)) throw new GoogleTokenError("Google token refresh returned an invalid response");
50
63
  this.cached = {
51
- accessToken: data.access_token,
52
- expiresAt: Date.now() + data.expires_in * 1e3
64
+ accessToken: responseBody.access_token,
65
+ expiresAt: Date.now() + responseBody.expires_in * 1e3
53
66
  };
54
67
  this.log.debug("Google access token refreshed");
55
68
  return this.cached.accessToken;
56
69
  }
57
70
  };
71
+ function isTokenResponse(value) {
72
+ return typeof value === "object" && value !== null && "access_token" in value && typeof value.access_token === "string" && value.access_token.length > 0 && "expires_in" in value && typeof value.expires_in === "number" && Number.isFinite(value.expires_in) && value.expires_in > 0;
73
+ }
74
+ async function readTokenResponse(response) {
75
+ const declaredLength = response.headers.get("content-length");
76
+ if (declaredLength) {
77
+ const length = Number(declaredLength);
78
+ if (!Number.isFinite(length) || length < 0 || length > MAX_TOKEN_RESPONSE_BYTES) {
79
+ await response.body?.cancel().catch(() => void 0);
80
+ throw new GoogleTokenError("Google token response exceeded the size limit", response.status);
81
+ }
82
+ }
83
+ if (!response.body) return void 0;
84
+ const reader = response.body.getReader();
85
+ const decoder = new TextDecoder();
86
+ let total = 0;
87
+ let raw = "";
88
+ let chunk = await reader.read();
89
+ while (!chunk.done) {
90
+ total += chunk.value.byteLength;
91
+ if (total > MAX_TOKEN_RESPONSE_BYTES) {
92
+ await reader.cancel().catch(() => void 0);
93
+ throw new GoogleTokenError("Google token response exceeded the size limit", response.status);
94
+ }
95
+ raw += decoder.decode(chunk.value, { stream: true });
96
+ chunk = await reader.read();
97
+ }
98
+ raw += decoder.decode();
99
+ try {
100
+ return JSON.parse(raw);
101
+ } catch {
102
+ return;
103
+ }
104
+ }
105
+ function readOAuthErrorCode(data) {
106
+ if (typeof data === "object" && data !== null && "error" in data && typeof data.error === "string" && /^[a-z0-9_]{1,64}$/i.test(data.error)) return data.error;
107
+ }
58
108
  //#endregion
59
109
  export { TokenManager };
package/dist/plugin.d.cts CHANGED
@@ -50,7 +50,7 @@ declare const plugin: {
50
50
  description: string;
51
51
  version: string;
52
52
  activate(api: PluginApi): void;
53
- deactivate(api: PluginApi): void;
53
+ deactivate(api: PluginApi): Promise<void>;
54
54
  };
55
55
  //#endregion
56
56
  export { plugin as default };
package/dist/plugin.d.ts CHANGED
@@ -50,7 +50,7 @@ declare const plugin: {
50
50
  description: string;
51
51
  version: string;
52
52
  activate(api: PluginApi): void;
53
- deactivate(api: PluginApi): void;
53
+ deactivate(api: PluginApi): Promise<void>;
54
54
  };
55
55
  //#endregion
56
56
  export { plugin as default };