Mimir Teams API Contracts
Package: @codedir/mimir-teams-api-contracts
Purpose: Shared TypeScript types, Zod schemas, and API client for Mimir Teams backend.
Overview
The API contracts package is a single source of truth for the interface between:
@codedir/mimir-code(CLI)@codedir/mimir-teams(Backend)
This ensures type safety and prevents breaking changes.
Package Structure
@codedir/mimir-teams-api-contracts/
├── src/
│ ├── v1/
│ │ ├── types/
│ │ │ ├── auth.ts
│ │ │ ├── organizations.ts
│ │ │ ├── teams.ts
│ │ │ ├── tools.ts
│ │ │ ├── config.ts
│ │ │ ├── audit.ts
│ │ │ ├── llm.ts
│ │ │ ├── conversations.ts
│ │ │ └── index.ts
│ │ ├── schemas/
│ │ │ ├── auth.schema.ts
│ │ │ ├── organizations.schema.ts
│ │ │ ├── teams.schema.ts
│ │ │ ├── tools.schema.ts
│ │ │ ├── config.schema.ts
│ │ │ ├── audit.schema.ts
│ │ │ ├── llm.schema.ts
│ │ │ ├── conversations.schema.ts
│ │ │ └── index.ts
│ │ ├── client/
│ │ │ ├── TeamsAPIClient.ts
│ │ │ ├── endpoints/
│ │ │ │ ├── auth.ts
│ │ │ │ ├── organizations.ts
│ │ │ │ ├── teams.ts
│ │ │ │ ├── tools.ts
│ │ │ │ ├── audit.ts
│ │ │ │ ├── llm.ts
│ │ │ │ └── conversations.ts
│ │ │ └── index.ts
│ │ └── index.ts
│ └── index.ts (re-exports v1 as default)
├── package.json
├── tsconfig.json
├── README.md
└── CHANGELOG.mdType Definitions
Auth Types
// src/v1/types/auth.ts
export interface LoginRequest {
email: string;
password: string;
}
export interface LoginResponse {
access_token: string;
refresh_token: string;
expires_in: number; // seconds
user: UserProfile;
}
export interface RefreshTokenRequest {
refresh_token: string;
}
export interface RefreshTokenResponse {
access_token: string;
refresh_token: string;
expires_in: number;
}
export interface UserProfile {
id: string;
email: string;
full_name: string;
avatar_url?: string;
organization: {
slug: string;
name: string;
role: 'owner' | 'admin' | 'member' | 'auditor';
};
active_team?: {
id: string;
slug: string;
name: string;
role: 'admin' | 'developer' | 'viewer';
};
}
export interface AuthContext {
accessToken: string;
refreshToken: string;
expiresAt: Date;
orgSlug: string;
teamId?: string;
userId: string;
userEmail: string;
}Organization Types
// src/v1/types/organizations.ts
export interface Organization {
id: string;
slug: string;
name: string;
subscription_tier: 'free' | 'teams' | 'enterprise';
subscription_status: 'active' | 'trial' | 'suspended' | 'cancelled';
features: SubscriptionFeatures;
budget?: OrganizationBudget;
}
export interface SubscriptionFeatures {
max_teams: number; // -1 = unlimited
max_members: number; // -1 = unlimited
sso: boolean;
llm_proxy: boolean;
audit_retention_days: number;
support_sla: 'community' | 'email' | 'priority';
custom_tools: boolean;
conversation_sync: boolean;
cloud_sandbox: boolean;
on_premise: boolean;
api_access: boolean;
}
export interface OrganizationBudget {
monthly_usd?: number; // undefined = unlimited
alert_threshold: number; // 0-1 (e.g., 0.8 = 80%)
current_usage: number;
}
export interface GetOrganizationConfigRequest {
org_slug: string;
team_id?: string; // Optional team context
}
export interface GetOrganizationConfigResponse {
organization: OrganizationConfig;
team?: TeamConfig;
}
export interface OrganizationConfig {
slug: string;
name: string;
subscription_tier: 'free' | 'teams' | 'enterprise';
features: SubscriptionFeatures;
enforcement: EnforcementConfig;
budget: OrganizationBudget;
api_url: string;
}
export interface EnforcementConfig {
allowed_models: string[]; // ['*'] = all
blocked_models: string[];
allowed_providers: string[]; // ['*'] = all
allowed_sub_agents: string[]; // ['*'] = all
forced_sub_agents: Record<string, ForcedSubAgent>;
global_allowlist: string[];
global_blocklist: string[];
docker_mode: 'local' | 'cloud' | 'auto';
}
export interface ForcedSubAgent {
enabled: boolean;
model: string;
trigger: 'always' | 'on-write' | 'on-commit' | 'manual';
}Team Types
// src/v1/types/teams.ts
export interface Team {
id: string;
org_id: string;
slug: string;
name: string;
description?: string;
budget?: TeamBudget;
repositories: Repository[];
member_count: number;
created_at: string; // ISO 8601
}
export interface TeamBudget {
monthly_usd?: number;
percentage?: number; // % of org budget
current_usage: number;
}
export interface Repository {
url: string;
branch: string;
}
export interface TeamConfig {
id: string;
slug: string;
name: string;
config: TeamConfigOverrides;
}
export interface TeamConfigOverrides {
allowed_models?: string[]; // Subset of org
blocked_models?: string[];
allowed_providers?: string[];
allowed_sub_agents?: string[];
forced_sub_agents?: Record<string, ForcedSubAgent>;
enabled_tools?: string[];
disabled_tools?: string[];
team_allowlist?: string[];
team_blocklist?: string[];
custom_settings?: Record<string, any>;
}
export interface ListTeamsRequest {
org_slug: string;
}
export interface ListTeamsResponse {
teams: Team[];
}Tool Types
// src/v1/types/tools.ts
export interface CustomTool {
id: string;
name: string;
description: string;
version: string;
source: 'org' | 'team';
definition: CustomToolDefinition;
code: string;
compiled_code?: string;
is_active: boolean;
created_at: string;
}
export interface CustomToolDefinition {
name: string;
description: string;
enabled: boolean;
tokenCost?: number;
schema: JSONSchema;
runtime: 'typescript' | 'node';
permissions?: {
allowlist?: string[];
autoAccept?: boolean;
riskLevel?: 'low' | 'medium' | 'high' | 'critical';
};
}
export interface JSONSchema {
type: 'object';
properties: Record<string, JSONSchemaProperty>;
required?: string[];
}
export interface JSONSchemaProperty {
type: 'string' | 'number' | 'boolean' | 'array' | 'object';
description?: string;
enum?: string[];
items?: JSONSchemaProperty;
properties?: Record<string, JSONSchemaProperty>;
}
export interface ListToolsRequest {
org_slug: string;
team_id?: string;
}
export interface ListToolsResponse {
tools: CustomTool[];
}
export interface CustomCommand {
id: string;
name: string;
description: string;
usage: string;
source: 'org' | 'team';
definition: CustomCommandDefinition;
is_active: boolean;
created_at: string;
}
export interface CustomCommandDefinition {
name: string;
description: string;
usage: string;
aliases?: string[];
prompt: string;
}
export interface ListCommandsRequest {
org_slug: string;
team_id?: string;
}
export interface ListCommandsResponse {
commands: CustomCommand[];
}Audit Types
// src/v1/types/audit.ts
export interface AuditLogEntry {
id: string; // Generated by CLI
team_id?: string;
user_id: string;
event_type: string;
action: string;
repository?: string;
branch?: string;
working_directory?: string;
risk_level: 'low' | 'medium' | 'high' | 'critical';
approved: boolean;
approval_method?: 'auto' | 'manual' | 'allowlist' | 'always';
success?: boolean;
output?: string; // Truncated
error_message?: string;
cli_version?: string;
ip_address?: string;
timestamp: string; // ISO 8601
signature: string; // HMAC
}
export interface SyncAuditLogsRequest {
org_slug: string;
logs: AuditLogEntry[];
}
export interface SyncAuditLogsResponse {
synced_count: number;
failed_ids: string[];
}
export interface GetAuditLogsRequest {
org_slug: string;
team_id?: string;
user_id?: string;
event_type?: string;
start_date?: string; // ISO 8601
end_date?: string;
limit?: number;
offset?: number;
}
export interface GetAuditLogsResponse {
logs: AuditLogEntry[];
total: number;
limit: number;
offset: number;
}LLM Types
// src/v1/types/llm.ts
export interface ProxiedChatRequest {
org_slug: string;
team_id?: string;
user_id: string;
provider: string; // 'anthropic', 'openai', 'deepseek', etc.
model: string;
messages: Message[];
tools?: Tool[];
temperature?: number;
max_tokens?: number;
}
export interface Message {
role: 'user' | 'assistant' | 'system' | 'tool';
content: string;
tool_calls?: ToolCall[];
tool_call_id?: string;
}
export interface Tool {
name: string;
description: string;
parameters: JSONSchema;
}
export interface ToolCall {
id: string;
name: string;
arguments: Record<string, any>;
}
export interface ChatResponse {
id: string;
model: string;
usage: {
input_tokens: number;
output_tokens: number;
total_tokens: number;
};
cost: {
base_cost_usd: number;
margin_fee_usd: number;
total_cost_usd: number;
};
content: string;
tool_calls?: ToolCall[];
finish_reason: 'stop' | 'length' | 'tool_calls';
}
export interface ChatChunk {
id: string;
delta: {
role?: 'assistant';
content?: string;
tool_calls?: Partial<ToolCall>[];
};
finish_reason?: 'stop' | 'length' | 'tool_calls';
}
export interface BudgetCheckRequest {
org_slug: string;
estimated_cost: number;
}
export interface BudgetCheckResponse {
allowed: boolean;
reason?: string;
budget?: {
monthly_limit: number;
current_usage: number;
remaining: number;
};
}
export interface GetUsageRequest {
org_slug: string;
period: 'day' | 'week' | 'month';
}
export interface GetUsageResponse {
total_cost: number;
total_tokens: number;
request_count: number;
breakdown_by_model: Array<{
model: string;
cost: number;
tokens: number;
requests: number;
}>;
breakdown_by_user: Array<{
user_id: string;
user_email: string;
cost: number;
tokens: number;
requests: number;
}>;
}Conversation Types
// src/v1/types/conversations.ts
export interface Conversation {
id: string;
org_id: string;
team_id?: string;
user_id: string;
title?: string;
visibility: 'private' | 'team' | 'org';
started_at: string; // ISO 8601
ended_at?: string;
total_messages: number;
total_cost_usd: number;
total_tokens: number;
}
export interface ConversationMessage {
id: string;
conversation_id: string;
role: 'user' | 'assistant' | 'system' | 'tool';
content: string;
tokens?: number;
cost_usd?: number;
timestamp: string;
}
export interface SyncConversationsRequest {
org_slug: string;
conversations: Conversation[];
}
export interface SyncConversationsResponse {
synced_count: number;
failed_ids: string[];
}
export interface FetchConversationsRequest {
org_slug: string;
team_id?: string;
user_id?: string;
since?: string; // ISO 8601
limit?: number;
offset?: number;
}
export interface FetchConversationsResponse {
conversations: Conversation[];
total: number;
limit: number;
offset: number;
}Zod Schemas
All types have corresponding Zod schemas for runtime validation:
// src/v1/schemas/auth.schema.ts
import { z } from 'zod';
export const LoginRequestSchema = z.object({
email: z.string().email(),
password: z.string().min(8)
});
export const LoginResponseSchema = z.object({
access_token: z.string(),
refresh_token: z.string(),
expires_in: z.number(),
user: z.object({
id: z.string().uuid(),
email: z.string().email(),
full_name: z.string(),
avatar_url: z.string().url().optional(),
organization: z.object({
slug: z.string(),
name: z.string(),
role: z.enum(['owner', 'admin', 'member', 'auditor'])
}),
active_team: z.object({
id: z.string().uuid(),
slug: z.string(),
name: z.string(),
role: z.enum(['admin', 'developer', 'viewer'])
}).optional()
})
});
// ... other schemasAPI Client
Base Client
// src/v1/client/TeamsAPIClient.ts
import axios, { AxiosInstance } from 'axios';
import { AuthEndpoints } from './endpoints/auth.js';
import { OrganizationEndpoints } from './endpoints/organizations.js';
import { TeamEndpoints } from './endpoints/teams.js';
import { ToolEndpoints } from './endpoints/tools.js';
import { AuditEndpoints } from './endpoints/audit.js';
import { LLMEndpoints } from './endpoints/llm.js';
import { ConversationEndpoints } from './endpoints/conversations.js';
export class TeamsAPIClient {
private http: AxiosInstance;
public auth: AuthEndpoints;
public organizations: OrganizationEndpoints;
public teams: TeamEndpoints;
public tools: ToolEndpoints;
public audit: AuditEndpoints;
public llm: LLMEndpoints;
public conversations: ConversationEndpoints;
constructor(
baseUrl: string,
private getAccessToken?: () => Promise<string | null>,
private onTokenExpired?: () => Promise<void>
) {
this.http = axios.create({
baseURL: baseUrl,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// Request interceptor (add auth token)
this.http.interceptors.request.use(async (config) => {
if (this.getAccessToken) {
const token = await this.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
}
return config;
});
// Response interceptor (handle 401)
this.http.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401 && this.onTokenExpired) {
await this.onTokenExpired();
}
throw error;
}
);
// Initialize endpoint groups
this.auth = new AuthEndpoints(this.http);
this.organizations = new OrganizationEndpoints(this.http);
this.teams = new TeamEndpoints(this.http);
this.tools = new ToolEndpoints(this.http);
this.audit = new AuditEndpoints(this.http);
this.llm = new LLMEndpoints(this.http);
this.conversations = new ConversationEndpoints(this.http);
}
setTeamContext(teamId?: string): void {
if (teamId) {
this.http.defaults.headers['X-Mimir-Team-Id'] = teamId;
} else {
delete this.http.defaults.headers['X-Mimir-Team-Id'];
}
}
}Endpoint Groups
// src/v1/client/endpoints/auth.ts
import { AxiosInstance } from 'axios';
import {
LoginRequest,
LoginResponse,
RefreshTokenRequest,
RefreshTokenResponse
} from '../../types/auth.js';
import {
LoginRequestSchema,
LoginResponseSchema,
RefreshTokenRequestSchema,
RefreshTokenResponseSchema
} from '../../schemas/auth.schema.js';
export class AuthEndpoints {
constructor(private http: AxiosInstance) {}
async login(request: LoginRequest): Promise<LoginResponse> {
// Validate request
LoginRequestSchema.parse(request);
const response = await this.http.post<LoginResponse>(
'/api/v1/auth/login',
request
);
// Validate response
return LoginResponseSchema.parse(response.data);
}
async refreshToken(request: RefreshTokenRequest): Promise<RefreshTokenResponse> {
RefreshTokenRequestSchema.parse(request);
const response = await this.http.post<RefreshTokenResponse>(
'/api/v1/auth/refresh',
request
);
return RefreshTokenResponseSchema.parse(response.data);
}
async logout(): Promise<void> {
await this.http.post('/api/v1/auth/logout');
}
}
// src/v1/client/endpoints/organizations.ts
export class OrganizationEndpoints {
constructor(private http: AxiosInstance) {}
async getConfig(
orgSlug: string,
teamId?: string
): Promise<GetOrganizationConfigResponse> {
const response = await this.http.get<GetOrganizationConfigResponse>(
`/api/v1/orgs/${orgSlug}/config`,
{
headers: teamId ? { 'X-Mimir-Team-Id': teamId } : {}
}
);
return GetOrganizationConfigResponseSchema.parse(response.data);
}
}
// src/v1/client/endpoints/teams.ts
export class TeamEndpoints {
constructor(private http: AxiosInstance) {}
async list(orgSlug: string): Promise<ListTeamsResponse> {
const response = await this.http.get<ListTeamsResponse>(
`/api/v1/orgs/${orgSlug}/teams`
);
return ListTeamsResponseSchema.parse(response.data);
}
}
// src/v1/client/endpoints/tools.ts
export class ToolEndpoints {
constructor(private http: AxiosInstance) {}
async list(
orgSlug: string,
teamId?: string
): Promise<ListToolsResponse> {
const response = await this.http.get<ListToolsResponse>(
`/api/v1/orgs/${orgSlug}/tools`,
{ params: { team_id: teamId } }
);
return ListToolsResponseSchema.parse(response.data);
}
async listCommands(
orgSlug: string,
teamId?: string
): Promise<ListCommandsResponse> {
const response = await this.http.get<ListCommandsResponse>(
`/api/v1/orgs/${orgSlug}/commands`,
{ params: { team_id: teamId } }
);
return ListCommandsResponseSchema.parse(response.data);
}
}
// src/v1/client/endpoints/audit.ts
export class AuditEndpoints {
constructor(private http: AxiosInstance) {}
async sync(
orgSlug: string,
logs: AuditLogEntry[]
): Promise<SyncAuditLogsResponse> {
const request: SyncAuditLogsRequest = { org_slug: orgSlug, logs };
SyncAuditLogsRequestSchema.parse(request);
const response = await this.http.post<SyncAuditLogsResponse>(
`/api/v1/orgs/${orgSlug}/audit/sync`,
request
);
return SyncAuditLogsResponseSchema.parse(response.data);
}
async get(
orgSlug: string,
params?: Omit<GetAuditLogsRequest, 'org_slug'>
): Promise<GetAuditLogsResponse> {
const response = await this.http.get<GetAuditLogsResponse>(
`/api/v1/orgs/${orgSlug}/audit`,
{ params }
);
return GetAuditLogsResponseSchema.parse(response.data);
}
}
// src/v1/client/endpoints/llm.ts
export class LLMEndpoints {
constructor(private http: AxiosInstance) {}
async chat(request: ProxiedChatRequest): Promise<ChatResponse> {
ProxiedChatRequestSchema.parse(request);
const response = await this.http.post<ChatResponse>(
'/api/v1/llm/chat',
request
);
return ChatResponseSchema.parse(response.data);
}
async *streamChat(request: ProxiedChatRequest): AsyncGenerator<ChatChunk> {
ProxiedChatRequestSchema.parse(request);
const response = await this.http.post(
'/api/v1/llm/chat/stream',
request,
{ responseType: 'stream' }
);
for await (const chunk of response.data) {
const parsed = JSON.parse(chunk.toString());
yield ChatChunkSchema.parse(parsed);
}
}
async checkBudget(
orgSlug: string,
estimatedCost: number
): Promise<BudgetCheckResponse> {
const request: BudgetCheckRequest = { org_slug: orgSlug, estimated_cost: estimatedCost };
const response = await this.http.post<BudgetCheckResponse>(
`/api/v1/orgs/${orgSlug}/budget/check`,
request
);
return BudgetCheckResponseSchema.parse(response.data);
}
async getUsage(
orgSlug: string,
period: 'day' | 'week' | 'month'
): Promise<GetUsageResponse> {
const response = await this.http.get<GetUsageResponse>(
`/api/v1/orgs/${orgSlug}/usage`,
{ params: { period } }
);
return GetUsageResponseSchema.parse(response.data);
}
}
// src/v1/client/endpoints/conversations.ts
export class ConversationEndpoints {
constructor(private http: AxiosInstance) {}
async sync(
orgSlug: string,
conversations: Conversation[]
): Promise<SyncConversationsResponse> {
const request: SyncConversationsRequest = { org_slug: orgSlug, conversations };
const response = await this.http.post<SyncConversationsResponse>(
`/api/v1/orgs/${orgSlug}/conversations/sync`,
request
);
return SyncConversationsResponseSchema.parse(response.data);
}
async fetch(
orgSlug: string,
params?: Omit<FetchConversationsRequest, 'org_slug'>
): Promise<FetchConversationsResponse> {
const response = await this.http.get<FetchConversationsResponse>(
`/api/v1/orgs/${orgSlug}/conversations`,
{ params }
);
return FetchConversationsResponseSchema.parse(response.data);
}
}Usage Examples
CLI Usage
// In @codedir/mimir-code
import { TeamsAPIClient } from '@codedir/mimir-teams-api-contracts';
const client = new TeamsAPIClient(
'https://teams.mimir.dev',
async () => {
// Get token from auth manager
const auth = await authManager.getAuth();
return auth?.accessToken || null;
},
async () => {
// Token expired, try refresh
await authManager.refreshToken();
}
);
// Login
const loginResult = await client.auth.login({
email: 'user@acme.com',
password: 'password123'
});
// Get org config
const config = await client.organizations.getConfig('acme-corp');
// Sync audit logs
await client.audit.sync('acme-corp', [
{
id: 'log-1',
user_id: 'user-123',
event_type: 'command_executed',
action: 'git push origin main',
risk_level: 'medium',
approved: true,
timestamp: new Date().toISOString(),
signature: 'hmac-signature'
}
]);
// Chat via proxy
const response = await client.llm.chat({
org_slug: 'acme-corp',
user_id: 'user-123',
provider: 'anthropic',
model: 'claude-sonnet-4.5',
messages: [
{ role: 'user', content: 'Hello, Claude!' }
]
});Backend Usage
// In @codedir/mimir-teams (Next.js API route)
import { LoginRequestSchema, LoginResponseSchema } from '@codedir/mimir-teams-api-contracts';
export async function POST(request: Request) {
const body = await request.json();
// Validate request
const loginRequest = LoginRequestSchema.parse(body);
// Authenticate
const user = await authenticate(loginRequest.email, loginRequest.password);
// Create response
const response: LoginResponse = {
access_token: generateJWT(user),
refresh_token: generateRefreshToken(user),
expires_in: 900, // 15 minutes
user: {
id: user.id,
email: user.email,
full_name: user.full_name,
avatar_url: user.avatar_url,
organization: {
slug: user.organization.slug,
name: user.organization.name,
role: user.organization_role
},
active_team: user.active_team
}
};
// Validate response (ensures we're returning correct shape)
LoginResponseSchema.parse(response);
return Response.json(response);
}Versioning Strategy
Major Version (Breaking Changes)
Increment when:
- Removing fields from responses
- Changing field types
- Renaming endpoints
- Changing required fields
Example:
- v1:
{ email: string } - v2:
{ email_address: string }← Breaking change
Minor Version (Additions)
Increment when:
- Adding new endpoints
- Adding optional fields to requests
- Adding new fields to responses
Example:
- v1.0:
{ email: string } - v1.1:
{ email: string, phone?: string }← Non-breaking
Patch Version (Fixes)
Increment when:
- Fixing bugs in validation
- Updating documentation
- Internal refactoring
Publishing
# Build
npm run build
# Test
npm run test
# Publish to npm
npm publish --access public
# Tag release
git tag v1.0.0
git push --tagspackage.json
{
"name": "@codedir/mimir-teams-api-contracts",
"version": "1.0.0",
"description": "Shared API contracts for Mimir Teams",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"test": "vitest",
"lint": "eslint src --ext .ts",
"format": "prettier --write \"src/**/*.ts\""
},
"dependencies": {
"axios": "^1.7.0",
"zod": "^3.24.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"eslint": "^9.0.0",
"prettier": "^3.0.0",
"typescript": "^5.7.0",
"vitest": "^2.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/codedir/mimir-teams-api-contracts.git"
},
"license": "MIT"
}Testing
// tests/client.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { TeamsAPIClient } from '../src/v1/client/TeamsAPIClient.js';
import { mockServer } from './mocks/server.js';
describe('TeamsAPIClient', () => {
let client: TeamsAPIClient;
beforeEach(() => {
client = new TeamsAPIClient('http://localhost:3000');
});
it('should login successfully', async () => {
const response = await client.auth.login({
email: 'test@example.com',
password: 'password123'
});
expect(response.access_token).toBeDefined();
expect(response.user.email).toBe('test@example.com');
});
it('should validate response schema', async () => {
// Mock returns invalid response
mockServer.use(
http.post('/api/v1/auth/login', () => {
return HttpResponse.json({
// Missing required fields
access_token: 'token'
});
})
);
await expect(
client.auth.login({
email: 'test@example.com',
password: 'password123'
})
).rejects.toThrow('Validation error');
});
});Migration Guide
From v1.x to v2.x (Future)
When breaking changes are needed:
- Publish v2 as separate package version
- Maintain v1 for 6 months (security fixes only)
- Provide migration guide:
// v1
const config = await client.getOrgConfig('acme-corp');
// v2
const config = await client.organizations.getConfig('acme-corp');- CLI supports both versions (for transition period)
- Backend maintains both API versions (
/api/v1/...,/api/v2/...)
Next Steps
- ✅ Initialize package - Set up repo, package.json
- ✅ Define core types - Auth, Organizations, Teams
- ✅ Add Zod schemas - Runtime validation
- ✅ Build API client - Axios-based client
- ✅ Write tests - Unit tests for client
- ✅ Publish to npm - Make available to CLI and backend
- ✅ Integrate with CLI - Use in TeamsAPIClient
- ✅ Integrate with backend - Use in API routes
Open Questions
- Should we generate OpenAPI spec from Zod schemas automatically?
- Should we provide a mock server for testing?
- Should we support custom base URLs per environment (dev, staging, prod)?
- Should we add retry logic to the client (exponential backoff)?
- Should we support request/response interceptors (for logging, metrics)?