@openclaw/nextcloud-talk 2026.8.1-beta.3 → 2026.8.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-gYP-jQPr.js";
1
+ import { t as nextcloudTalkPlugin } from "./channel-C7Xe0Nkb.js";
2
2
  export { nextcloudTalkPlugin };
@@ -15,19 +15,19 @@ import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
15
15
  import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
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";
@@ -147,13 +147,6 @@ function resolveNextcloudTalkAccount(params) {
147
147
  const accountEnabled = merged.enabled !== false;
148
148
  const enabled = baseEnabled && accountEnabled;
149
149
  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
150
  const baseUrl = merged.baseUrl?.trim()?.replace(/\/$/, "") ?? "";
158
151
  debugAccounts("resolve", {
159
152
  accountId,
@@ -169,8 +162,7 @@ function resolveNextcloudTalkAccount(params) {
169
162
  secret: secretResolution.secret,
170
163
  secretSource: secretResolution.source,
171
164
  tokenStatus: secretResolution.status,
172
- apiCredentialStatus: apiCredentialResolution.status,
173
- ...diagnostics.length > 0 ? { credentialDiagnostics: diagnostics } : {},
165
+ ...secretResolution.diagnostic ? { credentialDiagnostics: [secretResolution.diagnostic] } : {},
174
166
  config: merged
175
167
  };
176
168
  };
@@ -182,6 +174,21 @@ function resolveNextcloudTalkAccount(params) {
182
174
  resolveDefaultAccountId: () => resolveDefaultNextcloudTalkAccountId(params.cfg)
183
175
  });
184
176
  }
177
+ function inspectNextcloudTalkAccount(params) {
178
+ const account = resolveNextcloudTalkAccount(params);
179
+ const apiCredentialResolution = resolveNextcloudTalkApiCredentialsResult({
180
+ apiUser: account.config.apiUser,
181
+ apiPassword: account.config.apiPassword,
182
+ apiPasswordFile: account.config.apiPasswordFile,
183
+ configPath: `channels.nextcloud-talk.accounts.${account.accountId}.apiPasswordFile`
184
+ });
185
+ const credentialDiagnostics = [...account.credentialDiagnostics ?? [], ...apiCredentialResolution.status === "configured_unavailable" ? [apiCredentialResolution.diagnostic] : []];
186
+ return {
187
+ ...account,
188
+ apiCredentialStatus: apiCredentialResolution.status,
189
+ ...credentialDiagnostics.length > 0 ? { credentialDiagnostics } : {}
190
+ };
191
+ }
185
192
  //#endregion
186
193
  //#region extensions/nextcloud-talk/src/approval-auth.ts
