@kodelyth/nextcloud-talk 2026.5.42 → 2026.6.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.
Files changed (58) hide show
  1. package/klaw.plugin.json +799 -2
  2. package/package.json +18 -6
  3. package/api.ts +0 -1
  4. package/channel-plugin-api.ts +0 -1
  5. package/contract-api.ts +0 -4
  6. package/doctor-contract-api.ts +0 -1
  7. package/index.ts +0 -20
  8. package/runtime-api.ts +0 -29
  9. package/secret-contract-api.ts +0 -5
  10. package/setup-entry.ts +0 -13
  11. package/src/accounts.test.ts +0 -31
  12. package/src/accounts.ts +0 -149
  13. package/src/api-credentials.ts +0 -31
  14. package/src/approval-auth.test.ts +0 -17
  15. package/src/approval-auth.ts +0 -27
  16. package/src/bot-preflight.test.ts +0 -135
  17. package/src/bot-preflight.ts +0 -183
  18. package/src/channel-api.ts +0 -5
  19. package/src/channel.adapters.ts +0 -52
  20. package/src/channel.core.test.ts +0 -75
  21. package/src/channel.lifecycle.test.ts +0 -91
  22. package/src/channel.status.test.ts +0 -28
  23. package/src/channel.ts +0 -225
  24. package/src/config-schema.ts +0 -79
  25. package/src/core.test.ts +0 -325
  26. package/src/doctor-contract.ts +0 -9
  27. package/src/doctor.test.ts +0 -87
  28. package/src/doctor.ts +0 -40
  29. package/src/gateway.ts +0 -109
  30. package/src/inbound.authz.test.ts +0 -146
  31. package/src/inbound.behavior.test.ts +0 -309
  32. package/src/inbound.ts +0 -392
  33. package/src/message-actions.test.ts +0 -270
  34. package/src/message-actions.ts +0 -82
  35. package/src/message-adapter.ts +0 -28
  36. package/src/monitor-runtime.ts +0 -138
  37. package/src/monitor.replay.test.ts +0 -276
  38. package/src/monitor.test-fixtures.ts +0 -30
  39. package/src/monitor.test-harness.ts +0 -59
  40. package/src/monitor.ts +0 -385
  41. package/src/normalize.ts +0 -44
  42. package/src/policy.ts +0 -111
  43. package/src/replay-guard.ts +0 -128
  44. package/src/room-info.test.ts +0 -160
  45. package/src/room-info.ts +0 -130
  46. package/src/runtime.ts +0 -9
  47. package/src/secret-contract.ts +0 -103
  48. package/src/secret-input.ts +0 -4
  49. package/src/send.cfg-threading.test.ts +0 -359
  50. package/src/send.runtime.ts +0 -8
  51. package/src/send.ts +0 -269
  52. package/src/session-route.ts +0 -40
  53. package/src/setup-core.ts +0 -250
  54. package/src/setup-surface.ts +0 -195
  55. package/src/setup.test.ts +0 -445
  56. package/src/signature.ts +0 -82
  57. package/src/types.ts +0 -195
  58. package/tsconfig.json +0 -16
