@badgerclaw/connect 1.0.1 → 1.1.1
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/package.json
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { getMatrixLogService } from "../sdk-runtime.js";
|
|
6
|
+
|
|
7
|
+
export type KeyBackupStatus = {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
version: string | null;
|
|
10
|
+
deviceId: string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function getEncryptionKeyBackupStatus(
|
|
14
|
+
client: MatrixClient,
|
|
15
|
+
): Promise<KeyBackupStatus> {
|
|
16
|
+
const LogService = getMatrixLogService();
|
|
17
|
+
try {
|
|
18
|
+
const backupInfo = await client.getKeyBackupVersion();
|
|
19
|
+
const whoami = await client.getWhoAmI();
|
|
20
|
+
const deviceId = whoami.device_id ?? null;
|
|
21
|
+
return {
|
|
22
|
+
enabled: backupInfo !== null,
|
|
23
|
+
version: backupInfo?.version ?? null,
|
|
24
|
+
deviceId,
|
|
25
|
+
};
|
|
26
|
+
} catch (err) {
|
|
27
|
+
LogService.warn("MatrixKeyBackup", "Failed to check key backup status:", err);
|
|
28
|
+
return { enabled: false, version: null, deviceId: null };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function setupKeyBackup(client: MatrixClient): Promise<void> {
|
|
33
|
+
const LogService = getMatrixLogService();
|
|
34
|
+
|
|
35
|
+
let deviceId = "(unknown)";
|
|
36
|
+
try {
|
|
37
|
+
const whoami = await client.getWhoAmI();
|
|
38
|
+
deviceId = whoami.device_id ?? deviceId;
|
|
39
|
+
} catch {
|
|
40
|
+
// Non-fatal
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
LogService.info("MatrixKeyBackup", `Crypto ready — device ID: ${deviceId}`);
|
|
44
|
+
|
|
45
|
+
if (!client.crypto) {
|
|
46
|
+
LogService.info("MatrixKeyBackup", "Crypto client not available, skipping key backup setup");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const backupInfo = await client.getKeyBackupVersion();
|
|
52
|
+
if (!backupInfo) {
|
|
53
|
+
LogService.info(
|
|
54
|
+
"MatrixKeyBackup",
|
|
55
|
+
"No key backup version found on server — skipping backup setup",
|
|
56
|
+
);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await client.crypto.enableKeyBackup(backupInfo);
|
|
61
|
+
LogService.info(
|
|
62
|
+
"MatrixKeyBackup",
|
|
63
|
+
`Key backup enabled (version ${backupInfo.version}) on device ${deviceId}`,
|
|
64
|
+
);
|
|
65
|
+
_persistBackupRecord(backupInfo.version, deviceId);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
LogService.warn("MatrixKeyBackup", "Key backup setup failed:", err);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _persistBackupRecord(version: string, deviceId: string): void {
|
|
72
|
+
try {
|
|
73
|
+
const dir = join(homedir(), ".openclaw", "backup");
|
|
74
|
+
mkdirSync(dir, { recursive: true });
|
|
75
|
+
const path = join(dir, "key-backup-record.log");
|
|
76
|
+
const entry =
|
|
77
|
+
JSON.stringify({ version, deviceId, timestamp: new Date().toISOString() }) + "\n";
|
|
78
|
+
appendFileSync(path, entry, "utf8");
|
|
79
|
+
} catch {
|
|
80
|
+
// Non-fatal — backup is enabled, record persistence is best-effort
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -100,6 +100,31 @@ export function resolveMatrixConfig(
|
|
|
100
100
|
return resolveMatrixConfigForAccount(cfg, DEFAULT_ACCOUNT_ID, env);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the actual homeserver base URL via Matrix .well-known discovery.
|
|
105
|
+
* Falls back to the provided URL if discovery fails or returns no result.
|
|
106
|
+
* This ensures the bot always connects to the correct shard even when only
|
|
107
|
+
* the canonical domain (e.g. badger.signout.io) is configured.
|
|
108
|
+
*/
|
|
109
|
+
async function resolveHomeserverViaWellKnown(homeserver: string): Promise<string> {
|
|
110
|
+
try {
|
|
111
|
+
const url = new URL(homeserver);
|
|
112
|
+
const wellKnownUrl = `${url.protocol}//${url.host}/.well-known/matrix/client`;
|
|
113
|
+
const resp = await fetch(wellKnownUrl, { signal: AbortSignal.timeout(5000) });
|
|
114
|
+
if (resp.ok) {
|
|
115
|
+
const data = (await resp.json()) as Record<string, unknown>;
|
|
116
|
+
const baseUrl = (data?.["m.homeserver"] as Record<string, unknown>)?.["base_url"];
|
|
117
|
+
if (typeof baseUrl === "string" && baseUrl.startsWith("http")) {
|
|
118
|
+
// Strip trailing slash for consistency
|
|
119
|
+
return baseUrl.replace(/\/$/, "");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// Discovery failed — use configured URL as-is
|
|
124
|
+
}
|
|
125
|
+
return homeserver;
|
|
126
|
+
}
|
|
127
|
+
|
|
103
128
|
export async function resolveMatrixAuth(params?: {
|
|
104
129
|
cfg?: CoreConfig;
|
|
105
130
|
env?: NodeJS.ProcessEnv;
|
|
@@ -112,6 +137,10 @@ export async function resolveMatrixAuth(params?: {
|
|
|
112
137
|
throw new Error("BadgerClaw homeserver is required (matrix.homeserver)");
|
|
113
138
|
}
|
|
114
139
|
|
|
140
|
+
// Resolve the actual shard via .well-known so we always hit the right server
|
|
141
|
+
// even when the canonical domain (e.g. badger.signout.io) is configured.
|
|
142
|
+
resolved.homeserver = await resolveHomeserverViaWellKnown(resolved.homeserver);
|
|
143
|
+
|
|
115
144
|
const {
|
|
116
145
|
loadMatrixCredentials,
|
|
117
146
|
saveMatrixCredentials,
|
|
@@ -2,6 +2,7 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
|
2
2
|
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
3
3
|
import type { CoreConfig } from "../../types.js";
|
|
4
4
|
import { getMatrixLogService } from "../sdk-runtime.js";
|
|
5
|
+
import { setupKeyBackup } from "./backup.js";
|
|
5
6
|
import { resolveMatrixAuth } from "./config.js";
|
|
6
7
|
import { createMatrixClient } from "./create-client.js";
|
|
7
8
|
import { startMatrixClientWithGrace } from "./startup.js";
|
|
@@ -79,6 +80,7 @@ async function ensureSharedClientStarted(params: {
|
|
|
79
80
|
joinedRooms,
|
|
80
81
|
);
|
|
81
82
|
params.state.cryptoReady = true;
|
|
83
|
+
await setupKeyBackup(client);
|
|
82
84
|
}
|
|
83
85
|
} catch (err) {
|
|
84
86
|
const LogService = getMatrixLogService();
|
|
@@ -91,7 +93,18 @@ async function ensureSharedClientStarted(params: {
|
|
|
91
93
|
onError: (err: unknown) => {
|
|
92
94
|
params.state.started = false;
|
|
93
95
|
const LogService = getMatrixLogService();
|
|
94
|
-
|
|
96
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
97
|
+
// OTK conflict means the server has stale keys for this device.
|
|
98
|
+
// Log clearly so the user knows to re-pair.
|
|
99
|
+
if (message.includes("One time key") && message.includes("already exists")) {
|
|
100
|
+
LogService.error(
|
|
101
|
+
"MatrixClientLite",
|
|
102
|
+
"E2EE key conflict detected — the bot's encryption session is stale.",
|
|
103
|
+
"Run: /bot pair <botname> in BadgerClaw and reconnect with 'openclaw badgerclaw connect <code>'",
|
|
104
|
+
);
|
|
105
|
+
} else {
|
|
106
|
+
LogService.error("MatrixClientLite", "client.start() error:", err);
|
|
107
|
+
}
|
|
95
108
|
},
|
|
96
109
|
});
|
|
97
110
|
params.state.started = true;
|
|
@@ -4,6 +4,9 @@ import { getMatrixRuntime } from "../../runtime.js";
|
|
|
4
4
|
import type { CoreConfig } from "../../types.js";
|
|
5
5
|
import { loadMatrixSdk } from "../sdk-runtime.js";
|
|
6
6
|
|
|
7
|
+
// Track clients that already have auto-join registered to prevent duplicate listeners
|
|
8
|
+
const autoJoinRegistered = new WeakSet<object>();
|
|
9
|
+
|
|
7
10
|
export function registerMatrixAutoJoin(params: {
|
|
8
11
|
client: MatrixClient;
|
|
9
12
|
cfg: CoreConfig;
|
|
@@ -24,6 +27,13 @@ export function registerMatrixAutoJoin(params: {
|
|
|
24
27
|
return;
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
// Prevent duplicate listener registration on the same client instance
|
|
31
|
+
if (autoJoinRegistered.has(client)) {
|
|
32
|
+
logVerbose("badgerclaw: auto-join already registered for this client, skipping");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
autoJoinRegistered.add(client);
|
|
36
|
+
|
|
27
37
|
if (autoJoin === "always") {
|
|
28
38
|
// Use the built-in autojoin mixin for "always" mode
|
|
29
39
|
const { AutojoinRoomsMixin } = loadMatrixSdk();
|
package/src/onboarding.ts
CHANGED
|
@@ -227,6 +227,7 @@ export const badgerclawOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
227
227
|
|
|
228
228
|
// Pairing code flow — the primary onboarding path
|
|
229
229
|
const pairing = await promptPairingCode(prompter);
|
|
230
|
+
const isFreshPairing = Boolean(pairing);
|
|
230
231
|
if (pairing) {
|
|
231
232
|
next = {
|
|
232
233
|
...next,
|
|
@@ -240,7 +241,10 @@ export const badgerclawOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
240
241
|
userId: pairing.userId,
|
|
241
242
|
deviceName: pairing.botName || "OpenClaw Gateway",
|
|
242
243
|
encryption: true,
|
|
244
|
+
// Auto-join every invite immediately — iMessage-style behavior
|
|
243
245
|
autoJoin: "always",
|
|
246
|
+
// Open policy: bot responds in any room it's invited to
|
|
247
|
+
groupPolicy: "open",
|
|
244
248
|
dm: {
|
|
245
249
|
...next.channels?.badgerclaw?.dm,
|
|
246
250
|
policy: "open",
|
|
@@ -255,15 +259,20 @@ export const badgerclawOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
255
259
|
next = await promptMatrixAllowFrom({ cfg: next, prompter });
|
|
256
260
|
}
|
|
257
261
|
|
|
262
|
+
// Skip the room allowlist prompt for fresh pairing setups — groupPolicy is already
|
|
263
|
+
// set to "open" above, which is the correct default (join and respond in any room).
|
|
264
|
+
// Only show the advanced room config prompt when updating an existing setup.
|
|
258
265
|
const existingGroups = next.channels?.badgerclaw?.groups ?? next.channels?.badgerclaw?.rooms;
|
|
259
|
-
const accessConfig =
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
266
|
+
const accessConfig = isFreshPairing
|
|
267
|
+
? null
|
|
268
|
+
: await promptChannelAccessConfig({
|
|
269
|
+
prompter,
|
|
270
|
+
label: "BadgerClaw rooms",
|
|
271
|
+
currentPolicy: next.channels?.badgerclaw?.groupPolicy ?? "open",
|
|
272
|
+
currentEntries: Object.keys(existingGroups ?? {}),
|
|
273
|
+
placeholder: "!roomId:server, #alias:server, Project Room",
|
|
274
|
+
updatePrompt: Boolean(existingGroups),
|
|
275
|
+
});
|
|
267
276
|
if (accessConfig) {
|
|
268
277
|
if (accessConfig.policy !== "allowlist") {
|
|
269
278
|
next = setMatrixGroupPolicy(next, accessConfig.policy);
|