@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,79 +0,0 @@
1
- import {
2
- DmPolicySchema,
3
- GroupPolicySchema,
4
- MarkdownConfigSchema,
5
- ReplyRuntimeConfigSchemaShape,
6
- ToolPolicySchema,
7
- requireOpenAllowFrom,
8
- } from "klaw/plugin-sdk/channel-config-schema";
9
- import { requireChannelOpenAllowFrom } from "klaw/plugin-sdk/extension-shared";
10
- import { z } from "zod";
11
- import { buildSecretInputSchema } from "./secret-input.js";
12
-
13
- const NextcloudTalkRoomSchema = z
14
- .object({
15
- requireMention: z.boolean().optional(),
16
- tools: ToolPolicySchema,
17
- skills: z.array(z.string()).optional(),
18
- enabled: z.boolean().optional(),
19
- allowFrom: z.array(z.string()).optional(),
20
- systemPrompt: z.string().optional(),
21
- })
22
- .strict();
23
-
24
- const NextcloudTalkNetworkSchema = z
25
- .object({
26
- /** Dangerous opt-in for self-hosted Nextcloud Talk on trusted private/internal hosts. */
27
- dangerouslyAllowPrivateNetwork: z.boolean().optional(),
28
- })
29
- .strict()
30
- .optional();
31
-
32
- const NextcloudTalkAccountSchemaBase = z
33
- .object({
34
- name: z.string().optional(),
35
- enabled: z.boolean().optional(),
36
- markdown: MarkdownConfigSchema,
37
- baseUrl: z.string().optional(),
38
- botSecret: buildSecretInputSchema().optional(),
39
- botSecretFile: z.string().optional(),
40
- apiUser: z.string().optional(),
41
- apiPassword: buildSecretInputSchema().optional(),
42
- apiPasswordFile: z.string().optional(),
43
- dmPolicy: DmPolicySchema.optional().default("pairing"),
44
- webhookPort: z.number().int().positive().optional(),
45
- webhookHost: z.string().optional(),
46
- webhookPath: z.string().optional(),
47
- webhookPublicUrl: z.string().optional(),
48
- allowFrom: z.array(z.string()).optional(),
49
- groupAllowFrom: z.array(z.string()).optional(),
50
- groupPolicy: GroupPolicySchema.optional().default("allowlist"),
51
- rooms: z.record(z.string(), NextcloudTalkRoomSchema.optional()).optional(),
52
- /** Network policy overrides for self-hosted Nextcloud Talk on trusted private/internal hosts. */
53
- network: NextcloudTalkNetworkSchema,
54
- ...ReplyRuntimeConfigSchemaShape,
55
- })
56
- .strict();
57
-
58
- const NextcloudTalkAccountSchema = NextcloudTalkAccountSchemaBase.superRefine((value, ctx) => {
59
- requireChannelOpenAllowFrom({
60
- channel: "nextcloud-talk",
61
- policy: value.dmPolicy,
62
- allowFrom: value.allowFrom,
63
- ctx,
64
- requireOpenAllowFrom,
65
- });
66
- });
67
-
68
- export const NextcloudTalkConfigSchema = NextcloudTalkAccountSchemaBase.extend({
69
- accounts: z.record(z.string(), NextcloudTalkAccountSchema.optional()).optional(),
70
- defaultAccount: z.string().optional(),
71
- }).superRefine((value, ctx) => {
72
- requireChannelOpenAllowFrom({
73
- channel: "nextcloud-talk",
74
- policy: value.dmPolicy,
75
- allowFrom: value.allowFrom,
76
- ctx,
77
- requireOpenAllowFrom,
78
- });
79
- });
package/src/core.test.ts DELETED
@@ -1,325 +0,0 @@
1
- import { mkdtemp, rm } from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { afterEach, describe, expect, it, vi } from "vitest";
5
- import {
6
- looksLikeNextcloudTalkTargetId,
7
- normalizeNextcloudTalkMessagingTarget,
8
- stripNextcloudTalkTargetPrefix,
9
- } from "./normalize.js";
10
- import { resolveNextcloudTalkAllowlistMatch } from "./policy.js";
11
- import { createNextcloudTalkReplayGuard } from "./replay-guard.js";
12
- import { resolveNextcloudTalkOutboundSessionRoute } from "./session-route.js";
13
- import {
14
- extractNextcloudTalkHeaders,
15
- generateNextcloudTalkSignature,
16
- verifyNextcloudTalkSignature,
17
- } from "./signature.js";
18
-
19
- const tempDirs: string[] = [];
20
-
21
- afterEach(async () => {
22
- while (tempDirs.length > 0) {
23
- const dir = tempDirs.pop();
24
- if (dir) {
25
- await rm(dir, { recursive: true, force: true });
26
- }
27
- }
28
- });
29
-
30
- async function makeTempDir(): Promise<string> {
31
- const dir = await mkdtemp(path.join(os.tmpdir(), "nextcloud-talk-replay-"));
32
- tempDirs.push(dir);
33
- return dir;
34
- }
35
-
36
- function requireFirstTimingSafeEqualCall(mock: ReturnType<typeof vi.fn>): [unknown, unknown] {
37
- const [call] = mock.mock.calls;
38
- if (!call) {
39
- throw new Error("expected timingSafeEqual call");
40
- }
41
- return call as [unknown, unknown];
42
- }
43
-
44
- describe("nextcloud talk core", () => {
45
- it("builds an outbound session route for normalized room targets", () => {
46
- const route = resolveNextcloudTalkOutboundSessionRoute({
47
- cfg: {},
48
- agentId: "main",
49
- accountId: "acct-1",
50
- target: "nextcloud-talk:room-123",
51
- });
52
-
53
- expect(route).toEqual({
54
- sessionKey: "agent:main:nextcloud-talk:group:room-123",
55
- baseSessionKey: "agent:main:nextcloud-talk:group:room-123",
56
- peer: {
57
- kind: "group",
58
- id: "room-123",
59
- },
60
- chatType: "group",
61
- from: "nextcloud-talk:room:room-123",
62
- to: "nextcloud-talk:room-123",
63
- });
64
- });
65
-
66
- it("returns null when the target cannot be normalized to a room id", () => {
67
- expect(
68
- resolveNextcloudTalkOutboundSessionRoute({
69
- cfg: {},
70
- agentId: "main",
71
- accountId: "acct-1",
72
- target: "",
73
- }),
74
- ).toBeNull();
75
- });
76
-
77
- it("normalizes and recognizes supported room target formats", () => {
78
- expect(stripNextcloudTalkTargetPrefix(" room:abc123 ")).toBe("abc123");
79
- expect(stripNextcloudTalkTargetPrefix("nextcloud-talk:room:AbC123")).toBe("AbC123");
80
- expect(stripNextcloudTalkTargetPrefix("nc-talk:room:ops")).toBe("ops");
81
- expect(stripNextcloudTalkTargetPrefix("nc:room:ops")).toBe("ops");
82
- expect(stripNextcloudTalkTargetPrefix("room: ")).toBeUndefined();
83
-
84
- expect(normalizeNextcloudTalkMessagingTarget("room:AbC123")).toBe("nextcloud-talk:abc123");
85
- expect(normalizeNextcloudTalkMessagingTarget("nc-talk:room:Ops")).toBe("nextcloud-talk:ops");
86
-
87
- expect(looksLikeNextcloudTalkTargetId("nextcloud-talk:room:abc12345")).toBe(true);
88
- expect(looksLikeNextcloudTalkTargetId("nc:opsroom1")).toBe(true);
89
- expect(looksLikeNextcloudTalkTargetId("abc12345")).toBe(true);
90
- expect(looksLikeNextcloudTalkTargetId("")).toBe(false);
91
- });
92
-
93
- it("verifies generated signatures and extracts normalized headers", () => {
94
- const body = JSON.stringify({ hello: "world" });
95
- const generated = generateNextcloudTalkSignature({
96
- body,
97
- secret: "secret-123",
98
- });
99
-
100
- expect(generated.random).toMatch(/^[0-9a-f]{64}$/);
101
- expect(generated.signature).toMatch(/^[0-9a-f]{64}$/);
102
- expect(
103
- verifyNextcloudTalkSignature({
104
- signature: generated.signature,
105
- random: generated.random,
106
- body,
107
- secret: "secret-123",
108
- }),
109
- ).toBe(true);
110
- expect(
111
- verifyNextcloudTalkSignature({
112
- signature: "",
113
- random: "abc",
114
- body: "body",
115
- secret: "secret",
116
- }),
117
- ).toBe(false);
118
- expect(
119
- verifyNextcloudTalkSignature({
120
- signature: "deadbeef",
121
- random: "abc",
122
- body: "body",
123
- secret: "secret",
124
- }),
125
- ).toBe(false);
126
-
127
- expect(
128
- extractNextcloudTalkHeaders({
129
- "x-nextcloud-talk-signature": "sig",
130
- "x-nextcloud-talk-random": "rand",
131
- "x-nextcloud-talk-backend": "backend",
132
- }),
133
- ).toEqual({
134
- signature: "sig",
135
- random: "rand",
136
- backend: "backend",
137
- });
138
- expect(
139
- extractNextcloudTalkHeaders({
140
- "X-Nextcloud-Talk-Signature": "sig",
141
- }),
142
- ).toBeNull();
143
- });
144
-
145
- it("rejects tampered bodies, wrong secrets, and tampered signatures", () => {
146
- const body = JSON.stringify({ hello: "world" });
147
- const generated = generateNextcloudTalkSignature({
148
- body,
149
- secret: "secret-123",
150
- });
151
-
152
- expect(
153
- verifyNextcloudTalkSignature({
154
- signature: generated.signature,
155
- random: generated.random,
156
- body: JSON.stringify({ hello: "tampered" }),
157
- secret: "secret-123",
158
- }),
159
- ).toBe(false);
160
- expect(
161
- verifyNextcloudTalkSignature({
162
- signature: generated.signature,
163
- random: generated.random,
164
- body,
165
- secret: "wrong-secret",
166
- }),
167
- ).toBe(false);
168
- expect(
169
- verifyNextcloudTalkSignature({
170
- signature: "a".repeat(generated.signature.length),
171
- random: generated.random,
172
- body,
173
- secret: "secret-123",
174
- }),
175
- ).toBe(false);
176
- });
177
-
178
- it("takes the first value from array-backed headers", () => {
179
- expect(
180
- extractNextcloudTalkHeaders({
181
- "x-nextcloud-talk-signature": ["sig1", "sig2"],
182
- "x-nextcloud-talk-random": ["rand1", "rand2"],
183
- "x-nextcloud-talk-backend": ["backend1", "backend2"],
184
- }),
185
- ).toEqual({
186
- signature: "sig1",
187
- random: "rand1",
188
- backend: "backend1",
189
- });
190
- });
191
-
192
- it("still runs timingSafeEqual when the supplied signature length mismatches", async () => {
193
- const timingSafeEqualMock = vi.fn();
194
-
195
- vi.resetModules();
196
- vi.doMock("node:crypto", async (importOriginal) => {
197
- const actual = await importOriginal<typeof import("node:crypto")>();
198
- return {
199
- ...actual,
200
- timingSafeEqual: vi.fn((left: NodeJS.ArrayBufferView, right: NodeJS.ArrayBufferView) => {
201
- timingSafeEqualMock(left, right);
202
- return actual.timingSafeEqual(left, right);
203
- }),
204
- };
205
- });
206
-
207
- try {
208
- const { generateNextcloudTalkSignature, verifyNextcloudTalkSignature } =
209
- await import("./signature.js");
210
- const body = JSON.stringify({ hello: "world" });
211
- const generated = generateNextcloudTalkSignature({
212
- body,
213
- secret: "secret-123",
214
- });
215
- const shortSignature = generated.signature.slice(0, 12);
216
-
217
- expect(
218
- verifyNextcloudTalkSignature({
219
- signature: shortSignature,
220
- random: generated.random,
221
- body,
222
- secret: "secret-123",
223
- }),
224
- ).toBe(false);
225
-
226
- expect(timingSafeEqualMock).toHaveBeenCalledOnce();
227
- const [leftBuffer, rightBuffer] = requireFirstTimingSafeEqualCall(timingSafeEqualMock);
228
- expect(Buffer.isBuffer(leftBuffer)).toBe(true);
229
- expect(Buffer.isBuffer(rightBuffer)).toBe(true);
230
- if (!Buffer.isBuffer(leftBuffer) || !Buffer.isBuffer(rightBuffer)) {
231
- throw new TypeError("Expected timingSafeEqual to receive Buffer arguments");
232
- }
233
- expect(leftBuffer).toHaveLength(rightBuffer.length);
234
- } finally {
235
- vi.doUnmock("node:crypto");
236
- vi.resetModules();
237
- }
238
- });
239
-
240
- it("persists replay decisions across guard instances and scopes account namespaces", async () => {
241
- const stateDir = await makeTempDir();
242
-
243
- const firstGuard = createNextcloudTalkReplayGuard({ stateDir });
244
- const firstAttempt = await firstGuard.shouldProcessMessage({
245
- accountId: "account-a",
246
- roomToken: "room-1",
247
- messageId: "msg-1",
248
- });
249
- const replayAttempt = await firstGuard.shouldProcessMessage({
250
- accountId: "account-a",
251
- roomToken: "room-1",
252
- messageId: "msg-1",
253
- });
254
-
255
- const secondGuard = createNextcloudTalkReplayGuard({ stateDir });
256
- const restartReplayAttempt = await secondGuard.shouldProcessMessage({
257
- accountId: "account-a",
258
- roomToken: "room-1",
259
- messageId: "msg-1",
260
- });
261
- const otherAccountFirstAttempt = await secondGuard.shouldProcessMessage({
262
- accountId: "account-b",
263
- roomToken: "room-1",
264
- messageId: "msg-1",
265
- });
266
-
267
- expect(firstAttempt).toBe(true);
268
- expect(replayAttempt).toBe(false);
269
- expect(restartReplayAttempt).toBe(false);
270
- expect(otherAccountFirstAttempt).toBe(true);
271
- });
272
-
273
- it("releases in-flight replay claims when processing fails", async () => {
274
- const guard = createNextcloudTalkReplayGuard({});
275
-
276
- const firstClaim = await guard.claimMessage({
277
- accountId: "account-a",
278
- roomToken: "room-1",
279
- messageId: "msg-claim",
280
- });
281
- const secondClaim = await guard.claimMessage({
282
- accountId: "account-a",
283
- roomToken: "room-1",
284
- messageId: "msg-claim",
285
- });
286
-
287
- expect(firstClaim).toBe("claimed");
288
- expect(secondClaim).toBe("inflight");
289
-
290
- guard.releaseMessage({
291
- accountId: "account-a",
292
- roomToken: "room-1",
293
- messageId: "msg-claim",
294
- error: new Error("transient"),
295
- });
296
-
297
- const retryClaim = await guard.claimMessage({
298
- accountId: "account-a",
299
- roomToken: "room-1",
300
- messageId: "msg-claim",
301
- });
302
- expect(retryClaim).toBe("claimed");
303
- });
304
-
305
- it("resolves allowlist matches", () => {
306
- expect(
307
- resolveNextcloudTalkAllowlistMatch({
308
- allowFrom: ["*"],
309
- senderId: "user-id",
310
- }).allowed,
311
- ).toBe(true);
312
- expect(
313
- resolveNextcloudTalkAllowlistMatch({
314
- allowFrom: ["nc:User-Id"],
315
- senderId: "user-id",
316
- }),
317
- ).toEqual({ allowed: true, matchKey: "user-id", matchSource: "id" });
318
- expect(
319
- resolveNextcloudTalkAllowlistMatch({
320
- allowFrom: ["allowed"],
321
- senderId: "other",
322
- }).allowed,
323
- ).toBe(false);
324
- });
325
- });
@@ -1,9 +0,0 @@
1
- import { createLegacyPrivateNetworkDoctorContract } from "klaw/plugin-sdk/ssrf-runtime";
2
-
3
- const contract = createLegacyPrivateNetworkDoctorContract({
4
- channelKey: "nextcloud-talk",
5
- });
6
-
7
- export const legacyConfigRules = contract.legacyConfigRules;
8
-
9
- export const normalizeCompatibilityConfig = contract.normalizeCompatibilityConfig;
@@ -1,87 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
-
3
- const hoisted = vi.hoisted(() => ({
4
- probeNextcloudTalkBotResponseFeature: vi.fn(),
5
- }));
6
-
7
- vi.mock("./bot-preflight.js", () => ({
8
- probeNextcloudTalkBotResponseFeature: hoisted.probeNextcloudTalkBotResponseFeature,
9
- }));
10
-
11
- const { nextcloudTalkDoctor } = await import("./doctor.js");
12
-
13
- function getNextcloudTalkCompatibilityNormalizer(): NonNullable<
14
- typeof nextcloudTalkDoctor.normalizeCompatibilityConfig
15
- > {
16
- const normalize = nextcloudTalkDoctor.normalizeCompatibilityConfig;
17
- if (!normalize) {
18
- throw new Error("Expected nextcloud-talk doctor to expose normalizeCompatibilityConfig");
19
- }
20
- return normalize;
21
- }
22
-
23
- describe("nextcloud-talk doctor", () => {
24
- beforeEach(() => {
25
- hoisted.probeNextcloudTalkBotResponseFeature.mockReset();
26
- });
27
-
28
- it("normalizes legacy private-network aliases", () => {
29
- const normalize = getNextcloudTalkCompatibilityNormalizer();
30
-
31
- const result = normalize({
32
- cfg: {
33
- channels: {
34
- "nextcloud-talk": {
35
- allowPrivateNetwork: true,
36
- accounts: {
37
- work: {
38
- allowPrivateNetwork: false,
39
- },
40
- },
41
- },
42
- },
43
- } as never,
44
- });
45
-
46
- expect(result.config.channels?.["nextcloud-talk"]?.network).toEqual({
47
- dangerouslyAllowPrivateNetwork: true,
48
- });
49
- expect(
50
- (
51
- result.config.channels?.["nextcloud-talk"]?.accounts?.work as
52
- | { network?: Record<string, unknown> }
53
- | undefined
54
- )?.network,
55
- ).toEqual({
56
- dangerouslyAllowPrivateNetwork: false,
57
- });
58
- });
59
-
60
- it("warns when the configured bot is missing the response feature", async () => {
61
- hoisted.probeNextcloudTalkBotResponseFeature.mockResolvedValueOnce({
62
- ok: false,
63
- code: "missing_response_feature",
64
- message:
65
- 'Nextcloud Talk bot "Klaw" (1) is missing the response feature (features=9); outbound replies will fail.',
66
- });
67
-
68
- await expect(
69
- nextcloudTalkDoctor.collectPreviewWarnings?.({
70
- cfg: {
71
- channels: {
72
- "nextcloud-talk": {
73
- baseUrl: "https://cloud.example.com",
74
- botSecret: "secret",
75
- apiUser: "admin",
76
- apiPassword: "app-password",
77
- webhookPublicUrl: "https://gateway.example.com/nextcloud-talk-webhook",
78
- },
79
- },
80
- } as never,
81
- doctorFixCommand: "klaw doctor --fix",
82
- }),
83
- ).resolves.toEqual([
84
- '- channels.nextcloud-talk.default: Nextcloud Talk bot "Klaw" (1) is missing the response feature (features=9); outbound replies will fail.',
85
- ]);
86
- });
87
- });
package/src/doctor.ts DELETED
@@ -1,40 +0,0 @@
1
- import type { ChannelDoctorAdapter } from "klaw/plugin-sdk/channel-contract";
2
- import { listNextcloudTalkAccountIds, resolveNextcloudTalkAccount } from "./accounts.js";
3
- import { probeNextcloudTalkBotResponseFeature } from "./bot-preflight.js";
4
- import {
5
- legacyConfigRules as NEXTCLOUD_TALK_LEGACY_CONFIG_RULES,
6
- normalizeCompatibilityConfig as normalizeNextcloudTalkCompatibilityConfig,
7
- } from "./doctor-contract.js";
8
- import type { CoreConfig } from "./types.js";
9
-
10
- async function collectNextcloudTalkBotResponseWarnings(params: {
11
- cfg: CoreConfig;
12
- }): Promise<string[]> {
13
- const warnings: string[] = [];
14
- for (const accountId of listNextcloudTalkAccountIds(params.cfg)) {
15
- const account = resolveNextcloudTalkAccount({ cfg: params.cfg, accountId });
16
- if (!account.enabled || !account.secret || !account.baseUrl) {
17
- continue;
18
- }
19
- const result = await probeNextcloudTalkBotResponseFeature({
20
- account,
21
- timeoutMs: 5_000,
22
- });
23
- if (
24
- result.code === "missing_response_feature" ||
25
- result.code === "bot_not_found" ||
26
- result.code === "api_error" ||
27
- result.code === "request_failed"
28
- ) {
29
- warnings.push(`- channels.nextcloud-talk.${account.accountId}: ${result.message}`);
30
- }
31
- }
32
- return warnings;
33
- }
34
-
35
- export const nextcloudTalkDoctor: ChannelDoctorAdapter = {
36
- legacyConfigRules: NEXTCLOUD_TALK_LEGACY_CONFIG_RULES,
37
- normalizeCompatibilityConfig: normalizeNextcloudTalkCompatibilityConfig,
38
- collectPreviewWarnings: async ({ cfg }) =>
39
- await collectNextcloudTalkBotResponseWarnings({ cfg: cfg as CoreConfig }),
40
- };
package/src/gateway.ts DELETED
@@ -1,109 +0,0 @@
1
- import { createAccountStatusSink } from "klaw/plugin-sdk/channel-lifecycle";
2
- import { runStoppablePassiveMonitor } from "klaw/plugin-sdk/extension-shared";
3
- import { resolveNextcloudTalkAccount, type ResolvedNextcloudTalkAccount } from "./accounts.js";
4
- import {
5
- clearAccountEntryFields,
6
- DEFAULT_ACCOUNT_ID,
7
- type ChannelPlugin,
8
- type KlawConfig,
9
- } from "./channel-api.js";
10
- import { monitorNextcloudTalkProvider } from "./monitor-runtime.js";
11
- import { getNextcloudTalkRuntime } from "./runtime.js";
12
- import type { CoreConfig } from "./types.js";
13
-
14
- export const nextcloudTalkGatewayAdapter: NonNullable<
15
- ChannelPlugin<ResolvedNextcloudTalkAccount>["gateway"]
16
- > = {
17
- startAccount: async (ctx) => {
18
- const account = ctx.account;
19
- if (!account.secret || !account.baseUrl) {
20
- throw new Error(
21
- `Nextcloud Talk not configured for account "${account.accountId}" (missing secret or baseUrl)`,
22
- );
23
- }
24
-
25
- ctx.log?.info(`[${account.accountId}] starting Nextcloud Talk webhook server`);
26
-
27
- const statusSink = createAccountStatusSink({
28
- accountId: ctx.accountId,
29
- setStatus: ctx.setStatus,
30
- });
31
-
32
- await runStoppablePassiveMonitor({
33
- abortSignal: ctx.abortSignal,
34
- start: async () =>
35
- await monitorNextcloudTalkProvider({
36
- accountId: account.accountId,
37
- config: ctx.cfg as CoreConfig,
38
- runtime: ctx.runtime,
39
- abortSignal: ctx.abortSignal,
40
- statusSink,
41
- }),
42
- });
43
- },
44
- logoutAccount: async ({ accountId, cfg }) => {
45
- const nextCfg = { ...cfg } as KlawConfig;
46
- const nextSection = cfg.channels?.["nextcloud-talk"]
47
- ? { ...cfg.channels["nextcloud-talk"] }
48
- : undefined;
49
- let cleared = false;
50
- let changed = false;
51
-
52
- if (nextSection) {
53
- if (accountId === DEFAULT_ACCOUNT_ID && nextSection.botSecret) {
54
- delete nextSection.botSecret;
55
- cleared = true;
56
- changed = true;
57
- }
58
- const accountCleanup = clearAccountEntryFields({
59
- accounts: nextSection.accounts as Record<string, object> | undefined,
60
- accountId,
61
- fields: ["botSecret"],
62
- });
63
- if (accountCleanup.changed) {
64
- changed = true;
65
- if (accountCleanup.cleared) {
66
- cleared = true;
67
- }
68
- if (accountCleanup.nextAccounts) {
69
- nextSection.accounts = accountCleanup.nextAccounts as Record<string, unknown>;
70
- } else {
71
- delete nextSection.accounts;
72
- }
73
- }
74
- }
75
-
76
- if (changed) {
77
- if (nextSection && Object.keys(nextSection).length > 0) {
78
- nextCfg.channels = { ...nextCfg.channels, "nextcloud-talk": nextSection };
79
- } else {
80
- const nextChannels = { ...nextCfg.channels } as Record<string, unknown>;
81
- delete nextChannels["nextcloud-talk"];
82
- if (Object.keys(nextChannels).length > 0) {
83
- nextCfg.channels = nextChannels as KlawConfig["channels"];
84
- } else {
85
- delete nextCfg.channels;
86
- }
87
- }
88
- }
89
-
90
- const resolved = resolveNextcloudTalkAccount({
91
- cfg: changed ? (nextCfg as CoreConfig) : (cfg as CoreConfig),
92
- accountId,
93
- });
94
- const loggedOut = resolved.secretSource === "none";
95
-
96
- if (changed) {
97
- await getNextcloudTalkRuntime().config.replaceConfigFile({
98
- nextConfig: nextCfg,
99
- afterWrite: { mode: "auto" },
100
- });
101
- }
102
-
103
- return {
104
- cleared,
105
- envSecret: Boolean(process.env.NEXTCLOUD_TALK_BOT_SECRET?.trim()),
106
- loggedOut,
107
- };
108
- },
109
- };