@openclaw/nextcloud-talk 2026.5.2 → 2026.5.3-beta.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 (61) hide show
  1. package/dist/api.js +2 -0
  2. package/dist/channel-BVVRsVr5.js +1788 -0
  3. package/dist/channel-plugin-api.js +2 -0
  4. package/dist/contract-api.js +2 -0
  5. package/dist/doctor-contract-CYlB-4Bf.js +7 -0
  6. package/dist/doctor-contract-api.js +2 -0
  7. package/dist/index.js +22 -0
  8. package/dist/runtime-api-BcCzeRN9.js +15 -0
  9. package/dist/runtime-api.js +2 -0
  10. package/dist/secret-contract-api.js +2 -0
  11. package/dist/secret-contract-bczDw2-2.js +86 -0
  12. package/dist/setup-entry.js +15 -0
  13. package/package.json +14 -6
  14. package/api.ts +0 -1
  15. package/channel-plugin-api.ts +0 -1
  16. package/contract-api.ts +0 -4
  17. package/doctor-contract-api.ts +0 -1
  18. package/index.ts +0 -20
  19. package/runtime-api.ts +0 -33
  20. package/secret-contract-api.ts +0 -5
  21. package/setup-entry.ts +0 -13
  22. package/src/accounts.ts +0 -139
  23. package/src/approval-auth.test.ts +0 -17
  24. package/src/approval-auth.ts +0 -27
  25. package/src/channel-api.ts +0 -5
  26. package/src/channel.adapters.ts +0 -52
  27. package/src/channel.core.test.ts +0 -75
  28. package/src/channel.lifecycle.test.ts +0 -81
  29. package/src/channel.ts +0 -195
  30. package/src/config-schema.ts +0 -79
  31. package/src/core.test.ts +0 -397
  32. package/src/doctor-contract.ts +0 -9
  33. package/src/doctor.test.ts +0 -40
  34. package/src/doctor.ts +0 -10
  35. package/src/gateway.ts +0 -109
  36. package/src/inbound.authz.test.ts +0 -149
  37. package/src/inbound.behavior.test.ts +0 -202
  38. package/src/inbound.ts +0 -320
  39. package/src/monitor-runtime.ts +0 -138
  40. package/src/monitor.replay.test.ts +0 -279
  41. package/src/monitor.test-fixtures.ts +0 -30
  42. package/src/monitor.test-harness.ts +0 -59
  43. package/src/monitor.ts +0 -385
  44. package/src/normalize.ts +0 -44
  45. package/src/policy.ts +0 -180
  46. package/src/replay-guard.ts +0 -128
  47. package/src/room-info.test.ts +0 -116
  48. package/src/room-info.ts +0 -148
  49. package/src/runtime.ts +0 -9
  50. package/src/secret-contract.ts +0 -103
  51. package/src/secret-input.ts +0 -4
  52. package/src/send.cfg-threading.test.ts +0 -153
  53. package/src/send.runtime.ts +0 -8
  54. package/src/send.ts +0 -236
  55. package/src/session-route.ts +0 -40
  56. package/src/setup-core.ts +0 -248
  57. package/src/setup-surface.ts +0 -190
  58. package/src/setup.test.ts +0 -422
  59. package/src/signature.ts +0 -82
  60. package/src/types.ts +0 -193
  61. package/tsconfig.json +0 -16
