Record provider auth sessions

This commit is contained in:
ZacharyZhang-NY committed 2026-05-23 23:03:09 -04:00
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} />
+1
View File
@@ -160,6 +160,7 @@ export interface SaveProviderCredentialInput {
providerId: string;
secret: string;
model: string | null;
employeeId?: string | null;
}
export interface RevokeProviderCredentialInput {
+3 -2
View File
@@ -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);