@openclaw/nextcloud-talk 2026.3.1 → 2026.3.7

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/src/room-info.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { readFileSync } from "node:fs";
2
- import type { RuntimeEnv } from "openclaw/plugin-sdk";
2
+ import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/nextcloud-talk";
3
+ import type { RuntimeEnv } from "openclaw/plugin-sdk/nextcloud-talk";
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(),
package/src/runtime.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk";
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk/nextcloud-talk";
2
2
 
3
3
  let runtime: PluginRuntime | null = null;
4
4
 
@@ -0,0 +1,13 @@
1
+ import {
2
+ buildSecretInputSchema,
3
+ hasConfiguredSecretInput,
4
+ normalizeResolvedSecretInputString,
5
+ normalizeSecretInputString,
6
+ } from "openclaw/plugin-sdk/nextcloud-talk";
7
+
8
+ export {
9
+ buildSecretInputSchema,
10
+ hasConfiguredSecretInput,
11
+ normalizeResolvedSecretInputString,
12
+ normalizeSecretInputString,
13
+ };
@@ -0,0 +1,104 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const hoisted = vi.hoisted(() => ({
4
+ loadConfig: vi.fn(),
5
+ resolveMarkdownTableMode: vi.fn(() => "preserve"),
6
+ convertMarkdownTables: vi.fn((text: string) => text),
7
+ record: vi.fn(),
8
+ resolveNextcloudTalkAccount: vi.fn(() => ({
9
+ accountId: "default",
10
+ baseUrl: "https://nextcloud.example.com",
11
+ secret: "secret-value", // pragma: allowlist secret
12
+ })),
13
+ generateNextcloudTalkSignature: vi.fn(() => ({
14
+ random: "r",
15
+ signature: "s",
16
+ })),
17
+ }));
18
+
19
+ vi.mock("./runtime.js", () => ({
20
+ getNextcloudTalkRuntime: () => ({
21
+ config: {
22
+ loadConfig: hoisted.loadConfig,
23
+ },
24
+ channel: {
25
+ text: {
26
+ resolveMarkdownTableMode: hoisted.resolveMarkdownTableMode,
27
+ convertMarkdownTables: hoisted.convertMarkdownTables,
28
+ },
29
+ activity: {
30
+ record: hoisted.record,
31
+ },
32
+ },
33
+ }),
34
+ }));
35
+
36
+ vi.mock("./accounts.js", () => ({
37
+ resolveNextcloudTalkAccount: hoisted.resolveNextcloudTalkAccount,
38
+ }));
39
+
40
+ vi.mock("./signature.js", () => ({
41
+ generateNextcloudTalkSignature: hoisted.generateNextcloudTalkSignature,
42
+ }));
43
+
44
+ import { sendMessageNextcloudTalk, sendReactionNextcloudTalk } from "./send.js";
45
+
46
+ describe("nextcloud-talk send cfg threading", () => {
47
+ const fetchMock = vi.fn<typeof fetch>();
48
+
49
+ beforeEach(() => {
50
+ vi.clearAllMocks();
51
+ fetchMock.mockReset();
52
+ vi.stubGlobal("fetch", fetchMock);
53
+ });
54
+
55
+ afterEach(() => {
56
+ vi.unstubAllGlobals();
57
+ });
58
+
59
+ it("uses provided cfg for sendMessage and skips runtime loadConfig", async () => {
60
+ const cfg = { source: "provided" } as const;
61
+ fetchMock.mockResolvedValueOnce(
62
+ new Response(
63
+ JSON.stringify({
64
+ ocs: { data: { id: 12345, timestamp: 1_706_000_000 } },
65
+ }),
66
+ { status: 200, headers: { "content-type": "application/json" } },
67
+ ),
68
+ );
69
+
70
+ const result = await sendMessageNextcloudTalk("room:abc123", "hello", {
71
+ cfg,
72
+ accountId: "work",
73
+ });
74
+
75
+ expect(hoisted.loadConfig).not.toHaveBeenCalled();
76
+ expect(hoisted.resolveNextcloudTalkAccount).toHaveBeenCalledWith({
77
+ cfg,
78
+ accountId: "work",
79
+ });
80
+ expect(fetchMock).toHaveBeenCalledTimes(1);
81
+ expect(result).toEqual({
82
+ messageId: "12345",
83
+ roomToken: "abc123",
84
+ timestamp: 1_706_000_000,
85
+ });
86
+ });
87
+
88
+ it("falls back to runtime cfg for sendReaction when cfg is omitted", async () => {
89
+ const runtimeCfg = { source: "runtime" } as const;
90
+ hoisted.loadConfig.mockReturnValueOnce(runtimeCfg);
91
+ fetchMock.mockResolvedValueOnce(new Response("{}", { status: 200 }));
92
+
93
+ const result = await sendReactionNextcloudTalk("room:ops", "m-1", "👍", {
94
+ accountId: "default",
95
+ });
96
+
97
+ expect(result).toEqual({ ok: true });
98
+ expect(hoisted.loadConfig).toHaveBeenCalledTimes(1);
99
+ expect(hoisted.resolveNextcloudTalkAccount).toHaveBeenCalledWith({
100
+ cfg: runtimeCfg,
101
+ accountId: "default",
102
+ });
103
+ });
104
+ });
package/src/send.ts CHANGED
@@ -9,6 +9,7 @@ type NextcloudTalkSendOpts = {
9
9
  accountId?: string;
10
10
  replyTo?: string;
11
11
  verbose?: boolean;
12
+ cfg?: CoreConfig;
12
13
  };
13
14
 
14
15
  function resolveCredentials(
@@ -60,7 +61,7 @@ export async function sendMessageNextcloudTalk(
60
61
  text: string,
61
62
  opts: NextcloudTalkSendOpts = {},
62
63
  ): Promise<NextcloudTalkSendResult> {
63
- const cfg = getNextcloudTalkRuntime().config.loadConfig() as CoreConfig;
64
+ const cfg = (opts.cfg ?? getNextcloudTalkRuntime().config.loadConfig()) as CoreConfig;
64
65
  const account = resolveNextcloudTalkAccount({
65
66
  cfg,
66
67
  accountId: opts.accountId,
@@ -175,7 +176,7 @@ export async function sendReactionNextcloudTalk(
175
176
  reaction: string,
176
177
  opts: Omit<NextcloudTalkSendOpts, "replyTo"> = {},
177
178
  ): Promise<{ ok: true }> {
178
- const cfg = getNextcloudTalkRuntime().config.loadConfig() as CoreConfig;
179
+ const cfg = (opts.cfg ?? getNextcloudTalkRuntime().config.loadConfig()) as CoreConfig;
179
180
  const account = resolveNextcloudTalkAccount({
180
181
  cfg,
181
182
  accountId: opts.accountId,
package/src/types.ts CHANGED
@@ -3,7 +3,8 @@ import type {
3
3
  DmConfig,
4
4
  DmPolicy,
5
5
  GroupPolicy,
6
- } from "openclaw/plugin-sdk";
6
+ SecretInput,
7
+ } from "openclaw/plugin-sdk/nextcloud-talk";
7
8
 
8
9
  export type { DmPolicy, GroupPolicy };
9
10
 
@@ -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). */