@@ -1,146 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import type { PluginRuntime, RuntimeEnv } from "../runtime-api.js";
3
- import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
4
- import { handleNextcloudTalkInbound } from "./inbound.js";
5
- import { setNextcloudTalkRuntime } from "./runtime.js";
6
- import type { CoreConfig, NextcloudTalkInboundMessage } from "./types.js";
7
-
8
- function installInboundAuthzRuntime(params: {
9
- readAllowFromStore: () => Promise<string[]>;
10
- buildMentionRegexes: () => RegExp[];
11
- }) {
12
- setNextcloudTalkRuntime({
13
- channel: {
14
- pairing: {
15
- readAllowFromStore: params.readAllowFromStore,
16
- },
17
- commands: {
18
- shouldHandleTextCommands: () => false,
19
- },
20
- text: {
21
- hasControlCommand: () => false,
22
- },
23
- mentions: {
24
- buildMentionRegexes: params.buildMentionRegexes,
25
- matchesMentionPatterns: () => false,
26
- },
27
- },
28
- } as unknown as PluginRuntime);
29
- }
30
-
31
- function createTestRuntimeEnv(): RuntimeEnv {
32
- return {
33
- log: vi.fn(),
34
- error: vi.fn(),
35
- } as unknown as RuntimeEnv;
36
- }
37
-
38
- describe("nextcloud-talk inbound authz", () => {
39
- it("does not treat DM pairing-store entries as group allowlist entries", async () => {
40
- const readAllowFromStore = vi.fn(async () => ["attacker"]);
41
- const buildMentionRegexes = vi.fn(() => [/@kodelyth/i]);
42
-
43
- installInboundAuthzRuntime({ readAllowFromStore, buildMentionRegexes });
44
-
45
- const message: NextcloudTalkInboundMessage = {
46
- messageId: "m-1",
47
- roomToken: "room-1",
48
- roomName: "Room 1",
49
- senderId: "attacker",
50
- senderName: "Attacker",
51
- text: "hello",
52
- mediaType: "text/plain",
53
- timestamp: Date.now(),
54
- isGroupChat: true,
55
- };
56
-
57
- const account: ResolvedNextcloudTalkAccount = {
58
- accountId: "default",
59
- enabled: true,
60
- baseUrl: "",
61
- secret: "",
62
- secretSource: "none", // pragma: allowlist secret
63
- config: {
64
- dmPolicy: "pairing",
65
- allowFrom: [],
66
- groupPolicy: "allowlist",
67
- groupAllowFrom: [],
68
- },
69
- };
70
-
71
- const config: CoreConfig = {
72
- channels: {
73
- "nextcloud-talk": {
74
- dmPolicy: "pairing",
75
- allowFrom: [],
76
- groupPolicy: "allowlist",
77
- groupAllowFrom: [],
78
- },
79
- },
80
- };
81
-
82
- await handleNextcloudTalkInbound({
83
- message,
84
- account,
85
- config,
86
- runtime: createTestRuntimeEnv(),
87
- });
88
-
89
- expect(readAllowFromStore).not.toHaveBeenCalled();
90
- expect(buildMentionRegexes).not.toHaveBeenCalled();
91
- });
92
-
93
- it("matches group rooms by token instead of colliding room names", async () => {
94
- const readAllowFromStore = vi.fn(async () => []);
95
- const buildMentionRegexes = vi.fn(() => [/@kodelyth/i]);
96
-
97
- installInboundAuthzRuntime({ readAllowFromStore, buildMentionRegexes });
98
-
99
- const message: NextcloudTalkInboundMessage = {
100
- messageId: "m-2",
101
- roomToken: "room-attacker",
102
- roomName: "Room Trusted",
103
- senderId: "trusted-user",
104
- senderName: "Trusted User",
105
- text: "hello",
106
- mediaType: "text/plain",
107
- timestamp: Date.now(),
108
- isGroupChat: true,
109
- };
110
-
111
- const account: ResolvedNextcloudTalkAccount = {
112
- accountId: "default",
113
- enabled: true,
114
- baseUrl: "",
115
- secret: "",
116
- secretSource: "none",
117
- config: {
118
- dmPolicy: "pairing",
119
- allowFrom: [],
120
- groupPolicy: "allowlist",
121
- groupAllowFrom: ["trusted-user"],
122
- rooms: {
123
- "room-trusted": {
124
- enabled: true,
125
- },
126
- },
127
- },
128
- };
129
-
130
- await handleNextcloudTalkInbound({
131
- message,
132
- account,
133
- config: {
134
- channels: {
135
- "nextcloud-talk": {
136
- groupPolicy: "allowlist",
137
- groupAllowFrom: ["trusted-user"],
138
- },
139
- },
140
- },
141
- runtime: createTestRuntimeEnv(),
142
- });
143
-
144
- expect(buildMentionRegexes).not.toHaveBeenCalled();
145
- });
146
- });
@@ -1,309 +0,0 @@
1
- import { createPluginRuntimeMock } from "klaw/plugin-sdk/channel-test-helpers";
2
- import { beforeEach, describe, expect, it, vi } from "vitest";
3
- import type { PluginRuntime, RuntimeEnv } from "../runtime-api.js";
4
- import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
5
- import { handleNextcloudTalkInbound } from "./inbound.js";
6
- import { setNextcloudTalkRuntime } from "./runtime.js";
7
- import type { CoreConfig, NextcloudTalkInboundMessage } from "./types.js";
8
-
9
- const {
10
- createChannelPairingControllerMock,
11
- resolveAllowlistProviderRuntimeGroupPolicyMock,
12
- resolveDefaultGroupPolicyMock,
13
- warnMissingProviderGroupPolicyFallbackOnceMock,
14
- } = vi.hoisted(() => {
15
- return {
16
- createChannelPairingControllerMock: vi.fn(),
17
- resolveAllowlistProviderRuntimeGroupPolicyMock: vi.fn(),
18
- resolveDefaultGroupPolicyMock: vi.fn(),
19
- warnMissingProviderGroupPolicyFallbackOnceMock: vi.fn(),
20
- };
21
- });
22
-
23
- const sendMessageNextcloudTalkMock = vi.hoisted(() => vi.fn());
24
- const resolveNextcloudTalkRoomKindMock = vi.hoisted(() => vi.fn());
25
-
26
- vi.mock("../runtime-api.js", async () => {
27
- const actual = await vi.importActual<typeof import("../runtime-api.js")>("../runtime-api.js");
28
- return {
29
- ...actual,
30
- createChannelPairingController: createChannelPairingControllerMock,
31
- resolveAllowlistProviderRuntimeGroupPolicy: resolveAllowlistProviderRuntimeGroupPolicyMock,
32
- resolveDefaultGroupPolicy: resolveDefaultGroupPolicyMock,
33
- warnMissingProviderGroupPolicyFallbackOnce: warnMissingProviderGroupPolicyFallbackOnceMock,
34
- };
35
- });
36
-
37
- vi.mock("./send.js", () => ({
38
- sendMessageNextcloudTalk: sendMessageNextcloudTalkMock,
39
- }));
40
-
41
- vi.mock("./room-info.js", async () => {
42
- const actual = await vi.importActual<typeof import("./room-info.js")>("./room-info.js");
43
- return {
44
- ...actual,
45
- resolveNextcloudTalkRoomKind: resolveNextcloudTalkRoomKindMock,
46
- };
47
- });
48
-
49
- function installRuntime(params?: {
50
- buildMentionRegexes?: () => RegExp[];
51
- hasControlCommand?: (body: string) => boolean;
52
- matchesMentionPatterns?: (body: string, regexes: RegExp[]) => boolean;
53
- shouldHandleTextCommands?: () => boolean;
54
- }) {
55
- const runtime = {
56
- channel: {
57
- turn: {
58
- runAssembled: vi.fn(async () => undefined),
59
- },
60
- pairing: {
61
- readAllowFromStore: vi.fn(async () => []),
62
- upsertPairingRequest: vi.fn(async () => ({ code: "123456", created: true })),
63
- },
64
- commands: {
65
- shouldHandleTextCommands: params?.shouldHandleTextCommands ?? vi.fn(() => false),
66
- },
67
- text: {
68
- hasControlCommand: params?.hasControlCommand ?? vi.fn(() => false),
69
- },
70
- mentions: {
71
- buildMentionRegexes: params?.buildMentionRegexes ?? vi.fn(() => []),
72
- matchesMentionPatterns: params?.matchesMentionPatterns ?? vi.fn(() => false),
73
- },
74
- },
75
- };
76
- setNextcloudTalkRuntime(runtime as unknown as PluginRuntime);
77
- return runtime;
78
- }
79
-
80
- function createRuntimeEnv() {
81
- return {
82
- log: vi.fn(),
83
- error: vi.fn(),
84
- } as unknown as RuntimeEnv;
85
- }
86
-
87
- function requireFirstMockArg(mock: ReturnType<typeof vi.fn>, label: string): unknown {
88
- const [call] = mock.mock.calls;
89
- if (!call) {
90
- throw new Error(`expected ${label}`);
91
- }
92
- return call[0];
93
- }
94
-
95
- function requireFirstSendMessageCall(): [unknown, unknown, unknown] {
96
- const [call] = sendMessageNextcloudTalkMock.mock.calls;
97
- if (!call) {
98
- throw new Error("expected Nextcloud Talk send call");
99
- }
100
- return call as [unknown, unknown, unknown];
101
- }
102
-
103
- function createAccount(
104
- overrides?: Partial<ResolvedNextcloudTalkAccount>,
105
- ): ResolvedNextcloudTalkAccount {
106
- return {
107
- accountId: "default",
108
- enabled: true,
109
- baseUrl: "https://cloud.example.com",
110
- secret: "secret",
111
- secretSource: "config",
112
- config: {
113
- dmPolicy: "pairing",
114
- allowFrom: [],
115
- groupPolicy: "allowlist",
116
- groupAllowFrom: [],
117
- },
118
- ...overrides,
119
- };
120
- }
121
-
122
- function createMessage(
123
- overrides?: Partial<NextcloudTalkInboundMessage>,
124
- ): NextcloudTalkInboundMessage {
125
- return {
126
- messageId: "msg-1",
127
- roomToken: "room-1",
128
- roomName: "Room 1",
129
- senderId: "user-1",
130
- senderName: "Alice",
131
- text: "hello",
132
- mediaType: "text/plain",
133
- timestamp: Date.now(),
134
- isGroupChat: false,
135
- ...overrides,
136
- };
137
- }
138
-
139
- describe("nextcloud-talk inbound behavior", () => {
140
- beforeEach(() => {
141
- vi.clearAllMocks();
142
- installRuntime();
143
- resolveNextcloudTalkRoomKindMock.mockResolvedValue("direct");
144
- resolveDefaultGroupPolicyMock.mockReturnValue("allowlist");
145
- resolveAllowlistProviderRuntimeGroupPolicyMock.mockReturnValue({
146
- groupPolicy: "allowlist",
147
- providerMissingFallbackApplied: false,
148
- });
149
- warnMissingProviderGroupPolicyFallbackOnceMock.mockReturnValue(undefined);
150
- });
151
-
152
- it("issues a DM pairing challenge and sends the challenge text", async () => {
153
- const issueChallenge = vi.fn(
154
- async (params: { sendPairingReply: (text: string) => Promise<void> }) => {
155
- await params.sendPairingReply("Pair with code 123456");
156
- },
157
- );
158
- createChannelPairingControllerMock.mockReturnValue({
159
- readStoreForDmPolicy: vi.fn(),
160
- issueChallenge,
161
- });
162
- sendMessageNextcloudTalkMock.mockResolvedValue(undefined);
163
-
164
- const statusSink = vi.fn();
165
- await handleNextcloudTalkInbound({
166
- message: createMessage({ timestamp: 1_736_380_800_000 }),
167
- account: createAccount(),
168
- config: { channels: { "nextcloud-talk": {} } } as CoreConfig,
169
- runtime: createRuntimeEnv(),
170
- statusSink,
171
- });
172
-
173
- const challengeParams = requireFirstMockArg(
174
- issueChallenge,
175
- "Nextcloud Talk pairing challenge",
176
- ) as {
177
- meta?: { name?: string };
178
- senderId?: string;
179
- senderIdLine?: string;
180
- };
181
- expect(challengeParams.senderId).toBe("user-1");
182
- expect(challengeParams.senderIdLine).toBe("Your Nextcloud user id: user-1");
183
- expect(challengeParams.meta).toEqual({ name: "Alice" });
184
- expect(sendMessageNextcloudTalkMock).toHaveBeenCalledTimes(1);
185
- const sendArgs = requireFirstSendMessageCall();
186
- expect(sendArgs[0]).toBe("room-1");
187
- expect(sendArgs[1]).toBe("Pair with code 123456");
188
- expect(sendArgs[2]).toEqual({
189
- cfg: { channels: { "nextcloud-talk": {} } },
190
- accountId: "default",
191
- });
192
- expect(statusSink).toHaveBeenCalledWith({ lastInboundAt: 1_736_380_800_000 });
193
- const outboundStatus = statusSink.mock.calls
194
- .map(([status]) => status as { lastOutboundAt?: unknown })
195
- .find((status) => status.lastOutboundAt !== undefined);
196
- expect(typeof outboundStatus?.lastOutboundAt).toBe("number");
197
- expect(outboundStatus?.lastOutboundAt).toBeGreaterThanOrEqual(1_736_380_800_000);
198
- expect(sendMessageNextcloudTalkMock).toHaveBeenCalledTimes(1);
199
- });
200
-
201
- it("drops unmentioned group traffic before dispatch", async () => {
202
- installRuntime({
203
- buildMentionRegexes: vi.fn(() => [/@kodelyth/i]),
204
- matchesMentionPatterns: vi.fn(() => false),
205
- });
206
- createChannelPairingControllerMock.mockReturnValue({
207
- readStoreForDmPolicy: vi.fn(),
208
- issueChallenge: vi.fn(),
209
- });
210
- resolveNextcloudTalkRoomKindMock.mockResolvedValue("group");
211
- const runtime = createRuntimeEnv();
212
-
213
- await handleNextcloudTalkInbound({
214
- message: createMessage({
215
- roomToken: "room-group",
216
- roomName: "Ops",
217
- isGroupChat: true,
218
- }),
219
- account: createAccount({
220
- config: {
221
- dmPolicy: "pairing",
222
- allowFrom: [],
223
- groupPolicy: "allowlist",
224
- groupAllowFrom: ["user-1"],
225
- },
226
- }),
227
- config: { channels: { "nextcloud-talk": {} } } as CoreConfig,
228
- runtime,
229
- });
230
-
231
- expect(sendMessageNextcloudTalkMock).not.toHaveBeenCalled();
232
- expect(runtime.log).toHaveBeenCalledWith("nextcloud-talk: drop room room-group (no mention)");
233
- });
234
-
235
- it("blocks unauthorized group text control commands even when room sender access allows chat", async () => {
236
- const buildMentionRegexes = vi.fn(() => [/@kodelyth/i]);
237
- const coreRuntime = installRuntime({
238
- buildMentionRegexes,
239
- hasControlCommand: vi.fn(() => true),
240
- shouldHandleTextCommands: vi.fn(() => true),
241
- });
242
- createChannelPairingControllerMock.mockReturnValue({
243
- readStoreForDmPolicy: vi.fn(),
244
- issueChallenge: vi.fn(),
245
- });
246
- resolveNextcloudTalkRoomKindMock.mockResolvedValue("group");
247
- const runtime = createRuntimeEnv();
248
-
249
- await handleNextcloudTalkInbound({
250
- message: createMessage({
251
- roomToken: "room-group",
252
- roomName: "Ops",
253
- isGroupChat: true,
254
- text: "/klaw reload",
255
- }),
256
- account: createAccount({
257
- config: {
258
- dmPolicy: "pairing",
259
- allowFrom: [],
260
- groupPolicy: "allowlist",
261
- groupAllowFrom: [],
262
- rooms: {
263
- "room-group": {
264
- allowFrom: ["user-1"],
265
- requireMention: false,
266
- },
267
- },
268
- },
269
- }),
270
- config: { channels: { "nextcloud-talk": {} } } as CoreConfig,
271
- runtime,
272
- });
273
-
274
- expect(coreRuntime.channel.turn.runAssembled).not.toHaveBeenCalled();
275
- expect(buildMentionRegexes).not.toHaveBeenCalled();
276
- expect(runtime.log).toHaveBeenCalledWith(
277
- "nextcloud-talk: drop control command (unauthorized) target=user-1",
278
- );
279
- });
280
-
281
- it("passes the shared reply pipeline for dispatched replies", async () => {
282
- const coreRuntime = createPluginRuntimeMock();
283
- setNextcloudTalkRuntime(coreRuntime as unknown as PluginRuntime);
284
- createChannelPairingControllerMock.mockReturnValue({
285
- readStoreForDmPolicy: vi.fn(async () => []),
286
- issueChallenge: vi.fn(),
287
- });
288
-
289
- await handleNextcloudTalkInbound({
290
- message: createMessage(),
291
- account: createAccount({
292
- config: {
293
- dmPolicy: "allowlist",
294
- allowFrom: ["user-1"],
295
- groupPolicy: "allowlist",
296
- groupAllowFrom: [],
297
- },
298
- }),
299
- config: { channels: { "nextcloud-talk": {} } } as CoreConfig,
300
- runtime: createRuntimeEnv(),
301
- });
302
-
303
- const assembledRequest = requireFirstMockArg(
304
- coreRuntime.channel.turn.runAssembled as ReturnType<typeof vi.fn>,
305
- "Nextcloud Talk assembled request",
306
- ) as { replyPipeline?: unknown };
307
- expect(assembledRequest.replyPipeline).toEqual({});
308
- });
309
- });