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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@ Official OpenClaw channel plugin for Nextcloud Talk conversations.
5
5
  Install from OpenClaw:
6
6
 
7
7
  ```bash
8
- openclaw plugin add @openclaw/nextcloud-talk
8
+ openclaw plugins install @openclaw/nextcloud-talk
9
9
  ```
10
10
 
11
11
  Configure the Nextcloud server and Talk credentials in OpenClaw, then enable the conversations where agents should receive and send messages.
package/dist/api.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as nextcloudTalkPlugin } from "./channel-De2OKnzd.js";
1
+ import { t as nextcloudTalkPlugin } from "./channel-D6MTjGj3.js";
2
2
  export { nextcloudTalkPlugin };
@@ -1,22 +1,22 @@
1
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
2
  import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-fNhK1wag.js";
3
3
  import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-DGgvfVqm.js";
4
- import { describeWebhookAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
4
+ import { createAccountListHelpers, 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
7
  import { createAllowlistProviderRouteAllowlistWarningCollector, resolveScopeRequireMention, resolveScopeToolsPolicy } from "openclaw/plugin-sdk/channel-policy";
8
8
  import { buildWebhookChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
9
- import { convertMarkdownTables, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
9
+ import { FormatCapabilityProfile, convertMarkdownTables, renderMarkdownWithMarkers, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
10
10
  import { createChannelApprovalAuth } from "openclaw/plugin-sdk/approval-auth-runtime";
11
11
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
12
- import { DEFAULT_ACCOUNT_ID, createAccountListHelpers, hasConfiguredAccountValue, normalizeAccountId, resolveAccountWithDefaultFallback, resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-core";
12
+ import { DEFAULT_ACCOUNT_ID, hasConfiguredAccountValue, normalizeAccountId, resolveAccountWithDefaultFallback } from "openclaw/plugin-sdk/account-core";
13
13
  import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
14
14
  import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
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
18
  import { buildChannelInboundEventContext, resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
19
- import { createAccountStatusSink, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
19
+ import { bindIngressLifecycleToReplyOptions, createAccountStatusSink, createChannelIngressMonitor, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, resolveChannelStreamingBlockEnabled, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-outbound";
20
20
  import { ssrfPolicyFromPrivateNetworkOptIn, ssrfPolicyFromPrivateNetworkOptIn as ssrfPolicyFromPrivateNetworkOptIn$1 } from "openclaw/plugin-sdk/ssrf-runtime";
21
21
  import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
22
22
  import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
@@ -27,11 +27,11 @@ import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2 } from "openclaw/plugin-sdk/
27
27
  import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, buildChannelConfigSchema, buildGroupEntrySchema, buildMultiAccountChannelSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
28
28
  import { formatAllowFromLowercase, resolveAllowlistMatchByCandidates } from "openclaw/plugin-sdk/allow-from";
29
29
  import { adaptScopedAccountAccessor, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
30
- import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime, runStoppablePassiveMonitor, safeParseJsonWithSchema } from "openclaw/plugin-sdk/extension-shared";
30
+ import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
31
31
  import { z } from "zod";
32
32
  import os from "node:os";
33
33
  import path from "node:path";
34
- import { createChannelReplayGuard, migratePersistentDedupeLegacyJsonFile } from "openclaw/plugin-sdk/persistent-dedupe";
34
+ import { migratePersistentDedupeLegacyJsonFile, resolvePersistentDedupePluginStateNamespace } 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
37
  import { buildChannelKeyCandidates, normalizeChannelSlug, resolveChannelEntryMatchWithFallback } from "openclaw/plugin-sdk/channel-targets";
@@ -41,6 +41,8 @@ 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";
44
46
  import { applyAccountNameToChannelSection, baseUrlTextInput, createSetupTranslator, createStandardChannelSetupStatus, defineTokenCredential, formatDocsLink, patchScopedAccountConfig, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
45
47
  import { createSetupInputPresenceValidator, createSetupTranslator as createSetupTranslator$1, mergeAllowFromEntries, promptParsedAllowFromForAccount, resolveSetupAccountId } from "openclaw/plugin-sdk/setup-runtime";
46
48
  import { formatDocsLink as formatDocsLink$1 } from "openclaw/plugin-sdk/setup-tools";
@@ -83,8 +85,9 @@ function isTruthyEnvValue(value) {
83
85
  const debugAccounts = (...args) => {
84
86
  if (isTruthyEnvValue(process.env.OPENCLAW_DEBUG_NEXTCLOUD_TALK_ACCOUNTS)) console.warn("[nextcloud-talk:accounts]", ...args);
85
87
  };
86
- const { listAccountIds: listNextcloudTalkAccountIdsInternal, resolveDefaultAccountId: resolveDefaultNextcloudTalkAccountId } = createAccountListHelpers("nextcloud-talk", {
88
+ const { listAccountIds: listNextcloudTalkAccountIdsInternal, resolveDefaultAccountId: resolveDefaultNextcloudTalkAccountId, resolveAccountConfig: mergeNextcloudTalkAccountConfig } = createAccountListHelpers("nextcloud-talk", {
87
89
  normalizeAccountId,
90
+ omitKeys: ["defaultAccount"],
88
91
  hasImplicitDefaultAccount: (cfg) => {
89
92
  const channel = cfg.channels?.["nextcloud-talk"];
90
93
  return Boolean(channel?.baseUrl?.trim() && (hasConfiguredAccountValue(channel.botSecret) || channel.botSecretFile?.trim() || process.env.NEXTCLOUD_TALK_BOT_SECRET?.trim()));
@@ -95,15 +98,6 @@ function listNextcloudTalkAccountIds(cfg) {
95
98
  debugAccounts("listNextcloudTalkAccountIds", ids);
96
99
  return ids;
97
100
  }
98
- function mergeNextcloudTalkAccountConfig(cfg, accountId) {
99
- return resolveMergedAccountConfig({
100
- channelConfig: cfg.channels?.["nextcloud-talk"],
101
- accounts: cfg.channels?.["nextcloud-talk"]?.accounts,
102
- accountId,
103
- omitKeys: ["defaultAccount"],
104
- normalizeAccountId
105
- });
106
- }
107
101
  function resolveNextcloudTalkSecret(cfg, opts) {
108
102
  const resolvedAccountId = opts.accountId ?? resolveDefaultNextcloudTalkAccountId(cfg);
109
103
  const merged = mergeNextcloudTalkAccountConfig(cfg, resolvedAccountId);
@@ -201,6 +195,12 @@ const nextcloudTalkApprovalAuth = createChannelApprovalAuth({
201
195
  normalizeApprover: normalizeNextcloudTalkApproverId
202
196
  }).approvalAuth;
203
197
  //#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();
202
+ }
203
+ //#endregion
204
204
  //#region extensions/nextcloud-talk/src/signature.ts
205
205
  const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
206
206
  const RANDOM_HEADER = "x-nextcloud-talk-random";
@@ -344,7 +344,10 @@ async function probeNextcloudTalkBotResponseFeature(params) {
344
344
  message: `Nextcloud Talk bot "${bot.name ?? bot.id ?? "matching bot"}" has the response feature.`
345
345
  };
346
346
  } finally {
347
- await release();
347
+ await releaseNextcloudTalkGuardedResponse({
348
+ response,
349
+ release
350
+ });
348
351
  }
349
352
  } catch (error) {
350
353
  return {
@@ -426,44 +429,13 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
426
429
  }
427
430
  });
428
431
  //#endregion
429
- //#region extensions/nextcloud-talk/src/replay-guard.ts
432
+ //#region extensions/nextcloud-talk/src/replay-migration-contract.ts
430
433
  const NEXTCLOUD_TALK_PLUGIN_ID = "nextcloud-talk";
431
434
  const NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX = "replay-dedupe";
432
- const DEFAULT_REPLAY_TTL_MS = 1440 * 60 * 1e3;
433
- const DEFAULT_MEMORY_MAX_SIZE = 1e3;
434
- const DEFAULT_STATE_MAX_ENTRIES = 1e4;
435
- function buildNextcloudTalkReplayKey(params) {
436
- const roomToken = params.roomToken.trim();
437
- const messageId = params.messageId.trim();
438
- if (!roomToken || !messageId) return null;
439
- return `${roomToken}:${messageId}`;
440
- }
441
- function createNextcloudTalkReplayGuard(options) {
442
- const stateDir = options.stateDir?.trim();
443
- const baseOptions = {
444
- ttlMs: options.ttlMs ?? DEFAULT_REPLAY_TTL_MS,
445
- memoryMaxSize: options.memoryMaxSize ?? DEFAULT_MEMORY_MAX_SIZE
446
- };
447
- return createChannelReplayGuard({
448
- dedupe: stateDir ? {
449
- ...baseOptions,
450
- pluginId: NEXTCLOUD_TALK_PLUGIN_ID,
451
- namespacePrefix: NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX,
452
- stateMaxEntries: options.stateMaxEntries ?? options.fileMaxEntries ?? DEFAULT_STATE_MAX_ENTRIES,
453
- env: {
454
- ...process.env,
455
- OPENCLAW_STATE_DIR: stateDir
456
- },
457
- onDiskError: options.onDiskError
458
- } : baseOptions,
459
- buildReplayKey: buildNextcloudTalkReplayKey,
460
- namespace: (event) => event.accountId
461
- });
462
- }
435
+ const NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS = 1440 * 60 * 1e3;
436
+ const NEXTCLOUD_TALK_REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
463
437
  //#endregion
464
438
  //#region extensions/nextcloud-talk/src/doctor.ts
465
- const REPLAY_DEDUPE_TTL_MS = 1440 * 60 * 1e3;
466
- const REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
467
439
  function sanitizeLegacyReplaySegment(value) {
468
440
  const trimmed = value.trim();
469
441
  if (!trimmed) return "default";
@@ -498,11 +470,11 @@ async function repairNextcloudTalkReplayDedupeState(params) {
498
470
  const result = await migratePersistentDedupeLegacyJsonFile({
499
471
  filePath: legacyPath,
500
472
  namespace: accountId,
501
- ttlMs: REPLAY_DEDUPE_TTL_MS,
473
+ ttlMs: NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS,
502
474
  memoryMaxSize: 0,
503
475
  pluginId: NEXTCLOUD_TALK_PLUGIN_ID,
504
476
  namespacePrefix: NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX,
505
- stateMaxEntries: REPLAY_DEDUPE_MAX_ENTRIES,
477
+ stateMaxEntries: NEXTCLOUD_TALK_REPLAY_DEDUPE_MAX_ENTRIES,
506
478
  env
507
479
  });
508
480
  changes.push(`Migrated Nextcloud Talk replay dedupe cache for account "${accountId}" to SQLite (${result.imported} imported, ${result.skippedExpired} expired, ${result.skippedExisting} already current).`);
@@ -685,7 +657,10 @@ async function resolveNextcloudTalkRoomKind(params) {
685
657
  });
686
658
  return kind;
687
659
  } finally {
688
- await release();
660
+ await releaseNextcloudTalkGuardedResponse({
661
+ response,
662
+ release
663
+ });
689
664
  }
690
665
  } catch (err) {
691
666
  cacheRoomInfo(key, {
@@ -724,6 +699,24 @@ function looksLikeNextcloudTalkTargetId(raw) {
724
699
  const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES = 8 * 1024;
725
700
  const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS = 200;
726
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
+ }
727
720
  /** Collapses whitespace and caps an error-body prefix to a short, log-safe snippet. */
728
721
  function collapseErrorSnippet(text) {
729
722
  const collapsed = text.replace(/\s+/g, " ").trim();
@@ -802,7 +795,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
802
795
  channel: "nextcloud-talk",
803
796
  accountId: account.accountId
804
797
  });
805
- const message = convertMarkdownTables(text.trim(), tableMode);
798
+ const message = convertMarkdownTables(renderNextcloudTalkMarkdown(text.trim()), tableMode);
806
799
  const body = { message };
807
800
  if (opts.replyTo) body.replyTo = opts.replyTo;
808
801
  const bodyStr = JSON.stringify(body);
@@ -858,7 +851,10 @@ async function sendMessageNextcloudTalk(to, text, opts) {
858
851
  timestamp
859
852
  };
860
853
  } finally {
861
- await release();
854
+ await releaseNextcloudTalkGuardedResponse({
855
+ response,
856
+ release
857
+ });
862
858
  }
863
859
  }
864
860
  async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
@@ -892,7 +888,10 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
892
888
  }
893
889
  return { ok: true };
894
890
  } finally {
895
- await release();
891
+ await releaseNextcloudTalkGuardedResponse({
892
+ response,
893
+ release
894
+ });
896
895
  }
897
896
  }
898
897
  //#endregion
@@ -1188,6 +1187,7 @@ async function handleNextcloudTalkInbound(params) {
1188
1187
  },
1189
1188
  replyPipeline: {},
1190
1189
  replyOptions: {
1190
+ ...params.turnAdoptionLifecycle ? bindIngressLifecycleToReplyOptions(params.turnAdoptionLifecycle) : {},
1191
1191
  skillFilter: roomConfig?.skills,
1192
1192
  disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : void 0
1193
1193
  },
@@ -1196,6 +1196,74 @@ async function handleNextcloudTalkInbound(params) {
1196
1196
  } }
1197
1197
  });
1198
1198
  }
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
+ }
1199
1267
  //#endregion
1200
1268
  //#region extensions/nextcloud-talk/src/monitor.ts
1201
1269
  const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
@@ -1203,34 +1271,6 @@ const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 64 * 1024;
1203
1271
  const PREAUTH_WEBHOOK_BODY_TIMEOUT_MS = 5e3;
1204
1272
  const HEALTH_PATH = "/healthz";
1205
1273
  const WEBHOOK_AUTH_RATE_LIMIT_SCOPE = "nextcloud-talk-webhook-auth";
1206
- const NextcloudTalkWebhookPayloadSchema = z.object({
1207
- type: z.enum([
1208
- "Create",
1209
- "Update",
1210
- "Delete"
1211
- ]),
1212
- actor: z.object({
1213
- type: z.literal("Person"),
1214
- id: z.string().min(1),
1215
- name: z.string()
1216
- }),
1217
- object: z.object({
1218
- type: z.literal("Note"),
1219
- id: z.string().min(1),
1220
- name: z.string(),
1221
- content: z.string(),
1222
- mediaType: z.string()
1223
- }),
1224
- target: z.object({
1225
- type: z.literal("Collection"),
1226
- id: z.string().min(1),
1227
- name: z.string()
1228
- })
1229
- });
1230
- const NextcloudTalkWebhookEnvelopeSchema = z.object({
1231
- type: z.string().min(1),
1232
- object: z.object({ type: z.string().min(1).optional() }).passthrough().optional()
1233
- });
1234
1274
  const WEBHOOK_ERRORS = {
1235
1275
  missingSignatureHeaders: "Missing signature headers",
1236
1276
  invalidBackend: "Invalid backend",
@@ -1239,20 +1279,10 @@ const WEBHOOK_ERRORS = {
1239
1279
  payloadTooLarge: "Payload too large",
1240
1280
  internalServerError: "Internal server error"
1241
1281
  };
1242
- async function processNextcloudTalkReplayGuardedMessage(params) {
1243
- return (await params.replayGuard.processGuarded({
1244
- accountId: params.accountId,
1245
- roomToken: params.message.roomToken,
1246
- messageId: params.message.messageId
1247
- }, params.handleMessage, { onError: "commit" })).kind === "processed" ? "processed" : "duplicate";
1248
- }
1249
1282
  function formatError(err) {
1250
1283
  if (err instanceof Error) return err.message;
1251
1284
  return typeof err === "string" ? err : JSON.stringify(err);
1252
1285
  }
1253
- function parseWebhookPayload(body) {
1254
- return safeParseJsonWithSchema(NextcloudTalkWebhookPayloadSchema, body);
1255
- }
1256
1286
  function writeJsonResponse(res, status, body) {
1257
1287
  if (body) {
1258
1288
  res.writeHead(status, { "Content-Type": "application/json" });
@@ -1292,37 +1322,6 @@ function verifyWebhookSignature(params) {
1292
1322
  params.authRateLimiter.reset(params.clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE);
1293
1323
  return true;
1294
1324
  }
1295
- function decodeWebhookCreateMessage(params) {
1296
- const envelope = safeParseJsonWithSchema(NextcloudTalkWebhookEnvelopeSchema, params.body);
1297
- if (!envelope) {
1298
- writeWebhookError(params.res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
1299
- return { kind: "invalid" };
1300
- }
1301
- if (envelope.type !== "Create") return { kind: "ignore" };
1302
- if (envelope.object?.type && envelope.object.type !== "Note") return { kind: "ignore" };
1303
- const payload = parseWebhookPayload(params.body);
1304
- if (!payload) {
1305
- writeWebhookError(params.res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
1306
- return { kind: "invalid" };
1307
- }
1308
- return {
1309
- kind: "message",
1310
- message: payloadToInboundMessage(payload)
1311
- };
1312
- }
1313
- function payloadToInboundMessage(payload) {
1314
- return {
1315
- messageId: payload.object.id,
1316
- roomToken: payload.target.id,
1317
- roomName: payload.target.name,
1318
- senderId: payload.actor.id,
1319
- senderName: payload.actor.name ?? "",
1320
- text: payload.object.content || payload.object.name || "",
1321
- mediaType: payload.object.mediaType || "text/plain",
1322
- timestamp: Date.now(),
1323
- isGroupChat: true
1324
- };
1325
- }
1326
1325
  function readNextcloudTalkWebhookBody(req, maxBodyBytes) {
1327
1326
  return readRequestBodyWithLimit(req, {
1328
1327
  maxBytes: Math.min(maxBodyBytes, PREAUTH_WEBHOOK_MAX_BODY_BYTES),
@@ -1330,12 +1329,10 @@ function readNextcloudTalkWebhookBody(req, maxBodyBytes) {
1330
1329
  });
1331
1330
  }
1332
1331
  function createNextcloudTalkWebhookServer(opts) {
1333
- const { port, host, path, secret, onMessage, onError, abortSignal } = opts;
1332
+ const { port, host, path, secret, onWebhook, onError, abortSignal } = opts;
1334
1333
  const maxBodyBytes = typeof opts.maxBodyBytes === "number" && Number.isFinite(opts.maxBodyBytes) && opts.maxBodyBytes > 0 ? Math.floor(opts.maxBodyBytes) : DEFAULT_WEBHOOK_MAX_BODY_BYTES;
1335
1334
  const readBody = opts.readBody ?? readNextcloudTalkWebhookBody;
1336
1335
  const isBackendAllowed = opts.isBackendAllowed;
1337
- const shouldProcessMessage = opts.shouldProcessMessage;
1338
- const processMessage = opts.processMessage;
1339
1336
  const authRateLimitMaxRequests = typeof opts.authRateLimit?.maxRequests === "number" ? opts.authRateLimit.maxRequests : WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests;
1340
1337
  const authRateLimitWindowMs = typeof opts.authRateLimit?.windowMs === "number" ? opts.authRateLimit.windowMs : WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs;
1341
1338
  const webhookAuthRateLimiter = createAuthRateLimiter({
@@ -1379,37 +1376,8 @@ function createNextcloudTalkWebhookServer(opts) {
1379
1376
  clientIp,
1380
1377
  authRateLimiter: webhookAuthRateLimiter
1381
1378
  })) return;
1382
- const decoded = decodeWebhookCreateMessage({
1383
- body,
1384
- res
1385
- });
1386
- if (decoded.kind === "invalid") return;
1387
- if (decoded.kind === "ignore") {
1388
- writeJsonResponse(res, 200);
1389
- return;
1390
- }
1391
- const message = decoded.message;
1392
- if (processMessage) {
1393
- writeJsonResponse(res, 200);
1394
- try {
1395
- await processMessage(message);
1396
- } catch (err) {
1397
- onError?.(err instanceof Error ? err : new Error(formatError(err)));
1398
- }
1399
- return;
1400
- }
1401
- if (shouldProcessMessage) {
1402
- if (!await shouldProcessMessage(message)) {
1403
- writeJsonResponse(res, 200);
1404
- return;
1405
- }
1406
- }
1379
+ await onWebhook(body);
1407
1380
  writeJsonResponse(res, 200);
1408
- try {
1409
- await onMessage(message);
1410
- } catch (err) {
1411
- onError?.(err instanceof Error ? err : new Error(formatError(err)));
1412
- }
1413
1381
  } catch (err) {
1414
1382
  if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
1415
1383
  writeWebhookError(res, 413, WEBHOOK_ERRORS.payloadTooLarge);
@@ -1419,27 +1387,48 @@ function createNextcloudTalkWebhookServer(opts) {
1419
1387
  writeWebhookError(res, 408, requestBodyErrorToText("REQUEST_BODY_TIMEOUT"));
1420
1388
  return;
1421
1389
  }
1390
+ if (err instanceof NextcloudTalkWebhookPayloadError) {
1391
+ writeWebhookError(res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
1392
+ return;
1393
+ }
1422
1394
  const error = err instanceof Error ? err : new Error(formatError(err));
1423
1395
  onError?.(error);
1424
1396
  writeWebhookError(res, 500, WEBHOOK_ERRORS.internalServerError);
1425
1397
  }
1426
1398
  })();
1427
1399
  });
1428
- const start = () => {
1429
- return new Promise((resolve) => {
1430
- server.listen(port, host, () => resolve());
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;
1431
1409
  });
1410
+ return closePromise;
1432
1411
  };
1433
- let stopped = false;
1434
- const stop = () => {
1435
- if (stopped) return;
1436
- stopped = true;
1437
- try {
1438
- server.close();
1439
- } catch {}
1412
+ const stop = async () => {
1413
+ stopRequested = true;
1414
+ await closeIfListening();
1415
+ };
1416
+ 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
+ });
1428
+ });
1440
1429
  };
1441
1430
  if (abortSignal) if (abortSignal.aborted) stop();
1442
- else abortSignal.addEventListener("abort", stop, { once: true });
1431
+ else abortSignal.addEventListener("abort", () => void stop(), { once: true });
1443
1432
  return {
1444
1433
  server,
1445
1434
  start,
@@ -1447,6 +1436,164 @@ function createNextcloudTalkWebhookServer(opts) {
1447
1436
  };
1448
1437
  }
1449
1438
  //#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
1450
1597
  //#region extensions/nextcloud-talk/src/monitor-runtime.ts
1451
1598
  const DEFAULT_WEBHOOK_PORT = 8788;
1452
1599
  const DEFAULT_WEBHOOK_HOST = "0.0.0.0";
@@ -1475,13 +1622,29 @@ async function monitorNextcloudTalkProvider(opts) {
1475
1622
  accountId: account.accountId
1476
1623
  });
1477
1624
  const expectedBackendOrigin = normalizeOrigin(account.baseUrl);
1478
- const replayGuard = createNextcloudTalkReplayGuard({
1479
- stateDir: core.state.resolveStateDir(process.env, os.homedir),
1480
- onDiskError: (error) => {
1481
- logger.warn(`[nextcloud-talk:${account.accountId}] replay guard disk error: ${String(error)}`);
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
+ });
1482
1645
  }
1483
1646
  });
