@openclaw/nextcloud-talk 2026.3.12 → 2026.5.1-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 (60) hide show
  1. package/api.ts +1 -0
  2. package/channel-plugin-api.ts +1 -0
  3. package/contract-api.ts +4 -0
  4. package/doctor-contract-api.ts +1 -0
  5. package/index.ts +15 -12
  6. package/openclaw.plugin.json +804 -1
  7. package/package.json +31 -4
  8. package/runtime-api.ts +33 -0
  9. package/secret-contract-api.ts +5 -0
  10. package/setup-entry.ts +13 -0
  11. package/src/accounts.ts +25 -42
  12. package/src/approval-auth.test.ts +17 -0
  13. package/src/approval-auth.ts +27 -0
  14. package/src/channel-api.ts +5 -0
  15. package/src/channel.adapters.ts +52 -0
  16. package/src/channel.core.test.ts +75 -0
  17. package/src/channel.lifecycle.test.ts +81 -0
  18. package/src/channel.ts +157 -388
  19. package/src/config-schema.ts +27 -22
  20. package/src/core.test.ts +397 -0
  21. package/src/doctor-contract.ts +9 -0
  22. package/src/doctor.test.ts +40 -0
  23. package/src/doctor.ts +10 -0
  24. package/src/gateway.ts +109 -0
  25. package/src/inbound.authz.test.ts +87 -22
  26. package/src/inbound.behavior.test.ts +202 -0
  27. package/src/inbound.ts +28 -26
  28. package/src/monitor-runtime.ts +138 -0
  29. package/src/monitor.replay.test.ts +238 -0
  30. package/src/monitor.test-harness.ts +2 -2
  31. package/src/monitor.ts +125 -155
  32. package/src/normalize.ts +7 -2
  33. package/src/policy.ts +23 -31
  34. package/src/replay-guard.ts +74 -11
  35. package/src/room-info.test.ts +116 -0
  36. package/src/room-info.ts +11 -3
  37. package/src/runtime.ts +6 -3
  38. package/src/secret-contract.ts +103 -0
  39. package/src/secret-input.ts +1 -10
  40. package/src/send.cfg-threading.test.ts +153 -0
  41. package/src/send.runtime.ts +8 -0
  42. package/src/send.ts +126 -106
  43. package/src/session-route.ts +40 -0
  44. package/src/setup-core.ts +248 -0
  45. package/src/setup-surface.ts +190 -0
  46. package/src/setup.test.ts +422 -0
  47. package/src/signature.ts +20 -10
  48. package/src/types.ts +19 -16
  49. package/tsconfig.json +16 -0
  50. package/src/accounts.test.ts +0 -30
  51. package/src/channel.startup.test.ts +0 -83
  52. package/src/config-schema.test.ts +0 -36
  53. package/src/format.ts +0 -79
  54. package/src/monitor.auth-order.test.ts +0 -28
  55. package/src/monitor.backend.test.ts +0 -27
  56. package/src/monitor.read-body.test.ts +0 -16
  57. package/src/onboarding.ts +0 -302
  58. package/src/policy.test.ts +0 -138
  59. package/src/replay-guard.test.ts +0 -70
  60. package/src/send.test.ts +0 -104
package/src/types.ts CHANGED
@@ -4,9 +4,7 @@ import type {
4
4
  DmPolicy,
5
5
  GroupPolicy,
6
6
  SecretInput,
7
- } from "openclaw/plugin-sdk/nextcloud-talk";
8
-
9
- export type { DmPolicy, GroupPolicy };
7
+ } from "../runtime-api.js";
10
8
 
