@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.
- package/dist/api.js +2 -0
- package/dist/channel-BVVRsVr5.js +1788 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/contract-api.js +2 -0
- package/dist/doctor-contract-CYlB-4Bf.js +7 -0
- package/dist/doctor-contract-api.js +2 -0
- package/dist/index.js +22 -0
- package/dist/runtime-api-BcCzeRN9.js +15 -0
- package/dist/runtime-api.js +2 -0
- package/dist/secret-contract-api.js +2 -0
- package/dist/secret-contract-bczDw2-2.js +86 -0
- package/dist/setup-entry.js +15 -0
- package/package.json +14 -6
- package/api.ts +0 -1
- package/channel-plugin-api.ts +0 -1
- package/contract-api.ts +0 -4
- package/doctor-contract-api.ts +0 -1
- package/index.ts +0 -20
- package/runtime-api.ts +0 -33
- package/secret-contract-api.ts +0 -5
- package/setup-entry.ts +0 -13
- package/src/accounts.ts +0 -139
- package/src/approval-auth.test.ts +0 -17
- package/src/approval-auth.ts +0 -27
- package/src/channel-api.ts +0 -5
- package/src/channel.adapters.ts +0 -52
- package/src/channel.core.test.ts +0 -75
- package/src/channel.lifecycle.test.ts +0 -81
- package/src/channel.ts +0 -195
- package/src/config-schema.ts +0 -79
- package/src/core.test.ts +0 -397
- package/src/doctor-contract.ts +0 -9
- package/src/doctor.test.ts +0 -40
- package/src/doctor.ts +0 -10
- package/src/gateway.ts +0 -109
- package/src/inbound.authz.test.ts +0 -149
- package/src/inbound.behavior.test.ts +0 -202
- package/src/inbound.ts +0 -320
- package/src/monitor-runtime.ts +0 -138
- package/src/monitor.replay.test.ts +0 -279
- package/src/monitor.test-fixtures.ts +0 -30
- package/src/monitor.test-harness.ts +0 -59
- package/src/monitor.ts +0 -385
- package/src/normalize.ts +0 -44
- package/src/policy.ts +0 -180
- package/src/replay-guard.ts +0 -128
- package/src/room-info.test.ts +0 -116
- package/src/room-info.ts +0 -148
- package/src/runtime.ts +0 -9
- package/src/secret-contract.ts +0 -103
- package/src/secret-input.ts +0 -4
- package/src/send.cfg-threading.test.ts +0 -153
- package/src/send.runtime.ts +0 -8
- package/src/send.ts +0 -236
- package/src/session-route.ts +0 -40
- package/src/setup-core.ts +0 -248
- package/src/setup-surface.ts +0 -190
- package/src/setup.test.ts +0 -422
- package/src/signature.ts +0 -82
- package/src/types.ts +0 -193
- package/tsconfig.json +0 -16
package/src/replay-guard.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
|
|
3
|
-
|
|
4
|
-
const DEFAULT_REPLAY_TTL_MS = 24 * 60 * 60 * 1000;
|
|
5
|
-
const DEFAULT_MEMORY_MAX_SIZE = 1_000;
|
|
6
|
-
const DEFAULT_FILE_MAX_ENTRIES = 10_000;
|
|
7
|
-
|
|
8
|
-
function sanitizeSegment(value: string): string {
|
|
9
|
-
const trimmed = value.trim();
|
|
10
|
-
if (!trimmed) {
|
|
11
|
-
return "default";
|
|
12
|
-
}
|
|
13
|
-
return trimmed.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function buildReplayKey(params: { roomToken: string; messageId: string }): string | null {
|
|
17
|
-
const roomToken = params.roomToken.trim();
|
|
18
|
-
const messageId = params.messageId.trim();
|
|
19
|
-
if (!roomToken || !messageId) {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
return `${roomToken}:${messageId}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type NextcloudTalkReplayGuardOptions = {
|
|
26
|
-
stateDir?: string;
|
|
27
|
-
ttlMs?: number;
|
|
28
|
-
memoryMaxSize?: number;
|
|
29
|
-
fileMaxEntries?: number;
|
|
30
|
-
onDiskError?: (error: unknown) => void;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export type NextcloudTalkReplayGuard = {
|
|
34
|
-
claimMessage: (params: {
|
|
35
|
-
accountId: string;
|
|
36
|
-
roomToken: string;
|
|
37
|
-
messageId: string;
|
|
38
|
-
}) => Promise<"claimed" | "duplicate" | "inflight" | "invalid">;
|
|
39
|
-
commitMessage: (params: {
|
|
40
|
-
accountId: string;
|
|
41
|
-
roomToken: string;
|
|
42
|
-
messageId: string;
|
|
43
|
-
}) => Promise<boolean>;
|
|
44
|
-
releaseMessage: (params: {
|
|
45
|
-
accountId: string;
|
|
46
|
-
roomToken: string;
|
|
47
|
-
messageId: string;
|
|
48
|
-
error?: unknown;
|
|
49
|
-
}) => void;
|
|
50
|
-
shouldProcessMessage: (params: {
|
|
51
|
-
accountId: string;
|
|
52
|
-
roomToken: string;
|
|
53
|
-
messageId: string;
|
|
54
|
-
}) => Promise<boolean>;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export function createNextcloudTalkReplayGuard(
|
|
58
|
-
options: NextcloudTalkReplayGuardOptions,
|
|
59
|
-
): NextcloudTalkReplayGuard {
|
|
60
|
-
const stateDir = options.stateDir?.trim();
|
|
61
|
-
const baseOptions = {
|
|
62
|
-
ttlMs: options.ttlMs ?? DEFAULT_REPLAY_TTL_MS,
|
|
63
|
-
memoryMaxSize: options.memoryMaxSize ?? DEFAULT_MEMORY_MAX_SIZE,
|
|
64
|
-
};
|
|
65
|
-
const dedupe = createClaimableDedupe(
|
|
66
|
-
stateDir
|
|
67
|
-
? {
|
|
68
|
-
...baseOptions,
|
|
69
|
-
fileMaxEntries: options.fileMaxEntries ?? DEFAULT_FILE_MAX_ENTRIES,
|
|
70
|
-
resolveFilePath: (namespace) =>
|
|
71
|
-
path.join(
|
|
72
|
-
stateDir,
|
|
73
|
-
"nextcloud-talk",
|
|
74
|
-
"replay-dedupe",
|
|
75
|
-
`${sanitizeSegment(namespace)}.json`,
|
|
76
|
-
),
|
|
77
|
-
onDiskError: options.onDiskError,
|
|
78
|
-
}
|
|
79
|
-
: baseOptions,
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
claimMessage: async ({ accountId, roomToken, messageId }) => {
|
|
84
|
-
const replayKey = buildReplayKey({ roomToken, messageId });
|
|
85
|
-
if (!replayKey) {
|
|
86
|
-
return "invalid";
|
|
87
|
-
}
|
|
88
|
-
const result = await dedupe.claim(replayKey, {
|
|
89
|
-
namespace: accountId,
|
|
90
|
-
});
|
|
91
|
-
return result.kind;
|
|
92
|
-
},
|
|
93
|
-
commitMessage: async ({ accountId, roomToken, messageId }) => {
|
|
94
|
-
const replayKey = buildReplayKey({ roomToken, messageId });
|
|
95
|
-
if (!replayKey) {
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
return await dedupe.commit(replayKey, {
|
|
99
|
-
namespace: accountId,
|
|
100
|
-
});
|
|
101
|
-
},
|
|
102
|
-
releaseMessage: ({ accountId, roomToken, messageId, error }) => {
|
|
103
|
-
const replayKey = buildReplayKey({ roomToken, messageId });
|
|
104
|
-
if (!replayKey) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
dedupe.release(replayKey, {
|
|
108
|
-
namespace: accountId,
|
|
109
|
-
error,
|
|
110
|
-
});
|
|
111
|
-
},
|
|
112
|
-
shouldProcessMessage: async ({ accountId, roomToken, messageId }) => {
|
|
113
|
-
const replayKey = buildReplayKey({ roomToken, messageId });
|
|
114
|
-
if (!replayKey) {
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
const result = await dedupe.claim(replayKey, {
|
|
118
|
-
namespace: accountId,
|
|
119
|
-
});
|
|
120
|
-
if (result.kind !== "claimed") {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
return await dedupe.commit(replayKey, {
|
|
124
|
-
namespace: accountId,
|
|
125
|
-
});
|
|
126
|
-
},
|
|
127
|
-
};
|
|
128
|
-
}
|
package/src/room-info.test.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { resolveNextcloudTalkRoomKind, __testing } from "./room-info.js";
|
|
3
|
-
|
|
4
|
-
const fetchWithSsrFGuard = vi.hoisted(() => vi.fn());
|
|
5
|
-
const readFileSync = vi.hoisted(() => vi.fn());
|
|
6
|
-
|
|
7
|
-
vi.mock("../runtime-api.js", () => {
|
|
8
|
-
return vi
|
|
9
|
-
.importActual<typeof import("../runtime-api.js")>("../runtime-api.js")
|
|
10
|
-
.then((actual) => ({
|
|
11
|
-
...actual,
|
|
12
|
-
fetchWithSsrFGuard,
|
|
13
|
-
}));
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
vi.mock("node:fs", () => {
|
|
17
|
-
return vi.importActual<typeof import("node:fs")>("node:fs").then((actual) => ({
|
|
18
|
-
...actual,
|
|
19
|
-
readFileSync,
|
|
20
|
-
}));
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
afterEach(() => {
|
|
24
|
-
fetchWithSsrFGuard.mockReset();
|
|
25
|
-
readFileSync.mockReset();
|
|
26
|
-
__testing.resetRoomCache();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe("nextcloud talk room info", () => {
|
|
30
|
-
it("resolves direct rooms from the room info endpoint", async () => {
|
|
31
|
-
const release = vi.fn(async () => {});
|
|
32
|
-
fetchWithSsrFGuard.mockResolvedValue({
|
|
33
|
-
response: {
|
|
34
|
-
ok: true,
|
|
35
|
-
json: async () => ({
|
|
36
|
-
ocs: {
|
|
37
|
-
data: {
|
|
38
|
-
type: 1,
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
}),
|
|
42
|
-
},
|
|
43
|
-
release,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const kind = await resolveNextcloudTalkRoomKind({
|
|
47
|
-
account: {
|
|
48
|
-
accountId: "acct-direct",
|
|
49
|
-
baseUrl: "https://nc.example.com",
|
|
50
|
-
config: {
|
|
51
|
-
apiUser: "bot",
|
|
52
|
-
apiPassword: "secret",
|
|
53
|
-
},
|
|
54
|
-
} as never,
|
|
55
|
-
roomToken: "room-direct",
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
expect(kind).toBe("direct");
|
|
59
|
-
expect(fetchWithSsrFGuard).toHaveBeenCalledWith(
|
|
60
|
-
expect.objectContaining({
|
|
61
|
-
url: "https://nc.example.com/ocs/v2.php/apps/spreed/api/v4/room/room-direct",
|
|
62
|
-
auditContext: "nextcloud-talk.room-info",
|
|
63
|
-
}),
|
|
64
|
-
);
|
|
65
|
-
expect(release).toHaveBeenCalledTimes(1);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("reads the api password from a file and logs non-ok room info responses", async () => {
|
|
69
|
-
const release = vi.fn(async () => {});
|
|
70
|
-
const log = vi.fn();
|
|
71
|
-
const error = vi.fn();
|
|
72
|
-
const exit = vi.fn();
|
|
73
|
-
readFileSync.mockReturnValue("file-secret\n");
|
|
74
|
-
fetchWithSsrFGuard.mockResolvedValue({
|
|
75
|
-
response: {
|
|
76
|
-
ok: false,
|
|
77
|
-
status: 403,
|
|
78
|
-
json: async () => ({}),
|
|
79
|
-
},
|
|
80
|
-
release,
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const kind = await resolveNextcloudTalkRoomKind({
|
|
84
|
-
account: {
|
|
85
|
-
accountId: "acct-group",
|
|
86
|
-
baseUrl: "https://nc.example.com",
|
|
87
|
-
config: {
|
|
88
|
-
apiUser: "bot",
|
|
89
|
-
apiPasswordFile: "/tmp/nextcloud-secret",
|
|
90
|
-
},
|
|
91
|
-
} as never,
|
|
92
|
-
roomToken: "room-group",
|
|
93
|
-
runtime: { log, error, exit },
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
expect(kind).toBeUndefined();
|
|
97
|
-
expect(readFileSync).toHaveBeenCalledWith("/tmp/nextcloud-secret", "utf-8");
|
|
98
|
-
expect(log).toHaveBeenCalledWith("nextcloud-talk: room lookup failed (403) token=room-group");
|
|
99
|
-
expect(release).toHaveBeenCalledTimes(1);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("returns undefined from room info without credentials or base url", async () => {
|
|
103
|
-
await expect(
|
|
104
|
-
resolveNextcloudTalkRoomKind({
|
|
105
|
-
account: {
|
|
106
|
-
accountId: "acct-missing",
|
|
107
|
-
baseUrl: "",
|
|
108
|
-
config: {},
|
|
109
|
-
} as never,
|
|
110
|
-
roomToken: "room-missing",
|
|
111
|
-
}),
|
|
112
|
-
).resolves.toBeUndefined();
|
|
113
|
-
|
|
114
|
-
expect(fetchWithSsrFGuard).not.toHaveBeenCalled();
|
|
115
|
-
});
|
|
116
|
-
});
|
package/src/room-info.ts
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
3
|
-
import { ssrfPolicyFromPrivateNetworkOptIn } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
4
|
-
import { fetchWithSsrFGuard, type RuntimeEnv } from "../runtime-api.js";
|
|
5
|
-
import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
|
|
6
|
-
import { normalizeResolvedSecretInputString } from "./secret-input.js";
|
|
7
|
-
|
|
8
|
-
const ROOM_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
9
|
-
const ROOM_CACHE_ERROR_TTL_MS = 30 * 1000;
|
|
10
|
-
|
|
11
|
-
const roomCache = new Map<
|
|
12
|
-
string,
|
|
13
|
-
{ kind?: "direct" | "group"; fetchedAt: number; error?: string }
|
|
14
|
-
>();
|
|
15
|
-
|
|
16
|
-
export const __testing = {
|
|
17
|
-
resetRoomCache() {
|
|
18
|
-
roomCache.clear();
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
function resolveRoomCacheKey(params: { accountId: string; roomToken: string }) {
|
|
23
|
-
return `${params.accountId}:${params.roomToken}`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function readApiPassword(params: {
|
|
27
|
-
apiPassword?: unknown;
|
|
28
|
-
apiPasswordFile?: string;
|
|
29
|
-
}): string | undefined {
|
|
30
|
-
const inlinePassword = normalizeResolvedSecretInputString({
|
|
31
|
-
value: params.apiPassword,
|
|
32
|
-
path: "channels.nextcloud-talk.apiPassword",
|
|
33
|
-
});
|
|
34
|
-
if (inlinePassword) {
|
|
35
|
-
return inlinePassword;
|
|
36
|
-
}
|
|
37
|
-
if (!params.apiPasswordFile) {
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
try {
|
|
41
|
-
const value = readFileSync(params.apiPasswordFile, "utf-8").trim();
|
|
42
|
-
return value || undefined;
|
|
43
|
-
} catch {
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function coerceRoomType(value: unknown): number | undefined {
|
|
49
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
50
|
-
return value;
|
|
51
|
-
}
|
|
52
|
-
if (typeof value === "string" && value.trim()) {
|
|
53
|
-
const parsed = Number.parseInt(value, 10);
|
|
54
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
55
|
-
}
|
|
56
|
-
return undefined;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function resolveRoomKindFromType(type: number | undefined): "direct" | "group" | undefined {
|
|
60
|
-
if (!type) {
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
if (type === 1 || type === 5 || type === 6) {
|
|
64
|
-
return "direct";
|
|
65
|
-
}
|
|
66
|
-
return "group";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export async function resolveNextcloudTalkRoomKind(params: {
|
|
70
|
-
account: ResolvedNextcloudTalkAccount;
|
|
71
|
-
roomToken: string;
|
|
72
|
-
runtime?: RuntimeEnv;
|
|
73
|
-
}): Promise<"direct" | "group" | undefined> {
|
|
74
|
-
const { account, roomToken, runtime } = params;
|
|
75
|
-
const key = resolveRoomCacheKey({ accountId: account.accountId, roomToken });
|
|
76
|
-
const cached = roomCache.get(key);
|
|
77
|
-
if (cached) {
|
|
78
|
-
const age = Date.now() - cached.fetchedAt;
|
|
79
|
-
if (cached.kind && age < ROOM_CACHE_TTL_MS) {
|
|
80
|
-
return cached.kind;
|
|
81
|
-
}
|
|
82
|
-
if (cached.error && age < ROOM_CACHE_ERROR_TTL_MS) {
|
|
83
|
-
return undefined;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const apiUser = account.config.apiUser?.trim();
|
|
88
|
-
const apiPassword = readApiPassword({
|
|
89
|
-
apiPassword: account.config.apiPassword,
|
|
90
|
-
apiPasswordFile: account.config.apiPasswordFile,
|
|
91
|
-
});
|
|
92
|
-
if (!apiUser || !apiPassword) {
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const baseUrl = account.baseUrl?.trim();
|
|
97
|
-
if (!baseUrl) {
|
|
98
|
-
return undefined;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v4/room/${roomToken}`;
|
|
102
|
-
const auth = Buffer.from(`${apiUser}:${apiPassword}`, "utf-8").toString("base64");
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
const { response, release } = await fetchWithSsrFGuard({
|
|
106
|
-
url,
|
|
107
|
-
init: {
|
|
108
|
-
method: "GET",
|
|
109
|
-
headers: {
|
|
110
|
-
Authorization: `Basic ${auth}`,
|
|
111
|
-
"OCS-APIRequest": "true",
|
|
112
|
-
Accept: "application/json",
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
auditContext: "nextcloud-talk.room-info",
|
|
116
|
-
policy: ssrfPolicyFromPrivateNetworkOptIn(account.config),
|
|
117
|
-
});
|
|
118
|
-
try {
|
|
119
|
-
if (!response.ok) {
|
|
120
|
-
roomCache.set(key, {
|
|
121
|
-
fetchedAt: Date.now(),
|
|
122
|
-
error: `status:${response.status}`,
|
|
123
|
-
});
|
|
124
|
-
runtime?.log?.(
|
|
125
|
-
`nextcloud-talk: room lookup failed (${response.status}) token=${roomToken}`,
|
|
126
|
-
);
|
|
127
|
-
return undefined;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const payload = (await response.json()) as {
|
|
131
|
-
ocs?: { data?: { type?: number | string } };
|
|
132
|
-
};
|
|
133
|
-
const type = coerceRoomType(payload.ocs?.data?.type);
|
|
134
|
-
const kind = resolveRoomKindFromType(type);
|
|
135
|
-
roomCache.set(key, { fetchedAt: Date.now(), kind });
|
|
136
|
-
return kind;
|
|
137
|
-
} finally {
|
|
138
|
-
await release();
|
|
139
|
-
}
|
|
140
|
-
} catch (err) {
|
|
141
|
-
roomCache.set(key, {
|
|
142
|
-
fetchedAt: Date.now(),
|
|
143
|
-
error: formatErrorMessage(err),
|
|
144
|
-
});
|
|
145
|
-
runtime?.error?.(`nextcloud-talk: room lookup error: ${String(err)}`);
|
|
146
|
-
return undefined;
|
|
147
|
-
}
|
|
148
|
-
}
|
package/src/runtime.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
2
|
-
import type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
|
|
3
|
-
|
|
4
|
-
const { setRuntime: setNextcloudTalkRuntime, getRuntime: getNextcloudTalkRuntime } =
|
|
5
|
-
createPluginRuntimeStore<PluginRuntime>({
|
|
6
|
-
pluginId: "nextcloud-talk",
|
|
7
|
-
errorMessage: "Nextcloud Talk runtime not initialized",
|
|
8
|
-
});
|
|
9
|
-
export { getNextcloudTalkRuntime, setNextcloudTalkRuntime };
|
package/src/secret-contract.ts
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
collectConditionalChannelFieldAssignments,
|
|
3
|
-
getChannelSurface,
|
|
4
|
-
hasOwnProperty,
|
|
5
|
-
type ChannelAccountEntry,
|
|
6
|
-
type ResolverContext,
|
|
7
|
-
type SecretDefaults,
|
|
8
|
-
} from "openclaw/plugin-sdk/channel-secret-basic-runtime";
|
|
9
|
-
|
|
10
|
-
export const secretTargetRegistryEntries: import("openclaw/plugin-sdk/channel-secret-basic-runtime").SecretTargetRegistryEntry[] =
|
|
11
|
-
[
|
|
12
|
-
{
|
|
13
|
-
id: "channels.nextcloud-talk.accounts.*.apiPassword",
|
|
14
|
-
targetType: "channels.nextcloud-talk.accounts.*.apiPassword",
|
|
15
|
-
configFile: "openclaw.json",
|
|
16
|
-
pathPattern: "channels.nextcloud-talk.accounts.*.apiPassword",
|
|
17
|
-
secretShape: "secret_input",
|
|
18
|
-
expectedResolvedValue: "string",
|
|
19
|
-
includeInPlan: true,
|
|
20
|
-
includeInConfigure: true,
|
|
21
|
-
includeInAudit: true,
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
id: "channels.nextcloud-talk.accounts.*.botSecret",
|
|
25
|
-
targetType: "channels.nextcloud-talk.accounts.*.botSecret",
|
|
26
|
-
configFile: "openclaw.json",
|
|
27
|
-
pathPattern: "channels.nextcloud-talk.accounts.*.botSecret",
|
|
28
|
-
secretShape: "secret_input",
|
|
29
|
-
expectedResolvedValue: "string",
|
|
30
|
-
includeInPlan: true,
|
|
31
|
-
includeInConfigure: true,
|
|
32
|
-
includeInAudit: true,
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
id: "channels.nextcloud-talk.apiPassword",
|
|
36
|
-
targetType: "channels.nextcloud-talk.apiPassword",
|
|
37
|
-
configFile: "openclaw.json",
|
|
38
|
-
pathPattern: "channels.nextcloud-talk.apiPassword",
|
|
39
|
-
secretShape: "secret_input",
|
|
40
|
-
expectedResolvedValue: "string",
|
|
41
|
-
includeInPlan: true,
|
|
42
|
-
includeInConfigure: true,
|
|
43
|
-
includeInAudit: true,
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
id: "channels.nextcloud-talk.botSecret",
|
|
47
|
-
targetType: "channels.nextcloud-talk.botSecret",
|
|
48
|
-
configFile: "openclaw.json",
|
|
49
|
-
pathPattern: "channels.nextcloud-talk.botSecret",
|
|
50
|
-
secretShape: "secret_input",
|
|
51
|
-
expectedResolvedValue: "string",
|
|
52
|
-
includeInPlan: true,
|
|
53
|
-
includeInConfigure: true,
|
|
54
|
-
includeInAudit: true,
|
|
55
|
-
},
|
|
56
|
-
];
|
|
57
|
-
|
|
58
|
-
export function collectRuntimeConfigAssignments(params: {
|
|
59
|
-
config: { channels?: Record<string, unknown> };
|
|
60
|
-
defaults?: SecretDefaults;
|
|
61
|
-
context: ResolverContext;
|
|
62
|
-
}): void {
|
|
63
|
-
const resolved = getChannelSurface(params.config, "nextcloud-talk");
|
|
64
|
-
if (!resolved) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
const { channel: nextcloudTalk, surface } = resolved;
|
|
68
|
-
const inheritsField =
|
|
69
|
-
(field: string) =>
|
|
70
|
-
({ account, enabled }: ChannelAccountEntry) =>
|
|
71
|
-
enabled && !hasOwnProperty(account, field);
|
|
72
|
-
collectConditionalChannelFieldAssignments({
|
|
73
|
-
channelKey: "nextcloud-talk",
|
|
74
|
-
field: "botSecret",
|
|
75
|
-
channel: nextcloudTalk,
|
|
76
|
-
surface,
|
|
77
|
-
defaults: params.defaults,
|
|
78
|
-
context: params.context,
|
|
79
|
-
topLevelActiveWithoutAccounts: true,
|
|
80
|
-
topLevelInheritedAccountActive: inheritsField("botSecret"),
|
|
81
|
-
accountActive: ({ enabled }) => enabled,
|
|
82
|
-
topInactiveReason: "no enabled Nextcloud Talk surface inherits this top-level botSecret.",
|
|
83
|
-
accountInactiveReason: "Nextcloud Talk account is disabled.",
|
|
84
|
-
});
|
|
85
|
-
collectConditionalChannelFieldAssignments({
|
|
86
|
-
channelKey: "nextcloud-talk",
|
|
87
|
-
field: "apiPassword",
|
|
88
|
-
channel: nextcloudTalk,
|
|
89
|
-
surface,
|
|
90
|
-
defaults: params.defaults,
|
|
91
|
-
context: params.context,
|
|
92
|
-
topLevelActiveWithoutAccounts: true,
|
|
93
|
-
topLevelInheritedAccountActive: inheritsField("apiPassword"),
|
|
94
|
-
accountActive: ({ enabled }) => enabled,
|
|
95
|
-
topInactiveReason: "no enabled Nextcloud Talk surface inherits this top-level apiPassword.",
|
|
96
|
-
accountInactiveReason: "Nextcloud Talk account is disabled.",
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export const channelSecrets = {
|
|
101
|
-
secretTargetRegistryEntries,
|
|
102
|
-
collectRuntimeConfigAssignments,
|
|
103
|
-
};
|
package/src/secret-input.ts
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createSendCfgThreadingRuntime,
|
|
3
|
-
expectProvidedCfgSkipsRuntimeLoad,
|
|
4
|
-
} from "openclaw/plugin-sdk/channel-test-helpers";
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
|
|
7
|
-
const hoisted = vi.hoisted(() => ({
|
|
8
|
-
loadConfig: vi.fn(),
|
|
9
|
-
resolveMarkdownTableMode: vi.fn(() => "preserve"),
|
|
10
|
-
convertMarkdownTables: vi.fn((text: string) => text),
|
|
11
|
-
record: vi.fn(),
|
|
12
|
-
resolveNextcloudTalkAccount: vi.fn(),
|
|
13
|
-
ssrfPolicyFromPrivateNetworkOptIn: vi.fn(() => undefined),
|
|
14
|
-
generateNextcloudTalkSignature: vi.fn(() => ({
|
|
15
|
-
random: "r",
|
|
16
|
-
signature: "s",
|
|
17
|
-
})),
|
|
18
|
-
mockFetchGuard: vi.fn(),
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
vi.mock("./send.runtime.js", () => {
|
|
22
|
-
return {
|
|
23
|
-
convertMarkdownTables: hoisted.convertMarkdownTables,
|
|
24
|
-
fetchWithSsrFGuard: hoisted.mockFetchGuard,
|
|
25
|
-
generateNextcloudTalkSignature: hoisted.generateNextcloudTalkSignature,
|
|
26
|
-
getNextcloudTalkRuntime: () => createSendCfgThreadingRuntime(hoisted),
|
|
27
|
-
requireRuntimeConfig: (cfg: unknown, context: string) => {
|
|
28
|
-
if (cfg) {
|
|
29
|
-
return cfg;
|
|
30
|
-
}
|
|
31
|
-
throw new Error(`${context} requires a resolved runtime config`);
|
|
32
|
-
},
|
|
33
|
-
resolveNextcloudTalkAccount: hoisted.resolveNextcloudTalkAccount,
|
|
34
|
-
resolveMarkdownTableMode: hoisted.resolveMarkdownTableMode,
|
|
35
|
-
ssrfPolicyFromPrivateNetworkOptIn: hoisted.ssrfPolicyFromPrivateNetworkOptIn,
|
|
36
|
-
};
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const { sendMessageNextcloudTalk, sendReactionNextcloudTalk } = await import("./send.js");
|
|
40
|
-
|
|
41
|
-
function expectProvidedMessageCfgThreading(cfg: unknown): void {
|
|
42
|
-
expectProvidedCfgSkipsRuntimeLoad({
|
|
43
|
-
loadConfig: hoisted.loadConfig,
|
|
44
|
-
resolveAccount: hoisted.resolveNextcloudTalkAccount,
|
|
45
|
-
cfg,
|
|
46
|
-
accountId: "work",
|
|
47
|
-
});
|
|
48
|
-
expect(hoisted.resolveMarkdownTableMode).toHaveBeenCalledWith({
|
|
49
|
-
cfg,
|
|
50
|
-
channel: "nextcloud-talk",
|
|
51
|
-
accountId: "default",
|
|
52
|
-
});
|
|
53
|
-
expect(hoisted.convertMarkdownTables).toHaveBeenCalledWith("hello", "preserve");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
describe("nextcloud-talk send cfg threading", () => {
|
|
57
|
-
const fetchMock = vi.fn<typeof fetch>();
|
|
58
|
-
const defaultAccount = {
|
|
59
|
-
accountId: "default",
|
|
60
|
-
baseUrl: "https://nextcloud.example.com",
|
|
61
|
-
secret: "secret-value",
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
function mockNextcloudMessageResponse(messageId: number, timestamp: number): void {
|
|
65
|
-
fetchMock.mockResolvedValueOnce(
|
|
66
|
-
new Response(
|
|
67
|
-
JSON.stringify({
|
|
68
|
-
ocs: { data: { id: messageId, timestamp } },
|
|
69
|
-
}),
|
|
70
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
71
|
-
),
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
beforeEach(() => {
|
|
76
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
77
|
-
// Route the SSRF guard mock through the global fetch mock.
|
|
78
|
-
hoisted.mockFetchGuard.mockImplementation(async (p: { url: string; init?: RequestInit }) => {
|
|
79
|
-
const response = await globalThis.fetch(p.url, p.init);
|
|
80
|
-
return { response, release: async () => {}, finalUrl: p.url };
|
|
81
|
-
});
|
|
82
|
-
hoisted.loadConfig.mockReset();
|
|
83
|
-
hoisted.resolveMarkdownTableMode.mockClear();
|
|
84
|
-
hoisted.convertMarkdownTables.mockClear();
|
|
85
|
-
hoisted.record.mockReset();
|
|
86
|
-
hoisted.ssrfPolicyFromPrivateNetworkOptIn.mockClear();
|
|
87
|
-
hoisted.generateNextcloudTalkSignature.mockClear();
|
|
88
|
-
hoisted.resolveNextcloudTalkAccount.mockReset();
|
|
89
|
-
hoisted.resolveNextcloudTalkAccount.mockReturnValue(defaultAccount);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
afterEach(() => {
|
|
93
|
-
fetchMock.mockReset();
|
|
94
|
-
hoisted.mockFetchGuard.mockReset();
|
|
95
|
-
vi.unstubAllGlobals();
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("uses provided cfg for sendMessage and skips runtime loadConfig", async () => {
|
|
99
|
-
const cfg = { source: "provided" } as const;
|
|
100
|
-
mockNextcloudMessageResponse(12345, 1_706_000_000);
|
|
101
|
-
|
|
102
|
-
const result = await sendMessageNextcloudTalk("room:abc123", "hello", {
|
|
103
|
-
cfg,
|
|
104
|
-
accountId: "work",
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
expectProvidedMessageCfgThreading(cfg);
|
|
108
|
-
expect(hoisted.record).toHaveBeenCalledWith({
|
|
109
|
-
channel: "nextcloud-talk",
|
|
110
|
-
accountId: "default",
|
|
111
|
-
direction: "outbound",
|
|
112
|
-
});
|
|
113
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
114
|
-
expect(result).toEqual({
|
|
115
|
-
messageId: "12345",
|
|
116
|
-
roomToken: "abc123",
|
|
117
|
-
timestamp: 1_706_000_000,
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("sends with provided cfg even when the runtime store is not initialized", async () => {
|
|
122
|
-
const cfg = { source: "provided" } as const;
|
|
123
|
-
hoisted.record.mockImplementation(() => {
|
|
124
|
-
throw new Error("Nextcloud Talk runtime not initialized");
|
|
125
|
-
});
|
|
126
|
-
mockNextcloudMessageResponse(12346, 1_706_000_001);
|
|
127
|
-
|
|
128
|
-
const result = await sendMessageNextcloudTalk("room:abc123", "hello", {
|
|
129
|
-
cfg,
|
|
130
|
-
accountId: "work",
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
expectProvidedMessageCfgThreading(cfg);
|
|
134
|
-
expect(result).toEqual({
|
|
135
|
-
messageId: "12346",
|
|
136
|
-
roomToken: "abc123",
|
|
137
|
-
timestamp: 1_706_000_001,
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("fails hard for sendReaction when cfg is omitted", async () => {
|
|
142
|
-
fetchMock.mockResolvedValueOnce(new Response("{}", { status: 200 }));
|
|
143
|
-
|
|
144
|
-
await expect(
|
|
145
|
-
sendReactionNextcloudTalk("room:ops", "m-1", "👍", {
|
|
146
|
-
accountId: "default",
|
|
147
|
-
} as never),
|
|
148
|
-
).rejects.toThrow("Nextcloud Talk send requires a resolved runtime config");
|
|
149
|
-
|
|
150
|
-
expect(hoisted.loadConfig).not.toHaveBeenCalled();
|
|
151
|
-
expect(hoisted.resolveNextcloudTalkAccount).not.toHaveBeenCalled();
|
|
152
|
-
});
|
|
153
|
-
});
|
package/src/send.runtime.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
2
|
-
export { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
|
3
|
-
export { ssrfPolicyFromPrivateNetworkOptIn } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
4
|
-
export { convertMarkdownTables } from "openclaw/plugin-sdk/text-runtime";
|
|
5
|
-
export { fetchWithSsrFGuard } from "../runtime-api.js";
|
|
6
|
-
export { resolveNextcloudTalkAccount } from "./accounts.js";
|
|
7
|
-
export { getNextcloudTalkRuntime } from "./runtime.js";
|
|
8
|
-
export { generateNextcloudTalkSignature } from "./signature.js";
|