refactor: unify dm policy store reads and reason codes

This commit is contained in:
Peter Steinberger
2026-02-26 17:47:51 +01:00
parent 53e30475e2
commit cd80c7e7ff
21 changed files with 259 additions and 92 deletions

View File

@@ -41,6 +41,7 @@ import {
} 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";
import { resolveDiscordComponentEntry, resolveDiscordModalEntry } from "../components-registry.js";
import {
createDiscordFormModal,
@@ -471,8 +472,11 @@ async function ensureDmComponentAuthorized(params: {
return true;
}
const storeAllowFrom =
dmPolicy === "allowlist" ? [] : await readChannelAllowFromStore("discord").catch(() => []);
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: "discord",
dmPolicy,
readStore: (provider) => readChannelAllowFromStore(provider),
});
const effectiveAllowFrom = [...(ctx.allowFrom ?? []), ...storeAllowFrom];
const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:", "pk:"]);
const allowMatch = allowList

View File

@@ -13,7 +13,10 @@ 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 { resolveDmGroupAccessWithLists } from "../../security/dm-policy-shared.js";
import {
readStoreAllowFromForDmPolicy,
resolveDmGroupAccessWithLists,
} from "../../security/dm-policy-shared.js";
import {
isDiscordGroupAllowedByPolicy,
normalizeDiscordAllowList,
@@ -233,10 +236,11 @@ async function authorizeDiscordReactionIngress(
return { allowed: false, reason: "group-dm-disabled" };
}
if (params.isDirectMessage) {
const storeAllowFrom =
params.dmPolicy === "allowlist"
? []
: await readChannelAllowFromStore("discord").catch(() => []);
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: "discord",
dmPolicy: params.dmPolicy,
readStore: (provider) => readChannelAllowFromStore(provider),
});
const access = resolveDmGroupAccessWithLists({
isGroup: false,
dmPolicy: params.dmPolicy,

View File

@@ -31,6 +31,7 @@ import {
} from "../../pairing/pairing-store.js";
import { resolveAgentRoute } from "../../routing/resolve-route.js";
import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
import { readStoreAllowFromForDmPolicy } from "../../security/dm-policy-shared.js";
import { fetchPluralKitMessageInfo } from "../pluralkit.js";
import { sendMessageDiscord } from "../send.js";
import {
@@ -183,8 +184,11 @@ export async function preflightDiscordMessage(
return null;
}
if (dmPolicy !== "open") {
const storeAllowFrom =
dmPolicy === "allowlist" ? [] : await readChannelAllowFromStore("discord").catch(() => []);
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: "discord",
dmPolicy,
readStore: (provider) => readChannelAllowFromStore(provider),
});
const effectiveAllowFrom = [...(params.allowFrom ?? []), ...storeAllowFrom];
const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:", "pk:"]);
const allowMatch = allowList

View File

@@ -53,6 +53,7 @@ import {
import { resolveAgentRoute } from "../../routing/resolve-route.js";
import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
import { readStoreAllowFromForDmPolicy } from "../../security/dm-policy-shared.js";
import { chunkItems } from "../../utils/chunk-items.js";
import { withTimeout } from "../../utils/with-timeout.js";
import { loadWebMedia } from "../../web/media.js";
@@ -1360,8 +1361,11 @@ async function dispatchDiscordCommandInteraction(params: {
return;
}
if (dmPolicy !== "open") {
const storeAllowFrom =
dmPolicy === "allowlist" ? [] : await readChannelAllowFromStore("discord").catch(() => []);
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: "discord",
dmPolicy,
readStore: (provider) => readChannelAllowFromStore(provider),
});
const effectiveAllowFrom = [
...(discordConfig?.allowFrom ?? discordConfig?.dm?.allowFrom ?? []),
...storeAllowFrom,

View File

@@ -20,7 +20,10 @@ import {
resolveChannelGroupRequireMention,
} from "../../config/group-policy.js";
import { resolveAgentRoute } from "../../routing/resolve-route.js";
import { resolveDmGroupAccessWithLists } from "../../security/dm-policy-shared.js";
import {
DM_GROUP_ACCESS_REASON,
resolveDmGroupAccessWithLists,
} from "../../security/dm-policy-shared.js";
import { truncateUtf16Safe } from "../../utils.js";
import {
formatIMessageChatTarget,
@@ -162,24 +165,24 @@ export function resolveIMessageInboundDecision(params: {
if (accessDecision.decision !== "allow") {
if (isGroup) {
if (accessDecision.reason === "groupPolicy=disabled") {
if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_DISABLED) {
params.logVerbose?.("Blocked iMessage group message (groupPolicy: disabled)");
return { kind: "drop", reason: "groupPolicy disabled" };
}
if (accessDecision.reason === "groupPolicy=allowlist (empty allowlist)") {
if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_EMPTY_ALLOWLIST) {
params.logVerbose?.(
"Blocked iMessage group message (groupPolicy: allowlist, no groupAllowFrom)",
);
return { kind: "drop", reason: "groupPolicy allowlist (empty groupAllowFrom)" };
}
if (accessDecision.reason === "groupPolicy=allowlist (not allowlisted)") {
if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_NOT_ALLOWLISTED) {
params.logVerbose?.(`Blocked iMessage sender ${sender} (not in groupAllowFrom)`);
return { kind: "drop", reason: "not in groupAllowFrom" };
}
params.logVerbose?.(`Blocked iMessage group message (${accessDecision.reason})`);
return { kind: "drop", reason: accessDecision.reason };
}
if (accessDecision.reason === "dmPolicy=disabled") {
if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.DM_POLICY_DISABLED) {
return { kind: "drop", reason: "dmPolicy disabled" };
}
if (accessDecision.decision === "pairing") {

View File

@@ -409,11 +409,14 @@ export {
} from "../agents/tools/common.js";
export { formatDocsLink } from "../terminal/links.js";
export {
DM_GROUP_ACCESS_REASON,
readStoreAllowFromForDmPolicy,
resolveDmAllowState,
resolveDmGroupAccessDecision,
resolveDmGroupAccessWithLists,
resolveEffectiveAllowFromLists,
} from "../security/dm-policy-shared.js";
export type { DmGroupAccessReasonCode } from "../security/dm-policy-shared.js";
export type { HookEntry } from "../hooks/types.js";
export { clamp, escapeRegExp, normalizeE164, safeParseJson, sleep } from "../utils.js";
export { stripAnsi } from "../terminal/ansi.js";

View File

@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import { isAllowedBlueBubblesSender } from "../../extensions/bluebubbles/src/targets.js";
import { isMattermostSenderAllowed } from "../../extensions/mattermost/src/mattermost/monitor-auth.js";
import { isSignalSenderAllowed, type SignalSender } from "../signal/identity.js";
import { resolveDmGroupAccessWithLists } from "./dm-policy-shared.js";
import { DM_GROUP_ACCESS_REASON, resolveDmGroupAccessWithLists } from "./dm-policy-shared.js";
type ChannelSmokeCase = {
name: string;
@@ -58,6 +58,7 @@ describe("security/dm-policy-shared channel smoke", () => {
isSenderAllowed: testCase.isSenderAllowed,
});
expect(access.decision).toBe("block");
expect(access.reasonCode).toBe(DM_GROUP_ACCESS_REASON.GROUP_POLICY_NOT_ALLOWLISTED);
expect(access.reason).toBe("groupPolicy=allowlist (not allowlisted)");
});
}

View File

@@ -1,5 +1,7 @@
import { describe, expect, it } from "vitest";
import {
DM_GROUP_ACCESS_REASON,
readStoreAllowFromForDmPolicy,
resolveDmAllowState,
resolveDmGroupAccessDecision,
resolveDmGroupAccessWithLists,
@@ -34,6 +36,34 @@ describe("security/dm-policy-shared", () => {
expect(state.isMultiUserDm).toBe(false);
});
it("skips pairing-store reads when dmPolicy is allowlist", async () => {
let called = false;
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: "telegram",
dmPolicy: "allowlist",
readStore: async () => {
called = true;
return ["should-not-be-read"];
},
});
expect(called).toBe(false);
expect(storeAllowFrom).toEqual([]);
});
it("skips pairing-store reads when shouldRead=false", async () => {
let called = false;
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: "slack",
shouldRead: false,
readStore: async () => {
called = true;
return ["should-not-be-read"];
},
});
expect(called).toBe(false);
expect(storeAllowFrom).toEqual([]);
});
it("builds effective DM/group allowlists from config + pairing store", () => {
const lists = resolveEffectiveAllowFromLists({
allowFrom: [" owner ", "", "owner2"],
@@ -98,6 +128,7 @@ describe("security/dm-policy-shared", () => {
isSenderAllowed: (allowFrom) => allowFrom.includes("paired-user"),
});
expect(resolved.decision).toBe("allow");
expect(resolved.reasonCode).toBe(DM_GROUP_ACCESS_REASON.DM_POLICY_ALLOWLISTED);
expect(resolved.reason).toBe("dmPolicy=pairing (allowlisted)");
expect(resolved.effectiveAllowFrom).toEqual(["owner", "paired-user"]);
expect(resolved.effectiveGroupAllowFrom).toEqual(["group:room"]);
@@ -114,6 +145,7 @@ describe("security/dm-policy-shared", () => {
isSenderAllowed: () => false,
});
expect(resolved.decision).toBe("block");
expect(resolved.reasonCode).toBe(DM_GROUP_ACCESS_REASON.DM_POLICY_NOT_ALLOWLISTED);
expect(resolved.reason).toBe("dmPolicy=allowlist (not allowlisted)");
expect(resolved.effectiveAllowFrom).toEqual(["owner"]);
});
@@ -237,6 +269,7 @@ describe("security/dm-policy-shared", () => {
});
expect(decision).toEqual({
decision: "block",
reasonCode: DM_GROUP_ACCESS_REASON.DM_POLICY_NOT_ALLOWLISTED,
reason: "dmPolicy=allowlist (not allowlisted)",
});
});
@@ -252,6 +285,7 @@ describe("security/dm-policy-shared", () => {
});
expect(decision).toEqual({
decision: "pairing",
reasonCode: DM_GROUP_ACCESS_REASON.DM_POLICY_PAIRING_REQUIRED,
reason: "dmPolicy=pairing (not allowlisted)",
});
});
@@ -279,6 +313,7 @@ describe("security/dm-policy-shared", () => {
});
expect(decision).toEqual({
decision: "block",
reasonCode: DM_GROUP_ACCESS_REASON.GROUP_POLICY_NOT_ALLOWLISTED,
reason: "groupPolicy=allowlist (not allowlisted)",
});
});

