@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 +27 -0
- package/dist/gchat-token.cjs +56 -6
- package/dist/gchat-token.js +56 -6
- package/dist/plugin.d.cts +1 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin2.cjs +381 -153
- package/dist/plugin2.js +382 -154
- package/package.json +4 -3
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.
|
package/dist/gchat-token.cjs
CHANGED
|
@@ -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
|
|
46
|
-
this.log.error(`Token refresh failed (${String(response.status)}
|
|
47
|
-
throw new
|
|
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
|
-
|
|
62
|
+
if (!isTokenResponse(responseBody)) throw new GoogleTokenError("Google token refresh returned an invalid response");
|
|
50
63
|
this.cached = {
|
|
51
|
-
accessToken:
|
|
52
|
-
expiresAt: Date.now() +
|
|
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;
|
package/dist/gchat-token.js
CHANGED
|
@@ -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
|
|
46
|
-
this.log.error(`Token refresh failed (${String(response.status)}
|
|
47
|
-
throw new
|
|
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
|
-
|
|
62
|
+
if (!isTokenResponse(responseBody)) throw new GoogleTokenError("Google token refresh returned an invalid response");
|
|
50
63
|
this.cached = {
|
|
51
|
-
accessToken:
|
|
52
|
-
expiresAt: Date.now() +
|
|
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
package/dist/plugin.d.ts
CHANGED