logicsrc/packages/account-core/src/policy.ts
2026-06-15 12:39:18 +07:00

105 lines
3.9 KiB
TypeScript

import { isPolicyGatedPermission } from "./permissions.js";
import type { LogicSrcPolicyEvaluationInput, LogicSrcPolicyEvaluationResult, LogicSrcRiskBand } from "./types.js";
const WRITE_ACTIONS = new Set([
"social:post:publish",
"social:post:delete",
"social:dm:send",
"email:send",
"email:reply",
"email:forward",
"email:delete",
"email:labels:modify",
"email:archive"
]);
export function riskBandForScore(score: number): LogicSrcRiskBand {
if (!Number.isFinite(score)) {
return "critical";
}
if (score >= 0.75) {
return "critical";
}
if (score >= 0.5) {
return "high";
}
if (score >= 0.25) {
return "medium";
}
return "low";
}
export function scoreAccountActionRisk(input: {
action: string;
externalRecipientCount?: number;
newRecipientOrDomain?: boolean;
hasAttachment?: boolean;
highReachAccount?: boolean;
sensitiveKeywordDetected?: boolean;
rawCredentialAccessAttempted?: boolean;
}) {
let score = 0;
if (input.externalRecipientCount && input.externalRecipientCount > 0) score += 0.1;
if (input.newRecipientOrDomain) score += 0.15;
if (input.hasAttachment) score += 0.15;
if (input.action === "social:post:publish") score += 0.2;
if (input.action.includes("delete")) score += 0.2;
if (input.action.includes("dm:read") || input.action.includes("attachments:read")) score += 0.25;
if (input.highReachAccount) score += 0.25;
if (input.sensitiveKeywordDetected) score += 0.3;
if (input.rawCredentialAccessAttempted) score += 0.4;
return Math.min(1, Number(score.toFixed(2)));
}
function normalizeRiskScore(score: number): number {
if (!Number.isFinite(score)) {
return 1;
}
return Math.min(1, Math.max(0, score));
}
export function evaluateAccountPolicy(input: LogicSrcPolicyEvaluationInput): LogicSrcPolicyEvaluationResult {
const riskScore = normalizeRiskScore(input.riskScore ?? scoreAccountActionRisk({ action: input.action }));
const grantActive = input.grant && !input.grant.revokedAt && (!input.grant.expiresAt || Date.parse(input.grant.expiresAt) > Date.now());
const hasPermission = Boolean(grantActive && input.grant?.permissions.includes(input.action));
if (!hasPermission) {
return { decision: "deny", riskScore, reason: `missing grant for ${input.action}` };
}
if (input.dryRun) {
return { decision: "allow", riskScore, reason: "dry run with matching grant" };
}
const policy = input.grant?.policy.find((entry) => entry.action === input.action);
if (policy?.default === "deny") {
return { decision: "deny", riskScore, reason: `policy ${policy.id} denies ${input.action}` };
}
if (policy?.default === "approval_required") {
return { decision: "approval_required", riskScore, reason: `policy ${policy.id} requires approval` };
}
if (policy?.default === "allow_if_trusted_agent") {
return input.principal?.trusted
? { decision: "allow", riskScore, reason: `policy ${policy.id} allows trusted principal` }
: { decision: "approval_required", riskScore, reason: `policy ${policy.id} requires a trusted principal` };
}
if (policy?.default === "allow_if_below_risk_score") {
const maxRiskScore = typeof policy.conditions?.maxRiskScore === "number" ? policy.conditions.maxRiskScore : 0.25;
return riskScore <= maxRiskScore
? { decision: "allow", riskScore, reason: `risk score is within policy ${policy.id}` }
: { decision: "approval_required", riskScore, reason: `risk score exceeds policy ${policy.id}` };
}
const band = riskBandForScore(riskScore);
if (band === "critical") {
return { decision: "deny", riskScore, reason: "critical risk requires admin override" };
}
if (isPolicyGatedPermission(input.action) || (WRITE_ACTIONS.has(input.action) && band !== "low")) {
return { decision: "approval_required", riskScore, reason: "default gate for risky account action" };
}
return { decision: "allow", riskScore, reason: "grant allows account action" };
}