Files
AgentCompany/src/main/services/social-browser-policy-service.ts
T
ZacharyZhang-NY 07d0b4587d Promote AgentCompany-New to the repository root and retire the old app
The legacy application - its src, tests, landing site, promo assets, docs,
release tooling, CI workflows, and root configs - leaves the tree; it stays
reachable in history. Everything under AgentCompany-New moves up unchanged:
same package name, so the production userData/database path is untouched,
and e2e/build scripts already resolved paths relative to the project. The
developer cred.txt stays gitignored at its new location. Full verify:ci
passes from the new root.
2026-07-07 00:04:13 -04:00

166 lines
6.1 KiB
TypeScript

import type { DatabaseSync } from "node:sqlite";
import { SOCIAL_PLATFORM, type RiskLevel, type SocialPlatform } from "../../shared/domain";
import { createId } from "../../shared/ids";
export const SOCIAL_BROWSER_ACTION = {
LOGIN: "login",
DRAFT: "draft",
APPROVE: "approve",
} as const;
export type SocialBrowserAction = typeof SOCIAL_BROWSER_ACTION[keyof typeof SOCIAL_BROWSER_ACTION];
const PLATFORM_HOSTS: Record<SocialPlatform, string[]> = {
[SOCIAL_PLATFORM.X]: ["x.com", "twitter.com"],
[SOCIAL_PLATFORM.LINKEDIN]: ["linkedin.com", "www.linkedin.com"],
[SOCIAL_PLATFORM.YOUTUBE]: ["accounts.google.com", "youtube.com", "www.youtube.com"],
[SOCIAL_PLATFORM.INSTAGRAM]: ["instagram.com", "www.instagram.com"],
};
const ACTION_POLICIES: ReadonlyArray<{
actionType: SocialBrowserAction;
contentCategory: string;
riskLevel: RiskLevel;
approvalRequired: boolean;
approvalKind: string | null;
}> = [
{ actionType: SOCIAL_BROWSER_ACTION.LOGIN, contentCategory: "auth", riskLevel: "low", approvalRequired: false, approvalKind: null },
{ actionType: SOCIAL_BROWSER_ACTION.DRAFT, contentCategory: "content", riskLevel: "low", approvalRequired: false, approvalKind: null },
{ actionType: SOCIAL_BROWSER_ACTION.APPROVE, contentCategory: "content", riskLevel: "medium", approvalRequired: true, approvalKind: "social_publish" },
];
export interface BrowserSessionContainerRow {
id: string;
partition_key: string;
host_allowlist_json: string;
permission_policy: string;
sandbox_enabled: number;
}
export interface BrowserActionPolicyRow {
id: string;
action_type: SocialBrowserAction;
risk_level: RiskLevel;
approval_required: number;
approval_kind: string | null;
}
export interface SocialApprovalRouteRow {
id: string;
approval_kind: string;
route_to_type: string;
route_to_id: string | null;
}
export class SocialBrowserPolicyService {
constructor(private readonly db: DatabaseSync) {}
seedAccountControls(input: {
orgId: string;
socialAccountId: string;
platform: SocialPlatform;
authorizedEmployeeId: string | null;
createdAt: string;
}): void {
this.db.prepare(`
insert or ignore into browser_session_containers(
id, org_id, social_account_id, platform, partition_key, host_allowlist_json,
permission_policy, sandbox_enabled, created_at, last_used_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, null)
`).run(
createId("bsc"),
input.orgId,
input.socialAccountId,
input.platform,
`persist:agentcompany-social-${input.socialAccountId}`,
JSON.stringify(PLATFORM_HOSTS[input.platform]),
"deny_all",
1,
input.createdAt,
);
for (const policy of ACTION_POLICIES) {
this.db.prepare(`
insert or ignore into browser_action_policies(
id, org_id, social_account_id, action_type, content_category, risk_level,
approval_required, approval_kind, created_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
createId("bap"),
input.orgId,
input.socialAccountId,
policy.actionType,
policy.contentCategory,
policy.riskLevel,
policy.approvalRequired ? 1 : 0,
policy.approvalKind,
input.createdAt,
);
}
this.db.prepare(`
insert or ignore into social_approval_routes(
id, org_id, social_account_id, action_type, risk_level, approval_kind,
route_to_type, route_to_id, created_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
createId("sar"),
input.orgId,
input.socialAccountId,
SOCIAL_BROWSER_ACTION.APPROVE,
"medium",
"social_publish",
"authorized_employee",
input.authorizedEmployeeId,
input.createdAt,
);
}
requireSessionContainer(orgId: string, socialAccountId: string): BrowserSessionContainerRow {
const row = this.db.prepare(`
select id, partition_key, host_allowlist_json, permission_policy, sandbox_enabled
from browser_session_containers
where org_id = ? and social_account_id = ?
`).get(orgId, socialAccountId) as BrowserSessionContainerRow | undefined;
if (!row) throw new Error("Browser session container is required.");
this.parseHostAllowlist(row.host_allowlist_json);
if (row.permission_policy !== "deny_all") throw new Error("Browser permission policy is invalid.");
if (row.sandbox_enabled !== 1) throw new Error("Browser sandbox policy is required.");
return row;
}
allowedHosts(container: BrowserSessionContainerRow): string[] {
return this.parseHostAllowlist(container.host_allowlist_json);
}
markSessionUsed(containerId: string, usedAt: string): void {
this.db.prepare("update browser_session_containers set last_used_at = ? where id = ?").run(usedAt, containerId);
}
requireActionPolicy(orgId: string, socialAccountId: string, actionType: SocialBrowserAction): BrowserActionPolicyRow {
const row = this.db.prepare(`
select id, action_type, risk_level, approval_required, approval_kind
from browser_action_policies
where org_id = ? and social_account_id = ? and action_type = ?
`).get(orgId, socialAccountId, actionType) as BrowserActionPolicyRow | undefined;
if (!row) throw new Error("Browser action policy is required.");
return row;
}
requireApprovalRoute(orgId: string, socialAccountId: string, actionType: SocialBrowserAction, riskLevel: RiskLevel): SocialApprovalRouteRow {
const row = this.db.prepare(`
select id, approval_kind, route_to_type, route_to_id
from social_approval_routes
where org_id = ? and social_account_id = ? and action_type = ? and risk_level = ?
`).get(orgId, socialAccountId, actionType, riskLevel) as SocialApprovalRouteRow | undefined;
if (!row) throw new Error("Social approval route is required.");
return row;
}
private parseHostAllowlist(raw: string): string[] {
const parsed = JSON.parse(raw) as unknown;
if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === "string" && item.trim())) {
throw new Error("Browser host allowlist is invalid.");
}
return parsed.map((item) => item.toLowerCase());
}
}