@openclaw/nextcloud-talk 2026.7.2-beta.5 → 2026.7.33

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