feat(phase-3): workstream 1 — Multi-Tenancy

Introduces full multi-tenant organization model to AgentIdP:

Schema:
- 6 migrations: organizations + organization_members tables; organization_id FK
  added to agents, credentials, audit_logs; PostgreSQL RLS policies on all three
  tables; system org seed + backfill

API:
- 6 new /api/v1/organizations endpoints (CRUD + members) gated by admin:orgs scope
- OPA scopes.json updated with 6 new org endpoint → admin:orgs mappings

Implementation:
- OrgRepository, OrgService, OrgController, createOrgsRouter
- OrgContextMiddleware: sets app.organization_id session variable so RLS enforces
  per-request org isolation at the database layer
- JWT payload extended with organization_id claim; auth.ts backfills org_system
  for backward-compatible tokens
- New error classes: OrgNotFoundError, OrgHasActiveAgentsError, AlreadyMemberError

Tests: 373 passing, 80.64% branch coverage, zero any types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SentryAgent.ai Developer
2026-03-30 00:29:32 +00:00
parent cb7d079ef6
commit d252097f71
31 changed files with 3043 additions and 35 deletions

View File

@@ -28,7 +28,7 @@ export type DeploymentEnv = 'development' | 'staging' | 'production';
export type CredentialStatus = 'active' | 'revoked';
/** OAuth 2.0 scope values supported by this IdP. */
export type OAuthScope = 'agents:read' | 'agents:write' | 'tokens:read' | 'audit:read';
export type OAuthScope = 'agents:read' | 'agents:write' | 'tokens:read' | 'audit:read' | 'admin:orgs';
/** Audit action identifiers for all significant platform events. */
export type AuditAction =
@@ -43,7 +43,11 @@ export type AuditAction =
| 'credential.generated'
| 'credential.rotated'
| 'credential.revoked'
| 'auth.failed';
| 'auth.failed'
| 'org.created'
| 'org.updated'
| 'org.deleted'
| 'org.member_added';
/** Outcome of an audited action. */
export type AuditOutcome = 'success' | 'failure';
@@ -55,6 +59,7 @@ export type AuditOutcome = 'success' | 'failure';
/** Full representation of a registered AI agent identity. */
export interface IAgent {
agentId: string;
organizationId: string;
email: string;
agentType: AgentType;
version: string;
@@ -74,6 +79,7 @@ export interface ICreateAgentRequest {
capabilities: string[];
owner: string;
deploymentEnv: DeploymentEnv;
organizationId?: string;
}
/** Request body for partially updating an agent. */
@@ -172,6 +178,8 @@ export interface ITokenPayload {
iat: number;
/** Expiry (Unix seconds). */
exp: number;
/** Organization the agent belongs to (optional for backward compat). */
organization_id?: string;
}
/** OAuth 2.0 token request (form-encoded). */

90
src/types/organization.ts Normal file
View File

@@ -0,0 +1,90 @@
/**
* Organization and multi-tenancy types for SentryAgent.ai AgentIdP.
* All interfaces and types for the organization/tenant domain live here.
*/
// ============================================================================
// Enumerations / Union Types
// ============================================================================
/** Lifecycle status of an organization. */
export type OrgStatus = 'active' | 'suspended' | 'deleted';
/** Subscription plan tier for an organization. */
export type PlanTier = 'free' | 'pro' | 'enterprise';
/** Role of an agent within an organization. */
export type OrgRole = 'member' | 'admin';
// ============================================================================
// Organization
// ============================================================================
/** Full representation of a registered organization (tenant). */
export interface IOrganization {
organizationId: string;
name: string;
slug: string;
planTier: PlanTier;
maxAgents: number;
maxTokensPerMonth: number;
status: OrgStatus;
createdAt: Date;
updatedAt: Date;
}
/** Request body for creating a new organization. */
export interface ICreateOrgRequest {
name: string;
slug: string;
planTier?: PlanTier;
maxAgents?: number;
maxTokensPerMonth?: number;
}
/**
* Request body for partially updating an organization.
* Note: status may only be set to 'active' or 'suspended' via update;
* 'deleted' is applied only through the soft-delete operation.
*/
export interface IUpdateOrgRequest {
name?: string;
planTier?: PlanTier;
maxAgents?: number;
maxTokensPerMonth?: number;
status?: 'active' | 'suspended';
}
/** Paginated list of organizations. */
export interface IPaginatedOrgsResponse {
data: IOrganization[];
total: number;
page: number;
limit: number;
}
/** Query filters for listing organizations. */
export interface IOrgListFilters {
status?: OrgStatus;
page: number;
limit: number;
}
// ============================================================================
// Organization Membership
// ============================================================================
/** An agent's membership record within an organization. */
export interface IOrgMember {
memberId: string;
organizationId: string;
agentId: string;
role: OrgRole;
joinedAt: Date;
}
/** Request body for adding an agent to an organization. */
export interface IAddMemberRequest {
agentId: string;
role: OrgRole;
}