@@ -1,190 +0,0 @@
1
- import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
2
- import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input";
3
- import {
4
- createStandardChannelSetupStatus,
5
- formatDocsLink,
6
- setSetupChannelEnabled,
7
- type ChannelSetupWizard,
8
- } from "openclaw/plugin-sdk/setup";
9
- import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
10
- import { resolveNextcloudTalkAccount } from "./accounts.js";
11
- import {
12
- clearNextcloudTalkAccountFields,
13
- nextcloudTalkDmPolicy,
14
- normalizeNextcloudTalkBaseUrl,
15
- setNextcloudTalkAccountConfig,
16
- validateNextcloudTalkBaseUrl,
17
- } from "./setup-core.js";
18
- import type { CoreConfig } from "./types.js";
19
-
20
- const channel = "nextcloud-talk" as const;
21
- const CONFIGURE_API_FLAG = "__nextcloudTalkConfigureApiCredentials";
22
-
23
- export const nextcloudTalkSetupWizard: ChannelSetupWizard = {
24
- channel,
25
- stepOrder: "text-first",
26
- status: createStandardChannelSetupStatus({
27
- channelLabel: "Nextcloud Talk",
28
- configuredLabel: "configured",
29
- unconfiguredLabel: "needs setup",
30
- configuredHint: "configured",
31
- unconfiguredHint: "self-hosted chat",
32
- configuredScore: 1,
33
- unconfiguredScore: 5,
34
- resolveConfigured: ({ cfg, accountId }) => {
35
- const account = resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId });
36
- return Boolean(account.secret && account.baseUrl);
37
- },
38
- }),
39
- introNote: {
40
- title: "Nextcloud Talk bot setup",
41
- lines: [
42
- "1) SSH into your Nextcloud server",
43
- '2) Run: ./occ talk:bot:install "OpenClaw" "<shared-secret>" "<webhook-url>" --feature reaction',
44
- "3) Copy the shared secret you used in the command",
45
- "4) Enable the bot in your Nextcloud Talk room settings",
46
- "Tip: you can also set NEXTCLOUD_TALK_BOT_SECRET in your env.",
47
- `Docs: ${formatDocsLink("/channels/nextcloud-talk", "channels/nextcloud-talk")}`,
48
- ],
49
- shouldShow: ({ cfg, accountId }) => {
50
- const account = resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId });
51
- return !account.secret || !account.baseUrl;
52
- },
53
- },
54
- prepare: async ({ cfg, accountId, credentialValues, prompter }) => {
55
- const resolvedAccount = resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId });
56
- const hasApiCredentials = Boolean(
57
- resolvedAccount.config.apiUser?.trim() &&
58
- (hasConfiguredSecretInput(resolvedAccount.config.apiPassword) ||
59
- resolvedAccount.config.apiPasswordFile),
60
- );
61
- const configureApiCredentials = await prompter.confirm({
62
- message: "Configure optional Nextcloud Talk API credentials for room lookups?",
63
- initialValue: hasApiCredentials,
64
- });
65
- if (!configureApiCredentials) {
66
- return undefined;
67
- }
68
- return {
69
- credentialValues: {
70
- ...credentialValues,
71
- [CONFIGURE_API_FLAG]: "1",
72
- },
73
- };
74
- },
75
- credentials: [
76
- {
77
- inputKey: "token",
78
- providerHint: channel,
79
- credentialLabel: "bot secret",
80
- preferredEnvVar: "NEXTCLOUD_TALK_BOT_SECRET",
81
- envPrompt: "NEXTCLOUD_TALK_BOT_SECRET detected. Use env var?",
82
- keepPrompt: "Nextcloud Talk bot secret already configured. Keep it?",
83
- inputPrompt: "Enter Nextcloud Talk bot secret",
84
- allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID,
85
- inspect: ({ cfg, accountId }) => {
86
- const resolvedAccount = resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId });
87
- return {
88
- accountConfigured: Boolean(resolvedAccount.secret && resolvedAccount.baseUrl),
89
- hasConfiguredValue: Boolean(
90
- hasConfiguredSecretInput(resolvedAccount.config.botSecret) ||
91
- resolvedAccount.config.botSecretFile,
92
- ),
93
- resolvedValue: resolvedAccount.secret || undefined,
94
- envValue:
95
- accountId === DEFAULT_ACCOUNT_ID
96
- ? normalizeOptionalString(process.env.NEXTCLOUD_TALK_BOT_SECRET)
97
- : undefined,
98
- };
99
- },
100
- applyUseEnv: async (params) => {
101
- const resolvedAccount = resolveNextcloudTalkAccount({
102
- cfg: params.cfg as CoreConfig,
103
- accountId: params.accountId,
104
- });
105
- const cleared = clearNextcloudTalkAccountFields(
106
- params.cfg as CoreConfig,
107
- params.accountId,
108
- ["botSecret", "botSecretFile"],
109
- );
110
- return setNextcloudTalkAccountConfig(cleared, params.accountId, {
111
- baseUrl: resolvedAccount.baseUrl,
112
- });
113
- },
114
- applySet: async (params) =>
115
- setNextcloudTalkAccountConfig(
116
- clearNextcloudTalkAccountFields(params.cfg as CoreConfig, params.accountId, [
117
- "botSecret",
118
- "botSecretFile",
119
- ]),
120
- params.accountId,
121
- {
122
- botSecret: params.value,
123
- },
124
- ),
125
- },
126
- {
127
- inputKey: "password",
128
- providerHint: "nextcloud-talk-api",
129
- credentialLabel: "API password",
130
- preferredEnvVar: "NEXTCLOUD_TALK_API_PASSWORD",
131
- envPrompt: "",
132
- keepPrompt: "Nextcloud Talk API password already configured. Keep it?",
133
- inputPrompt: "Enter Nextcloud Talk API password",
134
- inspect: ({ cfg, accountId }) => {
135
- const resolvedAccount = resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId });
136
- const apiUser = resolvedAccount.config.apiUser?.trim();
137
- const apiPasswordConfigured = Boolean(
138
- hasConfiguredSecretInput(resolvedAccount.config.apiPassword) ||
139
- resolvedAccount.config.apiPasswordFile,
140
- );
141
- return {
142
- accountConfigured: Boolean(apiUser && apiPasswordConfigured),
143
- hasConfiguredValue: apiPasswordConfigured,
144
- };
145
- },
146
- shouldPrompt: ({ credentialValues }) => credentialValues[CONFIGURE_API_FLAG] === "1",
147
- applySet: async (params) =>
148
- setNextcloudTalkAccountConfig(
149
- clearNextcloudTalkAccountFields(params.cfg as CoreConfig, params.accountId, [
150
- "apiPassword",
151
- "apiPasswordFile",
152
- ]),
153
- params.accountId,
154
- {
155
- apiPassword: params.value,
156
- },
157
- ),
158
- },
159
- ],
160
- textInputs: [
161
- {
162
- inputKey: "httpUrl",
163
- message: "Enter Nextcloud instance URL (e.g., https://cloud.example.com)",
164
- currentValue: ({ cfg, accountId }) =>
165
- resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId }).baseUrl || undefined,
166
- shouldPrompt: ({ currentValue }) => !currentValue,
167
- validate: ({ value }) => validateNextcloudTalkBaseUrl(value),
168
- normalizeValue: ({ value }) => normalizeNextcloudTalkBaseUrl(value),
169
- applySet: async (params) =>
170
- setNextcloudTalkAccountConfig(params.cfg as CoreConfig, params.accountId, {
171
- baseUrl: params.value,
172
- }),
173
- },
174
- {
175
- inputKey: "userId",
176
- message: "Nextcloud Talk API user",
177
- currentValue: ({ cfg, accountId }) =>
178
- resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId }).config.apiUser?.trim() ||
179
- undefined,
180
- shouldPrompt: ({ credentialValues }) => credentialValues[CONFIGURE_API_FLAG] === "1",
181
- validate: ({ value }) => (value ? undefined : "Required"),
182
- applySet: async (params) =>
183
- setNextcloudTalkAccountConfig(params.cfg as CoreConfig, params.accountId, {
184
- apiUser: params.value,
185
- }),
186
- },
187
- ],
188
- dmPolicy: nextcloudTalkDmPolicy,
189
- disable: (cfg) => setSetupChannelEnabled(cfg, channel, false),
190
- };
package/src/setup.test.ts DELETED
@@ -1,422 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
5
- import { describe, expect, it } from "vitest";
6
- import { resolveNextcloudTalkAccount } from "./accounts.js";
7
- import {
8
- clearNextcloudTalkAccountFields,
9
- nextcloudTalkDmPolicy,
10
- nextcloudTalkSetupAdapter,
11
- normalizeNextcloudTalkBaseUrl,
12
- setNextcloudTalkAccountConfig,
13
- validateNextcloudTalkBaseUrl,
14
- } from "./setup-core.js";
15
- import { nextcloudTalkSetupWizard } from "./setup-surface.js";
16
- import type { CoreConfig } from "./types.js";
17
-
18
- describe("nextcloud talk setup", () => {
19
- it("normalizes and validates base urls", () => {
20
- expect(normalizeNextcloudTalkBaseUrl(" https://cloud.example.com/// ")).toBe(
21
- "https://cloud.example.com",
22
- );
23
- expect(normalizeNextcloudTalkBaseUrl(undefined)).toBe("");
24
-
25
- expect(validateNextcloudTalkBaseUrl("")).toBe("Required");
26
- expect(validateNextcloudTalkBaseUrl("cloud.example.com")).toBe(
27
- "URL must start with http:// or https://",
28
- );
29
- expect(validateNextcloudTalkBaseUrl("https://cloud.example.com")).toBeUndefined();
30
- });
31
-
32
- it("patches scoped account config and clears selected fields", () => {
33
- const cfg: CoreConfig = {
34
- channels: {
35
- "nextcloud-talk": {
36
- baseUrl: "https://cloud.example.com",
37
- botSecret: "top-secret",
38
- accounts: {
39
- work: {
40
- botSecret: "work-secret",
41
- botSecretFile: "/tmp/work-secret",
42
- apiPassword: "api-secret",
43
- },
44
- },
45
- },
46
- },
47
- };
48
-
49
- expect(
50
- setNextcloudTalkAccountConfig(cfg, DEFAULT_ACCOUNT_ID, {
51
- apiUser: "bot",
52
- }),
53
- ).toMatchObject({
54
- channels: {
55
- "nextcloud-talk": {
56
- apiUser: "bot",
57
- },
58
- },
59
- });
60
-
61
- expect(clearNextcloudTalkAccountFields(cfg, DEFAULT_ACCOUNT_ID, ["botSecret"])).toMatchObject({
62
- channels: {
63
- "nextcloud-talk": {
64
- baseUrl: "https://cloud.example.com",
65
- },
66
- },
67
- });
68
- expect(
69
- clearNextcloudTalkAccountFields(cfg, DEFAULT_ACCOUNT_ID, ["botSecret"]),
70
- ).not.toMatchObject({
71
- channels: {
72
- "nextcloud-talk": {
73
- botSecret: expect.anything(),
74
- },
75
- },
76
- });
77
-
78
- expect(
79
- clearNextcloudTalkAccountFields(cfg, "work", ["botSecret", "botSecretFile"]),
80
- ).toMatchObject({
81
- channels: {
82
- "nextcloud-talk": {
83
- accounts: {
84
- work: {
85
- apiPassword: "api-secret",
86
- },
87
- },
88
- },
89
- },
90
- });
91
- });
92
-
93
- it("sets top-level DM policy state", async () => {
94
- const base: CoreConfig = {
95
- channels: {
96
- "nextcloud-talk": {},
97
- },
98
- };
99
-
100
- expect(nextcloudTalkDmPolicy.getCurrent(base)).toBe("pairing");
101
- expect(nextcloudTalkDmPolicy.setPolicy(base, "open")).toMatchObject({
102
- channels: {
103
- "nextcloud-talk": {
104
- dmPolicy: "open",
105
- },
106
- },
107
- });
108
- });
109
-
110
- it("honors named-account DM policy state and config keys", () => {
111
- const base: CoreConfig = {
112
- channels: {
113
- "nextcloud-talk": {
114
- dmPolicy: "disabled",
115
- accounts: {
116
- work: {
117
- baseUrl: "https://cloud.example.com",
118
- botSecret: "work-secret",
119
- dmPolicy: "allowlist",
120
- },
121
- },
122
- },
123
- },
124
- };
125
-
126
- expect(nextcloudTalkDmPolicy.getCurrent(base, "work")).toBe("allowlist");
127
- expect(nextcloudTalkDmPolicy.resolveConfigKeys?.(base, "work")).toEqual({
128
- policyKey: "channels.nextcloud-talk.accounts.work.dmPolicy",
129
- allowFromKey: "channels.nextcloud-talk.accounts.work.allowFrom",
130
- });
131
- });
132
-
133
- it("uses configured defaultAccount for omitted DM policy account context", () => {
134
- const base: CoreConfig = {
135
- channels: {
136
- "nextcloud-talk": {
137
- defaultAccount: "work",
138
- dmPolicy: "disabled",
139
- accounts: {
140
- work: {
141
- baseUrl: "https://cloud.example.com",
142
- botSecret: "work-secret",
143
- dmPolicy: "allowlist",
144
- },
145
- },
146
- },
147
- },
148
- };
149
-
150
- expect(nextcloudTalkDmPolicy.getCurrent(base)).toBe("allowlist");
151
- expect(nextcloudTalkDmPolicy.resolveConfigKeys?.(base)).toEqual({
152
- policyKey: "channels.nextcloud-talk.accounts.work.dmPolicy",
153
- allowFromKey: "channels.nextcloud-talk.accounts.work.allowFrom",
154
- });
155
-
156
- const next = nextcloudTalkDmPolicy.setPolicy(base, "open");
157
- expect(next.channels?.["nextcloud-talk"]?.dmPolicy).toBe("disabled");
158
- const workAccount = next.channels?.["nextcloud-talk"]?.accounts?.work as
159
- | { dmPolicy?: string; allowFrom?: Array<string | number> }
160
- | undefined;
161
- expect(workAccount?.dmPolicy).toBe("open");
162
- });
163
-
164
- it('writes open DM policy to the named account and preserves inherited allowFrom with "*"', () => {
165
- const next = nextcloudTalkDmPolicy.setPolicy(
166
- {
167
- channels: {
168
- "nextcloud-talk": {
169
- allowFrom: ["alice"],
170
- accounts: {
171
- work: {
172
- baseUrl: "https://cloud.example.com",
173
- botSecret: "work-secret",
174
- },
175
- },
176
- },
177
- },
178
- },
179
- "open",
180
- "work",
181
- );
182
-
183
- expect(next.channels?.["nextcloud-talk"]?.dmPolicy).toBeUndefined();
184
- const workAccount = next.channels?.["nextcloud-talk"]?.accounts?.work as
185
- | { dmPolicy?: string; allowFrom?: Array<string | number> }
186
- | undefined;
187
- expect(workAccount?.dmPolicy).toBe("open");
188
- expect(workAccount?.allowFrom).toEqual(["alice", "*"]);
189
- });
190
-
191
- it("validates env/default-account constraints and applies config patches", () => {
192
- const validateInput = nextcloudTalkSetupAdapter.validateInput;
193
- const applyAccountConfig = nextcloudTalkSetupAdapter.applyAccountConfig;
194
- expect(validateInput).toBeTypeOf("function");
195
- expect(applyAccountConfig).toBeTypeOf("function");
196
-
197
- expect(
198
- validateInput!({
199
- accountId: "work",
200
- input: { useEnv: true },
201
- } as never),
202
- ).toBe("NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.");
203
-
204
- expect(
205
- validateInput!({
206
- accountId: DEFAULT_ACCOUNT_ID,
207
- input: { useEnv: false, baseUrl: "", secret: "" },
208
- } as never),
209
- ).toBe("Nextcloud Talk requires bot secret or --secret-file (or --use-env).");
210
-
211
- expect(
212
- validateInput!({
213
- accountId: DEFAULT_ACCOUNT_ID,
214
- input: { useEnv: false, secret: "secret", baseUrl: "" },
215
- } as never),
216
- ).toBe("Nextcloud Talk requires --base-url.");
217
-
218
- expect(
219
- applyAccountConfig({
220
- cfg: {
221
- channels: {
222
- "nextcloud-talk": {},
223
- },
224
- },
225
- accountId: DEFAULT_ACCOUNT_ID,
226
- input: {
227
- name: "Default",
228
- baseUrl: "https://cloud.example.com///",
229
- secret: "bot-secret",
230
- },
231
- } as never),
232
- ).toEqual({
233
- channels: {
234
- "nextcloud-talk": {
235
- enabled: true,
236
- name: "Default",
237
- baseUrl: "https://cloud.example.com",
238
- botSecret: "bot-secret",
239
- },
240
- },
241
- });
242
-
243
- expect(
244
- applyAccountConfig({
245
- cfg: {
246
- channels: {
247
- "nextcloud-talk": {
248
- accounts: {
249
- work: {
250
- botSecret: "old-secret",
251
- },
252
- },
253
- },
254
- },
255
- },
256
- accountId: "work",
257
- input: {
258
- name: "Work",
259
- useEnv: true,
260
- baseUrl: "https://cloud.example.com",
261
- },
262
- } as never),
263
- ).toMatchObject({
264
- channels: {
265
- "nextcloud-talk": {
266
- accounts: {
267
- work: {
268
- enabled: true,
269
- name: "Work",
270
- baseUrl: "https://cloud.example.com",
271
- },
272
- },
273
- },
274
- },
275
- });
276
- });
277
-
278
- it("clears stored bot secret fields when switching the default account to env", () => {
279
- type ApplyAccountConfigContext = Parameters<
280
- typeof nextcloudTalkSetupAdapter.applyAccountConfig
281
- >[0];
282
-
283
- const next = nextcloudTalkSetupAdapter.applyAccountConfig({
284
- cfg: {
285
- channels: {
286
- "nextcloud-talk": {
287
- enabled: true,
288
- baseUrl: "https://cloud.old.example",
289
- botSecret: "stored-secret",
290
- botSecretFile: "/tmp/secret.txt",
291
- },
292
- },
293
- },
294
- accountId: DEFAULT_ACCOUNT_ID,
295
- input: {
296
- baseUrl: "https://cloud.example.com",
297
- useEnv: true,
298
- },
299
- } as unknown as ApplyAccountConfigContext);
300
-
301
- expect(next.channels?.["nextcloud-talk"]?.baseUrl).toBe("https://cloud.example.com");
302
- expect(next.channels?.["nextcloud-talk"]).not.toHaveProperty("botSecret");
303
- expect(next.channels?.["nextcloud-talk"]).not.toHaveProperty("botSecretFile");
304
- });
305
-
306
- it("clears stored bot secret fields when the wizard switches to env", async () => {
307
- const credential = nextcloudTalkSetupWizard.credentials[0];
308
- const next = await credential.applyUseEnv?.({
309
- cfg: {
310
- channels: {
311
- "nextcloud-talk": {
312
- enabled: true,
313
- baseUrl: "https://cloud.example.com",
314
- botSecret: "stored-secret",
315
- botSecretFile: "/tmp/secret.txt",
316
- },
317
- },
318
- },
319
- accountId: DEFAULT_ACCOUNT_ID,
320
- });
321
-
322
- expect(next?.channels?.["nextcloud-talk"]).not.toHaveProperty("botSecret");
323
- expect(next?.channels?.["nextcloud-talk"]).not.toHaveProperty("botSecretFile");
324
- });
325
- });
326
-
327
- describe("resolveNextcloudTalkAccount", () => {
328
- it("matches normalized configured account ids", () => {
329
- const account = resolveNextcloudTalkAccount({
330
- cfg: {
331
- channels: {
332
- "nextcloud-talk": {
333
- accounts: {
334
- "Ops Team": {
335
- baseUrl: "https://cloud.example.com",
336
- botSecret: "bot-secret",
337
- },
338
- },
339
- },
340
- },
341
- } as CoreConfig,
342
- accountId: "ops-team",
343
- });
344
-
345
- expect(account.accountId).toBe("ops-team");
346
- expect(account.baseUrl).toBe("https://cloud.example.com");
347
- expect(account.secret).toBe("bot-secret");
348
- expect(account.secretSource).toBe("config");
349
- });
350
-
351
- it.runIf(process.platform !== "win32")("rejects symlinked botSecretFile paths", () => {
352
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-nextcloud-talk-"));
353
- const secretFile = path.join(dir, "secret.txt");
354
- const secretLink = path.join(dir, "secret-link.txt");
355
- fs.writeFileSync(secretFile, "bot-secret\n", "utf8");
356
- fs.symlinkSync(secretFile, secretLink);
357
-
358
- const cfg = {
359
- channels: {
360
- "nextcloud-talk": {
361
- baseUrl: "https://cloud.example.com",
362
- botSecretFile: secretLink,
363
- },
364
- },
365
- } as CoreConfig;
366
-
367
- const account = resolveNextcloudTalkAccount({ cfg });
368
- expect(account.secret).toBe("");
369
- expect(account.secretSource).toBe("none");
370
- fs.rmSync(dir, { recursive: true, force: true });
371
- });
372
-
373
- it("uses configured defaultAccount when accountId is omitted", () => {
374
- const account = resolveNextcloudTalkAccount({
375
- cfg: {
376
- channels: {
377
- "nextcloud-talk": {
378
- defaultAccount: "work",
379
- botSecret: "top-secret",
380
- accounts: {
381
- work: {
382
- baseUrl: "https://cloud.example.com",
383
- botSecret: "work-secret",
384
- },
385
- },
386
- },
387
- },
388
- } as CoreConfig,
389
- });
390
-
391
- expect(account.accountId).toBe("work");
392
- expect(account.baseUrl).toBe("https://cloud.example.com");
393
- expect(account.secret).toBe("work-secret");
394
- expect(account.secretSource).toBe("config");
395
- });
396
-
397
- it("uses configured defaultAccount for omitted setup configured state", () => {
398
- const configured = nextcloudTalkSetupWizard.status.resolveConfigured({
399
- cfg: {
400
- channels: {
401
- "nextcloud-talk": {
402
- defaultAccount: "work",
403
- baseUrl: "https://root.example.com",
404
- botSecret: "root-secret",
405
- accounts: {
406
- alerts: {
407
- baseUrl: "https://alerts.example.com",
408
- botSecret: "alerts-secret",
409
- },
410
- work: {
411
- baseUrl: "",
412
- botSecret: "",
413
- },
414
- },
415
- },
416
- },
417
- } as CoreConfig,
418
- });
419
-
420
- expect(configured).toBe(false);
421
- });
422
- });
package/src/signature.ts DELETED
@@ -1,82 +0,0 @@
1
- import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
- import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
3
- import type { NextcloudTalkWebhookHeaders } from "./types.js";
4
-
5
- const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
6
- const RANDOM_HEADER = "x-nextcloud-talk-random";
7
- const BACKEND_HEADER = "x-nextcloud-talk-backend";
8
-
9
- /**
10
- * Verify the HMAC-SHA256 signature of an incoming webhook request.
11
- * Signature is calculated as: HMAC-SHA256(random + body, secret)
12
- */
13
- export function verifyNextcloudTalkSignature(params: {
14
- signature: string;
15
- random: string;
16
- body: string;
17
- secret: string;
18
- }): boolean {
19
- const { signature, random, body, secret } = params;
20
- if (!signature || !random || !secret) {
21
- return false;
22
- }
23
-
24
- const expected = createHmac("sha256", secret)
25
- .update(random + body)
26
- .digest("hex");
27
-
28
- const expectedBuf = Buffer.from(expected, "utf8");
29
- const signatureBuf = Buffer.from(signature, "utf8");
30
-
31
- // Pad to equal length before constant-time comparison to prevent
32
- // leaking length information via early-return timing.
33
- // Note: digest("hex") always produces lowercase ASCII (64 bytes for SHA-256),
34
- // so expectedBuf is always 64 bytes — no variable-length concern on the expected side.
35
- const maxLen = Math.max(expectedBuf.length, signatureBuf.length);
36
- const paddedExpected = Buffer.alloc(maxLen);
37
- const paddedSignature = Buffer.alloc(maxLen);
38
- expectedBuf.copy(paddedExpected);
39
- signatureBuf.copy(paddedSignature);
40
-
41
- // Use crypto.timingSafeEqual instead of manual XOR loop to avoid
42
- // potential JIT-optimisation timing leaks in the JavaScript engine.
43
- const timingResult = timingSafeEqual(paddedExpected, paddedSignature);
44
- return expectedBuf.length === signatureBuf.length && timingResult;
45
- }
46
-
47
- /**
48
- * Extract webhook headers from an incoming request.
49
- */
50
- export function extractNextcloudTalkHeaders(
51
- headers: Record<string, string | string[] | undefined>,
52
- ): NextcloudTalkWebhookHeaders | null {
53
- const getHeader = (name: string): string | undefined => {
54
- const value = headers[name] ?? headers[normalizeLowercaseStringOrEmpty(name)];
55
- return Array.isArray(value) ? value[0] : value;
56
- };
57
-
58
- const signature = getHeader(SIGNATURE_HEADER);
59
- const random = getHeader(RANDOM_HEADER);
60
- const backend = getHeader(BACKEND_HEADER);
61
-
62
- if (!signature || !random || !backend) {
63
- return null;
64
- }
65
-
66
- return { signature, random, backend };
67
- }
68
-
69
- /**
70
- * Generate signature headers for an outbound request to Nextcloud Talk.
71
- */
72
- export function generateNextcloudTalkSignature(params: { body: string; secret: string }): {
73
- random: string;
74
- signature: string;
75
- } {
76
- const { body, secret } = params;
77
- const random = randomBytes(32).toString("hex");
78
- const signature = createHmac("sha256", secret)
79
- .update(random + body)
80
- .digest("hex");
81
- return { random, signature };
82
- }