@openclaw/synology-chat 2026.2.22 → 2026.5.2-beta.1
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/api.ts +3 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +1 -0
- package/index.ts +11 -12
- package/openclaw.plugin.json +35 -1
- package/package.json +20 -5
- package/setup-api.ts +1 -0
- package/setup-entry.ts +9 -0
- package/src/accounts.ts +107 -43
- package/src/approval-auth.test.ts +17 -0
- package/src/approval-auth.ts +22 -0
- package/src/channel.integration.test.ts +191 -0
- package/src/channel.test-mocks.ts +176 -0
- package/src/channel.test.ts +471 -174
- package/src/channel.ts +327 -278
- package/src/client.test.ts +296 -44
- package/src/client.ts +217 -24
- package/src/config-schema.ts +11 -0
- package/src/core.test.ts +373 -0
- package/src/gateway-runtime.ts +212 -0
- package/src/inbound-context.ts +10 -0
- package/src/inbound-turn.ts +171 -0
- package/src/runtime.ts +8 -20
- package/src/security-audit.test.ts +66 -0
- package/src/security-audit.ts +28 -0
- package/src/security.ts +67 -53
- package/src/session-key.ts +21 -0
- package/src/setup-surface.ts +338 -0
- package/src/test-http-utils.ts +75 -0
- package/src/types.ts +13 -14
- package/src/webhook-handler.test.ts +418 -75
- package/src/webhook-handler.ts +565 -138
- package/tsconfig.json +16 -0
- package/src/accounts.test.ts +0 -133
- package/src/security.test.ts +0 -98
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAllowFromSection,
|
|
3
|
+
createStandardChannelSetupStatus,
|
|
4
|
+
DEFAULT_ACCOUNT_ID,
|
|
5
|
+
formatDocsLink,
|
|
6
|
+
mergeAllowFromEntries,
|
|
7
|
+
normalizeAccountId,
|
|
8
|
+
setSetupChannelEnabled,
|
|
9
|
+
splitSetupEntries,
|
|
10
|
+
type ChannelSetupAdapter,
|
|
11
|
+
type ChannelSetupWizard,
|
|
12
|
+
type OpenClawConfig,
|
|
13
|
+
} from "openclaw/plugin-sdk/setup";
|
|
14
|
+
import { listAccountIds, resolveAccount } from "./accounts.js";
|
|
15
|
+
import type { SynologyChatAccountRaw, SynologyChatChannelConfig } from "./types.js";
|
|
16
|
+
|
|
17
|
+
const channel = "synology-chat" as const;
|
|
18
|
+
const DEFAULT_WEBHOOK_PATH = "/webhook/synology";
|
|
19
|
+
|
|
20
|
+
const SYNOLOGY_SETUP_HELP_LINES = [
|
|
21
|
+
"1) Create an incoming webhook in Synology Chat and copy its URL",
|
|
22
|
+
"2) Create an outgoing webhook and copy its secret token",
|
|
23
|
+
`3) Point the outgoing webhook to https://<gateway-host>${DEFAULT_WEBHOOK_PATH}`,
|
|
24
|
+
"4) Keep allowed user IDs handy for DM allowlisting",
|
|
25
|
+
`Docs: ${formatDocsLink("/channels/synology-chat", "channels/synology-chat")}`,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const SYNOLOGY_ALLOW_FROM_HELP_LINES = [
|
|
29
|
+
"Allowlist Synology Chat DMs by numeric user id.",
|
|
30
|
+
"Examples:",
|
|
31
|
+
"- 123456",
|
|
32
|
+
"- synology-chat:123456",
|
|
33
|
+
"Multiple entries: comma-separated.",
|
|
34
|
+
`Docs: ${formatDocsLink("/channels/synology-chat", "channels/synology-chat")}`,
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function normalizeOptionalString(value: unknown): string | undefined {
|
|
38
|
+
if (typeof value !== "string") {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const trimmed = value.trim();
|
|
42
|
+
return trimmed || undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getChannelConfig(cfg: OpenClawConfig): SynologyChatChannelConfig {
|
|
46
|
+
return (cfg.channels?.[channel] as SynologyChatChannelConfig | undefined) ?? {};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getRawAccountConfig(cfg: OpenClawConfig, accountId: string): SynologyChatAccountRaw {
|
|
50
|
+
const channelConfig = getChannelConfig(cfg);
|
|
51
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
52
|
+
return channelConfig;
|
|
53
|
+
}
|
|
54
|
+
return channelConfig.accounts?.[accountId] ?? {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function patchSynologyChatAccountConfig(params: {
|
|
58
|
+
cfg: OpenClawConfig;
|
|
59
|
+
accountId: string;
|
|
60
|
+
patch: Record<string, unknown>;
|
|
61
|
+
clearFields?: string[];
|
|
62
|
+
enabled?: boolean;
|
|
63
|
+
}): OpenClawConfig {
|
|
64
|
+
const channelConfig = getChannelConfig(params.cfg);
|
|
65
|
+
if (params.accountId === DEFAULT_ACCOUNT_ID) {
|
|
66
|
+
const nextChannelConfig = { ...channelConfig } as Record<string, unknown>;
|
|
67
|
+
for (const field of params.clearFields ?? []) {
|
|
68
|
+
delete nextChannelConfig[field];
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
...params.cfg,
|
|
72
|
+
channels: {
|
|
73
|
+
...params.cfg.channels,
|
|
74
|
+
[channel]: {
|
|
75
|
+
...nextChannelConfig,
|
|
76
|
+
...(params.enabled ? { enabled: true } : {}),
|
|
77
|
+
...params.patch,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const nextAccounts = { ...channelConfig.accounts } as Record<string, Record<string, unknown>>;
|
|
84
|
+
const nextAccountConfig = { ...nextAccounts[params.accountId] };
|
|
85
|
+
for (const field of params.clearFields ?? []) {
|
|
86
|
+
delete nextAccountConfig[field];
|
|
87
|
+
}
|
|
88
|
+
nextAccounts[params.accountId] = {
|
|
89
|
+
...nextAccountConfig,
|
|
90
|
+
...(params.enabled ? { enabled: true } : {}),
|
|
91
|
+
...params.patch,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
...params.cfg,
|
|
96
|
+
channels: {
|
|
97
|
+
...params.cfg.channels,
|
|
98
|
+
[channel]: {
|
|
99
|
+
...channelConfig,
|
|
100
|
+
...(params.enabled ? { enabled: true } : {}),
|
|
101
|
+
accounts: nextAccounts,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isSynologyChatConfigured(cfg: OpenClawConfig, accountId: string): boolean {
|
|
108
|
+
const account = resolveAccount(cfg, accountId);
|
|
109
|
+
return Boolean(account.token.trim() && account.incomingUrl.trim());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function validateWebhookUrl(value: string): string | undefined {
|
|
113
|
+
try {
|
|
114
|
+
const parsed = new URL(value);
|
|
115
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
116
|
+
return "Incoming webhook must use http:// or https://.";
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
return "Incoming webhook must be a valid URL.";
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function validateWebhookPath(value: string): string | undefined {
|
|
125
|
+
const trimmed = value.trim();
|
|
126
|
+
if (!trimmed) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
return trimmed.startsWith("/") ? undefined : "Webhook path must start with /.";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseSynologyUserId(value: string): string | null {
|
|
133
|
+
const cleaned = value.replace(/^synology(?:[-_]?chat)?:/i, "").trim();
|
|
134
|
+
return /^\d+$/.test(cleaned) ? cleaned : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeSynologyAllowedUserId(value: unknown): string {
|
|
138
|
+
if (
|
|
139
|
+
typeof value === "string" ||
|
|
140
|
+
typeof value === "number" ||
|
|
141
|
+
typeof value === "boolean" ||
|
|
142
|
+
typeof value === "bigint"
|
|
143
|
+
) {
|
|
144
|
+
return `${value}`.trim();
|
|
145
|
+
}
|
|
146
|
+
return "";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resolveExistingAllowedUserIds(cfg: OpenClawConfig, accountId: string): string[] {
|
|
150
|
+
const raw = getRawAccountConfig(cfg, accountId).allowedUserIds;
|
|
151
|
+
if (Array.isArray(raw)) {
|
|
152
|
+
return raw.map(normalizeSynologyAllowedUserId).filter(Boolean);
|
|
153
|
+
}
|
|
154
|
+
return normalizeSynologyAllowedUserId(raw)
|
|
155
|
+
.split(",")
|
|
156
|
+
.map((value) => value.trim())
|
|
157
|
+
.filter(Boolean);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const synologyChatSetupAdapter: ChannelSetupAdapter = {
|
|
161
|
+
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID,
|
|
162
|
+
validateInput: ({ accountId, input }) => {
|
|
163
|
+
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
|
|
164
|
+
return "Synology Chat env credentials only support the default account.";
|
|
165
|
+
}
|
|
166
|
+
if (!input.useEnv && !input.token?.trim()) {
|
|
167
|
+
return "Synology Chat requires --token or --use-env.";
|
|
168
|
+
}
|
|
169
|
+
if (!input.url?.trim()) {
|
|
170
|
+
return "Synology Chat requires --url for the incoming webhook.";
|
|
171
|
+
}
|
|
172
|
+
const urlError = validateWebhookUrl(input.url.trim());
|
|
173
|
+
if (urlError) {
|
|
174
|
+
return urlError;
|
|
175
|
+
}
|
|
176
|
+
if (input.webhookPath?.trim()) {
|
|
177
|
+
return validateWebhookPath(input.webhookPath.trim()) ?? null;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
},
|
|
181
|
+
applyAccountConfig: ({ cfg, accountId, input }) =>
|
|
182
|
+
patchSynologyChatAccountConfig({
|
|
183
|
+
cfg,
|
|
184
|
+
accountId,
|
|
185
|
+
enabled: true,
|
|
186
|
+
clearFields: input.useEnv ? ["token"] : undefined,
|
|
187
|
+
patch: {
|
|
188
|
+
...(input.useEnv ? {} : { token: input.token?.trim() }),
|
|
189
|
+
incomingUrl: input.url?.trim(),
|
|
190
|
+
...(input.webhookPath?.trim() ? { webhookPath: input.webhookPath.trim() } : {}),
|
|
191
|
+
},
|
|
192
|
+
}),
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const synologyChatSetupWizard: ChannelSetupWizard = {
|
|
196
|
+
channel,
|
|
197
|
+
status: createStandardChannelSetupStatus({
|
|
198
|
+
channelLabel: "Synology Chat",
|
|
199
|
+
configuredLabel: "configured",
|
|
200
|
+
unconfiguredLabel: "needs token + incoming webhook",
|
|
201
|
+
configuredHint: "configured",
|
|
202
|
+
unconfiguredHint: "needs token + incoming webhook",
|
|
203
|
+
configuredScore: 1,
|
|
204
|
+
unconfiguredScore: 0,
|
|
205
|
+
includeStatusLine: true,
|
|
206
|
+
resolveConfigured: ({ cfg, accountId }) =>
|
|
207
|
+
accountId
|
|
208
|
+
? isSynologyChatConfigured(cfg, accountId)
|
|
209
|
+
: listAccountIds(cfg).some((candidateAccountId) =>
|
|
210
|
+
isSynologyChatConfigured(cfg, candidateAccountId),
|
|
211
|
+
),
|
|
212
|
+
resolveExtraStatusLines: ({ cfg }) => [`Accounts: ${listAccountIds(cfg).length || 0}`],
|
|
213
|
+
}),
|
|
214
|
+
introNote: {
|
|
215
|
+
title: "Synology Chat webhook setup",
|
|
216
|
+
lines: SYNOLOGY_SETUP_HELP_LINES,
|
|
217
|
+
},
|
|
218
|
+
credentials: [
|
|
219
|
+
{
|
|
220
|
+
inputKey: "token",
|
|
221
|
+
providerHint: channel,
|
|
222
|
+
credentialLabel: "outgoing webhook token",
|
|
223
|
+
preferredEnvVar: "SYNOLOGY_CHAT_TOKEN",
|
|
224
|
+
helpTitle: "Synology Chat webhook token",
|
|
225
|
+
helpLines: SYNOLOGY_SETUP_HELP_LINES,
|
|
226
|
+
envPrompt: "SYNOLOGY_CHAT_TOKEN detected. Use env var?",
|
|
227
|
+
keepPrompt: "Synology Chat webhook token already configured. Keep it?",
|
|
228
|
+
inputPrompt: "Enter Synology Chat outgoing webhook token",
|
|
229
|
+
allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID,
|
|
230
|
+
inspect: ({ cfg, accountId }) => {
|
|
231
|
+
const account = resolveAccount(cfg, accountId);
|
|
232
|
+
const raw = getRawAccountConfig(cfg, accountId);
|
|
233
|
+
return {
|
|
234
|
+
accountConfigured: isSynologyChatConfigured(cfg, accountId),
|
|
235
|
+
hasConfiguredValue: Boolean(normalizeOptionalString(raw.token)),
|
|
236
|
+
resolvedValue: normalizeOptionalString(account.token),
|
|
237
|
+
envValue:
|
|
238
|
+
accountId === DEFAULT_ACCOUNT_ID
|
|
239
|
+
? normalizeOptionalString(process.env.SYNOLOGY_CHAT_TOKEN)
|
|
240
|
+
: undefined,
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
applyUseEnv: async ({ cfg, accountId }) =>
|
|
244
|
+
patchSynologyChatAccountConfig({
|
|
245
|
+
cfg,
|
|
246
|
+
accountId,
|
|
247
|
+
enabled: true,
|
|
248
|
+
clearFields: ["token"],
|
|
249
|
+
patch: {},
|
|
250
|
+
}),
|
|
251
|
+
applySet: async ({ cfg, accountId, resolvedValue }) =>
|
|
252
|
+
patchSynologyChatAccountConfig({
|
|
253
|
+
cfg,
|
|
254
|
+
accountId,
|
|
255
|
+
enabled: true,
|
|
256
|
+
patch: { token: resolvedValue },
|
|
257
|
+
}),
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
textInputs: [
|
|
261
|
+
{
|
|
262
|
+
inputKey: "url",
|
|
263
|
+
message: "Incoming webhook URL",
|
|
264
|
+
placeholder:
|
|
265
|
+
"https://nas.example.com/webapi/entry.cgi?api=SYNO.Chat.External&method=incoming...",
|
|
266
|
+
helpTitle: "Synology Chat incoming webhook",
|
|
267
|
+
helpLines: [
|
|
268
|
+
"Use the incoming webhook URL from Synology Chat integrations.",
|
|
269
|
+
"This is the URL OpenClaw uses to send replies back to Chat.",
|
|
270
|
+
],
|
|
271
|
+
currentValue: ({ cfg, accountId }) => getRawAccountConfig(cfg, accountId).incomingUrl?.trim(),
|
|
272
|
+
keepPrompt: (value) => `Incoming webhook URL set (${value}). Keep it?`,
|
|
273
|
+
validate: ({ value }) => validateWebhookUrl(value),
|
|
274
|
+
applySet: async ({ cfg, accountId, value }) =>
|
|
275
|
+
patchSynologyChatAccountConfig({
|
|
276
|
+
cfg,
|
|
277
|
+
accountId,
|
|
278
|
+
enabled: true,
|
|
279
|
+
patch: { incomingUrl: value.trim() },
|
|
280
|
+
}),
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
inputKey: "webhookPath",
|
|
284
|
+
message: "Outgoing webhook path (optional)",
|
|
285
|
+
placeholder: DEFAULT_WEBHOOK_PATH,
|
|
286
|
+
required: false,
|
|
287
|
+
applyEmptyValue: true,
|
|
288
|
+
helpTitle: "Synology Chat outgoing webhook path",
|
|
289
|
+
helpLines: [
|
|
290
|
+
`Default path: ${DEFAULT_WEBHOOK_PATH}`,
|
|
291
|
+
"Change this only if you need multiple Synology Chat webhook routes.",
|
|
292
|
+
],
|
|
293
|
+
currentValue: ({ cfg, accountId }) => getRawAccountConfig(cfg, accountId).webhookPath?.trim(),
|
|
294
|
+
keepPrompt: (value) => `Outgoing webhook path set (${value}). Keep it?`,
|
|
295
|
+
validate: ({ value }) => validateWebhookPath(value),
|
|
296
|
+
applySet: async ({ cfg, accountId, value }) =>
|
|
297
|
+
patchSynologyChatAccountConfig({
|
|
298
|
+
cfg,
|
|
299
|
+
accountId,
|
|
300
|
+
enabled: true,
|
|
301
|
+
clearFields: value.trim() ? undefined : ["webhookPath"],
|
|
302
|
+
patch: value.trim() ? { webhookPath: value.trim() } : {},
|
|
303
|
+
}),
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
allowFrom: createAllowFromSection({
|
|
307
|
+
helpTitle: "Synology Chat allowlist",
|
|
308
|
+
helpLines: SYNOLOGY_ALLOW_FROM_HELP_LINES,
|
|
309
|
+
message: "Allowed Synology Chat user ids",
|
|
310
|
+
placeholder: "123456, 987654",
|
|
311
|
+
invalidWithoutCredentialNote: "Synology Chat user ids must be numeric.",
|
|
312
|
+
parseInputs: splitSetupEntries,
|
|
313
|
+
parseId: parseSynologyUserId,
|
|
314
|
+
apply: async ({ cfg, accountId, allowFrom }) =>
|
|
315
|
+
patchSynologyChatAccountConfig({
|
|
316
|
+
cfg,
|
|
317
|
+
accountId,
|
|
318
|
+
enabled: true,
|
|
319
|
+
patch: {
|
|
320
|
+
dmPolicy: "allowlist",
|
|
321
|
+
allowedUserIds: mergeAllowFromEntries(
|
|
322
|
+
resolveExistingAllowedUserIds(cfg, accountId),
|
|
323
|
+
allowFrom,
|
|
324
|
+
),
|
|
325
|
+
},
|
|
326
|
+
}),
|
|
327
|
+
}),
|
|
328
|
+
completionNote: {
|
|
329
|
+
title: "Synology Chat access control",
|
|
330
|
+
lines: [
|
|
331
|
+
`Default outgoing webhook path: ${DEFAULT_WEBHOOK_PATH}`,
|
|
332
|
+
'Set allowed user IDs, or manually switch `channels.synology-chat.dmPolicy` to `"open"` with `allowedUserIds: ["*"]` for public DMs.',
|
|
333
|
+
'With `dmPolicy="allowlist"`, an empty allowedUserIds list blocks the route from starting.',
|
|
334
|
+
`Docs: ${formatDocsLink("/channels/synology-chat", "channels/synology-chat")}`,
|
|
335
|
+
],
|
|
336
|
+
},
|
|
337
|
+
disable: (cfg) => setSetupChannelEnabled(cfg, channel, false),
|
|
338
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
|
|
4
|
+
function makeBaseReq(
|
|
5
|
+
method: string,
|
|
6
|
+
opts: { headers?: Record<string, string>; url?: string } = {},
|
|
7
|
+
): IncomingMessage & { destroyed: boolean } {
|
|
8
|
+
const req = new EventEmitter() as IncomingMessage & { destroyed: boolean };
|
|
9
|
+
req.method = method;
|
|
10
|
+
req.headers = opts.headers ?? {};
|
|
11
|
+
req.url = opts.url ?? "/webhook/synology";
|
|
12
|
+
req.socket = { remoteAddress: "127.0.0.1" } as unknown as IncomingMessage["socket"];
|
|
13
|
+
req.destroyed = false;
|
|
14
|
+
req.destroy = ((_: Error | undefined) => {
|
|
15
|
+
if (req.destroyed) {
|
|
16
|
+
return req;
|
|
17
|
+
}
|
|
18
|
+
req.destroyed = true;
|
|
19
|
+
return req;
|
|
20
|
+
}) as IncomingMessage["destroy"];
|
|
21
|
+
return req;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function makeReq(
|
|
25
|
+
method: string,
|
|
26
|
+
body: string,
|
|
27
|
+
opts: { headers?: Record<string, string>; url?: string } = {},
|
|
28
|
+
): IncomingMessage {
|
|
29
|
+
const req = makeBaseReq(method, opts);
|
|
30
|
+
process.nextTick(() => {
|
|
31
|
+
if (req.destroyed) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
req.emit("data", Buffer.from(body));
|
|
35
|
+
req.emit("end");
|
|
36
|
+
});
|
|
37
|
+
return req;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function makeStalledReq(
|
|
41
|
+
method: string,
|
|
42
|
+
opts: { headers?: Record<string, string>; url?: string } = {},
|
|
43
|
+
): IncomingMessage {
|
|
44
|
+
return makeBaseReq(method, opts);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function makeRes(): ServerResponse & { _status: number; _body: string } {
|
|
48
|
+
const res = {
|
|
49
|
+
_status: 0,
|
|
50
|
+
_body: "",
|
|
51
|
+
writeHead(statusCode: number, _headers: Record<string, string>) {
|
|
52
|
+
res._status = statusCode;
|
|
53
|
+
},
|
|
54
|
+
end(body?: string) {
|
|
55
|
+
res._body = body ?? "";
|
|
56
|
+
},
|
|
57
|
+
} as unknown as ServerResponse & { _status: number; _body: string };
|
|
58
|
+
Object.defineProperty(res, "statusCode", {
|
|
59
|
+
configurable: true,
|
|
60
|
+
enumerable: true,
|
|
61
|
+
get() {
|
|
62
|
+
return res._status;
|
|
63
|
+
},
|
|
64
|
+
set(value: number) {
|
|
65
|
+
res._status = value;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
return res;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function makeFormBody(fields: Record<string, string>): string {
|
|
72
|
+
return Object.entries(fields)
|
|
73
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
74
|
+
.join("&");
|
|
75
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -2,34 +2,30 @@
|
|
|
2
2
|
* Type definitions for the Synology Chat channel plugin.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
export interface SynologyChatChannelConfig {
|
|
5
|
+
type SynologyChatConfigFields = {
|
|
7
6
|
enabled?: boolean;
|
|
8
7
|
token?: string;
|
|
9
8
|
incomingUrl?: string;
|
|
10
9
|
nasHost?: string;
|
|
11
10
|
webhookPath?: string;
|
|
11
|
+
dangerouslyAllowNameMatching?: boolean;
|
|
12
|
+
dangerouslyAllowInheritedWebhookPath?: boolean;
|
|
12
13
|
dmPolicy?: "open" | "allowlist" | "disabled";
|
|
13
14
|
allowedUserIds?: string | string[];
|
|
14
15
|
rateLimitPerMinute?: number;
|
|
15
16
|
botName?: string;
|
|
16
17
|
allowInsecureSsl?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type SynologyWebhookPathSource = "default" | "inherited-base" | "explicit";
|
|
21
|
+
|
|
22
|
+
/** Raw channel config from openclaw.json channels.synology-chat */
|
|
23
|
+
export interface SynologyChatChannelConfig extends SynologyChatConfigFields {
|
|
17
24
|
accounts?: Record<string, SynologyChatAccountRaw>;
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
/** Raw per-account config (overrides base config) */
|
|
21
|
-
export interface SynologyChatAccountRaw {
|
|
22
|
-
enabled?: boolean;
|
|
23
|
-
token?: string;
|
|
24
|
-
incomingUrl?: string;
|
|
25
|
-
nasHost?: string;
|
|
26
|
-
webhookPath?: string;
|
|
27
|
-
dmPolicy?: "open" | "allowlist" | "disabled";
|
|
28
|
-
allowedUserIds?: string | string[];
|
|
29
|
-
rateLimitPerMinute?: number;
|
|
30
|
-
botName?: string;
|
|
31
|
-
allowInsecureSsl?: boolean;
|
|
32
|
-
}
|
|
28
|
+
export interface SynologyChatAccountRaw extends SynologyChatConfigFields {}
|
|
33
29
|
|
|
34
30
|
/** Fully resolved account config with defaults applied */
|
|
35
31
|
export interface ResolvedSynologyChatAccount {
|
|
@@ -39,6 +35,9 @@ export interface ResolvedSynologyChatAccount {
|
|
|
39
35
|
incomingUrl: string;
|
|
40
36
|
nasHost: string;
|
|
41
37
|
webhookPath: string;
|
|
38
|
+
webhookPathSource: SynologyWebhookPathSource;
|
|
39
|
+
dangerouslyAllowNameMatching: boolean;
|
|
40
|
+
dangerouslyAllowInheritedWebhookPath: boolean;
|
|
42
41
|
dmPolicy: "open" | "allowlist" | "disabled";
|
|
43
42
|
allowedUserIds: string[];
|
|
44
43
|
rateLimitPerMinute: number;
|