@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/index.ts +2 -2
- package/package.json +4 -1
- package/src/accounts.ts +26 -59
- package/src/channel.startup.test.ts +9 -41
- package/src/channel.ts +82 -85
- package/src/config-schema.test.ts +36 -0
- package/src/config-schema.ts +4 -3
- package/src/inbound.authz.test.ts +2 -2
- package/src/inbound.ts +31 -50
- package/src/monitor.backend.test.ts +4 -23
- package/src/monitor.replay.test.ts +2 -28
- package/src/monitor.test-fixtures.ts +30 -0
- package/src/monitor.ts +1 -1
- package/src/onboarding.ts +126 -145
- package/src/policy.test.ts +106 -1
- package/src/policy.ts +27 -19
- package/src/replay-guard.ts +1 -1
- package/src/room-info.ts +40 -25
- package/src/runtime.ts +1 -1
- package/src/secret-input.ts +13 -0
- package/src/send.test.ts +104 -0
- package/src/send.ts +3 -2
- package/src/types.ts +4 -3
|
@@ -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
|
|
15
|
-
|
|
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
|
|
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", // pragma: allowlist 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/monitor.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
isRequestBodyLimitError,
|
|
7
7
|
readRequestBodyWithLimit,
|
|
8
8
|
requestBodyErrorToText,
|
|
9
|
-
} from "openclaw/plugin-sdk";
|
|
9
|
+
} from "openclaw/plugin-sdk/nextcloud-talk";
|
|
10
10
|
import { resolveNextcloudTalkAccount } from "./accounts.js";
|
|
11
11
|
import { handleNextcloudTalkInbound } from "./inbound.js";
|
|
12
12
|
import { createNextcloudTalkReplayGuard } from "./replay-guard.js";
|
package/src/onboarding.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
buildSingleChannelSecretPromptState,
|
|
3
3
|
formatDocsLink,
|
|
4
|
+
hasConfiguredSecretInput,
|
|
5
|
+
mapAllowFromEntries,
|
|
4
6
|
mergeAllowFromEntries,
|
|
5
|
-
|
|
7
|
+
promptSingleChannelSecretInput,
|
|
8
|
+
resolveAccountIdForConfigure,
|
|
6
9
|
DEFAULT_ACCOUNT_ID,
|
|
7
10
|
normalizeAccountId,
|
|
11
|
+
setTopLevelChannelDmPolicyWithAllowFrom,
|
|
12
|
+
type SecretInput,
|
|
8
13
|
type ChannelOnboardingAdapter,
|
|
9
14
|
type ChannelOnboardingDmPolicy,
|
|
10
15
|
type OpenClawConfig,
|
|
11
16
|
type WizardPrompter,
|
|
12
|
-
} from "openclaw/plugin-sdk";
|
|
17
|
+
} from "openclaw/plugin-sdk/nextcloud-talk";
|
|
13
18
|
import {
|
|
14
19
|
listNextcloudTalkAccountIds,
|
|
15
20
|
resolveDefaultNextcloudTalkAccountId,
|
|
@@ -20,24 +25,52 @@ import type { CoreConfig, DmPolicy } from "./types.js";
|
|
|
20
25
|
const channel = "nextcloud-talk" as const;
|
|
21
26
|
|
|
22
27
|
function setNextcloudTalkDmPolicy(cfg: CoreConfig, dmPolicy: DmPolicy): CoreConfig {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
dmPolicy === "open" ? (addWildcardAllowFrom(existingAllowFrom) as string[]) : existingAllowFrom;
|
|
27
|
-
|
|
28
|
-
const newNextcloudTalkConfig = {
|
|
29
|
-
...existingConfig,
|
|
28
|
+
return setTopLevelChannelDmPolicyWithAllowFrom({
|
|
29
|
+
cfg,
|
|
30
|
+
channel: "nextcloud-talk",
|
|
30
31
|
dmPolicy,
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
getAllowFrom: (inputCfg) =>
|
|
33
|
+
mapAllowFromEntries(inputCfg.channels?.["nextcloud-talk"]?.allowFrom),
|
|
34
|
+
}) as CoreConfig;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setNextcloudTalkAccountConfig(
|
|
38
|
+
cfg: CoreConfig,
|
|
39
|
+
accountId: string,
|
|
40
|
+
updates: Record<string, unknown>,
|
|
41
|
+
): 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
|
+
}
|
|
33
55
|
|
|
34
56
|
return {
|
|
35
57
|
...cfg,
|
|
36
58
|
channels: {
|
|
37
59
|
...cfg.channels,
|
|
38
|
-
"nextcloud-talk":
|
|
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
|
+
},
|
|
39
72
|
},
|
|
40
|
-
}
|
|
73
|
+
};
|
|
41
74
|
}
|
|
42
75
|
|
|
43
76
|
async function noteNextcloudTalkSecretHelp(prompter: WizardPrompter): Promise<void> {
|
|
@@ -102,40 +135,10 @@ async function promptNextcloudTalkAllowFrom(params: {
|
|
|
102
135
|
];
|
|
103
136
|
const unique = mergeAllowFromEntries(undefined, merged);
|
|
104
137
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
...cfg.channels,
|
|
110
|
-
"nextcloud-talk": {
|
|
111
|
-
...cfg.channels?.["nextcloud-talk"],
|
|
112
|
-
enabled: true,
|
|
113
|
-
dmPolicy: "allowlist",
|
|
114
|
-
allowFrom: unique,
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
...cfg,
|
|
122
|
-
channels: {
|
|
123
|
-
...cfg.channels,
|
|
124
|
-
"nextcloud-talk": {
|
|
125
|
-
...cfg.channels?.["nextcloud-talk"],
|
|
126
|
-
enabled: true,
|
|
127
|
-
accounts: {
|
|
128
|
-
...cfg.channels?.["nextcloud-talk"]?.accounts,
|
|
129
|
-
[accountId]: {
|
|
130
|
-
...cfg.channels?.["nextcloud-talk"]?.accounts?.[accountId],
|
|
131
|
-
enabled: cfg.channels?.["nextcloud-talk"]?.accounts?.[accountId]?.enabled ?? true,
|
|
132
|
-
dmPolicy: "allowlist",
|
|
133
|
-
allowFrom: unique,
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
};
|
|
138
|
+
return setNextcloudTalkAccountConfig(cfg, accountId, {
|
|
139
|
+
dmPolicy: "allowlist",
|
|
140
|
+
allowFrom: unique,
|
|
141
|
+
});
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
async function promptNextcloudTalkAllowFromForAccount(params: {
|
|
@@ -190,22 +193,16 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
190
193
|
shouldPromptAccountIds,
|
|
191
194
|
forceAllowFrom,
|
|
192
195
|
}) => {
|
|
193
|
-
const nextcloudTalkOverride = accountOverrides["nextcloud-talk"]?.trim();
|
|
194
196
|
const defaultAccountId = resolveDefaultNextcloudTalkAccountId(cfg as CoreConfig);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
currentId: accountId,
|
|
205
|
-
listAccountIds: listNextcloudTalkAccountIds as (cfg: OpenClawConfig) => string[],
|
|
206
|
-
defaultAccountId,
|
|
207
|
-
});
|
|
208
|
-
}
|
|
197
|
+
const accountId = await resolveAccountIdForConfigure({
|
|
198
|
+
cfg,
|
|
199
|
+
prompter,
|
|
200
|
+
label: "Nextcloud Talk",
|
|
201
|
+
accountOverride: accountOverrides["nextcloud-talk"],
|
|
202
|
+
shouldPromptAccountIds,
|
|
203
|
+
listAccountIds: listNextcloudTalkAccountIds as (cfg: OpenClawConfig) => string[],
|
|
204
|
+
defaultAccountId,
|
|
205
|
+
});
|
|
209
206
|
|
|
210
207
|
let next = cfg as CoreConfig;
|
|
211
208
|
const resolvedAccount = resolveNextcloudTalkAccount({
|
|
@@ -214,10 +211,16 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
214
211
|
});
|
|
215
212
|
const accountConfigured = Boolean(resolvedAccount.secret && resolvedAccount.baseUrl);
|
|
216
213
|
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
|
217
|
-
const canUseEnv = allowEnv && Boolean(process.env.NEXTCLOUD_TALK_BOT_SECRET?.trim());
|
|
218
214
|
const hasConfigSecret = Boolean(
|
|
219
|
-
resolvedAccount.config.botSecret ||
|
|
215
|
+
hasConfiguredSecretInput(resolvedAccount.config.botSecret) ||
|
|
216
|
+
resolvedAccount.config.botSecretFile,
|
|
220
217
|
);
|
|
218
|
+
const secretPromptState = buildSingleChannelSecretPromptState({
|
|
219
|
+
accountConfigured,
|
|
220
|
+
hasConfigToken: hasConfigSecret,
|
|
221
|
+
allowEnv,
|
|
222
|
+
envValue: process.env.NEXTCLOUD_TALK_BOT_SECRET,
|
|
223
|
+
});
|
|
221
224
|
|
|
222
225
|
let baseUrl = resolvedAccount.baseUrl;
|
|
223
226
|
if (!baseUrl) {
|
|
@@ -238,94 +241,72 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
238
241
|
).trim();
|
|
239
242
|
}
|
|
240
243
|
|
|
241
|
-
let secret:
|
|
244
|
+
let secret: SecretInput | null = null;
|
|
242
245
|
if (!accountConfigured) {
|
|
243
246
|
await noteNextcloudTalkSecretHelp(prompter);
|
|
244
247
|
}
|
|
245
248
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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,
|
|
249
|
+
const secretResult = await promptSingleChannelSecretInput({
|
|
250
|
+
cfg: next,
|
|
251
|
+
prompter,
|
|
252
|
+
providerHint: "nextcloud-talk",
|
|
253
|
+
credentialLabel: "bot secret",
|
|
254
|
+
accountConfigured: secretPromptState.accountConfigured,
|
|
255
|
+
canUseEnv: secretPromptState.canUseEnv,
|
|
256
|
+
hasConfigToken: secretPromptState.hasConfigToken,
|
|
257
|
+
envPrompt: "NEXTCLOUD_TALK_BOT_SECRET detected. Use env var?",
|
|
258
|
+
keepPrompt: "Nextcloud Talk bot secret already configured. Keep it?",
|
|
259
|
+
inputPrompt: "Enter Nextcloud Talk bot secret",
|
|
260
|
+
preferredEnvVar: "NEXTCLOUD_TALK_BOT_SECRET",
|
|
261
|
+
});
|
|
262
|
+
if (secretResult.action === "set") {
|
|
263
|
+
secret = secretResult.value;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (secretResult.action === "use-env" || secret || baseUrl !== resolvedAccount.baseUrl) {
|
|
267
|
+
next = setNextcloudTalkAccountConfig(next, accountId, {
|
|
268
|
+
baseUrl,
|
|
269
|
+
...(secret ? { botSecret: secret } : {}),
|
|
275
270
|
});
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const existingApiUser = resolvedAccount.config.apiUser?.trim();
|
|
274
|
+
const existingApiPasswordConfigured = Boolean(
|
|
275
|
+
hasConfiguredSecretInput(resolvedAccount.config.apiPassword) ||
|
|
276
|
+
resolvedAccount.config.apiPasswordFile,
|
|
277
|
+
);
|
|
278
|
+
const configureApiCredentials = await prompter.confirm({
|
|
279
|
+
message: "Configure optional Nextcloud Talk API credentials for room lookups?",
|
|
280
|
+
initialValue: Boolean(existingApiUser && existingApiPasswordConfigured),
|
|
281
|
+
});
|
|
282
|
+
if (configureApiCredentials) {
|
|
283
|
+
const apiUser = String(
|
|
286
284
|
await prompter.text({
|
|
287
|
-
message: "
|
|
288
|
-
|
|
285
|
+
message: "Nextcloud Talk API user",
|
|
286
|
+
initialValue: existingApiUser,
|
|
287
|
+
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
289
288
|
}),
|
|
290
289
|
).trim();
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
...next.channels,
|
|
312
|
-
"nextcloud-talk": {
|
|
313
|
-
...next.channels?.["nextcloud-talk"],
|
|
314
|
-
enabled: true,
|
|
315
|
-
accounts: {
|
|
316
|
-
...next.channels?.["nextcloud-talk"]?.accounts,
|
|
317
|
-
[accountId]: {
|
|
318
|
-
...next.channels?.["nextcloud-talk"]?.accounts?.[accountId],
|
|
319
|
-
enabled:
|
|
320
|
-
next.channels?.["nextcloud-talk"]?.accounts?.[accountId]?.enabled ?? true,
|
|
321
|
-
baseUrl,
|
|
322
|
-
...(secret ? { botSecret: secret } : {}),
|
|
323
|
-
},
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
},
|
|
327
|
-
};
|
|
328
|
-
}
|
|
290
|
+
const apiPasswordResult = await promptSingleChannelSecretInput({
|
|
291
|
+
cfg: next,
|
|
292
|
+
prompter,
|
|
293
|
+
providerHint: "nextcloud-talk-api",
|
|
294
|
+
credentialLabel: "API password",
|
|
295
|
+
...buildSingleChannelSecretPromptState({
|
|
296
|
+
accountConfigured: Boolean(existingApiUser && existingApiPasswordConfigured),
|
|
297
|
+
hasConfigToken: existingApiPasswordConfigured,
|
|
298
|
+
allowEnv: false,
|
|
299
|
+
}),
|
|
300
|
+
envPrompt: "",
|
|
301
|
+
keepPrompt: "Nextcloud Talk API password already configured. Keep it?",
|
|
302
|
+
inputPrompt: "Enter Nextcloud Talk API password",
|
|
303
|
+
preferredEnvVar: "NEXTCLOUD_TALK_API_PASSWORD",
|
|
304
|
+
});
|
|
305
|
+
const apiPassword = apiPasswordResult.action === "set" ? apiPasswordResult.value : undefined;
|
|
306
|
+
next = setNextcloudTalkAccountConfig(next, accountId, {
|
|
307
|
+
apiUser,
|
|
308
|
+
...(apiPassword ? { apiPassword } : {}),
|
|
309
|
+
});
|
|
329
310
|
}
|
|
330
311
|
|
|
331
312
|
if (forceAllowFrom) {
|
package/src/policy.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { resolveNextcloudTalkAllowlistMatch } from "./policy.js";
|
|
2
|
+
import { resolveNextcloudTalkAllowlistMatch, resolveNextcloudTalkGroupAllow } from "./policy.js";
|
|
3
3
|
|
|
4
4
|
describe("nextcloud-talk policy", () => {
|
|
5
5
|
describe("resolveNextcloudTalkAllowlistMatch", () => {
|
|
@@ -30,4 +30,109 @@ describe("nextcloud-talk policy", () => {
|
|
|
30
30
|
).toBe(false);
|
|
31
31
|
});
|
|
32
32
|
});
|
|
33
|
+
|
|
34
|
+
describe("resolveNextcloudTalkGroupAllow", () => {
|
|
35
|
+
it("blocks disabled policy", () => {
|
|
36
|
+
expect(
|
|
37
|
+
resolveNextcloudTalkGroupAllow({
|
|
38
|
+
groupPolicy: "disabled",
|
|
39
|
+
outerAllowFrom: ["owner"],
|
|
40
|
+
innerAllowFrom: ["room-user"],
|
|
41
|
+
senderId: "owner",
|
|
42
|
+
}),
|
|
43
|
+
).toEqual({
|
|
44
|
+
allowed: false,
|
|
45
|
+
outerMatch: { allowed: false },
|
|
46
|
+
innerMatch: { allowed: false },
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("allows open policy", () => {
|
|
51
|
+
expect(
|
|
52
|
+
resolveNextcloudTalkGroupAllow({
|
|
53
|
+
groupPolicy: "open",
|
|
54
|
+
outerAllowFrom: [],
|
|
55
|
+
innerAllowFrom: [],
|
|
56
|
+
senderId: "owner",
|
|
57
|
+
}),
|
|
58
|
+
).toEqual({
|
|
59
|
+
allowed: true,
|
|
60
|
+
outerMatch: { allowed: true },
|
|
61
|
+
innerMatch: { allowed: true },
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("blocks allowlist mode when both outer and inner allowlists are empty", () => {
|
|
66
|
+
expect(
|
|
67
|
+
resolveNextcloudTalkGroupAllow({
|
|
68
|
+
groupPolicy: "allowlist",
|
|
69
|
+
outerAllowFrom: [],
|
|
70
|
+
innerAllowFrom: [],
|
|
71
|
+
senderId: "owner",
|
|
72
|
+
}),
|
|
73
|
+
).toEqual({
|
|
74
|
+
allowed: false,
|
|
75
|
+
outerMatch: { allowed: false },
|
|
76
|
+
innerMatch: { allowed: false },
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("requires inner match when only room-specific allowlist is configured", () => {
|
|
81
|
+
expect(
|
|
82
|
+
resolveNextcloudTalkGroupAllow({
|
|
83
|
+
groupPolicy: "allowlist",
|
|
84
|
+
outerAllowFrom: [],
|
|
85
|
+
innerAllowFrom: ["room-user"],
|
|
86
|
+
senderId: "room-user",
|
|
87
|
+
}),
|
|
88
|
+
).toEqual({
|
|
89
|
+
allowed: true,
|
|
90
|
+
outerMatch: { allowed: false },
|
|
91
|
+
innerMatch: { allowed: true, matchKey: "room-user", matchSource: "id" },
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("blocks when outer allowlist misses even if inner allowlist matches", () => {
|
|
96
|
+
expect(
|
|
97
|
+
resolveNextcloudTalkGroupAllow({
|
|
98
|
+
groupPolicy: "allowlist",
|
|
99
|
+
outerAllowFrom: ["team-owner"],
|
|
100
|
+
innerAllowFrom: ["room-user"],
|
|
101
|
+
senderId: "room-user",
|
|
102
|
+
}),
|
|
103
|
+
).toEqual({
|
|
104
|
+
allowed: false,
|
|
105
|
+
outerMatch: { allowed: false },
|
|
106
|
+
innerMatch: { allowed: true, matchKey: "room-user", matchSource: "id" },
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("allows when both outer and inner allowlists match", () => {
|
|
111
|
+
expect(
|
|
112
|
+
resolveNextcloudTalkGroupAllow({
|
|
113
|
+
groupPolicy: "allowlist",
|
|
114
|
+
outerAllowFrom: ["team-owner"],
|
|
115
|
+
innerAllowFrom: ["room-user"],
|
|
116
|
+
senderId: "team-owner",
|
|
117
|
+
}),
|
|
118
|
+
).toEqual({
|
|
119
|
+
allowed: false,
|
|
120
|
+
outerMatch: { allowed: true, matchKey: "team-owner", matchSource: "id" },
|
|
121
|
+
innerMatch: { allowed: false },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(
|
|
125
|
+
resolveNextcloudTalkGroupAllow({
|
|
126
|
+
groupPolicy: "allowlist",
|
|
127
|
+
outerAllowFrom: ["shared-user"],
|
|
128
|
+
innerAllowFrom: ["shared-user"],
|
|
129
|
+
senderId: "shared-user",
|
|
130
|
+
}),
|
|
131
|
+
).toEqual({
|
|
132
|
+
allowed: true,
|
|
133
|
+
outerMatch: { allowed: true, matchKey: "shared-user", matchSource: "id" },
|
|
134
|
+
innerMatch: { allowed: true, matchKey: "shared-user", matchSource: "id" },
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
33
138
|
});
|
package/src/policy.ts
CHANGED
|
@@ -3,14 +3,15 @@ import type {
|
|
|
3
3
|
ChannelGroupContext,
|
|
4
4
|
GroupPolicy,
|
|
5
5
|
GroupToolPolicyConfig,
|
|
6
|
-
} from "openclaw/plugin-sdk";
|
|
6
|
+
} from "openclaw/plugin-sdk/nextcloud-talk";
|
|
7
7
|
import {
|
|
8
8
|
buildChannelKeyCandidates,
|
|
9
|
+
evaluateMatchedGroupAccessForPolicy,
|
|
9
10
|
normalizeChannelSlug,
|
|
10
11
|
resolveChannelEntryMatchWithFallback,
|
|
11
12
|
resolveMentionGatingWithBypass,
|
|
12
13
|
resolveNestedAllowlistDecision,
|
|
13
|
-
} from "openclaw/plugin-sdk";
|
|
14
|
+
} from "openclaw/plugin-sdk/nextcloud-talk";
|
|
14
15
|
import type { NextcloudTalkRoomConfig } from "./types.js";
|
|
15
16
|
|
|
16
17
|
function normalizeAllowEntry(raw: string): string {
|
|
@@ -128,19 +129,8 @@ export function resolveNextcloudTalkGroupAllow(params: {
|
|
|
128
129
|
innerAllowFrom: Array<string | number> | undefined;
|
|
129
130
|
senderId: string;
|
|
130
131
|
}): { allowed: boolean; outerMatch: AllowlistMatch; innerMatch: AllowlistMatch } {
|
|
131
|
-
if (params.groupPolicy === "disabled") {
|
|
132
|
-
return { allowed: false, outerMatch: { allowed: false }, innerMatch: { allowed: false } };
|
|
133
|
-
}
|
|
134
|
-
if (params.groupPolicy === "open") {
|
|
135
|
-
return { allowed: true, outerMatch: { allowed: true }, innerMatch: { allowed: true } };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
132
|
const outerAllow = normalizeNextcloudTalkAllowlist(params.outerAllowFrom);
|
|
139
133
|
const innerAllow = normalizeNextcloudTalkAllowlist(params.innerAllowFrom);
|
|
140
|
-
if (outerAllow.length === 0 && innerAllow.length === 0) {
|
|
141
|
-
return { allowed: false, outerMatch: { allowed: false }, innerMatch: { allowed: false } };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
134
|
const outerMatch = resolveNextcloudTalkAllowlistMatch({
|
|
145
135
|
allowFrom: params.outerAllowFrom,
|
|
146
136
|
senderId: params.senderId,
|
|
@@ -149,14 +139,32 @@ export function resolveNextcloudTalkGroupAllow(params: {
|
|
|
149
139
|
allowFrom: params.innerAllowFrom,
|
|
150
140
|
senderId: params.senderId,
|
|
151
141
|
});
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
142
|
+
const access = evaluateMatchedGroupAccessForPolicy({
|
|
143
|
+
groupPolicy: params.groupPolicy,
|
|
144
|
+
allowlistConfigured: outerAllow.length > 0 || innerAllow.length > 0,
|
|
145
|
+
allowlistMatched: resolveNestedAllowlistDecision({
|
|
146
|
+
outerConfigured: outerAllow.length > 0 || innerAllow.length > 0,
|
|
147
|
+
outerMatched: outerAllow.length > 0 ? outerMatch.allowed : true,
|
|
148
|
+
innerConfigured: innerAllow.length > 0,
|
|
149
|
+
innerMatched: innerMatch.allowed,
|
|
150
|
+
}),
|
|
157
151
|
});
|
|
158
152
|
|
|
159
|
-
return {
|
|
153
|
+
return {
|
|
154
|
+
allowed: access.allowed,
|
|
155
|
+
outerMatch:
|
|
156
|
+
params.groupPolicy === "open"
|
|
157
|
+
? { allowed: true }
|
|
158
|
+
: params.groupPolicy === "disabled"
|
|
159
|
+
? { allowed: false }
|
|
160
|
+
: outerMatch,
|
|
161
|
+
innerMatch:
|
|
162
|
+
params.groupPolicy === "open"
|
|
163
|
+
? { allowed: true }
|
|
164
|
+
: params.groupPolicy === "disabled"
|
|
165
|
+
? { allowed: false }
|
|
166
|
+
: innerMatch,
|
|
167
|
+
};
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
export function resolveNextcloudTalkMentionGate(params: {
|
package/src/replay-guard.ts
CHANGED