UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

2,182 lines 79.8 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import { b as parseStrictPositiveInteger, y as parseStrictNonNegativeInteger } from "./number-coercion-CJQ8TR--.js";
import { y as resolveStateDir } from "./paths-mvMm5bYV.js";
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { t as formatDocsLink } from "./links-CsLBrRff.js";
import { l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js";
import { s as hasConfiguredSecretInput, u as normalizeResolvedSecretInputString } from "./types.secrets-_0JOMGE5.js";
import { a as tryReadSecretFileSync } from "./secret-file-PClaG9G0.js";
import { n as normalizeAccountId, t as DEFAULT_ACCOUNT_ID } from "./account-id-Df9e41E6.js";
import { At as boolean, Et as array, Nn as record, Rn as string, Tn as object, dn as literal, wn as number, yt as _enum } from "./schemas-6cH6bZ7o.js";
import { t as safeParseJsonWithSchema } from "./zod-parse-Bip-sZi_.js";
import { s as createAuthRateLimiter } from "./auth-rate-limit-Bwr29t2Y.js";
import { h as MarkdownConfigSchema, l as GroupPolicySchema, o as DmPolicySchema, x as ReplyRuntimeConfigSchemaShape, z as requireOpenAllowFrom } from "./zod-schema.core-1wQTyhOa.js";
import { r as buildChannelConfigSchema } from "./config-schema-CO2ikh7C.js";
import { l as ToolPolicySchema } from "./zod-schema.agent-runtime-BTWfpsXE.js";
import { r as fetchWithSsrFGuard } from "./fetch-guard-BttkNCLm.js";
import { a as resolveChannelEntryMatchWithFallback, n as buildChannelKeyCandidates, r as normalizeChannelSlug, s as resolveNestedAllowlistDecision } from "./channel-config-C-DSDncb.js";
import { r as deliverFormattedTextWithAttachments } from "./reply-payload-D-VMfpYI.js";
import { m as readProviderJsonResponse } from "./provider-http-errors-DqaqQLLZ.js";
import { b as readStringParam, l as jsonResult } from "./common-C4yy9V-D.js";
import { a as warnMissingProviderGroupPolicyFallbackOnce, n as resolveAllowlistProviderRuntimeGroupPolicy, r as resolveDefaultGroupPolicy, t as GROUP_POLICY_BLOCKED_LABEL } from "./runtime-group-policy-CSD1Is8G.js";
import { t as createMessageReceiptFromOutboundResults } from "./receipt-B3SXxhHV.js";
import { t as clearAccountEntryFields } from "./config-helpers-DU5uyx8S.js";
import { l as createScopedDmSecurityResolver, s as createScopedChannelConfigAdapter, t as adaptScopedAccountAccessor } from "./channel-config-helpers-lj5quus3.js";
import { t as buildOutboundBaseSessionKey } from "./base-session-key-BpTC-eng.js";
import "./channel-targets-B-q89MaL.js";
import "./error-runtime-C8vbtAJt.js";
import "./number-runtime-DBLVDypr.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import { t as convertMarkdownTables } from "./tables-BgtXxld3.js";
import "./text-chunking-D45TeeEs.js";
import { c as resolveMergedAccountConfig, i as hasConfiguredAccountValue, r as describeWebhookAccountSnapshot, t as createAccountListHelpers } from "./account-helpers-pcH3lGFY.js";
import { t as resolveAccountWithDefaultFallback } from "./account-core-DIaAc_G-.js";
import { a as createSetupInputPresenceValidator, l as patchScopedAccountConfig, t as applyAccountNameToChannelSection } from "./setup-helpers-DO5nSeNy.js";
import { i as createChatChannelPlugin } from "./core-DSxVv-v1.js";
import "./channel-core-IukkNCd-.js";
import "./routing-CQ63ICPX.js";
import { r as buildSecretInputSchema } from "./secret-input-DVCzFGxN.js";
import { t as resolveMarkdownTableMode } from "./markdown-tables-LfSv7QSx.js";
import { t as createAccountStatusSink } from "./channel-lifecycle.core-Bfh0_sXw.js";
import { t as formatAllowFromLowercase } from "./allow-from-DCaV3hzI.js";
import "./channel-config-schema-D7l1xlvD.js";
import { y as createAllowlistProviderRouteAllowlistWarningCollector } from "./channel-policy-CIW58SA5.js";
import { t as createSetupTranslator } from "./i18n-7g_f4oaD.js";
import { J as setSetupChannelEnabled, f as createStandardChannelSetupStatus, j as promptParsedAllowFromForAccount, v as mergeAllowFromEntries, z as resolveSetupAccountId } from "./setup-wizard-helpers-Dv-t1nb8.js";
import "./setup-DHfdBmUU.js";
import "./setup-runtime-BvLK-H0G.js";
import "./setup-tools-c-3eI4zR.js";
import { t as resolveApprovalApprovers } from "./approval-approvers-CM-4zRvR.js";
import { t as createResolvedApproverActionAuthAdapter } from "./approval-auth-helpers-DU3OxkhL.js";
import { t as requireRuntimeConfig } from "./plugin-config-runtime-CpE6DYgJ.js";
import { r as resolveInboundRouteEnvelopeBuilderWithRuntime } from "./inbound-envelope-DF4N8x5q.js";
import { T as defineChannelMessageAdapter } from "./channel-outbound-B3_Zy-kG.js";
import { n as logInboundDrop } from "./logging-gUWPKC5g.js";
import { a as isRequestBodyLimitError, c as requestBodyErrorToText, s as readRequestBodyWithLimit } from "./http-body-DASeqdKJ.js";
import { d as ssrfPolicyFromPrivateNetworkOptIn } from "./ssrf-policy-sZLyyYht.js";
import "./ssrf-runtime-BOGN5pUi.js";
import "./secret-file-runtime-DFl-BIAk.js";
import "./markdown-table-runtime-j_C4iTF_.js";
import "./channel-plugin-common-DTFXpVi6.js";
import { d as createDefaultChannelRuntimeState, s as buildWebhookChannelStatusSummary, u as createComputedAccountStatusAdapter } from "./status-helpers-BevxX6Pu.js";
import { f as requireChannelOpenAllowFrom, h as runStoppablePassiveMonitor, m as resolveLoggerBackedRuntime } from "./extension-shared-B8fkO3TV.js";
import { r as resolveReactionMessageId } from "./channel-actions-BOfvC-Gl.js";
import { i as resolveStableChannelMessageIngress, t as channelIngressRoutes } from "./message-access-mwxdTUGC.js";
import "./channel-ingress-runtime-rGltVkKC.js";
import { i as createPairingPrefixStripper, n as createChannelPairingController, r as createLoggedPairingApprovalNotifier } from "./channel-pairing-Bk6_JhTm.js";
import "./provider-http-BXCBCi4E.js";
import { a as migratePersistentDedupeLegacyJsonFile, t as createClaimableDedupe } from "./persistent-dedupe-tc3aSkBu.js";
import "./state-paths-DzVdErem.js";
import { r as WEBHOOK_RATE_LIMIT_DEFAULTS } from "./webhook-ingress-Dgd3Jtks.js";
import { t as getNextcloudTalkRuntime } from "./runtime-api-DlVkfe9B.js";
import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-8u75tHTk.js";
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-BMJpcdal.js";
import { readFileSync } from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import os from "node:os";
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
//#region extensions/nextcloud-talk/src/accounts.ts
function isTruthyEnvValue(value) {
	const normalized = normalizeLowercaseStringOrEmpty(value);
	return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on";
}
const debugAccounts = (...args) => {
	if (isTruthyEnvValue(process.env.OPENCLAW_DEBUG_NEXTCLOUD_TALK_ACCOUNTS)) console.warn("[nextcloud-talk:accounts]", ...args);
};
const { listAccountIds: listNextcloudTalkAccountIdsInternal, resolveDefaultAccountId: resolveDefaultNextcloudTalkAccountId } = createAccountListHelpers("nextcloud-talk", {
	normalizeAccountId,
	hasImplicitDefaultAccount: (cfg) => {
		const channel = cfg.channels?.["nextcloud-talk"];
		return Boolean(channel?.baseUrl?.trim() && (hasConfiguredAccountValue(channel.botSecret) || channel.botSecretFile?.trim() || process.env.NEXTCLOUD_TALK_BOT_SECRET?.trim()));
	}
});
function listNextcloudTalkAccountIds(cfg) {
	const ids = listNextcloudTalkAccountIdsInternal(cfg);
	debugAccounts("listNextcloudTalkAccountIds", ids);
	return ids;
}
function mergeNextcloudTalkAccountConfig(cfg, accountId) {
	return resolveMergedAccountConfig({
		channelConfig: cfg.channels?.["nextcloud-talk"],
		accounts: cfg.channels?.["nextcloud-talk"]?.accounts,
		accountId,
		omitKeys: ["defaultAccount"],
		normalizeAccountId
	});
}
function resolveNextcloudTalkSecret(cfg, opts) {
	const resolvedAccountId = opts.accountId ?? resolveDefaultNextcloudTalkAccountId(cfg);
	const merged = mergeNextcloudTalkAccountConfig(cfg, resolvedAccountId);
	const envSecret = normalizeOptionalString(process.env.NEXTCLOUD_TALK_BOT_SECRET);
	if (envSecret && resolvedAccountId === "default") return {
		secret: envSecret,
		source: "env"
	};
	if (merged.botSecretFile) {
		const fileSecret = tryReadSecretFileSync(merged.botSecretFile, "Nextcloud Talk bot secret file", { rejectSymlink: true });
		if (fileSecret) return {
			secret: fileSecret,
			source: "secretFile"
		};
	}
	const inlineSecret = normalizeResolvedSecretInputString({
		value: merged.botSecret,
		path: `channels.nextcloud-talk.accounts.${resolvedAccountId}.botSecret`
	});
	if (inlineSecret) return {
		secret: inlineSecret,
		source: "config"
	};
	return {
		secret: "",
		source: "none"
	};
}
function resolveNextcloudTalkAccount(params) {
	const baseEnabled = params.cfg.channels?.["nextcloud-talk"]?.enabled !== false;
	const resolvedAccountId = params.accountId ?? resolveDefaultNextcloudTalkAccountId(params.cfg);
	const resolve = (accountId) => {
		const merged = mergeNextcloudTalkAccountConfig(params.cfg, accountId);
		const accountEnabled = merged.enabled !== false;
		const enabled = baseEnabled && accountEnabled;
		const secretResolution = resolveNextcloudTalkSecret(params.cfg, { accountId });
		const baseUrl = merged.baseUrl?.trim()?.replace(/\/$/, "") ?? "";
		debugAccounts("resolve", {
			accountId,
			enabled,
			secretSource: secretResolution.source,
			baseUrl: baseUrl ? "[set]" : "[missing]"
		});
		return {
			accountId,
			enabled,
			name: normalizeOptionalString(merged.name),
			baseUrl,
			secret: secretResolution.secret,
			secretSource: secretResolution.source,
			config: merged
		};
	};
	return resolveAccountWithDefaultFallback({
		accountId: resolvedAccountId,
		normalizeAccountId,
		resolvePrimary: resolve,
		hasCredential: (account) => account.secretSource !== "none",
		resolveDefaultAccountId: () => resolveDefaultNextcloudTalkAccountId(params.cfg)
	});
}
//#endregion
//#region extensions/nextcloud-talk/src/approval-auth.ts
function normalizeNextcloudTalkApproverId(value) {
	return normalizeOptionalLowercaseString(String(value).trim().replace(/^(nextcloud-talk|nc-talk|nc):/i, ""));
}
const nextcloudTalkApprovalAuth = createResolvedApproverActionAuthAdapter({
	channelLabel: "Nextcloud Talk",
	resolveApprovers: ({ cfg, accountId }) => {
		return resolveApprovalApprovers({
			allowFrom: resolveNextcloudTalkAccount({
				cfg,
				accountId
			}).config.allowFrom,
			normalizeApprover: normalizeNextcloudTalkApproverId
		});
	},
	normalizeSenderId: (value) => normalizeNextcloudTalkApproverId(value)
});
//#endregion
//#region extensions/nextcloud-talk/src/api-credentials.ts
function resolveNextcloudTalkApiCredentials(params) {
	const apiUser = params.apiUser?.trim();
	if (!apiUser) return;
	const inlinePassword = normalizeResolvedSecretInputString({
		value: params.apiPassword,
		path: "channels.nextcloud-talk.apiPassword"
	});
	if (inlinePassword) return {
		apiUser,
		apiPassword: inlinePassword
	};
	if (!params.apiPasswordFile) return;
	try {
		const filePassword = readFileSync(params.apiPasswordFile, "utf-8").trim();
		return filePassword ? {
			apiUser,
			apiPassword: filePassword
		} : void 0;
	} catch {
		return;
	}
}
//#endregion
//#region extensions/nextcloud-talk/src/signature.ts
const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
const RANDOM_HEADER = "x-nextcloud-talk-random";
const BACKEND_HEADER = "x-nextcloud-talk-backend";
/**
* Verify the HMAC-SHA256 signature of an incoming webhook request.
* Signature is calculated as: HMAC-SHA256(random + body, secret)
*/
function verifyNextcloudTalkSignature(params) {
	const { signature, random, body, secret } = params;
	if (!signature || !random || !secret) return false;
	const expected = createHmac("sha256", secret).update(random + body).digest("hex");
	const expectedBuf = Buffer.from(expected, "utf8");
	const signatureBuf = Buffer.from(signature, "utf8");
	const maxLen = Math.max(expectedBuf.length, signatureBuf.length);
	const paddedExpected = Buffer.alloc(maxLen);
	const paddedSignature = Buffer.alloc(maxLen);
	expectedBuf.copy(paddedExpected);
	signatureBuf.copy(paddedSignature);
	const timingResult = timingSafeEqual(paddedExpected, paddedSignature);
	return expectedBuf.length === signatureBuf.length && timingResult;
}
/**
* Extract webhook headers from an incoming request.
*/
function extractNextcloudTalkHeaders(headers) {
	const getHeader = (name) => {
		const value = headers[name] ?? headers[normalizeLowercaseStringOrEmpty(name)];
		return Array.isArray(value) ? value[0] : value;
	};
	const signature = getHeader(SIGNATURE_HEADER);
	const random = getHeader(RANDOM_HEADER);
	const backend = getHeader(BACKEND_HEADER);
	if (!signature || !random || !backend) return null;
	return {
		signature,
		random,
		backend
	};
}
/**
* Generate signature headers for an outbound request to Nextcloud Talk.
*/
function generateNextcloudTalkSignature(params) {
	const { body, secret } = params;
	const random = randomBytes(32).toString("hex");
	return {
		random,
		signature: createHmac("sha256", secret).update(random + body).digest("hex")
	};
}
//#endregion
//#region extensions/nextcloud-talk/src/bot-preflight.ts
const BOT_FEATURE_RESPONSE = 2;
function normalizeUrlForMatch(value) {
	if (!value?.trim()) return "";
	try {
		const url = new URL(value.trim());
		url.hash = "";
		return url.toString().replace(/\/$/, "");
	} catch {
		return value.trim().replace(/\/$/, "");
	}
}
function coerceFeatureMask(value) {
	if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value;
	return parseStrictNonNegativeInteger(value);
}
function formatMissingResponseFeatureMessage(bot, features) {
	const id = bot.id == null ? "unknown" : String(bot.id);
	return `Nextcloud Talk bot "${bot.name?.trim() || "matching bot"}" (${id}) is missing the response feature${typeof features === "number" ? ` (features=${features})` : ""}; outbound replies will fail. Run ./occ talk:bot:state --feature webhook --feature response --feature reaction ${id} 1 or reinstall the bot with --feature response.`;
}
async function probeNextcloudTalkBotResponseFeature(params) {
	const { account, timeoutMs } = params;
	const baseUrl = account.baseUrl?.trim();
	if (!baseUrl) return {
		ok: true,
		skipped: true,
		code: "missing_base_url",
		message: "Nextcloud Talk bot response feature probe skipped: baseUrl is not configured."
	};
	const webhookUrl = normalizeUrlForMatch(account.config.webhookPublicUrl);
	if (!webhookUrl) return {
		ok: true,
		skipped: true,
		code: "missing_webhook_url",
		message: "Nextcloud Talk bot response feature probe skipped: webhookPublicUrl is not configured."
	};
	const credentials = resolveNextcloudTalkApiCredentials({
		apiUser: account.config.apiUser,
		apiPassword: account.config.apiPassword,
		apiPasswordFile: account.config.apiPasswordFile
	});
	if (!credentials) return {
		ok: true,
		skipped: true,
		code: "missing_api_credentials",
		message: "Nextcloud Talk bot response feature probe skipped: apiUser/apiPassword are not configured."
	};
	const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/admin`;
	const auth = Buffer.from(`${credentials.apiUser}:${credentials.apiPassword}`, "utf-8").toString("base64");
	try {
		const { response, release } = await fetchWithSsrFGuard({
			url,
			init: {
				method: "GET",
				headers: {
					Authorization: `Basic ${auth}`,
					"OCS-APIRequest": "true",
					Accept: "application/json"
				}
			},
			auditContext: "nextcloud-talk.bot-response-preflight",
			policy: ssrfPolicyFromPrivateNetworkOptIn(account.config),
			timeoutMs
		});
		try {
			if (!response.ok) {
				const body = await response.text().catch(() => "");
				return {
					ok: false,
					code: "api_error",
					status: response.status,
					message: `Nextcloud Talk bot response feature probe failed (${response.status})${body ? `: ${body}` : ""}`
				};
			}
			const payload = await readProviderJsonResponse(response, "Nextcloud Talk bot response feature probe failed");
			const bot = (Array.isArray(payload.ocs?.data) ? payload.ocs.data : []).find((entry) => normalizeUrlForMatch(entry.url) === webhookUrl);
			if (!bot) return {
				ok: false,
				code: "bot_not_found",
				message: `Nextcloud Talk bot response feature probe could not find a bot with webhook URL ${webhookUrl}.`
			};
			const features = coerceFeatureMask(bot.features);
			if (features == null || (features & BOT_FEATURE_RESPONSE) !== BOT_FEATURE_RESPONSE) return {
				ok: false,
				code: "missing_response_feature",
				botId: bot.id == null ? void 0 : String(bot.id),
				botName: bot.name,
				features,
				message: formatMissingResponseFeatureMessage(bot, features)
			};
			return {
				ok: true,
				code: "ok",
				botId: bot.id == null ? void 0 : String(bot.id),
				botName: bot.name,
				features,
				message: `Nextcloud Talk bot "${bot.name ?? bot.id ?? "matching bot"}" has the response feature.`
			};
		} finally {
			await release();
		}
	} catch (error) {
		return {
			ok: false,
			code: "request_failed",
			message: `Nextcloud Talk bot response feature probe failed: ${error instanceof Error ? error.message : formatErrorMessage(error)}`
		};
	}
}
//#endregion
//#region extensions/nextcloud-talk/src/channel.adapters.ts
const nextcloudTalkConfigAdapter = createScopedChannelConfigAdapter({
	sectionKey: "nextcloud-talk",
	listAccountIds: listNextcloudTalkAccountIds,
	resolveAccount: adaptScopedAccountAccessor(resolveNextcloudTalkAccount),
	defaultAccountId: resolveDefaultNextcloudTalkAccountId,
	clearBaseFields: [
		"botSecret",
		"botSecretFile",
		"baseUrl",
		"name"
	],
	resolveAllowFrom: (account) => account.config.allowFrom,
	formatAllowFrom: (allowFrom) => formatAllowFromLowercase({
		allowFrom,
		stripPrefixRe: /^(nextcloud-talk|nc-talk|nc):/i
	})
});
const nextcloudTalkSecurityAdapter = { resolveDmPolicy: createScopedDmSecurityResolver({
	channelKey: "nextcloud-talk",
	resolvePolicy: (account) => account.config.dmPolicy,
	resolveAllowFrom: (account) => account.config.allowFrom,
	policyPathSuffix: "dmPolicy",
	normalizeEntry: (raw) => normalizeLowercaseStringOrEmpty(raw.trim().replace(/^(nextcloud-talk|nc-talk|nc):/i, ""))
}) };
const nextcloudTalkPairingTextAdapter = {
	idLabel: "nextcloudUserId",
	message: "OpenClaw: your access has been approved.",
	normalizeAllowEntry: createPairingPrefixStripper(/^(nextcloud-talk|nc-talk|nc):/i, (entry) => normalizeLowercaseStringOrEmpty(entry))
};
//#endregion
//#region extensions/nextcloud-talk/src/config-schema.ts
const NextcloudTalkRoomSchema = object({
	requireMention: boolean().optional(),
	tools: ToolPolicySchema,
	skills: array(string()).optional(),
	enabled: boolean().optional(),
	allowFrom: array(string()).optional(),
	systemPrompt: string().optional()
}).strict();
const NextcloudTalkNetworkSchema = object({ 
/** Dangerous opt-in for self-hosted Nextcloud Talk on trusted private/internal hosts. */
dangerouslyAllowPrivateNetwork: boolean().optional() }).strict().optional();
const NextcloudTalkAccountSchemaBase = object({
	name: string().optional(),
	enabled: boolean().optional(),
	markdown: MarkdownConfigSchema,
	baseUrl: string().optional(),
	botSecret: buildSecretInputSchema().optional(),
	botSecretFile: string().optional(),
	apiUser: string().optional(),
	apiPassword: buildSecretInputSchema().optional(),
	apiPasswordFile: string().optional(),
	dmPolicy: DmPolicySchema.optional().default("pairing"),
	webhookPort: number().int().positive().optional(),
	webhookHost: string().optional(),
	webhookPath: string().optional(),
	webhookPublicUrl: string().optional(),
	allowFrom: array(string()).optional(),
	groupAllowFrom: array(string()).optional(),
	groupPolicy: GroupPolicySchema.optional().default("allowlist"),
	rooms: record(string(), NextcloudTalkRoomSchema.optional()).optional(),
	/** Network policy overrides for self-hosted Nextcloud Talk on trusted private/internal hosts. */
	network: NextcloudTalkNetworkSchema,
	...ReplyRuntimeConfigSchemaShape
}).strict();
const NextcloudTalkAccountSchema = NextcloudTalkAccountSchemaBase.superRefine((value, ctx) => {
	requireChannelOpenAllowFrom({
		channel: "nextcloud-talk",
		policy: value.dmPolicy,
		allowFrom: value.allowFrom,
		ctx,
		requireOpenAllowFrom
	});
});
const NextcloudTalkConfigSchema = NextcloudTalkAccountSchemaBase.extend({
	accounts: record(string(), NextcloudTalkAccountSchema.optional()).optional(),
	defaultAccount: string().optional()
}).superRefine((value, ctx) => {
	requireChannelOpenAllowFrom({
		channel: "nextcloud-talk",
		policy: value.dmPolicy,
		allowFrom: value.allowFrom,
		ctx,
		requireOpenAllowFrom
	});
});
//#endregion
//#region extensions/nextcloud-talk/src/replay-guard.ts
const NEXTCLOUD_TALK_PLUGIN_ID = "nextcloud-talk";
const NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX = "replay-dedupe";
const DEFAULT_REPLAY_TTL_MS = 1440 * 60 * 1e3;
const DEFAULT_MEMORY_MAX_SIZE = 1e3;
const DEFAULT_STATE_MAX_ENTRIES = 1e4;
function buildReplayKey(params) {
	const roomToken = params.roomToken.trim();
	const messageId = params.messageId.trim();
	if (!roomToken || !messageId) return null;
	return `${roomToken}:${messageId}`;
}
function createNextcloudTalkReplayGuard(options) {
	const stateDir = options.stateDir?.trim();
	const baseOptions = {
		ttlMs: options.ttlMs ?? DEFAULT_REPLAY_TTL_MS,
		memoryMaxSize: options.memoryMaxSize ?? DEFAULT_MEMORY_MAX_SIZE
	};
	const dedupe = createClaimableDedupe(stateDir ? {
		...baseOptions,
		pluginId: NEXTCLOUD_TALK_PLUGIN_ID,
		namespacePrefix: NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX,
		stateMaxEntries: options.stateMaxEntries ?? options.fileMaxEntries ?? DEFAULT_STATE_MAX_ENTRIES,
		env: {
			...process.env,
			OPENCLAW_STATE_DIR: stateDir
		},
		onDiskError: options.onDiskError
	} : baseOptions);
	return {
		claimMessage: async ({ accountId, roomToken, messageId }) => {
			const replayKey = buildReplayKey({
				roomToken,
				messageId
			});
			if (!replayKey) return "invalid";
			return (await dedupe.claim(replayKey, { namespace: accountId })).kind;
		},
		commitMessage: async ({ accountId, roomToken, messageId }) => {
			const replayKey = buildReplayKey({
				roomToken,
				messageId
			});
			if (!replayKey) return true;
			return await dedupe.commit(replayKey, { namespace: accountId });
		},
		releaseMessage: ({ accountId, roomToken, messageId, error }) => {
			const replayKey = buildReplayKey({
				roomToken,
				messageId
			});
			if (!replayKey) return;
			dedupe.release(replayKey, {
				namespace: accountId,
				error
			});
		},
		shouldProcessMessage: async ({ accountId, roomToken, messageId }) => {
			const replayKey = buildReplayKey({
				roomToken,
				messageId
			});
			if (!replayKey) return true;
			if ((await dedupe.claim(replayKey, { namespace: accountId })).kind !== "claimed") return false;
			return await dedupe.commit(replayKey, { namespace: accountId });
		}
	};
}
//#endregion
//#region extensions/nextcloud-talk/src/doctor.ts
const REPLAY_DEDUPE_TTL_MS = 1440 * 60 * 1e3;
const REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
function sanitizeLegacyReplaySegment(value) {
	const trimmed = value.trim();
	if (!trimmed) return "default";
	return trimmed.replace(/[^a-zA-Z0-9_-]/g, "_");
}
async function fileExists(filePath) {
	try {
		await fs$1.access(filePath);
		return true;
	} catch {
		return false;
	}
}
async function collectNextcloudTalkBotResponseWarnings(params) {
	const warnings = [];
	for (const accountId of listNextcloudTalkAccountIds(params.cfg)) {
		const account = resolveNextcloudTalkAccount({
			cfg: params.cfg,
			accountId
		});
		if (!account.enabled || !account.secret || !account.baseUrl) continue;
		const result = await probeNextcloudTalkBotResponseFeature({
			account,
			timeoutMs: 5e3
		});
		if (result.code === "missing_response_feature" || result.code === "bot_not_found" || result.code === "api_error" || result.code === "request_failed") warnings.push(`- channels.nextcloud-talk.${account.accountId}: ${result.message}`);
	}
	return warnings;
}
async function repairNextcloudTalkReplayDedupeState(params) {
	const changes = [];
	const warnings = [];
	const env = params.env ?? process.env;
	const stateDir = resolveStateDir(env, os.homedir);
	const replayDir = path.join(stateDir, "nextcloud-talk", "replay-dedupe");
	for (const accountId of listNextcloudTalkAccountIds(params.cfg)) {
		const legacyPath = path.join(replayDir, `${sanitizeLegacyReplaySegment(accountId)}.json`);
		if (!await fileExists(legacyPath)) continue;
		try {
			const result = await migratePersistentDedupeLegacyJsonFile({
				filePath: legacyPath,
				namespace: accountId,
				ttlMs: REPLAY_DEDUPE_TTL_MS,
				memoryMaxSize: 0,
				pluginId: NEXTCLOUD_TALK_PLUGIN_ID,
				namespacePrefix: NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX,
				stateMaxEntries: REPLAY_DEDUPE_MAX_ENTRIES,
				env
			});
			changes.push(`Migrated Nextcloud Talk replay dedupe cache for account "${accountId}" to SQLite (${result.imported} imported, ${result.skippedExpired} expired, ${result.skippedExisting} already current).`);
		} catch (error) {
			warnings.push(`Skipped Nextcloud Talk replay dedupe cache for account "${accountId}": ${String(error)}`);
		}
	}
	return {
		changes,
		warnings
	};
}
const nextcloudTalkDoctor = {
	legacyConfigRules,
	normalizeCompatibilityConfig,
	collectPreviewWarnings: async ({ cfg }) => await collectNextcloudTalkBotResponseWarnings({ cfg }),
	repairConfig: async ({ cfg, env }) => {
		const repair = await repairNextcloudTalkReplayDedupeState({
			cfg,
			...env ? { env } : {}
		});
		return {
			config: cfg,
			changes: repair.changes,
			warnings: repair.warnings
		};
	}
};
//#endregion
//#region extensions/nextcloud-talk/src/policy.ts
function normalizeNextcloudTalkAllowEntry(raw) {
	return raw.trim().replace(/^(nextcloud-talk|nc-talk|nc):/i, "").toLowerCase();
}
function normalizeNextcloudTalkAllowlist(values) {
	return (values ?? []).map((value) => normalizeNextcloudTalkAllowEntry(String(value))).filter(Boolean);
}
function resolveNextcloudTalkAllowlistMatch(params) {
	const allowFrom = normalizeNextcloudTalkAllowlist(params.allowFrom);
	if (allowFrom.length === 0) return { allowed: false };
	if (allowFrom.includes("*")) return {
		allowed: true,
		matchKey: "*",
		matchSource: "wildcard"
	};
	const senderId = normalizeNextcloudTalkAllowEntry(params.senderId);
	if (allowFrom.includes(senderId)) return {
		allowed: true,
		matchKey: senderId,
		matchSource: "id"
	};
	return { allowed: false };
}
function resolveNextcloudTalkRoomMatch(params) {
	const rooms = params.rooms ?? {};
	const allowlistConfigured = Object.keys(rooms).length > 0;
	const match = resolveChannelEntryMatchWithFallback({
		entries: rooms,
		keys: buildChannelKeyCandidates(params.roomToken),
		wildcardKey: "*",
		normalizeKey: normalizeChannelSlug
	});
	const roomConfig = match.entry;
	const allowed = resolveNestedAllowlistDecision({
		outerConfigured: allowlistConfigured,
		outerMatched: Boolean(roomConfig),
		innerConfigured: false,
		innerMatched: false
	});
	return {
		roomConfig,
		wildcardConfig: match.wildcardEntry,
		roomKey: match.matchKey ?? match.key,
		matchSource: match.matchSource,
		allowed,
		allowlistConfigured
	};
}
function resolveNextcloudTalkGroupToolPolicy(params) {
	const cfg = params.cfg;
	const roomToken = params.groupId?.trim();
	if (!roomToken) return;
	const match = resolveNextcloudTalkRoomMatch({
		rooms: cfg.channels?.["nextcloud-talk"]?.rooms,
		roomToken
	});
	return match.roomConfig?.tools ?? match.wildcardConfig?.tools;
}
function resolveNextcloudTalkRequireMention(params) {
	if (typeof params.roomConfig?.requireMention === "boolean") return params.roomConfig.requireMention;
	if (typeof params.wildcardConfig?.requireMention === "boolean") return params.wildcardConfig.requireMention;
	return true;
}
//#endregion
//#region extensions/nextcloud-talk/src/room-info.ts
const ROOM_CACHE_TTL_MS = 300 * 1e3;
const ROOM_CACHE_ERROR_TTL_MS = 30 * 1e3;
const roomCache = /* @__PURE__ */ new Map();
function resolveRoomCacheKey(params) {
	return `${params.accountId}:${params.roomToken}`;
}
function coerceRoomType(value) {
	if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) return value;
	return parseStrictPositiveInteger(value);
}
function resolveRoomKindFromType(type) {
	if (!type) return;
	if (type === 1 || type === 5 || type === 6) return "direct";
	return "group";
}
async function resolveNextcloudTalkRoomKind(params) {
	const { account, roomToken, runtime } = params;
	const key = resolveRoomCacheKey({
		accountId: account.accountId,
		roomToken
	});
	const cached = roomCache.get(key);
	if (cached) {
		const age = Date.now() - cached.fetchedAt;
		if (cached.kind && age < ROOM_CACHE_TTL_MS) return cached.kind;
		if (cached.error && age < ROOM_CACHE_ERROR_TTL_MS) return;
	}
	const apiCredentials = resolveNextcloudTalkApiCredentials({
		apiUser: account.config.apiUser,
		apiPassword: account.config.apiPassword,
		apiPasswordFile: account.config.apiPasswordFile
	});
	if (!apiCredentials) return;
	const baseUrl = account.baseUrl?.trim();
	if (!baseUrl) return;
	const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v4/room/${roomToken}`;
	const auth = Buffer.from(`${apiCredentials.apiUser}:${apiCredentials.apiPassword}`, "utf-8").toString("base64");
	try {
		const { response, release } = await fetchWithSsrFGuard({
			url,
			init: {
				method: "GET",
				headers: {
					Authorization: `Basic ${auth}`,
					"OCS-APIRequest": "true",
					Accept: "application/json"
				}
			},
			auditContext: "nextcloud-talk.room-info",
			policy: ssrfPolicyFromPrivateNetworkOptIn(account.config)
		});
		try {
			if (!response.ok) {
				roomCache.set(key, {
					fetchedAt: Date.now(),
					error: `status:${response.status}`
				});
				runtime?.log?.(`nextcloud-talk: room lookup failed (${response.status}) token=${roomToken}`);
				return;
			}
			const kind = resolveRoomKindFromType(coerceRoomType((await readProviderJsonResponse(response, "Nextcloud Talk room info failed")).ocs?.data?.type));
			roomCache.set(key, {
				fetchedAt: Date.now(),
				kind
			});
			return kind;
		} finally {
			await release();
		}
	} catch (err) {
		roomCache.set(key, {
			fetchedAt: Date.now(),
			error: formatErrorMessage(err)
		});
		runtime?.error?.(`nextcloud-talk: room lookup error: ${String(err)}`);
		return;
	}
}
//#endregion
//#region extensions/nextcloud-talk/src/normalize.ts
function stripNextcloudTalkTargetPrefix(raw) {
	const trimmed = raw.trim();
	if (!trimmed) return;
	let normalized = trimmed;
	if (normalized.startsWith("nextcloud-talk:")) normalized = normalized.slice(15).trim();
	else if (normalized.startsWith("nc-talk:")) normalized = normalized.slice(8).trim();
	else if (normalized.startsWith("nc:")) normalized = normalized.slice(3).trim();
	if (normalized.startsWith("room:")) normalized = normalized.slice(5).trim();
	if (!normalized) return;
	return normalized;
}
function normalizeNextcloudTalkMessagingTarget(raw) {
	const normalized = stripNextcloudTalkTargetPrefix(raw);
	return normalized ? `nextcloud-talk:${normalized}`.toLowerCase() : void 0;
}
function looksLikeNextcloudTalkTargetId(raw) {
	const trimmed = raw.trim();
	if (!trimmed) return false;
	if (/^(nextcloud-talk|nc-talk|nc):/i.test(trimmed)) return true;
	return /^[a-z0-9]{8,}$/i.test(trimmed);
}
//#endregion
//#region extensions/nextcloud-talk/src/send.ts
function resolveCredentials(explicit, account) {
	const baseUrl = explicit.baseUrl?.trim() ?? account.baseUrl;
	const secret = explicit.secret?.trim() ?? account.secret;
	if (!baseUrl) throw new Error(`Nextcloud Talk baseUrl missing for account "${account.accountId}" (set channels.nextcloud-talk.baseUrl).`);
	if (!secret) throw new Error(`Nextcloud Talk bot secret missing for account "${account.accountId}" (set channels.nextcloud-talk.botSecret/botSecretFile or NEXTCLOUD_TALK_BOT_SECRET for default).`);
	return {
		baseUrl,
		secret
	};
}
function normalizeRoomToken(to) {
	const normalized = stripNextcloudTalkTargetPrefix(to);
	if (!normalized) throw new Error("Room token is required for Nextcloud Talk sends");
	return normalized;
}
function resolveNextcloudTalkSendContext(opts) {
	const cfg = requireRuntimeConfig(opts.cfg, "Nextcloud Talk send");
	const account = resolveNextcloudTalkAccount({
		cfg,
		accountId: opts.accountId
	});
	const { baseUrl, secret } = resolveCredentials({
		baseUrl: opts.baseUrl,
		secret: opts.secret
	}, account);
	return {
		cfg,
		account,
		baseUrl,
		secret
	};
}
function recordNextcloudTalkOutboundActivity(accountId) {
	try {
		getNextcloudTalkRuntime().channel.activity.record({
			channel: "nextcloud-talk",
			accountId,
			direction: "outbound"
		});
	} catch (error) {
		if (!(error instanceof Error) || error.message !== "Nextcloud Talk runtime not initialized") throw error;
	}
}
function createNextcloudTalkSendReceipt(params) {
	const messageId = params.messageId.trim();
	return createMessageReceiptFromOutboundResults({
		results: messageId && messageId !== "unknown" ? [{
			channel: "nextcloud-talk",
			messageId,
			conversationId: params.roomToken
		}] : [],
		kind: "text",
		...params.replyTo ? { replyToId: params.replyTo } : {}
	});
}
async function sendMessageNextcloudTalk(to, text, opts) {
	const { cfg, account, baseUrl, secret } = resolveNextcloudTalkSendContext(opts);
	const roomToken = normalizeRoomToken(to);
	if (!text?.trim()) throw new Error("Message must be non-empty for Nextcloud Talk sends");
	const tableMode = resolveMarkdownTableMode({
		cfg,
		channel: "nextcloud-talk",
		accountId: account.accountId
	});
	const message = convertMarkdownTables(text.trim(), tableMode);
	const body = { message };
	if (opts.replyTo) body.replyTo = opts.replyTo;
	const bodyStr = JSON.stringify(body);
	const { random, signature } = generateNextcloudTalkSignature({
		body: message,
		secret
	});
	const { response, release } = await fetchWithSsrFGuard({
		url: `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${roomToken}/message`,
		init: {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
				"OCS-APIRequest": "true",
				"X-Nextcloud-Talk-Bot-Random": random,
				"X-Nextcloud-Talk-Bot-Signature": signature
			},
			body: bodyStr
		},
		auditContext: "nextcloud-talk-send",
		policy: ssrfPolicyFromPrivateNetworkOptIn(account.config)
	});
	try {
		if (!response.ok) {
			const errorBody = await response.text().catch(() => "");
			const status = response.status;
			let errorMsg = `Nextcloud Talk send failed (${status})`;
			if (status === 400) errorMsg = `Nextcloud Talk: bad request - ${errorBody || "invalid message format"}`;
			else if (status === 401) errorMsg = "Nextcloud Talk: bot send was rejected - check the bot secret and ensure the bot was installed with --feature response";
			else if (status === 403) errorMsg = "Nextcloud Talk: forbidden - bot may not have permission in this room";
			else if (status === 404) errorMsg = `Nextcloud Talk: room not found (token=${roomToken})`;
			else if (errorBody) errorMsg = `Nextcloud Talk send failed: ${errorBody}`;
			throw new Error(errorMsg);
		}
		let messageId = "unknown";
		let timestamp;
		try {
			const data = await response.json();
			if (data.ocs?.data?.id != null) messageId = String(data.ocs.data.id);
			if (typeof data.ocs?.data?.timestamp === "number") timestamp = data.ocs.data.timestamp;
		} catch {}
		if (opts.verbose) console.log(`[nextcloud-talk] Sent message ${messageId} to room ${roomToken}`);
		recordNextcloudTalkOutboundActivity(account.accountId);
		return {
			messageId,
			roomToken,
			receipt: createNextcloudTalkSendReceipt({
				messageId,
				roomToken,
				...opts.replyTo ? { replyTo: opts.replyTo } : {}
			}),
			timestamp
		};
	} finally {
		await release();
	}
}
async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
	const { account, baseUrl, secret } = resolveNextcloudTalkSendContext(opts);
	const normalizedToken = normalizeRoomToken(roomToken);
	const body = JSON.stringify({ reaction });
	const { random, signature } = generateNextcloudTalkSignature({
		body: reaction,
		secret
	});
	const { response, release } = await fetchWithSsrFGuard({
		url: `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${normalizedToken}/reaction/${messageId}`,
		init: {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
				"OCS-APIRequest": "true",
				"X-Nextcloud-Talk-Bot-Random": random,
				"X-Nextcloud-Talk-Bot-Signature": signature
			},
			body
		},
		auditContext: "nextcloud-talk-reaction",
		policy: ssrfPolicyFromPrivateNetworkOptIn(account.config)
	});
	try {
		if (!response.ok) {
			const errorBody = await response.text().catch(() => "");
			throw new Error(`Nextcloud Talk reaction failed: ${response.status} ${errorBody}`.trim());
		}
		return { ok: true };
	} finally {
		await release();
	}
}
//#endregion
//#region extensions/nextcloud-talk/src/inbound.ts
const CHANNEL_ID = "nextcloud-talk";
function hasAllowEntries(entries) {
	return normalizeNextcloudTalkAllowlist(entries).length > 0;
}
function roomRoutes(params) {
	if (!params.isGroup) return [];
	const roomSenderConfigured = params.groupPolicy === "allowlist" && hasAllowEntries(params.roomAllowFrom);
	return channelIngressRoutes(params.roomMatch.allowlistConfigured && {
		id: "nextcloud-talk:room",
		allowed: params.roomMatch.allowed,
		precedence: 0,
		matchId: "nextcloud-talk-room",
		blockReason: "room_not_allowlisted"
	}, params.roomConfig?.enabled === false && {
		id: "nextcloud-talk:room-enabled",
		enabled: false,
		precedence: 10,
		blockReason: "room_disabled"
	}, roomSenderConfigured && {
		id: "nextcloud-talk:room-sender",
		kind: "nestedAllowlist",
		precedence: 20,
		blockReason: "room_sender_not_allowlisted",
		...!hasAllowEntries(params.outerGroupAllowFrom) ? {
			senderPolicy: "replace",
			senderAllowFrom: params.roomAllowFrom
		} : {
			allowed: resolveNextcloudTalkAllowlistMatch({
				allowFrom: params.roomAllowFrom,
				senderId: params.senderId
			}).allowed,
			matchId: "nextcloud-talk-room-sender"
		}
	});
}
async function deliverNextcloudTalkReply(params) {
	const { cfg, payload, roomToken, accountId, statusSink } = params;
	await deliverFormattedTextWithAttachments({
		payload,
		send: async ({ text, replyToId }) => {
			await sendMessageNextcloudTalk(roomToken, text, {
				cfg,
				accountId,
				replyTo: replyToId
			});
			statusSink?.({ lastOutboundAt: Date.now() });
		}
	});
}
async function handleNextcloudTalkInbound(params) {
	const { message, account, config, runtime, statusSink } = params;
	const core = getNextcloudTalkRuntime();
	const pairing = createChannelPairingController({
		core,
		channel: CHANNEL_ID,
		accountId: account.accountId
	});
	const rawBody = message.text?.trim() ?? "";
	if (!rawBody) return;
	const roomKind = await resolveNextcloudTalkRoomKind({
		account,
		roomToken: message.roomToken,
		runtime
	});
	const isGroup = roomKind === "direct" ? false : roomKind === "group" ? true : message.isGroupChat;
	const senderId = message.senderId;
	const senderName = message.senderName;
	const roomToken = message.roomToken;
	const roomName = message.roomName;
	statusSink?.({ lastInboundAt: message.timestamp });
	const roomMatch = resolveNextcloudTalkRoomMatch({
		rooms: account.config.rooms,
		roomToken
	});
	const roomConfig = roomMatch.roomConfig;
	const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
		cfg: config,
		surface: CHANNEL_ID
	});
	const hasControlCommand = core.channel.text.hasControlCommand(rawBody, config);
	const shouldRequireMention = isGroup ? resolveNextcloudTalkRequireMention({
		roomConfig,
		wildcardConfig: roomMatch.wildcardConfig
	}) : false;
	const { groupPolicy, providerMissingFallbackApplied } = resolveAllowlistProviderRuntimeGroupPolicy({
		providerConfigPresent: (config.channels?.[CHANNEL_ID] ?? void 0) !== void 0,
		groupPolicy: account.config.groupPolicy,
		defaultGroupPolicy: resolveDefaultGroupPolicy(config)
	});
	const allowFrom = normalizeStringEntries(account.config.allowFrom);
	const outerGroupAllowFrom = account.config.groupAllowFrom?.length ? normalizeStringEntries(account.config.groupAllowFrom) : allowFrom;
	const roomAllowFrom = normalizeStringEntries(roomConfig?.allowFrom);
	const resolveAccess = async (wasMentioned) => await resolveStableChannelMessageIngress({
		channelId: CHANNEL_ID,
		accountId: account.accountId,
		identity: {
			key: "nextcloud-talk-user-id",
			normalize: (value) => normalizeNextcloudTalkAllowEntry(value) || null,
			sensitivity: "pii",
			entryIdPrefix: "nextcloud-talk-entry"
		},
		cfg: config,
		readStoreAllowFrom: async () => await pairing.readStoreForDmPolicy(CHANNEL_ID, account.accountId),
		subject: { stableId: senderId },
		conversation: {
			kind: isGroup ? "group" : "direct",
			id: isGroup ? roomToken : senderId
		},
		route: roomRoutes({
			isGroup,
			groupPolicy,
			roomMatch,
			roomConfig,
			senderId,
			outerGroupAllowFrom,
			roomAllowFrom
		}),
		dmPolicy: account.config.dmPolicy ?? "pairing",
		groupPolicy,
		policy: {
			groupAllowFromFallbackToAllowFrom: true,
			activation: {
				requireMention: isGroup && shouldRequireMention,
				allowTextCommands
			}
		},
		mentionFacts: isGroup && wasMentioned !== void 0 ? {
			canDetectMention: true,
			wasMentioned,
			hasAnyMention: wasMentioned
		} : void 0,
		allowFrom,
		groupAllowFrom: account.config.groupAllowFrom,
		command: {
			allowTextCommands,
			hasControlCommand
		}
	});
	let access = await resolveAccess();
	warnMissingProviderGroupPolicyFallbackOnce({
		providerMissingFallbackApplied,
		providerKey: "nextcloud-talk",
		accountId: account.accountId,
		blockedLabel: GROUP_POLICY_BLOCKED_LABEL.room,
		log: (messageValue) => runtime.log?.(messageValue)
	});
	const commandAuthorized = access.commandAccess.authorized;
	const accessReason = access.ingress.reasonCode === "route_blocked" ? "route blocked" : access.senderAccess.reasonCode;
	if (isGroup) {
		if (access.routeAccess.reason === "room_not_allowlisted") {
			runtime.log?.(`nextcloud-talk: drop room ${roomToken} (not allowlisted)`);
			return;
		}
		if (access.routeAccess.reason === "room_disabled") {
			runtime.log?.(`nextcloud-talk: drop room ${roomToken} (disabled)`);
			return;
		}
		if (access.routeAccess.reason === "room_sender_not_allowlisted") {
			runtime.log?.(`nextcloud-talk: drop group sender ${senderId} (policy=${groupPolicy})`);
			return;
		}
		if (access.senderAccess.decision !== "allow") {
			runtime.log?.(`nextcloud-talk: drop group sender ${senderId} (reason=${accessReason})`);
			return;
		}
	} else if (access.senderAccess.decision !== "allow") {
		if (access.senderAccess.decision === "pairing") await pairing.issueChallenge({
			senderId,
			senderIdLine: `Your Nextcloud user id: ${senderId}`,
			meta: { name: senderName || void 0 },
			sendPairingReply: async (text) => {
				await sendMessageNextcloudTalk(roomToken, text, {
					cfg: config,
					accountId: account.accountId
				});
				statusSink?.({ lastOutboundAt: Date.now() });
			},
			onReplyError: (err) => {
				runtime.error?.(`nextcloud-talk: pairing reply failed for ${senderId}: ${String(err)}`);
			}
		});
		runtime.log?.(`nextcloud-talk: drop DM sender ${senderId} (reason=${accessReason})`);
		return;
	}
	if (access.commandAccess.shouldBlockControlCommand) {
		logInboundDrop({
			log: (messageLocal) => runtime.log?.(messageLocal),
			channel: CHANNEL_ID,
			reason: "control command (unauthorized)",
			target: senderId
		});
		return;
	}
	const mentionRegexes = core.channel.mentions.buildMentionRegexes(config);
	const wasMentioned = mentionRegexes.length ? core.channel.mentions.matchesMentionPatterns(rawBody, mentionRegexes) : false;
	if (isGroup) access = await resolveAccess(wasMentioned);
	if (isGroup && access.activationAccess.shouldSkip) {
		runtime.log?.(`nextcloud-talk: drop room ${roomToken} (no mention)`);
		return;
	}
	const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
		cfg: config,
		channel: CHANNEL_ID,
		accountId: account.accountId,
		peer: {
			kind: isGroup ? "group" : "direct",
			id: isGroup ? roomToken : senderId
		},
		runtime: core.channel,
		sessionStore: config.session?.store
	});
	const fromLabel = isGroup ? `room:${roomName || roomToken}` : senderName || `user:${senderId}`;
	const { storePath, body } = buildEnvelope({
		channel: "Nextcloud Talk",
		from: fromLabel,
		timestamp: message.timestamp,
		body: rawBody
	});
	const groupSystemPrompt = normalizeOptionalString(roomConfig?.systemPrompt);
	const ctxPayload = core.channel.reply.finalizeInboundContext({
		Body: body,
		BodyForAgent: rawBody,
		RawBody: rawBody,
		CommandBody: rawBody,
		From: isGroup ? `nextcloud-talk:room:${roomToken}` : `nextcloud-talk:${senderId}`,
		To: `nextcloud-talk:${roomToken}`,
		SessionKey: route.sessionKey,
		AccountId: route.accountId,
		ChatType: isGroup ? "group" : "direct",
		ConversationLabel: fromLabel,
		SenderName: senderName || void 0,
		SenderId: senderId,
		GroupSubject: isGroup ? roomName || roomToken : void 0,
		GroupSystemPrompt: isGroup ? groupSystemPrompt : void 0,
		Provider: CHANNEL_ID,
		Surface: CHANNEL_ID,
		WasMentioned: isGroup ? wasMentioned : void 0,
		MessageSid: message.messageId,
		Timestamp: message.timestamp,
		OriginatingChannel: CHANNEL_ID,
		OriginatingTo: `nextcloud-talk:${roomToken}`,
		CommandAuthorized: commandAuthorized
	});
	await core.channel.inbound.dispatchReply({
		cfg: config,
		channel: CHANNEL_ID,
		accountId: account.accountId,
		agentId: route.agentId,
		routeSessionKey: route.sessionKey,
		storePath,
		ctxPayload,
		recordInboundSession: core.channel.session.recordInboundSession,
		dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
		delivery: {
			deliver: async (payload) => {
				await deliverNextcloudTalkReply({
					cfg: config,
					payload,
					roomToken,
					accountId: account.accountId,
					statusSink
				});
			},
			onError: (err, info) => {
				runtime.error?.(`nextcloud-talk ${info.kind} reply failed: ${String(err)}`);
			}
		},
		replyPipeline: {},
		replyOptions: {
			skillFilter: roomConfig?.skills,
			disableBlockStreaming: typeof account.config.blockStreaming === "boolean" ? !account.config.blockStreaming : void 0
		},
		record: { onRecordError: (err) => {
			runtime.error?.(`nextcloud-talk: failed updating session meta: ${String(err)}`);
		} }
	});
}
//#endregion
//#region extensions/nextcloud-talk/src/monitor.ts
const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 64 * 1024;
const PREAUTH_WEBHOOK_BODY_TIMEOUT_MS = 5e3;
const HEALTH_PATH = "/healthz";
const WEBHOOK_AUTH_RATE_LIMIT_SCOPE = "nextcloud-talk-webhook-auth";
const NextcloudTalkWebhookPayloadSchema = object({
	type: _enum([
		"Create",
		"Update",
		"Delete"
	]),
	actor: object({
		type: literal("Person"),
		id: string().min(1),
		name: string()
	}),
	object: object({
		type: literal("Note"),
		id: string().min(1),
		name: string(),
		content: string(),
		mediaType: string()
	}),
	target: object({
		type: literal("Collection"),
		id: string().min(1),
		name: string()
	})
});
const WEBHOOK_ERRORS = {
	missingSignatureHeaders: "Missing signature headers",
	invalidBackend: "Invalid backend",
	invalidSignature: "Invalid signature",
	invalidPayloadFormat: "Invalid payload format",
	payloadTooLarge: "Payload too large",
	internalServerError: "Internal server error"
};
var NextcloudTalkRetryableWebhookError = class extends Error {
	constructor(message, options) {
		super(message, options);
		this.name = "NextcloudTalkRetryableWebhookError";
	}
};
async function processNextcloudTalkReplayGuardedMessage(params) {
	if (await params.replayGuard.claimMessage({
		accountId: params.accountId,
		roomToken: params.message.roomToken,
		messageId: params.message.messageId
	}) !== "claimed") return "duplicate";
	try {
		await params.handleMessage();
		await params.replayGuard.commitMessage({
			accountId: params.accountId,
			roomToken: params.message.roomToken,
			messageId: params.message.messageId
		});
		return "processed";
	} catch (error) {
		if (error instanceof NextcloudTalkRetryableWebhookError) params.replayGuard.releaseMessage({
			accountId: params.accountId,
			roomToken: params.message.roomToken,
			messageId: params.message.messageId,
			error
		});
		else await params.replayGuard.commitMessage({
			accountId: params.accountId,
			roomToken: params.message.roomToken,
			messageId: params.message.messageId
		});
		throw error;
	}
}
function formatError(err) {
	if (err instanceof Error) return err.message;
	return typeof err === "string" ? err : JSON.stringify(err);
}
function parseWebhookPayload(body) {
	return safeParseJsonWithSchema(NextcloudTalkWebhookPayloadSchema, body);
}
function writeJsonResponse(res, status, body) {
	if (body) {
		res.writeHead(status, { "Content-Type": "application/json" });
		res.end(JSON.stringify(body));
		return;
	}
	res.writeHead(status);
	res.end();
}
function writeWebhookError(res, status, error) {
	if (res.headersSent) return;
	writeJsonResponse(res, status, { error });
}
function validateWebhookHeaders(params) {
	const headers = extractNextcloudTalkHeaders(params.req.headers);
	if (!headers) {
		writeWebhookError(params.res, 400, WEBHOOK_ERRORS.missingSignatureHeaders);
		return null;
	}
	if (params.isBackendAllowed && !params.isBackendAllowed(headers.backend)) {
		writeWebhookError(params.res, 401, WEBHOOK_ERRORS.invalidBackend);
		return null;
	}
	return headers;
}
function verifyWebhookSignature(params) {
	if (!verifyNextcloudTalkSignature({
		signature: params.headers.signature,
		random: params.headers.random,
		body: params.body,
		secret: params.secret
	})) {
		params.authRateLimiter.recordFailure(params.clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE);
		writeWebhookError(params.res, 401, WEBHOOK_ERRORS.invalidSignature);
		return false;
	}
	params.authRateLimiter.reset(params.clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE);
	return true;
}
function decodeWebhookCreateMessage(params) {
	const payload = parseWebhookPayload(params.body);
	if (!payload) {
		writeWebhookError(params.res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
		return { kind: "invalid" };
	}
	if (payload.type !== "Create") return { kind: "ignore" };
	return {
		kind: "message",
		message: payloadToInboundMessage(payload)
	};
}
function payloadToInboundMessage(payload) {
	return {
		messageId: payload.object.id,
		roomToken: payload.target.id,
		roomName: payload.target.name,
		senderId: payload.actor.id,
		senderName: payload.actor.name ?? "",
		text: payload.object.content || payload.object.name || "",
		mediaType: payload.object.mediaType || "text/plain",
		timestamp: Date.now(),
		isGroupChat: true
	};
}
function readNextcloudTalkWebhookBody(req, maxBodyBytes) {
	return readRequestBodyWithLimit(req, {
		maxBytes: Math.min(maxBodyBytes, PREAUTH_WEBHOOK_MAX_BODY_BYTES),
		timeoutMs: PREAUTH_WEBHOOK_BODY_TIMEOUT_MS
	});
}
function createNextcloudTalkWebhookServer(opts) {
	const { port, host, path, secret, onMessage, onError, abortSignal } = opts;
	const maxBodyBytes = typeof opts.maxBodyBytes === "number" && Number.isFinite(opts.maxBodyBytes) && opts.maxBodyBytes > 0 ? Math.floor(opts.maxBodyBytes) : DEFAULT_WEBHOOK_MAX_BODY_BYTES;
	const readBody = opts.readBody ?? readNextcloudTalkWebhookBody;
	const isBackendAllowed = opts.isBackendAllowed;
	const shouldProcessMessage = opts.shouldProcessMessage;
	const processMessage = opts.processMessage;
	const authRateLimitMaxRequests = typeof opts.authRateLimit?.maxRequests === "number" ? opts.authRateLimit.maxRequests : WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests;
	const authRateLimitWindowMs = typeof opts.authRateLimit?.windowMs === "number" ? opts.authRateLimit.windowMs : WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs;
	const webhookAuthRateLimiter = createAuthRateLimiter({
		maxAttempts: authRateLimitMaxRequests,
		windowMs: authRateLimitWindowMs,
		lockoutMs: authRateLimitWindowMs,
		exemptLoopback: false,
		pruneIntervalMs: authRateLimitWindowMs
	});
	const server = createServer((req, res) => {
		(async () => {
			if (req.url === HEALTH_PATH) {
				res.writeHead(200, { "Content-Type": "text/plain" });
				res.end("ok");
				return;
			}
			if (req.url !== path || req.method !== "POST") {
				res.writeHead(404);
				res.end();
				return;
			}
			const clientIp = req.socket.remoteAddress ?? "unknown";
			if (!webhookAuthRateLimiter.check(clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE).allowed) {
				res.writeHead(429);
				res.end("Too Many Requests");
				return;
			}
			try {
				const headers = validateWebhookHeaders({
					req,
					res,
					isBackendAllowed
				});
				if (!headers) return;
				const body = await readBody(req, maxBodyBytes);
				if (!verifyWebhookSignature({
					headers,
					body,
					secret,
					res,
					clientIp,
					authRateLimiter: webhookAuthRateLimiter
				})) return;
				const decoded = decodeWebhookCreateMessage({
					body,
					res
				});
				if (decoded.kind === "invalid") return;
				if (decoded.kind === "ignore") {
					writeJsonResponse(res, 200);
					return;
				}
				const message = decoded.message;
				if (processMessage) {
					writeJsonResponse(res, 200);
					try {
						await processMessage(message);
					} catch (err) {
						onError?.(err instanceof Error ? err : new Error(formatError(err)));
					}
					return;
				}
				if (shouldProcessMessage) {
					if (!await shouldProcessMessage(message)) {
						writeJsonResponse(res, 200);
						return;
					}
				}
				writeJsonResponse(res, 200);
				try {
					await onMessage(message);
				} catch (err) {
					onError?.(err instanceof Error ? err : new Error(formatError(err)));
				}
			} catch (err) {
				if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
					writeWebhookError(res, 413, WEBHOOK_ERRORS.payloadTooLarge);
					return;
				}
				if (isRequestBodyLimitError(err, "REQUEST_BODY_TIMEOUT")) {
					writeWebhookError(res, 408, requestBodyErrorToText("REQUEST_BODY_TIMEOUT"));
					return;
				}
				const error = err instanceof Error ? err : new Error(formatError(err));
				onError?.(error);
				writeWebhookError(res, 500, WEBHOOK_ERRORS.internalServerError);
			}
		})();
	});
	const start = () => {
		return new Promise((resolve) => {
			server.listen(port, host, () => resolve());
		});
	};
	let stopped = false;
	const stop = () => {
		if (stopped) return;
		stopped = true;
		try {
			server.close();
		} catch {}
	};
	if (abortSignal) if (abortSignal.aborted) stop();
	else abortSignal.addEventListener("abort", stop, { once: true });
	return {
		server,
		start,
		stop
	};
}
//#endregion
//#region extensions/nextcloud-talk/src/monitor-runtime.ts
const DEFAULT_WEBHOOK_PORT = 8788;
const DEFAULT_WEBHOOK_HOST = "0.0.0.0";
const DEFAULT_WEBHOOK_PATH = "/nextcloud-talk-webhook";
function normalizeOrigin(value) {
	try {
		return normalizeLowercaseStringOrEmpty(new URL(value).origin);
	} catch {
		return null;
	}
}
async function monitorNextcloudTalkProvider(opts) {
	const core = getNextcloudTalkRuntime();
	const cfg = opts.config ?? core.config.current();
	const account = resolveNextcloudTalkAccount({
		cfg,
		accountId: opts.accountId
	});
	const runtime = resolveLoggerBackedRuntime(opts.runtime, core.logging.getChildLogger());
	if (!account.secret) throw new Error(`Nextcloud Talk bot secret not configured for account "${account.accountId}"`);
	const port = account.config.webhookPort ?? DEFAULT_WEBHOOK_PORT;
	const host = account.config.webhookHost ?? DEFAULT_WEBHOOK_HOST;
	const path = account.config.webhookPath ?? DEFAULT_WEBHOOK_PATH;
	const logger = core.logging.getChildLogger({
		channel: "nextcloud-talk",
		accountId: account.accountId
	});
	const expectedBackendOrigin = normalizeOrigin(account.baseUrl);
	const replayGuard = createNextcloudTalkReplayGuard({
		stateDir: core.state.resolveStateDir(process.env, os.homedir),
		onDiskError: (error) => {
			logger.warn(`[nextcloud-talk:${account.accountId}] replay guard disk error: ${String(error)}`);
		}
	});
	const { start, stop } = createNextcloudTalkWebhookServer({
		port,
		host,
		path,
		secret: account.secret,
		isBackendAllowed: (backend) => {
			if (!expectedBackendOrigin) return true;
			return normalizeOrigin(backend) === expectedBackendOrigin;
		},
		processMessage: async (message) => {
			if (await processNextcloudTalkReplayGuardedMessage({
				replayGuard,
				accountId: account.accountId,
				message,
				handleMessage: async () => {
					core.channel.activity.record({
						channel: "nextcloud-talk",
						accountId: account.accountId,
						direction: "inbound",
						at: message.timestamp
					});
					if (opts.onMessage) await opts.onMessage(message);
					else await handleNextcloudTalkInbound({
						message,
						account,
						config: cfg,
						runtime,
						statusSink: opts.statusSink
					});
				}
			}) === "duplicate") logger.warn(`[nextcloud-talk:${account.accountId}] replayed webhook ignored room=${message.roomToken} messageId=${message.messageId}`);
		},
		onMessage: async () => {},
		onError: (error) => {
			logger.error(`[nextcloud-talk:${account.accountId}] webhook error: ${error.message}`);
		},
		abortSignal: opts.abortSignal
	});
	if (opts.abortSignal?.aborted) return { stop };
	await start();
	if (opts.abortSignal?.aborted) {
		stop();
		return { stop };
	}
	const publicUrl = account.config.webhookPublicUrl ?? `http://${host === "0.0.0.0" ? "localhost" : host}:${port}${path}`;
	logger.info(`[nextcloud-talk:${account.accountId}] webhook listening on ${publicUrl}`);
	return { stop };
}
//#endregion
//#region extensions/nextcloud-talk/src/gateway.ts
const nextcloudTalkGatewayAdapter = {
	startAccount: async (ctx) => {
		const account = ctx.account;
		if (!account.secret || !account.baseUrl) throw new Error(`Nextcloud Talk not configured for account "${account.accountId}" (missing secret or baseUrl)`);
		ctx.log?.info(`[${account.accountId}] starting Nextcloud Talk webhook server`);
		const statusSink = createAccountStatusSink({
			accountId: ctx.accountId,
			setStatus: ctx.setStatus
		});
		await runStoppablePassiveMonitor({
			abortSignal: ctx.abortSignal,
			start: async () => await monitorNextcloudTalkProvider({
				accountId: account.accountId,
				config: ctx.cfg,
				runtime: ctx.runtime,
				abortSignal: ctx.abortSignal,
				statusSink
			})
		});
	},
	logoutAccount: async ({ accountId, cfg }) => {
		const nextCfg = { ...cfg };
		const nextSection = cfg.channels?.["nextcloud-talk"] ? { ...cfg.channels["nextcloud-talk"] } : void 0;
		let cleared = false;
		let changed = false;
		if (nextSection) {
			if (accountId === "default" && nextSection.botSecret) {
				delete nextSection.botSecret;
				cleared = true;
				changed = true;
			}
			const accountCleanup = clearAccountEntryFields({
				accounts: nextSection.accounts,
				accountId,
				fields: ["botSecret"]
			});
			if (accountCleanup.changed) {
				changed = true;
				if (accountCleanup.cleared) cleared = true;
				if (accountCleanup.nextAccounts) nextSection.accounts = accountCleanup.nextAccounts;
				else delete nextSection.accounts;
			}
		}
		if (changed) if (nextSection && Object.keys(nextSection).length > 0) nextCfg.channels = {
			...nextCfg.channels,
			"nextcloud-talk": nextSection
		};
		else {
			const nextChannels = { ...nextCfg.channels };
			delete nextChannels["nextcloud-talk"];
			if (Object.keys(nextChannels).length > 0) nextCfg.channels = nextChannels;
			else delete nextCfg.channels;
		}
		const loggedOut = resolveNextcloudTalkAccount({
			cfg: changed ? nextCfg : cfg,
			accountId
		}).secretSource === "none";
		if (changed) await getNextcloudTalkRuntime().config.replaceConfigFile({
			nextConfig: nextCfg,
			afterWrite: { mode: "auto" }
		});
		return {
			cleared,
			envSecret: Boolean(process.env.NEXTCLOUD_TALK_BOT_SECRET?.trim()),
			loggedOut
		};
	}
};
//#endregion
//#region extensions/nextcloud-talk/src/message-actions.ts
const providerId = "nextcloud-talk";
function isAccountConfigured(account) {
	return Boolean(account.enabled && account.secret?.trim() && account.baseUrl?.trim());
}
function hasConfiguredAccount(cfg, accountId) {
	if (accountId) return isAccountConfigured(resolveNextcloudTalkAccount({
		cfg,
		accountId
	}));
	return listNextcloudTalkAccountIds(cfg).map((id) => resolveNextcloudTalkAccount({
		cfg,
		accountId: id
	})).some(isAccountConfigured);
}
const nextcloudTalkMessageActions = {
	describeMessageTool: ({ cfg, accountId }) => {
		if (!hasConfiguredAccount(cfg, accountId)) return null;
		return { actions: ["send", "react"] };
	},
	supportsAction: ({ action }) => action !== "send",
	handleAction: async ({ action, params, cfg, accountId, toolContext }) => {
		if (action === "send") throw new Error("Send should be handled by outbound, not actions handler.");
		if (action === "react") {
			const target = readStringParam(params, "to", {
				required: true,
				label: "to (room token)"
			});
			const messageIdRaw = resolveReactionMessageId({
				args: params,
				toolContext
			});
			if (messageIdRaw == null) throw new Error("messageId required");
			const messageId = String(messageIdRaw);
			const emoji = readStringParam(params, "emoji", { required: true });
			if (params.remove === true) throw new Error("Nextcloud Talk reaction removal is not supported yet; only adding reactions is implemented.");
			await sendReactionNextcloudTalk(target, messageId, emoji, {
				accountId: accountId ?? void 0,
				cfg
			});
			return jsonResult({
				ok: true,
				added: emoji
			});
		}
		throw new Error(`Action ${action} not supported for ${providerId}.`);
	}
};
//#endregion
//#region extensions/nextcloud-talk/src/message-adapter.ts
const nextcloudTalkMessageAdapter = defineChannelMessageAdapter({
	id: "nextcloud-talk",
	durableFinal: { capabilities: {
		text: true,
		media: true,
		replyTo: true
	} },
	send: {
		text: async ({ cfg, to, text, accountId, replyToId }) => await sendMessageNextcloudTalk(to, text, {
			accountId: accountId ?? void 0,
			replyTo: replyToId ?? void 0,
			cfg
		}),
		media: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => await sendMessageNextcloudTalk(to, mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text, {
			accountId: accountId ?? void 0,
			replyTo: replyToId ?? void 0,
			cfg
		})
	}
});
//#endregion
//#region extensions/nextcloud-talk/src/session-route.ts
function resolveNextcloudTalkOutboundSessionRoute(params) {
	const roomId = stripNextcloudTalkTargetPrefix(params.target);
	if (!roomId) return null;
	const baseSessionKey = buildOutboundBaseSessionKey({
		cfg: params.cfg,
		agentId: params.agentId,
		channel: "nextcloud-talk",
		accountId: params.accountId,
		peer: {
			kind: "group",
			id: roomId
		}
	});
	return {
		sessionKey: baseSessionKey,
		baseSessionKey,
		peer: {
			kind: "group",
			id: roomId
		},
		chatType: "group",
		from: `nextcloud-talk:room:${roomId}`,
		to: `nextcloud-talk:${roomId}`
	};
}
//#endregion
//#region extensions/nextcloud-talk/src/setup-core.ts
const t$1 = createSetupTranslator();
const channel$1 = "nextcloud-talk";
function addWildcardAllowFrom(allowFrom) {
	return mergeAllowFromEntries(allowFrom, ["*"]);
}
function normalizeNextcloudTalkBaseUrl(value) {
	return value?.trim().replace(/\/+$/, "") ?? "";
}
function validateNextcloudTalkBaseUrl(value) {
	if (!value) return "Required";
	if (!value.startsWith("http://") && !value.startsWith("https://")) return "URL must start with http:// or https://";
}
function setNextcloudTalkAccountConfig(cfg, accountId, updates) {
	return patchScopedAccountConfig({
		cfg,
		channelKey: channel$1,
		accountId,
		patch: updates
	});
}
function clearNextcloudTalkAccountFields(cfg, accountId, fields) {
	const section = cfg.channels?.["nextcloud-talk"];
	if (!section) return cfg;
	if (accountId === "default") {
		const nextSection = { ...section };
		for (const field of fields) delete nextSection[field];
		return {
			...cfg,
			channels: {
				...cfg.channels,
				"nextcloud-talk": nextSection
			}
		};
	}
	const currentAccount = section.accounts?.[accountId];
	if (!currentAccount) return cfg;
	const nextAccount = { ...currentAccount };
	for (const field of fields) delete nextAccount[field];
	return {
		...cfg,
		channels: {
			...cfg.channels,
			"nextcloud-talk": {
				...section,
				accounts: {
					...section.accounts,
					[accountId]: nextAccount
				}
			}
		}
	};
}
async function promptNextcloudTalkAllowFrom(params) {
	return await promptParsedAllowFromForAccount({
		cfg: params.cfg,
		accountId: params.accountId,
		defaultAccountId: params.accountId,
		prompter: params.prompter,
		noteTitle: t$1("wizard.nextcloudTalk.userIdTitle"),
		noteLines: [
			t$1("wizard.nextcloudTalk.userIdHelpAdmin"),
			t$1("wizard.nextcloudTalk.userIdHelpLogs"),
			t$1("wizard.nextcloudTalk.userIdHelpLowercase"),
			t$1("wizard.channels.docs", { link: formatDocsLink("/channels/nextcloud-talk", "nextcloud-talk") })
		],
		message: t$1("wizard.nextcloudTalk.allowFromPrompt"),
		placeholder: "username",
		parseEntries: (raw) => ({ entries: raw.split(/[\n,;]+/g).map(normalizeLowercaseStringOrEmpty).filter(Boolean) }),
		getExistingAllowFrom: ({ cfg, accountId }) => resolveNextcloudTalkAccount({
			cfg,
			accountId
		}).config.allowFrom ?? [],
		mergeEntries: ({ existing, parsed }) => mergeAllowFromEntries(existing.map((value) => normalizeLowercaseStringOrEmpty(String(value))), parsed),
		applyAllowFrom: ({ cfg, accountId, allowFrom }) => setNextcloudTalkAccountConfig(cfg, accountId, {
			dmPolicy: "allowlist",
			allowFrom
		})
	});
}
async function promptNextcloudTalkAllowFromForAccount(params) {
	const accountId = resolveSetupAccountId({
		accountId: params.accountId,
		defaultAccountId: resolveDefaultNextcloudTalkAccountId(params.cfg)
	});
	return await promptNextcloudTalkAllowFrom({
		cfg: params.cfg,
		prompter: params.prompter,
		accountId
	});
}
const nextcloudTalkDmPolicy = {
	label: "Nextcloud Talk",
	channel: channel$1,
	policyKey: "channels.nextcloud-talk.dmPolicy",
	allowFromKey: "channels.nextcloud-talk.allowFrom",
	resolveConfigKeys: (cfg, accountId) => (accountId ?? resolveDefaultNextcloudTalkAccountId(cfg)) !== "default" ? {
		policyKey: `channels.nextcloud-talk.accounts.${accountId ?? resolveDefaultNextcloudTalkAccountId(cfg)}.dmPolicy`,
		allowFromKey: `channels.nextcloud-talk.accounts.${accountId ?? resolveDefaultNextcloudTalkAccountId(cfg)}.allowFrom`
	} : {
		policyKey: "channels.nextcloud-talk.dmPolicy",
		allowFromKey: "channels.nextcloud-talk.allowFrom"
	},
	getCurrent: (cfg, accountId) => resolveNextcloudTalkAccount({
		cfg,
		accountId: accountId ?? resolveDefaultNextcloudTalkAccountId(cfg)
	}).config.dmPolicy ?? "pairing",
	setPolicy: (cfg, policy, accountId) => {
		const resolvedAccountId = accountId ?? resolveDefaultNextcloudTalkAccountId(cfg);
		const resolved = resolveNextcloudTalkAccount({
			cfg,
			accountId: resolvedAccountId
		});
		return setNextcloudTalkAccountConfig(cfg, resolvedAccountId, {
			dmPolicy: policy,
			...policy === "open" ? { allowFrom: addWildcardAllowFrom(resolved.config.allowFrom) } : {}
		});
	},
	promptAllowFrom: promptNextcloudTalkAllowFromForAccount
};
const nextcloudTalkSetupAdapter = {
	resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
	applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
		cfg,
		channelKey: channel$1,
		accountId,
		name
	}),
	validateInput: createSetupInputPresenceValidator({
		defaultAccountOnlyEnvError: "NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.",
		validate: ({ input }) => {
			const setupInput = input;
			if (!setupInput.useEnv && !setupInput.secret && !setupInput.secretFile) return "Nextcloud Talk requires bot secret or --secret-file (or --use-env).";
			if (!setupInput.baseUrl) return "Nextcloud Talk requires --base-url.";
			return null;
		}
	}),
	applyAccountConfig: ({ cfg, accountId, input }) => {
		const setupInput = input;
		const namedConfig = applyAccountNameToChannelSection({
			cfg,
			channelKey: channel$1,
			accountId,
			name: setupInput.name
		});
		return setNextcloudTalkAccountConfig(setupInput.useEnv ? clearNextcloudTalkAccountFields(namedConfig, accountId, ["botSecret", "botSecretFile"]) : namedConfig, accountId, {
			baseUrl: normalizeNextcloudTalkBaseUrl(setupInput.baseUrl),
			...setupInput.useEnv ? {} : setupInput.secretFile ? { botSecretFile: setupInput.secretFile } : setupInput.secret ? { botSecret: setupInput.secret } : {}
		});
	}
};
//#endregion
//#region extensions/nextcloud-talk/src/setup-surface.ts
const t = createSetupTranslator();
const channel = "nextcloud-talk";
const CONFIGURE_API_FLAG = "__nextcloudTalkConfigureApiCredentials";
const nextcloudTalkSetupWizard = {
	channel,
	stepOrder: "text-first",
	status: createStandardChannelSetupStatus({
		channelLabel: "Nextcloud Talk",
		configuredLabel: t("wizard.channels.statusConfigured"),
		unconfiguredLabel: t("wizard.channels.statusNeedsSetup"),
		configuredHint: t("wizard.channels.statusConfigured"),
		unconfiguredHint: t("wizard.channels.statusSelfHostedChat"),
		configuredScore: 1,
		unconfiguredScore: 5,
		resolveConfigured: ({ cfg, accountId }) => {
			const account = resolveNextcloudTalkAccount({
				cfg,
				accountId
			});
			return Boolean(account.secret && account.baseUrl);
		}
	}),
	introNote: {
		title: t("wizard.nextcloudTalk.setupTitle"),
		lines: [
			t("wizard.nextcloudTalk.helpSsh"),
			t("wizard.nextcloudTalk.helpInstallCommand"),
			t("wizard.nextcloudTalk.helpCopySecret"),
			t("wizard.nextcloudTalk.helpEnableRoom"),
			t("wizard.nextcloudTalk.helpEnvTip"),
			t("wizard.channels.docs", { link: formatDocsLink("/channels/nextcloud-talk", "channels/nextcloud-talk") })
		],
		shouldShow: ({ cfg, accountId }) => {
			const account = resolveNextcloudTalkAccount({
				cfg,
				accountId
			});
			return !account.secret || !account.baseUrl;
		}
	},
	prepare: async ({ cfg, accountId, credentialValues, prompter }) => {
		const resolvedAccount = resolveNextcloudTalkAccount({
			cfg,
			accountId
		});
		const hasApiCredentials = Boolean(resolvedAccount.config.apiUser?.trim() && (hasConfiguredSecretInput(resolvedAccount.config.apiPassword) || resolvedAccount.config.apiPasswordFile));
		if (!await prompter.confirm({
			message: t("wizard.nextcloudTalk.configureApiCredentials"),
			initialValue: hasApiCredentials
		})) return;
		return { credentialValues: {
			...credentialValues,
			[CONFIGURE_API_FLAG]: "1"
		} };
	},
	credentials: [{
		inputKey: "token",
		providerHint: channel,
		credentialLabel: t("wizard.nextcloudTalk.botSecret"),
		preferredEnvVar: "NEXTCLOUD_TALK_BOT_SECRET",
		envPrompt: t("wizard.nextcloudTalk.botSecretEnvPrompt"),
		keepPrompt: t("wizard.nextcloudTalk.botSecretKeep"),
		inputPrompt: t("wizard.nextcloudTalk.botSecretInput"),
		allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID,
		inspect: ({ cfg, accountId }) => {
			const resolvedAccount = resolveNextcloudTalkAccount({
				cfg,
				accountId
			});
			return {
				accountConfigured: Boolean(resolvedAccount.secret && resolvedAccount.baseUrl),
				hasConfiguredValue: Boolean(hasConfiguredSecretInput(resolvedAccount.config.botSecret) || resolvedAccount.config.botSecretFile),
				resolvedValue: resolvedAccount.secret || void 0,
				envValue: accountId === "default" ? normalizeOptionalString(process.env.NEXTCLOUD_TALK_BOT_SECRET) : void 0
			};
		},
		applyUseEnv: async (params) => {
			const resolvedAccount = resolveNextcloudTalkAccount({
				cfg: params.cfg,
				accountId: params.accountId
			});
			return setNextcloudTalkAccountConfig(clearNextcloudTalkAccountFields(params.cfg, params.accountId, ["botSecret", "botSecretFile"]), params.accountId, { baseUrl: resolvedAccount.baseUrl });
		},
		applySet: async (params) => setNextcloudTalkAccountConfig(clearNextcloudTalkAccountFields(params.cfg, params.accountId, ["botSecret", "botSecretFile"]), params.accountId, { botSecret: params.value })
	}, {
		inputKey: "password",
		providerHint: "nextcloud-talk-api",
		credentialLabel: t("wizard.nextcloudTalk.apiPassword"),
		preferredEnvVar: "NEXTCLOUD_TALK_API_PASSWORD",
		envPrompt: "",
		keepPrompt: t("wizard.nextcloudTalk.apiPasswordKeep"),
		inputPrompt: t("wizard.nextcloudTalk.apiPasswordInput"),
		inspect: ({ cfg, accountId }) => {
			const resolvedAccount = resolveNextcloudTalkAccount({
				cfg,
				accountId
			});
			const apiUser = resolvedAccount.config.apiUser?.trim();
			const apiPasswordConfigured = Boolean(hasConfiguredSecretInput(resolvedAccount.config.apiPassword) || resolvedAccount.config.apiPasswordFile);
			return {
				accountConfigured: Boolean(apiUser && apiPasswordConfigured),
				hasConfiguredValue: apiPasswordConfigured
			};
		},
		shouldPrompt: ({ credentialValues }) => credentialValues[CONFIGURE_API_FLAG] === "1",
		applySet: async (params) => setNextcloudTalkAccountConfig(clearNextcloudTalkAccountFields(params.cfg, params.accountId, ["apiPassword", "apiPasswordFile"]), params.accountId, { apiPassword: params.value })
	}],
	textInputs: [{
		inputKey: "httpUrl",
		message: t("wizard.nextcloudTalk.instanceUrlPrompt"),
		currentValue: ({ cfg, accountId }) => resolveNextcloudTalkAccount({
			cfg,
			accountId
		}).baseUrl || void 0,
		shouldPrompt: ({ currentValue }) => !currentValue,
		validate: ({ value }) => validateNextcloudTalkBaseUrl(value),
		normalizeValue: ({ value }) => normalizeNextcloudTalkBaseUrl(value),
		applySet: async (params) => setNextcloudTalkAccountConfig(params.cfg, params.accountId, { baseUrl: params.value })
	}, {
		inputKey: "userId",
		message: t("wizard.nextcloudTalk.apiUserPrompt"),
		currentValue: ({ cfg, accountId }) => resolveNextcloudTalkAccount({
			cfg,
			accountId
		}).config.apiUser?.trim() || void 0,
		shouldPrompt: ({ credentialValues }) => credentialValues[CONFIGURE_API_FLAG] === "1",
		validate: ({ value }) => value ? void 0 : t("common.required"),
		applySet: async (params) => setNextcloudTalkAccountConfig(params.cfg, params.accountId, { apiUser: params.value })
	}],
	dmPolicy: nextcloudTalkDmPolicy,
	disable: (cfg) => setSetupChannelEnabled(cfg, channel, false)
};
//#endregion
//#region extensions/nextcloud-talk/src/channel.ts
const meta = {
	id: "nextcloud-talk",
	label: "Nextcloud Talk",
	selectionLabel: "Nextcloud Talk (self-hosted)",
	docsPath: "/channels/nextcloud-talk",
	docsLabel: "nextcloud-talk",
	blurb: "Self-hosted chat via Nextcloud Talk webhook bots.",
	aliases: ["nc-talk", "nc"],
	order: 65,
	quickstartAllowFrom: true
};
const collectNextcloudTalkSecurityWarnings = createAllowlistProviderRouteAllowlistWarningCollector({
	providerConfigPresent: (cfg) => cfg.channels?.["nextcloud-talk"] !== void 0,
	resolveGroupPolicy: (account) => account.config.groupPolicy,
	resolveRouteAllowlistConfigured: (account) => Boolean(account.config.rooms) && Object.keys(account.config.rooms ?? {}).length > 0,
	restrictSenders: {
		surface: "Nextcloud Talk rooms",
		openScope: "any member in allowed rooms",
		groupPolicyPath: "channels.nextcloud-talk.groupPolicy",
		groupAllowFromPath: "channels.nextcloud-talk.groupAllowFrom"
	},
	noRouteAllowlist: {
		surface: "Nextcloud Talk rooms",
		routeAllowlistPath: "channels.nextcloud-talk.rooms",
		routeScope: "room",
		groupPolicyPath: "channels.nextcloud-talk.groupPolicy",
		groupAllowFromPath: "channels.nextcloud-talk.groupAllowFrom"
	}
});
const nextcloudTalkPlugin = createChatChannelPlugin({
	base: {
		id: "nextcloud-talk",
		meta,
		setupWizard: nextcloudTalkSetupWizard,
		capabilities: {
			chatTypes: ["direct", "group"],
			reactions: true,
			threads: false,
			media: true,
			nativeCommands: false,
			blockStreaming: true
		},
		reload: { configPrefixes: ["channels.nextcloud-talk"] },
		configSchema: buildChannelConfigSchema(NextcloudTalkConfigSchema),
		config: {
			...nextcloudTalkConfigAdapter,
			isConfigured: (account) => Boolean(account.secret?.trim() && account.baseUrl?.trim()),
			describeAccount: (account) => describeWebhookAccountSnapshot({
				account,
				configured: Boolean(account.secret?.trim() && account.baseUrl?.trim()),
				extra: {
					secretSource: account.secretSource,
					baseUrl: account.baseUrl ? "[set]" : "[missing]"
				}
			})
		},
		approvalCapability: nextcloudTalkApprovalAuth,
		doctor: nextcloudTalkDoctor,
		groups: {
			resolveRequireMention: ({ cfg, accountId, groupId }) => {
				const rooms = resolveNextcloudTalkAccount({
					cfg,
					accountId
				}).config.rooms;
				if (!rooms || !groupId) return true;
				const roomConfig = rooms[groupId];
				if (roomConfig?.requireMention !== void 0) return roomConfig.requireMention;
				const wildcardConfig = rooms["*"];
				if (wildcardConfig?.requireMention !== void 0) return wildcardConfig.requireMention;
				return true;
			},
			resolveToolPolicy: resolveNextcloudTalkGroupToolPolicy
		},
		messaging: {
			targetPrefixes: [
				"nextcloud-talk",
				"nc-talk",
				"nc"
			],
			normalizeTarget: normalizeNextcloudTalkMessagingTarget,
			resolveOutboundSessionRoute: (params) => resolveNextcloudTalkOutboundSessionRoute(params),
			targetResolver: {
				looksLikeId: looksLikeNextcloudTalkTargetId,
				hint: "<roomToken>"
			}
		},
		secrets: {
			secretTargetRegistryEntries,
			collectRuntimeConfigAssignments
		},
		setup: nextcloudTalkSetupAdapter,
		status: createComputedAccountStatusAdapter({
			defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
			buildChannelSummary: ({ snapshot }) => buildWebhookChannelStatusSummary(snapshot, { secretSource: snapshot.secretSource ?? "none" }),
			collectStatusIssues: (accounts) => accounts.flatMap((account) => {
				const probe = account.probe;
				if (!probe || probe.ok !== false || probe.code !== "missing_response_feature" || !probe.message) return [];
				return [{
					channel: "nextcloud-talk",
					accountId: account.accountId ?? "default",
					kind: "config",
					message: probe.message,
					fix: "Add --feature response to the Talk bot."
				}];
			}),
			probeAccount: async ({ account, timeoutMs }) => await probeNextcloudTalkBotResponseFeature({
				account,
				timeoutMs
			}),
			resolveAccountSnapshot: ({ account }) => ({
				accountId: account.accountId,
				name: account.name,
				enabled: account.enabled,
				configured: Boolean(account.secret?.trim() && account.baseUrl?.trim()),
				extra: {
					secretSource: account.secretSource,
					baseUrl: account.baseUrl ? "[set]" : "[missing]",
					mode: "webhook"
				}
			})
		}),
		gateway: nextcloudTalkGatewayAdapter,
		message: nextcloudTalkMessageAdapter,
		actions: nextcloudTalkMessageActions
	},
	pairing: { text: {
		...nextcloudTalkPairingTextAdapter,
		notify: createLoggedPairingApprovalNotifier(({ id }) => `[nextcloud-talk] User ${id} approved for pairing`)
	} },
	security: {
		...nextcloudTalkSecurityAdapter,
		collectWarnings: collectNextcloudTalkSecurityWarnings
	},
	outbound: {
		base: {
			deliveryMode: "direct",
			chunker: (text, limit) => getNextcloudTalkRuntime().channel.text.chunkMarkdownText(text, limit),
			chunkerMode: "markdown",
			textChunkLimit: 4e3
		},
		attachedResults: {
			channel: "nextcloud-talk",
			sendText: async ({ cfg, to, text, accountId, replyToId }) => await nextcloudTalkMessageAdapter.send.text({
				cfg,
				to,
				text,
				accountId,
				replyToId
			}),
			sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => await nextcloudTalkMessageAdapter.send.media({
				cfg,
				to,
				text,
				mediaUrl: mediaUrl ?? "",
				accountId,
				replyToId
			})
		}
	}
});
//#endregion
export { nextcloudTalkPlugin as t };