refactor(channels): unify dm pairing policy flows

This commit is contained in:
Peter Steinberger
2026-02-26 22:36:05 +01:00
parent 7e0b3f16e3
commit 564be6b402
9 changed files with 443 additions and 252 deletions

View File

@@ -0,0 +1,87 @@
import { issuePairingChallenge } from "../../pairing/pairing-challenge.js";
import { upsertChannelPairingRequest } from "../../pairing/pairing-store.js";
import {
readStoreAllowFromForDmPolicy,
resolveDmGroupAccessWithLists,
} from "../../security/dm-policy-shared.js";
import { isSignalSenderAllowed, type SignalSender } from "../identity.js";
type SignalDmPolicy = "open" | "pairing" | "allowlist" | "disabled";
type SignalGroupPolicy = "open" | "allowlist" | "disabled";
export async function resolveSignalAccessState(params: {
accountId: string;
dmPolicy: SignalDmPolicy;
groupPolicy: SignalGroupPolicy;
allowFrom: string[];
groupAllowFrom: string[];
sender: SignalSender;
}) {
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: "signal",
accountId: params.accountId,
dmPolicy: params.dmPolicy,
});
const resolveAccessDecision = (isGroup: boolean) =>
resolveDmGroupAccessWithLists({
isGroup,
dmPolicy: params.dmPolicy,
groupPolicy: params.groupPolicy,
allowFrom: params.allowFrom,
groupAllowFrom: params.groupAllowFrom,
storeAllowFrom,
isSenderAllowed: (allowEntries) => isSignalSenderAllowed(params.sender, allowEntries),
});
const dmAccess = resolveAccessDecision(false);
return {
resolveAccessDecision,
dmAccess,
effectiveDmAllow: dmAccess.effectiveAllowFrom,
effectiveGroupAllow: dmAccess.effectiveGroupAllowFrom,
};
}
export async function handleSignalDirectMessageAccess(params: {
dmPolicy: SignalDmPolicy;
dmAccessDecision: "allow" | "block" | "pairing";
senderId: string;
senderIdLine: string;
senderDisplay: string;
senderName?: string;
accountId: string;
sendPairingReply: (text: string) => Promise<void>;
log: (message: string) => void;
}): Promise<boolean> {
if (params.dmAccessDecision === "allow") {
return true;
}
if (params.dmAccessDecision === "block") {
if (params.dmPolicy !== "disabled") {
params.log(`Blocked signal sender ${params.senderDisplay} (dmPolicy=${params.dmPolicy})`);
}
return false;
}
if (params.dmPolicy === "pairing") {
await issuePairingChallenge({
channel: "signal",
senderId: params.senderId,
senderIdLine: params.senderIdLine,
meta: { name: params.senderName },
upsertPairingRequest: async ({ id, meta }) =>
await upsertChannelPairingRequest({
channel: "signal",
id,
accountId: params.accountId,
meta,
}),
sendPairingReply: params.sendPairingReply,
onCreated: () => {
params.log(`signal pairing request sender=${params.senderId}`);
},
onReplyError: (err) => {
params.log(`signal pairing reply failed for ${params.senderId}: ${String(err)}`);
},
});
}
return false;
}

View File

@@ -30,14 +30,8 @@ import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js
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 { upsertChannelPairingRequest } from "../../pairing/pairing-store.js";
import { resolveAgentRoute } from "../../routing/resolve-route.js";
import {
DM_GROUP_ACCESS_REASON,
readStoreAllowFromForDmPolicy,
resolveDmGroupAccessWithLists,
} from "../../security/dm-policy-shared.js";
import { DM_GROUP_ACCESS_REASON } from "../../security/dm-policy-shared.js";
import { normalizeE164 } from "../../utils.js";
import {
formatSignalPairingIdLine,
@@ -50,6 +44,7 @@ import {
type SignalSender,
} from "../identity.js";
import { sendMessageSignal, sendReadReceiptSignal, sendTypingSignal } from "../send.js";
import { handleSignalDirectMessageAccess, resolveSignalAccessState } from "./access-policy.js";
import type {
SignalEnvelope,
SignalEventHandlerDeps,
@@ -454,24 +449,15 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
const hasBodyContent =
Boolean(messageText || quoteText) || Boolean(!reaction && dataMessage?.attachments?.length);
const senderDisplay = formatSignalSenderDisplay(sender);
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: "signal",
accountId: deps.accountId,
dmPolicy: deps.dmPolicy,
});
const resolveAccessDecision = (isGroup: boolean) =>
resolveDmGroupAccessWithLists({
isGroup,
const { resolveAccessDecision, dmAccess, effectiveDmAllow, effectiveGroupAllow } =
await resolveSignalAccessState({
accountId: deps.accountId,
dmPolicy: deps.dmPolicy,
groupPolicy: deps.groupPolicy,
allowFrom: deps.allowFrom,
groupAllowFrom: deps.groupAllowFrom,
storeAllowFrom,
isSenderAllowed: (allowEntries) => isSignalSenderAllowed(sender, allowEntries),
sender,
});
const dmAccess = resolveAccessDecision(false);
const effectiveDmAllow = dmAccess.effectiveAllowFrom;
const effectiveGroupAllow = dmAccess.effectiveGroupAllowFrom;
if (
reaction &&
@@ -502,43 +488,25 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
const isGroup = Boolean(groupId);
if (!isGroup) {
if (dmAccess.decision === "block") {
if (deps.dmPolicy !== "disabled") {
logVerbose(`Blocked signal sender ${senderDisplay} (dmPolicy=${deps.dmPolicy})`);
}
return;
}
if (dmAccess.decision === "pairing") {
if (deps.dmPolicy === "pairing") {
const senderId = senderAllowId;
const { code, created } = await upsertChannelPairingRequest({
channel: "signal",
id: senderId,
const allowedDirectMessage = await handleSignalDirectMessageAccess({
dmPolicy: deps.dmPolicy,
dmAccessDecision: dmAccess.decision,
senderId: senderAllowId,
senderIdLine,
senderDisplay,
senderName: envelope.sourceName ?? undefined,
accountId: deps.accountId,
sendPairingReply: async (text) => {
await sendMessageSignal(`signal:${senderRecipient}`, text, {
baseUrl: deps.baseUrl,
account: deps.account,
maxBytes: deps.mediaMaxBytes,
accountId: deps.accountId,
meta: { name: envelope.sourceName ?? undefined },
});
if (created) {
logVerbose(`signal pairing request sender=${senderId}`);
try {
await sendMessageSignal(
`signal:${senderRecipient}`,
buildPairingReply({
channel: "signal",
idLine: senderIdLine,
code,
}),
{
baseUrl: deps.baseUrl,
account: deps.account,
maxBytes: deps.mediaMaxBytes,
accountId: deps.accountId,
},
);
} catch (err) {
logVerbose(`signal pairing reply failed for ${senderId}: ${String(err)}`);
}
}
}
},
log: logVerbose,
});
if (!allowedDirectMessage) {
return;
}
}