refactor: unify DM pairing challenge flows

This commit is contained in:
Peter Steinberger
2026-03-07 19:36:02 +00:00
parent dab0e97c22
commit 2bcd56cfac
21 changed files with 356 additions and 241 deletions

View File

@@ -4,6 +4,7 @@ import {
createScopedPairingAccess,
createReplyPrefixOptions,
evictOldHistoryKeys,
issuePairingChallenge,
logAckFailure,
logInboundDrop,
logTypingFailure,
@@ -595,25 +596,24 @@ export async function processMessage(
}
if (accessDecision.decision === "pairing") {
const { code, created } = await pairing.upsertPairingRequest({
id: message.senderId,
await issuePairingChallenge({
channel: "bluebubbles",
senderId: message.senderId,
senderIdLine: `Your BlueBubbles sender id: ${message.senderId}`,
meta: { name: message.senderName },
});
runtime.log?.(`[bluebubbles] pairing request sender=${message.senderId} created=${created}`);
if (created) {
logVerbose(core, runtime, `bluebubbles pairing request sender=${message.senderId}`);
try {
await sendMessageBlueBubbles(
message.senderId,
core.channel.pairing.buildPairingReply({
channel: "bluebubbles",
idLine: `Your BlueBubbles sender id: ${message.senderId}`,
code,
}),
{ cfg: config, accountId: account.accountId },
);
upsertPairingRequest: pairing.upsertPairingRequest,
onCreated: () => {
runtime.log?.(`[bluebubbles] pairing request sender=${message.senderId} created=true`);
logVerbose(core, runtime, `bluebubbles pairing request sender=${message.senderId}`);
},
sendPairingReply: async (text) => {
await sendMessageBlueBubbles(message.senderId, text, {
cfg: config,
accountId: account.accountId,
});
statusSink?.({ lastOutboundAt: Date.now() });
} catch (err) {
},
onReplyError: (err) => {
logVerbose(
core,
runtime,
@@ -622,8 +622,8 @@ export async function processMessage(
runtime.error?.(
`[bluebubbles] pairing reply failed sender=${message.senderId}: ${String(err)}`,
);
}
}
},
});
return;
}

View File

@@ -6,6 +6,7 @@ import {
createScopedPairingAccess,
DEFAULT_GROUP_HISTORY_LIMIT,
type HistoryEntry,
issuePairingChallenge,
normalizeAgentId,
recordPendingHistoryEntryIfEnabled,
resolveOpenProviderRuntimeGroupPolicy,
@@ -1101,29 +1102,29 @@ export async function handleFeishuMessage(params: {
if (isDirect && dmPolicy !== "open" && !dmAllowed) {
if (dmPolicy === "pairing") {
const { code, created } = await pairing.upsertPairingRequest({
id: ctx.senderOpenId,
await issuePairingChallenge({
channel: "feishu",
senderId: ctx.senderOpenId,
senderIdLine: `Your Feishu user id: ${ctx.senderOpenId}`,
meta: { name: ctx.senderName },
});
if (created) {
log(`feishu[${account.accountId}]: pairing request sender=${ctx.senderOpenId}`);
try {
upsertPairingRequest: pairing.upsertPairingRequest,
onCreated: () => {
log(`feishu[${account.accountId}]: pairing request sender=${ctx.senderOpenId}`);
},
sendPairingReply: async (text) => {
await sendMessageFeishu({
cfg,
to: `chat:${ctx.chatId}`,
text: core.channel.pairing.buildPairingReply({
channel: "feishu",
idLine: `Your Feishu user id: ${ctx.senderOpenId}`,
code,
}),
text,
accountId: account.accountId,
});
} catch (err) {
},
onReplyError: (err) => {
log(
`feishu[${account.accountId}]: pairing reply failed for ${ctx.senderOpenId}: ${String(err)}`,
);
}
}
},
});
} else {
log(
`feishu[${account.accountId}]: blocked unauthorized sender ${ctx.senderOpenId} (dmPolicy=${dmPolicy})`,

View File

@@ -1,6 +1,7 @@
import {
GROUP_POLICY_BLOCKED_LABEL,
createScopedPairingAccess,
issuePairingChallenge,
isDangerousNameMatchingEnabled,
resolveAllowlistProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
@@ -311,27 +312,27 @@ export async function applyGoogleChatInboundAccessPolicy(params: {
if (access.decision !== "allow") {
if (access.decision === "pairing") {
const { code, created } = await pairing.upsertPairingRequest({
id: senderId,
await issuePairingChallenge({
channel: "googlechat",
senderId,
senderIdLine: `Your Google Chat user id: ${senderId}`,
meta: { name: senderName || undefined, email: senderEmail },
});
if (created) {
logVerbose(`googlechat pairing request sender=${senderId}`);
try {
upsertPairingRequest: pairing.upsertPairingRequest,
onCreated: () => {
logVerbose(`googlechat pairing request sender=${senderId}`);
},
sendPairingReply: async (text) => {
await sendGoogleChatMessage({
account,
space: spaceId,
text: core.channel.pairing.buildPairingReply({
channel: "googlechat",
idLine: `Your Google Chat user id: ${senderId}`,
code,
}),
text,
});
statusSink?.({ lastOutboundAt: Date.now() });
} catch (err) {
},
onReplyError: (err) => {
logVerbose(`pairing reply failed for ${senderId}: ${String(err)}`);
}
}
},
});
} else {
logVerbose(`Blocked unauthorized Google Chat sender ${senderId} (dmPolicy=${dmPolicy})`);
}

View File

@@ -3,6 +3,7 @@ import {
createScopedPairingAccess,
dispatchInboundReplyWithBase,
formatTextWithAttachmentLinks,
issuePairingChallenge,
logInboundDrop,
isDangerousNameMatchingEnabled,
readStoreAllowFromForDmPolicy,
@@ -208,28 +209,25 @@ export async function handleIrcInbound(params: {
}).allowed;
if (!dmAllowed) {
if (dmPolicy === "pairing") {
const { code, created } = await pairing.upsertPairingRequest({
id: senderDisplay.toLowerCase(),
await issuePairingChallenge({
channel: CHANNEL_ID,
senderId: senderDisplay.toLowerCase(),
senderIdLine: `Your IRC id: ${senderDisplay}`,
meta: { name: message.senderNick || undefined },
});
if (created) {
try {
const reply = core.channel.pairing.buildPairingReply({
channel: CHANNEL_ID,
idLine: `Your IRC id: ${senderDisplay}`,
code,
});
upsertPairingRequest: pairing.upsertPairingRequest,
sendPairingReply: async (text) => {
await deliverIrcReply({
payload: { text: reply },
payload: { text },
target: message.senderNick,
accountId: account.accountId,
sendReply: params.sendReply,
statusSink,
});
} catch (err) {
},
onReplyError: (err) => {
runtime.error?.(`irc: pairing reply failed for ${senderDisplay}: ${String(err)}`);
}
}
},
});
}
runtime.log?.(`irc: drop DM sender ${senderDisplay} (dmPolicy=${dmPolicy})`);
return;

View File

@@ -3,6 +3,7 @@ import {
createScopedPairingAccess,
dispatchInboundReplyWithBase,
formatTextWithAttachmentLinks,
issuePairingChallenge,
logInboundDrop,
readStoreAllowFromForDmPolicy,
resolveDmGroupAccessWithCommandGate,
@@ -173,26 +174,20 @@ export async function handleNextcloudTalkInbound(params: {
} else {
if (access.decision !== "allow") {
if (access.decision === "pairing") {
const { code, created } = await pairing.upsertPairingRequest({
id: senderId,
await issuePairingChallenge({
channel: CHANNEL_ID,
senderId,
senderIdLine: `Your Nextcloud user id: ${senderId}`,
meta: { name: senderName || undefined },
});
if (created) {
try {
await sendMessageNextcloudTalk(
roomToken,
core.channel.pairing.buildPairingReply({
channel: CHANNEL_ID,
idLine: `Your Nextcloud user id: ${senderId}`,
code,
}),
{ accountId: account.accountId },
);
upsertPairingRequest: pairing.upsertPairingRequest,
sendPairingReply: async (text) => {
await sendMessageNextcloudTalk(roomToken, text, { accountId: account.accountId });
statusSink?.({ lastOutboundAt: Date.now() });
} catch (err) {
},
onReplyError: (err) => {
runtime.error?.(`nextcloud-talk: pairing reply failed for ${senderId}: ${String(err)}`);
}
}
},
});
}
runtime.log?.(`nextcloud-talk: drop DM sender ${senderId} (reason=${access.reason})`);
return;

View File

@@ -7,6 +7,7 @@ import type {
import {
createScopedPairingAccess,
createReplyPrefixOptions,
issuePairingChallenge,
resolveDirectDmAuthorizationOutcome,
resolveSenderCommandAuthorizationWithRuntime,
resolveOutboundMediaUrls,
@@ -414,31 +415,30 @@ async function processMessageWithPipeline(params: {
}
if (directDmOutcome === "unauthorized") {
if (dmPolicy === "pairing") {
const { code, created } = await pairing.upsertPairingRequest({
id: senderId,
await issuePairingChallenge({
channel: "zalo",
senderId,
senderIdLine: `Your Zalo user id: ${senderId}`,
meta: { name: senderName ?? undefined },
});
if (created) {
logVerbose(core, runtime, `zalo pairing request sender=${senderId}`);
try {
upsertPairingRequest: pairing.upsertPairingRequest,
onCreated: () => {
logVerbose(core, runtime, `zalo pairing request sender=${senderId}`);
},
sendPairingReply: async (text) => {
await sendMessage(
token,
{
chat_id: chatId,
text: core.channel.pairing.buildPairingReply({
channel: "zalo",
idLine: `Your Zalo user id: ${senderId}`,
code,
}),
text,
},
fetcher,
);
statusSink?.({ lastOutboundAt: Date.now() });
} catch (err) {
},
onReplyError: (err) => {
logVerbose(core, runtime, `zalo pairing reply failed for ${senderId}: ${String(err)}`);
}
}
},
});
} else {
logVerbose(
core,

View File

@@ -8,6 +8,7 @@ import {
createTypingCallbacks,
createScopedPairingAccess,
createReplyPrefixOptions,
issuePairingChallenge,
resolveOutboundMediaUrls,
mergeAllowlist,
resolveMentionGatingWithBypass,
@@ -262,32 +263,27 @@ async function processMessage(
const allowed = senderAllowedForCommands;
if (!allowed) {
if (dmPolicy === "pairing") {
const { code, created } = await pairing.upsertPairingRequest({
id: senderId,
await issuePairingChallenge({
channel: "zalouser",
senderId,
senderIdLine: `Your Zalo user id: ${senderId}`,
meta: { name: senderName || undefined },
});
if (created) {
logVerbose(core, runtime, `zalouser pairing request sender=${senderId}`);
try {
await sendMessageZalouser(
chatId,
core.channel.pairing.buildPairingReply({
channel: "zalouser",
idLine: `Your Zalo user id: ${senderId}`,
code,
}),
{ profile: account.profile },
);
upsertPairingRequest: pairing.upsertPairingRequest,
onCreated: () => {
logVerbose(core, runtime, `zalouser pairing request sender=${senderId}`);
},
sendPairingReply: async (text) => {
await sendMessageZalouser(chatId, text, { profile: account.profile });
statusSink?.({ lastOutboundAt: Date.now() });
} catch (err) {
},
onReplyError: (err) => {
logVerbose(
core,
runtime,
`zalouser pairing reply failed for ${senderId}: ${String(err)}`,
);
}
}
},
});
} else {
logVerbose(
core,