mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
refactor(security): enforce account-scoped pairing APIs
This commit is contained in:
@@ -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",
|
||||
|
||||
157
scripts/check-pairing-account-scope.mjs
Normal file
157
scripts/check-pairing-account-scope.mjs
Normal file
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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])];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"];
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user