@openclaw/nextcloud-talk 2026.3.8-beta.1 → 2026.3.11

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": "@openclaw/nextcloud-talk",
3
- "version": "2026.3.8-beta.1",
3
+ "version": "2026.3.11",
4
4
  "description": "OpenClaw Nextcloud Talk channel plugin",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -0,0 +1,30 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { resolveNextcloudTalkAccount } from "./accounts.js";
6
+ import type { CoreConfig } from "./types.js";
7
+
8
+ describe("resolveNextcloudTalkAccount", () => {
9
+ it.runIf(process.platform !== "win32")("rejects symlinked botSecretFile paths", () => {
10
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-nextcloud-talk-"));
11
+ const secretFile = path.join(dir, "secret.txt");
12
+ const secretLink = path.join(dir, "secret-link.txt");
13
+ fs.writeFileSync(secretFile, "bot-secret\n", "utf8");
14
+ fs.symlinkSync(secretFile, secretLink);
15
+
16
+ const cfg = {
17
+ channels: {
18
+ "nextcloud-talk": {
19
+ baseUrl: "https://cloud.example.com",
20
+ botSecretFile: secretLink,
21
+ },
22
+ },
23
+ } as CoreConfig;
24
+
25
+ const account = resolveNextcloudTalkAccount({ cfg });
26
+ expect(account.secret).toBe("");
27
+ expect(account.secretSource).toBe("none");
28
+ fs.rmSync(dir, { recursive: true, force: true });
29
+ });
30
+ });
package/src/accounts.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import { tryReadSecretFileSync } from "openclaw/plugin-sdk/core";
2
2
  import {
3
3
  createAccountListHelpers,
4
4
  DEFAULT_ACCOUNT_ID,
@@ -88,13 +88,13 @@ function resolveNextcloudTalkSecret(
88
88
  }
89
89
 
90
90
  if (merged.botSecretFile) {
91
- try {
92
- const fileSecret = readFileSync(merged.botSecretFile, "utf-8").trim();
93
- if (fileSecret) {
94
- return { secret: fileSecret, source: "secretFile" };
95
- }
96
- } catch {
97
- // File not found or unreadable, fall through.
91
+ const fileSecret = tryReadSecretFileSync(
92
+ merged.botSecretFile,
93
+ "Nextcloud Talk bot secret file",
94
+ { rejectSymlink: true },
95
+ );
96
+ if (fileSecret) {
97
+ return { secret: fileSecret, source: "secretFile" };
98
98
  }
99
99
  }
100
100
 
package/src/channel.ts CHANGED
@@ -2,8 +2,10 @@ import {
2
2
  buildAccountScopedDmSecurityPolicy,
3
3
  collectAllowlistProviderGroupPolicyWarnings,
4
4
  collectOpenGroupPolicyRouteAllowlistWarnings,
5
+ createAccountStatusSink,
5
6
  formatAllowFromLowercase,
6
7
  mapAllowFromEntries,
8
+ runPassiveAccountLifecycle,
7
9
  } from "openclaw/plugin-sdk/compat";
8
10
  import {
9
11
  applyAccountNameToChannelSection,
@@ -15,7 +17,6 @@ import {
15
17
  deleteAccountFromConfigSection,
16
18
  normalizeAccountId,
17
19
  setAccountEnabledInConfigSection,
18
- waitForAbortSignal,
19
20
  type ChannelPlugin,
20
21
  type OpenClawConfig,
21
22
  type ChannelSetupInput,
@@ -338,17 +339,25 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
338
339
 
339
340
  ctx.log?.info(`[${account.accountId}] starting Nextcloud Talk webhook server`);
340
341
 
341
- const { stop } = await monitorNextcloudTalkProvider({
342
- accountId: account.accountId,
343
- config: ctx.cfg as CoreConfig,
344
- runtime: ctx.runtime,
345
- abortSignal: ctx.abortSignal,
346
- statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
342
+ const statusSink = createAccountStatusSink({
343
+ accountId: ctx.accountId,
344
+ setStatus: ctx.setStatus,
347
345
  });
348
346
 
349
- // Keep webhook channels pending for the account lifecycle.
350
- await waitForAbortSignal(ctx.abortSignal);
351
- stop();
347
+ await runPassiveAccountLifecycle({
348
+ abortSignal: ctx.abortSignal,
349
+ start: async () =>
350
+ await monitorNextcloudTalkProvider({
351
+ accountId: account.accountId,
352
+ config: ctx.cfg as CoreConfig,
353
+ runtime: ctx.runtime,
354
+ abortSignal: ctx.abortSignal,
355
+ statusSink,
356
+ }),
357
+ stop: async (monitor) => {
358
+ monitor.stop();
359
+ },
360
+ });
352
361
  },
353
362
  logoutAccount: async ({ accountId, cfg }) => {
354
363
  const nextCfg = { ...cfg } as OpenClawConfig;
package/src/onboarding.ts CHANGED
@@ -1,15 +1,14 @@
1
1
  import {
2
- buildSingleChannelSecretPromptState,
3
2
  formatDocsLink,
4
3
  hasConfiguredSecretInput,
5
4
  mapAllowFromEntries,
6
5
  mergeAllowFromEntries,
7
- promptSingleChannelSecretInput,
6
+ patchScopedAccountConfig,
7
+ runSingleChannelSecretStep,
8
8
  resolveAccountIdForConfigure,
9
9
  DEFAULT_ACCOUNT_ID,
10
10
  normalizeAccountId,
11
11
  setTopLevelChannelDmPolicyWithAllowFrom,
12
- type SecretInput,
13
12
  type ChannelOnboardingAdapter,
14
13
  type ChannelOnboardingDmPolicy,
15
14
  type OpenClawConfig,
@@ -39,38 +38,12 @@ function setNextcloudTalkAccountConfig(
39
38
  accountId: string,
40
39
  updates: Record<string, unknown>,
41
40
  ): CoreConfig {
42
- if (accountId === DEFAULT_ACCOUNT_ID) {
43
- return {
44
- ...cfg,
45
- channels: {
46
- ...cfg.channels,
47
- "nextcloud-talk": {
48
- ...cfg.channels?.["nextcloud-talk"],
49
- enabled: true,
50
- ...updates,
51
- },
52
- },
53
- };
54
- }
55
-
56
- return {
57
- ...cfg,
58
- channels: {
59
- ...cfg.channels,
60
- "nextcloud-talk": {
61
- ...cfg.channels?.["nextcloud-talk"],
62
- enabled: true,
63
- accounts: {
64
- ...cfg.channels?.["nextcloud-talk"]?.accounts,
65
- [accountId]: {
66
- ...cfg.channels?.["nextcloud-talk"]?.accounts?.[accountId],
67
- enabled: cfg.channels?.["nextcloud-talk"]?.accounts?.[accountId]?.enabled ?? true,
68
- ...updates,
69
- },
70
- },
71
- },
72
- },
73
- };
41
+ return patchScopedAccountConfig({
42
+ cfg,
43
+ channelKey: channel,
44
+ accountId,
45
+ patch: updates,
46
+ }) as CoreConfig;
74
47
  }
75
48
 
76
49
  async function noteNextcloudTalkSecretHelp(prompter: WizardPrompter): Promise<void> {
@@ -215,12 +188,6 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
215
188
  hasConfiguredSecretInput(resolvedAccount.config.botSecret) ||
216
189
  resolvedAccount.config.botSecretFile,
217
190
  );
218
- const secretPromptState = buildSingleChannelSecretPromptState({
219
- accountConfigured,
220
- hasConfigToken: hasConfigSecret,
221
- allowEnv,
222
- envValue: process.env.NEXTCLOUD_TALK_BOT_SECRET,
223
- });
224
191
 
225
192
  let baseUrl = resolvedAccount.baseUrl;
226
193
  if (!baseUrl) {
@@ -241,32 +208,35 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
241
208
  ).trim();
242
209
  }
243
210
 
244
- let secret: SecretInput | null = null;
245
- if (!accountConfigured) {
246
- await noteNextcloudTalkSecretHelp(prompter);
247
- }
248
-
249
- const secretResult = await promptSingleChannelSecretInput({
211
+ const secretStep = await runSingleChannelSecretStep({
250
212
  cfg: next,
251
213
  prompter,
252
214
  providerHint: "nextcloud-talk",
253
215
  credentialLabel: "bot secret",
254
- accountConfigured: secretPromptState.accountConfigured,
255
- canUseEnv: secretPromptState.canUseEnv,
256
- hasConfigToken: secretPromptState.hasConfigToken,
216
+ accountConfigured,
217
+ hasConfigToken: hasConfigSecret,
218
+ allowEnv,
219
+ envValue: process.env.NEXTCLOUD_TALK_BOT_SECRET,
257
220
  envPrompt: "NEXTCLOUD_TALK_BOT_SECRET detected. Use env var?",
258
221
  keepPrompt: "Nextcloud Talk bot secret already configured. Keep it?",
259
222
  inputPrompt: "Enter Nextcloud Talk bot secret",
260
223
  preferredEnvVar: "NEXTCLOUD_TALK_BOT_SECRET",
224
+ onMissingConfigured: async () => await noteNextcloudTalkSecretHelp(prompter),
225
+ applyUseEnv: async (cfg) =>
226
+ setNextcloudTalkAccountConfig(cfg as CoreConfig, accountId, {
227
+ baseUrl,
228
+ }),
229
+ applySet: async (cfg, value) =>
230
+ setNextcloudTalkAccountConfig(cfg as CoreConfig, accountId, {
231
+ baseUrl,
232
+ botSecret: value,
233
+ }),
261
234
  });
262
- if (secretResult.action === "set") {
263
- secret = secretResult.value;
264
- }
235
+ next = secretStep.cfg as CoreConfig;
265
236
 
266
- if (secretResult.action === "use-env" || secret || baseUrl !== resolvedAccount.baseUrl) {
237
+ if (secretStep.action === "keep" && baseUrl !== resolvedAccount.baseUrl) {
267
238
  next = setNextcloudTalkAccountConfig(next, accountId, {
268
239
  baseUrl,
269
- ...(secret ? { botSecret: secret } : {}),
270
240
  });
271
241
  }
272
242
 
@@ -287,26 +257,28 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
287
257
  validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
288
258
  }),
289
259
  ).trim();
290
- const apiPasswordResult = await promptSingleChannelSecretInput({
260
+ const apiPasswordStep = await runSingleChannelSecretStep({
291
261
  cfg: next,
292
262
  prompter,
293
263
  providerHint: "nextcloud-talk-api",
294
264
  credentialLabel: "API password",
295
- ...buildSingleChannelSecretPromptState({
296
- accountConfigured: Boolean(existingApiUser && existingApiPasswordConfigured),
297
- hasConfigToken: existingApiPasswordConfigured,
298
- allowEnv: false,
299
- }),
265
+ accountConfigured: Boolean(existingApiUser && existingApiPasswordConfigured),
266
+ hasConfigToken: existingApiPasswordConfigured,
267
+ allowEnv: false,
300
268
  envPrompt: "",
301
269
  keepPrompt: "Nextcloud Talk API password already configured. Keep it?",
302
270
  inputPrompt: "Enter Nextcloud Talk API password",
303
271
  preferredEnvVar: "NEXTCLOUD_TALK_API_PASSWORD",
272
+ applySet: async (cfg, value) =>
273
+ setNextcloudTalkAccountConfig(cfg as CoreConfig, accountId, {
274
+ apiUser,
275
+ apiPassword: value,
276
+ }),
304
277
  });
305
- const apiPassword = apiPasswordResult.action === "set" ? apiPasswordResult.value : undefined;
306
- next = setNextcloudTalkAccountConfig(next, accountId, {
307
- apiUser,
308
- ...(apiPassword ? { apiPassword } : {}),
309
- });
278
+ next =
279
+ apiPasswordStep.action === "keep"
280
+ ? setNextcloudTalkAccountConfig(next, accountId, { apiUser })
281
+ : (apiPasswordStep.cfg as CoreConfig);
310
282
  }
311
283
 
312
284
  if (forceAllowFrom) {