View File

@@ -35,6 +35,31 @@ export function resolveEffectiveAllowFromLists(params: {
}
export type DmGroupAccessDecision = "allow" | "block" | "pairing";
export const DM_GROUP_ACCESS_REASON = {
GROUP_POLICY_ALLOWED: "group_policy_allowed",
GROUP_POLICY_DISABLED: "group_policy_disabled",
GROUP_POLICY_EMPTY_ALLOWLIST: "group_policy_empty_allowlist",
GROUP_POLICY_NOT_ALLOWLISTED: "group_policy_not_allowlisted",
DM_POLICY_OPEN: "dm_policy_open",
DM_POLICY_DISABLED: "dm_policy_disabled",
DM_POLICY_ALLOWLISTED: "dm_policy_allowlisted",
DM_POLICY_PAIRING_REQUIRED: "dm_policy_pairing_required",
DM_POLICY_NOT_ALLOWLISTED: "dm_policy_not_allowlisted",
} as const;
export type DmGroupAccessReasonCode =
(typeof DM_GROUP_ACCESS_REASON)[keyof typeof DM_GROUP_ACCESS_REASON];
export async function readStoreAllowFromForDmPolicy(params: {
provider: ChannelId;
dmPolicy?: string | null;
shouldRead?: boolean | null;
readStore?: (provider: ChannelId) => Promise<string[]>;
}): Promise<string[]> {
if (params.shouldRead === false || params.dmPolicy === "allowlist") {
return [];
}
return await (params.readStore ?? readChannelAllowFromStore)(params.provider).catch(() => []);
}
export function resolveDmGroupAccessDecision(params: {
isGroup: boolean;
@@ -45,6 +70,7 @@ export function resolveDmGroupAccessDecision(params: {
isSenderAllowed: (allowFrom: string[]) => boolean;
}): {
decision: DmGroupAccessDecision;
reasonCode: DmGroupAccessReasonCode;
reason: string;
} {
const dmPolicy = params.dmPolicy ?? "pairing";
@@ -54,32 +80,68 @@ export function resolveDmGroupAccessDecision(params: {
if (params.isGroup) {
if (groupPolicy === "disabled") {
return { decision: "block", reason: "groupPolicy=disabled" };
return {
decision: "block",
reasonCode: DM_GROUP_ACCESS_REASON.GROUP_POLICY_DISABLED,
reason: "groupPolicy=disabled",
};
}
if (groupPolicy === "allowlist") {
if (effectiveGroupAllowFrom.length === 0) {
return { decision: "block", reason: "groupPolicy=allowlist (empty allowlist)" };
return {
decision: "block",
reasonCode: DM_GROUP_ACCESS_REASON.GROUP_POLICY_EMPTY_ALLOWLIST,
reason: "groupPolicy=allowlist (empty allowlist)",
};
}
if (!params.isSenderAllowed(effectiveGroupAllowFrom)) {
return { decision: "block", reason: "groupPolicy=allowlist (not allowlisted)" };
return {
decision: "block",
reasonCode: DM_GROUP_ACCESS_REASON.GROUP_POLICY_NOT_ALLOWLISTED,
reason: "groupPolicy=allowlist (not allowlisted)",
};
}
}
return { decision: "allow", reason: `groupPolicy=${groupPolicy}` };
return {
decision: "allow",
reasonCode: DM_GROUP_ACCESS_REASON.GROUP_POLICY_ALLOWED,
reason: `groupPolicy=${groupPolicy}`,
};
}
if (dmPolicy === "disabled") {
return { decision: "block", reason: "dmPolicy=disabled" };
return {
decision: "block",
reasonCode: DM_GROUP_ACCESS_REASON.DM_POLICY_DISABLED,
reason: "dmPolicy=disabled",
};
}
if (dmPolicy === "open") {
return { decision: "allow", reason: "dmPolicy=open" };
return {
decision: "allow",
reasonCode: DM_GROUP_ACCESS_REASON.DM_POLICY_OPEN,
reason: "dmPolicy=open",
};
}
if (params.isSenderAllowed(effectiveAllowFrom)) {
return { decision: "allow", reason: `dmPolicy=${dmPolicy} (allowlisted)` };
return {
decision: "allow",
reasonCode: DM_GROUP_ACCESS_REASON.DM_POLICY_ALLOWLISTED,
reason: `dmPolicy=${dmPolicy} (allowlisted)`,
};
}
if (dmPolicy === "pairing") {
return { decision: "pairing", reason: "dmPolicy=pairing (not allowlisted)" };
return {
decision: "pairing",
reasonCode: DM_GROUP_ACCESS_REASON.DM_POLICY_PAIRING_REQUIRED,
reason: "dmPolicy=pairing (not allowlisted)",
};
}
return { decision: "block", reason: `dmPolicy=${dmPolicy} (not allowlisted)` };
return {
decision: "block",
reasonCode: DM_GROUP_ACCESS_REASON.DM_POLICY_NOT_ALLOWLISTED,
reason: `dmPolicy=${dmPolicy} (not allowlisted)`,
};
}
export function resolveDmGroupAccessWithLists(params: {
@@ -93,6 +155,7 @@ export function resolveDmGroupAccessWithLists(params: {
isSenderAllowed: (allowFrom: string[]) => boolean;
}): {
decision: DmGroupAccessDecision;
reasonCode: DmGroupAccessReasonCode;
reason: string;
effectiveAllowFrom: string[];
effectiveGroupAllowFrom: string[];
@@ -134,9 +197,10 @@ export async function resolveDmAllowState(params: {
Array.isArray(params.allowFrom) ? params.allowFrom : undefined,
);
const hasWildcard = configAllowFrom.includes("*");
const storeAllowFrom = await (params.readStore ?? readChannelAllowFromStore)(
params.provider,
).catch(() => []);
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: params.provider,
readStore: params.readStore,
});
const normalizeEntry = params.normalizeEntry ?? ((value: string) => value);
const normalizedCfg = configAllowFrom
.filter((value) => value !== "*")

View File

@@ -36,7 +36,11 @@ import {
upsertChannelPairingRequest,
} from "../../pairing/pairing-store.js";
import { resolveAgentRoute } from "../../routing/resolve-route.js";
import { resolveDmGroupAccessWithLists } from "../../security/dm-policy-shared.js";
import {
DM_GROUP_ACCESS_REASON,
readStoreAllowFromForDmPolicy,
resolveDmGroupAccessWithLists,
} from "../../security/dm-policy-shared.js";
import { normalizeE164 } from "../../utils.js";
import {
formatSignalPairingIdLine,
@@ -453,10 +457,11 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
const hasBodyContent =
Boolean(messageText || quoteText) || Boolean(!reaction && dataMessage?.attachments?.length);
const senderDisplay = formatSignalSenderDisplay(sender);
const storeAllowFrom =
deps.dmPolicy === "allowlist"
? []
: await readChannelAllowFromStore("signal").catch(() => []);
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: "signal",
dmPolicy: deps.dmPolicy,
readStore: (provider) => readChannelAllowFromStore(provider),
});
const resolveAccessDecision = (isGroup: boolean) =>
resolveDmGroupAccessWithLists({
isGroup,
@@ -543,9 +548,9 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
if (isGroup) {
const groupAccess = resolveAccessDecision(true);
if (groupAccess.decision !== "allow") {
if (groupAccess.reason === "groupPolicy=disabled") {
if (groupAccess.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_DISABLED) {
logVerbose("Blocked signal group message (groupPolicy: disabled)");
} else if (groupAccess.reason === "groupPolicy=allowlist (empty allowlist)") {
} else if (groupAccess.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_EMPTY_ALLOWLIST) {
logVerbose("Blocked signal group message (groupPolicy: allowlist, no groupAllowFrom)");
} else {
logVerbose(`Blocked signal group sender ${senderDisplay} (not in groupAllowFrom)`);

View File

@@ -1,4 +1,5 @@
import { readChannelAllowFromStore } from "../../pairing/pairing-store.js";
import { readStoreAllowFromForDmPolicy } from "../../security/dm-policy-shared.js";
import {
allowListMatches,
normalizeAllowList,
@@ -9,8 +10,11 @@ import { resolveSlackChannelConfig } from "./channel-config.js";
import { normalizeSlackChannelType, type SlackMonitorContext } from "./context.js";
export async function resolveSlackEffectiveAllowFrom(ctx: SlackMonitorContext) {
const storeAllowFrom =
ctx.dmPolicy === "allowlist" ? [] : await readChannelAllowFromStore("slack").catch(() => []);
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: "slack",
dmPolicy: ctx.dmPolicy,
readStore: (provider) => readChannelAllowFromStore(provider),
});
const allowFrom = normalizeAllowList([...ctx.allowFrom, ...storeAllowFrom]);
const allowFromLower = normalizeAllowListLower(allowFrom);
return { allowFrom, allowFromLower };

View File

@@ -10,6 +10,7 @@ import {
readChannelAllowFromStore,
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";
import {
@@ -335,10 +336,11 @@ export async function registerSlackMonitorSlashCommands(params: {
return;
}
const storeAllowFrom =
ctx.dmPolicy === "allowlist"
? []
: await readChannelAllowFromStore("slack").catch(() => []);
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: "slack",
dmPolicy: ctx.dmPolicy,
readStore: (provider) => readChannelAllowFromStore(provider),
});
const effectiveAllowFrom = normalizeAllowList([...ctx.allowFrom, ...storeAllowFrom]);
const effectiveAllowFromLower = normalizeAllowListLower(effectiveAllowFrom);

View File

@@ -27,6 +27,7 @@ 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 } from "../../../security/dm-policy-shared.js";
import { jidToE164, normalizeE164 } from "../../../utils.js";
import { resolveWhatsAppAccount } from "../../accounts.js";
import { newConnectionId } from "../../reconnect.js";
@@ -90,12 +91,11 @@ async function resolveWhatsAppCommandAuthorized(params: {
return normalizeAllowFromE164(configuredGroupAllowFrom).includes(senderE164);
}
const storeAllowFrom =
dmPolicy === "allowlist"
? []
: await readChannelAllowFromStore("whatsapp", process.env, params.msg.accountId).catch(
() => [],
);
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: "whatsapp",
dmPolicy,
readStore: (provider) => readChannelAllowFromStore(provider, process.env, params.msg.accountId),
});
const combinedAllowFrom = Array.from(
new Set([...(configuredAllowFrom ?? []), ...storeAllowFrom]),
);

View File

@@ -10,6 +10,7 @@ import {
readChannelAllowFromStore,
upsertChannelPairingRequest,
} from "../../pairing/pairing-store.js";
import { readStoreAllowFromForDmPolicy } from "../../security/dm-policy-shared.js";
import { isSelfChatMode, normalizeE164 } from "../../utils.js";
import { resolveWhatsAppAccount } from "../accounts.js";
@@ -60,10 +61,11 @@ export async function checkInboundAccessControl(params: {
});
const dmPolicy = account.dmPolicy ?? "pairing";
const configuredAllowFrom = account.allowFrom;
const storeAllowFrom =
dmPolicy === "allowlist"
? []
: await readChannelAllowFromStore("whatsapp", process.env, account.accountId).catch(() => []);
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
provider: "whatsapp",
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 combinedAllowFrom = Array.from(
new Set([...(configuredAllowFrom ?? []), ...storeAllowFrom]),