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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as nextcloudTalkPlugin } from "./channel-De2OKnzd.js";
1
+ import { t as nextcloudTalkPlugin } from "./channel-DtnECxVM.js";
2
2
  export { nextcloudTalkPlugin };
@@ -6,7 +6,7 @@ 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 { 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
12
  import { DEFAULT_ACCOUNT_ID, createAccountListHelpers, hasConfiguredAccountValue, normalizeAccountId, resolveAccountWithDefaultFallback, resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-core";
@@ -16,7 +16,7 @@ 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,7 @@ 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 { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
44
45
  import { applyAccountNameToChannelSection, baseUrlTextInput, createSetupTranslator, createStandardChannelSetupStatus, defineTokenCredential, formatDocsLink, patchScopedAccountConfig, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
45
46
  import { createSetupInputPresenceValidator, createSetupTranslator as createSetupTranslator$1, mergeAllowFromEntries, promptParsedAllowFromForAccount, resolveSetupAccountId } from "openclaw/plugin-sdk/setup-runtime";
46
47
  import { formatDocsLink as formatDocsLink$1 } from "openclaw/plugin-sdk/setup-tools";
@@ -201,6 +202,12 @@ const nextcloudTalkApprovalAuth = createChannelApprovalAuth({
201
202
  normalizeApprover: normalizeNextcloudTalkApproverId
202
203
  }).approvalAuth;
203
204
  //#endregion
205
+ //#region extensions/nextcloud-talk/src/guarded-response.ts
206
+ async function releaseNextcloudTalkGuardedResponse(params) {
207
+ if (!params.response.bodyUsed) await params.response.body?.cancel().catch(() => void 0);
208
+ await params.release();
209
+ }
210
+ //#endregion
204
211
  //#region extensions/nextcloud-talk/src/signature.ts
205
212
  const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
206
213
  const RANDOM_HEADER = "x-nextcloud-talk-random";
@@ -344,7 +351,10 @@ async function probeNextcloudTalkBotResponseFeature(params) {
344
351
  message: `Nextcloud Talk bot "${bot.name ?? bot.id ?? "matching bot"}" has the response feature.`
345
352
  };
346
353
  } finally {
347
- await release();
354
+ await releaseNextcloudTalkGuardedResponse({
355
+ response,
356
+ release
357
+ });
348
358
  }
349
359
  } catch (error) {
350
360
  return {
@@ -426,44 +436,13 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
426
436
  }
427
437
  });
428
438
  //#endregion
429
- //#region extensions/nextcloud-talk/src/replay-guard.ts
439
+ //#region extensions/nextcloud-talk/src/replay-migration-contract.ts
430
440
  const NEXTCLOUD_TALK_PLUGIN_ID = "nextcloud-talk";
431
441
  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
- }
442
+ const NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS = 1440 * 60 * 1e3;
443
+ const NEXTCLOUD_TALK_REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
463
444
  //#endregion
464
445
  //#region extensions/nextcloud-talk/src/doctor.ts
465
- const REPLAY_DEDUPE_TTL_MS = 1440 * 60 * 1e3;
466
- const REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
467
446
  function sanitizeLegacyReplaySegment(value) {
468
447
  const trimmed = value.trim();
469
448
  if (!trimmed) return "default";
@@ -498,11 +477,11 @@ async function repairNextcloudTalkReplayDedupeState(params) {
498
477
  const result = await migratePersistentDedupeLegacyJsonFile({
499
478
  filePath: legacyPath,
500
479
  namespace: accountId,
501
- ttlMs: REPLAY_DEDUPE_TTL_MS,
480
+ ttlMs: NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS,
502
481
  memoryMaxSize: 0,
503
482
  pluginId: NEXTCLOUD_TALK_PLUGIN_ID,
504
483
  namespacePrefix: NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX,
505
- stateMaxEntries: REPLAY_DEDUPE_MAX_ENTRIES,
484
+ stateMaxEntries: NEXTCLOUD_TALK_REPLAY_DEDUPE_MAX_ENTRIES,
506
485
  env
507
486
  });
508
487
  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 +664,10 @@ async function resolveNextcloudTalkRoomKind(params) {
685
664
  });
686
665
  return kind;
687
666
  } finally {
688
- await release();
667
+ await releaseNextcloudTalkGuardedResponse({
668
+ response,
669
+ release
670
+ });
689
671
  }
