@badgerclaw/connect 1.0.1 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@badgerclaw/connect",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "BadgerClaw channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -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,
@@ -91,7 +91,18 @@ async function ensureSharedClientStarted(params: {
91
91
  onError: (err: unknown) => {
92
92
  params.state.started = false;
93
93
  const LogService = getMatrixLogService();
94
- LogService.error("MatrixClientLite", "client.start() error:", err);
94
+ const message = err instanceof Error ? err.message : String(err);
95
+ // OTK conflict means the server has stale keys for this device.
96
+ // Log clearly so the user knows to re-pair.
97
+ if (message.includes("One time key") && message.includes("already exists")) {
98
+ LogService.error(
99
+ "MatrixClientLite",
100
+ "E2EE key conflict detected — the bot's encryption session is stale.",
101
+ "Run: /bot pair <botname> in BadgerClaw and reconnect with 'openclaw badgerclaw connect <code>'",
102
+ );
103
+ } else {
104
+ LogService.error("MatrixClientLite", "client.start() error:", err);
105
+ }
95
106
  },
96
107
  });
97
108
  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 = await promptChannelAccessConfig({
260
- prompter,
261
- label: "BadgerClaw rooms",
262
- currentPolicy: next.channels?.badgerclaw?.groupPolicy ?? "allowlist",
263
- currentEntries: Object.keys(existingGroups ?? {}),
264
- placeholder: "!roomId:server, #alias:server, Project Room",
265
- updatePrompt: Boolean(existingGroups),
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);