11
9
  export type NextcloudTalkRoomConfig = {
12
10
  requireMention?: boolean;
@@ -22,6 +20,11 @@ export type NextcloudTalkRoomConfig = {
22
20
  systemPrompt?: string;
23
21
  };
24
22
 
23
+ type NextcloudTalkNetworkConfig = {
24
+ /** Dangerous opt-in for self-hosted Nextcloud Talk on trusted private/internal hosts. */
25
+ dangerouslyAllowPrivateNetwork?: boolean;
26
+ };
27
+
25
28
  export type NextcloudTalkAccountConfig = {
26
29
  /** Optional display name for this account (used in CLI/UI lists). */
27
30
  name?: string;
@@ -75,9 +78,11 @@ export type NextcloudTalkAccountConfig = {
75
78
  responsePrefix?: string;
76
79
  /** Media upload max size in MB. */
77
80
  mediaMaxMb?: number;
81
+ /** Network policy overrides for self-hosted Nextcloud Talk on trusted private/internal hosts. */
82
+ network?: NextcloudTalkNetworkConfig;
78
83
  };
79
84
 
80
- export type NextcloudTalkConfig = {
85
+ type NextcloudTalkConfig = {
81
86
  /** Optional per-account Nextcloud Talk configuration (multi-account). */
82
87
  accounts?: Record<string, NextcloudTalkAccountConfig>;
83
88
  /** Optional default account id when multiple accounts are configured. */
@@ -97,7 +102,7 @@ export type CoreConfig = {
97
102
  */
98
103
 
99
104
  /** Actor in the activity (the message sender). */
100
- export type NextcloudTalkActor = {
105
+ type NextcloudTalkActor = {
101
106
  type: "Person";
102
107
  /** User ID in Nextcloud. */
103
108
  id: string;
@@ -106,7 +111,7 @@ export type NextcloudTalkActor = {
106
111
  };
107
112
 
108
113
  /** The message object in the activity. */
109
- export type NextcloudTalkObject = {
114
+ type NextcloudTalkObject = {
110
115
  type: "Note";
111
116
  /** Message ID. */
112
117
  id: string;
@@ -119,7 +124,7 @@ export type NextcloudTalkObject = {
119
124
  };
120
125
 
121
126
  /** Target conversation/room. */
122
- export type NextcloudTalkTarget = {
127
+ type NextcloudTalkTarget = {
123
128
  type: "Collection";
124
129
  /** Room token. */
125
130
  id: string;
@@ -172,19 +177,17 @@ export type NextcloudTalkWebhookServerOptions = {
172
177
  path: string;
173
178
  secret: string;
174
179
  maxBodyBytes?: number;
180
+ authRateLimit?: {
181
+ maxRequests?: number;
182
+ windowMs?: number;
183
+ };
175
184
  readBody?: (req: import("node:http").IncomingMessage, maxBodyBytes: number) => Promise<string>;
176
185
  isBackendAllowed?: (backend: string) => boolean;
177
186
  shouldProcessMessage?: (message: NextcloudTalkInboundMessage) => boolean | Promise<boolean>;
187
+ processMessage?: (
188
+ message: NextcloudTalkInboundMessage,
189
+ ) => void | "processed" | "duplicate" | Promise<void | "processed" | "duplicate">;
178
190
  onMessage: (message: NextcloudTalkInboundMessage) => void | Promise<void>;
179
191
  onError?: (error: Error) => void;
180
192
  abortSignal?: AbortSignal;
181
193
  };
182
-
183
- /** Options for sending a message. */
184
- export type NextcloudTalkSendOptions = {
185
- baseUrl: string;
186
- secret: string;
187
- roomToken: string;
188
- message: string;
189
- replyTo?: string;
190
- };
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../tsconfig.package-boundary.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "."
5
+ },
6
+ "include": ["./*.ts", "./src/**/*.ts"],
7
+ "exclude": [
8
+ "./**/*.test.ts",
9
+ "./dist/**",
10
+ "./node_modules/**",
11
+ "./src/test-support/**",
12
+ "./src/**/*test-helpers.ts",
13
+ "./src/**/*test-harness.ts",
14
+ "./src/**/*test-support.ts"
15
+ ]
16
+ }
@@ -1,30 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { describe, expect, it } from "vitest";
5
- import { resolveNextcloudTalkAccount } from "./accounts.js";
6
- import type { CoreConfig } from "./types.js";
7
-
8
- describe("resolveNextcloudTalkAccount", () => {
9
- it.runIf(process.platform !== "win32")("rejects symlinked botSecretFile paths", () => {
10
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-nextcloud-talk-"));
11
- const secretFile = path.join(dir, "secret.txt");
12
- const secretLink = path.join(dir, "secret-link.txt");
13
- fs.writeFileSync(secretFile, "bot-secret\n", "utf8");
14
- fs.symlinkSync(secretFile, secretLink);
15
-
16
- const cfg = {
17
- channels: {
18
- "nextcloud-talk": {
19
- baseUrl: "https://cloud.example.com",
20
- botSecretFile: secretLink,
21
- },
22
- },
23
- } as CoreConfig;
24
-
25
- const account = resolveNextcloudTalkAccount({ cfg });
26
- expect(account.secret).toBe("");
27
- expect(account.secretSource).toBe("none");
28
- fs.rmSync(dir, { recursive: true, force: true });
29
- });
30
- });
@@ -1,83 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
- import { createStartAccountContext } from "../../test-utils/start-account-context.js";
3
- import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
4
-
5
- const hoisted = vi.hoisted(() => ({
6
- monitorNextcloudTalkProvider: vi.fn(),
7
- }));
8
-
9
- vi.mock("./monitor.js", async () => {
10
- const actual = await vi.importActual<typeof import("./monitor.js")>("./monitor.js");
11
- return {
12
- ...actual,
13
- monitorNextcloudTalkProvider: hoisted.monitorNextcloudTalkProvider,
14
- };
15
- });
16
-
17
- import { nextcloudTalkPlugin } from "./channel.js";
18
-
19
- function buildAccount(): ResolvedNextcloudTalkAccount {
20
- return {
21
- accountId: "default",
22
- enabled: true,
23
- baseUrl: "https://nextcloud.example.com",
24
- secret: "secret", // pragma: allowlist secret
25
- secretSource: "config", // pragma: allowlist secret
26
- config: {
27
- baseUrl: "https://nextcloud.example.com",
28
- botSecret: "secret", // pragma: allowlist secret
29
- webhookPath: "/nextcloud-talk-webhook",
30
- webhookPort: 8788,
31
- },
32
- };
33
- }
34
-
35
- describe("nextcloudTalkPlugin gateway.startAccount", () => {
36
- afterEach(() => {
37
- vi.clearAllMocks();
38
- });
39
-
40
- it("keeps startAccount pending until abort, then stops the monitor", async () => {
41
- const stop = vi.fn();
42
- hoisted.monitorNextcloudTalkProvider.mockResolvedValue({ stop });
43
- const abort = new AbortController();
44
-
45
- const task = nextcloudTalkPlugin.gateway!.startAccount!(
46
- createStartAccountContext({
47
- account: buildAccount(),
48
- abortSignal: abort.signal,
49
- }),
50
- );
51
- let settled = false;
52
- void task.then(() => {
53
- settled = true;
54
- });
55
- await vi.waitFor(() => {
56
- expect(hoisted.monitorNextcloudTalkProvider).toHaveBeenCalledOnce();
57
- });
58
- expect(settled).toBe(false);
59
- expect(stop).not.toHaveBeenCalled();
60
-
61
- abort.abort();
62
- await task;
63
-
64
- expect(stop).toHaveBeenCalledOnce();
65
- });
66
-
67
- it("stops immediately when startAccount receives an already-aborted signal", async () => {
68
- const stop = vi.fn();
69
- hoisted.monitorNextcloudTalkProvider.mockResolvedValue({ stop });
70
- const abort = new AbortController();
71
- abort.abort();
72
-
73
- await nextcloudTalkPlugin.gateway!.startAccount!(
74
- createStartAccountContext({
75
- account: buildAccount(),
76
- abortSignal: abort.signal,
77
- }),
78
- );
79
-
80
- expect(hoisted.monitorNextcloudTalkProvider).toHaveBeenCalledOnce();
81
- expect(stop).toHaveBeenCalledOnce();
82
- });
83
- });
@@ -1,36 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { NextcloudTalkConfigSchema } from "./config-schema.js";
3
-
4
- describe("NextcloudTalkConfigSchema SecretInput", () => {
5
- it("accepts SecretRef botSecret and apiPassword at top-level", () => {
6
- const result = NextcloudTalkConfigSchema.safeParse({
7
- baseUrl: "https://cloud.example.com",
8
- botSecret: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_BOT_SECRET" },
9
- apiUser: "bot",
10
- apiPassword: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_API_PASSWORD" },
11
- });
12
- expect(result.success).toBe(true);
13
- });
14
-
15
- it("accepts SecretRef botSecret and apiPassword on account", () => {
16
- const result = NextcloudTalkConfigSchema.safeParse({
17
- accounts: {
18
- main: {
19
- baseUrl: "https://cloud.example.com",
20
- botSecret: {
21
- source: "env",
22
- provider: "default",
23
- id: "NEXTCLOUD_TALK_MAIN_BOT_SECRET",
24
- },
25
- apiUser: "bot",
26
- apiPassword: {
27
- source: "env",
28
- provider: "default",
29
- id: "NEXTCLOUD_TALK_MAIN_API_PASSWORD",
30
- },
31
- },
32
- },
33
- });
34
- expect(result.success).toBe(true);
35
- });
36
- });
package/src/format.ts DELETED
@@ -1,79 +0,0 @@
1
- /**
2
- * Format utilities for Nextcloud Talk messages.
3
- *
4
- * Nextcloud Talk supports markdown natively, so most formatting passes through.
5
- * This module handles any edge cases or transformations needed.
6
- */
7
-
8
- /**
9
- * Convert markdown to Nextcloud Talk compatible format.
10
- * Nextcloud Talk supports standard markdown, so minimal transformation needed.
11
- */
12
- export function markdownToNextcloudTalk(text: string): string {
13
- return text.trim();
14
- }
15
-
16
- /**
17
- * Escape special characters in text to prevent markdown interpretation.
18
- */
19
- export function escapeNextcloudTalkMarkdown(text: string): string {
20
- return text.replace(/([*_`~[\]()#>+\-=|{}!\\])/g, "\\$1");
21
- }
22
-
23
- /**
24
- * Format a mention for a Nextcloud user.
25
- * Nextcloud Talk uses @user format for mentions.
26
- */
27
- export function formatNextcloudTalkMention(userId: string): string {
28
- return `@${userId.replace(/^@/, "")}`;
29
- }
30
-
31
- /**
32
- * Format a code block for Nextcloud Talk.
33
- */
34
- export function formatNextcloudTalkCodeBlock(code: string, language?: string): string {
35
- const lang = language ?? "";
36
- return `\`\`\`${lang}\n${code}\n\`\`\``;
37
- }
38
-
39
- /**
40
- * Format inline code for Nextcloud Talk.
41
- */
42
- export function formatNextcloudTalkInlineCode(code: string): string {
43
- if (code.includes("`")) {
44
- return `\`\` ${code} \`\``;
45
- }
46
- return `\`${code}\``;
47
- }
48
-
49
- /**
50
- * Strip Nextcloud Talk specific formatting from text.
51
- * Useful for extracting plain text content.
52
- */
53
- export function stripNextcloudTalkFormatting(text: string): string {
54
- return text
55
- .replace(/```[\s\S]*?```/g, "")
56
- .replace(/`[^`]+`/g, "")
57
- .replace(/\*\*([^*]+)\*\*/g, "$1")
58
- .replace(/\*([^*]+)\*/g, "$1")
59
- .replace(/_([^_]+)_/g, "$1")
60
- .replace(/~~([^~]+)~~/g, "$1")
61
- .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
62
- .replace(/\s+/g, " ")
63
- .trim();
64
- }
65
-
66
- /**
67
- * Truncate text to a maximum length, preserving word boundaries.
68
- */
69
- export function truncateNextcloudTalkText(text: string, maxLength: number, suffix = "..."): string {
70
- if (text.length <= maxLength) {
71
- return text;
72
- }
73
- const truncated = text.slice(0, maxLength - suffix.length);
74
- const lastSpace = truncated.lastIndexOf(" ");
75
- if (lastSpace > maxLength * 0.7) {
76
- return truncated.slice(0, lastSpace) + suffix;
77
- }
78
- return truncated + suffix;
79
- }
@@ -1,28 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { startWebhookServer } from "./monitor.test-harness.js";
3
-
4
- describe("createNextcloudTalkWebhookServer auth order", () => {
5
- it("rejects missing signature headers before reading request body", async () => {
6
- const readBody = vi.fn(async () => {
7
- throw new Error("should not be called for missing signature headers");
8
- });
9
- const harness = await startWebhookServer({
10
- path: "/nextcloud-auth-order",
11
- maxBodyBytes: 128,
12
- readBody,
13
- onMessage: vi.fn(),
14
- });
15
-
16
- const response = await fetch(harness.webhookUrl, {
17
- method: "POST",
18
- headers: {
19
- "content-type": "application/json",
20
- },
21
- body: "{}",
22
- });
23
-
24
- expect(response.status).toBe(400);
25
- expect(await response.json()).toEqual({ error: "Missing signature headers" });
26
- expect(readBody).not.toHaveBeenCalled();
27
- });
28
- });
@@ -1,27 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { createSignedCreateMessageRequest } from "./monitor.test-fixtures.js";
3
- import { startWebhookServer } from "./monitor.test-harness.js";
4
-
5
- describe("createNextcloudTalkWebhookServer backend allowlist", () => {
6
- it("rejects requests from unexpected backend origins", async () => {
7
- const onMessage = vi.fn(async () => {});
8
- const harness = await startWebhookServer({
9
- path: "/nextcloud-backend-check",
10
- isBackendAllowed: (backend) => backend === "https://nextcloud.expected",
11
- onMessage,
12
- });
13
-
14
- const { body, headers } = createSignedCreateMessageRequest({
15
- backend: "https://nextcloud.unexpected",
16
- });
17
- const response = await fetch(harness.webhookUrl, {
18
- method: "POST",
19
- headers,
20
- body,
21
- });
22
-
23
- expect(response.status).toBe(401);
24
- expect(await response.json()).toEqual({ error: "Invalid backend" });
25
- expect(onMessage).not.toHaveBeenCalled();
26
- });
27
- });
@@ -1,16 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { createMockIncomingRequest } from "../../../test/helpers/mock-incoming-request.js";
3
- import { readNextcloudTalkWebhookBody } from "./monitor.js";
4
-
5
- describe("readNextcloudTalkWebhookBody", () => {
6
- it("reads valid body within max bytes", async () => {
7
- const req = createMockIncomingRequest(['{"type":"Create"}']);
8
- const body = await readNextcloudTalkWebhookBody(req, 1024);
9
- expect(body).toBe('{"type":"Create"}');
10
- });
11
-
12
- it("rejects when payload exceeds max bytes", async () => {
13
- const req = createMockIncomingRequest(["x".repeat(300)]);
14
- await expect(readNextcloudTalkWebhookBody(req, 128)).rejects.toThrow("PayloadTooLarge");
15
- });
16
- });