690
672
  } catch (err) {
691
673
  cacheRoomInfo(key, {
@@ -724,6 +706,43 @@ function looksLikeNextcloudTalkTargetId(raw) {
724
706
  const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES = 8 * 1024;
725
707
  const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS = 200;
726
708
  const NEXTCLOUD_TALK_SEND_TIMEOUT_MS = 3e4;
709
+ const NEXTCLOUD_TALK_FORMAT_PROFILE = {
710
+ mechanism: "markdown",
711
+ constructs: {
712
+ bold: "native",
713
+ italic: "native",
714
+ underline: "native",
715
+ strikethrough: "native",
716
+ spoiler: "native",
717
+ codeInline: "native",
718
+ codeBlock: "native",
719
+ codeLanguage: "native",
720
+ linkLabel: "native",
721
+ heading: "native",
722
+ bulletList: "native",
723
+ orderedList: "native",
724
+ taskList: "native",
725
+ table: "native",
726
+ blockquote: "native",
727
+ image: "native",
728
+ mention: "native"
729
+ },
730
+ chunk: {
731
+ limit: 4e3,
732
+ unit: "chars",
733
+ hardCap: 32e3
734
+ }
735
+ };
736
+ function renderNextcloudTalkMarkdown(markdown) {
737
+ return renderMarkdownWithMarkers({
738
+ text: markdown,
739
+ styles: [],
740
+ links: []
741
+ }, {
742
+ styleMarkers: {},
743
+ escapeText: (text) => text
744
+ }, NEXTCLOUD_TALK_FORMAT_PROFILE);
745
+ }
727
746
  /** Collapses whitespace and caps an error-body prefix to a short, log-safe snippet. */
728
747
  function collapseErrorSnippet(text) {
729
748
  const collapsed = text.replace(/\s+/g, " ").trim();
@@ -802,7 +821,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
802
821
  channel: "nextcloud-talk",
803
822
  accountId: account.accountId
804
823
  });
805
- const message = convertMarkdownTables(text.trim(), tableMode);
824
+ const message = convertMarkdownTables(renderNextcloudTalkMarkdown(text.trim()), tableMode);
806
825
  const body = { message };
807
826
  if (opts.replyTo) body.replyTo = opts.replyTo;
808
827
  const bodyStr = JSON.stringify(body);
@@ -858,7 +877,10 @@ async function sendMessageNextcloudTalk(to, text, opts) {
858
877
  timestamp
859
878
  };
860
879
  } finally {
861
- await release();
880
+ await releaseNextcloudTalkGuardedResponse({
881
+ response,
882
+ release
883
+ });
862
884
  }
863
885
  }
864
886
  async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
@@ -892,7 +914,10 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
892
914
  }
893
915
  return { ok: true };
894
916
  } finally {
895
- await release();
917
+ await releaseNextcloudTalkGuardedResponse({
918
+ response,
919
+ release
920
+ });
896
921
  }
897
922
  }
898
923
  //#endregion
@@ -1188,6 +1213,7 @@ async function handleNextcloudTalkInbound(params) {
1188
1213
  },
1189
1214
  replyPipeline: {},
1190
1215
  replyOptions: {
1216
+ ...params.turnAdoptionLifecycle ? bindIngressLifecycleToReplyOptions(params.turnAdoptionLifecycle) : {},
1191
1217
  skillFilter: roomConfig?.skills,
1192
1218
  disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : void 0
1193
1219
  },
@@ -1196,6 +1222,74 @@ async function handleNextcloudTalkInbound(params) {
1196
1222
  } }
1197
1223
  });
1198
1224
  }
