@openclaw/nextcloud-talk 2026.7.2-beta.7 → 2026.7.34

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.
@@ -1,91 +1,59 @@
1
- import { a as fetchWithSsrFGuard, c as resolveDefaultGroupPolicy, i as deliverFormattedTextWithAttachments, l as warnMissingProviderGroupPolicyFallbackOnce, o as logInboundDrop, r as createChannelPairingController, s as resolveAllowlistProviderRuntimeGroupPolicy, t as GROUP_POLICY_BLOCKED_LABEL, u as getNextcloudTalkRuntime } from "./runtime-api-BKsxTw4J.js";
2
- import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-fNhK1wag.js";
3
- import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-DGgvfVqm.js";
4
- import { createAccountListHelpers, describeWebhookAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
1
+ import { a as fetchWithSsrFGuard, c as resolveDefaultGroupPolicy, i as deliverFormattedTextWithAttachments, l as warnMissingProviderGroupPolicyFallbackOnce, o as logInboundDrop, r as createChannelPairingController, s as resolveAllowlistProviderRuntimeGroupPolicy, t as GROUP_POLICY_BLOCKED_LABEL, u as getNextcloudTalkRuntime } from "./runtime-api-BKsxTw4J.mjs";
2
+ import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-BY7I5-v9.mjs";
3
+ import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-CsCJ-vOP.mjs";
4
+ import { describeWebhookAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
5
5
  import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
6
6
  import { createLoggedPairingApprovalNotifier, createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
7
- import { createAllowlistProviderRouteAllowlistWarningCollector, resolveScopeRequireMention, resolveScopeToolsPolicy } from "openclaw/plugin-sdk/channel-policy";
7
+ import { createAllowlistProviderRouteAllowlistWarningCollector } from "openclaw/plugin-sdk/channel-policy";
8
8
  import { buildWebhookChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
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 } from "openclaw/plugin-sdk/string-coerce-runtime";
12
- import { DEFAULT_ACCOUNT_ID, hasConfiguredAccountValue, normalizeAccountId, resolveAccountWithDefaultFallback } from "openclaw/plugin-sdk/account-core";
13
- import { isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env";
9
+ import { convertMarkdownTables, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
10
+ import { DEFAULT_ACCOUNT_ID, createAccountListHelpers, hasConfiguredAccountValue, normalizeAccountId, resolveAccountWithDefaultFallback, resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-core";
14
11
  import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
12
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
15
13
  import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
14
+ import { createResolvedApproverActionAuthAdapter, resolveApprovalApprovers } from "openclaw/plugin-sdk/approval-auth-runtime";
16
15
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
17
16
  import { parseStrictNonNegativeInteger, parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
18
17
  import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
19
- import { buildChannelInboundEventContext, resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
20
- import { bindIngressLifecycleToReplyOptions, createAccountStatusSink, createChannelIngressMonitor, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, resolveChannelStreamingBlockEnabled, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-outbound";
18
+ import { createAccountStatusSink, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-outbound";
21
19
  import { ssrfPolicyFromPrivateNetworkOptIn, ssrfPolicyFromPrivateNetworkOptIn as ssrfPolicyFromPrivateNetworkOptIn$1 } from "openclaw/plugin-sdk/ssrf-runtime";
20
+ import { readFileSync } from "node:fs";
22
21
  import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
23
22
  import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
24
- import { createHmac, randomBytes } from "node:crypto";
25
- import { fileExists, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
23
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
26
24
  import { clearAccountEntryFields } from "openclaw/plugin-sdk/channel-plugin-common";
27
25
  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";
29
- import { formatAllowFromLowercase, resolveAllowlistMatchByCandidates } from "openclaw/plugin-sdk/allow-from";
26
+ import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, ToolPolicySchema, buildChannelConfigSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
27
+ import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
30
28
  import { adaptScopedAccountAccessor, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
31
- import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
29
+ import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime, runStoppablePassiveMonitor, safeParseJsonWithSchema } from "openclaw/plugin-sdk/extension-shared";
32
30
  import { z } from "zod";
31
+ import fs from "node:fs/promises";
33
32
  import os from "node:os";
34
33
  import path from "node:path";
35
- import { migratePersistentDedupeLegacyJsonFile, resolvePersistentDedupePluginStateNamespace } from "openclaw/plugin-sdk/persistent-dedupe";
34
+ import { createClaimableDedupe, migratePersistentDedupeLegacyJsonFile } from "openclaw/plugin-sdk/persistent-dedupe";
36
35
  import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
37
36
  import { channelIngressRoutes, resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
38
- import { buildChannelKeyCandidates, normalizeChannelSlug, resolveChannelEntryMatchWithFallback } from "openclaw/plugin-sdk/channel-targets";
37
+ import { resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk/inbound-envelope";
38
+ import { buildChannelKeyCandidates, normalizeChannelSlug, resolveChannelEntryMatchWithFallback, resolveNestedAllowlistDecision } from "openclaw/plugin-sdk/channel-targets";
39
39
  import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
40
- import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
41
40
  import { createServer } from "node:http";
42
41
  import { WEBHOOK_RATE_LIMIT_DEFAULTS, createAuthRateLimiter, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText } from "openclaw/plugin-sdk/webhook-ingress";
43
- import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
44
42
  import { jsonResult, readStringParam, resolveReactionMessageId } from "openclaw/plugin-sdk/channel-actions";
45
43
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, buildOutboundBaseSessionKey, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/routing";
46
- import { createChannelDmPolicy } from "openclaw/plugin-sdk/channel-dm-policy";
47
- import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
48
- import { applyAccountNameToChannelSection, baseUrlTextInput, createSetupTranslator, createStandardChannelSetupStatus, defineTokenCredential, formatDocsLink, patchScopedAccountConfig, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
44
+ import { applyAccountNameToChannelSection, createSetupTranslator, createStandardChannelSetupStatus, formatDocsLink, patchScopedAccountConfig, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
49
45
  import { createSetupInputPresenceValidator, createSetupTranslator as createSetupTranslator$1, mergeAllowFromEntries, promptParsedAllowFromForAccount, resolveSetupAccountId } from "openclaw/plugin-sdk/setup-runtime";
50
46
  import { formatDocsLink as formatDocsLink$1 } from "openclaw/plugin-sdk/setup-tools";
51
- //#region extensions/nextcloud-talk/src/api-credentials.ts
52
- function resolveNextcloudTalkApiCredentialsResult(params) {
53
- const apiUser = params.apiUser?.trim();
54
- if (!apiUser) return { status: "missing" };
55
- const inlinePassword = normalizeResolvedSecretInputString({
56
- value: params.apiPassword,
57
- path: "channels.nextcloud-talk.apiPassword"
58
- });
59
- if (inlinePassword) return {
60
- status: "available",
61
- value: {
62
- apiUser,
63
- apiPassword: inlinePassword
64
- }
65
- };
66
- if (!params.apiPasswordFile?.trim()) return { status: "missing" };
67
- const result = tryReadSecretFileSync(params.apiPasswordFile, "Nextcloud Talk API password", { rejectHardlinks: false }, { configPath: params.configPath ?? "channels.nextcloud-talk.apiPasswordFile" });
68
- if (result.status === "available") return result.value ? {
69
- status: "available",
70
- value: {
71
- apiUser,
72
- apiPassword: result.value
73
- }
74
- } : { status: "missing" };
75
- return result;
76
- }
77
- function resolveNextcloudTalkApiCredentials(params) {
78
- const result = resolveNextcloudTalkApiCredentialsResult(params);
79
- return result.status === "available" ? result.value : void 0;
80
- }
81
- //#endregion
82
47
  //#region extensions/nextcloud-talk/src/accounts.ts
48
+ function isTruthyEnvValue(value) {
49
+ const normalized = normalizeLowercaseStringOrEmpty(value);
50
+ return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on";
51
+ }
83
52
  const debugAccounts = (...args) => {
84
53
  if (isTruthyEnvValue(process.env.OPENCLAW_DEBUG_NEXTCLOUD_TALK_ACCOUNTS)) console.warn("[nextcloud-talk:accounts]", ...args);
85
54
  };
86
- const { listAccountIds: listNextcloudTalkAccountIdsInternal, resolveDefaultAccountId: resolveDefaultNextcloudTalkAccountId, resolveAccountConfig: mergeNextcloudTalkAccountConfig } = createAccountListHelpers("nextcloud-talk", {
55
+ const { listAccountIds: listNextcloudTalkAccountIdsInternal, resolveDefaultAccountId: resolveDefaultNextcloudTalkAccountId } = createAccountListHelpers("nextcloud-talk", {
87
56
  normalizeAccountId,
88
- omitKeys: ["defaultAccount"],
89
57
  hasImplicitDefaultAccount: (cfg) => {
90
58
  const channel = cfg.channels?.["nextcloud-talk"];
91
59
  return Boolean(channel?.baseUrl?.trim() && (hasConfiguredAccountValue(channel.botSecret) || channel.botSecretFile?.trim() || process.env.NEXTCLOUD_TALK_BOT_SECRET?.trim()));
@@ -96,27 +64,28 @@ function listNextcloudTalkAccountIds(cfg) {
96
64
  debugAccounts("listNextcloudTalkAccountIds", ids);
97
65
  return ids;
98
66
  }
67
+ function mergeNextcloudTalkAccountConfig(cfg, accountId) {
68
+ return resolveMergedAccountConfig({
69
+ channelConfig: cfg.channels?.["nextcloud-talk"],
70
+ accounts: cfg.channels?.["nextcloud-talk"]?.accounts,
71
+ accountId,
72
+ omitKeys: ["defaultAccount"],
73
+ normalizeAccountId
74
+ });
75
+ }
99
76
  function resolveNextcloudTalkSecret(cfg, opts) {
100
77
  const resolvedAccountId = opts.accountId ?? resolveDefaultNextcloudTalkAccountId(cfg);
101
78
  const merged = mergeNextcloudTalkAccountConfig(cfg, resolvedAccountId);
102
79
  const envSecret = normalizeOptionalString(process.env.NEXTCLOUD_TALK_BOT_SECRET);
103
80
  if (envSecret && resolvedAccountId === DEFAULT_ACCOUNT_ID) return {
104
81
  secret: envSecret,
105
- source: "env",
106
- status: "available"
82
+ source: "env"
107
83
  };
108
- const botSecretFile = normalizeOptionalString(merged.botSecretFile);
109
- if (botSecretFile) {
110
- const result = tryReadSecretFileSync(botSecretFile, "Nextcloud Talk bot secret file", { rejectSymlink: true }, { configPath: `channels.nextcloud-talk.accounts.${resolvedAccountId}.botSecretFile` });
111
- return result.status === "available" ? {
112
- secret: result.value,
113
- source: "secretFile",
114
- status: "available"
115
- } : {
116
- secret: "",
117
- source: "secretFile",
118
- status: "configured_unavailable",
119
- diagnostic: result.diagnostic
84
+ if (merged.botSecretFile) {
85
+ const fileSecret = tryReadSecretFileSync(merged.botSecretFile, "Nextcloud Talk bot secret file", { rejectSymlink: true });
86
+ if (fileSecret) return {
87
+ secret: fileSecret,
88
+ source: "secretFile"
120
89
  };
121
90
  }
122
91
  const inlineSecret = normalizeResolvedSecretInputString({
@@ -125,13 +94,11 @@ function resolveNextcloudTalkSecret(cfg, opts) {
125
94
  });
126
95
  if (inlineSecret) return {
127
96
  secret: inlineSecret,
128
- source: "config",
129
- status: "available"
97
+ source: "config"
130
98
  };
131
99
  return {
132
100
  secret: "",
133
- source: "none",
134
- status: "missing"
101
+ source: "none"
135
102
  };
136
103
  }
137
104
  function resolveNextcloudTalkAccount(params) {
@@ -142,13 +109,6 @@ function resolveNextcloudTalkAccount(params) {
142
109
  const accountEnabled = merged.enabled !== false;
143
110
  const enabled = baseEnabled && accountEnabled;
144
111
  const secretResolution = resolveNextcloudTalkSecret(params.cfg, { accountId });
145
- const apiCredentialResolution = resolveNextcloudTalkApiCredentialsResult({
146
- apiUser: merged.apiUser,
147
- apiPassword: merged.apiPassword,
148
- apiPasswordFile: merged.apiPasswordFile,
149
- configPath: `channels.nextcloud-talk.accounts.${accountId}.apiPasswordFile`
150
- });
151
- const diagnostics = [secretResolution.diagnostic, apiCredentialResolution.status === "configured_unavailable" ? apiCredentialResolution.diagnostic : void 0].filter((diagnostic) => Boolean(diagnostic));
152
112
  const baseUrl = merged.baseUrl?.trim()?.replace(/\/$/, "") ?? "";
153
113
  debugAccounts("resolve", {
154
114
  accountId,
@@ -163,9 +123,6 @@ function resolveNextcloudTalkAccount(params) {
163
123
  baseUrl,
164
124
  secret: secretResolution.secret,
165
125
  secretSource: secretResolution.source,
166
- tokenStatus: secretResolution.status,
167
- apiCredentialStatus: apiCredentialResolution.status,
168
- ...diagnostics.length > 0 ? { credentialDiagnostics: diagnostics } : {},
169
126
  config: merged
170
127
  };
171
128
  };
@@ -173,7 +130,7 @@ function resolveNextcloudTalkAccount(params) {
173
130
  accountId: resolvedAccountId,
174
131
  normalizeAccountId,
175
132
  resolvePrimary: resolve,
176
- hasCredential: (account) => account.tokenStatus !== "missing",
133
+ hasCredential: (account) => account.secretSource !== "none",
177
134
  resolveDefaultAccountId: () => resolveDefaultNextcloudTalkAccountId(params.cfg)
178
135
  });
179
136
  }
@@ -182,21 +139,43 @@ function resolveNextcloudTalkAccount(params) {
182
139
  function normalizeNextcloudTalkApproverId(value) {
183
140
  return normalizeOptionalLowercaseString(String(value).trim().replace(/^(nextcloud-talk|nc-talk|nc):/i, ""));
184
141
  }
185
- const nextcloudTalkApprovalAuth = createChannelApprovalAuth({
142
+ const nextcloudTalkApprovalAuth = createResolvedApproverActionAuthAdapter({
186
143
  channelLabel: "Nextcloud Talk",
187
- resolveInputs: ({ cfg, accountId }) => {
188
- return { allowFrom: resolveNextcloudTalkAccount({
144
+ resolveApprovers: ({ cfg, accountId }) => {
145
+ const account = resolveNextcloudTalkAccount({
189
146
  cfg,
190
147
  accountId
191
- }).config.allowFrom };
148
+ });
149
+ return resolveApprovalApprovers({
150
+ allowFrom: account.config.allowFrom,
151
+ normalizeApprover: normalizeNextcloudTalkApproverId
152
+ });
192
153
  },
193
- normalizeApprover: normalizeNextcloudTalkApproverId
194
- }).approvalAuth;
154
+ normalizeSenderId: (value) => normalizeNextcloudTalkApproverId(value)
155
+ });
195
156
  //#endregion
196
- //#region extensions/nextcloud-talk/src/guarded-response.ts
197
- async function releaseNextcloudTalkGuardedResponse(params) {
198
- if (!params.response.bodyUsed) await params.response.body?.cancel().catch(() => void 0);
199
- await params.release();
157
+ //#region extensions/nextcloud-talk/src/api-credentials.ts
158
+ function resolveNextcloudTalkApiCredentials(params) {
159
+ const apiUser = params.apiUser?.trim();
160
+ if (!apiUser) return;
161
+ const inlinePassword = normalizeResolvedSecretInputString({
162
+ value: params.apiPassword,
163
+ path: "channels.nextcloud-talk.apiPassword"
164
+ });
165
+ if (inlinePassword) return {
166
+ apiUser,
167
+ apiPassword: inlinePassword
168
+ };
169
+ if (!params.apiPasswordFile) return;
170
+ try {
171
+ const filePassword = readFileSync(params.apiPasswordFile, "utf-8").trim();
172
+ return filePassword ? {
173
+ apiUser,
174
+ apiPassword: filePassword
175
+ } : void 0;
176
+ } catch {
177
+ return;
178
+ }
200
179
  }
201
180
  //#endregion
202
181
  //#region extensions/nextcloud-talk/src/signature.ts
@@ -210,7 +189,16 @@ const BACKEND_HEADER = "x-nextcloud-talk-backend";
210
189
  function verifyNextcloudTalkSignature(params) {
211
190
  const { signature, random, body, secret } = params;
212
191
  if (!signature || !random || !secret) return false;
213
- return safeEqualSecret(signature, createHmac("sha256", secret).update(random + body).digest("hex"));
192
+ const expected = createHmac("sha256", secret).update(random + body).digest("hex");
193
+ const expectedBuf = Buffer.from(expected, "utf8");
194
+ const signatureBuf = Buffer.from(signature, "utf8");
195
+ const maxLen = Math.max(expectedBuf.length, signatureBuf.length);
196
+ const paddedExpected = Buffer.alloc(maxLen);
197
+ const paddedSignature = Buffer.alloc(maxLen);
198
+ expectedBuf.copy(paddedExpected);
199
+ signatureBuf.copy(paddedSignature);
200
+ const timingResult = timingSafeEqual(paddedExpected, paddedSignature);
201
+ return expectedBuf.length === signatureBuf.length && timingResult;
214
202
  }
215
203
  /**
216
204
  * Extract webhook headers from an incoming request.
@@ -244,7 +232,7 @@ function generateNextcloudTalkSignature(params) {
244
232
  //#endregion
245
233
  //#region extensions/nextcloud-talk/src/bot-preflight.ts
246
234
  const BOT_FEATURE_RESPONSE = 2;
247
- const BOT_PREFLIGHT_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
235
+ const BOT_PREFLIGHT_ERROR_BODY_LIMIT_BYTES = 8192;
248
236
  function normalizeUrlForMatch(value) {
249
237
  if (!value?.trim()) return "";
250
238
  try {
@@ -342,10 +330,7 @@ async function probeNextcloudTalkBotResponseFeature(params) {
342
330
  message: `Nextcloud Talk bot "${bot.name ?? bot.id ?? "matching bot"}" has the response feature.`
343
331
  };
344
332
  } finally {
345
- await releaseNextcloudTalkGuardedResponse({
346
- response,
347
- release
348
- });
333
+ await release();
349
334
  }
350
335
  } catch (error) {
351
336
  return {
@@ -388,11 +373,18 @@ const nextcloudTalkPairingTextAdapter = {
388
373
  };
389
374
  //#endregion
390
375
  //#region extensions/nextcloud-talk/src/config-schema.ts
391
- const NextcloudTalkRoomSchema = buildGroupEntrySchema({ allowFrom: z.array(z.string()).optional() }).omit({ toolsBySender: true });
376
+ const NextcloudTalkRoomSchema = z.object({
377
+ requireMention: z.boolean().optional(),
378
+ tools: ToolPolicySchema,
379
+ skills: z.array(z.string()).optional(),
380
+ enabled: z.boolean().optional(),
381
+ allowFrom: z.array(z.string()).optional(),
382
+ systemPrompt: z.string().optional()
383
+ }).strict();
392
384
  const NextcloudTalkNetworkSchema = z.object({
393
385
  /** Dangerous opt-in for self-hosted Nextcloud Talk on trusted private/internal hosts. */
394
386
  dangerouslyAllowPrivateNetwork: z.boolean().optional() }).strict().optional();
395
- const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
387
+ const NextcloudTalkAccountSchemaBase = z.object({
396
388
  name: z.string().optional(),
397
389
  enabled: z.boolean().optional(),
398
390
  markdown: MarkdownConfigSchema,
@@ -414,31 +406,114 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
414
406
  /** Network policy overrides for self-hosted Nextcloud Talk on trusted private/internal hosts. */
415
407
  network: NextcloudTalkNetworkSchema,
416
408
  ...ReplyRuntimeConfigSchemaShape
417
- }).strict(), {
418
- optionalAccount: true,
419
- refine: (value, ctx) => {
420
- requireChannelOpenAllowFrom({
421
- channel: "nextcloud-talk",
422
- policy: value.dmPolicy,
423
- allowFrom: value.allowFrom,
424
- ctx,
425
- requireOpenAllowFrom
426
- });
427
- }
409
+ }).strict();
410
+ const NextcloudTalkAccountSchema = NextcloudTalkAccountSchemaBase.superRefine((value, ctx) => {
411
+ requireChannelOpenAllowFrom({
412
+ channel: "nextcloud-talk",
413
+ policy: value.dmPolicy,
414
+ allowFrom: value.allowFrom,
415
+ ctx,
416
+ requireOpenAllowFrom
417
+ });
418
+ });
419
+ const NextcloudTalkConfigSchema = NextcloudTalkAccountSchemaBase.extend({
420
+ accounts: z.record(z.string(), NextcloudTalkAccountSchema.optional()).optional(),
421
+ defaultAccount: z.string().optional()
422
+ }).superRefine((value, ctx) => {
423
+ requireChannelOpenAllowFrom({
424
+ channel: "nextcloud-talk",
425
+ policy: value.dmPolicy,
426
+ allowFrom: value.allowFrom,
427
+ ctx,
428
+ requireOpenAllowFrom
429
+ });
428
430
  });
429
431
  //#endregion
430
- //#region extensions/nextcloud-talk/src/replay-migration-contract.ts
432
+ //#region extensions/nextcloud-talk/src/replay-guard.ts
431
433
  const NEXTCLOUD_TALK_PLUGIN_ID = "nextcloud-talk";
432
434
  const NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX = "replay-dedupe";
433
- const NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS = 1440 * 60 * 1e3;
434
- const NEXTCLOUD_TALK_REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
435
+ const DEFAULT_REPLAY_TTL_MS = 864e5;
436
+ const DEFAULT_MEMORY_MAX_SIZE = 1e3;
437
+ const DEFAULT_STATE_MAX_ENTRIES = 1e4;
438
+ function buildReplayKey(params) {
439
+ const roomToken = params.roomToken.trim();
440
+ const messageId = params.messageId.trim();
441
+ if (!roomToken || !messageId) return null;
442
+ return `${roomToken}:${messageId}`;
443
+ }
444
+ function createNextcloudTalkReplayGuard(options) {
445
+ const stateDir = options.stateDir?.trim();
446
+ const baseOptions = {
447
+ ttlMs: options.ttlMs ?? DEFAULT_REPLAY_TTL_MS,
448
+ memoryMaxSize: options.memoryMaxSize ?? DEFAULT_MEMORY_MAX_SIZE
449
+ };
450
+ const dedupe = createClaimableDedupe(stateDir ? {
451
+ ...baseOptions,
452
+ pluginId: NEXTCLOUD_TALK_PLUGIN_ID,
453
+ namespacePrefix: NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX,
454
+ stateMaxEntries: options.stateMaxEntries ?? options.fileMaxEntries ?? DEFAULT_STATE_MAX_ENTRIES,
455
+ env: {
456
+ ...process.env,
457
+ OPENCLAW_STATE_DIR: stateDir
458
+ },
459
+ onDiskError: options.onDiskError
460
+ } : baseOptions);
461
+ return {
462
+ claimMessage: async ({ accountId, roomToken, messageId }) => {
463
+ const replayKey = buildReplayKey({
464
+ roomToken,
465
+ messageId
466
+ });
467
+ if (!replayKey) return "invalid";
468
+ return (await dedupe.claim(replayKey, { namespace: accountId })).kind;
469
+ },
470
+ commitMessage: async ({ accountId, roomToken, messageId }) => {
471
+ const replayKey = buildReplayKey({
472
+ roomToken,
473
+ messageId
474
+ });
475
+ if (!replayKey) return true;
476
+ return await dedupe.commit(replayKey, { namespace: accountId });
477
+ },
478
+ releaseMessage: ({ accountId, roomToken, messageId, error }) => {
479
+ const replayKey = buildReplayKey({
480
+ roomToken,
481
+ messageId
482
+ });
483
+ if (!replayKey) return;
484
+ dedupe.release(replayKey, {
485
+ namespace: accountId,
486
+ error
487
+ });
488
+ },
489
+ shouldProcessMessage: async ({ accountId, roomToken, messageId }) => {
490
+ const replayKey = buildReplayKey({
491
+ roomToken,
492
+ messageId
493
+ });
494
+ if (!replayKey) return true;
495
+ if ((await dedupe.claim(replayKey, { namespace: accountId })).kind !== "claimed") return false;
496
+ return await dedupe.commit(replayKey, { namespace: accountId });
497
+ }
498
+ };
499
+ }
435
500
  //#endregion
436
501
  //#region extensions/nextcloud-talk/src/doctor.ts
502
+ const REPLAY_DEDUPE_TTL_MS = 864e5;
503
+ const REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
437
504
  function sanitizeLegacyReplaySegment(value) {
438
505
  const trimmed = value.trim();
439
506
  if (!trimmed) return "default";
440
507
  return trimmed.replace(/[^a-zA-Z0-9_-]/g, "_");
441
508
  }
509
+ async function fileExists(filePath) {
510
+ try {
511
+ await fs.access(filePath);
512
+ return true;
513
+ } catch {
514
+ return false;
515
+ }
516
+ }
442
517
  async function collectNextcloudTalkBotResponseWarnings(params) {
443
518
  const warnings = [];
444
519
  for (const accountId of listNextcloudTalkAccountIds(params.cfg)) {
@@ -463,16 +538,16 @@ async function repairNextcloudTalkReplayDedupeState(params) {
463
538
  const replayDir = path.join(stateDir, "nextcloud-talk", "replay-dedupe");
464
539
  for (const accountId of listNextcloudTalkAccountIds(params.cfg)) {
465
540
  const legacyPath = path.join(replayDir, `${sanitizeLegacyReplaySegment(accountId)}.json`);
466
- if (!fileExists(legacyPath)) continue;
541
+ if (!await fileExists(legacyPath)) continue;
467
542
  try {
468
543
  const result = await migratePersistentDedupeLegacyJsonFile({
469
544
  filePath: legacyPath,
470
545
  namespace: accountId,
471
- ttlMs: NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS,
546
+ ttlMs: REPLAY_DEDUPE_TTL_MS,
472
547
  memoryMaxSize: 0,
473
548
  pluginId: NEXTCLOUD_TALK_PLUGIN_ID,
474
549
  namespacePrefix: NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX,
475
- stateMaxEntries: NEXTCLOUD_TALK_REPLAY_DEDUPE_MAX_ENTRIES,
550
+ stateMaxEntries: REPLAY_DEDUPE_MAX_ENTRIES,
476
551
  env
477
552
  });
478
553
  changes.push(`Migrated Nextcloud Talk replay dedupe cache for account "${accountId}" to SQLite (${result.imported} imported, ${result.skippedExpired} expired, ${result.skippedExisting} already current).`);
@@ -510,25 +585,38 @@ function normalizeNextcloudTalkAllowlist(values) {
510
585
  return (values ?? []).map((value) => normalizeNextcloudTalkAllowEntry(String(value))).filter(Boolean);
511
586
  }
512
587
  function resolveNextcloudTalkAllowlistMatch(params) {
513
- return resolveAllowlistMatchByCandidates({
514
- allowList: normalizeNextcloudTalkAllowlist(params.allowFrom),
515
- candidates: [{
516
- value: normalizeNextcloudTalkAllowEntry(params.senderId),
517
- source: "id"
518
- }]
519
- });
588
+ const allowFrom = normalizeNextcloudTalkAllowlist(params.allowFrom);
589
+ if (allowFrom.length === 0) return { allowed: false };
590
+ if (allowFrom.includes("*")) return {
591
+ allowed: true,
592
+ matchKey: "*",
593
+ matchSource: "wildcard"
594
+ };
595
+ const senderId = normalizeNextcloudTalkAllowEntry(params.senderId);
596
+ if (allowFrom.includes(senderId)) return {
597
+ allowed: true,
598
+ matchKey: senderId,
599
+ matchSource: "id"
600
+ };
601
+ return { allowed: false };
520
602
  }
521
603
  function resolveNextcloudTalkRoomMatch(params) {
522
604
  const rooms = params.rooms ?? {};
523
605
  const allowlistConfigured = Object.keys(rooms).length > 0;
606
+ const roomCandidates = buildChannelKeyCandidates(params.roomToken);
524
607
  const match = resolveChannelEntryMatchWithFallback({
525
608
  entries: rooms,
526
- keys: buildChannelKeyCandidates(params.roomToken),
609
+ keys: roomCandidates,
527
610
  wildcardKey: "*",
528
611
  normalizeKey: normalizeChannelSlug
529
612
  });
530
613
  const roomConfig = match.entry;
531
- const allowed = !allowlistConfigured || Boolean(roomConfig);
614
+ const allowed = resolveNestedAllowlistDecision({
615
+ outerConfigured: allowlistConfigured,
616
+ outerMatched: Boolean(roomConfig),
617
+ innerConfigured: false,
618
+ innerMatched: false
619
+ });
532
620
  return {
533
621
  roomConfig,
534
622
  wildcardConfig: match.wildcardEntry,
@@ -539,50 +627,24 @@ function resolveNextcloudTalkRoomMatch(params) {
539
627
  };
540
628
  }
541
629
  function resolveNextcloudTalkGroupToolPolicy(params) {
630
+ const cfg = params.cfg;
542
631
  const roomToken = params.groupId?.trim();
543
632
  if (!roomToken) return;
544
- const { tree, toolsPath } = buildNextcloudTalkRoomScope(resolveNextcloudTalkAccount({
545
- cfg: params.cfg,
546
- accountId: params.accountId
547
- }).config.rooms, roomToken);
548
- return resolveScopeToolsPolicy({
549
- tree,
550
- path: toolsPath
551
- });
552
- }
553
- function buildNextcloudTalkRoomScope(rooms, roomToken) {
554
- const { "*": defaults, ...scopes } = rooms ?? {};
555
- const tree = {
556
- defaults,
557
- scopes
558
- };
559
- const exactPath = Object.hasOwn(scopes, roomToken) ? [roomToken] : [];
560
- const toolsMatch = resolveChannelEntryMatchWithFallback({
561
- entries: scopes,
562
- keys: buildChannelKeyCandidates(roomToken),
563
- normalizeKey: normalizeChannelSlug
633
+ const match = resolveNextcloudTalkRoomMatch({
634
+ rooms: cfg.channels?.["nextcloud-talk"]?.rooms,
635
+ roomToken
564
636
  });
565
- return {
566
- tree,
567
- exactPath,
568
- toolsPath: toolsMatch.matchKey ? [toolsMatch.matchKey] : []
569
- };
637
+ return match.roomConfig?.tools ?? match.wildcardConfig?.tools;
570
638
  }
571
- function resolveNextcloudTalkGroupRequireMention(params) {
572
- if (!params.groupId) return true;
573
- const { tree, exactPath } = buildNextcloudTalkRoomScope(resolveNextcloudTalkAccount({
574
- cfg: params.cfg,
575
- accountId: params.accountId
576
- }).config.rooms, params.groupId);
577
- return resolveScopeRequireMention({
578
- tree,
579
- path: exactPath
580
- });
639
+ function resolveNextcloudTalkRequireMention(params) {
640
+ if (typeof params.roomConfig?.requireMention === "boolean") return params.roomConfig.requireMention;
641
+ if (typeof params.wildcardConfig?.requireMention === "boolean") return params.wildcardConfig.requireMention;
642
+ return true;
581
643
  }
582
644
  //#endregion
583
645
  //#region extensions/nextcloud-talk/src/room-info.ts
584
- const ROOM_CACHE_TTL_MS = 300 * 1e3;
585
- const ROOM_CACHE_ERROR_TTL_MS = 30 * 1e3;
646
+ const ROOM_CACHE_TTL_MS = 3e5;
647
+ const ROOM_CACHE_ERROR_TTL_MS = 3e4;
586
648
  const ROOM_CACHE_MAX_ENTRIES = 1e3;
587
649
  const NEXTCLOUD_TALK_ROOM_INFO_TIMEOUT_MS = 3e4;
588
650
  const roomCache = /* @__PURE__ */ new Map();
@@ -655,10 +717,7 @@ async function resolveNextcloudTalkRoomKind(params) {
655
717
  });
656
718
  return kind;
657
719
  } finally {
658
- await releaseNextcloudTalkGuardedResponse({
659
- response,
660
- release
661
- });
720
+ await release();
662
721
  }
663
722
  } catch (err) {
664
723
  cacheRoomInfo(key, {
@@ -675,10 +734,10 @@ function stripNextcloudTalkTargetPrefix(raw) {
675
734
  const trimmed = raw.trim();
676
735
  if (!trimmed) return;
677
736
  let normalized = trimmed;
678
- if (/^nextcloud-talk:/i.test(normalized)) normalized = normalized.slice(15).trim();
679
- else if (/^nc-talk:/i.test(normalized)) normalized = normalized.slice(8).trim();
680
- else if (/^nc:/i.test(normalized)) normalized = normalized.slice(3).trim();
681
- if (/^room:/i.test(normalized)) normalized = normalized.slice(5).trim();
737
+ if (normalized.startsWith("nextcloud-talk:")) normalized = normalized.slice(15).trim();
738
+ else if (normalized.startsWith("nc-talk:")) normalized = normalized.slice(8).trim();
739
+ else if (normalized.startsWith("nc:")) normalized = normalized.slice(3).trim();
740
+ if (normalized.startsWith("room:")) normalized = normalized.slice(5).trim();
682
741
  if (!normalized) return;
683
742
  return normalized;
684
743
  }
@@ -689,36 +748,17 @@ function normalizeNextcloudTalkMessagingTarget(raw) {
689
748
  function looksLikeNextcloudTalkTargetId(raw) {
690
749
  const trimmed = raw.trim();
691
750
  if (!trimmed) return false;
692
- if (/^(nextcloud-talk|nc-talk|nc|room):/i.test(trimmed)) return true;
751
+ if (/^(nextcloud-talk|nc-talk|nc):/i.test(trimmed)) return true;
693
752
  return /^[a-z0-9]{8,}$/i.test(trimmed);
694
753
  }
695
754
  //#endregion
696
755
  //#region extensions/nextcloud-talk/src/send.ts
697
- const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES = 8 * 1024;
756
+ const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES = 8192;
698
757
  const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS = 200;
699
- const NEXTCLOUD_TALK_SEND_TIMEOUT_MS = 3e4;
700
- const NEXTCLOUD_TALK_FORMAT_PROFILE = FormatCapabilityProfile.define({
701
- mechanism: "markdown",
702
- chunk: {
703
- limit: 4e3,
704
- unit: "chars",
705
- hardCap: 32e3
706
- }
707
- });
708
- function renderNextcloudTalkMarkdown(markdown) {
709
- return renderMarkdownWithMarkers({
710
- text: markdown,
711
- styles: [],
712
- links: []
713
- }, {
714
- styleMarkers: {},
715
- escapeText: (text) => text
716
- }, NEXTCLOUD_TALK_FORMAT_PROFILE);
717
- }
718
758
  /** Collapses whitespace and caps an error-body prefix to a short, log-safe snippet. */
719
759
  function collapseErrorSnippet(text) {
720
760
  const collapsed = text.replace(/\s+/g, " ").trim();
721
- if (collapsed.length > NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS) return `${truncateUtf16Safe(collapsed, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS)}…`;
761
+ if (collapsed.length > NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS) return `${collapsed.slice(0, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS)}…`;
722
762
  return collapsed;
723
763
  }
724
764
  /** Reads a bounded, collapsed error-body snippet without buffering hostile responses. */
@@ -793,7 +833,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
793
833
  channel: "nextcloud-talk",
794
834
  accountId: account.accountId
795
835
  });
796
- const message = convertMarkdownTables(renderNextcloudTalkMarkdown(text.trim()), tableMode);
836
+ const message = convertMarkdownTables(text.trim(), tableMode);
797
837
  const body = { message };
798
838
  if (opts.replyTo) body.replyTo = opts.replyTo;
799
839
  const bodyStr = JSON.stringify(body);
@@ -801,8 +841,9 @@ async function sendMessageNextcloudTalk(to, text, opts) {
801
841
  body: message,
802
842
  secret
803
843
  });
844
+ const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${roomToken}/message`;
804
845
  const { response, release } = await fetchWithSsrFGuard({
805
- url: `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${roomToken}/message`,
846
+ url,
806
847
  init: {
807
848
  method: "POST",
808
849
  headers: {
@@ -814,8 +855,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
814
855
  body: bodyStr
815
856
  },
816
857
  auditContext: "nextcloud-talk-send",
817
- policy: ssrfPolicyFromPrivateNetworkOptIn$1(account.config),
818
- timeoutMs: opts.timeoutMs ?? NEXTCLOUD_TALK_SEND_TIMEOUT_MS
858
+ policy: ssrfPolicyFromPrivateNetworkOptIn$1(account.config)
819
859
  });
820
860
  try {
821
861
  if (!response.ok) {
@@ -849,10 +889,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
849
889
  timestamp
850
890
  };
851
891
  } finally {
852
- await releaseNextcloudTalkGuardedResponse({
853
- response,
854
- release
855
- });
892
+ await release();
856
893
  }
857
894
  }
858
895
  async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
@@ -863,8 +900,9 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
863
900
  body: reaction,
864
901
  secret
865
902
  });
903
+ const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${normalizedToken}/reaction/${messageId}`;
866
904
  const { response, release } = await fetchWithSsrFGuard({
867
- url: `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${normalizedToken}/reaction/${messageId}`,
905
+ url,
868
906
  init: {
869
907
  method: "POST",
870
908
  headers: {
@@ -876,8 +914,7 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
876
914
  body
877
915
  },
878
916
  auditContext: "nextcloud-talk-reaction",
879
- policy: ssrfPolicyFromPrivateNetworkOptIn$1(account.config),
880
- timeoutMs: opts.timeoutMs ?? NEXTCLOUD_TALK_SEND_TIMEOUT_MS
917
+ policy: ssrfPolicyFromPrivateNetworkOptIn$1(account.config)
881
918
  });
882
919
  try {
883
920
  if (!response.ok) {
@@ -886,10 +923,7 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
886
923
  }
887
924
  return { ok: true };
888
925
  } finally {
889
- await releaseNextcloudTalkGuardedResponse({
890
- response,
891
- release
892
- });
926
+ await release();
893
927
  }
894
928
  }
895
929
  //#endregion
@@ -974,10 +1008,9 @@ async function handleNextcloudTalkInbound(params) {
974
1008
  surface: CHANNEL_ID
975
1009
  });
976
1010
  const hasControlCommand = core.channel.text.hasControlCommand(rawBody, config);
977
- const shouldRequireMention = isGroup ? resolveNextcloudTalkGroupRequireMention({
978
- cfg: config,
979
- accountId: account.accountId,
980
- groupId: roomToken
1011
+ const shouldRequireMention = isGroup ? resolveNextcloudTalkRequireMention({
1012
+ roomConfig,
1013
+ wildcardConfig: roomMatch.wildcardConfig
981
1014
  }) : false;
982
1015
  const { groupPolicy, providerMissingFallbackApplied } = resolveAllowlistProviderRuntimeGroupPolicy({
983
1016
  providerConfigPresent: (config.channels?.[CHANNEL_ID] ?? void 0) !== void 0,
@@ -1095,76 +1128,59 @@ async function handleNextcloudTalkInbound(params) {
1095
1128
  runtime.log?.(`nextcloud-talk: drop room ${roomToken} (no mention)`);
1096
1129
  return;
1097
1130
  }
1098
- const { route, buildEnvelope } = resolveChannelInboundRouteEnvelope({
1131
+ const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
1099
1132
  cfg: config,
1100
1133
  channel: CHANNEL_ID,
1101
1134
  accountId: account.accountId,
1102
1135
  peer: {
1103
1136
  kind: isGroup ? "group" : "direct",
1104
1137
  id: isGroup ? roomToken : senderId
1105
- }
1138
+ },
1139
+ runtime: core.channel,
1140
+ sessionStore: config.session?.store
1106
1141
  });
1107
1142
  const fromLabel = isGroup ? `room:${roomName || roomToken}` : senderName || `user:${senderId}`;
1108
- const body = buildEnvelope({
1143
+ const { storePath, body } = buildEnvelope({
1109
1144
  channel: "Nextcloud Talk",
1110
1145
  from: fromLabel,
1111
1146
  timestamp: message.timestamp,
1112
1147
  body: rawBody
1113
1148
  });
1114
1149
  const groupSystemPrompt = normalizeOptionalString(roomConfig?.systemPrompt);
1115
- const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(account.config);
1116
- const ctxPayload = buildChannelInboundEventContext({
1117
- channel: CHANNEL_ID,
1118
- accountId: route.accountId,
1119
- messageId: message.messageId,
1120
- timestamp: message.timestamp,
1121
- from: isGroup ? `nextcloud-talk:room:${roomToken}` : `nextcloud-talk:${senderId}`,
1122
- sender: {
1123
- id: senderId,
1124
- name: senderName || void 0
1125
- },
1126
- conversation: {
1127
- kind: isGroup ? "group" : "direct",
1128
- id: roomToken,
1129
- label: fromLabel
1130
- },
1131
- route: {
1132
- agentId: route.agentId,
1133
- dmScope: route.dmScope,
1134
- accountId: route.accountId,
1135
- routeSessionKey: route.sessionKey
1136
- },
1137
- reply: {
1138
- to: `nextcloud-talk:${roomToken}`,
1139
- originatingTo: `nextcloud-talk:${roomToken}`
1140
- },
1141
- message: {
1142
- body,
1143
- bodyForAgent: rawBody,
1144
- rawBody,
1145
- commandBody: rawBody
1146
- },
1147
- access: {
1148
- commands: { authorized: commandAuthorized },
1149
- mentions: {
1150
- canDetectMention: isGroup,
1151
- wasMentioned: isGroup && wasMentioned
1152
- }
1153
- },
1154
- extra: {
1155
- GroupSubject: isGroup ? roomName || roomToken : void 0,
1156
- GroupSystemPrompt: isGroup ? groupSystemPrompt : void 0
1157
- }
1150
+ const ctxPayload = core.channel.reply.finalizeInboundContext({
1151
+ Body: body,
1152
+ BodyForAgent: rawBody,
1153
+ RawBody: rawBody,
1154
+ CommandBody: rawBody,
1155
+ From: isGroup ? `nextcloud-talk:room:${roomToken}` : `nextcloud-talk:${senderId}`,
1156
+ To: `nextcloud-talk:${roomToken}`,
1157
+ SessionKey: route.sessionKey,
1158
+ AccountId: route.accountId,
1159
+ ChatType: isGroup ? "group" : "direct",
1160
+ ConversationLabel: fromLabel,
1161
+ SenderName: senderName || void 0,
1162
+ SenderId: senderId,
1163
+ GroupSubject: isGroup ? roomName || roomToken : void 0,
1164
+ GroupSystemPrompt: isGroup ? groupSystemPrompt : void 0,
1165
+ Provider: CHANNEL_ID,
1166
+ Surface: CHANNEL_ID,
1167
+ WasMentioned: isGroup ? wasMentioned : void 0,
1168
+ MessageSid: message.messageId,
1169
+ Timestamp: message.timestamp,
1170
+ OriginatingChannel: CHANNEL_ID,
1171
+ OriginatingTo: `nextcloud-talk:${roomToken}`,
1172
+ CommandAuthorized: commandAuthorized
1158
1173
  });
1159
- await core.channel.inbound.dispatch({
1174
+ await core.channel.inbound.dispatchReply({
1160
1175
  cfg: config,
1161
1176
  channel: CHANNEL_ID,
1162
1177
  accountId: account.accountId,
1163
- route: {
1164
- agentId: route.agentId,
1165
- sessionKey: route.sessionKey
1166
- },
1178
+ agentId: route.agentId,
1179
+ routeSessionKey: route.sessionKey,
1180
+ storePath,
1167
1181
  ctxPayload,
1182
+ recordInboundSession: core.channel.session.recordInboundSession,
1183
+ dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
1168
1184
  delivery: {
1169
1185
  preparePayload: (payload) => payload.text === void 0 ? payload : {
1170
1186
  ...payload,
@@ -1185,89 +1201,49 @@ async function handleNextcloudTalkInbound(params) {
1185
1201
  },
1186
1202
  replyPipeline: {},
1187
1203
  replyOptions: {
1188
- ...params.turnAdoptionLifecycle ? bindIngressLifecycleToReplyOptions(params.turnAdoptionLifecycle) : {},
1189
1204
  skillFilter: roomConfig?.skills,
1190
- disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : void 0
1205
+ disableBlockStreaming: typeof account.config.blockStreaming === "boolean" ? !account.config.blockStreaming : void 0
1191
1206
  },
1192
1207
  record: { onRecordError: (err) => {
1193
1208
  runtime.error?.(`nextcloud-talk: failed updating session meta: ${String(err)}`);
1194
1209
  } }
1195
1210
  });
1196
1211
  }
1197
- var NextcloudTalkWebhookPayloadError = class extends Error {
1198
- constructor(message, options) {
1199
- super(message, options);
1200
- this.name = "NextcloudTalkWebhookPayloadError";
1201
- }
1202
- };
1203
- function parseRawObject(rawEvent) {
1204
- let parsed;
1205
- try {
1206
- parsed = JSON.parse(rawEvent);
1207
- } catch (error) {
1208
- throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook contains invalid JSON.", { cause: error });
1209
- }
1210
- if (!isRecord(parsed)) throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook must be a JSON object.");
1211
- return parsed;
1212
- }
1213
- function requiredString(value, field) {
1214
- if (typeof value === "string" && value.trim()) return value.trim();
1215
- throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk webhook is missing ${field}.`);
1216
- }
1217
- function inspectNextcloudTalkWebhookEnvelope(rawEvent) {
1218
- const envelope = parseRawObject(rawEvent);
1219
- if (envelope.type !== "Create") return null;
1220
- const object = isRecord(envelope.object) ? envelope.object : null;
1221
- if (object?.type !== void 0 && object.type !== "Note") return null;
1222
- if (!object) throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook is missing object.");
1223
- const target = isRecord(envelope.target) ? envelope.target : null;
1224
- return {
1225
- eventId: requiredString(object.id, "object.id"),
1226
- laneKey: `room:${requiredString(target?.id, "target.id")}`
1227
- };
1228
- }
1229
- function parseLegacyReplayKey(key) {
1230
- const separator = key.lastIndexOf(":");
1231
- const roomId = key.slice(0, separator).trim();
1232
- const messageId = key.slice(separator + 1).trim();
1233
- return separator > 0 && roomId && messageId ? {
1234
- messageId,
1235
- roomId
1236
- } : null;
1237
- }
1238
- /** Convert the shipped replay guard's live window into durable completion tombstones. */
1239
- async function migrateNextcloudTalkLegacyReplayState(params) {
1240
- const entries = await params.store.entries();
1241
- let migrated = 0;
1242
- for (const entry of entries) {
1243
- const identity = parseLegacyReplayKey(entry.value.key);
1244
- if (!identity || !Number.isFinite(entry.value.seenAt)) continue;
1245
- const marker = {
1246
- version: 1,
1247
- receivedAt: entry.value.seenAt,
1248
- rawEvent: ""
1249
- };
1250
- const result = await params.queue.enqueue(identity.messageId, marker, {
1251
- receivedAt: entry.value.seenAt,
1252
- laneKey: `room:${identity.roomId}`
1253
- });
1254
- if (result.kind === "accepted" || result.kind === "pending" && result.record.payload.rawEvent === "") {
1255
- if (!await params.queue.complete(identity.messageId, { completedAt: entry.value.seenAt })) throw new Error(`Failed to migrate Nextcloud Talk replay key ${entry.value.key}.`);
1256
- }
1257
- migrated += 1;
1258
- }
1259
- await params.store.clear();
1260
- return migrated;
1261
- }
1262
1212
  //#endregion
1263
1213
  //#region extensions/nextcloud-talk/src/monitor.ts
1264
- const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
1265
- const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 64 * 1024;
1266
- const NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_HEADER = "x-openclaw-delivery-accepted";
1267
- const NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_VALUE = "durable";
1214
+ const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1048576;
1215
+ const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 65536;
1268
1216
  const PREAUTH_WEBHOOK_BODY_TIMEOUT_MS = 5e3;
1269
1217
  const HEALTH_PATH = "/healthz";
1270
1218
  const WEBHOOK_AUTH_RATE_LIMIT_SCOPE = "nextcloud-talk-webhook-auth";
1219
+ const NextcloudTalkWebhookPayloadSchema = z.object({
1220
+ type: z.enum([
1221
+ "Create",
1222
+ "Update",
1223
+ "Delete"
1224
+ ]),
1225
+ actor: z.object({
1226
+ type: z.literal("Person"),
1227
+ id: z.string().min(1),
1228
+ name: z.string()
1229
+ }),
1230
+ object: z.object({
1231
+ type: z.literal("Note"),
1232
+ id: z.string().min(1),
1233
+ name: z.string(),
1234
+ content: z.string(),
1235
+ mediaType: z.string()
1236
+ }),
1237
+ target: z.object({
1238
+ type: z.literal("Collection"),
1239
+ id: z.string().min(1),
1240
+ name: z.string()
1241
+ })
1242
+ });
1243
+ const NextcloudTalkWebhookEnvelopeSchema = z.object({
1244
+ type: z.string().min(1),
1245
+ object: z.object({ type: z.string().min(1).optional() }).passthrough().optional()
1246
+ });
1271
1247
  const WEBHOOK_ERRORS = {
1272
1248
  missingSignatureHeaders: "Missing signature headers",
1273
1249
  invalidBackend: "Invalid backend",
@@ -1276,10 +1252,48 @@ const WEBHOOK_ERRORS = {
1276
1252
  payloadTooLarge: "Payload too large",
1277
1253
  internalServerError: "Internal server error"
1278
1254
  };
1255
+ var NextcloudTalkRetryableWebhookError = class extends Error {
1256
+ constructor(message, options) {
1257
+ super(message, options);
1258
+ this.name = "NextcloudTalkRetryableWebhookError";
1259
+ }
1260
+ };
1261
+ async function processNextcloudTalkReplayGuardedMessage(params) {
1262
+ if (await params.replayGuard.claimMessage({
1263
+ accountId: params.accountId,
1264
+ roomToken: params.message.roomToken,
1265
+ messageId: params.message.messageId
1266
+ }) !== "claimed") return "duplicate";
1267
+ try {
1268
+ await params.handleMessage();
1269
+ await params.replayGuard.commitMessage({
1270
+ accountId: params.accountId,
1271
+ roomToken: params.message.roomToken,
1272
+ messageId: params.message.messageId
1273
+ });
1274
+ return "processed";
1275
+ } catch (error) {
1276
+ if (error instanceof NextcloudTalkRetryableWebhookError) params.replayGuard.releaseMessage({
1277
+ accountId: params.accountId,
1278
+ roomToken: params.message.roomToken,
1279
+ messageId: params.message.messageId,
1280
+ error
1281
+ });
1282
+ else await params.replayGuard.commitMessage({
1283
+ accountId: params.accountId,
1284
+ roomToken: params.message.roomToken,
1285
+ messageId: params.message.messageId
1286
+ });
1287
+ throw error;
1288
+ }
1289
+ }
1279
1290
  function formatError(err) {
1280
1291
  if (err instanceof Error) return err.message;
1281
1292
  return typeof err === "string" ? err : JSON.stringify(err);
1282
1293
  }
1294
+ function parseWebhookPayload(body) {
1295
+ return safeParseJsonWithSchema(NextcloudTalkWebhookPayloadSchema, body);
1296
+ }
1283
1297
  function writeJsonResponse(res, status, body) {
1284
1298
  if (body) {
1285
1299
  res.writeHead(status, { "Content-Type": "application/json" });
@@ -1319,6 +1333,37 @@ function verifyWebhookSignature(params) {
1319
1333
  params.authRateLimiter.reset(params.clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE);
1320
1334
  return true;
1321
1335
  }
1336
+ function decodeWebhookCreateMessage(params) {
1337
+ const envelope = safeParseJsonWithSchema(NextcloudTalkWebhookEnvelopeSchema, params.body);
1338
+ if (!envelope) {
1339
+ writeWebhookError(params.res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
1340
+ return { kind: "invalid" };
1341
+ }
1342
+ if (envelope.type !== "Create") return { kind: "ignore" };
1343
+ if (envelope.object?.type && envelope.object.type !== "Note") return { kind: "ignore" };
1344
+ const payload = parseWebhookPayload(params.body);
1345
+ if (!payload) {
1346
+ writeWebhookError(params.res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
1347
+ return { kind: "invalid" };
1348
+ }
1349
+ return {
1350
+ kind: "message",
1351
+ message: payloadToInboundMessage(payload)
1352
+ };
1353
+ }
1354
+ function payloadToInboundMessage(payload) {
1355
+ return {
1356
+ messageId: payload.object.id,
1357
+ roomToken: payload.target.id,
1358
+ roomName: payload.target.name,
1359
+ senderId: payload.actor.id,
1360
+ senderName: payload.actor.name ?? "",
1361
+ text: payload.object.content || payload.object.name || "",
1362
+ mediaType: payload.object.mediaType || "text/plain",
1363
+ timestamp: Date.now(),
1364
+ isGroupChat: true
1365
+ };
1366
+ }
1322
1367
  function readNextcloudTalkWebhookBody(req, maxBodyBytes) {
1323
1368
  return readRequestBodyWithLimit(req, {
1324
1369
  maxBytes: Math.min(maxBodyBytes, PREAUTH_WEBHOOK_MAX_BODY_BYTES),
@@ -1326,10 +1371,12 @@ function readNextcloudTalkWebhookBody(req, maxBodyBytes) {
1326
1371
  });
1327
1372
  }
1328
1373
  function createNextcloudTalkWebhookServer(opts) {
1329
- const { port, host, path, secret, onWebhook, onError, abortSignal } = opts;
1374
+ const { port, host, path, secret, onMessage, onError, abortSignal } = opts;
1330
1375
  const maxBodyBytes = typeof opts.maxBodyBytes === "number" && Number.isFinite(opts.maxBodyBytes) && opts.maxBodyBytes > 0 ? Math.floor(opts.maxBodyBytes) : DEFAULT_WEBHOOK_MAX_BODY_BYTES;
1331
1376
  const readBody = opts.readBody ?? readNextcloudTalkWebhookBody;
1332
1377
  const isBackendAllowed = opts.isBackendAllowed;
1378
+ const shouldProcessMessage = opts.shouldProcessMessage;
1379
+ const processMessage = opts.processMessage;
1333
1380
  const authRateLimitMaxRequests = typeof opts.authRateLimit?.maxRequests === "number" ? opts.authRateLimit.maxRequests : WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests;
1334
1381
  const authRateLimitWindowMs = typeof opts.authRateLimit?.windowMs === "number" ? opts.authRateLimit.windowMs : WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs;
1335
1382
  const webhookAuthRateLimiter = createAuthRateLimiter({
@@ -1373,8 +1420,37 @@ function createNextcloudTalkWebhookServer(opts) {
1373
1420
  clientIp,
1374
1421
  authRateLimiter: webhookAuthRateLimiter
1375
1422
  })) return;
1376
- if (await onWebhook(body) === "accepted") res.setHeader(NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_HEADER, NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_VALUE);
1423
+ const decoded = decodeWebhookCreateMessage({
1424
+ body,
1425
+ res
1426
+ });
1427
+ if (decoded.kind === "invalid") return;
1428
+ if (decoded.kind === "ignore") {
1429
+ writeJsonResponse(res, 200);
1430
+ return;
1431
+ }
1432
+ const message = decoded.message;
1433
+ if (processMessage) {
1434
+ writeJsonResponse(res, 200);
1435
+ try {
1436
+ await processMessage(message);
1437
+ } catch (err) {
1438
+ onError?.(err instanceof Error ? err : new Error(formatError(err)));
1439
+ }
1440
+ return;
1441
+ }
1442
+ if (shouldProcessMessage) {
1443
+ if (!await shouldProcessMessage(message)) {
1444
+ writeJsonResponse(res, 200);
1445
+ return;
1446
+ }
1447
+ }
1377
1448
  writeJsonResponse(res, 200);
1449
+ try {
1450
+ await onMessage(message);
1451
+ } catch (err) {
1452
+ onError?.(err instanceof Error ? err : new Error(formatError(err)));
1453
+ }
1378
1454
  } catch (err) {
1379
1455
  if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
1380
1456
  writeWebhookError(res, 413, WEBHOOK_ERRORS.payloadTooLarge);
@@ -1384,48 +1460,29 @@ function createNextcloudTalkWebhookServer(opts) {
1384
1460
  writeWebhookError(res, 408, requestBodyErrorToText("REQUEST_BODY_TIMEOUT"));
1385
1461
  return;
1386
1462
  }
1387
- if (err instanceof NextcloudTalkWebhookPayloadError) {
1388
- writeWebhookError(res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
1389
- return;
1390
- }
1391
1463
  const error = err instanceof Error ? err : new Error(formatError(err));
1392
1464
  onError?.(error);
1393
1465
  writeWebhookError(res, 500, WEBHOOK_ERRORS.internalServerError);
1394
1466
  }
1395
1467
  })();
1396
1468
  });
1397
- let stopRequested = false;
1398
- let closePromise;
1399
- const closeIfListening = () => {
1400
- if (closePromise) return closePromise;
1401
- if (!server.listening) return Promise.resolve();
1402
- closePromise = new Promise((resolve) => {
1403
- server.close(() => resolve());
1404
- }).finally(() => {
1405
- closePromise = void 0;
1406
- });
1407
- return closePromise;
1408
- };
1409
- const stop = async () => {
1410
- stopRequested = true;
1411
- await closeIfListening();
1412
- };
1413
1469
  const start = () => {
1414
- if (stopRequested) return Promise.resolve();
1415
- return new Promise((resolve, reject) => {
1416
- const onListenError = (error) => reject(error);
1417
- server.once("error", onListenError);
1418
- server.listen(port, host, () => {
1419
- server.off("error", onListenError);
1420
- (async () => {
1421
- if (stopRequested) await closeIfListening();
1422
- resolve();
1423
- })().catch(reject);
1424
- });
1470
+ return new Promise((resolve) => {
1471
+ server.listen(port, host, () => resolve());
1425
1472
  });
1426
1473
  };
1427
- if (abortSignal) if (abortSignal.aborted) stop();
1428
- else abortSignal.addEventListener("abort", () => void stop(), { once: true });
1474
+ let stopped = false;
1475
+ const stop = () => {
1476
+ if (stopped) return;
1477
+ stopped = true;
1478
+ try {
1479
+ server.close();
1480
+ } catch {}
1481
+ };
1482
+ if (abortSignal) {
1483
+ if (abortSignal.aborted) stop();
1484
+ else abortSignal.addEventListener("abort", stop, { once: true });
1485
+ }
1429
1486
  return {
1430
1487
  server,
1431
1488
  start,
@@ -1433,156 +1490,6 @@ function createNextcloudTalkWebhookServer(opts) {
1433
1490
  };
1434
1491
  }
1435
1492
  //#endregion
1436
- //#region extensions/nextcloud-talk/src/webhook-spool.ts
1437
- const NEXTCLOUD_TALK_INGRESS_POLL_INTERVAL_MS = 500;
1438
- const NextcloudTalkWebhookPayloadSchema = z.object({
1439
- type: z.enum([
1440
- "Create",
1441
- "Update",
1442
- "Delete"
1443
- ]),
1444
- actor: z.object({
1445
- type: z.literal("Person"),
1446
- id: z.string().min(1),
1447
- name: z.string()
1448
- }),
1449
- object: z.object({
1450
- type: z.literal("Note"),
1451
- id: z.string().min(1),
1452
- name: z.string(),
1453
- content: z.string(),
1454
- mediaType: z.string()
1455
- }),
1456
- target: z.object({
1457
- type: z.literal("Collection"),
1458
- id: z.string().min(1),
1459
- name: z.string()
1460
- })
1461
- });
1462
- function parseClaimedMessage(payload, claimedId, claimedLaneKey) {
1463
- if (payload.version !== 1) throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk ingress row ${claimedId} has an unsupported version.`);
1464
- const result = NextcloudTalkWebhookPayloadSchema.safeParse(parseRawObject(payload.rawEvent));
1465
- if (!result.success || result.data.type !== "Create" || result.data.object.id !== claimedId) throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk ingress row ${claimedId} has invalid message identity.`);
1466
- const webhook = result.data;
1467
- const roomId = requiredString(webhook.target.id, "target.id");
1468
- if (claimedLaneKey !== `room:${roomId}`) throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk ingress row ${claimedId} changed room identity.`);
1469
- return {
1470
- messageId: webhook.object.id,
1471
- roomToken: roomId,
1472
- roomName: webhook.target.name,
1473
- senderId: webhook.actor.id,
1474
- senderName: webhook.actor.name,
1475
- text: webhook.object.content || webhook.object.name,
1476
- mediaType: webhook.object.mediaType || "text/plain",
1477
- timestamp: payload.receivedAt,
1478
- isGroupChat: true
1479
- };
1480
- }
1481
- function resolveNonRetryableFailure(error) {
1482
- if (error instanceof NextcloudTalkWebhookPayloadError) return {
1483
- reason: "invalid-event",
1484
- message: error.message
1485
- };
1486
- const message = formatErrorMessage(error);
1487
- if (message.includes("Nextcloud Talk: bot send was rejected") || message.includes("Nextcloud Talk: forbidden")) return {
1488
- reason: "nextcloud-talk-auth",
1489
- message
1490
- };
1491
- return null;
1492
- }
1493
- function createNextcloudTalkWebhookSpool(options) {
1494
- let queue = options.queue;
1495
- const getQueue = () => {
1496
- queue ??= getNextcloudTalkRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
1497
- return queue;
1498
- };
1499
- const legacyReplayStore = options.legacyReplayStore === null ? null : options.legacyReplayStore ?? getNextcloudTalkRuntime().state.openKeyedStore({
1500
- namespace: resolvePersistentDedupePluginStateNamespace({
1501
- namespace: options.accountId,
1502
- namespacePrefix: "replay-dedupe"
1503
- }),
1504
- maxEntries: 1e4,
1505
- defaultTtlMs: 864e5
1506
- });
1507
- const legacyMigration = legacyReplayStore ? migrateNextcloudTalkLegacyReplayState({
1508
- queue: getQueue(),
1509
- store: legacyReplayStore
1510
- }) : Promise.resolve();
1511
- const monitor = createChannelIngressMonitor({
1512
- queue: getQueue,
1513
- inspect: (rawEvent) => inspectNextcloudTalkWebhookEnvelope(rawEvent),
1514
- payload: {
1515
- version: 1,
1516
- serialize: (rawEvent, { receivedAt }) => ({
1517
- receivedAt,
1518
- rawEvent
1519
- }),
1520
- deserialize: (body) => body.rawEvent,
1521
- encode: ({ body }) => ({
1522
- version: 1,
1523
- ...body
1524
- }),
1525
- decode: (payload) => ({
1526
- version: payload.version,
1527
- body: {
1528
- receivedAt: payload.receivedAt,
1529
- rawEvent: payload.rawEvent
1530
- }
1531
- }),
1532
- createClaimError: (kind, claim) => new NextcloudTalkWebhookPayloadError(kind === "invalid-version" ? `Nextcloud Talk ingress row ${claim.id} has an unsupported version.` : `Nextcloud Talk ingress row ${claim.id} has invalid message identity.`)
1533
- },
1534
- deliver: async (_rawEvent, lifecycle, claim) => {
1535
- const message = parseClaimedMessage(claim.payload, claim.id, claim.laneKey);
1536
- await options.deliver(message, lifecycle);
1537
- },
1538
- pollIntervalMs: options.pollIntervalMs ?? NEXTCLOUD_TALK_INGRESS_POLL_INTERVAL_MS,
1539
- waitForDeliveryIdleBeforeRepump: true,
1540
- retention: {
1541
- completedMaxEntries: 1e4,
1542
- failedMaxEntries: 1e4
1543
- },
1544
- drain: {
1545
- resolveNonRetryableFailure,
1546
- ...options.adoptionStallTimeoutMs === void 0 ? {} : { adoptionStallTimeoutMs: options.adoptionStallTimeoutMs },
1547
- onLog: (message) => options.runtime.log?.(`nextcloud-talk ${message}`)
1548
- },
1549
- ...options.abortSignal ? { abortSignal: options.abortSignal } : {},
1550
- createStoppedError: () => /* @__PURE__ */ new Error("Nextcloud Talk ingress stopped"),
1551
- onError: (error) => options.runtime.error?.(`nextcloud-talk ingress drain failed: ${formatErrorMessage(error)}`)
1552
- });
1553
- let stopping = false;
1554
- const inFlightReceives = /* @__PURE__ */ new Set();
1555
- const startAfterMigration = legacyMigration.then(() => {
1556
- if (!stopping) monitor.start();
1557
- });
1558
- return {
1559
- ready: async () => await startAfterMigration,
1560
- receive: (rawEvent) => {
1561
- if (stopping) return Promise.reject(/* @__PURE__ */ new Error("Nextcloud Talk ingress stopped"));
1562
- const receiveTask = (async () => {
1563
- await startAfterMigration;
1564
- return (await monitor.admit(rawEvent)).kind === "ignored" ? "ignored" : "accepted";
1565
- })();
1566
- inFlightReceives.add(receiveTask);
1567
- receiveTask.then(() => inFlightReceives.delete(receiveTask), () => inFlightReceives.delete(receiveTask));
1568
- return receiveTask;
1569
- },
1570
- stop: async () => {
1571
- stopping = true;
1572
- const pendingReceives = [...inFlightReceives];
1573
- const pauseTask = monitor.pause();
1574
- await startAfterMigration.catch(() => void 0);
1575
- await Promise.allSettled(pendingReceives);
1576
- await monitor.stop();
1577
- await pauseTask;
1578
- },
1579
- waitForIdle: async () => {
1580
- await startAfterMigration.catch(() => void 0);
1581
- await monitor.waitForIdle();
1582
- }
1583
- };
1584
- }
1585
- //#endregion
1586
1493
  //#region extensions/nextcloud-talk/src/monitor-runtime.ts
1587
1494
  const DEFAULT_WEBHOOK_PORT = 8788;
1588
1495
  const DEFAULT_WEBHOOK_HOST = "0.0.0.0";
@@ -1611,29 +1518,13 @@ async function monitorNextcloudTalkProvider(opts) {
1611
1518
  accountId: account.accountId
1612
1519
  });
1613
1520
  const expectedBackendOrigin = normalizeOrigin(account.baseUrl);
1614
- const spool = (opts.createSpool ?? createNextcloudTalkWebhookSpool)({
1615
- accountId: account.accountId,
1616
- runtime,
1617
- abortSignal: opts.abortSignal,
1618
- deliver: async (message, lifecycle) => {
1619
- core.channel.activity.record({
1620
- channel: "nextcloud-talk",
1621
- accountId: account.accountId,
1622
- direction: "inbound",
1623
- at: message.timestamp
1624
- });
1625
- if (opts.onMessage) await opts.onMessage(message, lifecycle);
1626
- else await handleNextcloudTalkInbound({
1627
- message,
1628
- account,
1629
- config: cfg,
1630
- runtime,
1631
- statusSink: opts.statusSink,
1632
- turnAdoptionLifecycle: lifecycle
1633
- });
1521
+ const replayGuard = createNextcloudTalkReplayGuard({
1522
+ stateDir: core.state.resolveStateDir(process.env, os.homedir),
1523
+ onDiskError: (error) => {
1524
+ logger.warn(`[nextcloud-talk:${account.accountId}] replay guard disk error: ${String(error)}`);
1634
1525
  }
1635
1526
  });
1636
- const server = (opts.createServer ?? createNextcloudTalkWebhookServer)({
1527
+ const { start, stop } = createNextcloudTalkWebhookServer({
1637
1528
  port,
1638
1529
  host,
1639
1530
  path,
@@ -1642,34 +1533,39 @@ async function monitorNextcloudTalkProvider(opts) {
1642
1533
  if (!expectedBackendOrigin) return true;
1643
1534
  return normalizeOrigin(backend) === expectedBackendOrigin;
1644
1535
  },
1645
- onWebhook: spool.receive,
1536
+ processMessage: async (message) => {
1537
+ if (await processNextcloudTalkReplayGuardedMessage({
1538
+ replayGuard,
1539
+ accountId: account.accountId,
1540
+ message,
1541
+ handleMessage: async () => {
1542
+ core.channel.activity.record({
1543
+ channel: "nextcloud-talk",
1544
+ accountId: account.accountId,
1545
+ direction: "inbound",
1546
+ at: message.timestamp
1547
+ });
1548
+ if (opts.onMessage) await opts.onMessage(message);
1549
+ else await handleNextcloudTalkInbound({
1550
+ message,
1551
+ account,
1552
+ config: cfg,
1553
+ runtime,
1554
+ statusSink: opts.statusSink
1555
+ });
1556
+ }
1557
+ }) === "duplicate") logger.warn(`[nextcloud-talk:${account.accountId}] replayed webhook ignored room=${message.roomToken} messageId=${message.messageId}`);
1558
+ },
1559
+ onMessage: async () => {},
1646
1560
  onError: (error) => {
1647
1561
  logger.error(`[nextcloud-talk:${account.accountId}] webhook error: ${error.message}`);
1648
1562
  },
1649
1563
  abortSignal: opts.abortSignal
1650
1564
  });
1651
- let stopPromise;
1652
- const stop = () => {
1653
- stopPromise ??= (async () => {
1654
- await server.stop();
1655
- await spool.stop();
1656
- })();
1657
- return stopPromise;
1658
- };
1659
- if (opts.abortSignal && !opts.abortSignal.aborted) opts.abortSignal.addEventListener("abort", () => void stop(), { once: true });
1565
+ if (opts.abortSignal?.aborted) return { stop };
1566
+ await start();
1660
1567
  if (opts.abortSignal?.aborted) {
1661
- await stop();
1662
- return { stop };
1663
- }
1664
- try {
1665
- await spool.ready();
1666
- await server.start();
1667
- } catch (error) {
1668
- await stop();
1669
- throw error;
1670
- }
1671
- if (opts.abortSignal?.aborted) {
1672
- await stop();
1568
+ stop();
1673
1569
  return { stop };
1674
1570
  }
1675
1571
  const publicUrl = account.config.webhookPublicUrl ?? `http://${host === "0.0.0.0" ? "localhost" : host}:${port}${path}`;
@@ -1687,7 +1583,7 @@ const nextcloudTalkGatewayAdapter = {
1687
1583
  accountId: ctx.accountId,
1688
1584
  setStatus: ctx.setStatus
1689
1585
  });
1690
- await runPassiveAccountLifecycle({
1586
+ await runStoppablePassiveMonitor({
1691
1587
  abortSignal: ctx.abortSignal,
1692
1588
  start: async () => await monitorNextcloudTalkProvider({
1693
1589
  accountId: account.accountId,
@@ -1695,10 +1591,7 @@ const nextcloudTalkGatewayAdapter = {
1695
1591
  runtime: ctx.runtime,
1696
1592
  abortSignal: ctx.abortSignal,
1697
1593
  statusSink
1698
- }),
1699
- stop: async (monitor) => {
1700
- await monitor.stop();
1701
- }
1594
+ })
1702
1595
  });
1703
1596
  },
1704
1597
  logoutAccount: async ({ accountId, cfg }) => {
@@ -1724,15 +1617,17 @@ const nextcloudTalkGatewayAdapter = {
1724
1617
  else delete nextSection.accounts;
1725
1618
  }
1726
1619
  }
1727
- if (changed) if (nextSection && Object.keys(nextSection).length > 0) nextCfg.channels = {
1728
- ...nextCfg.channels,
1729
- "nextcloud-talk": nextSection
1730
- };
1731
- else {
1732
- const nextChannels = { ...nextCfg.channels };
1733
- delete nextChannels["nextcloud-talk"];
1734
- if (Object.keys(nextChannels).length > 0) nextCfg.channels = nextChannels;
1735
- else delete nextCfg.channels;
1620
+ if (changed) {
1621
+ if (nextSection && Object.keys(nextSection).length > 0) nextCfg.channels = {
1622
+ ...nextCfg.channels,
1623
+ "nextcloud-talk": nextSection
1624
+ };
1625
+ else {
1626
+ const nextChannels = { ...nextCfg.channels };
1627
+ delete nextChannels["nextcloud-talk"];
1628
+ if (Object.keys(nextChannels).length > 0) nextCfg.channels = nextChannels;
1629
+ else delete nextCfg.channels;
1630
+ }
1736
1631
  }
1737
1632
  const loggedOut = resolveNextcloudTalkAccount({
1738
1633
  cfg: changed ? nextCfg : cfg,
@@ -1770,15 +1665,10 @@ const nextcloudTalkMessageActions = {
1770
1665
  if (!hasConfiguredAccount(cfg, accountId)) return null;
1771
1666
  return { actions: ["send", "react"] };
1772
1667
  },
1773
- supportsAction: ({ action }) => action === "react",
1668
+ supportsAction: ({ action }) => action !== "send",
1774
1669
  handleAction: async ({ action, params, cfg, accountId, toolContext }) => {
1775
1670
  if (action === "send") throw new Error("Send should be handled by outbound, not actions handler.");
1776
1671
  if (action === "react") {
1777
- const account = resolveNextcloudTalkAccount({
1778
- cfg,
1779
- accountId
1780
- });
1781
- if (!isAccountConfigured(account)) throw new Error(`Nextcloud Talk account "${account.accountId}" is disabled or not configured.`);
1782
1672
  const target = readStringParam(params, "to", {
1783
1673
  required: true,
1784
1674
  label: "to (room token)"
@@ -1857,8 +1747,8 @@ function resolveNextcloudTalkOutboundSessionRoute(params) {
1857
1747
  //#region extensions/nextcloud-talk/src/setup-core.ts
1858
1748
  const t$1 = createSetupTranslator$1();
1859
1749
  const channel$1 = "nextcloud-talk";
1860
- function readOptionalString(value) {
1861
- return typeof value === "string" && value.length > 0 ? value : void 0;
1750
+ function addWildcardAllowFrom(allowFrom) {
1751
+ return mergeAllowFromEntries(allowFrom, ["*"]);
1862
1752
  }
1863
1753
  function normalizeNextcloudTalkBaseUrl(value) {
1864
1754
  return value?.trim().replace(/\/+$/, "") ?? "";
@@ -1867,15 +1757,46 @@ function validateNextcloudTalkBaseUrl(value) {
1867
1757
  if (!value) return "Required";
1868
1758
  if (!value.startsWith("http://") && !value.startsWith("https://")) return "URL must start with http:// or https://";
1869
1759
  }
1870
- function setNextcloudTalkAccountConfig(cfg, accountId, updates, clearFields) {
1760
+ function setNextcloudTalkAccountConfig(cfg, accountId, updates) {
1871
1761
  return patchScopedAccountConfig({
1872
1762
  cfg,
1873
1763
  channelKey: channel$1,
1874
1764
  accountId,
1875
- patch: updates,
1876
- ...clearFields ? { clearFields } : {}
1765
+ patch: updates
1877
1766
  });
1878
1767
  }
1768
+ function clearNextcloudTalkAccountFields(cfg, accountId, fields) {
1769
+ const section = cfg.channels?.["nextcloud-talk"];
1770
+ if (!section) return cfg;
1771
+ if (accountId === DEFAULT_ACCOUNT_ID$1) {
1772
+ const nextSection = { ...section };
1773
+ for (const field of fields) delete nextSection[field];
1774
+ return {
1775
+ ...cfg,
1776
+ channels: {
1777
+ ...cfg.channels,
1778
+ "nextcloud-talk": nextSection
1779
+ }
1780
+ };
1781
+ }
1782
+ const currentAccount = section.accounts?.[accountId];
1783
+ if (!currentAccount) return cfg;
1784
+ const nextAccount = { ...currentAccount };
1785
+ for (const field of fields) delete nextAccount[field];
1786
+ return {
1787
+ ...cfg,
1788
+ channels: {
1789
+ ...cfg.channels,
1790
+ "nextcloud-talk": {
1791
+ ...section,
1792
+ accounts: {
1793
+ ...section.accounts,
1794
+ [accountId]: nextAccount
1795
+ }
1796
+ }
1797
+ }
1798
+ };
1799
+ }
1879
1800
  async function promptNextcloudTalkAllowFrom(params) {
1880
1801
  return await promptParsedAllowFromForAccount({
1881
1802
  cfg: params.cfg,
@@ -1914,28 +1835,37 @@ async function promptNextcloudTalkAllowFromForAccount(params) {
1914
1835
  accountId
1915
1836
  });
1916
1837
  }
1917
- const nextcloudTalkDmPolicy = createChannelDmPolicy({
1838
+ const nextcloudTalkDmPolicy = {
1918
1839
  label: "Nextcloud Talk",
1919
1840
  channel: channel$1,
1920
- resolveAccount: (cfg, accountId) => resolveNextcloudTalkAccount({
1841
+ policyKey: "channels.nextcloud-talk.dmPolicy",
1842
+ allowFromKey: "channels.nextcloud-talk.allowFrom",
1843
+ resolveConfigKeys: (cfg, accountId) => (accountId ?? resolveDefaultNextcloudTalkAccountId(cfg)) !== DEFAULT_ACCOUNT_ID$1 ? {
1844
+ policyKey: `channels.nextcloud-talk.accounts.${accountId ?? resolveDefaultNextcloudTalkAccountId(cfg)}.dmPolicy`,
1845
+ allowFromKey: `channels.nextcloud-talk.accounts.${accountId ?? resolveDefaultNextcloudTalkAccountId(cfg)}.allowFrom`
1846
+ } : {
1847
+ policyKey: "channels.nextcloud-talk.dmPolicy",
1848
+ allowFromKey: "channels.nextcloud-talk.allowFrom"
1849
+ },
1850
+ getCurrent: (cfg, accountId) => resolveNextcloudTalkAccount({
1921
1851
  cfg,
1922
1852
  accountId: accountId ?? resolveDefaultNextcloudTalkAccountId(cfg)
1923
- }),
1924
- applyPatch: ({ cfg, account, patch }) => setNextcloudTalkAccountConfig(cfg, account.accountId, patch),
1853
+ }).config.dmPolicy ?? "pairing",
1854
+ setPolicy: (cfg, policy, accountId) => {
1855
+ const resolvedAccountId = accountId ?? resolveDefaultNextcloudTalkAccountId(cfg);
1856
+ const resolved = resolveNextcloudTalkAccount({
1857
+ cfg,
1858
+ accountId: resolvedAccountId
1859
+ });
1860
+ return setNextcloudTalkAccountConfig(cfg, resolvedAccountId, {
1861
+ dmPolicy: policy,
1862
+ ...policy === "open" ? { allowFrom: addWildcardAllowFrom(resolved.config.allowFrom) } : {}
1863
+ });
1864
+ },
1925
1865
  promptAllowFrom: promptNextcloudTalkAllowFromForAccount
1926
- });
1866
+ };
1927
1867
  const nextcloudTalkSetupAdapter = {
1928
- singleAccountKeysToMove: ["rooms"],
1929
1868
  resolveAccountId: ({ accountId }) => normalizeAccountId$1(accountId),
1930
- prepareAccountConfigInput: ({ input }) => {
1931
- const setupInput = input;
1932
- return {
1933
- ...setupInput,
1934
- baseUrl: setupInput.baseUrl ?? readOptionalString(setupInput.url),
1935
- secret: setupInput.secret ?? readOptionalString(setupInput.token) ?? readOptionalString(setupInput.password),
1936
- secretFile: setupInput.secretFile ?? readOptionalString(setupInput.tokenFile)
1937
- };
1938
- },
1939
1869
  applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
1940
1870
  cfg,
1941
1871
  channelKey: channel$1,
@@ -1953,83 +1883,18 @@ const nextcloudTalkSetupAdapter = {
1953
1883
  }),
1954
1884
  applyAccountConfig: ({ cfg, accountId, input }) => {
1955
1885
  const setupInput = input;
1956
- return setNextcloudTalkAccountConfig(applyAccountNameToChannelSection({
1886
+ const namedConfig = applyAccountNameToChannelSection({
1957
1887
  cfg,
1958
1888
  channelKey: channel$1,
1959
1889
  accountId,
1960
1890
  name: setupInput.name
1961
- }), accountId, {
1891
+ });
1892
+ return setNextcloudTalkAccountConfig(setupInput.useEnv ? clearNextcloudTalkAccountFields(namedConfig, accountId, ["botSecret", "botSecretFile"]) : namedConfig, accountId, {
1962
1893
  baseUrl: normalizeNextcloudTalkBaseUrl(setupInput.baseUrl),
1963
1894
  ...setupInput.useEnv ? {} : setupInput.secretFile ? { botSecretFile: setupInput.secretFile } : setupInput.secret ? { botSecret: setupInput.secret } : {}
1964
- }, setupInput.useEnv ? ["botSecret", "botSecretFile"] : void 0);
1895
+ });
1965
1896
  }
1966
1897
  };
1967
- const nextcloudTalkSetupContract = defineChannelSetupContract({
1968
- fields: {
1969
- baseUrl: {
1970
- kind: "string",
1971
- cli: {
1972
- flags: "--base-url <url>",
1973
- description: "Nextcloud base URL"
1974
- }
1975
- },
1976
- url: {
1977
- kind: "string",
1978
- cli: {
1979
- flags: "--url <url>",
1980
- description: "Legacy Nextcloud base URL alias"
1981
- }
1982
- },
1983
- secret: {
1984
- kind: "string",
1985
- sensitive: true,
1986
- cli: {
1987
- flags: "--secret <secret>",
1988
- description: "Nextcloud Talk bot secret"
1989
- }
1990
- },
1991
- token: {
1992
- kind: "string",
1993
- sensitive: true,
1994
- cli: {
1995
- flags: "--token <secret>",
1996
- description: "Legacy Nextcloud bot secret alias"
1997
- }
1998
- },
1999
- password: {
2000
- kind: "string",
2001
- sensitive: true,
2002
- cli: {
2003
- flags: "--password <secret>",
2004
- description: "Legacy Nextcloud bot secret alias"
2005
- }
2006
- },
2007
- secretFile: {
2008
- kind: "string",
2009
- sensitive: true,
2010
- cli: {
2011
- flags: "--secret-file <path>",
2012
- description: "Nextcloud Talk bot secret file"
2013
- }
2014
- },
2015
- tokenFile: {
2016
- kind: "string",
2017
- sensitive: true,
2018
- cli: {
2019
- flags: "--token-file <path>",
2020
- description: "Legacy Nextcloud bot secret file alias"
2021
- }
2022
- },
2023
- useEnv: {
2024
- kind: "boolean",
2025
- cli: {
2026
- flags: "--use-env",
2027
- description: "Use Nextcloud Talk environment credentials"
2028
- }
2029
- }
2030
- },
2031
- legacyAdapter: nextcloudTalkSetupAdapter
2032
- });
2033
1898
  //#endregion
2034
1899
  //#region extensions/nextcloud-talk/src/setup-surface.ts
2035
1900
  const t = createSetupTranslator();
@@ -2087,10 +1952,8 @@ const nextcloudTalkSetupWizard = {
2087
1952
  [CONFIGURE_API_FLAG]: "1"
2088
1953
  } };
2089
1954
  },
2090
- credentials: [defineTokenCredential({
1955
+ credentials: [{
2091
1956
  inputKey: "token",
2092
- configKey: "botSecret",
2093
- configuredFields: ["botSecret", "botSecretFile"],
2094
1957
  providerHint: channel,
2095
1958
  credentialLabel: t("wizard.nextcloudTalk.botSecret"),
2096
1959
  preferredEnvVar: "NEXTCLOUD_TALK_BOT_SECRET",
@@ -2098,54 +1961,61 @@ const nextcloudTalkSetupWizard = {
2098
1961
  keepPrompt: t("wizard.nextcloudTalk.botSecretKeep"),
2099
1962
  inputPrompt: t("wizard.nextcloudTalk.botSecretInput"),
2100
1963
  allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID$1,
2101
- resolveAccount: ({ cfg, accountId }) => resolveNextcloudTalkAccount({
2102
- cfg,
2103
- accountId
2104
- }),
2105
- accountConfigured: (account) => Boolean(account.secret && account.baseUrl),
2106
- hasConfiguredValue: (account) => Boolean(hasConfiguredSecretInput(account.config.botSecret) || account.config.botSecretFile),
2107
- resolvedValue: (account) => account.secret || void 0,
2108
- envValue: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID$1 ? normalizeOptionalString(process.env.NEXTCLOUD_TALK_BOT_SECRET) : void 0,
2109
- patchAccount: ({ cfg, accountId, patch, clearFields }) => setNextcloudTalkAccountConfig(cfg, accountId, patch, clearFields),
2110
- useEnv: {
2111
- clearFields: ["botSecret", "botSecretFile"],
2112
- patch: (account) => ({ baseUrl: account.baseUrl })
1964
+ inspect: ({ cfg, accountId }) => {
1965
+ const resolvedAccount = resolveNextcloudTalkAccount({
1966
+ cfg,
1967
+ accountId
1968
+ });
1969
+ return {
1970
+ accountConfigured: Boolean(resolvedAccount.secret && resolvedAccount.baseUrl),
1971
+ hasConfiguredValue: Boolean(hasConfiguredSecretInput(resolvedAccount.config.botSecret) || resolvedAccount.config.botSecretFile),
1972
+ resolvedValue: resolvedAccount.secret || void 0,
1973
+ envValue: accountId === DEFAULT_ACCOUNT_ID$1 ? normalizeOptionalString(process.env.NEXTCLOUD_TALK_BOT_SECRET) : void 0
1974
+ };
1975
+ },
1976
+ applyUseEnv: async (params) => {
1977
+ const resolvedAccount = resolveNextcloudTalkAccount({
1978
+ cfg: params.cfg,
1979
+ accountId: params.accountId
1980
+ });
1981
+ return setNextcloudTalkAccountConfig(clearNextcloudTalkAccountFields(params.cfg, params.accountId, ["botSecret", "botSecretFile"]), params.accountId, { baseUrl: resolvedAccount.baseUrl });
2113
1982
  },
2114
- set: { clearFields: ["botSecret", "botSecretFile"] }
2115
- }), defineTokenCredential({
1983
+ applySet: async (params) => setNextcloudTalkAccountConfig(clearNextcloudTalkAccountFields(params.cfg, params.accountId, ["botSecret", "botSecretFile"]), params.accountId, { botSecret: params.value })
1984
+ }, {
2116
1985
  inputKey: "password",
2117
- configKey: "apiPassword",
2118
- configuredFields: ["apiPassword", "apiPasswordFile"],
2119
1986
  providerHint: "nextcloud-talk-api",
2120
1987
  credentialLabel: t("wizard.nextcloudTalk.apiPassword"),
2121
1988
  preferredEnvVar: "NEXTCLOUD_TALK_API_PASSWORD",
2122
1989
  envPrompt: "",
2123
1990
  keepPrompt: t("wizard.nextcloudTalk.apiPasswordKeep"),
2124
1991
  inputPrompt: t("wizard.nextcloudTalk.apiPasswordInput"),
2125
- resolveAccount: ({ cfg, accountId }) => resolveNextcloudTalkAccount({
2126
- cfg,
2127
- accountId
2128
- }),
2129
- accountConfigured: (account) => Boolean(account.config.apiUser?.trim() && (hasConfiguredSecretInput(account.config.apiPassword) || account.config.apiPasswordFile)),
2130
- hasConfiguredValue: (account) => Boolean(hasConfiguredSecretInput(account.config.apiPassword) || account.config.apiPasswordFile),
1992
+ inspect: ({ cfg, accountId }) => {
1993
+ const resolvedAccount = resolveNextcloudTalkAccount({
1994
+ cfg,
1995
+ accountId
1996
+ });
1997
+ const apiUser = resolvedAccount.config.apiUser?.trim();
1998
+ const apiPasswordConfigured = Boolean(hasConfiguredSecretInput(resolvedAccount.config.apiPassword) || resolvedAccount.config.apiPasswordFile);
1999
+ return {
2000
+ accountConfigured: Boolean(apiUser && apiPasswordConfigured),
2001
+ hasConfiguredValue: apiPasswordConfigured
2002
+ };
2003
+ },
2131
2004
  shouldPrompt: ({ credentialValues }) => credentialValues[CONFIGURE_API_FLAG] === "1",
2132
- patchAccount: ({ cfg, accountId, patch, clearFields }) => setNextcloudTalkAccountConfig(cfg, accountId, patch, clearFields),
2133
- set: { clearFields: ["apiPassword", "apiPasswordFile"] }
2134
- })],
2135
- textInputs: [baseUrlTextInput({
2005
+ applySet: async (params) => setNextcloudTalkAccountConfig(clearNextcloudTalkAccountFields(params.cfg, params.accountId, ["apiPassword", "apiPasswordFile"]), params.accountId, { apiPassword: params.value })
2006
+ }],
2007
+ textInputs: [{
2136
2008
  inputKey: "httpUrl",
2137
- configKey: "baseUrl",
2138
2009
  message: t("wizard.nextcloudTalk.instanceUrlPrompt"),
2139
- resolveAccount: ({ cfg, accountId }) => resolveNextcloudTalkAccount({
2010
+ currentValue: ({ cfg, accountId }) => resolveNextcloudTalkAccount({
2140
2011
  cfg,
2141
2012
  accountId
2142
- }),
2143
- currentValue: (account) => account.baseUrl || void 0,
2013
+ }).baseUrl || void 0,
2144
2014
  shouldPrompt: ({ currentValue }) => !currentValue,
2145
- validate: validateNextcloudTalkBaseUrl,
2146
- normalize: normalizeNextcloudTalkBaseUrl,
2147
- patchAccount: ({ cfg, accountId, patch }) => setNextcloudTalkAccountConfig(cfg, accountId, patch)
2148
- }), {
2015
+ validate: ({ value }) => validateNextcloudTalkBaseUrl(value),
2016
+ normalizeValue: ({ value }) => normalizeNextcloudTalkBaseUrl(value),
2017
+ applySet: async (params) => setNextcloudTalkAccountConfig(params.cfg, params.accountId, { baseUrl: params.value })
2018
+ }, {
2149
2019
  inputKey: "userId",
2150
2020
  message: t("wizard.nextcloudTalk.apiUserPrompt"),
2151
2021
  currentValue: ({ cfg, accountId }) => resolveNextcloudTalkAccount({
@@ -2207,14 +2077,12 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
2207
2077
  configSchema: buildChannelConfigSchema(NextcloudTalkConfigSchema),
2208
2078
  config: {
2209
2079
  ...nextcloudTalkConfigAdapter,
2210
- isConfigured: (account) => Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim()),
2080
+ isConfigured: (account) => Boolean(account.secret?.trim() && account.baseUrl?.trim()),
2211
2081
  describeAccount: (account) => describeWebhookAccountSnapshot({
2212
2082
  account,
2213
- configured: Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim()),
2083
+ configured: Boolean(account.secret?.trim() && account.baseUrl?.trim()),
2214
2084
  extra: {
2215
2085
  secretSource: account.secretSource,
2216
- tokenStatus: account.tokenStatus,
2217
- apiCredentialStatus: account.apiCredentialStatus,
2218
2086
  baseUrl: account.baseUrl ? "[set]" : "[missing]"
2219
2087
  }
2220
2088
  })
@@ -2222,7 +2090,18 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
2222
2090
  approvalCapability: nextcloudTalkApprovalAuth,
2223
2091
  doctor: nextcloudTalkDoctor,
2224
2092
  groups: {
2225
- resolveRequireMention: resolveNextcloudTalkGroupRequireMention,
2093
+ resolveRequireMention: ({ cfg, accountId, groupId }) => {
2094
+ const rooms = resolveNextcloudTalkAccount({
2095
+ cfg,
2096
+ accountId
2097
+ }).config.rooms;
2098
+ if (!rooms || !groupId) return true;
2099
+ const roomConfig = rooms[groupId];
2100
+ if (roomConfig?.requireMention !== void 0) return roomConfig.requireMention;
2101
+ const wildcardConfig = rooms["*"];
2102
+ if (wildcardConfig?.requireMention !== void 0) return wildcardConfig.requireMention;
2103
+ return true;
2104
+ },
2226
2105
  resolveToolPolicy: resolveNextcloudTalkGroupToolPolicy
2227
2106
  },
2228
2107
  messaging: {
@@ -2243,7 +2122,6 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
2243
2122
  collectRuntimeConfigAssignments
2244
2123
  },
2245
2124
  setup: nextcloudTalkSetupAdapter,
2246
- setupContract: nextcloudTalkSetupContract,
2247
2125
  status: createComputedAccountStatusAdapter({
2248
2126
  defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID$2),
2249
2127
  buildChannelSummary: ({ snapshot }) => buildWebhookChannelStatusSummary(snapshot, { secretSource: snapshot.secretSource ?? "none" }),
@@ -2266,11 +2144,9 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
2266
2144
  accountId: account.accountId,
2267
2145
  name: account.name,
2268
2146
  enabled: account.enabled,
2269
- configured: Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim()),
2147
+ configured: Boolean(account.secret?.trim() && account.baseUrl?.trim()),
2270
2148
  extra: {
2271
2149
  secretSource: account.secretSource,
2272
- tokenStatus: account.tokenStatus,
2273
- apiCredentialStatus: account.apiCredentialStatus,
2274
2150
  baseUrl: account.baseUrl ? "[set]" : "[missing]",
2275
2151
  mode: "webhook"
2276
2152
  }