From bce643a0bd145d3e9cb55400af33bd1b85baeb02 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Feb 2026 21:57:10 +0100 Subject: [PATCH] refactor(security): enforce account-scoped pairing APIs --- package.json | 3 +- scripts/check-pairing-account-scope.mjs | 157 ++++++++++++++++++ src/auto-reply/reply/commands-allowlist.ts | 2 +- src/channels/plugins/whatsapp-heartbeat.ts | 7 +- src/commands/doctor-security.ts | 3 + src/cron/isolated-agent/delivery-target.ts | 8 +- src/discord/monitor/agent-components.ts | 8 +- src/discord/monitor/listeners.ts | 8 +- .../monitor/message-handler.preflight.ts | 11 +- src/discord/monitor/native-command.ts | 8 +- src/imessage/monitor/monitor-provider.ts | 7 +- src/line/bot-handlers.ts | 7 +- src/pairing/pairing-store.test.ts | 19 ++- src/pairing/pairing-store.ts | 57 ++++--- src/plugins/runtime/index.ts | 13 +- src/plugins/runtime/types.ts | 12 +- src/security/audit-channel.ts | 21 ++- src/security/dm-policy-shared.test.ts | 12 +- src/security/dm-policy-shared.ts | 13 +- src/security/fix.ts | 8 +- src/signal/monitor/event-handler.ts | 8 +- src/slack/monitor/auth.ts | 3 +- src/slack/monitor/message-handler/prepare.ts | 1 + src/slack/monitor/slash.ts | 8 +- src/telegram/bot/helpers.ts | 10 +- src/web/auto-reply/monitor/process-message.ts | 4 +- src/web/inbound/access-control.ts | 7 +- 27 files changed, 331 insertions(+), 94 deletions(-) create mode 100644 scripts/check-pairing-account-scope.mjs diff --git a/package.json b/package.json index 51c24aef7..243d1a6ca 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "build": "pnpm canvas:a2ui:bundle && tsdown && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-compat.ts", "build:plugin-sdk:dts": "tsc -p tsconfig.plugin-sdk.dts.json", "canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh", - "check": "pnpm format:check && pnpm tsgo && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:auth:no-pairing-store-group && pnpm check:host-env-policy:swift", + "check": "pnpm format:check && pnpm tsgo && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope && pnpm check:host-env-policy:swift", "check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-links", "check:host-env-policy:swift": "node scripts/generate-host-env-security-policy-swift.mjs --check", "check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500", @@ -93,6 +93,7 @@ "lint": "oxlint --type-aware", "lint:all": "pnpm lint && pnpm lint:swift", "lint:auth:no-pairing-store-group": "node scripts/check-no-pairing-store-group-auth.mjs", + "lint:auth:pairing-account-scope": "node scripts/check-pairing-account-scope.mjs", "lint:docs": "pnpm dlx markdownlint-cli2", "lint:docs:fix": "pnpm dlx markdownlint-cli2 --fix", "lint:fix": "oxlint --type-aware --fix && pnpm format", diff --git a/scripts/check-pairing-account-scope.mjs b/scripts/check-pairing-account-scope.mjs new file mode 100644 index 000000000..21db11a87 --- /dev/null +++ b/scripts/check-pairing-account-scope.mjs @@ -0,0 +1,157 @@ +#!/usr/bin/env node + +import { promises as fs } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import ts from "typescript"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const sourceRoots = [path.join(repoRoot, "src"), path.join(repoRoot, "extensions")]; + +function isTestLikeFile(filePath) { + return ( + filePath.endsWith(".test.ts") || + filePath.endsWith(".test-utils.ts") || + filePath.endsWith(".test-harness.ts") || + filePath.endsWith(".e2e-harness.ts") + ); +} + +async function collectTypeScriptFiles(dir) { + const entries = await fs.readdir(dir, { withFileTypes: true }); + const out = []; + for (const entry of entries) { + const entryPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + out.push(...(await collectTypeScriptFiles(entryPath))); + continue; + } + if (!entry.isFile() || !entryPath.endsWith(".ts") || isTestLikeFile(entryPath)) { + continue; + } + out.push(entryPath); + } + return out; +} + +function toLine(sourceFile, node) { + return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1; +} + +function getPropertyNameText(name) { + if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) { + return name.text; + } + return null; +} + +function isUndefinedLikeExpression(node) { + if (ts.isIdentifier(node) && node.text === "undefined") { + return true; + } + return node.kind === ts.SyntaxKind.NullKeyword; +} + +function hasRequiredAccountIdProperty(node) { + if (!ts.isObjectLiteralExpression(node)) { + return false; + } + for (const property of node.properties) { + if (ts.isShorthandPropertyAssignment(property) && property.name.text === "accountId") { + return true; + } + if (!ts.isPropertyAssignment(property)) { + continue; + } + if (getPropertyNameText(property.name) !== "accountId") { + continue; + } + if (isUndefinedLikeExpression(property.initializer)) { + return false; + } + return true; + } + return false; +} + +function findViolations(content, filePath) { + const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true); + const violations = []; + + const visit = (node) => { + if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) { + const callName = node.expression.text; + if (callName === "readChannelAllowFromStore") { + if (node.arguments.length < 3 || isUndefinedLikeExpression(node.arguments[2])) { + violations.push({ + line: toLine(sourceFile, node), + reason: "readChannelAllowFromStore call must pass explicit accountId as 3rd arg", + }); + } + } else if ( + callName === "readLegacyChannelAllowFromStore" || + callName === "readLegacyChannelAllowFromStoreSync" + ) { + violations.push({ + line: toLine(sourceFile, node), + reason: `${callName} is legacy-only; use account-scoped readChannelAllowFromStore* APIs`, + }); + } else if (callName === "upsertChannelPairingRequest") { + const firstArg = node.arguments[0]; + if (!firstArg || !hasRequiredAccountIdProperty(firstArg)) { + violations.push({ + line: toLine(sourceFile, node), + reason: "upsertChannelPairingRequest call must include accountId in params", + }); + } + } + } + ts.forEachChild(node, visit); + }; + + visit(sourceFile); + return violations; +} + +async function main() { + const files = ( + await Promise.all(sourceRoots.map(async (root) => await collectTypeScriptFiles(root))) + ).flat(); + const violations = []; + + for (const filePath of files) { + const content = await fs.readFile(filePath, "utf8"); + const fileViolations = findViolations(content, filePath); + for (const violation of fileViolations) { + violations.push({ + path: path.relative(repoRoot, filePath), + ...violation, + }); + } + } + + if (violations.length === 0) { + return; + } + + console.error("Found unscoped pairing-store calls:"); + for (const violation of violations) { + console.error(`- ${violation.path}:${violation.line} (${violation.reason})`); + } + process.exit(1); +} + +const isDirectExecution = (() => { + const entry = process.argv[1]; + if (!entry) { + return false; + } + return path.resolve(entry) === fileURLToPath(import.meta.url); +})(); + +if (isDirectExecution) { + main().catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/src/auto-reply/reply/commands-allowlist.ts b/src/auto-reply/reply/commands-allowlist.ts index 1ba35827f..079e03437 100644 --- a/src/auto-reply/reply/commands-allowlist.ts +++ b/src/auto-reply/reply/commands-allowlist.ts @@ -390,7 +390,7 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo const pairingChannels = listPairingChannels(); const supportsStore = pairingChannels.includes(channelId); const storeAllowFrom = supportsStore - ? await readChannelAllowFromStore(channelId).catch(() => []) + ? await readChannelAllowFromStore(channelId, process.env, accountId).catch(() => []) : []; let dmAllowFrom: string[] = []; diff --git a/src/channels/plugins/whatsapp-heartbeat.ts b/src/channels/plugins/whatsapp-heartbeat.ts index d91e5dd25..35ec38d42 100644 --- a/src/channels/plugins/whatsapp-heartbeat.ts +++ b/src/channels/plugins/whatsapp-heartbeat.ts @@ -1,6 +1,7 @@ import type { OpenClawConfig } from "../../config/config.js"; import { loadSessionStore, resolveStorePath } from "../../config/sessions.js"; import { readChannelAllowFromStoreSync } from "../../pairing/pairing-store.js"; +import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js"; import { normalizeE164 } from "../../utils.js"; import { normalizeChatChannelId } from "../registry.js"; @@ -56,7 +57,11 @@ export function resolveWhatsAppHeartbeatRecipients( Array.isArray(cfg.channels?.whatsapp?.allowFrom) && cfg.channels.whatsapp.allowFrom.length > 0 ? cfg.channels.whatsapp.allowFrom.filter((v) => v !== "*").map(normalizeE164) : []; - const storeAllowFrom = readChannelAllowFromStoreSync("whatsapp").map(normalizeE164); + const storeAllowFrom = readChannelAllowFromStoreSync( + "whatsapp", + process.env, + DEFAULT_ACCOUNT_ID, + ).map(normalizeE164); const unique = (list: string[]) => [...new Set(list.filter(Boolean))]; const allowFrom = unique([...configuredAllowFrom, ...storeAllowFrom]); diff --git a/src/commands/doctor-security.ts b/src/commands/doctor-security.ts index dc06f6396..d1672c2ea 100644 --- a/src/commands/doctor-security.ts +++ b/src/commands/doctor-security.ts @@ -90,6 +90,7 @@ export async function noteSecurityWarnings(cfg: OpenClawConfig) { const warnDmPolicy = async (params: { label: string; provider: ChannelId; + accountId: string; dmPolicy: string; allowFrom?: Array | null; policyPath?: string; @@ -101,6 +102,7 @@ export async function noteSecurityWarnings(cfg: OpenClawConfig) { const policyPath = params.policyPath ?? `${params.allowFromPath}policy`; const { hasWildcard, allowCount, isMultiUserDm } = await resolveDmAllowState({ provider: params.provider, + accountId: params.accountId, allowFrom: params.allowFrom, normalizeEntry: params.normalizeEntry, }); @@ -158,6 +160,7 @@ export async function noteSecurityWarnings(cfg: OpenClawConfig) { await warnDmPolicy({ label: plugin.meta.label ?? plugin.id, provider: plugin.id, + accountId: defaultAccountId, dmPolicy: dmPolicy.policy, allowFrom: dmPolicy.allowFrom, policyPath: dmPolicy.policyPath, diff --git a/src/cron/isolated-agent/delivery-target.ts b/src/cron/isolated-agent/delivery-target.ts index 1af69ee02..a8b4bc7d7 100644 --- a/src/cron/isolated-agent/delivery-target.ts +++ b/src/cron/isolated-agent/delivery-target.ts @@ -13,7 +13,7 @@ import { } from "../../infra/outbound/targets.js"; import { readChannelAllowFromStoreSync } from "../../pairing/pairing-store.js"; import { buildChannelAccountBindings } from "../../routing/bindings.js"; -import { normalizeAgentId } from "../../routing/session-key.js"; +import { normalizeAccountId, normalizeAgentId } from "../../routing/session-key.js"; import { resolveWhatsAppAccount } from "../../web/accounts.js"; import { normalizeWhatsAppTarget } from "../../whatsapp/normalize.js"; @@ -160,13 +160,15 @@ export async function resolveDeliveryTarget( let allowFromOverride: string[] | undefined; if (channel === "whatsapp") { - const configuredAllowFromRaw = resolveWhatsAppAccount({ cfg, accountId }).allowFrom ?? []; + const resolvedAccountId = normalizeAccountId(accountId); + const configuredAllowFromRaw = + resolveWhatsAppAccount({ cfg, accountId: resolvedAccountId }).allowFrom ?? []; const configuredAllowFrom = configuredAllowFromRaw .map((entry) => String(entry).trim()) .filter((entry) => entry && entry !== "*") .map((entry) => normalizeWhatsAppTarget(entry)) .filter((entry): entry is string => Boolean(entry)); - const storeAllowFrom = readChannelAllowFromStoreSync("whatsapp", process.env, accountId) + const storeAllowFrom = readChannelAllowFromStoreSync("whatsapp", process.env, resolvedAccountId) .map((entry) => normalizeWhatsAppTarget(entry)) .filter((entry): entry is string => Boolean(entry)); allowFromOverride = [...new Set([...configuredAllowFrom, ...storeAllowFrom])]; diff --git a/src/discord/monitor/agent-components.ts b/src/discord/monitor/agent-components.ts index bdfdbf5f1..1c2a3cbe0 100644 --- a/src/discord/monitor/agent-components.ts +++ b/src/discord/monitor/agent-components.ts @@ -35,10 +35,7 @@ import { logVerbose } from "../../globals.js"; import { enqueueSystemEvent } from "../../infra/system-events.js"; import { logDebug, logError } from "../../logger.js"; import { buildPairingReply } from "../../pairing/pairing-messages.js"; -import { - readChannelAllowFromStore, - upsertChannelPairingRequest, -} from "../../pairing/pairing-store.js"; +import { upsertChannelPairingRequest } from "../../pairing/pairing-store.js"; import { resolveAgentRoute } from "../../routing/resolve-route.js"; import { createNonExitingRuntime, type RuntimeEnv } from "../../runtime.js"; import { readStoreAllowFromForDmPolicy } from "../../security/dm-policy-shared.js"; @@ -474,8 +471,8 @@ async function ensureDmComponentAuthorized(params: { const storeAllowFrom = await readStoreAllowFromForDmPolicy({ provider: "discord", + accountId: ctx.accountId, dmPolicy, - readStore: (provider) => readChannelAllowFromStore(provider), }); const effectiveAllowFrom = [...(ctx.allowFrom ?? []), ...storeAllowFrom]; const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:", "pk:"]); @@ -498,6 +495,7 @@ async function ensureDmComponentAuthorized(params: { const { code, created } = await upsertChannelPairingRequest({ channel: "discord", id: user.id, + accountId: ctx.accountId, meta: { tag: formatDiscordUserTag(user), name: user.username, diff --git a/src/discord/monitor/listeners.ts b/src/discord/monitor/listeners.ts index b0aedf275..e6679c4b9 100644 --- a/src/discord/monitor/listeners.ts +++ b/src/discord/monitor/listeners.ts @@ -11,7 +11,6 @@ import { danger, logVerbose } from "../../globals.js"; import { formatDurationSeconds } from "../../infra/format-time/format-duration.ts"; import { enqueueSystemEvent } from "../../infra/system-events.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; -import { readChannelAllowFromStore } from "../../pairing/pairing-store.js"; import { resolveAgentRoute } from "../../routing/resolve-route.js"; import { readStoreAllowFromForDmPolicy, @@ -208,6 +207,7 @@ async function runDiscordReactionHandler(params: { } type DiscordReactionIngressAuthorizationParams = { + accountId: string; user: User; isDirectMessage: boolean; isGroupDm: boolean; @@ -238,8 +238,8 @@ async function authorizeDiscordReactionIngress( if (params.isDirectMessage) { const storeAllowFrom = await readStoreAllowFromForDmPolicy({ provider: "discord", + accountId: params.accountId, dmPolicy: params.dmPolicy, - readStore: (provider) => readChannelAllowFromStore(provider), }); const access = resolveDmGroupAccessWithLists({ isGroup: false, @@ -358,6 +358,7 @@ async function handleDiscordReactionEvent(params: { channelType === ChannelType.PrivateThread || channelType === ChannelType.AnnouncementThread; const ingressAccess = await authorizeDiscordReactionIngress({ + accountId: params.accountId, user, isDirectMessage, isGroupDm, @@ -486,6 +487,7 @@ async function handleDiscordReactionEvent(params: { const channelConfig = resolveThreadChannelConfig(); const threadAccess = await authorizeDiscordReactionIngress({ + accountId: params.accountId, user, isDirectMessage, isGroupDm, @@ -528,6 +530,7 @@ async function handleDiscordReactionEvent(params: { const channelConfig = resolveThreadChannelConfig(); const threadAccess = await authorizeDiscordReactionIngress({ + accountId: params.accountId, user, isDirectMessage, isGroupDm, @@ -571,6 +574,7 @@ async function handleDiscordReactionEvent(params: { }); if (isGuildMessage) { const channelAccess = await authorizeDiscordReactionIngress({ + accountId: params.accountId, user, isDirectMessage, isGroupDm, diff --git a/src/discord/monitor/message-handler.preflight.ts b/src/discord/monitor/message-handler.preflight.ts index 27dff979c..2777ba01b 100644 --- a/src/discord/monitor/message-handler.preflight.ts +++ b/src/discord/monitor/message-handler.preflight.ts @@ -25,12 +25,9 @@ import { enqueueSystemEvent } from "../../infra/system-events.js"; import { logDebug } from "../../logger.js"; import { getChildLogger } from "../../logging.js"; import { buildPairingReply } from "../../pairing/pairing-messages.js"; -import { - readChannelAllowFromStore, - upsertChannelPairingRequest, -} from "../../pairing/pairing-store.js"; +import { upsertChannelPairingRequest } from "../../pairing/pairing-store.js"; import { resolveAgentRoute } from "../../routing/resolve-route.js"; -import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js"; +import { DEFAULT_ACCOUNT_ID, resolveAgentIdFromSessionKey } from "../../routing/session-key.js"; import { readStoreAllowFromForDmPolicy } from "../../security/dm-policy-shared.js"; import { fetchPluralKitMessageInfo } from "../pluralkit.js"; import { sendMessageDiscord } from "../send.js"; @@ -177,6 +174,7 @@ export async function preflightDiscordMessage( } const dmPolicy = params.discordConfig?.dmPolicy ?? params.discordConfig?.dm?.policy ?? "pairing"; + const resolvedAccountId = params.accountId ?? DEFAULT_ACCOUNT_ID; let commandAuthorized = true; if (isDirectMessage) { if (dmPolicy === "disabled") { @@ -186,8 +184,8 @@ export async function preflightDiscordMessage( if (dmPolicy !== "open") { const storeAllowFrom = await readStoreAllowFromForDmPolicy({ provider: "discord", + accountId: resolvedAccountId, dmPolicy, - readStore: (provider) => readChannelAllowFromStore(provider), }); const effectiveAllowFrom = [...(params.allowFrom ?? []), ...storeAllowFrom]; const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:", "pk:"]); @@ -210,6 +208,7 @@ export async function preflightDiscordMessage( const { code, created } = await upsertChannelPairingRequest({ channel: "discord", id: author.id, + accountId: resolvedAccountId, meta: { tag: formatDiscordUserTag(author), name: author.username ?? undefined, diff --git a/src/discord/monitor/native-command.ts b/src/discord/monitor/native-command.ts index 24a6eb601..feeb89f2d 100644 --- a/src/discord/monitor/native-command.ts +++ b/src/discord/monitor/native-command.ts @@ -46,10 +46,7 @@ import { logVerbose } from "../../globals.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js"; import { buildPairingReply } from "../../pairing/pairing-messages.js"; -import { - readChannelAllowFromStore, - upsertChannelPairingRequest, -} from "../../pairing/pairing-store.js"; +import { upsertChannelPairingRequest } from "../../pairing/pairing-store.js"; import { resolveAgentRoute } from "../../routing/resolve-route.js"; import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js"; import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js"; @@ -1363,8 +1360,8 @@ async function dispatchDiscordCommandInteraction(params: { if (dmPolicy !== "open") { const storeAllowFrom = await readStoreAllowFromForDmPolicy({ provider: "discord", + accountId, dmPolicy, - readStore: (provider) => readChannelAllowFromStore(provider), }); const effectiveAllowFrom = [ ...(discordConfig?.allowFrom ?? discordConfig?.dm?.allowFrom ?? []), @@ -1388,6 +1385,7 @@ async function dispatchDiscordCommandInteraction(params: { const { code, created } = await upsertChannelPairingRequest({ channel: "discord", id: user.id, + accountId, meta: { tag: sender.tag, name: sender.name, diff --git a/src/imessage/monitor/monitor-provider.ts b/src/imessage/monitor/monitor-provider.ts index 3bfdc6911..838e840f5 100644 --- a/src/imessage/monitor/monitor-provider.ts +++ b/src/imessage/monitor/monitor-provider.ts @@ -230,7 +230,11 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P : ""; const bodyText = messageText || placeholder; - const storeAllowFrom = await readChannelAllowFromStore("imessage").catch(() => []); + const storeAllowFrom = await readChannelAllowFromStore( + "imessage", + process.env, + accountInfo.accountId, + ).catch(() => []); const decision = resolveIMessageInboundDecision({ cfg, accountId: accountInfo.accountId, @@ -262,6 +266,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P const { code, created } = await upsertChannelPairingRequest({ channel: "imessage", id: decision.senderId, + accountId: accountInfo.accountId, meta: { sender: decision.senderId, chatId: chatId ? String(chatId) : undefined, diff --git a/src/line/bot-handlers.ts b/src/line/bot-handlers.ts index c77d9d9b0..ae432bcc5 100644 --- a/src/line/bot-handlers.ts +++ b/src/line/bot-handlers.ts @@ -74,6 +74,7 @@ async function sendLinePairingReply(params: { const { code, created } = await upsertChannelPairingRequest({ channel: "line", id: senderId, + accountId: context.account.accountId, }); if (!created) { return; @@ -121,7 +122,11 @@ async function shouldProcessLineEvent( const senderId = userId ?? ""; const dmPolicy = account.config.dmPolicy ?? "pairing"; - const storeAllowFrom = await readChannelAllowFromStore("line").catch(() => []); + const storeAllowFrom = await readChannelAllowFromStore( + "line", + process.env, + account.accountId, + ).catch(() => []); const effectiveDmAllow = normalizeDmAllowFromWithStore({ allowFrom: account.config.allowFrom, storeAllowFrom, diff --git a/src/pairing/pairing-store.test.ts b/src/pairing/pairing-store.test.ts index 130a8dc38..9f0ba5357 100644 --- a/src/pairing/pairing-store.test.ts +++ b/src/pairing/pairing-store.test.ts @@ -4,12 +4,15 @@ import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { resolveOAuthDir } from "../config/paths.js"; +import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; import { withEnvAsync } from "../test-utils/env.js"; import { addChannelAllowFromStoreEntry, approveChannelPairingCode, listChannelPairingRequests, readChannelAllowFromStore, + readLegacyChannelAllowFromStore, + readLegacyChannelAllowFromStoreSync, readChannelAllowFromStoreSync, removeChannelAllowFromStoreEntry, upsertChannelPairingRequest, @@ -69,10 +72,12 @@ describe("pairing store", () => { const first = await upsertChannelPairingRequest({ channel: "discord", id: "u1", + accountId: DEFAULT_ACCOUNT_ID, }); const second = await upsertChannelPairingRequest({ channel: "discord", id: "u1", + accountId: DEFAULT_ACCOUNT_ID, }); expect(first.created).toBe(true); expect(second.created).toBe(false); @@ -89,6 +94,7 @@ describe("pairing store", () => { const created = await upsertChannelPairingRequest({ channel: "signal", id: "+15550001111", + accountId: DEFAULT_ACCOUNT_ID, }); expect(created.created).toBe(true); @@ -111,6 +117,7 @@ describe("pairing store", () => { const next = await upsertChannelPairingRequest({ channel: "signal", id: "+15550001111", + accountId: DEFAULT_ACCOUNT_ID, }); expect(next.created).toBe(true); }); @@ -128,6 +135,7 @@ describe("pairing store", () => { const first = await upsertChannelPairingRequest({ channel: "telegram", id: "123", + accountId: DEFAULT_ACCOUNT_ID, }); expect(first.code).toBe("AAAAAAAA"); @@ -137,6 +145,7 @@ describe("pairing store", () => { const second = await upsertChannelPairingRequest({ channel: "telegram", id: "456", + accountId: DEFAULT_ACCOUNT_ID, }); expect(second.code).toBe("BBBBBBBB"); } finally { @@ -152,6 +161,7 @@ describe("pairing store", () => { const created = await upsertChannelPairingRequest({ channel: "whatsapp", id, + accountId: DEFAULT_ACCOUNT_ID, }); expect(created.created).toBe(true); } @@ -159,6 +169,7 @@ describe("pairing store", () => { const blocked = await upsertChannelPairingRequest({ channel: "whatsapp", id: "+15550000004", + accountId: DEFAULT_ACCOUNT_ID, }); expect(blocked.created).toBe(false); @@ -181,7 +192,7 @@ describe("pairing store", () => { }); const accountScoped = await readChannelAllowFromStore("telegram", process.env, "yy"); - const channelScoped = await readChannelAllowFromStore("telegram"); + const channelScoped = await readLegacyChannelAllowFromStore("telegram"); expect(accountScoped).toContain("12345"); expect(channelScoped).not.toContain("12345"); }); @@ -203,7 +214,7 @@ describe("pairing store", () => { expect(approved?.id).toBe("12345"); const accountScoped = await readChannelAllowFromStore("telegram", process.env, "yy"); - const channelScoped = await readChannelAllowFromStore("telegram"); + const channelScoped = await readLegacyChannelAllowFromStore("telegram"); expect(accountScoped).toContain("12345"); expect(channelScoped).not.toContain("12345"); }); @@ -278,7 +289,7 @@ describe("pairing store", () => { }); const scoped = readChannelAllowFromStoreSync("telegram", process.env, "yy"); - const channelScoped = readChannelAllowFromStoreSync("telegram"); + const channelScoped = readLegacyChannelAllowFromStoreSync("telegram"); expect(scoped).toEqual(["1002", "1001"]); expect(channelScoped).toEqual(["1001"]); }); @@ -380,7 +391,7 @@ describe("pairing store", () => { allowFrom: ["1002"], }); - const scoped = await readChannelAllowFromStore("telegram", process.env, "default"); + const scoped = await readChannelAllowFromStore("telegram", process.env, DEFAULT_ACCOUNT_ID); expect(scoped).toEqual(["1002", "1001"]); }); }); diff --git a/src/pairing/pairing-store.ts b/src/pairing/pairing-store.ts index d6a8b9e6c..fe373b3ea 100644 --- a/src/pairing/pairing-store.ts +++ b/src/pairing/pairing-store.ts @@ -8,6 +8,7 @@ import { resolveOAuthDir, resolveStateDir } from "../config/paths.js"; import { withFileLock as withPathLock } from "../infra/file-lock.js"; import { resolveRequiredHomeDir } from "../infra/home-dir.js"; import { readJsonFileWithFallback, writeJsonFileAtomically } from "../plugin-sdk/json-store.js"; +import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; const PAIRING_CODE_LENGTH = 8; const PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; @@ -221,7 +222,7 @@ function requestMatchesAccountId(entry: PairingRequest, normalizedAccountId: str function shouldIncludeLegacyAllowFromEntries(normalizedAccountId: string): boolean { // Keep backward compatibility for legacy channel-scoped allowFrom only on default account. // Non-default accounts should remain isolated to avoid cross-account implicit approvals. - return !normalizedAccountId || normalizedAccountId === "default"; + return !normalizedAccountId || normalizedAccountId === DEFAULT_ACCOUNT_ID; } function normalizeId(value: string | number): string { @@ -383,25 +384,30 @@ async function updateAllowFromStoreEntry(params: { ); } +export async function readLegacyChannelAllowFromStore( + channel: PairingChannel, + env: NodeJS.ProcessEnv = process.env, +): Promise { + const filePath = resolveAllowFromPath(channel, env); + return await readAllowFromStateForPath(channel, filePath); +} + export async function readChannelAllowFromStore( channel: PairingChannel, env: NodeJS.ProcessEnv = process.env, - accountId?: string, + accountId: string, ): Promise { - const normalizedAccountId = accountId?.trim().toLowerCase() ?? ""; - if (!normalizedAccountId) { - const filePath = resolveAllowFromPath(channel, env); - return await readAllowFromStateForPath(channel, filePath); - } + const normalizedAccountId = accountId.trim().toLowerCase(); + const resolvedAccountId = normalizedAccountId || DEFAULT_ACCOUNT_ID; - if (!shouldIncludeLegacyAllowFromEntries(normalizedAccountId)) { + if (!shouldIncludeLegacyAllowFromEntries(resolvedAccountId)) { return await readNonDefaultAccountAllowFrom({ channel, env, - accountId: normalizedAccountId, + accountId: resolvedAccountId, }); } - const scopedPath = resolveAllowFromPath(channel, env, accountId); + const scopedPath = resolveAllowFromPath(channel, env, resolvedAccountId); const scopedEntries = await readAllowFromStateForPath(channel, scopedPath); // Backward compatibility: legacy channel-level allowFrom store was unscoped. // Keep honoring it for default account to prevent re-pair prompts after upgrades. @@ -410,25 +416,30 @@ export async function readChannelAllowFromStore( return dedupePreserveOrder([...scopedEntries, ...legacyEntries]); } +export function readLegacyChannelAllowFromStoreSync( + channel: PairingChannel, + env: NodeJS.ProcessEnv = process.env, +): string[] { + const filePath = resolveAllowFromPath(channel, env); + return readAllowFromStateForPathSync(channel, filePath); +} + export function readChannelAllowFromStoreSync( channel: PairingChannel, env: NodeJS.ProcessEnv = process.env, - accountId?: string, + accountId: string, ): string[] { - const normalizedAccountId = accountId?.trim().toLowerCase() ?? ""; - if (!normalizedAccountId) { - const filePath = resolveAllowFromPath(channel, env); - return readAllowFromStateForPathSync(channel, filePath); - } + const normalizedAccountId = accountId.trim().toLowerCase(); + const resolvedAccountId = normalizedAccountId || DEFAULT_ACCOUNT_ID; - if (!shouldIncludeLegacyAllowFromEntries(normalizedAccountId)) { + if (!shouldIncludeLegacyAllowFromEntries(resolvedAccountId)) { return readNonDefaultAccountAllowFromSync({ channel, env, - accountId: normalizedAccountId, + accountId: resolvedAccountId, }); } - const scopedPath = resolveAllowFromPath(channel, env, accountId); + const scopedPath = resolveAllowFromPath(channel, env, resolvedAccountId); const scopedEntries = readAllowFromStateForPathSync(channel, scopedPath); const legacyPath = resolveAllowFromPath(channel, env); const legacyEntries = readAllowFromStateForPathSync(channel, legacyPath); @@ -537,7 +548,7 @@ export async function listChannelPairingRequests( export async function upsertChannelPairingRequest(params: { channel: PairingChannel; id: string | number; - accountId?: string; + accountId: string; meta?: Record; env?: NodeJS.ProcessEnv; /** Extension channels can pass their adapter directly to bypass registry lookup. */ @@ -552,7 +563,7 @@ export async function upsertChannelPairingRequest(params: { const now = new Date().toISOString(); const nowMs = Date.now(); const id = normalizeId(params.id); - const normalizedAccountId = params.accountId?.trim(); + const normalizedAccountId = normalizePairingAccountId(params.accountId) || DEFAULT_ACCOUNT_ID; const baseMeta = params.meta && typeof params.meta === "object" ? Object.fromEntries( @@ -561,7 +572,7 @@ export async function upsertChannelPairingRequest(params: { .filter(([_, v]) => Boolean(v)), ) : undefined; - const meta = normalizedAccountId ? { ...baseMeta, accountId: normalizedAccountId } : baseMeta; + const meta = { ...baseMeta, accountId: normalizedAccountId }; let reqs = await readPairingRequests(filePath); const { requests: prunedExpired, removed: expiredRemoved } = pruneExpiredRequests( @@ -569,7 +580,7 @@ export async function upsertChannelPairingRequest(params: { nowMs, ); reqs = prunedExpired; - const normalizedMatchingAccountId = normalizePairingAccountId(normalizedAccountId); + const normalizedMatchingAccountId = normalizedAccountId; const existingIdx = reqs.findIndex((r) => { if (r.id !== id) { return false; diff --git a/src/plugins/runtime/index.ts b/src/plugins/runtime/index.ts index aa29294f7..cba4e9f6d 100644 --- a/src/plugins/runtime/index.ts +++ b/src/plugins/runtime/index.ts @@ -317,8 +317,17 @@ function createRuntimeChannel(): PluginRuntime["channel"] { }, pairing: { buildPairingReply, - readAllowFromStore: readChannelAllowFromStore, - upsertPairingRequest: upsertChannelPairingRequest, + readAllowFromStore: ({ channel, accountId, env }) => + readChannelAllowFromStore(channel, env, accountId), + upsertPairingRequest: ({ channel, id, accountId, meta, env, pairingAdapter }) => + upsertChannelPairingRequest({ + channel, + id, + accountId, + meta, + env, + pairingAdapter, + }), }, media: { fetchRemoteMedia, diff --git a/src/plugins/runtime/types.ts b/src/plugins/runtime/types.ts index 0e2c20cf7..39ada4cd4 100644 --- a/src/plugins/runtime/types.ts +++ b/src/plugins/runtime/types.ts @@ -14,6 +14,14 @@ type ReadChannelAllowFromStore = typeof import("../../pairing/pairing-store.js").readChannelAllowFromStore; type UpsertChannelPairingRequest = typeof import("../../pairing/pairing-store.js").upsertChannelPairingRequest; +type ReadChannelAllowFromStoreForAccount = (params: { + channel: Parameters[0]; + accountId: string; + env?: Parameters[1]; +}) => ReturnType; +type UpsertChannelPairingRequestForAccount = ( + params: Omit[0], "accountId"> & { accountId: string }, +) => ReturnType; type FetchRemoteMedia = typeof import("../../media/fetch.js").fetchRemoteMedia; type SaveMediaBuffer = typeof import("../../media/store.js").saveMediaBuffer; type TextToSpeechTelephony = typeof import("../../tts/tts.js").textToSpeechTelephony; @@ -235,8 +243,8 @@ export type PluginRuntime = { }; pairing: { buildPairingReply: BuildPairingReply; - readAllowFromStore: ReadChannelAllowFromStore; - upsertPairingRequest: UpsertChannelPairingRequest; + readAllowFromStore: ReadChannelAllowFromStoreForAccount; + upsertPairingRequest: UpsertChannelPairingRequestForAccount; }; media: { fetchRemoteMedia: FetchRemoteMedia; diff --git a/src/security/audit-channel.ts b/src/security/audit-channel.ts index dcf344891..551437ffd 100644 --- a/src/security/audit-channel.ts +++ b/src/security/audit-channel.ts @@ -115,6 +115,7 @@ export async function collectChannelSecurityFindings(params: { const warnDmPolicy = async (input: { label: string; provider: ChannelId; + accountId: string; dmPolicy: string; allowFrom?: Array | null; policyPath?: string; @@ -124,6 +125,7 @@ export async function collectChannelSecurityFindings(params: { const policyPath = input.policyPath ?? `${input.allowFromPath}policy`; const { hasWildcard, isMultiUserDm } = await resolveDmAllowState({ provider: input.provider, + accountId: input.accountId, allowFrom: input.allowFrom, normalizeEntry: input.normalizeEntry, }); @@ -224,7 +226,11 @@ export async function collectChannelSecurityFindings(params: { (account as { config?: Record } | null)?.config ?? ({} as Record); const dangerousNameMatchingEnabled = isDangerousNameMatchingEnabled(discordCfg); - const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []); + const storeAllowFrom = await readChannelAllowFromStore( + "discord", + process.env, + accountId, + ).catch(() => []); const discordNameBasedAllowEntries = new Set(); const discordPathPrefix = orderedAccountIds.length > 1 || hasExplicitAccountPath @@ -427,7 +433,11 @@ export async function collectChannelSecurityFindings(params: { : Array.isArray(legacyAllowFromRaw) ? legacyAllowFromRaw : []; - const storeAllowFrom = await readChannelAllowFromStore("slack").catch(() => []); + const storeAllowFrom = await readChannelAllowFromStore( + "slack", + process.env, + accountId, + ).catch(() => []); const ownerAllowFromConfigured = normalizeAllowFromList([...allowFrom, ...storeAllowFrom]).length > 0; const channels = (slackCfg.channels as Record | undefined) ?? {}; @@ -462,6 +472,7 @@ export async function collectChannelSecurityFindings(params: { await warnDmPolicy({ label: plugin.meta.label ?? plugin.id, provider: plugin.id, + accountId, dmPolicy: dmPolicy.policy, allowFrom: dmPolicy.allowFrom, policyPath: dmPolicy.policyPath, @@ -513,7 +524,11 @@ export async function collectChannelSecurityFindings(params: { continue; } - const storeAllowFrom = await readChannelAllowFromStore("telegram").catch(() => []); + const storeAllowFrom = await readChannelAllowFromStore( + "telegram", + process.env, + accountId, + ).catch(() => []); const storeHasWildcard = storeAllowFrom.some((v) => String(v).trim() === "*"); const invalidTelegramAllowFromEntries = new Set(); for (const entry of storeAllowFrom) { diff --git a/src/security/dm-policy-shared.test.ts b/src/security/dm-policy-shared.test.ts index 636e0e6de..b68489222 100644 --- a/src/security/dm-policy-shared.test.ts +++ b/src/security/dm-policy-shared.test.ts @@ -13,9 +13,10 @@ describe("security/dm-policy-shared", () => { it("normalizes config + store allow entries and counts distinct senders", async () => { const state = await resolveDmAllowState({ provider: "telegram", + accountId: "default", allowFrom: [" * ", " alice ", "ALICE", "bob"], normalizeEntry: (value) => value.toLowerCase(), - readStore: async () => [" Bob ", "carol", ""], + readStore: async (_provider, _accountId) => [" Bob ", "carol", ""], }); expect(state.configAllowFrom).toEqual(["*", "alice", "ALICE", "bob"]); expect(state.hasWildcard).toBe(true); @@ -26,8 +27,9 @@ describe("security/dm-policy-shared", () => { it("handles empty allowlists and store failures", async () => { const state = await resolveDmAllowState({ provider: "slack", + accountId: "default", allowFrom: undefined, - readStore: async () => { + readStore: async (_provider, _accountId) => { throw new Error("offline"); }, }); @@ -41,8 +43,9 @@ describe("security/dm-policy-shared", () => { let called = false; const storeAllowFrom = await readStoreAllowFromForDmPolicy({ provider: "telegram", + accountId: "default", dmPolicy: "allowlist", - readStore: async () => { + readStore: async (_provider, _accountId) => { called = true; return ["should-not-be-read"]; }, @@ -55,8 +58,9 @@ describe("security/dm-policy-shared", () => { let called = false; const storeAllowFrom = await readStoreAllowFromForDmPolicy({ provider: "slack", + accountId: "default", shouldRead: false, - readStore: async () => { + readStore: async (_provider, _accountId) => { called = true; return ["should-not-be-read"]; }, diff --git a/src/security/dm-policy-shared.ts b/src/security/dm-policy-shared.ts index 6d5a45413..35c9fceaf 100644 --- a/src/security/dm-policy-shared.ts +++ b/src/security/dm-policy-shared.ts @@ -52,14 +52,19 @@ export type DmGroupAccessReasonCode = export async function readStoreAllowFromForDmPolicy(params: { provider: ChannelId; + accountId: string; dmPolicy?: string | null; shouldRead?: boolean | null; - readStore?: (provider: ChannelId) => Promise; + readStore?: (provider: ChannelId, accountId: string) => Promise; }): Promise { if (params.shouldRead === false || params.dmPolicy === "allowlist") { return []; } - return await (params.readStore ?? readChannelAllowFromStore)(params.provider).catch(() => []); + const readStore = + params.readStore ?? + ((provider: ChannelId, accountId: string) => + readChannelAllowFromStore(provider, process.env, accountId)); + return await readStore(params.provider, params.accountId).catch(() => []); } export function resolveDmGroupAccessDecision(params: { @@ -258,9 +263,10 @@ export function resolveDmGroupAccessWithCommandGate(params: { export async function resolveDmAllowState(params: { provider: ChannelId; + accountId: string; allowFrom?: Array | null; normalizeEntry?: (raw: string) => string; - readStore?: (provider: ChannelId) => Promise; + readStore?: (provider: ChannelId, accountId: string) => Promise; }): Promise<{ configAllowFrom: string[]; hasWildcard: boolean; @@ -273,6 +279,7 @@ export async function resolveDmAllowState(params: { const hasWildcard = configAllowFrom.includes("*"); const storeAllowFrom = await readStoreAllowFromForDmPolicy({ provider: params.provider, + accountId: params.accountId, readStore: params.readStore, }); const normalizeEntry = params.normalizeEntry ?? ((value: string) => value); diff --git a/src/security/fix.ts b/src/security/fix.ts index 6de16b088..d0c86e528 100644 --- a/src/security/fix.ts +++ b/src/security/fix.ts @@ -7,7 +7,7 @@ import { collectIncludePathsRecursive } from "../config/includes-scan.js"; import { resolveConfigPath, resolveOAuthDir, resolveStateDir } from "../config/paths.js"; import { readChannelAllowFromStore } from "../pairing/pairing-store.js"; import { runExec } from "../process/exec.js"; -import { normalizeAgentId } from "../routing/session-key.js"; +import { DEFAULT_ACCOUNT_ID, normalizeAgentId } from "../routing/session-key.js"; import { createIcaclsResetCommand, formatIcaclsResetCommand, type ExecFn } from "./windows-acl.js"; export type SecurityFixChmodAction = { @@ -412,7 +412,11 @@ export async function fixSecurityFootguns(opts?: { const fixed = applyConfigFixes({ cfg: snap.config, env }); changes = fixed.changes; - const whatsappStoreAllowFrom = await readChannelAllowFromStore("whatsapp", env).catch(() => []); + const whatsappStoreAllowFrom = await readChannelAllowFromStore( + "whatsapp", + env, + DEFAULT_ACCOUNT_ID, + ).catch(() => []); if (whatsappStoreAllowFrom.length > 0) { setWhatsAppGroupAllowFromFromStore({ cfg: fixed.cfg, diff --git a/src/signal/monitor/event-handler.ts b/src/signal/monitor/event-handler.ts index 5691446bd..f5e89d8cb 100644 --- a/src/signal/monitor/event-handler.ts +++ b/src/signal/monitor/event-handler.ts @@ -31,10 +31,7 @@ import { danger, logVerbose, shouldLogVerbose } from "../../globals.js"; import { enqueueSystemEvent } from "../../infra/system-events.js"; import { mediaKindFromMime } from "../../media/constants.js"; import { buildPairingReply } from "../../pairing/pairing-messages.js"; -import { - readChannelAllowFromStore, - upsertChannelPairingRequest, -} from "../../pairing/pairing-store.js"; +import { upsertChannelPairingRequest } from "../../pairing/pairing-store.js"; import { resolveAgentRoute } from "../../routing/resolve-route.js"; import { DM_GROUP_ACCESS_REASON, @@ -459,8 +456,8 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { const senderDisplay = formatSignalSenderDisplay(sender); const storeAllowFrom = await readStoreAllowFromForDmPolicy({ provider: "signal", + accountId: deps.accountId, dmPolicy: deps.dmPolicy, - readStore: (provider) => readChannelAllowFromStore(provider), }); const resolveAccessDecision = (isGroup: boolean) => resolveDmGroupAccessWithLists({ @@ -517,6 +514,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { const { code, created } = await upsertChannelPairingRequest({ channel: "signal", id: senderId, + accountId: deps.accountId, meta: { name: envelope.sourceName ?? undefined }, }); if (created) { diff --git a/src/slack/monitor/auth.ts b/src/slack/monitor/auth.ts index 238a32d7e..421bc084d 100644 --- a/src/slack/monitor/auth.ts +++ b/src/slack/monitor/auth.ts @@ -1,4 +1,3 @@ -import { readChannelAllowFromStore } from "../../pairing/pairing-store.js"; import { readStoreAllowFromForDmPolicy } from "../../security/dm-policy-shared.js"; import { allowListMatches, @@ -17,8 +16,8 @@ export async function resolveSlackEffectiveAllowFrom( const storeAllowFrom = includePairingStore ? await readStoreAllowFromForDmPolicy({ provider: "slack", + accountId: ctx.accountId, dmPolicy: ctx.dmPolicy, - readStore: (provider) => readChannelAllowFromStore(provider), }) : []; const allowFrom = normalizeAllowList([...ctx.allowFrom, ...storeAllowFrom]); diff --git a/src/slack/monitor/message-handler/prepare.ts b/src/slack/monitor/message-handler/prepare.ts index 2cc26b41f..7b9f9f9d5 100644 --- a/src/slack/monitor/message-handler/prepare.ts +++ b/src/slack/monitor/message-handler/prepare.ts @@ -155,6 +155,7 @@ export async function prepareSlackMessage(params: { const { code, created } = await upsertChannelPairingRequest({ channel: "slack", id: directUserId, + accountId: account.accountId, meta: { name: senderName }, }); if (created) { diff --git a/src/slack/monitor/slash.ts b/src/slack/monitor/slash.ts index 0f4a5e161..7567609ae 100644 --- a/src/slack/monitor/slash.ts +++ b/src/slack/monitor/slash.ts @@ -6,10 +6,7 @@ import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command- import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../../config/commands.js"; import { danger, logVerbose } from "../../globals.js"; import { buildPairingReply } from "../../pairing/pairing-messages.js"; -import { - readChannelAllowFromStore, - upsertChannelPairingRequest, -} from "../../pairing/pairing-store.js"; +import { upsertChannelPairingRequest } from "../../pairing/pairing-store.js"; import { readStoreAllowFromForDmPolicy } from "../../security/dm-policy-shared.js"; import { chunkItems } from "../../utils/chunk-items.js"; import type { ResolvedSlackAccount } from "../accounts.js"; @@ -339,8 +336,8 @@ export async function registerSlackMonitorSlashCommands(params: { const storeAllowFrom = isDirectMessage ? await readStoreAllowFromForDmPolicy({ provider: "slack", + accountId: ctx.accountId, dmPolicy: ctx.dmPolicy, - readStore: (provider) => readChannelAllowFromStore(provider), }) : []; const effectiveAllowFrom = normalizeAllowList([...ctx.allowFrom, ...storeAllowFrom]); @@ -373,6 +370,7 @@ export async function registerSlackMonitorSlashCommands(params: { const { code, created } = await upsertChannelPairingRequest({ channel: "slack", id: command.user_id, + accountId: ctx.accountId, meta: { name: senderName }, }); if (created) { diff --git a/src/telegram/bot/helpers.ts b/src/telegram/bot/helpers.ts index ebfe36fba..11d9798e2 100644 --- a/src/telegram/bot/helpers.ts +++ b/src/telegram/bot/helpers.ts @@ -3,6 +3,7 @@ import { formatLocationText, type NormalizedLocation } from "../../channels/loca import { resolveTelegramPreviewStreamMode } from "../../config/discord-preview-streaming.js"; import type { TelegramGroupConfig, TelegramTopicConfig } from "../../config/types.js"; import { readChannelAllowFromStore } from "../../pairing/pairing-store.js"; +import { normalizeAccountId } from "../../routing/session-key.js"; import { firstDefined, normalizeAllowFrom, type NormalizedAllowFrom } from "../bot-access.js"; import type { TelegramStreamMode } from "./types.js"; @@ -32,15 +33,14 @@ export async function resolveTelegramGroupAllowFromContext(params: { effectiveGroupAllow: NormalizedAllowFrom; hasGroupAllowOverride: boolean; }> { + const accountId = normalizeAccountId(params.accountId); const resolvedThreadId = resolveTelegramForumThreadId({ isForum: params.isForum, messageThreadId: params.messageThreadId, }); - const storeAllowFrom = await readChannelAllowFromStore( - "telegram", - process.env, - params.accountId, - ).catch(() => []); + const storeAllowFrom = await readChannelAllowFromStore("telegram", process.env, accountId).catch( + () => [], + ); const { groupConfig, topicConfig } = params.resolveTelegramGroupConfig( params.chatId, resolvedThreadId, diff --git a/src/web/auto-reply/monitor/process-message.ts b/src/web/auto-reply/monitor/process-message.ts index ce84d1352..2e49e9c79 100644 --- a/src/web/auto-reply/monitor/process-message.ts +++ b/src/web/auto-reply/monitor/process-message.ts @@ -25,7 +25,6 @@ import { import { logVerbose, shouldLogVerbose } from "../../../globals.js"; import type { getChildLogger } from "../../../logging.js"; import { getAgentScopedMediaLocalRoots } from "../../../media/local-roots.js"; -import { readChannelAllowFromStore } from "../../../pairing/pairing-store.js"; import type { resolveAgentRoute } from "../../../routing/resolve-route.js"; import { readStoreAllowFromForDmPolicy, @@ -80,9 +79,8 @@ async function resolveWhatsAppCommandAuthorized(params: { ? [] : await readStoreAllowFromForDmPolicy({ provider: "whatsapp", + accountId: params.msg.accountId, dmPolicy, - readStore: (provider) => - readChannelAllowFromStore(provider, process.env, params.msg.accountId), }); const dmAllowFrom = configuredAllowFrom.length > 0 diff --git a/src/web/inbound/access-control.ts b/src/web/inbound/access-control.ts index bb160403e..2363434f3 100644 --- a/src/web/inbound/access-control.ts +++ b/src/web/inbound/access-control.ts @@ -6,10 +6,7 @@ import { } from "../../config/runtime-group-policy.js"; import { logVerbose } from "../../globals.js"; import { buildPairingReply } from "../../pairing/pairing-messages.js"; -import { - readChannelAllowFromStore, - upsertChannelPairingRequest, -} from "../../pairing/pairing-store.js"; +import { upsertChannelPairingRequest } from "../../pairing/pairing-store.js"; import { readStoreAllowFromForDmPolicy, resolveDmGroupAccessWithLists, @@ -66,8 +63,8 @@ export async function checkInboundAccessControl(params: { const configuredAllowFrom = account.allowFrom ?? []; const storeAllowFrom = await readStoreAllowFromForDmPolicy({ provider: "whatsapp", + accountId: account.accountId, dmPolicy, - readStore: (provider) => readChannelAllowFromStore(provider, process.env, account.accountId), }); // Without user config, default to self-only DM access so the owner can talk to themselves. const defaultAllowFrom =