1225
+ var NextcloudTalkWebhookPayloadError = class extends Error {
1226
+ constructor(message, options) {
1227
+ super(message, options);
1228
+ this.name = "NextcloudTalkWebhookPayloadError";
1229
+ }
1230
+ };
1231
+ function isRecord(value) {
1232
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1233
+ }
1234
+ function parseRawObject(rawEvent) {
1235
+ let parsed;
1236
+ try {
1237
+ parsed = JSON.parse(rawEvent);
1238
+ } catch (error) {
1239
+ throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook contains invalid JSON.", { cause: error });
1240
+ }
1241
+ if (!isRecord(parsed)) throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook must be a JSON object.");
1242
+ return parsed;
1243
+ }
1244
+ function requiredString(value, field) {
1245
+ if (typeof value === "string" && value.trim()) return value.trim();
1246
+ throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk webhook is missing ${field}.`);
1247
+ }
1248
+ function inspectNextcloudTalkWebhookEnvelope(rawEvent) {
1249
+ const envelope = parseRawObject(rawEvent);
1250
+ if (envelope.type !== "Create") return null;
1251
+ const object = isRecord(envelope.object) ? envelope.object : null;
1252
+ if (object?.type !== void 0 && object.type !== "Note") return null;
1253
+ if (!object) throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook is missing object.");
1254
+ const target = isRecord(envelope.target) ? envelope.target : null;
1255
+ return {
1256
+ eventId: requiredString(object.id, "object.id"),
1257
+ laneKey: `room:${requiredString(target?.id, "target.id")}`
1258
+ };
1259
+ }
1260
+ function parseLegacyReplayKey(key) {
1261
+ const separator = key.lastIndexOf(":");
1262
+ const roomId = key.slice(0, separator).trim();
1263
+ const messageId = key.slice(separator + 1).trim();
1264
+ return separator > 0 && roomId && messageId ? {
1265
+ messageId,
1266
+ roomId
1267
+ } : null;
1268
+ }
1269
+ /** Convert the shipped replay guard's live window into durable completion tombstones. */
1270
+ async function migrateNextcloudTalkLegacyReplayState(params) {
1271
+ const entries = await params.store.entries();
1272
+ let migrated = 0;
1273
+ for (const entry of entries) {
1274
+ const identity = parseLegacyReplayKey(entry.value.key);
1275
+ if (!identity || !Number.isFinite(entry.value.seenAt)) continue;
1276
+ const marker = {
1277
+ version: 1,
1278
+ receivedAt: entry.value.seenAt,
1279
+ rawEvent: ""
1280
+ };
1281
+ const result = await params.queue.enqueue(identity.messageId, marker, {
1282
+ receivedAt: entry.value.seenAt,
1283
+ laneKey: `room:${identity.roomId}`
1284
+ });
1285
+ if (result.kind === "accepted" || result.kind === "pending" && result.record.payload.rawEvent === "") {
1286
+ if (!await params.queue.complete(identity.messageId, { completedAt: entry.value.seenAt })) throw new Error(`Failed to migrate Nextcloud Talk replay key ${entry.value.key}.`);
1287
+ }
1288
+ migrated += 1;
1289
+ }
1290
+ await params.store.clear();
1291
+ return migrated;
1292
+ }
1199
1293
  //#endregion
1200
1294
  //#region extensions/nextcloud-talk/src/monitor.ts
1201
1295
  const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
@@ -1203,34 +1297,6 @@ const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 64 * 1024;
1203
1297
  const PREAUTH_WEBHOOK_BODY_TIMEOUT_MS = 5e3;
1204
1298
  const HEALTH_PATH = "/healthz";
1205
1299
  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
1300
  const WEBHOOK_ERRORS = {
1235
1301
  missingSignatureHeaders: "Missing signature headers",
1236
1302
  invalidBackend: "Invalid backend",
@@ -1239,20 +1305,10 @@ const WEBHOOK_ERRORS = {
1239
1305
  payloadTooLarge: "Payload too large",
1240
1306
  internalServerError: "Internal server error"
1241
1307
  };
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
1308
  function formatError(err) {
1250
1309
  if (err instanceof Error) return err.message;
1251
1310
  return typeof err === "string" ? err : JSON.stringify(err);
1252
1311
  }
1253
- function parseWebhookPayload(body) {
1254
- return safeParseJsonWithSchema(NextcloudTalkWebhookPayloadSchema, body);
1255
- }
1256
1312
  function writeJsonResponse(res, status, body) {
1257
1313
  if (body) {
1258
1314
  res.writeHead(status, { "Content-Type": "application/json" });
@@ -1292,37 +1348,6 @@ function verifyWebhookSignature(params) {
1292
1348
  params.authRateLimiter.reset(params.clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE);
1293
1349
  return true;
1294
1350
  }
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
1351
  function readNextcloudTalkWebhookBody(req, maxBodyBytes) {
1327
1352
  return readRequestBodyWithLimit(req, {
1328
1353
  maxBytes: Math.min(maxBodyBytes, PREAUTH_WEBHOOK_MAX_BODY_BYTES),
@@ -1330,12 +1355,10 @@ function readNextcloudTalkWebhookBody(req, maxBodyBytes) {
1330
1355
  });
1331
1356
  }
1332
1357
  function createNextcloudTalkWebhookServer(opts) {
1333
- const { port, host, path, secret, onMessage, onError, abortSignal } = opts;
1358
+ const { port, host, path, secret, onWebhook, onError, abortSignal } = opts;
1334
1359
  const maxBodyBytes = typeof opts.maxBodyBytes === "number" && Number.isFinite(opts.maxBodyBytes) && opts.maxBodyBytes > 0 ? Math.floor(opts.maxBodyBytes) : DEFAULT_WEBHOOK_MAX_BODY_BYTES;
1335
1360
  const readBody = opts.readBody ?? readNextcloudTalkWebhookBody;
1336
1361
  const isBackendAllowed = opts.isBackendAllowed;
1337
- const shouldProcessMessage = opts.shouldProcessMessage;
1338
- const processMessage = opts.processMessage;
1339
1362
  const authRateLimitMaxRequests = typeof opts.authRateLimit?.maxRequests === "number" ? opts.authRateLimit.maxRequests : WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests;
1340
1363
  const authRateLimitWindowMs = typeof opts.authRateLimit?.windowMs === "number" ? opts.authRateLimit.windowMs : WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs;
1341
1364
  const webhookAuthRateLimiter = createAuthRateLimiter({
@@ -1379,37 +1402,8 @@ function createNextcloudTalkWebhookServer(opts) {
1379
1402
  clientIp,
1380
1403
  authRateLimiter: webhookAuthRateLimiter
1381
1404
  })) 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
- }
1405
+ await onWebhook(body);
1407
1406
  writeJsonResponse(res, 200);
1408
- try {
1409
- await onMessage(message);
1410
- } catch (err) {
1411
- onError?.(err instanceof Error ? err : new Error(formatError(err)));
1412
- }
1413
1407
  } catch (err) {
1414
1408
  if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
1415
1409
  writeWebhookError(res, 413, WEBHOOK_ERRORS.payloadTooLarge);
@@ -1419,27 +1413,48 @@ function createNextcloudTalkWebhookServer(opts) {
1419
1413
  writeWebhookError(res, 408, requestBodyErrorToText("REQUEST_BODY_TIMEOUT"));
1420
1414
  return;
1421
1415
  }
1416
+ if (err instanceof NextcloudTalkWebhookPayloadError) {
1417
+ writeWebhookError(res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
1418
+ return;
1419
+ }
1422
1420
  const error = err instanceof Error ? err : new Error(formatError(err));
1423
1421
  onError?.(error);
1424
1422
  writeWebhookError(res, 500, WEBHOOK_ERRORS.internalServerError);
1425
1423
  }
1426
1424
  })();
1427
1425
  });
1428
- const start = () => {
1429
- return new Promise((resolve) => {
1430
- server.listen(port, host, () => resolve());
1426
+ let stopRequested = false;
1427
+ let closePromise;
1428
+ const closeIfListening = () => {
1429
+ if (closePromise) return closePromise;
1430
+ if (!server.listening) return Promise.resolve();
1431
+ closePromise = new Promise((resolve) => {
1432
+ server.close(() => resolve());
1433
+ }).finally(() => {
1434
+ closePromise = void 0;
1431
1435
  });
1436
+ return closePromise;
1432
1437
  };
1433
- let stopped = false;
1434
- const stop = () => {
1435
- if (stopped) return;
1436
- stopped = true;
1437
- try {
1438
- server.close();
1439
- } catch {}
1438
+ const stop = async () => {
1439
+ stopRequested = true;
1440
+ await closeIfListening();
1441
+ };
1442
+ const start = () => {
1443
+ if (stopRequested) return Promise.resolve();
1444
+ return new Promise((resolve, reject) => {
1445
+ const onListenError = (error) => reject(error);
1446
+ server.once("error", onListenError);
1447
+ server.listen(port, host, () => {
1448
+ server.off("error", onListenError);
1449
+ (async () => {
1450
+ if (stopRequested) await closeIfListening();
1451
+ resolve();
1452
+ })().catch(reject);
1453
+ });
1454
+ });
1440
1455
  };
1441
1456
  if (abortSignal) if (abortSignal.aborted) stop();
1442
- else abortSignal.addEventListener("abort", stop, { once: true });
1457
+ else abortSignal.addEventListener("abort", () => void stop(), { once: true });
1443
1458
  return {
1444
1459
  server,
1445
1460
  start,
@@ -1447,6 +1462,164 @@ function createNextcloudTalkWebhookServer(opts) {
1447
1462
  };
1448
1463
  }
1449
1464
  //#endregion
1465
+ //#region extensions/nextcloud-talk/src/webhook-spool.ts
1466
+ const NEXTCLOUD_TALK_INGRESS_POLL_INTERVAL_MS = 500;
1467
+ const NEXTCLOUD_TALK_INGRESS_PRUNE_INTERVAL_MS = 3600 * 1e3;
1468
+ const NEXTCLOUD_TALK_INGRESS_COMPLETED_TTL_MS = 720 * 60 * 60 * 1e3;
1469
+ const NEXTCLOUD_TALK_INGRESS_COMPLETED_MAX_ENTRIES = 1e4;
1470
+ const NEXTCLOUD_TALK_INGRESS_FAILED_TTL_MS = 720 * 60 * 60 * 1e3;
1471
+ const NEXTCLOUD_TALK_INGRESS_FAILED_MAX_ENTRIES = 1e4;
1472
+ const NextcloudTalkWebhookPayloadSchema = z.object({
1473
+ type: z.enum([
1474
+ "Create",
1475
+ "Update",
1476
+ "Delete"
1477
+ ]),
1478
+ actor: z.object({
1479
+ type: z.literal("Person"),
1480
+ id: z.string().min(1),
1481
+ name: z.string()
1482
+ }),
1483
+ object: z.object({
1484
+ type: z.literal("Note"),
1485
+ id: z.string().min(1),
1486
+ name: z.string(),
1487
+ content: z.string(),
1488
+ mediaType: z.string()
1489
+ }),
1490
+ target: z.object({
1491
+ type: z.literal("Collection"),
1492
+ id: z.string().min(1),
1493
+ name: z.string()
1494
+ })
1495
+ });
1496
+ function parseClaimedMessage(payload, claimedId, claimedLaneKey) {
1497
+ if (payload.version !== 1) throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk ingress row ${claimedId} has an unsupported version.`);
1498
+ const result = NextcloudTalkWebhookPayloadSchema.safeParse(parseRawObject(payload.rawEvent));
1499
+ if (!result.success || result.data.type !== "Create" || result.data.object.id !== claimedId) throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk ingress row ${claimedId} has invalid message identity.`);
1500
+ const webhook = result.data;
1501
+ const roomId = requiredString(webhook.target.id, "target.id");
1502
+ if (claimedLaneKey !== `room:${roomId}`) throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk ingress row ${claimedId} changed room identity.`);
1503
+ return {
1504
+ messageId: webhook.object.id,
1505
+ roomToken: roomId,
1506
+ roomName: webhook.target.name,
1507
+ senderId: webhook.actor.id,
1508
+ senderName: webhook.actor.name,
1509
+ text: webhook.object.content || webhook.object.name,
1510
+ mediaType: webhook.object.mediaType || "text/plain",
1511
+ timestamp: payload.receivedAt,
1512
+ isGroupChat: true
1513
+ };
1514
+ }
1515
+ function resolveNonRetryableFailure(error) {
1516
+ if (error instanceof NextcloudTalkWebhookPayloadError) return {
1517
+ reason: "invalid-event",
1518
+ message: error.message
1519
+ };
1520
+ const message = formatErrorMessage(error);
1521
+ if (message.includes("Nextcloud Talk: bot send was rejected") || message.includes("Nextcloud Talk: forbidden")) return {
1522
+ reason: "nextcloud-talk-auth",
1523
+ message
1524
+ };
1525
+ return null;
1526
+ }
1527
+ function createNextcloudTalkWebhookSpool(options) {
1528
+ let queue = options.queue;
1529
+ const getQueue = () => {
1530
+ queue ??= getNextcloudTalkRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
1531
+ return queue;
1532
+ };
1533
+ const legacyReplayStore = options.legacyReplayStore === null ? null : options.legacyReplayStore ?? getNextcloudTalkRuntime().state.openKeyedStore({
1534
+ namespace: resolvePersistentDedupePluginStateNamespace({
1535
+ namespace: options.accountId,
1536
+ namespacePrefix: "replay-dedupe"
1537
+ }),
1538
+ maxEntries: 1e4,
1539
+ defaultTtlMs: 864e5
1540
+ });
1541
+ const legacyMigration = legacyReplayStore ? migrateNextcloudTalkLegacyReplayState({
1542
+ queue: getQueue(),
1543
+ store: legacyReplayStore
1544
+ }) : Promise.resolve();
1545
+ const monitor = createChannelIngressMonitor({
1546
+ queue: getQueue,
1547
+ inspect: (rawEvent) => inspectNextcloudTalkWebhookEnvelope(rawEvent),
1548
+ payload: {
1549
+ version: 1,
1550
+ serialize: (rawEvent, { receivedAt }) => ({
1551
+ receivedAt,
1552
+ rawEvent
1553
+ }),
1554
+ deserialize: (body) => body.rawEvent,
1555
+ encode: ({ body }) => ({
1556
+ version: 1,
1557
+ ...body
1558
+ }),
1559
+ decode: (payload) => ({
1560
+ version: payload.version,
1561
+ body: {
1562
+ receivedAt: payload.receivedAt,
1563
+ rawEvent: payload.rawEvent
1564
+ }
1565
+ }),
1566
+ 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.`)
1567
+ },
1568
+ deliver: async (_rawEvent, lifecycle, claim) => {
1569
+ const message = parseClaimedMessage(claim.payload, claim.id, claim.laneKey);
1570
+ await options.deliver(message, lifecycle);
1571
+ },
1572
+ pollIntervalMs: options.pollIntervalMs ?? NEXTCLOUD_TALK_INGRESS_POLL_INTERVAL_MS,
1573
+ waitForDeliveryIdleBeforeRepump: true,
1574
+ retention: {
1575
+ pruneIntervalMs: NEXTCLOUD_TALK_INGRESS_PRUNE_INTERVAL_MS,
1576
+ completedTtlMs: NEXTCLOUD_TALK_INGRESS_COMPLETED_TTL_MS,
1577
+ completedMaxEntries: NEXTCLOUD_TALK_INGRESS_COMPLETED_MAX_ENTRIES,
1578
+ failedTtlMs: NEXTCLOUD_TALK_INGRESS_FAILED_TTL_MS,
1579
+ failedMaxEntries: NEXTCLOUD_TALK_INGRESS_FAILED_MAX_ENTRIES
1580
+ },
1581
+ drain: {
1582
+ resolveNonRetryableFailure,
1583
+ ...options.adoptionStallTimeoutMs === void 0 ? {} : { adoptionStallTimeoutMs: options.adoptionStallTimeoutMs },
1584
+ onLog: (message) => options.runtime.log?.(`nextcloud-talk ${message}`)
1585
+ },
1586
+ ...options.abortSignal ? { abortSignal: options.abortSignal } : {},
1587
+ createStoppedError: () => /* @__PURE__ */ new Error("Nextcloud Talk ingress stopped"),
1588
+ onError: (error) => options.runtime.error?.(`nextcloud-talk ingress drain failed: ${formatErrorMessage(error)}`)
1589
+ });
1590
+ let stopping = false;
1591
+ const inFlightReceives = /* @__PURE__ */ new Set();
1592
+ const startAfterMigration = legacyMigration.then(() => {
1593
+ if (!stopping) monitor.start();
1594
+ });
1595
+ return {
1596
+ ready: async () => await startAfterMigration,
1597
+ receive: (rawEvent) => {
1598
+ if (stopping) return Promise.reject(/* @__PURE__ */ new Error("Nextcloud Talk ingress stopped"));
1599
+ const receiveTask = (async () => {
1600
+ await startAfterMigration;
1601
+ return (await monitor.admit(rawEvent)).kind === "ignored" ? "ignored" : "accepted";
1602
+ })();
1603
+ inFlightReceives.add(receiveTask);
1604
+ receiveTask.then(() => inFlightReceives.delete(receiveTask), () => inFlightReceives.delete(receiveTask));
1605
+ return receiveTask;
1606
+ },
1607
+ stop: async () => {
1608
+ stopping = true;
1609
+ const pendingReceives = [...inFlightReceives];
1610
+ const pauseTask = monitor.pause();
1611
+ await startAfterMigration.catch(() => void 0);
1612
+ await Promise.allSettled(pendingReceives);
1613
+ await monitor.stop();
1614
+ await pauseTask;
1615
+ },
1616
+ waitForIdle: async () => {
1617
+ await startAfterMigration.catch(() => void 0);
1618
+ await monitor.waitForIdle();
1619
+ }
1620
+ };
1621
+ }
1622
+ //#endregion
1450
1623
  //#region extensions/nextcloud-talk/src/monitor-runtime.ts
1451
1624
  const DEFAULT_WEBHOOK_PORT = 8788;
1452
1625
  const DEFAULT_WEBHOOK_HOST = "0.0.0.0";
@@ -1475,13 +1648,29 @@ async function monitorNextcloudTalkProvider(opts) {
1475
1648
  accountId: account.accountId
1476
1649
  });
1477
1650
  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)}`);
1651
+ const spool = (opts.createSpool ?? createNextcloudTalkWebhookSpool)({
1652
+ accountId: account.accountId,
1653
+ runtime,
1654
+ abortSignal: opts.abortSignal,
1655
+ deliver: async (message, lifecycle) => {
1656
+ core.channel.activity.record({
1657
+ channel: "nextcloud-talk",
1658
+ accountId: account.accountId,
1659
+ direction: "inbound",
1660
+ at: message.timestamp
1661
+ });
1662
+ if (opts.onMessage) await opts.onMessage(message, lifecycle);
1663
+ else await handleNextcloudTalkInbound({
1664
+ message,
1665
+ account,
1666
+ config: cfg,
1667
+ runtime,
1668
+ statusSink: opts.statusSink,
1669
+ turnAdoptionLifecycle: lifecycle
1670
+ });
1482
1671
  }
1483
1672
  });
1484
- const { start, stop } = createNextcloudTalkWebhookServer({
1673
+ const server = (opts.createServer ?? createNextcloudTalkWebhookServer)({
1485
1674
  port,
1486
1675
  host,
1487
1676
  path,
@@ -1490,39 +1679,34 @@ async function monitorNextcloudTalkProvider(opts) {
1490
1679
  if (!expectedBackendOrigin) return true;
1491
1680
  return normalizeOrigin(backend) === expectedBackendOrigin;
1492
1681
  },
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 () => {},
1682
+ onWebhook: spool.receive,
1517
1683
  onError: (error) => {
1518
1684
  logger.error(`[nextcloud-talk:${account.accountId}] webhook error: ${error.message}`);
1519
1685
  },
1520
1686
  abortSignal: opts.abortSignal
1521
1687
  });
1522
- if (opts.abortSignal?.aborted) return { stop };
1523
- await start();
1688
+ let stopPromise;
1689
+ const stop = () => {
1690
+ stopPromise ??= (async () => {
1691
+ await server.stop();
1692
+ await spool.stop();
1693
+ })();
1694
+ return stopPromise;
1695
+ };
1696
+ if (opts.abortSignal && !opts.abortSignal.aborted) opts.abortSignal.addEventListener("abort", () => void stop(), { once: true });
1524
1697
  if (opts.abortSignal?.aborted) {
1525
- stop();
1698
+ await stop();
1699
+ return { stop };
1700
+ }
1701
+ try {
1702
+ await spool.ready();
1703
+ await server.start();
1704
+ } catch (error) {
1705
+ await stop();
1706
+ throw error;
1707
+ }
1708
+ if (opts.abortSignal?.aborted) {
1709
+ await stop();
1526
1710
  return { stop };
1527
1711
  }
1528
1712
  const publicUrl = account.config.webhookPublicUrl ?? `http://${host === "0.0.0.0" ? "localhost" : host}:${port}${path}`;
@@ -1540,7 +1724,7 @@ const nextcloudTalkGatewayAdapter = {
1540
1724
  accountId: ctx.accountId,
1541
1725
  setStatus: ctx.setStatus
1542
1726
  });
1543
- await runStoppablePassiveMonitor({
1727
+ await runPassiveAccountLifecycle({
1544
1728
  abortSignal: ctx.abortSignal,
1545
1729
  start: async () => await monitorNextcloudTalkProvider({
1546
1730
  accountId: account.accountId,
@@ -1548,7 +1732,10 @@ const nextcloudTalkGatewayAdapter = {
1548
1732
  runtime: ctx.runtime,
1549
1733
  abortSignal: ctx.abortSignal,
1550
1734
  statusSink
1551
- })
1735
+ }),
1736
+ stop: async (monitor) => {
1737
+ await monitor.stop();
1738
+ }
1552
1739
  });
1553
1740
  },
1554
1741
  logoutAccount: async ({ accountId, cfg }) => {
@@ -1702,6 +1889,9 @@ function resolveNextcloudTalkOutboundSessionRoute(params) {
1702
1889
  //#region extensions/nextcloud-talk/src/setup-core.ts
1703
1890
  const t$1 = createSetupTranslator$1();
1704
1891
  const channel$1 = "nextcloud-talk";
1892
+ function readOptionalString(value) {
1893
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1894
+ }
1705
1895
  function addWildcardAllowFrom(allowFrom) {
1706
1896
  return mergeAllowFromEntries(allowFrom, ["*"]);
1707
1897
  }
@@ -1820,7 +2010,17 @@ const nextcloudTalkDmPolicy = {
1820
2010
  promptAllowFrom: promptNextcloudTalkAllowFromForAccount
1821
2011
  };
1822
2012
  const nextcloudTalkSetupAdapter = {
2013
+ singleAccountKeysToMove: ["rooms"],
1823
2014
  resolveAccountId: ({ accountId }) => normalizeAccountId$1(accountId),
2015
+ prepareAccountConfigInput: ({ input }) => {
2016
+ const setupInput = input;
2017
+ return {
2018
+ ...setupInput,
2019
+ baseUrl: setupInput.baseUrl ?? readOptionalString(setupInput.url),
2020
+ secret: setupInput.secret ?? readOptionalString(setupInput.token) ?? readOptionalString(setupInput.password),
2021
+ secretFile: setupInput.secretFile ?? readOptionalString(setupInput.tokenFile)
2022
+ };
2023
+ },
1824
2024
  applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
1825
2025
  cfg,
1826
2026
  channelKey: channel$1,
@@ -1850,6 +2050,72 @@ const nextcloudTalkSetupAdapter = {
1850
2050
  });
1851
2051
  }
1852
2052
  };
2053
+ const nextcloudTalkSetupContract = defineChannelSetupContract({
2054
+ fields: {
2055
+ baseUrl: {
2056
+ kind: "string",
2057
+ cli: {
2058
+ flags: "--base-url <url>",
2059
+ description: "Nextcloud base URL"
2060
+ }
2061
+ },
2062
+ url: {
2063
+ kind: "string",
2064
+ cli: {
2065
+ flags: "--url <url>",
2066
+ description: "Legacy Nextcloud base URL alias"
2067
+ }
2068
+ },
2069
+ secret: {
2070
+ kind: "string",
2071
+ sensitive: true,
2072
+ cli: {
2073
+ flags: "--secret <secret>",
2074
+ description: "Nextcloud Talk bot secret"
2075
+ }
2076
+ },
2077
+ token: {
2078
+ kind: "string",
2079
+ sensitive: true,
2080
+ cli: {
2081
+ flags: "--token <secret>",
2082
+ description: "Legacy Nextcloud bot secret alias"
2083
+ }
2084
+ },
2085
+ password: {
2086
+ kind: "string",
2087
+ sensitive: true,
2088
+ cli: {
2089
+ flags: "--password <secret>",
2090
+ description: "Legacy Nextcloud bot secret alias"
2091
+ }
2092
+ },
2093
+ secretFile: {
2094
+ kind: "string",
2095
+ sensitive: true,
2096
+ cli: {
2097
+ flags: "--secret-file <path>",
2098
+ description: "Nextcloud Talk bot secret file"
2099
+ }
2100
+ },
2101
+ tokenFile: {
2102
+ kind: "string",
2103
+ sensitive: true,
2104
+ cli: {
2105
+ flags: "--token-file <path>",
2106
+ description: "Legacy Nextcloud bot secret file alias"
2107
+ }
2108
+ },
2109
+ useEnv: {
2110
+ kind: "boolean",
2111
+ cli: {
2112
+ flags: "--use-env",
2113
+ description: "Use Nextcloud Talk environment credentials"
2114
+ }
2115
+ }
2116
+ },
2117
+ legacyAdapter: nextcloudTalkSetupAdapter
2118
+ });
1853
2119
  //#endregion
1854
2120
  //#region extensions/nextcloud-talk/src/setup-surface.ts
1855
2121
  const t = createSetupTranslator();
@@ -2065,6 +2331,7 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
2065
2331
  collectRuntimeConfigAssignments
2066
2332
  },
2067
2333
  setup: nextcloudTalkSetupAdapter,
2334
+ setupContract: nextcloudTalkSetupContract,
2068
2335
  status: createComputedAccountStatusAdapter({
2069
2336
  defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID$2),
2070
2337
  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-DtnECxVM.js";
2
2
  export { nextcloudTalkPlugin };
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@openclaw/nextcloud-talk",
3
- "version": "2026.7.2-beta.3",
3
+ "version": "2026.7.2-beta.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/nextcloud-talk",
9
- "version": "2026.7.2-beta.3",
9
+ "version": "2026.7.2-beta.4",
10
10
  "dependencies": {
11
11
  "zod": "4.4.3"
12
12
  },
13
13
  "peerDependencies": {
14
- "openclaw": ">=2026.7.2-beta.3"
14
+ "openclaw": ">=2026.7.2-beta.4"
15
15
  },
16
16
  "peerDependenciesMeta": {
17
17
  "openclaw": {
@@ -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.4",
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.4"
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.4"
44
128
  },
45
129
  "build": {
46
- "openclawVersion": "2026.7.2-beta.3"
130
+ "openclawVersion": "2026.7.2-beta.4"
47
131
  },
48
132
  "release": {
49
133
  "publishToClawHub": true,