@openclaw/nextcloud-talk 2026.3.1 → 2026.3.2

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.1",
3
+ "version": "2026.3.2",
4
4
  "description": "OpenClaw Nextcloud Talk channel plugin",
5
5
  "type": "module",
6
6
  "openclaw": {
package/src/accounts.ts CHANGED
@@ -1,9 +1,14 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import {
3
+ listConfiguredAccountIds as listConfiguredAccountIdsFromSection,
4
+ resolveAccountWithDefaultFallback,
5
+ } from "openclaw/plugin-sdk";
2
6
  import {
3
7
  DEFAULT_ACCOUNT_ID,
4
8
  normalizeAccountId,
5
9
  normalizeOptionalAccountId,
6
10
  } from "openclaw/plugin-sdk/account-id";
11
+ import { normalizeResolvedSecretInputString } from "./secret-input.js";
7
12
  import type { CoreConfig, NextcloudTalkAccountConfig } from "./types.js";
8
13
 
9
14
  function isTruthyEnvValue(value?: string): boolean {
@@ -28,18 +33,10 @@ export type ResolvedNextcloudTalkAccount = {
28
33
  };
29
34
 
30
35
  function listConfiguredAccountIds(cfg: CoreConfig): string[] {
31
- const accounts = cfg.channels?.["nextcloud-talk"]?.accounts;
32
- if (!accounts || typeof accounts !== "object") {
33
- return [];
34
- }
35
- const ids = new Set<string>();
36
- for (const key of Object.keys(accounts)) {
37
- if (!key) {
38
- continue;
39
- }
40
- ids.add(normalizeAccountId(key));
41
- }
42
- return [...ids];
36
+ return listConfiguredAccountIdsFromSection({
37
+ accounts: cfg.channels?.["nextcloud-talk"]?.accounts as Record<string, unknown> | undefined,
38
+ normalizeAccountId,
39
+ });
43
40
  }
44
41
 
45
42
  export function listNextcloudTalkAccountIds(cfg: CoreConfig): string[] {
@@ -123,8 +120,12 @@ function resolveNextcloudTalkSecret(
123
120
  }
124
121
  }
125
122
 
126
- if (merged.botSecret?.trim()) {
127
- return { secret: merged.botSecret.trim(), source: "config" };
123
+ const inlineSecret = normalizeResolvedSecretInputString({
124
+ value: merged.botSecret,
125
+ path: `channels.nextcloud-talk.accounts.${opts.accountId ?? DEFAULT_ACCOUNT_ID}.botSecret`,
126
+ });
127
+ if (inlineSecret) {
128
+ return { secret: inlineSecret, source: "config" };
128
129
  }
129
130
 
130
131
  return { secret: "", source: "none" };
@@ -134,7 +135,6 @@ export function resolveNextcloudTalkAccount(params: {
134
135
  cfg: CoreConfig;
135
136
  accountId?: string | null;
136
137
  }): ResolvedNextcloudTalkAccount {
137
- const hasExplicitAccountId = Boolean(params.accountId?.trim());
138
138
  const baseEnabled = params.cfg.channels?.["nextcloud-talk"]?.enabled !== false;
139
139
 
140
140
  const resolve = (accountId: string) => {
@@ -162,24 +162,13 @@ export function resolveNextcloudTalkAccount(params: {
162
162
  } satisfies ResolvedNextcloudTalkAccount;
163
163
  };
164
164
 
165
- const normalized = normalizeAccountId(params.accountId);
166
- const primary = resolve(normalized);
167
- if (hasExplicitAccountId) {
168
- return primary;
169
- }
170
- if (primary.secretSource !== "none") {
171
- return primary;
172
- }
173
-
174
- const fallbackId = resolveDefaultNextcloudTalkAccountId(params.cfg);
175
- if (fallbackId === primary.accountId) {
176
- return primary;
177
- }
178
- const fallback = resolve(fallbackId);
179
- if (fallback.secretSource === "none") {
180
- return primary;
181
- }
182
- return fallback;
165
+ return resolveAccountWithDefaultFallback({
166
+ accountId: params.accountId,
167
+ normalizeAccountId,
168
+ resolvePrimary: resolve,
169
+ hasCredential: (account) => account.secretSource !== "none",
170
+ resolveDefaultAccountId: () => resolveDefaultNextcloudTalkAccountId(params.cfg),
171
+ });
183
172
  }
184
173
 
185
174
  export function listEnabledNextcloudTalkAccounts(cfg: CoreConfig): ResolvedNextcloudTalkAccount[] {
@@ -1,10 +1,5 @@
1
- import type {
2
- ChannelAccountSnapshot,
3
- ChannelGatewayContext,
4
- OpenClawConfig,
5
- } from "openclaw/plugin-sdk";
6
1
  import { afterEach, describe, expect, it, vi } from "vitest";
7
- import { createRuntimeEnv } from "../../test-utils/runtime-env.js";
2
+ import { createStartAccountContext } from "../../test-utils/start-account-context.js";
8
3
  import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
9
4
 
10
5
  const hoisted = vi.hoisted(() => ({
@@ -21,30 +16,6 @@ vi.mock("./monitor.js", async () => {
21
16
 
22
17
  import { nextcloudTalkPlugin } from "./channel.js";
23
18
 
24
- function createStartAccountCtx(params: {
25
- account: ResolvedNextcloudTalkAccount;
26
- abortSignal: AbortSignal;
27
- }): ChannelGatewayContext<ResolvedNextcloudTalkAccount> {
28
- const snapshot: ChannelAccountSnapshot = {
29
- accountId: params.account.accountId,
30
- configured: true,
31
- enabled: true,
32
- running: false,
33
- };
34
- return {
35
- accountId: params.account.accountId,
36
- account: params.account,
37
- cfg: {} as OpenClawConfig,
38
- runtime: createRuntimeEnv(),
39
- abortSignal: params.abortSignal,
40
- log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
41
- getStatus: () => snapshot,
42
- setStatus: (next) => {
43
- Object.assign(snapshot, next);
44
- },
45
- };
46
- }
47
-
48
19
  function buildAccount(): ResolvedNextcloudTalkAccount {
49
20
  return {
50
21
  accountId: "default",
@@ -72,22 +43,19 @@ describe("nextcloudTalkPlugin gateway.startAccount", () => {
72
43
  const abort = new AbortController();
73
44
 
74
45
  const task = nextcloudTalkPlugin.gateway!.startAccount!(
75
- createStartAccountCtx({
46
+ createStartAccountContext({
76
47
  account: buildAccount(),
77
48
  abortSignal: abort.signal,
78
49
  }),
79
50
  );
80
-
81
- await new Promise((resolve) => setTimeout(resolve, 20));
82
-
83
51
  let settled = false;
84
52
  void task.then(() => {
85
53
  settled = true;
86
54
  });
87
-
88
- await new Promise((resolve) => setTimeout(resolve, 20));
55
+ await vi.waitFor(() => {
56
+ expect(hoisted.monitorNextcloudTalkProvider).toHaveBeenCalledOnce();
57
+ });
89
58
  expect(settled).toBe(false);
90
- expect(hoisted.monitorNextcloudTalkProvider).toHaveBeenCalledOnce();
91
59
  expect(stop).not.toHaveBeenCalled();
92
60
 
93
61
  abort.abort();
@@ -103,7 +71,7 @@ describe("nextcloudTalkPlugin gateway.startAccount", () => {
103
71
  abort.abort();
104
72
 
105
73
  await nextcloudTalkPlugin.gateway!.startAccount!(
106
- createStartAccountCtx({
74
+ createStartAccountContext({
107
75
  account: buildAccount(),
108
76
  abortSignal: abort.signal,
109
77
  }),
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { NextcloudTalkConfigSchema } from "./config-schema.js";
3
+
4
+ describe("NextcloudTalkConfigSchema SecretInput", () => {
5
+ it("accepts SecretRef botSecret and apiPassword at top-level", () => {
6
+ const result = NextcloudTalkConfigSchema.safeParse({
7
+ baseUrl: "https://cloud.example.com",
8
+ botSecret: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_BOT_SECRET" },
9
+ apiUser: "bot",
10
+ apiPassword: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_API_PASSWORD" },
11
+ });
12
+ expect(result.success).toBe(true);
13
+ });
14
+
15
+ it("accepts SecretRef botSecret and apiPassword on account", () => {
16
+ const result = NextcloudTalkConfigSchema.safeParse({
17
+ accounts: {
18
+ main: {
19
+ baseUrl: "https://cloud.example.com",
20
+ botSecret: {
21
+ source: "env",
22
+ provider: "default",
23
+ id: "NEXTCLOUD_TALK_MAIN_BOT_SECRET",
24
+ },
25
+ apiUser: "bot",
26
+ apiPassword: {
27
+ source: "env",
28
+ provider: "default",
29
+ id: "NEXTCLOUD_TALK_MAIN_API_PASSWORD",
30
+ },
31
+ },
32
+ },
33
+ });
34
+ expect(result.success).toBe(true);
35
+ });
36
+ });
@@ -9,6 +9,7 @@ import {
9
9
  requireOpenAllowFrom,
10
10
  } from "openclaw/plugin-sdk";
11
11
  import { z } from "zod";
12
+ import { buildSecretInputSchema } from "./secret-input.js";
12
13
 
13
14
  export const NextcloudTalkRoomSchema = z
14
15
  .object({
@@ -27,10 +28,10 @@ export const NextcloudTalkAccountSchemaBase = z
27
28
  enabled: z.boolean().optional(),
28
29
  markdown: MarkdownConfigSchema,
29
30
  baseUrl: z.string().optional(),
30
- botSecret: z.string().optional(),
31
+ botSecret: buildSecretInputSchema().optional(),
31
32
  botSecretFile: z.string().optional(),
32
33
  apiUser: z.string().optional(),
33
- apiPassword: z.string().optional(),
34
+ apiPassword: buildSecretInputSchema().optional(),
34
35
  apiPasswordFile: z.string().optional(),
35
36
  dmPolicy: DmPolicySchema.optional().default("pairing"),
36
37
  webhookPort: z.number().int().positive().optional(),
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
+ import { createSignedCreateMessageRequest } from "./monitor.test-fixtures.js";
2
3
  import { startWebhookServer } from "./monitor.test-harness.js";
3
- import { generateNextcloudTalkSignature } from "./signature.js";
4
4
 
5
5
  describe("createNextcloudTalkWebhookServer backend allowlist", () => {
6
6
  it("rejects requests from unexpected backend origins", async () => {
@@ -11,31 +11,12 @@ describe("createNextcloudTalkWebhookServer backend allowlist", () => {
11
11
  onMessage,
12
12
  });
13
13
 
14
- const payload = {
15
- type: "Create",
16
- actor: { type: "Person", id: "alice", name: "Alice" },
17
- object: {
18
- type: "Note",
19
- id: "msg-1",
20
- name: "hello",
21
- content: "hello",
22
- mediaType: "text/plain",
23
- },
24
- target: { type: "Collection", id: "room-1", name: "Room 1" },
25
- };
26
- const body = JSON.stringify(payload);
27
- const { random, signature } = generateNextcloudTalkSignature({
28
- body,
29
- secret: "nextcloud-secret",
14
+ const { body, headers } = createSignedCreateMessageRequest({
15
+ backend: "https://nextcloud.unexpected",
30
16
  });
31
17
  const response = await fetch(harness.webhookUrl, {
32
18
  method: "POST",
33
- headers: {
34
- "content-type": "application/json",
35
- "x-nextcloud-talk-random": random,
36
- "x-nextcloud-talk-signature": signature,
37
- "x-nextcloud-talk-backend": "https://nextcloud.unexpected",
38
- },
19
+ headers,
39
20
  body,
40
21
  });
41
22
 
@@ -1,15 +1,8 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
+ import { createSignedCreateMessageRequest } from "./monitor.test-fixtures.js";
2
3
  import { startWebhookServer } from "./monitor.test-harness.js";
3
- import { generateNextcloudTalkSignature } from "./signature.js";
4
4
  import type { NextcloudTalkInboundMessage } from "./types.js";
5
5
 
6
- function createSignedRequest(body: string): { random: string; signature: string } {
7
- return generateNextcloudTalkSignature({
8
- body,
9
- secret: "nextcloud-secret",
10
- });
11
- }
12
-
13
6
  describe("createNextcloudTalkWebhookServer replay handling", () => {
14
7
  it("acknowledges replayed requests and skips onMessage side effects", async () => {
15
8
  const seen = new Set<string>();
@@ -27,26 +20,7 @@ describe("createNextcloudTalkWebhookServer replay handling", () => {
27
20
  onMessage,
28
21
  });
29
22
 
30
- const payload = {
31
- type: "Create",
32
- actor: { type: "Person", id: "alice", name: "Alice" },
33
- object: {
34
- type: "Note",
35
- id: "msg-1",
36
- name: "hello",
37
- content: "hello",
38
- mediaType: "text/plain",
39
- },
40
- target: { type: "Collection", id: "room-1", name: "Room 1" },
41
- };
42
- const body = JSON.stringify(payload);
43
- const { random, signature } = createSignedRequest(body);
44
- const headers = {
45
- "content-type": "application/json",
46
- "x-nextcloud-talk-random": random,
47
- "x-nextcloud-talk-signature": signature,
48
- "x-nextcloud-talk-backend": "https://nextcloud.example",
49
- };
23
+ const { body, headers } = createSignedCreateMessageRequest();
50
24
 
51
25
  const first = await fetch(harness.webhookUrl, {
52
26
  method: "POST",
@@ -0,0 +1,30 @@
1
+ import { generateNextcloudTalkSignature } from "./signature.js";
2
+
3
+ export function createSignedCreateMessageRequest(params?: { backend?: string }) {
4
+ const payload = {
5
+ type: "Create",
6
+ actor: { type: "Person", id: "alice", name: "Alice" },
7
+ object: {
8
+ type: "Note",
9
+ id: "msg-1",
10
+ name: "hello",
11
+ content: "hello",
12
+ mediaType: "text/plain",
13
+ },
14
+ target: { type: "Collection", id: "room-1", name: "Room 1" },
15
+ };
16
+ const body = JSON.stringify(payload);
17
+ const { random, signature } = generateNextcloudTalkSignature({
18
+ body,
19
+ secret: "nextcloud-secret",
20
+ });
21
+ return {
22
+ body,
23
+ headers: {
24
+ "content-type": "application/json",
25
+ "x-nextcloud-talk-random": random,
26
+ "x-nextcloud-talk-signature": signature,
27
+ "x-nextcloud-talk-backend": params?.backend ?? "https://nextcloud.example",
28
+ },
29
+ };
30
+ }
package/src/onboarding.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import {
2
2
  addWildcardAllowFrom,
3
3
  formatDocsLink,
4
+ hasConfiguredSecretInput,
4
5
  mergeAllowFromEntries,
6
+ promptSingleChannelSecretInput,
5
7
  promptAccountId,
6
8
  DEFAULT_ACCOUNT_ID,
7
9
  normalizeAccountId,
10
+ type SecretInput,
8
11
  type ChannelOnboardingAdapter,
9
12
  type ChannelOnboardingDmPolicy,
10
13
  type OpenClawConfig,
@@ -216,7 +219,8 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
216
219
  const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
217
220
  const canUseEnv = allowEnv && Boolean(process.env.NEXTCLOUD_TALK_BOT_SECRET?.trim());
218
221
  const hasConfigSecret = Boolean(
219
- resolvedAccount.config.botSecret || resolvedAccount.config.botSecretFile,
222
+ hasConfiguredSecretInput(resolvedAccount.config.botSecret) ||
223
+ resolvedAccount.config.botSecretFile,
220
224
  );
221
225
 
222
226
  let baseUrl = resolvedAccount.baseUrl;
@@ -238,17 +242,30 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
238
242
  ).trim();
239
243
  }
240
244
 
241
- let secret: string | null = null;
245
+ let secret: SecretInput | null = null;
242
246
  if (!accountConfigured) {
243
247
  await noteNextcloudTalkSecretHelp(prompter);
244
248
  }
245
249
 
246
- if (canUseEnv && !resolvedAccount.config.botSecret) {
247
- const keepEnv = await prompter.confirm({
248
- message: "NEXTCLOUD_TALK_BOT_SECRET detected. Use env var?",
249
- initialValue: true,
250
- });
251
- if (keepEnv) {
250
+ const secretResult = await promptSingleChannelSecretInput({
251
+ cfg: next,
252
+ prompter,
253
+ providerHint: "nextcloud-talk",
254
+ credentialLabel: "bot secret",
255
+ accountConfigured,
256
+ canUseEnv: canUseEnv && !hasConfigSecret,
257
+ hasConfigToken: hasConfigSecret,
258
+ envPrompt: "NEXTCLOUD_TALK_BOT_SECRET detected. Use env var?",
259
+ keepPrompt: "Nextcloud Talk bot secret already configured. Keep it?",
260
+ inputPrompt: "Enter Nextcloud Talk bot secret",
261
+ preferredEnvVar: "NEXTCLOUD_TALK_BOT_SECRET",
262
+ });
263
+ if (secretResult.action === "set") {
264
+ secret = secretResult.value;
265
+ }
266
+
267
+ if (secretResult.action === "use-env" || secret || baseUrl !== resolvedAccount.baseUrl) {
268
+ if (accountId === DEFAULT_ACCOUNT_ID) {
252
269
  next = {
253
270
  ...next,
254
271
  channels: {
@@ -257,40 +274,65 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
257
274
  ...next.channels?.["nextcloud-talk"],
258
275
  enabled: true,
259
276
  baseUrl,
277
+ ...(secret ? { botSecret: secret } : {}),
260
278
  },
261
279
  },
262
280
  };
263
281
  } else {
264
- secret = String(
265
- await prompter.text({
266
- message: "Enter Nextcloud Talk bot secret",
267
- validate: (value) => (value?.trim() ? undefined : "Required"),
268
- }),
269
- ).trim();
270
- }
271
- } else if (hasConfigSecret) {
272
- const keep = await prompter.confirm({
273
- message: "Nextcloud Talk secret already configured. Keep it?",
274
- initialValue: true,
275
- });
276
- if (!keep) {
277
- secret = String(
278
- await prompter.text({
279
- message: "Enter Nextcloud Talk bot secret",
280
- validate: (value) => (value?.trim() ? undefined : "Required"),
281
- }),
282
- ).trim();
282
+ next = {
283
+ ...next,
284
+ channels: {
285
+ ...next.channels,
286
+ "nextcloud-talk": {
287
+ ...next.channels?.["nextcloud-talk"],
288
+ enabled: true,
289
+ accounts: {
290
+ ...next.channels?.["nextcloud-talk"]?.accounts,
291
+ [accountId]: {
292
+ ...next.channels?.["nextcloud-talk"]?.accounts?.[accountId],
293
+ enabled:
294
+ next.channels?.["nextcloud-talk"]?.accounts?.[accountId]?.enabled ?? true,
295
+ baseUrl,
296
+ ...(secret ? { botSecret: secret } : {}),
297
+ },
298
+ },
299
+ },
300
+ },
301
+ };
283
302
  }
284
- } else {
285
- secret = String(
303
+ }
304
+
305
+ const existingApiUser = resolvedAccount.config.apiUser?.trim();
306
+ const existingApiPasswordConfigured = Boolean(
307
+ hasConfiguredSecretInput(resolvedAccount.config.apiPassword) ||
308
+ resolvedAccount.config.apiPasswordFile,
309
+ );
310
+ const configureApiCredentials = await prompter.confirm({
311
+ message: "Configure optional Nextcloud Talk API credentials for room lookups?",
312
+ initialValue: Boolean(existingApiUser && existingApiPasswordConfigured),
313
+ });
314
+ if (configureApiCredentials) {
315
+ const apiUser = String(
286
316
  await prompter.text({
287
- message: "Enter Nextcloud Talk bot secret",
288
- validate: (value) => (value?.trim() ? undefined : "Required"),
317
+ message: "Nextcloud Talk API user",
318
+ initialValue: existingApiUser,
319
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
289
320
  }),
290
321
  ).trim();
291
- }
292
-
293
- if (secret || baseUrl !== resolvedAccount.baseUrl) {
322
+ const apiPasswordResult = await promptSingleChannelSecretInput({
323
+ cfg: next,
324
+ prompter,
325
+ providerHint: "nextcloud-talk-api",
326
+ credentialLabel: "API password",
327
+ accountConfigured: Boolean(existingApiUser && existingApiPasswordConfigured),
328
+ canUseEnv: false,
329
+ hasConfigToken: existingApiPasswordConfigured,
330
+ envPrompt: "",
331
+ keepPrompt: "Nextcloud Talk API password already configured. Keep it?",
332
+ inputPrompt: "Enter Nextcloud Talk API password",
333
+ preferredEnvVar: "NEXTCLOUD_TALK_API_PASSWORD",
334
+ });
335
+ const apiPassword = apiPasswordResult.action === "set" ? apiPasswordResult.value : undefined;
294
336
  if (accountId === DEFAULT_ACCOUNT_ID) {
295
337
  next = {
296
338
  ...next,
@@ -299,8 +341,8 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
299
341
  "nextcloud-talk": {
300
342
  ...next.channels?.["nextcloud-talk"],
301
343
  enabled: true,
302
- baseUrl,
303
- ...(secret ? { botSecret: secret } : {}),
344
+ apiUser,
345
+ ...(apiPassword ? { apiPassword } : {}),
304
346
  },
305
347
  },
306
348
  };
@@ -318,8 +360,8 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
318
360
  ...next.channels?.["nextcloud-talk"]?.accounts?.[accountId],
319
361
  enabled:
320
362
  next.channels?.["nextcloud-talk"]?.accounts?.[accountId]?.enabled ?? true,
321
- baseUrl,
322
- ...(secret ? { botSecret: secret } : {}),
363
+ apiUser,
364
+ ...(apiPassword ? { apiPassword } : {}),
323
365
  },
324
366
  },
325
367
  },
package/src/room-info.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import { fetchWithSsrFGuard } from "openclaw/plugin-sdk";
2
3
  import type { RuntimeEnv } from "openclaw/plugin-sdk";
3
4
  import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
5
+ import { normalizeResolvedSecretInputString } from "./secret-input.js";
4
6
 
5
7
  const ROOM_CACHE_TTL_MS = 5 * 60 * 1000;
6
8
  const ROOM_CACHE_ERROR_TTL_MS = 30 * 1000;
@@ -15,11 +17,15 @@ function resolveRoomCacheKey(params: { accountId: string; roomToken: string }) {
15
17
  }
16
18
 
17
19
  function readApiPassword(params: {
18
- apiPassword?: string;
20
+ apiPassword?: unknown;
19
21
  apiPasswordFile?: string;
20
22
  }): string | undefined {
21
- if (params.apiPassword?.trim()) {
22
- return params.apiPassword.trim();
23
+ const inlinePassword = normalizeResolvedSecretInputString({
24
+ value: params.apiPassword,
25
+ path: "channels.nextcloud-talk.apiPassword",
26
+ });
27
+ if (inlinePassword) {
28
+ return inlinePassword;
23
29
  }
24
30
  if (!params.apiPasswordFile) {
25
31
  return undefined;
@@ -89,31 +95,40 @@ export async function resolveNextcloudTalkRoomKind(params: {
89
95
  const auth = Buffer.from(`${apiUser}:${apiPassword}`, "utf-8").toString("base64");
90
96
 
91
97
  try {
92
- const response = await fetch(url, {
93
- method: "GET",
94
- headers: {
95
- Authorization: `Basic ${auth}`,
96
- "OCS-APIRequest": "true",
97
- Accept: "application/json",
98
+ const { response, release } = await fetchWithSsrFGuard({
99
+ url,
100
+ init: {
101
+ method: "GET",
102
+ headers: {
103
+ Authorization: `Basic ${auth}`,
104
+ "OCS-APIRequest": "true",
105
+ Accept: "application/json",
106
+ },
98
107
  },
108
+ auditContext: "nextcloud-talk.room-info",
99
109
  });
110
+ try {
111
+ if (!response.ok) {
112
+ roomCache.set(key, {
113
+ fetchedAt: Date.now(),
114
+ error: `status:${response.status}`,
115
+ });
116
+ runtime?.log?.(
117
+ `nextcloud-talk: room lookup failed (${response.status}) token=${roomToken}`,
118
+ );
119
+ return undefined;
120
+ }
100
121
 
101
- if (!response.ok) {
102
- roomCache.set(key, {
103
- fetchedAt: Date.now(),
104
- error: `status:${response.status}`,
105
- });
106
- runtime?.log?.(`nextcloud-talk: room lookup failed (${response.status}) token=${roomToken}`);
107
- return undefined;
122
+ const payload = (await response.json()) as {
123
+ ocs?: { data?: { type?: number | string } };
124
+ };
125
+ const type = coerceRoomType(payload.ocs?.data?.type);
126
+ const kind = resolveRoomKindFromType(type);
127
+ roomCache.set(key, { fetchedAt: Date.now(), kind });
128
+ return kind;
129
+ } finally {
130
+ await release();
108
131
  }
109
-
110
- const payload = (await response.json()) as {
111
- ocs?: { data?: { type?: number | string } };
112
- };
113
- const type = coerceRoomType(payload.ocs?.data?.type);
114
- const kind = resolveRoomKindFromType(type);
115
- roomCache.set(key, { fetchedAt: Date.now(), kind });
116
- return kind;
117
132
  } catch (err) {
118
133
  roomCache.set(key, {
119
134
  fetchedAt: Date.now(),
@@ -0,0 +1,19 @@
1
+ import {
2
+ hasConfiguredSecretInput,
3
+ normalizeResolvedSecretInputString,
4
+ normalizeSecretInputString,
5
+ } from "openclaw/plugin-sdk";
6
+ import { z } from "zod";
7
+
8
+ export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString };
9
+
10
+ export function buildSecretInputSchema() {
11
+ return z.union([
12
+ z.string(),
13
+ z.object({
14
+ source: z.enum(["env", "file", "exec"]),
15
+ provider: z.string().min(1),
16
+ id: z.string().min(1),
17
+ }),
18
+ ]);
19
+ }
package/src/types.ts CHANGED
@@ -3,6 +3,7 @@ import type {
3
3
  DmConfig,
4
4
  DmPolicy,
5
5
  GroupPolicy,
6
+ SecretInput,
6
7
  } from "openclaw/plugin-sdk";
7
8
 
8
9
  export type { DmPolicy, GroupPolicy };
@@ -29,13 +30,13 @@ export type NextcloudTalkAccountConfig = {
29
30
  /** Base URL of the Nextcloud instance (e.g., "https://cloud.example.com"). */
30
31
  baseUrl?: string;
31
32
  /** Bot shared secret from occ talk:bot:install output. */
32
- botSecret?: string;
33
+ botSecret?: SecretInput;
33
34
  /** Path to file containing bot secret (for secret managers). */
34
35
  botSecretFile?: string;
35
36
  /** Optional API user for room lookups (DM detection). */
36
37
  apiUser?: string;
37
38
  /** Optional API password/app password for room lookups. */
38
- apiPassword?: string;
39
+ apiPassword?: SecretInput;
39
40
  /** Path to file containing API password/app password. */
40
41
  apiPasswordFile?: string;
41
42
  /** Direct message policy (default: pairing). */