187
194
  function normalizeNextcloudTalkApproverId(value) {
@@ -203,6 +210,18 @@ async function releaseNextcloudTalkGuardedResponse(params) {
203
210
  if (!params.response.bodyUsed) await params.response.body?.cancel().catch(() => void 0);
204
211
  await params.release();
205
212
  }
213
+ async function readNextcloudTalkErrorBody(response, ...credentials) {
214
+ try {
215
+ let body = await readProviderTextResponse(response, "Nextcloud Talk error", {
216
+ maxBytes: 8 * 1024,
217
+ chunkTimeoutMs: 1e4
218
+ });
219
+ for (const credential of credentials) body = body.replaceAll(credential, "***");
220
+ return redactToolPayloadText(body);
221
+ } catch {
222
+ return "";
223
+ }
224
+ }
206
225
  //#endregion
207
226
  //#region extensions/nextcloud-talk/src/signature.ts
208
227
  const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
@@ -249,7 +268,6 @@ function generateNextcloudTalkSignature(params) {
249
268
  //#endregion
250
269
  //#region extensions/nextcloud-talk/src/bot-preflight.ts
251
270
  const BOT_FEATURE_RESPONSE = 2;
252
- const BOT_PREFLIGHT_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
253
271
  function normalizeUrlForMatch(value) {
254
272
  if (!value?.trim()) return "";
255
273
  try {
@@ -314,7 +332,7 @@ async function probeNextcloudTalkBotResponseFeature(params) {
314
332
  });
315
333
  try {
316
334
  if (!response.ok) {
317
- const body = await readResponseTextLimited(response, BOT_PREFLIGHT_ERROR_BODY_LIMIT_BYTES).catch(() => "");
335
+ const body = await readNextcloudTalkErrorBody(response, auth, credentials.apiPassword);
318
336
  return {
319
337
  ok: false,
320
338
  code: "api_error",
@@ -366,6 +384,7 @@ const nextcloudTalkConfigAdapter = createScopedChannelConfigAdapter({
366
384
  sectionKey: "nextcloud-talk",
367
385
  listAccountIds: listNextcloudTalkAccountIds,
368
386
  resolveAccount: adaptScopedAccountAccessor(resolveNextcloudTalkAccount),
387
+ inspectAccount: adaptScopedAccountAccessor(inspectNextcloudTalkAccount),
369
388
  defaultAccountId: resolveDefaultNextcloudTalkAccountId,
370
389
  clearBaseFields: [
371
390
  "botSecret",
@@ -401,6 +420,7 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
401
420
  name: z.string().optional(),
402
421
  enabled: z.boolean().optional(),
403
422
  configWrites: z.boolean().optional(),
423
+ replyToMode: ReplyToModeSchema.optional(),
404
424
  markdown: MarkdownConfigSchema,
405
425
  baseUrl: z.string().optional(),
406
426
  botSecret: buildSecretInputSchema().optional(),
@@ -696,7 +716,6 @@ function looksLikeNextcloudTalkTargetId(raw) {
696
716
  }
697
717
  //#endregion
698
718
  //#region extensions/nextcloud-talk/src/send.ts
699
- const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES = 8 * 1024;
700
719
  const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS = 200;
701
720
  const NEXTCLOUD_TALK_SEND_TIMEOUT_MS = 3e4;
702
721
  const NEXTCLOUD_TALK_FORMAT_PROFILE = FormatCapabilityProfile.define({
@@ -717,20 +736,12 @@ function renderNextcloudTalkMarkdown(markdown) {
717
736
  escapeText: (text) => text
718
737
  }, NEXTCLOUD_TALK_FORMAT_PROFILE);
719
738
  }
720
- /** Collapses whitespace and caps an error-body prefix to a short, log-safe snippet. */
739
+ /** Collapses and caps an already-redacted error body for display. */
721
740
  function collapseErrorSnippet(text) {
722
741
  const collapsed = text.replace(/\s+/g, " ").trim();
723
742
  if (collapsed.length > NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS) return `${truncateUtf16Safe(collapsed, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS)}…`;
724
743
  return collapsed;
725
744
  }
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
745
  function resolveCredentials(explicit, account) {
735
746
  const baseUrl = explicit.baseUrl?.trim() ?? account.baseUrl;
736
747
  const secret = explicit.secret?.trim() ?? account.secret;
@@ -821,7 +832,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
821
832
  });
822
833
  try {
823
834
  if (!response.ok) {
824
- const errorBody = await readNextcloudTalkErrorSnippet(response);
835
+ const errorBody = collapseErrorSnippet(await readNextcloudTalkErrorBody(response, signature));
825
836
  const status = response.status;
826
837
  let errorMsg = `Nextcloud Talk send failed (${status})`;
827
838
  if (status === 400) errorMsg = `Nextcloud Talk: bad request - ${errorBody || "invalid message format"}`;
@@ -883,7 +894,7 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
883
894
  });
884
895
  try {
885
896
  if (!response.ok) {
886
- const errorBody = await readNextcloudTalkErrorSnippet(response);
897
+ const errorBody = collapseErrorSnippet(await readNextcloudTalkErrorBody(response, signature));
887
898
  throw new Error(`Nextcloud Talk reaction failed: ${response.status} ${errorBody}`.trim());
888
899
  }
889
900
  return { ok: true };
@@ -954,7 +965,15 @@ async function handleNextcloudTalkInbound(params) {
954
965
  accountId: account.accountId
955
966
  });
956
967
  const rawBody = message.text?.trim() ?? "";
957
- if (!rawBody) return;
968
+ if (!rawBody) {
969
+ logInboundDrop({
970
+ log: (messageLocal) => runtime.log?.(messageLocal),
971
+ channel: CHANNEL_ID,
972
+ reason: `empty message body (mediaType=${message.mediaType})`,
973
+ target: message.senderId
974
+ });
975
+ return;
976
+ }
958
977
  const roomKind = await resolveNextcloudTalkRoomKind({
959
978
  account,
960
979
  roomToken: message.roomToken,
@@ -1414,6 +1433,7 @@ function createNextcloudTalkWebhookServer(opts) {
1414
1433
  const stop = async () => {
1415
1434
  stopRequested = true;
1416
1435
  await closeIfListening();
1436
+ webhookAuthRateLimiter.dispose();
1417
1437
  };
1418
1438
  const start = () => {
1419
1439
  if (stopRequested) return Promise.resolve();
@@ -1440,6 +1460,12 @@ function createNextcloudTalkWebhookServer(opts) {
1440
1460
  //#endregion
1441
1461
  //#region extensions/nextcloud-talk/src/webhook-spool.ts
1442
1462
  const NEXTCLOUD_TALK_INGRESS_POLL_INTERVAL_MS = 500;
1463
+ function describeIgnoredWebhookEvent(rawEvent) {
1464
+ const envelope = parseRawObject(rawEvent);
1465
+ const type = typeof envelope.type === "string" ? envelope.type : "unknown";
1466
+ const object = isRecord(envelope.object) ? envelope.object : null;
1467
+ return `type=${type} objectType=${typeof object?.type === "string" ? object.type : "unknown"}`;
1468
+ }
1443
1469
  const NextcloudTalkWebhookPayloadSchema = z.object({
1444
1470
  type: z.enum([
1445
1471
  "Create",
@@ -1566,7 +1592,11 @@ function createNextcloudTalkWebhookSpool(options) {
1566
1592
  if (stopping) return Promise.reject(/* @__PURE__ */ new Error("Nextcloud Talk ingress stopped"));
1567
1593
  const receiveTask = (async () => {
1568
1594
  await startAfterMigration;
1569
- return (await monitor.admit(rawEvent)).kind === "ignored" ? "ignored" : "accepted";
1595
+ if ((await monitor.admit(rawEvent)).kind === "ignored") {
1596
+ options.runtime.log?.(`nextcloud-talk: ignored non-message webhook event (${describeIgnoredWebhookEvent(rawEvent)})`);
1597
+ return "ignored";
1598
+ }
1599
+ return "accepted";
1570
1600
  })();
1571
1601
  inFlightReceives.add(receiveTask);
1572
1602
  receiveTask.then(() => inFlightReceives.delete(receiveTask), () => inFlightReceives.delete(receiveTask));
@@ -1710,44 +1740,18 @@ const nextcloudTalkGatewayAdapter = {
1710
1740
  });
1711
1741
  },
1712
1742
  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
- }
1743
+ const { nextConfig, changed, cleared } = clearAccountFieldsFromConfigSection({
1744
+ cfg,
1745
+ sectionKey: "nextcloud-talk",
1746
+ accountId,
1747
+ fields: ["botSecret"]
1748
+ });
1745
1749
  const loggedOut = resolveNextcloudTalkAccount({
1746
- cfg: changed ? nextCfg : cfg,
1750
+ cfg: nextConfig,
1747
1751
  accountId
1748
1752
  }).secretSource === "none";
1749
1753
  if (changed) await getNextcloudTalkRuntime().config.replaceConfigFile({
1750
- nextConfig: nextCfg,
1754
+ nextConfig,
1751
1755
  afterWrite: { mode: "auto" }
1752
1756
  });
1753
1757
  return {
@@ -1,2 +1,2 @@
1
- import { t as nextcloudTalkPlugin } from "./channel-gYP-jQPr.js";
1
+ import { t as nextcloudTalkPlugin } from "./channel-C7Xe0Nkb.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.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.8.1-beta.3"
11
+ "openclaw": ">=2026.8.1"
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.1"
131
133
  },
132
134
  "build": {
133
- "openclawVersion": "2026.8.1-beta.3"
135
+ "openclawVersion": "2026.8.1"
134
136
  },
135
137
  "release": {
136
138
  "publishToClawHub": true,