@openclaw/nextcloud-talk 2026.9.1-beta.1 → 2026.9.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,2 +1,2 @@
1
- import { t as nextcloudTalkPlugin } from "./channel-tgvjpexV.js";
1
+ import { t as nextcloudTalkPlugin } from "./channel-D2RVrR4p.js";
2
2
  export { nextcloudTalkPlugin };
@@ -7,31 +7,32 @@ import { createLoggedPairingApprovalNotifier, createPairingPrefixStripper } from
7
7
  import { createAllowlistProviderRouteAllowlistWarningCollector, createConditionalWarningCollector, resolveScopeRequireMention, resolveScopeToolsPolicy } from "openclaw/plugin-sdk/channel-policy";
8
8
  import { buildWebhookChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
9
9
  import { FormatCapabilityProfile, convertMarkdownTables, renderMarkdownWithMarkers, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
10
- import { createChannelApprovalAuth } from "openclaw/plugin-sdk/approval-auth-runtime";
11
- import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries, readNonEmptyStringPreservingWhitespace } from "openclaw/plugin-sdk/string-coerce-runtime";
12
10
  import { DEFAULT_ACCOUNT_ID, hasConfiguredAccountValue, normalizeAccountId, resolveAccountWithDefaultFallback } from "openclaw/plugin-sdk/account-core";
13
11
  import { isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env";
14
12
  import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
15
- import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
13
+ import { buildSecretInputSchema, hasConfiguredSecretInput, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
14
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries, readNonEmptyStringPreservingWhitespace } from "openclaw/plugin-sdk/string-coerce-runtime";
15
+ import { createChannelApprovalAuth } from "openclaw/plugin-sdk/approval-auth-runtime";
16
16
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
17
17
  import { parseStrictNonNegativeInteger, parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
18
- import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
18
+ import { readProviderJsonResponse, readProviderTextResponse } from "openclaw/plugin-sdk/provider-http";
19
19
  import { resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
20
20
  import { bindIngressLifecycleToReplyOptions, createAccountStatusSink, createChannelIngressMonitor, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, resolveChannelStreamingBlockEnabled, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-outbound";
21
21
  import { ssrfPolicyFromPrivateNetworkOptIn, ssrfPolicyFromPrivateNetworkOptIn as ssrfPolicyFromPrivateNetworkOptIn$1 } from "openclaw/plugin-sdk/ssrf-runtime";
22
+ import { redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
22
23
  import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
23
24
  import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
24
25
  import { createHmac, randomBytes } from "node:crypto";
25
- import { fileExists, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
26
- import { clearAccountEntryFields } from "openclaw/plugin-sdk/channel-plugin-common";
26
+ import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
27
27
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2 } from "openclaw/plugin-sdk/account-id";
28
28
  import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, ReplyToModeSchema, buildChannelConfigSchema, buildGroupEntrySchema, buildMultiAccountChannelSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
29
29
  import { formatAllowFromLowercase, resolveAllowlistMatchByCandidates } from "openclaw/plugin-sdk/allow-from";
30
- import { adaptScopedAccountAccessor, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
30
+ import { adaptScopedAccountAccessor, clearAccountFieldsFromConfigSection, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
31
31
  import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
32
32
  import { z } from "zod";
33
33
  import os from "node:os";
34
34
  import path from "node:path";
35
+ import { fileExists } from "openclaw/plugin-sdk/file-access-runtime";
35
36
  import { migratePersistentDedupeLegacyJsonFile, resolvePersistentDedupePluginStateNamespace } from "openclaw/plugin-sdk/persistent-dedupe";
36
37
  import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
37
38
  import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
@@ -41,6 +42,7 @@ import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
41
42
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
42
43
  import { createServer } from "node:http";
43
44
  import { WEBHOOK_RATE_LIMIT_DEFAULTS, createAuthRateLimiter, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, resolveRequestClientIp } from "openclaw/plugin-sdk/webhook-ingress";
45
+ import { sendHttpRequestRejection } from "openclaw/plugin-sdk/webhook-request-guards";
44
46
  import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
45
47
  import { jsonResult, readStringParam, resolveReactionMessageId } from "openclaw/plugin-sdk/channel-actions";
46
48
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, buildOutboundBaseSessionKey, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/routing";
@@ -53,17 +55,19 @@ import { formatDocsLink as formatDocsLink$1 } from "openclaw/plugin-sdk/setup-to
53
55
  function resolveNextcloudTalkApiCredentialsResult(params) {
54
56
  const apiUser = params.apiUser?.trim();
55
57
  if (!apiUser) return { status: "missing" };
56
- const inlinePassword = normalizeResolvedSecretInputString({
58
+ const inlinePassword = resolveSecretInputString({
57
59
  value: params.apiPassword,
58
- path: "channels.nextcloud-talk.apiPassword"
60
+ path: "channels.nextcloud-talk.apiPassword",
61
+ mode: params.mode
59
62
  });
60
- if (inlinePassword) return {
63
+ if (inlinePassword.status === "available") return {
61
64
  status: "available",
62
65
  value: {
63
66
  apiUser,
64
- apiPassword: inlinePassword
67
+ apiPassword: inlinePassword.value
65
68
  }
66
69
  };
70
+ if (inlinePassword.status === "configured_unavailable") return { status: "configured_unavailable" };
67
71
  if (!params.apiPasswordFile?.trim()) return { status: "missing" };
68
72
  const result = tryReadSecretFileSync(params.apiPasswordFile, "Nextcloud Talk API password", { rejectHardlinks: false }, { configPath: params.configPath ?? "channels.nextcloud-talk.apiPasswordFile" });
69
73
  if (result.status === "available") return result.value ? {
@@ -180,15 +184,21 @@ function inspectNextcloudTalkAccount(params) {
180
184
  apiUser: account.config.apiUser,
181
185
  apiPassword: account.config.apiPassword,
182
186
  apiPasswordFile: account.config.apiPasswordFile,
183
- configPath: `channels.nextcloud-talk.accounts.${account.accountId}.apiPasswordFile`
187
+ configPath: `channels.nextcloud-talk.accounts.${account.accountId}.apiPasswordFile`,
188
+ mode: "inspect"
184
189
  });
185
- const credentialDiagnostics = [...account.credentialDiagnostics ?? [], ...apiCredentialResolution.status === "configured_unavailable" ? [apiCredentialResolution.diagnostic] : []];
190
+ const credentialDiagnostics = [...account.credentialDiagnostics ?? [], ...apiCredentialResolution.status === "configured_unavailable" && apiCredentialResolution.diagnostic ? [apiCredentialResolution.diagnostic] : []];
186
191
  return {
187
192
  ...account,
193
+ configured: isNextcloudTalkAccountConfigured(account),
194
+ mode: "webhook",
188
195
  apiCredentialStatus: apiCredentialResolution.status,
189
196
  ...credentialDiagnostics.length > 0 ? { credentialDiagnostics } : {}
190
197
  };
191
198
  }
199
+ function isNextcloudTalkAccountConfigured(account) {
200
+ return Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim());
201
+ }
192
202
  //#endregion
193
203
  //#region extensions/nextcloud-talk/src/approval-auth.ts
194
204
  function normalizeNextcloudTalkApproverId(value) {
@@ -210,6 +220,18 @@ async function releaseNextcloudTalkGuardedResponse(params) {
210
220
  if (!params.response.bodyUsed) await params.response.body?.cancel().catch(() => void 0);
211
221
  await params.release();
212
222
  }
223
+ async function readNextcloudTalkErrorBody(response, ...credentials) {
224
+ try {
225
+ let body = await readProviderTextResponse(response, "Nextcloud Talk error", {
226
+ maxBytes: 8192,
227
+ chunkTimeoutMs: 1e4
228
+ });
229
+ for (const credential of credentials) body = body.replaceAll(credential, "***");
230
+ return redactToolPayloadText(body);
231
+ } catch {
232
+ return "";
233
+ }
234
+ }
213
235
  //#endregion
214
236
  //#region extensions/nextcloud-talk/src/signature.ts
215
237
  const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
@@ -222,7 +244,8 @@ const BACKEND_HEADER = "x-nextcloud-talk-backend";
222
244
  function verifyNextcloudTalkSignature(params) {
223
245
  const { signature, random, body, secret } = params;
224
246
  if (!signature || !random || !secret) return false;
225
- return safeEqualSecret(signature, createHmac("sha256", secret).update(random + body).digest("hex"));
247
+ const expected = createHmac("sha256", secret).update(random + body).digest("hex");
248
+ return safeEqualSecret(signature, expected);
226
249
  }
227
250
  /**
228
251
  * Extract webhook headers from an incoming request.
@@ -256,7 +279,6 @@ function generateNextcloudTalkSignature(params) {
256
279
  //#endregion
257
280
  //#region extensions/nextcloud-talk/src/bot-preflight.ts
258
281
  const BOT_FEATURE_RESPONSE = 2;
259
- const BOT_PREFLIGHT_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
260
282
  function normalizeUrlForMatch(value) {
261
283
  if (!value?.trim()) return "";
262
284
  try {
@@ -321,7 +343,7 @@ async function probeNextcloudTalkBotResponseFeature(params) {
321
343
  });
322
344
  try {
323
345
  if (!response.ok) {
324
- const body = await readResponseTextLimited(response, BOT_PREFLIGHT_ERROR_BODY_LIMIT_BYTES).catch(() => "");
346
+ const body = await readNextcloudTalkErrorBody(response, auth, credentials.apiPassword);
325
347
  return {
326
348
  ok: false,
327
349
  code: "api_error",
@@ -373,7 +395,16 @@ const nextcloudTalkConfigAdapter = createScopedChannelConfigAdapter({
373
395
  sectionKey: "nextcloud-talk",
374
396
  listAccountIds: listNextcloudTalkAccountIds,
375
397
  resolveAccount: adaptScopedAccountAccessor(resolveNextcloudTalkAccount),
376
- inspectAccount: adaptScopedAccountAccessor(inspectNextcloudTalkAccount),
398
+ inspectAccount: (cfg, accountId) => {
399
+ const account = inspectNextcloudTalkAccount({
400
+ cfg,
401
+ accountId
402
+ });
403
+ return {
404
+ ...account,
405
+ baseUrl: account.baseUrl ? "[set]" : "[missing]"
406
+ };
407
+ },
377
408
  defaultAccountId: resolveDefaultNextcloudTalkAccountId,
378
409
  clearBaseFields: [
379
410
  "botSecret",
@@ -405,7 +436,7 @@ const NextcloudTalkRoomSchema = buildGroupEntrySchema({ allowFrom: z.array(z.str
405
436
  const NextcloudTalkNetworkSchema = z.object({
406
437
  /** Dangerous opt-in for self-hosted Nextcloud Talk on trusted private/internal hosts. */
407
438
  dangerouslyAllowPrivateNetwork: z.boolean().optional() }).strict().optional();
408
- const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
439
+ const NextcloudTalkAccountSchemaBase = z.object({
409
440
  name: z.string().optional(),
410
441
  enabled: z.boolean().optional(),
411
442
  configWrites: z.boolean().optional(),
@@ -429,7 +460,8 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
429
460
  /** Network policy overrides for self-hosted Nextcloud Talk on trusted private/internal hosts. */
430
461
  network: NextcloudTalkNetworkSchema,
431
462
  ...ReplyRuntimeConfigSchemaShape
432
- }).strict(), {
463
+ }).strict();
464
+ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(NextcloudTalkAccountSchemaBase, {
433
465
  optionalAccount: true,
434
466
  refine: (value, ctx) => {
435
467
  requireChannelOpenAllowFrom({
@@ -445,7 +477,7 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
445
477
  //#region extensions/nextcloud-talk/src/replay-migration-contract.ts
446
478
  const NEXTCLOUD_TALK_PLUGIN_ID = "nextcloud-talk";
447
479
  const NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX = "replay-dedupe";
448
- const NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS = 1440 * 60 * 1e3;
480
+ const NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS = 864e5;
449
481
  const NEXTCLOUD_TALK_REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
450
482
  //#endregion
451
483
  //#region extensions/nextcloud-talk/src/doctor.ts
@@ -525,10 +557,12 @@ function normalizeNextcloudTalkAllowlist(values) {
525
557
  return (values ?? []).map((value) => normalizeNextcloudTalkAllowEntry(String(value))).filter(Boolean);
526
558
  }
527
559
  function resolveNextcloudTalkAllowlistMatch(params) {
560
+ const allowFrom = normalizeNextcloudTalkAllowlist(params.allowFrom);
561
+ const senderId = normalizeNextcloudTalkAllowEntry(params.senderId);
528
562
  return resolveAllowlistMatchByCandidates({
529
- allowList: normalizeNextcloudTalkAllowlist(params.allowFrom),
563
+ allowList: allowFrom,
530
564
  candidates: [{
531
- value: normalizeNextcloudTalkAllowEntry(params.senderId),
565
+ value: senderId,
532
566
  source: "id"
533
567
  }]
534
568
  });
@@ -596,8 +630,8 @@ function resolveNextcloudTalkGroupRequireMention(params) {
596
630
  }
597
631
  //#endregion
598
632
  //#region extensions/nextcloud-talk/src/room-info.ts
599
- const ROOM_CACHE_TTL_MS = 300 * 1e3;
600
- const ROOM_CACHE_ERROR_TTL_MS = 30 * 1e3;
633
+ const ROOM_CACHE_TTL_MS = 3e5;
634
+ const ROOM_CACHE_ERROR_TTL_MS = 3e4;
601
635
  const ROOM_CACHE_MAX_ENTRIES = 1e3;
602
636
  const NEXTCLOUD_TALK_ROOM_INFO_TIMEOUT_MS = 3e4;
603
637
  const roomCache = /* @__PURE__ */ new Map();
@@ -659,7 +693,8 @@ async function resolveNextcloudTalkRoomKind(params) {
659
693
  runtime?.log?.(`nextcloud-talk: room lookup failed (${response.status}) token=${roomToken}`);
660
694
  return;
661
695
  }
662
- const kind = resolveRoomKindFromType(parseStrictPositiveInteger((await readProviderJsonResponse(response, "Nextcloud Talk room info failed")).ocs?.data?.type));
696
+ const payload = await readProviderJsonResponse(response, "Nextcloud Talk room info failed");
697
+ const kind = resolveRoomKindFromType(parseStrictPositiveInteger(payload.ocs?.data?.type));
663
698
  cacheRoomInfo(key, {
664
699
  fetchedAt: Date.now(),
665
700
  kind
@@ -705,7 +740,6 @@ function looksLikeNextcloudTalkTargetId(raw) {
705
740
  }
706
741
  //#endregion
707
742
  //#region extensions/nextcloud-talk/src/send.ts
708
- const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES = 8 * 1024;
709
743
  const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS = 200;
710
744
  const NEXTCLOUD_TALK_SEND_TIMEOUT_MS = 3e4;
711
745
  const NEXTCLOUD_TALK_FORMAT_PROFILE = FormatCapabilityProfile.define({
@@ -726,20 +760,12 @@ function renderNextcloudTalkMarkdown(markdown) {
726
760
  escapeText: (text) => text
727
761
  }, NEXTCLOUD_TALK_FORMAT_PROFILE);
728
762
  }
729
- /** Collapses whitespace and caps an error-body prefix to a short, log-safe snippet. */
763
+ /** Collapses and caps an already-redacted error body for display. */
730
764
  function collapseErrorSnippet(text) {
731
765
  const collapsed = text.replace(/\s+/g, " ").trim();
732
766
  if (collapsed.length > NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS) return `${truncateUtf16Safe(collapsed, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS)}…`;
733
767
  return collapsed;
734
768
  }
735
- /** Reads a bounded, collapsed error-body snippet without buffering hostile responses. */
736
- async function readNextcloudTalkErrorSnippet(response) {
737
- try {
738
- return collapseErrorSnippet(await readResponseTextLimited(response, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES));
739
- } catch {
740
- return "";
741
- }
742
- }
743
769
  function resolveCredentials(explicit, account) {
744
770
  const baseUrl = explicit.baseUrl?.trim() ?? account.baseUrl;
745
771
  const secret = explicit.secret?.trim() ?? account.secret;
@@ -812,8 +838,9 @@ async function sendMessageNextcloudTalk(to, text, opts) {
812
838
  body: message,
813
839
  secret
814
840
  });
841
+ const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${roomToken}/message`;
815
842
  const { response, release } = await fetchWithSsrFGuard({
816
- url: `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${roomToken}/message`,
843
+ url,
817
844
  init: {
818
845
  method: "POST",
819
846
  headers: {
@@ -830,7 +857,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
830
857
  });
831
858
  try {
832
859
  if (!response.ok) {
833
- const errorBody = await readNextcloudTalkErrorSnippet(response);
860
+ const errorBody = collapseErrorSnippet(await readNextcloudTalkErrorBody(response, signature));
834
861
  const status = response.status;
835
862
  let errorMsg = `Nextcloud Talk send failed (${status})`;
836
863
  if (status === 400) errorMsg = `Nextcloud Talk: bad request - ${errorBody || "invalid message format"}`;
@@ -874,8 +901,9 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
874
901
  body: reaction,
875
902
  secret
876
903
  });
904
+ const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${normalizedToken}/reaction/${messageId}`;
877
905
  const { response, release } = await fetchWithSsrFGuard({
878
- url: `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${normalizedToken}/reaction/${messageId}`,
906
+ url,
879
907
  init: {
880
908
  method: "POST",
881
909
  headers: {
@@ -892,7 +920,7 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
892
920
  });
893
921
  try {
894
922
  if (!response.ok) {
895
- const errorBody = await readNextcloudTalkErrorSnippet(response);
923
+ const errorBody = collapseErrorSnippet(await readNextcloudTalkErrorBody(response, signature));
896
924
  throw new Error(`Nextcloud Talk reaction failed: ${response.status} ${errorBody}`.trim());
897
925
  }
898
926
  return { ok: true };
@@ -963,7 +991,15 @@ async function handleNextcloudTalkInbound(params) {
963
991
  accountId: account.accountId
964
992
  });
965
993
  const rawBody = message.text?.trim() ?? "";
966
- if (!rawBody) return;
994
+ if (!rawBody) {
995
+ logInboundDrop({
996
+ log: (messageLocal) => runtime.log?.(messageLocal),
997
+ channel: CHANNEL_ID,
998
+ reason: `empty message body (mediaType=${message.mediaType})`,
999
+ target: message.senderId
1000
+ });
1001
+ return;
1002
+ }
967
1003
  const roomKind = await resolveNextcloudTalkRoomKind({
968
1004
  account,
969
1005
  roomToken: message.roomToken,
@@ -1279,8 +1315,8 @@ async function migrateNextcloudTalkLegacyReplayState(params) {
1279
1315
  }
1280
1316
  //#endregion
1281
1317
  //#region extensions/nextcloud-talk/src/monitor.ts
1282
- const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
1283
- const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 64 * 1024;
1318
+ const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1048576;
1319
+ const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 65536;
1284
1320
  const NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_HEADER = "x-openclaw-delivery-accepted";
1285
1321
  const NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_VALUE = "durable";
1286
1322
  const PREAUTH_WEBHOOK_BODY_TIMEOUT_MS = 5e3;
@@ -1336,9 +1372,14 @@ function verifyWebhookSignature(params) {
1336
1372
  function readNextcloudTalkWebhookBody(req, maxBodyBytes) {
1337
1373
  return readRequestBodyWithLimit(req, {
1338
1374
  maxBytes: Math.min(maxBodyBytes, PREAUTH_WEBHOOK_MAX_BODY_BYTES),
1339
- timeoutMs: PREAUTH_WEBHOOK_BODY_TIMEOUT_MS
1375
+ timeoutMs: PREAUTH_WEBHOOK_BODY_TIMEOUT_MS,
1376
+ destroyOnLimit: false
1340
1377
  });
1341
1378
  }
1379
+ async function rejectWebhookRequest(req, res, status, error) {
1380
+ if (res.headersSent) return;
1381
+ await sendHttpRequestRejection(req, res, status, JSON.stringify({ error }), "application/json");
1382
+ }
1342
1383
  function createNextcloudTalkWebhookServer(opts) {
1343
1384
  const { port, host, path, secret, onWebhook, onError, abortSignal } = opts;
1344
1385
  const maxBodyBytes = typeof opts.maxBodyBytes === "number" && Number.isFinite(opts.maxBodyBytes) && opts.maxBodyBytes > 0 ? Math.floor(opts.maxBodyBytes) : DEFAULT_WEBHOOK_MAX_BODY_BYTES;
@@ -1391,11 +1432,11 @@ function createNextcloudTalkWebhookServer(opts) {
1391
1432
  writeJsonResponse(res, 200);
1392
1433
  } catch (err) {
1393
1434
  if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
1394
- writeWebhookError(res, 413, WEBHOOK_ERRORS.payloadTooLarge);
1435
+ await rejectWebhookRequest(req, res, 413, WEBHOOK_ERRORS.payloadTooLarge);
1395
1436
  return;
1396
1437
  }
1397
1438
  if (isRequestBodyLimitError(err, "REQUEST_BODY_TIMEOUT")) {
1398
- writeWebhookError(res, 408, requestBodyErrorToText("REQUEST_BODY_TIMEOUT"));
1439
+ await rejectWebhookRequest(req, res, 408, requestBodyErrorToText("REQUEST_BODY_TIMEOUT"));
1399
1440
  return;
1400
1441
  }
1401
1442
  if (err instanceof NextcloudTalkWebhookPayloadError) {
@@ -1439,8 +1480,10 @@ function createNextcloudTalkWebhookServer(opts) {
1439
1480
  });
1440
1481
  });
1441
1482
  };
1442
- if (abortSignal) if (abortSignal.aborted) stop();
1443
- else abortSignal.addEventListener("abort", () => void stop(), { once: true });
1483
+ if (abortSignal) {
1484
+ if (abortSignal.aborted) stop();
1485
+ else abortSignal.addEventListener("abort", () => void stop(), { once: true });
1486
+ }
1444
1487
  return {
1445
1488
  server,
1446
1489
  start,
@@ -1450,6 +1493,12 @@ function createNextcloudTalkWebhookServer(opts) {
1450
1493
  //#endregion
1451
1494
  //#region extensions/nextcloud-talk/src/webhook-spool.ts
1452
1495
  const NEXTCLOUD_TALK_INGRESS_POLL_INTERVAL_MS = 500;
1496
+ function describeIgnoredWebhookEvent(rawEvent) {
1497
+ const envelope = parseRawObject(rawEvent);
1498
+ const type = typeof envelope.type === "string" ? envelope.type : "unknown";
1499
+ const object = isRecord(envelope.object) ? envelope.object : null;
1500
+ return `type=${type} objectType=${typeof object?.type === "string" ? object.type : "unknown"}`;
1501
+ }
1453
1502
  const NextcloudTalkWebhookPayloadSchema = z.object({
1454
1503
  type: z.enum([
1455
1504
  "Create",
@@ -1576,7 +1625,11 @@ function createNextcloudTalkWebhookSpool(options) {
1576
1625
  if (stopping) return Promise.reject(/* @__PURE__ */ new Error("Nextcloud Talk ingress stopped"));
1577
1626
  const receiveTask = (async () => {
1578
1627
  await startAfterMigration;
1579
- return (await monitor.admit(rawEvent)).kind === "ignored" ? "ignored" : "accepted";
1628
+ if ((await monitor.admit(rawEvent)).kind === "ignored") {
1629
+ options.runtime.log?.(`nextcloud-talk: ignored non-message webhook event (${describeIgnoredWebhookEvent(rawEvent)})`);
1630
+ return "ignored";
1631
+ }
1632
+ return "accepted";
1580
1633
  })();
1581
1634
  inFlightReceives.add(receiveTask);
1582
1635
  receiveTask.then(() => inFlightReceives.delete(receiveTask), () => inFlightReceives.delete(receiveTask));
@@ -1720,44 +1773,18 @@ const nextcloudTalkGatewayAdapter = {
1720
1773
  });
1721
1774
  },
1722
1775
  logoutAccount: async ({ accountId, cfg }) => {
1723
- const nextCfg = { ...cfg };
1724
- const nextSection = cfg.channels?.["nextcloud-talk"] ? { ...cfg.channels["nextcloud-talk"] } : void 0;
1725
- let cleared = false;
1726
- let changed = false;
1727
- if (nextSection) {
1728
- if (accountId === DEFAULT_ACCOUNT_ID$2 && nextSection.botSecret) {
1729
- delete nextSection.botSecret;
1730
- cleared = true;
1731
- changed = true;
1732
- }
1733
- const accountCleanup = clearAccountEntryFields({
1734
- accounts: nextSection.accounts,
1735
- accountId,
1736
- fields: ["botSecret"]
1737
- });
1738
- if (accountCleanup.changed) {
1739
- changed = true;
1740
- if (accountCleanup.cleared) cleared = true;
1741
- if (accountCleanup.nextAccounts) nextSection.accounts = accountCleanup.nextAccounts;
1742
- else delete nextSection.accounts;
1743
- }
1744
- }
1745
- if (changed) if (nextSection && Object.keys(nextSection).length > 0) nextCfg.channels = {
1746
- ...nextCfg.channels,
1747
- "nextcloud-talk": nextSection
1748
- };
1749
- else {
1750
- const nextChannels = { ...nextCfg.channels };
1751
- delete nextChannels["nextcloud-talk"];
1752
- if (Object.keys(nextChannels).length > 0) nextCfg.channels = nextChannels;
1753
- else delete nextCfg.channels;
1754
- }
1776
+ const { nextConfig, changed, cleared } = clearAccountFieldsFromConfigSection({
1777
+ cfg,
1778
+ sectionKey: "nextcloud-talk",
1779
+ accountId,
1780
+ fields: ["botSecret"]
1781
+ });
1755
1782
  const loggedOut = resolveNextcloudTalkAccount({
1756
- cfg: changed ? nextCfg : cfg,
1783
+ cfg: nextConfig,
1757
1784
  accountId
1758
1785
  }).secretSource === "none";
1759
1786
  if (changed) await getNextcloudTalkRuntime().config.replaceConfigFile({
1760
- nextConfig: nextCfg,
1787
+ nextConfig,
1761
1788
  afterWrite: { mode: "auto" }
1762
1789
  });
1763
1790
  return {
@@ -1939,6 +1966,49 @@ const nextcloudTalkDmPolicy = createChannelDmPolicy({
1939
1966
  applyPatch: ({ cfg, account, patch }) => setNextcloudTalkAccountConfig(cfg, account.accountId, patch),
1940
1967
  promptAllowFrom: promptNextcloudTalkAllowFromForAccount
1941
1968
  });
1969
+ const nextcloudTalkSetupAdapter = {
1970
+ singleAccountKeysToMove: ["rooms"],
1971
+ resolveAccountId: ({ accountId }) => normalizeAccountId$1(accountId),
1972
+ prepareAccountConfigInput: ({ input }) => {
1973
+ const setupInput = input;
1974
+ return {
1975
+ ...setupInput,
1976
+ baseUrl: setupInput.baseUrl ?? readNonEmptyStringPreservingWhitespace(setupInput.url),
1977
+ secret: setupInput.secret ?? readNonEmptyStringPreservingWhitespace(setupInput.token) ?? readNonEmptyStringPreservingWhitespace(setupInput.password),
1978
+ secretFile: setupInput.secretFile ?? readNonEmptyStringPreservingWhitespace(setupInput.tokenFile)
1979
+ };
1980
+ },
1981
+ applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
1982
+ cfg,
1983
+ channelKey: channel$1,
1984
+ accountId,
1985
+ name
1986
+ }),
1987
+ validateInput: createSetupInputPresenceValidator({
1988
+ defaultAccountOnlyEnvError: "NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.",
1989
+ validate: ({ input }) => {
1990
+ const setupInput = input;
1991
+ if (!setupInput.useEnv && !setupInput.secret && !setupInput.secretFile) return "Nextcloud Talk requires bot secret or --secret-file (or --use-env).";
1992
+ const normalizedBaseUrl = normalizeNextcloudTalkBaseUrl(setupInput.baseUrl);
1993
+ if (!normalizedBaseUrl) return "Nextcloud Talk requires --base-url.";
1994
+ const baseUrlError = validateNextcloudTalkBaseUrl(normalizedBaseUrl);
1995
+ if (baseUrlError) return baseUrlError;
1996
+ return null;
1997
+ }
1998
+ }),
1999
+ applyAccountConfig: ({ cfg, accountId, input }) => {
2000
+ const setupInput = input;
2001
+ return setNextcloudTalkAccountConfig(applyAccountNameToChannelSection({
2002
+ cfg,
2003
+ channelKey: channel$1,
2004
+ accountId,
2005
+ name: setupInput.name
2006
+ }), accountId, {
2007
+ baseUrl: normalizeNextcloudTalkBaseUrl(setupInput.baseUrl),
2008
+ ...setupInput.useEnv ? {} : setupInput.secretFile ? { botSecretFile: setupInput.secretFile } : setupInput.secret ? { botSecret: setupInput.secret } : {}
2009
+ }, setupInput.useEnv ? ["botSecret", "botSecretFile"] : void 0);
2010
+ }
2011
+ };
1942
2012
  const nextcloudTalkSetupContract = defineChannelSetupContract({
1943
2013
  fields: {
1944
2014
  baseUrl: {
@@ -2004,49 +2074,7 @@ const nextcloudTalkSetupContract = defineChannelSetupContract({
2004
2074
  envVars: ["NEXTCLOUD_TALK_BOT_SECRET"]
2005
2075
  }
2006
2076
  },
2007
- legacyAdapter: {
2008
- singleAccountKeysToMove: ["rooms"],
2009
- resolveAccountId: ({ accountId }) => normalizeAccountId$1(accountId),
2010
- prepareAccountConfigInput: ({ input }) => {
2011
- const setupInput = input;
2012
- return {
2013
- ...setupInput,
2014
- baseUrl: setupInput.baseUrl ?? readNonEmptyStringPreservingWhitespace(setupInput.url),
2015
- secret: setupInput.secret ?? readNonEmptyStringPreservingWhitespace(setupInput.token) ?? readNonEmptyStringPreservingWhitespace(setupInput.password),
2016
- secretFile: setupInput.secretFile ?? readNonEmptyStringPreservingWhitespace(setupInput.tokenFile)
2017
- };
2018
- },
2019
- applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
2020
- cfg,
2021
- channelKey: channel$1,
2022
- accountId,
2023
- name
2024
- }),
2025
- validateInput: createSetupInputPresenceValidator({
2026
- defaultAccountOnlyEnvError: "NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.",
2027
- validate: ({ input }) => {
2028
- const setupInput = input;
2029
- if (!setupInput.useEnv && !setupInput.secret && !setupInput.secretFile) return "Nextcloud Talk requires bot secret or --secret-file (or --use-env).";
2030
- const normalizedBaseUrl = normalizeNextcloudTalkBaseUrl(setupInput.baseUrl);
2031
- if (!normalizedBaseUrl) return "Nextcloud Talk requires --base-url.";
2032
- const baseUrlError = validateNextcloudTalkBaseUrl(normalizedBaseUrl);
2033
- if (baseUrlError) return baseUrlError;
2034
- return null;
2035
- }
2036
- }),
2037
- applyAccountConfig: ({ cfg, accountId, input }) => {
2038
- const setupInput = input;
2039
- return setNextcloudTalkAccountConfig(applyAccountNameToChannelSection({
2040
- cfg,
2041
- channelKey: channel$1,
2042
- accountId,
2043
- name: setupInput.name
2044
- }), accountId, {
2045
- baseUrl: normalizeNextcloudTalkBaseUrl(setupInput.baseUrl),
2046
- ...setupInput.useEnv ? {} : setupInput.secretFile ? { botSecretFile: setupInput.secretFile } : setupInput.secret ? { botSecret: setupInput.secret } : {}
2047
- }, setupInput.useEnv ? ["botSecret", "botSecretFile"] : void 0);
2048
- }
2049
- }
2077
+ legacyAdapter: nextcloudTalkSetupAdapter
2050
2078
  });
2051
2079
  //#endregion
2052
2080
  //#region extensions/nextcloud-talk/src/setup-surface.ts
@@ -2231,10 +2259,10 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
2231
2259
  configSchema: buildChannelConfigSchema(NextcloudTalkConfigSchema),
2232
2260
  config: {
2233
2261
  ...nextcloudTalkConfigAdapter,
2234
- isConfigured: (account) => Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim()),
2262
+ isConfigured: isNextcloudTalkAccountConfigured,
2235
2263
  describeAccount: (account) => describeWebhookAccountSnapshot({
2236
2264
  account,
2237
- configured: Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim()),
2265
+ configured: isNextcloudTalkAccountConfigured(account),
2238
2266
  extra: {
2239
2267
  secretSource: account.secretSource,
2240
2268
  tokenStatus: account.tokenStatus,
@@ -2290,7 +2318,7 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
2290
2318
  accountId: account.accountId,
2291
2319
  name: account.name,
2292
2320
  enabled: account.enabled,
2293
- configured: Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim()),
2321
+ configured: isNextcloudTalkAccountConfigured(account),
2294
2322
  extra: {
2295
2323
  secretSource: account.secretSource,
2296
2324
  tokenStatus: account.tokenStatus,
@@ -1,2 +1,2 @@
1
- import { t as nextcloudTalkPlugin } from "./channel-tgvjpexV.js";
1
+ import { t as nextcloudTalkPlugin } from "./channel-D2RVrR4p.js";
2
2
  export { nextcloudTalkPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/nextcloud-talk",
3
- "version": "2026.9.1-beta.1",
3
+ "version": "2026.9.1",
4
4
  "description": "OpenClaw Nextcloud Talk channel plugin for conversations.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "type": "module",
10
10
  "peerDependencies": {
11
- "openclaw": ">=2026.9.1-beta.1"
11
+ "openclaw": ">=2026.9.1"
12
12
  },
13
13
  "peerDependenciesMeta": {
14
14
  "openclaw": {
@@ -129,10 +129,10 @@
129
129
  "minHostVersion": ">=2026.4.10"
130
130
  },
131
131
  "compat": {
132
- "pluginApi": ">=2026.9.1-beta.1"
132
+ "pluginApi": ">=2026.9.1"
133
133
  },
134
134
  "build": {
135
- "openclawVersion": "2026.9.1-beta.1"
135
+ "openclawVersion": "2026.9.1"
136
136
  },
137
137
  "release": {
138
138
  "publishToClawHub": true,