Record provider auth sessions
This commit is contained in:
1 parent
216d485c58
commit
648544114a
16 files changed
+280
-14
No files matched your search
@@ -0,0 +1,27 @@
|
||||
export const providerAuthSessionsSchema = `
|
||||
create table if not exists provider_auth_sessions (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
provider_id text not null references providers(id) on delete cascade,
|
||||
provider_key text not null,
|
||||
employee_id text not null references employees(id) on delete cascade,
|
||||
state text not null,
|
||||
nonce text not null,
|
||||
code_verifier_sha256 text not null,
|
||||
redirect_uri text not null,
|
||||
sign_in_url text not null,
|
||||
partition text not null,
|
||||
created_at text not null,
|
||||
expires_at text not null,
|
||||
completed_at text,
|
||||
unique(provider_id, state)
|
||||
);
|
||||
|
||||
create index if not exists idx_provider_auth_sessions_org_provider
|
||||
on provider_auth_sessions(org_id, provider_id, created_at);
|
||||
create index if not exists idx_provider_auth_sessions_employee
|
||||
on provider_auth_sessions(org_id, employee_id, created_at);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (34, 'provider_auth_sessions', datetime('now'));
|
||||
`;
|
||||
@@ -32,6 +32,7 @@ import { approvalSourceRefsSchema } from "./migrations/030-approval-source-refs"
|
||||
import { providerRequestWorkspaceSchema } from "./migrations/031-provider-request-workspace";
|
||||
import { browserActionMetadataSchema } from "./migrations/032-browser-action-metadata";
|
||||
import { auditEventImmutabilitySchema } from "./migrations/033-audit-event-immutability";
|
||||
import { providerAuthSessionsSchema } from "./migrations/034-provider-auth-sessions";
|
||||
|
||||
interface Migration {
|
||||
version: number;
|
||||
@@ -73,6 +74,7 @@ const MIGRATIONS: Migration[] = [
|
||||
{ version: 31, name: "provider_request_workspace", sql: providerRequestWorkspaceSchema },
|
||||
{ version: 32, name: "browser_action_metadata", sql: browserActionMetadataSchema },
|
||||
{ version: 33, name: "audit_event_immutability", sql: auditEventImmutabilitySchema },
|
||||
{ version: 34, name: "provider_auth_sessions", sql: providerAuthSessionsSchema },
|
||||
];
|
||||
|
||||
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
||||
|
||||
@@ -157,6 +157,7 @@ export class ComplianceService {
|
||||
roleGrants: this.db.prepare("select * from role_grants where org_id = ? and principal_type = 'employee' and principal_id = ? order by created_at").all(orgId, subjectId),
|
||||
socialAccountsAuthorized: this.db.prepare("select * from social_accounts where org_id = ? and authorized_employee_id = ? order by created_at").all(orgId, subjectId),
|
||||
socialAccountGrants: this.db.prepare("select * from social_account_grants where org_id = ? and employee_id = ? order by created_at").all(orgId, subjectId),
|
||||
providerAuthSessions: this.db.prepare("select * from provider_auth_sessions where org_id = ? and employee_id = ? order by created_at").all(orgId, subjectId),
|
||||
scheduledPostsCreated: this.db.prepare("select * from scheduled_posts where org_id = ? and created_by_employee_id = ? order by created_at").all(orgId, subjectId),
|
||||
complianceExportsRequested: this.db.prepare("select * from compliance_exports where org_id = ? and requested_by_employee_id = ? order by created_at").all(orgId, subjectId),
|
||||
auditEventsAsActor: this.db.prepare("select * from audit_events where org_id = ? and actor_type = 'employee' and actor_id = ? order by created_at").all(orgId, subjectId),
|
||||
@@ -184,6 +185,7 @@ export class ComplianceService {
|
||||
this.db.prepare("delete from employee_identities where org_id = ? and employee_id = ?").run(orgId, subjectId);
|
||||
this.db.prepare("delete from chat_read_state where org_id = ? and employee_id = ?").run(orgId, subjectId);
|
||||
this.db.prepare("delete from social_account_grants where org_id = ? and employee_id = ?").run(orgId, subjectId);
|
||||
this.db.prepare("delete from provider_auth_sessions where org_id = ? and employee_id = ?").run(orgId, subjectId);
|
||||
this.db.prepare("update social_accounts set authorized_employee_id = null where org_id = ? and authorized_employee_id = ?")
|
||||
.run(orgId, subjectId);
|
||||
this.db.prepare("update social_approval_routes set route_to_id = null where org_id = ? and route_to_type = 'authorized_employee' and route_to_id = ?")
|
||||
|
||||
@@ -52,6 +52,8 @@ const LEGACY_TABLES = [
|
||||
"provider_requests",
|
||||
"provider_usage_windows",
|
||||
"provider_rate_limit_observations",
|
||||
"provider_auth_profiles",
|
||||
"provider_auth_sessions",
|
||||
"credentials",
|
||||
"skill_definitions",
|
||||
"skill_revisions",
|
||||
@@ -199,6 +201,12 @@ const RELATION_CHECKS: RelationCheck[] = [
|
||||
{ table: "provider_usage_windows", column: "provider_id", referencedTable: "providers", referencedColumn: "id", reason: "dangling_provider_id" },
|
||||
{ table: "provider_rate_limit_observations", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "provider_rate_limit_observations", column: "provider_id", referencedTable: "providers", referencedColumn: "id", reason: "dangling_provider_id" },
|
||||
{ table: "provider_auth_profiles", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "provider_auth_profiles", column: "provider_id", referencedTable: "providers", referencedColumn: "id", reason: "dangling_provider_id" },
|
||||
{ table: "provider_auth_profiles", column: "credential_id", referencedTable: "credentials", referencedColumn: "id", reason: "dangling_credential_id" },
|
||||
{ table: "provider_auth_sessions", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "provider_auth_sessions", column: "provider_id", referencedTable: "providers", referencedColumn: "id", reason: "dangling_provider_id" },
|
||||
{ table: "provider_auth_sessions", column: "employee_id", referencedTable: "employees", referencedColumn: "id", reason: "dangling_employee_id" },
|
||||
{ table: "credentials", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "skill_definitions", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "skill_revisions", column: "skill_id", referencedTable: "skill_definitions", referencedColumn: "id", reason: "dangling_skill_id" },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import type { CancelProviderResponseInput, CreateProviderResponseInput, LaunchProviderSignInInput, RevokeProviderCredentialInput, SaveProviderCredentialInput, StreamProviderResponseInput, UpdateProviderPolicyInput } from "../../shared/commands";
|
||||
import { FEATURE_FLAG_KEY, PROVIDER_AUTH_TYPE, PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE, PROVIDER_REQUEST_STATE, type ProviderAuthType, type ProviderExecutionMode } from "../../shared/domain";
|
||||
import type { ProviderStreamRendererEvent } from "../../shared/provider-request-domain";
|
||||
@@ -23,6 +24,7 @@ const HEALTH_STATE_HEALTHY = "healthy";
|
||||
const HEALTH_STATE_FAILED = "failed";
|
||||
const HEALTH_STATE_UNCHECKED = "unchecked";
|
||||
const WORKSPACE_SCOPES = new Set(["organization", "workspace"]);
|
||||
const AUTH_SESSION_TTL_MS = 15 * 60 * 1000;
|
||||
|
||||
export type ProviderResponseCreateInput = CreateProviderResponseInput & {
|
||||
onRequestCreated?: (requestId: string) => void;
|
||||
@@ -61,6 +63,13 @@ export class ProviderService {
|
||||
const adapter = this.requireAdapter(provider.provider_key, provider.auth_type);
|
||||
adapter.parseAuthProfile(secret);
|
||||
const catalogEntry = this.requireCatalogEntry(provider.provider_key);
|
||||
const source = this.resolveCredentialSource({
|
||||
orgId: input.orgId,
|
||||
providerId: input.providerId,
|
||||
providerKey: provider.provider_key,
|
||||
authType: provider.auth_type,
|
||||
employeeId: input.employeeId ?? null,
|
||||
});
|
||||
const authProfile = this.authBroker.createProviderProfile({
|
||||
orgId: input.orgId,
|
||||
providerId: input.providerId,
|
||||
@@ -68,8 +77,8 @@ export class ProviderService {
|
||||
authType: provider.auth_type,
|
||||
secret,
|
||||
profileKind: provider.auth_type,
|
||||
sourceType: "manual_secret",
|
||||
sourceRef: null,
|
||||
sourceType: source.sourceType,
|
||||
sourceRef: source.sourceRef,
|
||||
capabilityTags: catalogEntry.capabilityTags,
|
||||
});
|
||||
this.db.prepare(`
|
||||
@@ -84,7 +93,7 @@ export class ProviderService {
|
||||
actorType: "system",
|
||||
actorId: null,
|
||||
action: "credential_saved",
|
||||
payload: { providerKey: provider.provider_key, authType: provider.auth_type, authProfileId: authProfile.authProfileId },
|
||||
payload: { providerKey: provider.provider_key, authType: provider.auth_type, authProfileId: authProfile.authProfileId, sourceType: source.sourceType, sourceRef: source.sourceRef },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,7 +161,14 @@ export class ProviderService {
|
||||
if (!provider) throw new Error("Provider was not found.");
|
||||
const catalogEntry = this.requireCatalogEntry(provider.provider_key);
|
||||
if (!catalogEntry.signInUrl) throw new Error(`${provider.label} does not support browser sign-in.`);
|
||||
this.launchSignIn({ providerId: provider.id, label: provider.label, signInUrl: catalogEntry.signInUrl });
|
||||
const session = this.createAuthSession({
|
||||
orgId: input.orgId,
|
||||
providerId: provider.id,
|
||||
providerKey: provider.provider_key,
|
||||
employeeId: input.employeeId,
|
||||
signInUrl: catalogEntry.signInUrl,
|
||||
});
|
||||
this.launchSignIn({ providerId: provider.id, label: provider.label, signInUrl: catalogEntry.signInUrl, partition: session.partition });
|
||||
|
||||
this.audit.record({
|
||||
orgId: input.orgId,
|
||||
@@ -161,7 +177,16 @@ export class ProviderService {
|
||||
actorType: "employee",
|
||||
actorId: input.employeeId,
|
||||
action: "sign_in_window_opened",
|
||||
payload: { providerKey: provider.provider_key, authType: provider.auth_type },
|
||||
payload: {
|
||||
providerKey: provider.provider_key,
|
||||
authType: provider.auth_type,
|
||||
authSessionId: session.id,
|
||||
state: session.state,
|
||||
nonce: session.nonce,
|
||||
codeVerifierSha256: session.codeVerifierSha256,
|
||||
redirectUri: session.redirectUri,
|
||||
partition: session.partition,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -473,6 +498,75 @@ export class ProviderService {
|
||||
if (!employee) throw new Error("Provider sign-in employee was not found.");
|
||||
}
|
||||
|
||||
private resolveCredentialSource(input: {
|
||||
orgId: string;
|
||||
providerId: string;
|
||||
providerKey: string;
|
||||
authType: ProviderAuthType;
|
||||
employeeId: string | null;
|
||||
}): { sourceType: string; sourceRef: string | null } {
|
||||
if (input.authType !== PROVIDER_AUTH_TYPE.OAUTH_PKCE && input.authType !== PROVIDER_AUTH_TYPE.ACCOUNT_SUBSCRIPTION) {
|
||||
return { sourceType: "manual_secret", sourceRef: null };
|
||||
}
|
||||
if (!input.employeeId) throw new Error("Provider sign-in employee is required.");
|
||||
const session = this.db.prepare(`
|
||||
select id
|
||||
from provider_auth_sessions
|
||||
where org_id = ?
|
||||
and provider_id = ?
|
||||
and provider_key = ?
|
||||
and employee_id = ?
|
||||
and completed_at is null
|
||||
and expires_at > ?
|
||||
order by created_at desc
|
||||
limit 1
|
||||
`).get(input.orgId, input.providerId, input.providerKey, input.employeeId, new Date().toISOString()) as { id: string } | undefined;
|
||||
if (!session) throw new Error("Provider sign-in session is required.");
|
||||
this.db.prepare("update provider_auth_sessions set completed_at = ? where id = ? and org_id = ?")
|
||||
.run(new Date().toISOString(), session.id, input.orgId);
|
||||
return { sourceType: "provider_sign_in_session", sourceRef: session.id };
|
||||
}
|
||||
|
||||
private createAuthSession(input: {
|
||||
orgId: string;
|
||||
providerId: string;
|
||||
providerKey: string;
|
||||
employeeId: string;
|
||||
signInUrl: string;
|
||||
}): ProviderAuthSessionDraft {
|
||||
const id = createProviderAuthSessionId();
|
||||
const state = randomToken();
|
||||
const nonce = randomToken();
|
||||
const codeVerifier = randomToken();
|
||||
const codeVerifierSha256 = createHash("sha256").update(codeVerifier).digest("hex");
|
||||
const redirectUri = `agentcompany://provider-auth/${input.providerId}`;
|
||||
const partition = `persist:agentcompany-provider-${input.providerId}`;
|
||||
const createdAt = new Date();
|
||||
const expiresAt = new Date(createdAt.getTime() + AUTH_SESSION_TTL_MS);
|
||||
this.db.prepare(`
|
||||
insert into provider_auth_sessions(
|
||||
id, org_id, provider_id, provider_key, employee_id, state, nonce,
|
||||
code_verifier_sha256, redirect_uri, sign_in_url, partition,
|
||||
created_at, expires_at, completed_at
|
||||
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, null)
|
||||
`).run(
|
||||
id,
|
||||
input.orgId,
|
||||
input.providerId,
|
||||
input.providerKey,
|
||||
input.employeeId,
|
||||
state,
|
||||
nonce,
|
||||
codeVerifierSha256,
|
||||
redirectUri,
|
||||
input.signInUrl,
|
||||
partition,
|
||||
createdAt.toISOString(),
|
||||
expiresAt.toISOString(),
|
||||
);
|
||||
return { id, state, nonce, codeVerifierSha256, redirectUri, partition };
|
||||
}
|
||||
|
||||
private recordProviderHealth(providerId: string, orgId: string, healthState: string, error: string | null): void {
|
||||
this.db.prepare(`
|
||||
update providers
|
||||
@@ -522,3 +616,20 @@ interface ProviderRow {
|
||||
monthly_request_limit: number;
|
||||
workspace_scope: string;
|
||||
}
|
||||
|
||||
interface ProviderAuthSessionDraft {
|
||||
id: string;
|
||||
state: string;
|
||||
nonce: string;
|
||||
codeVerifierSha256: string;
|
||||
redirectUri: string;
|
||||
partition: string;
|
||||
}
|
||||
|
||||
function createProviderAuthSessionId(): string {
|
||||
return `auths_${randomToken()}`;
|
||||
}
|
||||
|
||||
function randomToken(): string {
|
||||
return randomBytes(24).toString("base64url");
|
||||
}
|
||||
@@ -4,11 +4,11 @@ export interface LaunchProviderSignInWindowInput {
|
||||
providerId: string;
|
||||
label: string;
|
||||
signInUrl: string;
|
||||
partition: string;
|
||||
}
|
||||
|
||||
export function launchProviderSignInWindow(input: LaunchProviderSignInWindowInput): void {
|
||||
const partition = `persist:agentcompany-provider-${input.providerId}`;
|
||||
const providerSession = session.fromPartition(partition);
|
||||
const providerSession = session.fromPartition(input.partition);
|
||||
providerSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false));
|
||||
providerSession.setPermissionCheckHandler(() => false);
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ export class SnapshotService {
|
||||
.map((provider) => ({ ...provider, enabled: toBool(provider.enabled), backgroundEnabled: toBool(provider.backgroundEnabled) }));
|
||||
const providerAuthProfiles = list<AppSnapshot["providerAuthProfiles"][number]>(this.db, "provider_auth_profiles", "created_at desc")
|
||||
.map((profile) => ({ ...profile, active: toBool(profile.active) }));
|
||||
const providerAuthSessions = list<AppSnapshot["providerAuthSessions"][number]>(this.db, "provider_auth_sessions", "created_at desc");
|
||||
const providerRateLimitObservations = list<AppSnapshot["providerRateLimitObservations"][number]>(
|
||||
this.db,
|
||||
"provider_rate_limit_observations",
|
||||
@@ -107,6 +108,7 @@ export class SnapshotService {
|
||||
scheduledPosts: list(this.db, "scheduled_posts", "scheduled_for asc"),
|
||||
providers,
|
||||
providerAuthProfiles,
|
||||
providerAuthSessions,
|
||||
providerCatalog: PROVIDER_CATALOG,
|
||||
providerRequests: list(this.db, "provider_requests", "created_at desc"),
|
||||
providerRateLimits,
|
||||
|
||||
@@ -65,6 +65,7 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
const employees = useMemo(() => snapshot.employees.filter((item) => item.orgId === orgId), [snapshot.employees, orgId]);
|
||||
const agents = useMemo(() => snapshot.agents.filter((item) => item.orgId === orgId), [snapshot.agents, orgId]);
|
||||
const providers = useMemo(() => snapshot.providers.filter((item) => item.orgId === orgId), [snapshot.providers, orgId]);
|
||||
const providerAuthProfiles = useMemo(() => snapshot.providerAuthProfiles.filter((item) => item.orgId === orgId), [snapshot.providerAuthProfiles, orgId]);
|
||||
const providerRateLimits = useMemo(() => snapshot.providerRateLimits.filter((item) => item.orgId === orgId), [snapshot.providerRateLimits, orgId]);
|
||||
const skills = useMemo(() => snapshot.skillDefinitions.filter((item) => item.orgId === orgId), [snapshot.skillDefinitions, orgId]);
|
||||
const skillRevisions = useMemo(() => {
|
||||
@@ -137,6 +138,7 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
orgId={orgId}
|
||||
employeeId={employee.id}
|
||||
providers={providers}
|
||||
authProfiles={providerAuthProfiles}
|
||||
catalog={snapshot.providerCatalog}
|
||||
agents={agents}
|
||||
workspaces={workspaces}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Dialog } from "../../shared/Dialog";
|
||||
import type {
|
||||
AgentRecord,
|
||||
ProviderCatalogEntry,
|
||||
ProviderAuthProfileRecord,
|
||||
ProviderRateLimitStateRecord,
|
||||
ProviderRecord,
|
||||
ProviderRequestRecord,
|
||||
@@ -18,6 +19,7 @@ interface ProvidersPanelProps {
|
||||
orgId: string;
|
||||
employeeId: string;
|
||||
providers: ProviderRecord[];
|
||||
authProfiles: ProviderAuthProfileRecord[];
|
||||
catalog: ProviderCatalogEntry[];
|
||||
agents: AgentRecord[];
|
||||
workspaces: WorkspaceRecord[];
|
||||
@@ -37,7 +39,7 @@ const CLOUD_DEFAULTS: CloudFields = { baseUrl: "", region: "", projectId: "", de
|
||||
const POLICY_GRID = "minmax(150px, 1.2fr) minmax(118px, 0.8fr) minmax(140px, 1fr) 112px 124px 92px";
|
||||
type WorkspaceScope = "organization" | "workspace";
|
||||
|
||||
export function ProvidersPanel({ orgId, employeeId, providers, catalog, agents, workspaces, providerRateLimits, providerRequests, actions }: ProvidersPanelProps) {
|
||||
export function ProvidersPanel({ orgId, employeeId, providers, authProfiles, catalog, agents, workspaces, providerRateLimits, providerRequests, actions }: ProvidersPanelProps) {
|
||||
const [active, setActive] = useState<ProviderRecord | null>(null);
|
||||
const [signInLaunched, setSignInLaunched] = useState(false);
|
||||
const connected = providers.filter((provider) => provider.enabled);
|
||||
@@ -59,6 +61,7 @@ export function ProvidersPanel({ orgId, employeeId, providers, catalog, agents,
|
||||
providerId: active.id,
|
||||
secret,
|
||||
model: String(form.get("model") ?? "").trim() || null,
|
||||
employeeId,
|
||||
})).then((result) => { if (result) close(); });
|
||||
};
|
||||
|
||||
@@ -81,6 +84,7 @@ export function ProvidersPanel({ orgId, employeeId, providers, catalog, agents,
|
||||
<ProviderRow
|
||||
key={provider.id}
|
||||
provider={provider}
|
||||
authProfile={authProfiles.find((profile) => profile.providerId === provider.id && profile.active) ?? null}
|
||||
agents={agents}
|
||||
onCheck={() => void actions.run(() => window.agentCompany.checkProviderHealth({ orgId, providerId: provider.id }))}
|
||||
onManage={() => setActive(provider)}
|
||||
@@ -388,12 +392,14 @@ function composeSecret(entry: ProviderCatalogEntry, form: FormData): string {
|
||||
|
||||
function ProviderRow({
|
||||
provider,
|
||||
authProfile,
|
||||
agents,
|
||||
onCheck,
|
||||
onManage,
|
||||
onRevoke,
|
||||
}: {
|
||||
provider: ProviderRecord;
|
||||
authProfile: ProviderAuthProfileRecord | null;
|
||||
agents: AgentRecord[];
|
||||
onCheck: () => void;
|
||||
onManage: () => void;
|
||||
@@ -415,8 +421,8 @@ function ProviderRow({
|
||||
<ProviderLogo label={provider.label} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 13.5, fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{provider.label}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)" }}>
|
||||
{provider.model ? `${provider.model} · ` : ""}{readableAuth(provider.authType)}
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{provider.model ? `${provider.model} · ` : ""}{readableAuth(provider.authType)}{authProfile ? ` · ${authProfile.sourceType}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<ProviderHealth provider={provider} />
|
||||
|
||||
@@ -160,6 +160,7 @@ export interface SaveProviderCredentialInput {
|
||||
providerId: string;
|
||||
secret: string;
|
||||
model: string | null;
|
||||
employeeId?: string | null;
|
||||
}
|
||||
|
||||
export interface RevokeProviderCredentialInput {
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ComplianceExportRecord, LegalHoldRecord, RetentionFreezeRecord } f
|
||||
import type { AccessDecisionRecord, EmployeeIdentityRecord, RoleGrantRecord, ServiceIdentityRecord } from "./identity-domain";
|
||||
import type { ApprovalAssignmentRecord, ApprovalRoutingRuleRecord, HiringPolicyRecord, OrgDepartmentRecord } from "./org-governance-domain";
|
||||
import type { ChaosDrillRecord, ChaosDrillRunRecord, FeatureFlagRecord, OperationalEventRecord, SloTargetRecord } from "./operations-domain";
|
||||
import type { ProviderAuthProfileRecord } from "./provider-auth-domain";
|
||||
import type { ProviderAuthProfileRecord, ProviderAuthSessionRecord } from "./provider-auth-domain";
|
||||
import type { ProviderDataPolicy } from "./provider-policy-domain";
|
||||
import type { ProviderRequestRecord } from "./provider-request-domain";
|
||||
import type { ProviderRateLimitObservationRecord, ProviderRateLimitStateRecord } from "./provider-runtime-domain";
|
||||
@@ -17,7 +17,7 @@ export { COMPLIANCE_EXPORT_STATE, COMPLIANCE_EXPORT_TYPE, type ComplianceExportR
|
||||
export { ACCESS_DECISION, IDENTITY_SOURCE, ROLE_KEY, type AccessDecision, type AccessDecisionRecord, type EmployeeIdentityRecord, type IdentitySource, type PrincipalType, type RoleGrantRecord, type RoleKey, type ServiceIdentityRecord } from "./identity-domain";
|
||||
export { ROUTE_TO_TYPE, type ApprovalAssignmentRecord, type ApprovalRoutingRuleRecord, type HiringPolicyRecord, type OrgDepartmentRecord, type RouteToType } from "./org-governance-domain";
|
||||
export { CHAOS_DRILL_SCENARIO, CHAOS_DRILL_STATE, FEATURE_FLAG_KEY, SLO_SIGNAL_KEY, type ChaosDrillRecord, type ChaosDrillRunRecord, type ChaosDrillScenario, type ChaosDrillState, type FeatureFlagKey, type FeatureFlagRecord, type OperationalEventRecord, type SloSignalKey, type SloTargetRecord } from "./operations-domain";
|
||||
export type { ProviderAuthProfileRecord } from "./provider-auth-domain";
|
||||
export type { ProviderAuthProfileRecord, ProviderAuthSessionRecord } from "./provider-auth-domain";
|
||||
export { PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE, type ProviderDataPolicy, type ProviderExecutionMode } from "./provider-policy-domain";
|
||||
export { PROVIDER_REQUEST_STATE, type ProviderRequestRecord, type ProviderRequestState, type ProviderStreamRendererEvent } from "./provider-request-domain";
|
||||
export type { ProviderRateLimitObservationRecord, ProviderRateLimitStateRecord } from "./provider-runtime-domain";
|
||||
@@ -465,6 +465,7 @@ export interface AppSnapshot {
|
||||
scheduledPosts: ScheduledPostRecord[];
|
||||
providers: ProviderRecord[];
|
||||
providerAuthProfiles: ProviderAuthProfileRecord[];
|
||||
providerAuthSessions: ProviderAuthSessionRecord[];
|
||||
providerCatalog: ProviderCatalogEntry[];
|
||||
providerRequests: ProviderRequestRecord[];
|
||||
providerRateLimits: ProviderRateLimitStateRecord[];
|
||||
|
||||
@@ -15,3 +15,20 @@ export interface ProviderAuthProfileRecord {
|
||||
active: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ProviderAuthSessionRecord {
|
||||
id: string;
|
||||
orgId: string;
|
||||
providerId: string;
|
||||
providerKey: string;
|
||||
employeeId: string;
|
||||
state: string;
|
||||
nonce: string;
|
||||
codeVerifierSha256: string;
|
||||
redirectUri: string;
|
||||
signInUrl: string;
|
||||
partition: string;
|
||||
createdAt: string;
|
||||
expiresAt: string;
|
||||
completedAt: string | null;
|
||||
}
|
||||
@@ -107,6 +107,28 @@ describe("compliance service", () => {
|
||||
createdByEmployeeId: employeeId,
|
||||
authorizedEmployeeId: employeeId,
|
||||
});
|
||||
const providerId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'github-copilot'")
|
||||
.get(orgId) as { id: string }).id;
|
||||
connection.db.prepare(`
|
||||
insert into provider_auth_sessions(
|
||||
id, org_id, provider_id, provider_key, employee_id, state, nonce, code_verifier_sha256,
|
||||
redirect_uri, sign_in_url, partition, created_at, expires_at, completed_at
|
||||
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, null)
|
||||
`).run(
|
||||
"auths-test",
|
||||
orgId,
|
||||
providerId,
|
||||
"github-copilot",
|
||||
employeeId,
|
||||
"state",
|
||||
"nonce",
|
||||
"a".repeat(64),
|
||||
`agentcompany://provider-auth/${providerId}`,
|
||||
"https://github.com/login",
|
||||
`persist:agentcompany-provider-${providerId}`,
|
||||
new Date().toISOString(),
|
||||
new Date(Date.now() + 60000).toISOString(),
|
||||
);
|
||||
holdId = compliance.createLegalHold({
|
||||
orgId,
|
||||
entityType: "employee",
|
||||
@@ -144,6 +166,7 @@ describe("compliance service", () => {
|
||||
expect(redactedApproval?.decidedByEmployeeId).toBeNull();
|
||||
expect(redactedSocialAccount?.authorizedEmployeeId).toBeNull();
|
||||
expect(state.socialAccountGrants).toHaveLength(0);
|
||||
expect(state.providerAuthSessions).toHaveLength(0);
|
||||
expect(state.complianceExports[0]?.exportType).toBe("gdpr_delete");
|
||||
expect(state.complianceExports[0]?.requestedByEmployeeId).toBeNull();
|
||||
expect(state.auditEvents.some((event) => event.entityType === "compliance_export" && event.action === "gdpr_deleted")).toBe(true);
|
||||
|
||||
@@ -23,7 +23,7 @@ describe("domain services", () => {
|
||||
const connection = createConnection();
|
||||
|
||||
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]);
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
|
||||
@@ -152,6 +152,8 @@ describe("migration service", () => {
|
||||
create table provider_requests(id text primary key, org_id text, provider_id text, workspace_id text);
|
||||
create table provider_usage_windows(id text primary key, org_id text, provider_id text);
|
||||
create table provider_rate_limit_observations(id text primary key, org_id text, provider_id text);
|
||||
create table provider_auth_profiles(id text primary key, org_id text, provider_id text, credential_id text);
|
||||
create table provider_auth_sessions(id text primary key, org_id text, provider_id text, employee_id text);
|
||||
create table credentials(id text primary key, org_id text);
|
||||
create table skill_definitions(id text primary key, org_id text);
|
||||
create table skill_revisions(id text primary key, skill_id text);
|
||||
@@ -228,6 +230,8 @@ describe("migration service", () => {
|
||||
insert into provider_requests(id, org_id, provider_id, workspace_id) values ('req-1', 'org-1', 'prov-1', 'wsp-1');
|
||||
insert into provider_usage_windows(id, org_id, provider_id) values ('usage-1', 'org-1', 'prov-1');
|
||||
insert into provider_rate_limit_observations(id, org_id, provider_id) values ('quota-1', 'org-1', 'prov-1');
|
||||
insert into provider_auth_profiles(id, org_id, provider_id, credential_id) values ('authp-1', 'org-1', 'prov-1', 'cred-1');
|
||||
insert into provider_auth_sessions(id, org_id, provider_id, employee_id) values ('auths-1', 'org-1', 'prov-1', 'emp-1');
|
||||
insert into skill_definitions(id, org_id) values ('skill-1', 'org-1');
|
||||
insert into skill_revisions(id, skill_id) values ('rev-1', 'skill-1'), ('rev-2', 'skill-1');
|
||||
insert into skill_artifacts(id, revision_id) values ('artifact-1', 'rev-1');
|
||||
@@ -289,6 +293,8 @@ describe("migration service", () => {
|
||||
expect(body.tableCounts.provider_requests).toBe(1);
|
||||
expect(body.tableCounts.provider_usage_windows).toBe(1);
|
||||
expect(body.tableCounts.provider_rate_limit_observations).toBe(1);
|
||||
expect(body.tableCounts.provider_auth_profiles).toBe(1);
|
||||
expect(body.tableCounts.provider_auth_sessions).toBe(1);
|
||||
expect(body.tableCounts.browser_session_containers).toBe(1);
|
||||
expect(body.tableCounts.browser_action_policies).toBe(1);
|
||||
expect(body.tableCounts.social_approval_routes).toBe(1);
|
||||
@@ -317,6 +323,8 @@ describe("migration service", () => {
|
||||
expect(body.idMaps.migration_reconciliation_items).toEqual(["reconcile-1"]);
|
||||
expect(body.idMaps.provider_usage_windows).toEqual(["usage-1"]);
|
||||
expect(body.idMaps.provider_rate_limit_observations).toEqual(["quota-1"]);
|
||||
expect(body.idMaps.provider_auth_profiles).toEqual(["authp-1"]);
|
||||
expect(body.idMaps.provider_auth_sessions).toEqual(["auths-1"]);
|
||||
expect(body.idMaps.browser_session_containers).toEqual(["container-1"]);
|
||||
expect(body.idMaps.browser_action_policies).toEqual(["browser-policy-1"]);
|
||||
expect(body.idMaps.social_approval_routes).toEqual(["social-route-1"]);
|
||||
|
||||
@@ -173,7 +173,7 @@ describe("provider service", () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const snapshot = new SnapshotService(connection.db);
|
||||
let launched: { providerId: string; label: string; signInUrl: string } | null = null;
|
||||
let launched: { providerId: string; label: string; signInUrl: string; partition: string } | null = null;
|
||||
const providers = new ProviderService(connection.db, providerAdapters, testVault, (input) => { launched = input; });
|
||||
let orgId = "";
|
||||
let employeeId = "";
|
||||
@@ -198,7 +198,19 @@ describe("provider service", () => {
|
||||
providerId: copilotProviderId,
|
||||
label: "GitHub Copilot",
|
||||
signInUrl: "https://github.com/login",
|
||||
partition: `persist:agentcompany-provider-${copilotProviderId}`,
|
||||
});
|
||||
expect(state.providerAuthSessions[0]).toMatchObject({
|
||||
orgId,
|
||||
providerId: copilotProviderId,
|
||||
providerKey: "github-copilot",
|
||||
employeeId,
|
||||
partition: `persist:agentcompany-provider-${copilotProviderId}`,
|
||||
completedAt: null,
|
||||
});
|
||||
expect(state.providerAuthSessions[0]?.state).toHaveLength(32);
|
||||
expect(state.providerAuthSessions[0]?.nonce).toHaveLength(32);
|
||||
expect(state.providerAuthSessions[0]?.codeVerifierSha256).toHaveLength(64);
|
||||
expect(state.auditEvents).toContainEqual(expect.objectContaining({
|
||||
entityType: "provider",
|
||||
entityId: copilotProviderId,
|
||||
@@ -209,6 +221,50 @@ describe("provider service", () => {
|
||||
connection.close();
|
||||
});
|
||||
|
||||
test("binds OAuth credentials to a sign-in session source", () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const snapshot = new SnapshotService(connection.db);
|
||||
const providers = new ProviderService(connection.db, providerAdapters, testVault, () => {});
|
||||
let orgId = "";
|
||||
let employeeId = "";
|
||||
let copilotProviderId = "";
|
||||
|
||||
connection.transaction(() => {
|
||||
orgId = createOrganization(organization);
|
||||
employeeId = (connection.db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id;
|
||||
copilotProviderId = providerId(connection, orgId, "github-copilot");
|
||||
});
|
||||
|
||||
expect(() => connection.transaction(() => providers.saveCredential({
|
||||
orgId,
|
||||
providerId: copilotProviderId,
|
||||
secret: "ghu_live_provider",
|
||||
model: "claude-opus-4-5",
|
||||
employeeId,
|
||||
}))).toThrow("Provider sign-in session is required.");
|
||||
|
||||
connection.transaction(() => {
|
||||
providers.launchSignInWindow({ orgId, providerId: copilotProviderId, employeeId });
|
||||
providers.saveCredential({
|
||||
orgId,
|
||||
providerId: copilotProviderId,
|
||||
secret: "ghu_live_provider",
|
||||
model: "claude-opus-4-5",
|
||||
employeeId,
|
||||
});
|
||||
});
|
||||
|
||||
const state = snapshot.load();
|
||||
const profile = state.providerAuthProfiles.find((item) => item.providerId === copilotProviderId && item.active);
|
||||
const session = state.providerAuthSessions.find((item) => item.providerId === copilotProviderId);
|
||||
if (!profile || !session) throw new Error("Provider auth evidence was not created.");
|
||||
expect(profile).toMatchObject({ sourceType: "provider_sign_in_session", sourceRef: session.id });
|
||||
expect(session.completedAt).toBeTruthy();
|
||||
expect(state.providers.find((item) => item.id === copilotProviderId)?.enabled).toBe(true);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
test("records provider request results through direct execution adapters", async () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
|
||||
Reference in new issue
Block a user