@openclaw/nextcloud-talk 2026.8.1-beta.3 → 2026.8.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 CHANGED
@@ -1,2 +1,2 @@
1
- import { t as nextcloudTalkPlugin } from "./channel-gYP-jQPr.js";
1
+ import { t as nextcloudTalkPlugin } from "./channel-n-lgbvCj.js";
2
2
  export { nextcloudTalkPlugin };
@@ -7,27 +7,27 @@ 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
26
  import { fileExists, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
26
- import { clearAccountEntryFields } from "openclaw/plugin-sdk/channel-plugin-common";
27
27
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2 } from "openclaw/plugin-sdk/account-id";
28
- import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, buildChannelConfigSchema, buildGroupEntrySchema, buildMultiAccountChannelSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
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";
@@ -53,17 +53,19 @@ import { formatDocsLink as formatDocsLink$1 } from "openclaw/plugin-sdk/setup-to
53
53
  function resolveNextcloudTalkApiCredentialsResult(params) {
54
54
  const apiUser = params.apiUser?.trim();
55
55
  if (!apiUser) return { status: "missing" };
56
- const inlinePassword = normalizeResolvedSecretInputString({
56
+ const inlinePassword = resolveSecretInputString({
57
57
  value: params.apiPassword,
58
- path: "channels.nextcloud-talk.apiPassword"
58
+ path: "channels.nextcloud-talk.apiPassword",
59
+ mode: params.mode
59
60
  });
60
- if (inlinePassword) return {
61
+ if (inlinePassword.status === "available") return {
61
62
  status: "available",
62
63
  value: {
63
64
  apiUser,
64
- apiPassword: inlinePassword
65
+ apiPassword: inlinePassword.value
65
66
  }
66
67
  };
68
+ if (inlinePassword.status === "configured_unavailable") return { status: "configured_unavailable" };
67
69
  if (!params.apiPasswordFile?.trim()) return { status: "missing" };
68
70
  const result = tryReadSecretFileSync(params.apiPasswordFile, "Nextcloud Talk API password", { rejectHardlinks: false }, { configPath: params.configPath ?? "channels.nextcloud-talk.apiPasswordFile" });
69
71
  if (result.status === "available") return result.value ? {
@@ -147,13 +149,6 @@ function resolveNextcloudTalkAccount(params) {
147
149
  const accountEnabled = merged.enabled !== false;
148
150
  const enabled = baseEnabled && accountEnabled;
149
151
  const secretResolution = resolveNextcloudTalkSecret(accountId, merged);
150
- const apiCredentialResolution = resolveNextcloudTalkApiCredentialsResult({
151
- apiUser: merged.apiUser,
152
- apiPassword: merged.apiPassword,
153
- apiPasswordFile: merged.apiPasswordFile,
154
- configPath: `channels.nextcloud-talk.accounts.${accountId}.apiPasswordFile`
155
- });
156
- const diagnostics = [secretResolution.diagnostic, apiCredentialResolution.status === "configured_unavailable" ? apiCredentialResolution.diagnostic : void 0].filter((diagnostic) => Boolean(diagnostic));
157
152
  const baseUrl = merged.baseUrl?.trim()?.replace(/\/$/, "") ?? "";
158
153
  debugAccounts("resolve", {
159
154
  accountId,
@@ -169,8 +164,7 @@ function resolveNextcloudTalkAccount(params) {
169
164
  secret: secretResolution.secret,
170
165
  secretSource: secretResolution.source,
171
166
  tokenStatus: secretResolution.status,
172
- apiCredentialStatus: apiCredentialResolution.status,
173
- ...diagnostics.length > 0 ? { credentialDiagnostics: diagnostics } : {},
167
+ ...secretResolution.diagnostic ? { credentialDiagnostics: [secretResolution.diagnostic] } : {},
174
168
  config: merged
175
169
  };
176
170
  };
@@ -182,6 +176,27 @@ function resolveNextcloudTalkAccount(params) {
182
176
  resolveDefaultAccountId: () => resolveDefaultNextcloudTalkAccountId(params.cfg)
183
177
  });
184
178
  }
179
+ function inspectNextcloudTalkAccount(params) {
180
+ const account = resolveNextcloudTalkAccount(params);
181
+ const apiCredentialResolution = resolveNextcloudTalkApiCredentialsResult({
182
+ apiUser: account.config.apiUser,
183
+ apiPassword: account.config.apiPassword,
184
+ apiPasswordFile: account.config.apiPasswordFile,
185
+ configPath: `channels.nextcloud-talk.accounts.${account.accountId}.apiPasswordFile`,
186
+ mode: "inspect"
187
+ });
188
+ const credentialDiagnostics = [...account.credentialDiagnostics ?? [], ...apiCredentialResolution.status === "configured_unavailable" && apiCredentialResolution.diagnostic ? [apiCredentialResolution.diagnostic] : []];
189
+ return {
190
+ ...account,
191
+ configured: isNextcloudTalkAccountConfigured(account),
192
+ mode: "webhook",
193
+ apiCredentialStatus: apiCredentialResolution.status,
194
+ ...credentialDiagnostics.length > 0 ? { credentialDiagnostics } : {}
195
+ };
196
+ }
197
+ function isNextcloudTalkAccountConfigured(account) {
198
+ return Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim());
199
+ }
185
200
  //#endregion
186
201
  //#region extensions/nextcloud-talk/src/approval-auth.ts
187
202
  function normalizeNextcloudTalkApproverId(value) {
@@ -203,6 +218,18 @@ async function releaseNextcloudTalkGuardedResponse(params) {
203
218
  if (!params.response.bodyUsed) await params.response.body?.cancel().catch(() => void 0);
204
219
  await params.release();
205
220
  }
221
+ async function readNextcloudTalkErrorBody(response, ...credentials) {
222
+ try {
223
+ let body = await readProviderTextResponse(response, "Nextcloud Talk error", {
224
+ maxBytes: 8192,
225
+ chunkTimeoutMs: 1e4
226
+ });
227
+ for (const credential of credentials) body = body.replaceAll(credential, "***");
228
+ return redactToolPayloadText(body);
229
+ } catch {
230
+ return "";
231
+ }
232
+ }
206
233
  //#endregion
207
234
  //#region extensions/nextcloud-talk/src/signature.ts
208
235
  const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
@@ -215,7 +242,8 @@ const BACKEND_HEADER = "x-nextcloud-talk-backend";
215
242
  function verifyNextcloudTalkSignature(params) {
216
243
  const { signature, random, body, secret } = params;
217
244
  if (!signature || !random || !secret) return false;
218
- return safeEqualSecret(signature, createHmac("sha256", secret).update(random + body).digest("hex"));
245
+ const expected = createHmac("sha256", secret).update(random + body).digest("hex");
246
+ return safeEqualSecret(signature, expected);
219
247
  }
220
248
  /**
221
249
  * Extract webhook headers from an incoming request.
@@ -249,7 +277,6 @@ function generateNextcloudTalkSignature(params) {
249
277
  //#endregion
250
278
  //#region extensions/nextcloud-talk/src/bot-preflight.ts
251
279
  const BOT_FEATURE_RESPONSE = 2;
252
- const BOT_PREFLIGHT_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
253
280
  function normalizeUrlForMatch(value) {
254
281
  if (!value?.trim()) return "";
255
282
  try {
@@ -314,7 +341,7 @@ async function probeNextcloudTalkBotResponseFeature(params) {
314
341
  });
315
342
  try {
316
343
  if (!response.ok) {
317
- const body = await readResponseTextLimited(response, BOT_PREFLIGHT_ERROR_BODY_LIMIT_BYTES).catch(() => "");
344
+ const body = await readNextcloudTalkErrorBody(response, auth, credentials.apiPassword);
318
345
  return {
319
346
  ok: false,
320
347
  code: "api_error",
@@ -366,6 +393,16 @@ const nextcloudTalkConfigAdapter = createScopedChannelConfigAdapter({
366
393
  sectionKey: "nextcloud-talk",
367
394
  listAccountIds: listNextcloudTalkAccountIds,
368
395
  resolveAccount: adaptScopedAccountAccessor(resolveNextcloudTalkAccount),
396
+ inspectAccount: (cfg, accountId) => {
397
+ const account = inspectNextcloudTalkAccount({
398
+ cfg,
399
+ accountId
400
+ });
401
+ return {
402
+ ...account,
403
+ baseUrl: account.baseUrl ? "[set]" : "[missing]"
404
+ };
405
+ },
369
406
  defaultAccountId: resolveDefaultNextcloudTalkAccountId,
370
407
  clearBaseFields: [
371
408
  "botSecret",
@@ -397,10 +434,11 @@ const NextcloudTalkRoomSchema = buildGroupEntrySchema({ allowFrom: z.array(z.str
397
434
  const NextcloudTalkNetworkSchema = z.object({
398
435
  /** Dangerous opt-in for self-hosted Nextcloud Talk on trusted private/internal hosts. */
399
436
  dangerouslyAllowPrivateNetwork: z.boolean().optional() }).strict().optional();
400
- const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
437
+ const NextcloudTalkAccountSchemaBase = z.object({
401
438
  name: z.string().optional(),
402
439
  enabled: z.boolean().optional(),
403
440
  configWrites: z.boolean().optional(),
441
+ replyToMode: ReplyToModeSchema.optional(),
404
442
  markdown: MarkdownConfigSchema,
405
443
  baseUrl: z.string().optional(),
406
444
  botSecret: buildSecretInputSchema().optional(),
@@ -420,7 +458,8 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
420
458
  /** Network policy overrides for self-hosted Nextcloud Talk on trusted private/internal hosts. */
421
459
  network: NextcloudTalkNetworkSchema,
422
460
  ...ReplyRuntimeConfigSchemaShape
423
- }).strict(), {
461
+ }).strict();
462
+ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(NextcloudTalkAccountSchemaBase, {
424
463
  optionalAccount: true,
425
464
  refine: (value, ctx) => {
426
465
  requireChannelOpenAllowFrom({
@@ -436,7 +475,7 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
436
475
  //#region extensions/nextcloud-talk/src/replay-migration-contract.ts
437
476
  const NEXTCLOUD_TALK_PLUGIN_ID = "nextcloud-talk";
438
477
  const NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX = "replay-dedupe";
439
- const NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS = 1440 * 60 * 1e3;
478
+ const NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS = 864e5;
440
479
  const NEXTCLOUD_TALK_REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
441
480
  //#endregion
442
481
  //#region extensions/nextcloud-talk/src/doctor.ts
@@ -516,10 +555,12 @@ function normalizeNextcloudTalkAllowlist(values) {
516
555
  return (values ?? []).map((value) => normalizeNextcloudTalkAllowEntry(String(value))).filter(Boolean);
517
556
  }
518
557
  function resolveNextcloudTalkAllowlistMatch(params) {
558
+ const allowFrom = normalizeNextcloudTalkAllowlist(params.allowFrom);
559
+ const senderId = normalizeNextcloudTalkAllowEntry(params.senderId);
519
560
  return resolveAllowlistMatchByCandidates({
520
- allowList: normalizeNextcloudTalkAllowlist(params.allowFrom),
561
+ allowList: allowFrom,
521
562
  candidates: [{
522
- value: normalizeNextcloudTalkAllowEntry(params.senderId),
563
+ value: senderId,
523
564
  source: "id"
524
565
  }]
525
566
  });
@@ -587,8 +628,8 @@ function resolveNextcloudTalkGroupRequireMention(params) {
587
628
  }
588
629
  //#endregion
589
630
  //#region extensions/nextcloud-talk/src/room-info.ts
590
- const ROOM_CACHE_TTL_MS = 300 * 1e3;
591
- const ROOM_CACHE_ERROR_TTL_MS = 30 * 1e3;
631
+ const ROOM_CACHE_TTL_MS = 3e5;
632
+ const ROOM_CACHE_ERROR_TTL_MS = 3e4;
592
633
  const ROOM_CACHE_MAX_ENTRIES = 1e3;
593
634
  const NEXTCLOUD_TALK_ROOM_INFO_TIMEOUT_MS = 3e4;
594
635
  const roomCache = /* @__PURE__ */ new Map();
@@ -650,7 +691,8 @@ async function resolveNextcloudTalkRoomKind(params) {
650
691
  runtime?.log?.(`nextcloud-talk: room lookup failed (${response.status}) token=${roomToken}`);
651
692
  return;
652
693
  }
653
- const kind = resolveRoomKindFromType(parseStrictPositiveInteger((await readProviderJsonResponse(response, "Nextcloud Talk room info failed")).ocs?.data?.type));
694
+ const payload = await readProviderJsonResponse(response, "Nextcloud Talk room info failed");
695
+ const kind = resolveRoomKindFromType(parseStrictPositiveInteger(payload.ocs?.data?.type));
654
696
  cacheRoomInfo(key, {
655
697
  fetchedAt: Date.now(),
656
698
  kind
@@ -696,7 +738,6 @@ function looksLikeNextcloudTalkTargetId(raw) {
696
738
  }
697
739
  //#endregion
698
740
  //#region extensions/nextcloud-talk/src/send.ts
699
- const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES = 8 * 1024;
700
741
  const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS = 200;
701
742
  const NEXTCLOUD_TALK_SEND_TIMEOUT_MS = 3e4;
702
743
  const NEXTCLOUD_TALK_FORMAT_PROFILE = FormatCapabilityProfile.define({
@@ -717,20 +758,12 @@ function renderNextcloudTalkMarkdown(markdown) {
717
758
  escapeText: (text) => text
718
759
  }, NEXTCLOUD_TALK_FORMAT_PROFILE);
719
760
  }
720
- /** Collapses whitespace and caps an error-body prefix to a short, log-safe snippet. */
761
+ /** Collapses and caps an already-redacted error body for display. */
721
762
  function collapseErrorSnippet(text) {
722
763
  const collapsed = text.replace(/\s+/g, " ").trim();
723
764
  if (collapsed.length > NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS) return `${truncateUtf16Safe(collapsed, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS)}…`;
724
765
  return collapsed;
725
766
  }
726
- /** Reads a bounded, collapsed error-body snippet without buffering hostile responses. */
727
- async function readNextcloudTalkErrorSnippet(response) {
728
- try {
729
- return collapseErrorSnippet(await readResponseTextLimited(response, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES));
730
- } catch {
731
- return "";
732
- }
733
- }
734
767
  function resolveCredentials(explicit, account) {
735
768
  const baseUrl = explicit.baseUrl?.trim() ?? account.baseUrl;
736
769
  const secret = explicit.secret?.trim() ?? account.secret;
@@ -803,8 +836,9 @@ async function sendMessageNextcloudTalk(to, text, opts) {
803
836
  body: message,
804
837
  secret
805
838
  });
839
+ const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${roomToken}/message`;
806
840
  const { response, release } = await fetchWithSsrFGuard({
807
- url: `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${roomToken}/message`,
841
+ url,
808
842
  init: {
809
843
  method: "POST",
810
844
  headers: {
@@ -821,7 +855,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
821
855
  });
822
856
  try {
823
857
  if (!response.ok) {
824
- const errorBody = await readNextcloudTalkErrorSnippet(response);
858
+ const errorBody = collapseErrorSnippet(await readNextcloudTalkErrorBody(response, signature));
825
859
  const status = response.status;
826
860
  let errorMsg = `Nextcloud Talk send failed (${status})`;
827
861
  if (status === 400) errorMsg = `Nextcloud Talk: bad request - ${errorBody || "invalid message format"}`;
@@ -865,8 +899,9 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
865
899
  body: reaction,
866
900
  secret
867
901
  });
902
+ const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${normalizedToken}/reaction/${messageId}`;
868
903
  const { response, release } = await fetchWithSsrFGuard({
869
- url: `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${normalizedToken}/reaction/${messageId}`,
904
+ url,
870
905
  init: {
871
906
  method: "POST",
872
907
  headers: {
@@ -883,7 +918,7 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
883
918
  });
884
919
  try {
885
920
  if (!response.ok) {
886
- const errorBody = await readNextcloudTalkErrorSnippet(response);
921
+ const errorBody = collapseErrorSnippet(await readNextcloudTalkErrorBody(response, signature));
887
922
  throw new Error(`Nextcloud Talk reaction failed: ${response.status} ${errorBody}`.trim());
888
923
  }
889
924
  return { ok: true };
@@ -954,7 +989,15 @@ async function handleNextcloudTalkInbound(params) {
954
989
  accountId: account.accountId
955
990
  });
956
991
  const rawBody = message.text?.trim() ?? "";
957
- if (!rawBody) return;
992
+ if (!rawBody) {
993
+ logInboundDrop({
994
+ log: (messageLocal) => runtime.log?.(messageLocal),
995
+ channel: CHANNEL_ID,
996
+ reason: `empty message body (mediaType=${message.mediaType})`,
997
+ target: message.senderId
998
+ });
999
+ return;
1000
+ }
958
1001
  const roomKind = await resolveNextcloudTalkRoomKind({
959
1002
  account,
960
1003
  roomToken: message.roomToken,
@@ -1270,8 +1313,8 @@ async function migrateNextcloudTalkLegacyReplayState(params) {
1270
1313
  }
1271
1314
  //#endregion
1272
1315
  //#region extensions/nextcloud-talk/src/monitor.ts
1273
- const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
1274
- const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 64 * 1024;
1316
+ const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1048576;
1317
+ const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 65536;
1275
1318
  const NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_HEADER = "x-openclaw-delivery-accepted";
1276
1319
  const NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_VALUE = "durable";
1277
1320
  const PREAUTH_WEBHOOK_BODY_TIMEOUT_MS = 5e3;
@@ -1414,6 +1457,7 @@ function createNextcloudTalkWebhookServer(opts) {
1414
1457
  const stop = async () => {
1415
1458
  stopRequested = true;
1416
1459
  await closeIfListening();
1460
+ webhookAuthRateLimiter.dispose();
1417
1461
  };
1418
1462
  const start = () => {
1419
1463
  if (stopRequested) return Promise.resolve();
@@ -1429,8 +1473,10 @@ function createNextcloudTalkWebhookServer(opts) {
1429
1473
  });
1430
1474
  });
1431
1475
  };
1432
- if (abortSignal) if (abortSignal.aborted) stop();
1433
- else abortSignal.addEventListener("abort", () => void stop(), { once: true });
1476
+ if (abortSignal) {
1477
+ if (abortSignal.aborted) stop();
1478
+ else abortSignal.addEventListener("abort", () => void stop(), { once: true });
1479
+ }
1434
1480
  return {
1435
1481
  server,
1436
1482
  start,
@@ -1440,6 +1486,12 @@ function createNextcloudTalkWebhookServer(opts) {
1440
1486
  //#endregion
1441
1487
  //#region extensions/nextcloud-talk/src/webhook-spool.ts
1442
1488
  const NEXTCLOUD_TALK_INGRESS_POLL_INTERVAL_MS = 500;
1489
+ function describeIgnoredWebhookEvent(rawEvent) {
1490
+ const envelope = parseRawObject(rawEvent);
1491
+ const type = typeof envelope.type === "string" ? envelope.type : "unknown";
1492
+ const object = isRecord(envelope.object) ? envelope.object : null;
1493
+ return `type=${type} objectType=${typeof object?.type === "string" ? object.type : "unknown"}`;
1494
+ }
1443
1495
  const NextcloudTalkWebhookPayloadSchema = z.object({
1444
1496
  type: z.enum([
1445
1497
  "Create",
@@ -1566,7 +1618,11 @@ function createNextcloudTalkWebhookSpool(options) {
1566
1618
  if (stopping) return Promise.reject(/* @__PURE__ */ new Error("Nextcloud Talk ingress stopped"));
1567
1619
  const receiveTask = (async () => {
1568
1620
  await startAfterMigration;
1569
- return (await monitor.admit(rawEvent)).kind === "ignored" ? "ignored" : "accepted";
1621
+ if ((await monitor.admit(rawEvent)).kind === "ignored") {
1622
+ options.runtime.log?.(`nextcloud-talk: ignored non-message webhook event (${describeIgnoredWebhookEvent(rawEvent)})`);
1623
+ return "ignored";
1624
+ }
1625
+ return "accepted";
1570
1626
  })();
1571
1627
  inFlightReceives.add(receiveTask);
1572
1628
  receiveTask.then(() => inFlightReceives.delete(receiveTask), () => inFlightReceives.delete(receiveTask));
@@ -1710,44 +1766,18 @@ const nextcloudTalkGatewayAdapter = {
1710
1766
  });
1711
1767
  },
1712
1768
  logoutAccount: async ({ accountId, cfg }) => {
1713
- const nextCfg = { ...cfg };
1714
- const nextSection = cfg.channels?.["nextcloud-talk"] ? { ...cfg.channels["nextcloud-talk"] } : void 0;
1715
- let cleared = false;
1716
- let changed = false;
1717
- if (nextSection) {
1718
- if (accountId === DEFAULT_ACCOUNT_ID$2 && nextSection.botSecret) {
1719
- delete nextSection.botSecret;
1720
- cleared = true;
1721
- changed = true;
1722
- }
1723
- const accountCleanup = clearAccountEntryFields({
1724
- accounts: nextSection.accounts,
1725
- accountId,
1726
- fields: ["botSecret"]
1727
- });
1728
- if (accountCleanup.changed) {
1729
- changed = true;
1730
- if (accountCleanup.cleared) cleared = true;
1731
- if (accountCleanup.nextAccounts) nextSection.accounts = accountCleanup.nextAccounts;
1732
- else delete nextSection.accounts;
1733
- }
1734
- }
1735
- if (changed) if (nextSection && Object.keys(nextSection).length > 0) nextCfg.channels = {
1736
- ...nextCfg.channels,
1737
- "nextcloud-talk": nextSection
1738
- };
1739
- else {
1740
- const nextChannels = { ...nextCfg.channels };
1741
- delete nextChannels["nextcloud-talk"];
1742
- if (Object.keys(nextChannels).length > 0) nextCfg.channels = nextChannels;
1743
- else delete nextCfg.channels;
1744
- }
1769
+ const { nextConfig, changed, cleared } = clearAccountFieldsFromConfigSection({
1770
+ cfg,
1771
+ sectionKey: "nextcloud-talk",
1772
+ accountId,
1773
+ fields: ["botSecret"]
1774
+ });
1745
1775
  const loggedOut = resolveNextcloudTalkAccount({
1746
- cfg: changed ? nextCfg : cfg,
1776
+ cfg: nextConfig,
1747
1777
  accountId
1748
1778
  }).secretSource === "none";
1749
1779
  if (changed) await getNextcloudTalkRuntime().config.replaceConfigFile({
1750
- nextConfig: nextCfg,
1780
+ nextConfig,
1751
1781
  afterWrite: { mode: "auto" }
1752
1782
  });
1753
1783
  return {
@@ -1929,6 +1959,49 @@ const nextcloudTalkDmPolicy = createChannelDmPolicy({
1929
1959
  applyPatch: ({ cfg, account, patch }) => setNextcloudTalkAccountConfig(cfg, account.accountId, patch),
1930
1960
  promptAllowFrom: promptNextcloudTalkAllowFromForAccount
1931
1961
  });
1962
+ const nextcloudTalkSetupAdapter = {
1963
+ singleAccountKeysToMove: ["rooms"],
1964
+ resolveAccountId: ({ accountId }) => normalizeAccountId$1(accountId),
1965
+ prepareAccountConfigInput: ({ input }) => {
1966
+ const setupInput = input;
1967
+ return {
1968
+ ...setupInput,
1969
+ baseUrl: setupInput.baseUrl ?? readNonEmptyStringPreservingWhitespace(setupInput.url),
1970
+ secret: setupInput.secret ?? readNonEmptyStringPreservingWhitespace(setupInput.token) ?? readNonEmptyStringPreservingWhitespace(setupInput.password),
1971
+ secretFile: setupInput.secretFile ?? readNonEmptyStringPreservingWhitespace(setupInput.tokenFile)
1972
+ };
1973
+ },
1974
+ applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
1975
+ cfg,
1976
+ channelKey: channel$1,
1977
+ accountId,
1978
+ name
1979
+ }),
1980
+ validateInput: createSetupInputPresenceValidator({
1981
+ defaultAccountOnlyEnvError: "NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.",
1982
+ validate: ({ input }) => {
1983
+ const setupInput = input;
1984
+ if (!setupInput.useEnv && !setupInput.secret && !setupInput.secretFile) return "Nextcloud Talk requires bot secret or --secret-file (or --use-env).";
1985
+ const normalizedBaseUrl = normalizeNextcloudTalkBaseUrl(setupInput.baseUrl);
1986
+ if (!normalizedBaseUrl) return "Nextcloud Talk requires --base-url.";
1987
+ const baseUrlError = validateNextcloudTalkBaseUrl(normalizedBaseUrl);
1988
+ if (baseUrlError) return baseUrlError;
1989
+ return null;
1990
+ }
1991
+ }),
1992
+ applyAccountConfig: ({ cfg, accountId, input }) => {
1993
+ const setupInput = input;
1994
+ return setNextcloudTalkAccountConfig(applyAccountNameToChannelSection({
1995
+ cfg,
1996
+ channelKey: channel$1,
1997
+ accountId,
1998
+ name: setupInput.name
1999
+ }), accountId, {
2000
+ baseUrl: normalizeNextcloudTalkBaseUrl(setupInput.baseUrl),
2001
+ ...setupInput.useEnv ? {} : setupInput.secretFile ? { botSecretFile: setupInput.secretFile } : setupInput.secret ? { botSecret: setupInput.secret } : {}
2002
+ }, setupInput.useEnv ? ["botSecret", "botSecretFile"] : void 0);
2003
+ }
2004
+ };
1932
2005
  const nextcloudTalkSetupContract = defineChannelSetupContract({
1933
2006
  fields: {
1934
2007
  baseUrl: {
@@ -1994,49 +2067,7 @@ const nextcloudTalkSetupContract = defineChannelSetupContract({
1994
2067
  envVars: ["NEXTCLOUD_TALK_BOT_SECRET"]
1995
2068
  }
1996
2069
  },
1997
- legacyAdapter: {
1998
- singleAccountKeysToMove: ["rooms"],
1999
- resolveAccountId: ({ accountId }) => normalizeAccountId$1(accountId),
2000
- prepareAccountConfigInput: ({ input }) => {
2001
- const setupInput = input;
2002
- return {
2003
- ...setupInput,
2004
- baseUrl: setupInput.baseUrl ?? readNonEmptyStringPreservingWhitespace(setupInput.url),
2005
- secret: setupInput.secret ?? readNonEmptyStringPreservingWhitespace(setupInput.token) ?? readNonEmptyStringPreservingWhitespace(setupInput.password),
2006
- secretFile: setupInput.secretFile ?? readNonEmptyStringPreservingWhitespace(setupInput.tokenFile)
2007
- };
2008
- },
2009
- applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
2010
- cfg,
2011
- channelKey: channel$1,
2012
- accountId,
2013
- name
2014
- }),
2015
- validateInput: createSetupInputPresenceValidator({
2016
- defaultAccountOnlyEnvError: "NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.",
2017
- validate: ({ input }) => {
2018
- const setupInput = input;
2019
- if (!setupInput.useEnv && !setupInput.secret && !setupInput.secretFile) return "Nextcloud Talk requires bot secret or --secret-file (or --use-env).";
2020
- const normalizedBaseUrl = normalizeNextcloudTalkBaseUrl(setupInput.baseUrl);
2021
- if (!normalizedBaseUrl) return "Nextcloud Talk requires --base-url.";
2022
- const baseUrlError = validateNextcloudTalkBaseUrl(normalizedBaseUrl);
2023
- if (baseUrlError) return baseUrlError;
2024
- return null;
2025
- }
2026
- }),
2027
- applyAccountConfig: ({ cfg, accountId, input }) => {
2028
- const setupInput = input;
2029
- return setNextcloudTalkAccountConfig(applyAccountNameToChannelSection({
2030
- cfg,
2031
- channelKey: channel$1,
2032
- accountId,
2033
- name: setupInput.name
2034
- }), accountId, {
2035
- baseUrl: normalizeNextcloudTalkBaseUrl(setupInput.baseUrl),
2036
- ...setupInput.useEnv ? {} : setupInput.secretFile ? { botSecretFile: setupInput.secretFile } : setupInput.secret ? { botSecret: setupInput.secret } : {}
2037
- }, setupInput.useEnv ? ["botSecret", "botSecretFile"] : void 0);
2038
- }
2039
- }
2070
+ legacyAdapter: nextcloudTalkSetupAdapter
2040
2071
  });
2041
2072
  //#endregion
2042
2073
  //#region extensions/nextcloud-talk/src/setup-surface.ts
@@ -2221,10 +2252,10 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
2221
2252
  configSchema: buildChannelConfigSchema(NextcloudTalkConfigSchema),
2222
2253
  config: {
2223
2254
  ...nextcloudTalkConfigAdapter,
2224
- isConfigured: (account) => Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim()),
2255
+ isConfigured: isNextcloudTalkAccountConfigured,
2225
2256
  describeAccount: (account) => describeWebhookAccountSnapshot({
2226
2257
  account,
2227
- configured: Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim()),
2258
+ configured: isNextcloudTalkAccountConfigured(account),
2228
2259
  extra: {
2229
2260
  secretSource: account.secretSource,
2230
2261
  tokenStatus: account.tokenStatus,
@@ -2280,7 +2311,7 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
2280
2311
  accountId: account.accountId,
2281
2312
  name: account.name,
2282
2313
  enabled: account.enabled,
2283
- configured: Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim()),
2314
+ configured: isNextcloudTalkAccountConfigured(account),
2284
2315
  extra: {
2285
2316
  secretSource: account.secretSource,
2286
2317
  tokenStatus: account.tokenStatus,
@@ -1,2 +1,2 @@
1
- import { t as nextcloudTalkPlugin } from "./channel-gYP-jQPr.js";
1
+ import { t as nextcloudTalkPlugin } from "./channel-n-lgbvCj.js";
2
2
  export { nextcloudTalkPlugin };
@@ -0,0 +1,25 @@
1
+ import { DEFAULT_ACCOUNT_ID, hasConfiguredAccountValue, mergeAccountConfig } from "openclaw/plugin-sdk/account-core";
2
+ //#region extensions/nextcloud-talk/configured-state.ts
3
+ function hasConfiguredNextcloudAccount(account, env) {
4
+ return Boolean(account?.baseUrl?.trim() && (hasConfiguredAccountValue(account.botSecret) || hasConfiguredAccountValue(account.botSecretFile) || hasConfiguredAccountValue(env.NEXTCLOUD_TALK_BOT_SECRET)));
5
+ }
6
+ /** Require a Nextcloud server plus its account-owned bot credential. */
7
+ function hasConfiguredNextcloudTalkChannelState(params) {
8
+ const channel = params.cfg.channels?.["nextcloud-talk"];
9
+ if (channel?.enabled === false) return false;
10
+ const defaultAccount = channel?.accounts?.[DEFAULT_ACCOUNT_ID];
11
+ if (defaultAccount?.enabled !== false) {
12
+ if (hasConfiguredNextcloudAccount(defaultAccount ? mergeAccountConfig({
13
+ channelConfig: channel,
14
+ accountConfig: defaultAccount,
15
+ omitKeys: ["defaultAccount"]
16
+ }) : channel, params.env ?? process.env)) return true;
17
+ }
18
+ return Object.entries(channel?.accounts ?? {}).some(([accountId, account]) => accountId !== DEFAULT_ACCOUNT_ID && account.enabled !== false && hasConfiguredNextcloudAccount(mergeAccountConfig({
19
+ channelConfig: channel,
20
+ accountConfig: account,
21
+ omitKeys: ["defaultAccount"]
22
+ }), {}));
23
+ }
24
+ //#endregion
25
+ export { hasConfiguredNextcloudTalkChannelState };
@@ -32,6 +32,26 @@
32
32
  "configWrites": {
33
33
  "type": "boolean"
34
34
  },
35
+ "replyToMode": {
36
+ "anyOf": [
37
+ {
38
+ "type": "string",
39
+ "const": "off"
40
+ },
41
+ {
42
+ "type": "string",
43
+ "const": "first"
44
+ },
45
+ {
46
+ "type": "string",
47
+ "const": "all"
48
+ },
49
+ {
50
+ "type": "string",
51
+ "const": "batched"
52
+ }
53
+ ]
54
+ },
35
55
  "markdown": {
36
56
  "type": "object",
37
57
  "properties": {
@@ -479,6 +499,26 @@
479
499
  "configWrites": {
480
500
  "type": "boolean"
481
501
  },
502
+ "replyToMode": {
503
+ "anyOf": [
504
+ {
505
+ "type": "string",
506
+ "const": "off"
507
+ },
508
+ {
509
+ "type": "string",
510
+ "const": "first"
511
+ },
512
+ {
513
+ "type": "string",
514
+ "const": "all"
515
+ },
516
+ {
517
+ "type": "string",
518
+ "const": "batched"
519
+ }
520
+ ]
521
+ },
482
522
  "markdown": {
483
523
  "type": "object",
484
524
  "properties": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/nextcloud-talk",
3
- "version": "2026.8.1-beta.3",
3
+ "version": "2026.8.2",
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.8.1-beta.3"
11
+ "openclaw": ">=2026.8.2"
12
12
  },
13
13
  "peerDependenciesMeta": {
14
14
  "openclaw": {
@@ -31,7 +31,9 @@
31
31
  "NEXTCLOUD_TALK_BOT_SECRET",
32
32
  "NEXTCLOUD_TALK_API_PASSWORD"
33
33
  ]
34
- }
34
+ },
35
+ "specifier": "./dist/configured-state.js",
36
+ "exportName": "hasConfiguredNextcloudTalkChannelState"
35
37
  },
36
38
  "label": "Nextcloud Talk",
37
39
  "selectionLabel": "Nextcloud Talk (self-hosted)",
@@ -127,10 +129,10 @@
127
129
  "minHostVersion": ">=2026.4.10"
128
130
  },
129
131
  "compat": {
130
- "pluginApi": ">=2026.8.1-beta.3"
132
+ "pluginApi": ">=2026.8.2"
131
133
  },
132
134
  "build": {
133
- "openclawVersion": "2026.8.1-beta.3"
135
+ "openclawVersion": "2026.8.2"
134
136
  },
135
137
  "release": {
136
138
  "publishToClawHub": true,