@openclaw/synology-chat 2026.5.7 → 2026.5.10-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/dist/api.js CHANGED
@@ -1,3 +1,3 @@
1
- import { n as setSynologyRuntime, t as synologyChatPlugin } from "./channel-BYl2GyR_.js";
1
+ import { n as setSynologyRuntime, t as synologyChatPlugin } from "./channel-IPqYKUxY.js";
2
2
  import { t as collectSynologyChatSecurityAuditFindings } from "./security-audit-Zu_nkF2x.js";
3
3
  export { collectSynologyChatSecurityAuditFindings, setSynologyRuntime, synologyChatPlugin };
@@ -1,25 +1,27 @@
1
- import { i as resolveAccount, n as synologyChatSetupWizard, r as listAccountIds, t as synologyChatSetupAdapter } from "./setup-surface-BTp4n9pb.js";
1
+ import { i as resolveAccount, n as synologyChatSetupWizard, r as listAccountIds, t as synologyChatSetupAdapter } from "./setup-surface-daP5yN4Y.js";
2
2
  import { t as collectSynologyChatSecurityAuditFindings } from "./security-audit-Zu_nkF2x.js";
3
3
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
4
4
  import { createHybridChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
5
5
  import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
6
6
  import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
7
+ import { createMessageReceiptFromOutboundResults, defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-message";
7
8
  import { composeWarningCollectors, createConditionalWarningCollector, projectAccountConfigWarningCollector, projectAccountWarningCollector } from "openclaw/plugin-sdk/channel-policy";
8
- import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result";
9
9
  import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
10
+ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
10
11
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1 } from "openclaw/plugin-sdk/account-resolution";
11
12
  import { createResolvedApproverActionAuthAdapter, resolveApprovalApprovers } from "openclaw/plugin-sdk/approval-auth-runtime";
12
13
  import * as http from "node:http";
13
14
  import * as https from "node:https";
14
15
  import { safeParseJsonWithSchema, safeParseWithSchema } from "openclaw/plugin-sdk/extension-shared";
16
+ import { sleep } from "openclaw/plugin-sdk/runtime-env";
15
17
  import { formatErrorMessage, resolvePinnedHostnameWithPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
16
- import { z } from "zod";
18
+ import { z } from "openclaw/plugin-sdk/zod";
17
19
  import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
18
- import { z as z$1 } from "openclaw/plugin-sdk/zod";
19
20
  import { beginWebhookRequestPipelineOrReject, createFixedWindowRateLimiter, createWebhookInFlightLimiter, isRequestBodyLimitError, readRequestBodyWithLimit, registerPluginHttpRoute, requestBodyErrorToText } from "openclaw/plugin-sdk/webhook-ingress";
20
21
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
21
22
  import { buildAgentSessionKey } from "openclaw/plugin-sdk/routing";
22
23
  import * as querystring from "node:querystring";
24
+ import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
23
25
  import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
24
26
  //#region extensions/synology-chat/src/approval-auth.ts
25
27
  function normalizeSynologyChatApproverId(value) {
@@ -44,9 +46,6 @@ const synologyChatApprovalAuth = createResolvedApproverActionAuthAdapter({
44
46
  */
45
47
  const MIN_SEND_INTERVAL_MS = 500;
46
48
  let lastSendTime = 0;
47
- function normalizeLowercaseStringOrEmpty$2(value) {
48
- return typeof value === "string" ? value.trim().toLowerCase() : "";
49
- }
50
49
  const ChatUserSchema = z.object({
51
50
  user_id: z.number(),
52
51
  username: z.string().optional(),
@@ -179,10 +178,10 @@ async function assertSafeWebhookFileUrl(fileUrl) {
179
178
  */
180
179
  async function resolveLegacyWebhookNameToChatUserId(params) {
181
180
  const users = await fetchChatUsers(params.incomingUrl, params.allowInsecureSsl, params.log);
182
- const lower = normalizeLowercaseStringOrEmpty$2(params.mutableWebhookUsername);
183
- const byNickname = users.find((u) => normalizeLowercaseStringOrEmpty$2(u.nickname) === lower);
181
+ const lower = normalizeLowercaseStringOrEmpty(params.mutableWebhookUsername);
182
+ const byNickname = users.find((u) => normalizeLowercaseStringOrEmpty(u.nickname) === lower);
184
183
  if (byNickname) return byNickname.user_id;
185
- const byUsername = users.find((u) => normalizeLowercaseStringOrEmpty$2(u.username) === lower);
184
+ const byUsername = users.find((u) => normalizeLowercaseStringOrEmpty(u.username) === lower);
186
185
  if (byUsername) return byUsername.user_id;
187
186
  }
188
187
  function buildWebhookBody(payload, userId) {
@@ -230,14 +229,11 @@ function doPost(url, body, allowInsecureSsl = false) {
230
229
  req.end();
231
230
  });
232
231
  }
233
- function sleep(ms) {
234
- return new Promise((resolve) => setTimeout(resolve, ms));
235
- }
236
232
  //#endregion
237
233
  //#region extensions/synology-chat/src/config-schema.ts
238
- const SynologyChatChannelConfigSchema = buildChannelConfigSchema(z$1.object({
239
- dangerouslyAllowNameMatching: z$1.boolean().optional(),
240
- dangerouslyAllowInheritedWebhookPath: z$1.boolean().optional()
234
+ const SynologyChatChannelConfigSchema = buildChannelConfigSchema(z.object({
235
+ dangerouslyAllowNameMatching: z.boolean().optional(),
236
+ dangerouslyAllowInheritedWebhookPath: z.boolean().optional()
241
237
  }).passthrough());
242
238
  //#endregion
243
239
  //#region extensions/synology-chat/src/runtime.ts
@@ -288,8 +284,8 @@ function resolveSynologyChatInboundRoute(params) {
288
284
  }
289
285
  async function deliverSynologyChatReply(params) {
290
286
  const text = params.payload.text ?? params.payload.body;
291
- if (!text) return;
292
- await sendMessage(params.account.incomingUrl, text, params.sendUserId, params.account.allowInsecureSsl);
287
+ if (!text) return { visibleReplySent: false };
288
+ return { visibleReplySent: await sendMessage(params.account.incomingUrl, text, params.sendUserId, params.account.allowInsecureSsl) };
293
289
  }
294
290
  async function dispatchSynologyChatInboundTurn(params) {
295
291
  const currentCfg = getSynologyRuntime().config.current();
@@ -364,13 +360,16 @@ async function dispatchSynologyChatInboundTurn(params) {
364
360
  ctxPayload: msgCtx,
365
361
  recordInboundSession: resolved.rt.channel.session.recordInboundSession,
366
362
  dispatchReplyWithBufferedBlockDispatcher: resolved.rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
367
- delivery: { deliver: async (payload) => {
368
- await deliverSynologyChatReply({
369
- account: params.account,
370
- sendUserId,
371
- payload
372
- });
373
- } },
363
+ delivery: {
364
+ durable: () => ({ to: sendUserId }),
365
+ deliver: async (payload) => {
366
+ return await deliverSynologyChatReply({
367
+ account: params.account,
368
+ sendUserId,
369
+ payload
370
+ });
371
+ }
372
+ },
374
373
  dispatcherOptions: { onReplyStart: () => {
375
374
  params.log?.info?.(`Agent reply started for ${params.msg.from}`);
376
375
  } },
@@ -396,37 +395,23 @@ function validateToken(received, expected) {
396
395
  if (!received || !expected) return false;
397
396
  return safeEqualSecret(received, expected);
398
397
  }
399
- /**
400
- * Check if a user ID is in the allowed list.
401
- * Allowlist mode must be explicit; empty lists should not match any user.
402
- */
403
- function checkUserAllowed(userId, allowedUserIds) {
404
- if (allowedUserIds.length === 0) return false;
405
- if (allowedUserIds.includes("*")) return true;
406
- return allowedUserIds.includes(userId);
407
- }
408
- /**
409
- * Resolve DM authorization for a sender across all DM policy modes.
410
- * Keeps policy semantics in one place so webhook/startup behavior stays consistent.
411
- */
412
- function authorizeUserForDm(userId, dmPolicy, allowedUserIds) {
413
- if (dmPolicy === "disabled") return {
414
- allowed: false,
415
- reason: "disabled"
416
- };
417
- if (dmPolicy === "open") return checkUserAllowed(userId, allowedUserIds) ? { allowed: true } : {
418
- allowed: false,
419
- reason: "not-allowlisted"
420
- };
421
- if (allowedUserIds.length === 0) return {
422
- allowed: false,
423
- reason: "allowlist-empty"
424
- };
425
- if (!checkUserAllowed(userId, allowedUserIds)) return {
426
- allowed: false,
427
- reason: "not-allowlisted"
428
- };
429
- return { allowed: true };
398
+ async function authorizeUserForDmWithIngress(params) {
399
+ return await resolveStableChannelMessageIngress({
400
+ channelId: "synology-chat",
401
+ accountId: params.accountId,
402
+ identity: {
403
+ key: "sender-id",
404
+ entryIdPrefix: "synology-chat-entry"
405
+ },
406
+ subject: { stableId: params.userId },
407
+ conversation: {
408
+ kind: "direct",
409
+ id: "direct"
410
+ },
411
+ event: { mayPair: false },
412
+ dmPolicy: params.dmPolicy,
413
+ allowFrom: params.allowedUserIds
414
+ });
430
415
  }
431
416
  /**
432
417
  * Sanitize user input to prevent prompt injection attacks.
@@ -476,9 +461,6 @@ var RateLimiter = class {
476
461
  };
477
462
  //#endregion
478
463
  //#region extensions/synology-chat/src/webhook-handler.ts
479
- function normalizeLowercaseStringOrEmpty$1(value) {
480
- return typeof value === "string" ? value.trim().toLowerCase() : "";
481
- }
482
464
  const rateLimiters = /* @__PURE__ */ new Map();
483
465
  const invalidTokenRateLimiters = /* @__PURE__ */ new Map();
484
466
  const webhookInFlightLimiter = createWebhookInFlightLimiter();
@@ -642,7 +624,7 @@ function extractTokenFromHeaders(req) {
642
624
  * - text <- text | message | content
643
625
  */
644
626
  function parsePayload(req, body) {
645
- const contentType = normalizeLowercaseStringOrEmpty$1(req.headers["content-type"]);
627
+ const contentType = normalizeLowercaseStringOrEmpty(req.headers["content-type"]);
646
628
  let bodyFields = {};
647
629
  if (contentType.includes("application/json")) bodyFields = parseJsonBody(body);
648
630
  else if (contentType.includes("application/x-www-form-urlencoded")) bodyFields = parseFormBody(body);
@@ -727,7 +709,7 @@ async function parseWebhookPayloadRequest(params) {
727
709
  payload
728
710
  };
729
711
  }
730
- function authorizeSynologyWebhook(params) {
712
+ async function authorizeSynologyWebhook(params) {
731
713
  const invalidTokenRateLimitKey = getSynologyWebhookInvalidTokenRateLimitKey(params.req);
732
714
  if (params.invalidTokenRateLimiter.isLocked(invalidTokenRateLimitKey)) {
733
715
  params.log?.warn(`Rate limit exceeded for remote IP: ${invalidTokenRateLimitKey}`);
@@ -753,14 +735,19 @@ function authorizeSynologyWebhook(params) {
753
735
  error: "Invalid token"
754
736
  };
755
737
  }
756
- const auth = authorizeUserForDm(params.payload.user_id, params.account.dmPolicy, params.account.allowedUserIds);
757
- if (!auth.allowed) {
758
- if (auth.reason === "disabled") return {
738
+ const auth = await authorizeUserForDmWithIngress({
739
+ accountId: params.account.accountId,
740
+ userId: params.payload.user_id,
741
+ dmPolicy: params.account.dmPolicy,
742
+ allowedUserIds: params.account.allowedUserIds
743
+ });
744
+ if (!auth.senderAccess.allowed) {
745
+ if (auth.senderAccess.reasonCode === "dm_policy_disabled") return {
759
746
  ok: false,
760
747
  statusCode: 403,
761
748
  error: "DMs are disabled"
762
749
  };
763
- if (auth.reason === "allowlist-empty") {
750
+ if (params.account.dmPolicy === "allowlist" && params.account.allowedUserIds.length === 0) {
764
751
  params.log?.warn("Synology Chat allowlist is empty while dmPolicy=allowlist; rejecting message");
765
752
  return {
766
753
  ok: false,
@@ -785,7 +772,7 @@ function authorizeSynologyWebhook(params) {
785
772
  }
786
773
  return {
787
774
  ok: true,
788
- commandAuthorized: auth.allowed
775
+ commandAuthorized: auth.senderAccess.allowed
789
776
  };
790
777
  }
791
778
  function sanitizeSynologyWebhookText(payload) {
@@ -796,7 +783,7 @@ function sanitizeSynologyWebhookText(payload) {
796
783
  async function parseAndAuthorizeSynologyWebhook(params) {
797
784
  const parsed = await parseWebhookPayloadRequest(params);
798
785
  if (!parsed.ok) return { ok: false };
799
- const authorized = authorizeSynologyWebhook({
786
+ const authorized = await authorizeSynologyWebhook({
800
787
  req: params.req,
801
788
  account: params.account,
802
789
  payload: parsed.payload,
@@ -1016,9 +1003,6 @@ function registerSynologyWebhookRoute(params) {
1016
1003
  * Implements the ChannelPlugin interface following the LINE pattern.
1017
1004
  */
1018
1005
  const CHANNEL_ID = "synology-chat";
1019
- function normalizeLowercaseStringOrEmpty(value) {
1020
- return typeof value === "string" ? value.trim().toLowerCase() : "";
1021
- }
1022
1006
  const resolveSynologyChatDmPolicy = createScopedDmSecurityResolver({
1023
1007
  channelKey: CHANNEL_ID,
1024
1008
  resolvePolicy: (account) => account.dmPolicy,
@@ -1061,6 +1045,53 @@ function requireIncomingUrl(account) {
1061
1045
  if (!account.incomingUrl) throw new Error("Synology Chat incoming URL not configured");
1062
1046
  return account.incomingUrl;
1063
1047
  }
1048
+ function createSynologyChatSendResult(params) {
1049
+ return {
1050
+ channel: CHANNEL_ID,
1051
+ messageId: params.messageId,
1052
+ chatId: params.chatId,
1053
+ receipt: createMessageReceiptFromOutboundResults({
1054
+ results: [{
1055
+ channel: CHANNEL_ID,
1056
+ messageId: params.messageId,
1057
+ chatId: params.chatId,
1058
+ conversationId: params.chatId
1059
+ }],
1060
+ threadId: params.chatId,
1061
+ kind: params.kind
1062
+ })
1063
+ };
1064
+ }
1065
+ async function sendSynologyChatText(ctx) {
1066
+ const account = resolveOutboundAccount(ctx.cfg ?? {}, ctx.accountId);
1067
+ if (!await sendMessage(requireIncomingUrl(account), ctx.text, ctx.to, account.allowInsecureSsl)) throw new Error("Failed to send message to Synology Chat");
1068
+ return createSynologyChatSendResult({
1069
+ messageId: `sc-${Date.now()}`,
1070
+ chatId: ctx.to,
1071
+ kind: "text"
1072
+ });
1073
+ }
1074
+ async function sendSynologyChatMedia(ctx) {
1075
+ const account = resolveOutboundAccount(ctx.cfg ?? {}, ctx.accountId);
1076
+ if (!await sendFileUrl(requireIncomingUrl(account), ctx.mediaUrl, ctx.to, account.allowInsecureSsl)) throw new Error("Failed to send media to Synology Chat");
1077
+ return createSynologyChatSendResult({
1078
+ messageId: `sc-${Date.now()}`,
1079
+ chatId: ctx.to,
1080
+ kind: "media"
1081
+ });
1082
+ }
1083
+ const synologyChatMessageAdapter = defineChannelMessageAdapter({
1084
+ id: CHANNEL_ID,
1085
+ durableFinal: { capabilities: {
1086
+ text: true,
1087
+ media: true,
1088
+ messageSendingHooks: true
1089
+ } },
1090
+ send: {
1091
+ text: async (ctx) => await sendSynologyChatText(ctx),
1092
+ media: async (ctx) => await sendSynologyChatMedia(ctx)
1093
+ }
1094
+ });
1064
1095
  function createSynologyChatPlugin() {
1065
1096
  return createChatChannelPlugin({
1066
1097
  base: {
@@ -1160,7 +1191,8 @@ function createSynologyChatPlugin() {
1160
1191
  "- Use line breaks to separate sections",
1161
1192
  "- Use numbered or bulleted lists for clarity",
1162
1193
  "- Wrap URLs with `<URL|label>` for user-friendly links"
1163
- ] }
1194
+ ] },
1195
+ message: synologyChatMessageAdapter
1164
1196
  },
1165
1197
  pairing: { text: {
1166
1198
  idLabel: "synologyChatUserId",
@@ -1180,22 +1212,12 @@ function createSynologyChatPlugin() {
1180
1212
  outbound: {
1181
1213
  deliveryMode: "gateway",
1182
1214
  textChunkLimit: 2e3,
1183
- sendText: async ({ to, text, accountId, cfg }) => {
1184
- const account = resolveOutboundAccount(cfg ?? {}, accountId);
1185
- if (!await sendMessage(requireIncomingUrl(account), text, to, account.allowInsecureSsl)) throw new Error("Failed to send message to Synology Chat");
1186
- return attachChannelToResult(CHANNEL_ID, {
1187
- messageId: `sc-${Date.now()}`,
1188
- chatId: to
1189
- });
1190
- },
1191
- sendMedia: async ({ to, mediaUrl, accountId, cfg }) => {
1192
- const account = resolveOutboundAccount(cfg ?? {}, accountId);
1193
- const incomingUrl = requireIncomingUrl(account);
1194
- if (!mediaUrl) throw new Error("No media URL provided");
1195
- if (!await sendFileUrl(incomingUrl, mediaUrl, to, account.allowInsecureSsl)) throw new Error("Failed to send media to Synology Chat");
1196
- return attachChannelToResult(CHANNEL_ID, {
1197
- messageId: `sc-${Date.now()}`,
1198
- chatId: to
1215
+ sendText: sendSynologyChatText,
1216
+ sendMedia: async (ctx) => {
1217
+ if (!ctx.mediaUrl) throw new Error("Synology Chat media send requires mediaUrl");
1218
+ return await sendSynologyChatMedia({
1219
+ ...ctx,
1220
+ mediaUrl: ctx.mediaUrl
1199
1221
  });
1200
1222
  }
1201
1223
  }
@@ -1,2 +1,2 @@
1
- import { t as synologyChatPlugin } from "./channel-BYl2GyR_.js";
1
+ import { t as synologyChatPlugin } from "./channel-IPqYKUxY.js";
2
2
  export { synologyChatPlugin };
package/dist/setup-api.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as synologyChatSetupWizard, t as synologyChatSetupAdapter } from "./setup-surface-BTp4n9pb.js";
1
+ import { n as synologyChatSetupWizard, t as synologyChatSetupAdapter } from "./setup-surface-daP5yN4Y.js";
2
2
  export { synologyChatSetupAdapter, synologyChatSetupWizard };
@@ -1,3 +1,4 @@
1
+ import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
1
2
  import { DEFAULT_ACCOUNT_ID, listCombinedAccountIds, resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-resolution";
2
3
  import { resolveDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
3
4
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, createAllowFromSection, createStandardChannelSetupStatus, formatDocsLink, mergeAllowFromEntries, normalizeAccountId, setSetupChannelEnabled, splitSetupEntries } from "openclaw/plugin-sdk/setup";
@@ -114,10 +115,6 @@ const SYNOLOGY_ALLOW_FROM_HELP_LINES = [
114
115
  "Multiple entries: comma-separated.",
115
116
  `Docs: ${formatDocsLink("/channels/synology-chat", "channels/synology-chat")}`
116
117
  ];
117
- function normalizeOptionalString(value) {
118
- if (typeof value !== "string") return;
119
- return value.trim() || void 0;
120
- }
121
118
  function getChannelConfig(cfg) {
122
119
  return cfg.channels?.[channel] ?? {};
123
120
  }
package/package.json CHANGED
@@ -1,15 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/synology-chat",
3
- "version": "2026.5.7",
3
+ "version": "2026.5.10-beta.1",
4
4
  "description": "Synology Chat channel plugin for OpenClaw",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/openclaw/openclaw"
8
8
  },
9
9
  "type": "module",
10
- "dependencies": {
11
- "zod": "^4.4.3"
12
- },
13
10
  "devDependencies": {
14
11
  "@openclaw/plugin-sdk": "workspace:*"
15
12
  },
@@ -33,10 +30,10 @@
33
30
  "minHostVersion": ">=2026.4.10"
34
31
  },
35
32
  "compat": {
36
- "pluginApi": ">=2026.5.7"
33
+ "pluginApi": ">=2026.5.10-beta.1"
37
34
  },
38
35
  "build": {
39
- "openclawVersion": "2026.5.7"
36
+ "openclawVersion": "2026.5.10-beta.1"
40
37
  },
41
38
  "release": {
42
39
  "publishToClawHub": true,
@@ -52,7 +49,7 @@
52
49
  "openclaw.plugin.json"
53
50
  ],
54
51
  "peerDependencies": {
55
- "openclaw": ">=2026.5.7"
52
+ "openclaw": ">=2026.5.10-beta.1"
56
53
  },
57
54
  "peerDependenciesMeta": {
58
55
  "openclaw": {