1484
- const { start, stop } = createNextcloudTalkWebhookServer({
1647
+ const server = (opts.createServer ?? createNextcloudTalkWebhookServer)({
1485
1648
  port,
1486
1649
  host,
1487
1650
  path,
@@ -1490,39 +1653,34 @@ async function monitorNextcloudTalkProvider(opts) {
1490
1653
  if (!expectedBackendOrigin) return true;
1491
1654
  return normalizeOrigin(backend) === expectedBackendOrigin;
1492
1655
  },
1493
- processMessage: async (message) => {
1494
- if (await processNextcloudTalkReplayGuardedMessage({
1495
- replayGuard,
1496
- accountId: account.accountId,
1497
- message,
1498
- handleMessage: async () => {
1499
- core.channel.activity.record({
1500
- channel: "nextcloud-talk",
1501
- accountId: account.accountId,
1502
- direction: "inbound",
1503
- at: message.timestamp
1504
- });
1505
- if (opts.onMessage) await opts.onMessage(message);
1506
- else await handleNextcloudTalkInbound({
1507
- message,
1508
- account,
1509
- config: cfg,
1510
- runtime,
1511
- statusSink: opts.statusSink
1512
- });
1513
- }
1514
- }) === "duplicate") logger.warn(`[nextcloud-talk:${account.accountId}] replayed webhook ignored room=${message.roomToken} messageId=${message.messageId}`);
1515
- },
1516
- onMessage: async () => {},
1656
+ onWebhook: spool.receive,
1517
1657
  onError: (error) => {
1518
1658
  logger.error(`[nextcloud-talk:${account.accountId}] webhook error: ${error.message}`);
1519
1659
  },
1520
1660
  abortSignal: opts.abortSignal
1521
1661
  });
1522
- if (opts.abortSignal?.aborted) return { stop };
1523
- await start();
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 });
1524
1671
  if (opts.abortSignal?.aborted) {
1525
- stop();
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
+ }
1682
+ if (opts.abortSignal?.aborted) {
1683
+ await stop();
1526
1684
  return { stop };
1527
1685
  }
