VYPR
Low severityNVD Advisory· Published Mar 21, 2026· Updated Mar 24, 2026

OpenClaw < 2026.2.26 - Cross-Account Authorization Bypass in DM Pairing Store

CVE-2026-32067

Description

OpenClaw versions prior to 2026.2.26 contains an authorization bypass vulnerability in the pairing-store access control for direct message pairing policy that allows attackers to reuse pairing approvals across multiple accounts. An attacker approved as a sender in one account can be automatically accepted in another account in multi-account deployments without explicit approval, bypassing authorization boundaries.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.2.262026.2.26

Affected products

1

Patches

2
bce643a0bd14

refactor(security): enforce account-scoped pairing APIs

https://github.com/openclaw/openclawPeter SteinbergerFeb 26, 2026via ghsa
27 files changed · +331 94
  • package.json+2 1 modified
    @@ -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",
    
  • scripts/check-pairing-account-scope.mjs+157 0 added
    @@ -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);
    +  });
    +}
    
  • src/auto-reply/reply/commands-allowlist.ts+1 1 modified
    @@ -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[] = [];
    
  • src/channels/plugins/whatsapp-heartbeat.ts+6 1 modified
    @@ -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]);
    
  • src/commands/doctor-security.ts+3 0 modified
    @@ -90,6 +90,7 @@ export async function noteSecurityWarnings(cfg: OpenClawConfig) {
       const warnDmPolicy = async (params: {
         label: string;
         provider: ChannelId;
    +    accountId: string;
         dmPolicy: string;
         allowFrom?: Array<string | number> | 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,
    
  • src/cron/isolated-agent/delivery-target.ts+5 3 modified
    @@ -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])];
    
  • src/discord/monitor/agent-components.ts+3 5 modified
    @@ -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,
    
  • src/discord/monitor/listeners.ts+6 2 modified
    @@ -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,
    
  • src/discord/monitor/message-handler.preflight.ts+5 6 modified
    @@ -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,
    
  • src/discord/monitor/native-command.ts+3 5 modified
    @@ -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,
    
  • src/imessage/monitor/monitor-provider.ts+6 1 modified
    @@ -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,
    
  • src/line/bot-handlers.ts+6 1 modified
    @@ -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,
    
  • src/pairing/pairing-store.test.ts+15 4 modified
    @@ -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,13 +161,15 @@ describe("pairing store", () => {
             const created = await upsertChannelPairingRequest({
               channel: "whatsapp",
               id,
    +          accountId: DEFAULT_ACCOUNT_ID,
             });
             expect(created.created).toBe(true);
           }
     
           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"]);
         });
       });
    
  • src/pairing/pairing-store.ts+34 23 modified
    @@ -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<string[]> {
    +  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<string[]> {
    -  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<string, string | undefined | null>;
       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,15 +572,15 @@ 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(
             reqs,
             nowMs,
           );
           reqs = prunedExpired;
    -      const normalizedMatchingAccountId = normalizePairingAccountId(normalizedAccountId);
    +      const normalizedMatchingAccountId = normalizedAccountId;
           const existingIdx = reqs.findIndex((r) => {
             if (r.id !== id) {
               return false;
    
  • src/plugins/runtime/index.ts+11 2 modified
    @@ -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,
    
  • src/plugins/runtime/types.ts+10 2 modified
    @@ -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<ReadChannelAllowFromStore>[0];
    +  accountId: string;
    +  env?: Parameters<ReadChannelAllowFromStore>[1];
    +}) => ReturnType<ReadChannelAllowFromStore>;
    +type UpsertChannelPairingRequestForAccount = (
    +  params: Omit<Parameters<UpsertChannelPairingRequest>[0], "accountId"> & { accountId: string },
    +) => ReturnType<UpsertChannelPairingRequest>;
     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;
    
  • src/security/audit-channel.ts+18 3 modified
    @@ -115,6 +115,7 @@ export async function collectChannelSecurityFindings(params: {
       const warnDmPolicy = async (input: {
         label: string;
         provider: ChannelId;
    +    accountId: string;
         dmPolicy: string;
         allowFrom?: Array<string | number> | 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<string, unknown> } | null)?.config ??
               ({} as Record<string, unknown>);
             const dangerousNameMatchingEnabled = isDangerousNameMatchingEnabled(discordCfg);
    -        const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []);
    +        const storeAllowFrom = await readChannelAllowFromStore(
    +          "discord",
    +          process.env,
    +          accountId,
    +        ).catch(() => []);
             const discordNameBasedAllowEntries = new Set<string>();
             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<string, unknown> | 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<string>();
           for (const entry of storeAllowFrom) {
    
  • src/security/dm-policy-shared.test.ts+8 4 modified
    @@ -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"];
           },
    
  • src/security/dm-policy-shared.ts+10 3 modified
    @@ -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<string[]>;
    +  readStore?: (provider: ChannelId, accountId: string) => Promise<string[]>;
     }): Promise<string[]> {
       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<string | number> | null;
       normalizeEntry?: (raw: string) => string;
    -  readStore?: (provider: ChannelId) => Promise<string[]>;
    +  readStore?: (provider: ChannelId, accountId: string) => Promise<string[]>;
     }): 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);
    
  • src/security/fix.ts+6 2 modified
    @@ -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,
    
  • src/signal/monitor/event-handler.ts+3 5 modified
    @@ -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) {
    
  • src/slack/monitor/auth.ts+1 2 modified
    @@ -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]);
    
  • src/slack/monitor/message-handler/prepare.ts+1 0 modified
    @@ -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) {
    
  • src/slack/monitor/slash.ts+3 5 modified
    @@ -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) {
    
  • src/telegram/bot/helpers.ts+5 5 modified
    @@ -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,
    
  • src/web/auto-reply/monitor/process-message.ts+1 3 modified
    @@ -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
    
  • src/web/inbound/access-control.ts+2 5 modified
    @@ -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 =
    
a0c5e28f3bf0

refactor(extensions): use scoped pairing helper

https://github.com/openclaw/openclawPeter SteinbergerFeb 26, 2026via ghsa
12 files changed · +135 32
  • extensions/bluebubbles/src/monitor-processing.ts+16 4 modified
    @@ -1,6 +1,7 @@
     import type { OpenClawConfig } from "openclaw/plugin-sdk";
     import {
       DM_GROUP_ACCESS_REASON,
    +  createScopedPairingAccess,
       createReplyPrefixOptions,
       evictOldHistoryKeys,
       logAckFailure,
    @@ -421,6 +422,11 @@ export async function processMessage(
       target: WebhookTarget,
     ): Promise<void> {
       const { account, config, runtime, core, statusSink } = target;
    +  const pairing = createScopedPairingAccess({
    +    core,
    +    channel: "bluebubbles",
    +    accountId: account.accountId,
    +  });
       const privateApiEnabled = isBlueBubblesPrivateApiEnabled(account.accountId);
     
       const groupFlag = resolveGroupFlagFromChatGuid(message.chatGuid);
    @@ -505,8 +511,9 @@ export async function processMessage(
       const configuredAllowFrom = (account.config.allowFrom ?? []).map((entry) => String(entry));
       const storeAllowFrom = await readStoreAllowFromForDmPolicy({
         provider: "bluebubbles",
    +    accountId: account.accountId,
         dmPolicy,
    -    readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
    +    readStore: pairing.readStoreForDmPolicy,
       });
       const accessDecision = resolveDmGroupAccessWithLists({
         isGroup,
    @@ -587,8 +594,7 @@ export async function processMessage(
         }
     
         if (accessDecision.decision === "pairing") {
    -      const { code, created } = await core.channel.pairing.upsertPairingRequest({
    -        channel: "bluebubbles",
    +      const { code, created } = await pairing.upsertPairingRequest({
             id: message.senderId,
             meta: { name: message.senderName },
           });
    @@ -1381,6 +1387,11 @@ export async function processReaction(
       target: WebhookTarget,
     ): Promise<void> {
       const { account, config, runtime, core } = target;
    +  const pairing = createScopedPairingAccess({
    +    core,
    +    channel: "bluebubbles",
    +    accountId: account.accountId,
    +  });
       if (reaction.fromMe) {
         return;
       }
    @@ -1389,8 +1400,9 @@ export async function processReaction(
       const groupPolicy = account.config.groupPolicy ?? "allowlist";
       const storeAllowFrom = await readStoreAllowFromForDmPolicy({
         provider: "bluebubbles",
    +    accountId: account.accountId,
         dmPolicy,
    -    readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
    +    readStore: pairing.readStoreForDmPolicy,
       });
       const accessDecision = resolveDmGroupAccessWithLists({
         isGroup: reaction.isGroup,
    
  • extensions/feishu/src/bot.ts+8 3 modified
    @@ -3,6 +3,7 @@ import {
       buildAgentMediaPayload,
       buildPendingHistoryContextFromMap,
       clearHistoryEntriesIfEnabled,
    +  createScopedPairingAccess,
       DEFAULT_GROUP_HISTORY_LIMIT,
       type HistoryEntry,
       recordPendingHistoryEntryIfEnabled,
    @@ -675,6 +676,11 @@ export async function handleFeishuMessage(params: {
     
       try {
         const core = getFeishuRuntime();
    +    const pairing = createScopedPairingAccess({
    +      core,
    +      channel: "feishu",
    +      accountId: account.accountId,
    +    });
         const shouldComputeCommandAuthorized = core.channel.commands.shouldComputeCommandAuthorized(
           ctx.content,
           cfg,
    @@ -683,7 +689,7 @@ export async function handleFeishuMessage(params: {
           !isGroup &&
           dmPolicy !== "allowlist" &&
           (dmPolicy !== "open" || shouldComputeCommandAuthorized)
    -        ? await core.channel.pairing.readAllowFromStore("feishu").catch(() => [])
    +        ? await pairing.readAllowFromStore().catch(() => [])
             : [];
         const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom];
         const dmAllowed = resolveFeishuAllowlistMatch({
    @@ -695,8 +701,7 @@ export async function handleFeishuMessage(params: {
     
         if (!isGroup && dmPolicy !== "open" && !dmAllowed) {
           if (dmPolicy === "pairing") {
    -        const { code, created } = await core.channel.pairing.upsertPairingRequest({
    -          channel: "feishu",
    +        const { code, created } = await pairing.upsertPairingRequest({
               id: ctx.senderOpenId,
               meta: { name: ctx.senderName },
             });
    
  • extensions/googlechat/src/monitor.ts+8 3 modified
    @@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
     import type { OpenClawConfig } from "openclaw/plugin-sdk";
     import {
       GROUP_POLICY_BLOCKED_LABEL,
    +  createScopedPairingAccess,
       createReplyPrefixOptions,
       readJsonBodyWithLimit,
       registerWebhookTarget,
    @@ -396,6 +397,11 @@ async function processMessageWithPipeline(params: {
       mediaMaxMb: number;
     }): Promise<void> {
       const { event, account, config, runtime, core, statusSink, mediaMaxMb } = params;
    +  const pairing = createScopedPairingAccess({
    +    core,
    +    channel: "googlechat",
    +    accountId: account.accountId,
    +  });
       const space = event.space;
       const message = event.message;
       if (!space || !message) {
    @@ -514,7 +520,7 @@ async function processMessageWithPipeline(params: {
       const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(rawBody, config);
       const storeAllowFrom =
         !isGroup && dmPolicy !== "allowlist" && (dmPolicy !== "open" || shouldComputeAuth)
    -      ? await core.channel.pairing.readAllowFromStore("googlechat").catch(() => [])
    +      ? await pairing.readAllowFromStore().catch(() => [])
           : [];
       const access = resolveDmGroupAccessWithLists({
         isGroup,
    @@ -590,8 +596,7 @@ async function processMessageWithPipeline(params: {
     
         if (access.decision !== "allow") {
           if (access.decision === "pairing") {
    -        const { code, created } = await core.channel.pairing.upsertPairingRequest({
    -          channel: "googlechat",
    +        const { code, created } = await pairing.upsertPairingRequest({
               id: senderId,
               meta: { name: senderName || undefined, email: senderEmail },
             });
    
  • extensions/irc/src/inbound.ts+9 3 modified
    @@ -1,5 +1,6 @@
     import {
       GROUP_POLICY_BLOCKED_LABEL,
    +  createScopedPairingAccess,
       createNormalizedOutboundDeliverer,
       createReplyPrefixOptions,
       formatTextWithAttachmentLinks,
    @@ -90,6 +91,11 @@ export async function handleIrcInbound(params: {
     }): Promise<void> {
       const { message, account, config, runtime, connectedNick, statusSink } = params;
       const core = getIrcRuntime();
    +  const pairing = createScopedPairingAccess({
    +    core,
    +    channel: CHANNEL_ID,
    +    accountId: account.accountId,
    +  });
     
       const rawBody = message.text?.trim() ?? "";
       if (!rawBody) {
    @@ -123,8 +129,9 @@ export async function handleIrcInbound(params: {
       const configGroupAllowFrom = normalizeIrcAllowlist(account.config.groupAllowFrom);
       const storeAllowFrom = await readStoreAllowFromForDmPolicy({
         provider: CHANNEL_ID,
    +    accountId: account.accountId,
         dmPolicy,
    -    readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
    +    readStore: pairing.readStoreForDmPolicy,
       });
       const storeAllowList = normalizeIrcAllowlist(storeAllowFrom);
     
    @@ -202,8 +209,7 @@ export async function handleIrcInbound(params: {
           }).allowed;
           if (!dmAllowed) {
             if (dmPolicy === "pairing") {
    -          const { code, created } = await core.channel.pairing.upsertPairingRequest({
    -            channel: CHANNEL_ID,
    +          const { code, created } = await pairing.upsertPairingRequest({
                 id: senderDisplay.toLowerCase(),
                 meta: { name: message.senderNick || undefined },
               });
    
  • extensions/matrix/src/matrix/monitor/handler.ts+11 3 modified
    @@ -1,5 +1,7 @@
     import type { LocationMessageEventContent, MatrixClient } from "@vector-im/matrix-bot-sdk";
     import {
    +  DEFAULT_ACCOUNT_ID,
    +  createScopedPairingAccess,
       createReplyPrefixOptions,
       createTypingCallbacks,
       formatAllowlistMatchMeta,
    @@ -98,6 +100,12 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
         getMemberDisplayName,
         accountId,
       } = params;
    +  const resolvedAccountId = accountId?.trim() || DEFAULT_ACCOUNT_ID;
    +  const pairing = createScopedPairingAccess({
    +    core,
    +    channel: "matrix",
    +    accountId: resolvedAccountId,
    +  });
     
       return async (roomId: string, event: MatrixRawEvent) => {
         try {
    @@ -229,8 +237,9 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
           const storeAllowFrom = isDirectMessage
             ? await readStoreAllowFromForDmPolicy({
                 provider: "matrix",
    +            accountId: resolvedAccountId,
                 dmPolicy,
    -            readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
    +            readStore: pairing.readStoreForDmPolicy,
               })
             : [];
           const groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? [];
    @@ -270,8 +279,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
               });
               const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
               if (access.decision === "pairing") {
    -            const { code, created } = await core.channel.pairing.upsertPairingRequest({
    -              channel: "matrix",
    +            const { code, created } = await pairing.upsertPairingRequest({
                   id: senderId,
                   meta: { name: senderName },
                 });
    
  • extensions/mattermost/src/mattermost/monitor.ts+11 4 modified
    @@ -8,6 +8,7 @@ import type {
     import {
       buildAgentMediaPayload,
       DM_GROUP_ACCESS_REASON,
    +  createScopedPairingAccess,
       createReplyPrefixOptions,
       createTypingCallbacks,
       logInboundDrop,
    @@ -171,6 +172,11 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
         cfg,
         accountId: opts.accountId,
       });
    +  const pairing = createScopedPairingAccess({
    +    core,
    +    channel: "mattermost",
    +    accountId: account.accountId,
    +  });
       const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
       const botToken = opts.botToken?.trim() || account.botToken?.trim();
       if (!botToken) {
    @@ -362,8 +368,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
         const storeAllowFrom = normalizeMattermostAllowList(
           await readStoreAllowFromForDmPolicy({
             provider: "mattermost",
    +        accountId: account.accountId,
             dmPolicy,
    -        readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
    +        readStore: pairing.readStoreForDmPolicy,
           }),
         );
         const accessDecision = resolveDmGroupAccessWithLists({
    @@ -424,8 +431,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
               return;
             }
             if (accessDecision.decision === "pairing") {
    -          const { code, created } = await core.channel.pairing.upsertPairingRequest({
    -            channel: "mattermost",
    +          const { code, created } = await pairing.upsertPairingRequest({
                 id: senderId,
                 meta: { name: senderName },
               });
    @@ -862,8 +868,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
         const storeAllowFrom = normalizeMattermostAllowList(
           await readStoreAllowFromForDmPolicy({
             provider: "mattermost",
    +        accountId: account.accountId,
             dmPolicy,
    -        readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
    +        readStore: pairing.readStoreForDmPolicy,
           }),
         );
         const reactionAccess = resolveDmGroupAccessWithLists({
    
  • extensions/msteams/src/monitor-handler/message-handler.ts+10 3 modified
    @@ -1,7 +1,9 @@
     import {
    +  DEFAULT_ACCOUNT_ID,
       buildPendingHistoryContextFromMap,
       clearHistoryEntriesIfEnabled,
       DEFAULT_GROUP_HISTORY_LIMIT,
    +  createScopedPairingAccess,
       logInboundDrop,
       recordPendingHistoryEntryIfEnabled,
       resolveControlCommandGate,
    @@ -57,6 +59,11 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
         log,
       } = deps;
       const core = getMSTeamsRuntime();
    +  const pairing = createScopedPairingAccess({
    +    core,
    +    channel: "msteams",
    +    accountId: DEFAULT_ACCOUNT_ID,
    +  });
       const logVerboseMessage = (message: string) => {
         if (core.logging.shouldLogVerbose()) {
           log.debug?.(message);
    @@ -132,8 +139,9 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
         const dmPolicy = msteamsCfg?.dmPolicy ?? "pairing";
         const storedAllowFrom = await readStoreAllowFromForDmPolicy({
           provider: "msteams",
    +      accountId: pairing.accountId,
           dmPolicy,
    -      readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
    +      readStore: pairing.readStoreForDmPolicy,
         });
         const useAccessGroups = cfg.commands?.useAccessGroups !== false;
     
    @@ -200,8 +208,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
             allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
           });
           if (access.decision === "pairing") {
    -        const request = await core.channel.pairing.upsertPairingRequest({
    -          channel: "msteams",
    +        const request = await pairing.upsertPairingRequest({
               id: senderId,
               meta: { name: senderName },
             });
    
  • extensions/nextcloud-talk/src/inbound.ts+9 3 modified
    @@ -1,5 +1,6 @@
     import {
       GROUP_POLICY_BLOCKED_LABEL,
    +  createScopedPairingAccess,
       createNormalizedOutboundDeliverer,
       createReplyPrefixOptions,
       formatTextWithAttachmentLinks,
    @@ -58,6 +59,11 @@ export async function handleNextcloudTalkInbound(params: {
     }): Promise<void> {
       const { message, account, config, runtime, statusSink } = params;
       const core = getNextcloudTalkRuntime();
    +  const pairing = createScopedPairingAccess({
    +    core,
    +    channel: CHANNEL_ID,
    +    accountId: account.accountId,
    +  });
     
       const rawBody = message.text?.trim() ?? "";
       if (!rawBody) {
    @@ -99,8 +105,9 @@ export async function handleNextcloudTalkInbound(params: {
       const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom);
       const storeAllowFrom = await readStoreAllowFromForDmPolicy({
         provider: CHANNEL_ID,
    +    accountId: account.accountId,
         dmPolicy,
    -    readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
    +    readStore: pairing.readStoreForDmPolicy,
       });
       const storeAllowList = normalizeNextcloudTalkAllowlist(storeAllowFrom);
     
    @@ -167,8 +174,7 @@ export async function handleNextcloudTalkInbound(params: {
       } else {
         if (access.decision !== "allow") {
           if (access.decision === "pairing") {
    -        const { code, created } = await core.channel.pairing.upsertPairingRequest({
    -          channel: CHANNEL_ID,
    +        const { code, created } = await pairing.upsertPairingRequest({
               id: senderId,
               meta: { name: senderName || undefined },
             });
    
  • extensions/zalo/src/monitor.ts+8 3 modified
    @@ -1,6 +1,7 @@
     import type { IncomingMessage, ServerResponse } from "node:http";
     import type { MarkdownTableMode, OpenClawConfig, OutboundReplyPayload } from "openclaw/plugin-sdk";
     import {
    +  createScopedPairingAccess,
       createReplyPrefixOptions,
       resolveSenderCommandAuthorization,
       resolveOutboundMediaUrls,
    @@ -303,6 +304,11 @@ async function processMessageWithPipeline(params: {
         statusSink,
         fetcher,
       } = params;
    +  const pairing = createScopedPairingAccess({
    +    core,
    +    channel: "zalo",
    +    accountId: account.accountId,
    +  });
       const { from, chat, message_id, date } = message;
     
       const isGroup = chat.chat_type === "GROUP";
    @@ -358,7 +364,7 @@ async function processMessageWithPipeline(params: {
         configuredGroupAllowFrom: groupAllowFrom,
         senderId,
         isSenderAllowed: isZaloSenderAllowed,
    -    readAllowFromStore: () => core.channel.pairing.readAllowFromStore("zalo"),
    +    readAllowFromStore: pairing.readAllowFromStore,
         shouldComputeCommandAuthorized: (body, cfg) =>
           core.channel.commands.shouldComputeCommandAuthorized(body, cfg),
         resolveCommandAuthorizedFromAuthorizers: (params) =>
    @@ -376,8 +382,7 @@ async function processMessageWithPipeline(params: {
     
           if (!allowed) {
             if (dmPolicy === "pairing") {
    -          const { code, created } = await core.channel.pairing.upsertPairingRequest({
    -            channel: "zalo",
    +          const { code, created } = await pairing.upsertPairingRequest({
                 id: senderId,
                 meta: { name: senderName ?? undefined },
               });
    
  • extensions/zalouser/src/monitor.ts+8 3 modified
    @@ -6,6 +6,7 @@ import type {
       RuntimeEnv,
     } from "openclaw/plugin-sdk";
     import {
    +  createScopedPairingAccess,
       createReplyPrefixOptions,
       resolveOutboundMediaUrls,
       mergeAllowlist,
    @@ -177,6 +178,11 @@ async function processMessage(
       statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
     ): Promise<void> {
       const { threadId, content, timestamp, metadata } = message;
    +  const pairing = createScopedPairingAccess({
    +    core,
    +    channel: "zalouser",
    +    accountId: account.accountId,
    +  });
       if (!content?.trim()) {
         return;
       }
    @@ -225,7 +231,7 @@ async function processMessage(
         configuredAllowFrom: configAllowFrom,
         senderId,
         isSenderAllowed,
    -    readAllowFromStore: () => core.channel.pairing.readAllowFromStore("zalouser"),
    +    readAllowFromStore: pairing.readAllowFromStore,
         shouldComputeCommandAuthorized: (body, cfg) =>
           core.channel.commands.shouldComputeCommandAuthorized(body, cfg),
         resolveCommandAuthorizedFromAuthorizers: (params) =>
    @@ -243,8 +249,7 @@ async function processMessage(
     
           if (!allowed) {
             if (dmPolicy === "pairing") {
    -          const { code, created } = await core.channel.pairing.upsertPairingRequest({
    -            channel: "zalouser",
    +          const { code, created } = await pairing.upsertPairingRequest({
                 id: senderId,
                 meta: { name: senderName || undefined },
               });
    
  • src/plugin-sdk/index.ts+1 0 modified
    @@ -216,6 +216,7 @@ export {
       type SenderGroupAccessReason,
     } from "./group-access.js";
     export { resolveSenderCommandAuthorization } from "./command-auth.js";
    +export { createScopedPairingAccess } from "./pairing-access.js";
     export { handleSlackMessageAction } from "./slack-message-actions.js";
     export { extractToolSend } from "./tool-send.js";
     export {
    
  • src/plugin-sdk/pairing-access.ts+36 0 added
    @@ -0,0 +1,36 @@
    +import type { ChannelId } from "../channels/plugins/types.js";
    +import type { PluginRuntime } from "../plugins/runtime/types.js";
    +import { normalizeAccountId } from "../routing/session-key.js";
    +
    +type PairingApi = PluginRuntime["channel"]["pairing"];
    +type ScopedUpsertInput = Omit<
    +  Parameters<PairingApi["upsertPairingRequest"]>[0],
    +  "channel" | "accountId"
    +>;
    +
    +export function createScopedPairingAccess(params: {
    +  core: PluginRuntime;
    +  channel: ChannelId;
    +  accountId: string;
    +}) {
    +  const resolvedAccountId = normalizeAccountId(params.accountId);
    +  return {
    +    accountId: resolvedAccountId,
    +    readAllowFromStore: () =>
    +      params.core.channel.pairing.readAllowFromStore({
    +        channel: params.channel,
    +        accountId: resolvedAccountId,
    +      }),
    +    readStoreForDmPolicy: (provider: ChannelId, accountId: string) =>
    +      params.core.channel.pairing.readAllowFromStore({
    +        channel: provider,
    +        accountId: normalizeAccountId(accountId),
    +      }),
    +    upsertPairingRequest: (input: ScopedUpsertInput) =>
    +      params.core.channel.pairing.upsertPairingRequest({
    +        channel: params.channel,
    +        accountId: resolvedAccountId,
    +        ...input,
    +      }),
    +  };
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.