1528
1686
  const publicUrl = account.config.webhookPublicUrl ?? `http://${host === "0.0.0.0" ? "localhost" : host}:${port}${path}`;
@@ -1540,7 +1698,7 @@ const nextcloudTalkGatewayAdapter = {
1540
1698
  accountId: ctx.accountId,
1541
1699
  setStatus: ctx.setStatus
1542
1700
  });
1543
- await runStoppablePassiveMonitor({
1701
+ await runPassiveAccountLifecycle({
1544
1702
  abortSignal: ctx.abortSignal,
1545
1703
  start: async () => await monitorNextcloudTalkProvider({
1546
1704
  accountId: account.accountId,
@@ -1548,7 +1706,10 @@ const nextcloudTalkGatewayAdapter = {
1548
1706
  runtime: ctx.runtime,
1549
1707
  abortSignal: ctx.abortSignal,
1550
1708
  statusSink
1551
- })
1709
+ }),
1710
+ stop: async (monitor) => {
1711
+ await monitor.stop();
1712
+ }
1552
1713
  });
1553
1714
  },
1554
1715
  logoutAccount: async ({ accountId, cfg }) => {
@@ -1702,8 +1863,8 @@ function resolveNextcloudTalkOutboundSessionRoute(params) {
1702
1863
  //#region extensions/nextcloud-talk/src/setup-core.ts
1703
1864
  const t$1 = createSetupTranslator$1();
1704
1865
  const channel$1 = "nextcloud-talk";
1705
- function addWildcardAllowFrom(allowFrom) {
1706
- return mergeAllowFromEntries(allowFrom, ["*"]);
1866
+ function readOptionalString(value) {
1867
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1707
1868
  }
1708
1869
  function normalizeNextcloudTalkBaseUrl(value) {
1709
1870
  return value?.trim().replace(/\/+$/, "") ?? "";
@@ -1712,46 +1873,15 @@ function validateNextcloudTalkBaseUrl(value) {
1712
1873
  if (!value) return "Required";
1713
1874
  if (!value.startsWith("http://") && !value.startsWith("https://")) return "URL must start with http:// or https://";
1714
1875
  }
1715
- function setNextcloudTalkAccountConfig(cfg, accountId, updates) {
1876
+ function setNextcloudTalkAccountConfig(cfg, accountId, updates, clearFields) {
1716
1877
  return patchScopedAccountConfig({
1717
1878
  cfg,
1718
1879
  channelKey: channel$1,
1719
1880
  accountId,
1720
- patch: updates
1881
+ patch: updates,
1882
+ ...clearFields ? { clearFields } : {}
1721
1883
  });
1722
1884
  }
1723
- function clearNextcloudTalkAccountFields(cfg, accountId, fields) {
1724
- const section = cfg.channels?.["nextcloud-talk"];
1725
- if (!section) return cfg;
1726
- if (accountId === DEFAULT_ACCOUNT_ID$1) {
1727
- const nextSection = { ...section };
1728
- for (const field of fields) delete nextSection[field];
1729
- return {
1730
- ...cfg,
1731
- channels: {
1732
- ...cfg.channels,
1733
- "nextcloud-talk": nextSection
1734
- }
1735
- };
1736
- }
1737
- const currentAccount = section.accounts?.[accountId];
1738
- if (!currentAccount) return cfg;
1739
- const nextAccount = { ...currentAccount };
1740
- for (const field of fields) delete nextAccount[field];
1741
- return {
1742
- ...cfg,
1743
- channels: {
1744
- ...cfg.channels,
1745
- "nextcloud-talk": {
1746
- ...section,
1747
- accounts: {
1748
- ...section.accounts,
1749
- [accountId]: nextAccount
1750
- }
1751
- }
1752
- }
1753
- };
1754
- }
1755
1885
  async function promptNextcloudTalkAllowFrom(params) {
1756
1886
  return await promptParsedAllowFromForAccount({
1757
1887
  cfg: params.cfg,
@@ -1790,37 +1920,28 @@ async function promptNextcloudTalkAllowFromForAccount(params) {
1790
1920
  accountId
1791
1921
  });
1792
1922
  }
1793
- const nextcloudTalkDmPolicy = {
1923
+ const nextcloudTalkDmPolicy = createChannelDmPolicy({
1794
1924
  label: "Nextcloud Talk",
1795
1925
  channel: channel$1,
1796
- policyKey: "channels.nextcloud-talk.dmPolicy",
1797
- allowFromKey: "channels.nextcloud-talk.allowFrom",
1798
- resolveConfigKeys: (cfg, accountId) => (accountId ?? resolveDefaultNextcloudTalkAccountId(cfg)) !== DEFAULT_ACCOUNT_ID$1 ? {
1799
- policyKey: `channels.nextcloud-talk.accounts.${accountId ?? resolveDefaultNextcloudTalkAccountId(cfg)}.dmPolicy`,
1800
- allowFromKey: `channels.nextcloud-talk.accounts.${accountId ?? resolveDefaultNextcloudTalkAccountId(cfg)}.allowFrom`
1801
- } : {
1802
- policyKey: "channels.nextcloud-talk.dmPolicy",
1803
- allowFromKey: "channels.nextcloud-talk.allowFrom"
1804
- },
1805
- getCurrent: (cfg, accountId) => resolveNextcloudTalkAccount({
1926
+ resolveAccount: (cfg, accountId) => resolveNextcloudTalkAccount({
1806
1927
  cfg,
1807
1928
  accountId: accountId ?? resolveDefaultNextcloudTalkAccountId(cfg)
1808
- }).config.dmPolicy ?? "pairing",
1809
- setPolicy: (cfg, policy, accountId) => {
1810
- const resolvedAccountId = accountId ?? resolveDefaultNextcloudTalkAccountId(cfg);
1811
- const resolved = resolveNextcloudTalkAccount({
1812
- cfg,
1813
- accountId: resolvedAccountId
1814
- });
1815
- return setNextcloudTalkAccountConfig(cfg, resolvedAccountId, {
1816
- dmPolicy: policy,
1817
- ...policy === "open" ? { allowFrom: addWildcardAllowFrom(resolved.config.allowFrom) } : {}
1818
- });
1819
- },
1929
+ }),
1930
+ applyPatch: ({ cfg, account, patch }) => setNextcloudTalkAccountConfig(cfg, account.accountId, patch),
1820
1931
  promptAllowFrom: promptNextcloudTalkAllowFromForAccount
1821
- };
1932
+ });
1822
1933
  const nextcloudTalkSetupAdapter = {
1934
+ singleAccountKeysToMove: ["rooms"],
1823
1935
  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
+ },
1824
1945
  applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
1825
1946
  cfg,
1826
1947
  channelKey: channel$1,
@@ -1838,18 +1959,83 @@ const nextcloudTalkSetupAdapter = {
1838
1959
  }),
1839
1960
  applyAccountConfig: ({ cfg, accountId, input }) => {
1840
1961
  const setupInput = input;
1841
- const namedConfig = applyAccountNameToChannelSection({
1962
+ return setNextcloudTalkAccountConfig(applyAccountNameToChannelSection({
1842
1963
  cfg,
1843
1964
  channelKey: channel$1,
1844
1965
  accountId,
1845
1966
  name: setupInput.name
1846
- });
1847
- return setNextcloudTalkAccountConfig(setupInput.useEnv ? clearNextcloudTalkAccountFields(namedConfig, accountId, ["botSecret", "botSecretFile"]) : namedConfig, accountId, {
1967
+ }), accountId, {
1848
1968
  baseUrl: normalizeNextcloudTalkBaseUrl(setupInput.baseUrl),
1849
1969
  ...setupInput.useEnv ? {} : setupInput.secretFile ? { botSecretFile: setupInput.secretFile } : setupInput.secret ? { botSecret: setupInput.secret } : {}
1850
- });
1970
+ }, setupInput.useEnv ? ["botSecret", "botSecretFile"] : void 0);
1851
1971
  }
1852
1972
  };
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
+ });
1853
2039
  //#endregion
1854
2040
  //#region extensions/nextcloud-talk/src/setup-surface.ts
1855
2041
  const t = createSetupTranslator();
@@ -1926,9 +2112,7 @@ const nextcloudTalkSetupWizard = {
1926
2112
  hasConfiguredValue: (account) => Boolean(hasConfiguredSecretInput(account.config.botSecret) || account.config.botSecretFile),
1927
2113
  resolvedValue: (account) => account.secret || void 0,
1928
2114
  envValue: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID$1 ? normalizeOptionalString(process.env.NEXTCLOUD_TALK_BOT_SECRET) : void 0,
1929
- patchAccount: ({ cfg, accountId, patch, clearFields }) => {
1930
- return setNextcloudTalkAccountConfig(clearNextcloudTalkAccountFields(cfg, accountId, clearFields), accountId, patch);
1931
- },
2115
+ patchAccount: ({ cfg, accountId, patch, clearFields }) => setNextcloudTalkAccountConfig(cfg, accountId, patch, clearFields),
1932
2116
  useEnv: {
1933
2117
  clearFields: ["botSecret", "botSecretFile"],
1934
2118
  patch: (account) => ({ baseUrl: account.baseUrl })
@@ -1951,7 +2135,7 @@ const nextcloudTalkSetupWizard = {
1951
2135
  accountConfigured: (account) => Boolean(account.config.apiUser?.trim() && (hasConfiguredSecretInput(account.config.apiPassword) || account.config.apiPasswordFile)),
1952
2136
  hasConfiguredValue: (account) => Boolean(hasConfiguredSecretInput(account.config.apiPassword) || account.config.apiPasswordFile),
1953
2137
  shouldPrompt: ({ credentialValues }) => credentialValues[CONFIGURE_API_FLAG] === "1",
1954
- patchAccount: ({ cfg, accountId, patch, clearFields }) => setNextcloudTalkAccountConfig(clearNextcloudTalkAccountFields(cfg, accountId, clearFields), accountId, patch),
2138
+ patchAccount: ({ cfg, accountId, patch, clearFields }) => setNextcloudTalkAccountConfig(cfg, accountId, patch, clearFields),
1955
2139
  set: { clearFields: ["apiPassword", "apiPasswordFile"] }
1956
2140
  })],
1957
2141
  textInputs: [baseUrlTextInput({
@@ -2065,6 +2249,7 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
2065
2249
  collectRuntimeConfigAssignments
2066
2250
  },
2067
2251
  setup: nextcloudTalkSetupAdapter,
2252
+ setupContract: nextcloudTalkSetupContract,
2068
2253
  status: createComputedAccountStatusAdapter({
2069
2254
  defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID$2),
2070
2255
  buildChannelSummary: ({ snapshot }) => buildWebhookChannelStatusSummary(snapshot, { secretSource: snapshot.secretSource ?? "none" }),
@@ -1,2 +1,2 @@
1
- import { t as nextcloudTalkPlugin } from "./channel-De2OKnzd.js";
1
+ import { t as nextcloudTalkPlugin } from "./channel-D6MTjGj3.js";
2
2
  export { nextcloudTalkPlugin };
@@ -9,12 +9,6 @@
9
9
  "channels": [
10
10
  "nextcloud-talk"
11
11
  ],
12
- "channelEnvVars": {
13
- "nextcloud-talk": [
14
- "NEXTCLOUD_TALK_BOT_SECRET",
15
- "NEXTCLOUD_TALK_API_PASSWORD"
16
- ]
17
- },
18
12
  "configSchema": {
19
13
  "type": "object",
20
14
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/nextcloud-talk",
3
- "version": "2026.7.2-beta.3",
3
+ "version": "2026.7.2-beta.5",
4
4
  "description": "OpenClaw Nextcloud Talk channel plugin for conversations.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "type": "module",
10
10
  "peerDependencies": {
11
- "openclaw": ">=2026.7.2-beta.3"
11
+ "openclaw": ">=2026.7.2-beta.5"
12
12
  },
13
13
  "peerDependenciesMeta": {
14
14
  "openclaw": {
@@ -20,8 +20,19 @@
20
20
  "./index.ts"
21
21
  ],
22
22
  "setupEntry": "./dist/setup-entry.js",
23
+ "setupFeatures": {
24
+ "configPromotion": true
25
+ },
23
26
  "channel": {
24
27
  "id": "nextcloud-talk",
28
+ "configuredState": {
29
+ "env": {
30
+ "anyOf": [
31
+ "NEXTCLOUD_TALK_BOT_SECRET",
32
+ "NEXTCLOUD_TALK_API_PASSWORD"
33
+ ]
34
+ }
35
+ },
25
36
  "label": "Nextcloud Talk",
26
37
  "selectionLabel": "Nextcloud Talk (self-hosted)",
27
38
  "docsPath": "/channels/nextcloud-talk",
@@ -32,7 +43,80 @@
32
43
  "nc"
33
44
  ],
34
45
  "order": 65,
35
- "quickstartAllowFrom": true
46
+ "quickstartAllowFrom": true,
47
+ "setup": {
48
+ "fields": [
49
+ {
50
+ "key": "baseUrl",
51
+ "kind": "string",
52
+ "cli": {
53
+ "flags": "--base-url <url>",
54
+ "description": "Nextcloud base URL"
55
+ }
56
+ },
57
+ {
58
+ "key": "url",
59
+ "kind": "string",
60
+ "cli": {
61
+ "flags": "--url <url>",
62
+ "description": "Legacy Nextcloud base URL alias"
63
+ }
64
+ },
65
+ {
66
+ "key": "secret",
67
+ "kind": "string",
68
+ "sensitive": true,
69
+ "cli": {
70
+ "flags": "--secret <secret>",
71
+ "description": "Nextcloud Talk bot secret"
72
+ }
73
+ },
74
+ {
75
+ "key": "token",
76
+ "kind": "string",
77
+ "sensitive": true,
78
+ "cli": {
79
+ "flags": "--token <secret>",
80
+ "description": "Legacy Nextcloud bot secret alias"
81
+ }
82
+ },
83
+ {
84
+ "key": "password",
85
+ "kind": "string",
86
+ "sensitive": true,
87
+ "cli": {
88
+ "flags": "--password <secret>",
89
+ "description": "Legacy Nextcloud bot secret alias"
90
+ }
91
+ },
92
+ {
93
+ "key": "secretFile",
94
+ "kind": "string",
95
+ "sensitive": true,
96
+ "cli": {
97
+ "flags": "--secret-file <path>",
98
+ "description": "Nextcloud Talk bot secret file"
99
+ }
100
+ },
101
+ {
102
+ "key": "tokenFile",
103
+ "kind": "string",
104
+ "sensitive": true,
105
+ "cli": {
106
+ "flags": "--token-file <path>",
107
+ "description": "Legacy Nextcloud bot secret file alias"
108
+ }
109
+ },
110
+ {
111
+ "key": "useEnv",
112
+ "kind": "boolean",
113
+ "cli": {
114
+ "flags": "--use-env",
115
+ "description": "Use Nextcloud Talk environment credentials"
116
+ }
117
+ }
118
+ ]
119
+ }
36
120
  },
37
121
  "install": {
38
122
  "npmSpec": "@openclaw/nextcloud-talk",
@@ -40,10 +124,10 @@
40
124
  "minHostVersion": ">=2026.4.10"
41
125
  },
42
126
  "compat": {
43
- "pluginApi": ">=2026.7.2-beta.3"
127
+ "pluginApi": ">=2026.7.2-beta.5"
44
128
  },
45
129
  "build": {
46
- "openclawVersion": "2026.7.2-beta.3"
130
+ "openclawVersion": "2026.7.2-beta.5"
47
131
  },
48
132
  "release": {
49
133
  "publishToClawHub": true,
@@ -60,7 +144,6 @@
60
144
  "files": [
61
145
  "dist/**",
62
146
  "openclaw.plugin.json",
63
- "npm-shrinkwrap.json",
64
147
  "README.md"
65
148
  ],
66
149
  "bundledDependencies": [
@@ -1,32 +0,0 @@
1
- {
2
- "name": "@openclaw/nextcloud-talk",
3
- "version": "2026.7.2-beta.3",
4
- "lockfileVersion": 3,
5
- "requires": true,
6
- "packages": {
7
- "": {
8
- "name": "@openclaw/nextcloud-talk",
9
- "version": "2026.7.2-beta.3",
10
- "dependencies": {
11
- "zod": "4.4.3"
12
- },
13
- "peerDependencies": {
14
- "openclaw": ">=2026.7.2-beta.3"
15
- },
16
- "peerDependenciesMeta": {
17
- "openclaw": {
18
- "optional": true
19
- }
20
- }
21
- },
22
- "node_modules/zod": {
23
- "version": "4.4.3",
24
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
25
- "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
26
- "license": "MIT",
27
- "funding": {
28
- "url": "https://github.com/sponsors/colinhacks"
29
- }
30
- }
31
- }
32
- }