Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/insomnia-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export * from './trial';
export * from './project';
export * from './collaborators';
export * from './invite';
export * from './organizations';

export { configureFetch, type FetchConfig, ResponseFailError, isApiError } from './fetch';
209 changes: 209 additions & 0 deletions packages/insomnia-api/src/organizations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { fetch } from './fetch';

interface Branding {
logo_url: string;
}

export interface Metadata {
organizationType: string;
ownerAccountId: string;
}

export interface Organization {
id: string;
name: string;
display_name: string;
branding?: Branding;
metadata: Metadata;
}

export interface OrganizationsResponse {
start: number;
limit: number;
length: number;
total: number;
next: string;
organizations: Organization[];
}

export const getOrganizations = ({ sessionId }: { sessionId: string }) => {
return fetch<OrganizationsResponse>({
method: 'GET',
path: '/v1/organizations',
sessionId,
});
};

export const needsToUpgrade = 'NEEDS_TO_UPGRADE';
export const needsToIncreaseSeats = 'NEEDS_TO_INCREASE_SEATS';

export interface CheckSeatsResponse {
isAllowed: boolean;
code?: typeof needsToUpgrade | typeof needsToIncreaseSeats;
}

export const checkSeats = ({
organizationId,
sessionId,
emails,
}: {
organizationId: string;
sessionId: string;
emails: string[];
}) => {
return fetch<CheckSeatsResponse>({
method: 'POST',
path: `/v1/organizations/${organizationId}/check-seats`,
data: { emails },
sessionId,
});
};

export interface Role {
id: string;
name: string;
description?: string;
}

export const getOrganizationRoles = ({ sessionId }: { sessionId: string }) => {
return fetch<Role[]>({
method: 'GET',
path: `/v1/organizations/roles`,
sessionId,
});
};

export interface FeatureStatus {
enabled: boolean;
reason?: string;
}

export interface FeatureList {
bulkImport: FeatureStatus;
gitSync: FeatureStatus;
orgBasicRbac: FeatureStatus;
aiMockServers: FeatureStatus;
aiCommitMessages: FeatureStatus;
aiMcpClient: FeatureStatus;
}

export interface Billing {
// If true, the user has paid for the current period
isActive: boolean;
expirationWarningMessage: string;
expirationErrorMessage: string;
accessDenied: boolean;
}

export const getOrganizationFeatures = ({
organizationId,
sessionId,
}: {
organizationId: string;
sessionId: string;
}) => {
return fetch<{ features: FeatureList; billing: Billing }>({
method: 'GET',
path: `/v1/organizations/${organizationId}/features`,
sessionId,
});
};
Comment on lines +98 to +110
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type of getOrganizationFeatures should likely include undefined to match the previous implementation and the actual usage pattern. The old code typed this as insomniaFetch<{ features: FeatureList; billing: Billing } | undefined> and callers use optional chaining (e.g., res?.features). Consider changing the return type to: fetch<{ features: FeatureList; billing: Billing } | undefined>({...})

Copilot uses AI. Check for mistakes.

export interface StorageRules {
enableCloudSync: boolean;
enableLocalVault: boolean;
enableGitSync: boolean;
isOverridden: boolean;
}

export const getOrganizationStorageRule = ({
organizationId,
sessionId,
}: {
organizationId: string;
sessionId: string;
}) => {
return fetch<StorageRules>({
method: 'GET',
path: `/v1/organizations/${organizationId}/storage-rule`,
sessionId,
});
};

export type Permission =
| 'own:organization'
| 'read:organization'
| 'delete:organization'
| 'update:organization'
| 'read:membership'
| 'delete:membership'
| 'update:membership'
| 'read:invitation'
| 'create:invitation'
| 'delete:invitation'
| 'create:enterprise_connection'
| 'read:enterprise_connection'
| 'delete:enterprise_connection'
| 'update:enterprise_connection'
| 'leave:organization';

export const getOrgUserPermissions = ({ organizationId, sessionId }: { organizationId: string; sessionId: string }) => {
return fetch<Record<Permission, boolean>>({
method: 'GET',
path: `/v1/organizations/${organizationId}/user-permissions`,
sessionId,
});
};

export const deleteOrganizationMember = ({
organizationId,
userId,
sessionId,
}: {
organizationId: string;
userId: string;
sessionId: string;
}) => {
return fetch({
method: 'DELETE',
path: `/v1/organizations/${organizationId}/members/${userId}`,
sessionId,
});
};

export const updateUserRoles = ({
organizationId,
userId,
roleId,
sessionId,
}: {
organizationId: string;
userId: string;
roleId: string;
sessionId: string;
}) => {
return fetch({
method: 'PATCH',
path: `/v1/organizations/${organizationId}/members/${userId}/roles`,
data: {
roles: [roleId],
},
sessionId,
});
};
Comment on lines +174 to +193
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updateUserRoles function should specify a return type generic. The previous implementation expected the API to return { enabled: boolean }, but this function doesn't specify a return type (defaulting to void). If the API returns data, consider adding the return type: fetch<{ enabled: boolean }>({...}) to maintain type safety.

Copilot uses AI. Check for mistakes.

export const getOrganizationMemberRoles = ({
organizationId,
userId,
sessionId,
}: {
organizationId: string;
userId: string;
sessionId: string;
}) => {
return fetch<Role>({
method: 'GET',
path: `/v1/organizations/${organizationId}/members/${userId}/roles`,
sessionId,
});
};
34 changes: 1 addition & 33 deletions packages/insomnia/src/models/organization.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,4 @@
import { type PersonalPlanType } from 'insomnia-api';

interface Branding {
logo_url: string;
}

export interface Metadata {
organizationType: string;
ownerAccountId: string;
}

export interface Organization {
id: string;
name: string;
display_name: string;
branding?: Branding;
metadata: Metadata;
}

export interface StorageRules {
enableCloudSync: boolean;
enableLocalVault: boolean;
enableGitSync: boolean;
isOverridden: boolean;
}
import { type Organization, type PersonalPlanType } from 'insomnia-api';

export const SCRATCHPAD_ORGANIZATION_ID = 'org_scratchpad';
export const isScratchpadOrganizationId = (organizationId: string) => organizationId === SCRATCHPAD_ORGANIZATION_ID;
Expand All @@ -40,14 +16,6 @@ export const findPersonalOrganization = (organizations: Organization[], accountI
}),
);
};
export interface OrganizationsResponse {
start: number;
limit: number;
length: number;
total: number;
next: string;
organizations: Organization[];
}

export const formatCurrentPlanType = (type: PersonalPlanType) => {
switch (type) {
Expand Down
2 changes: 1 addition & 1 deletion packages/insomnia/src/models/project.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { StorageRules } from '~/models/organization';
import type { StorageRules } from 'insomnia-api';

import { database as db } from '../common/database';
import { generateId } from '../common/misc';
Expand Down
4 changes: 3 additions & 1 deletion packages/insomnia/src/routes/commands.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Organization } from 'insomnia-api';

import { database } from '~/common/database';
import { fuzzyMatch } from '~/common/misc';
import {
Expand All @@ -12,7 +14,7 @@ import {
} from '~/models';
import type { Environment } from '~/models/environment';
import type { GrpcRequest } from '~/models/grpc-request';
import { isScratchpadOrganizationId, type Organization } from '~/models/organization';
import { isScratchpadOrganizationId } from '~/models/organization';
import { isRemoteProject, type Project } from '~/models/project';
import type { Request } from '~/models/request';
import type { RequestGroup } from '~/models/request-group';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import { checkSeats } from 'insomnia-api';
import { href } from 'react-router';
import { v4 as uuidv4 } from 'uuid';

import { userSession } from '~/models';
import { insomniaFetch } from '~/ui/insomnia-fetch';
import { createFetcherLoadHook } from '~/utils/router';

import type { Route } from './+types/organization.$organizationId.collaborators-check-seats';

export const needsToUpgrade = 'NEEDS_TO_UPGRADE';
export const needsToIncreaseSeats = 'NEEDS_TO_INCREASE_SEATS';

export interface CheckSeatsResponse {
isAllowed: boolean;
code?: typeof needsToUpgrade | typeof needsToIncreaseSeats;
}

export async function clientLoader({ params }: Route.ClientLoaderArgs) {
const { id: sessionId } = await userSession.get();

Expand All @@ -23,12 +15,10 @@ export async function clientLoader({ params }: Route.ClientLoaderArgs) {
try {
// Check whether the user can add a new collaborator
// Use a random email to avoid hitting any existing member emails
const checkResponseData = await insomniaFetch<CheckSeatsResponse>({
method: 'POST',
path: `/v1/organizations/${organizationId}/check-seats`,
data: { emails: [`insomnia-mock-check-seats-${uuidv4()}@example.net`] },
const checkResponseData = await checkSeats({
organizationId,
sessionId,
onlyResolveOnSuccess: true,
emails: [`insomnia-mock-check-seats-${uuidv4()}@example.net`],
});
return checkResponseData;
} catch {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { updateUserRoles } from 'insomnia-api';
import { href } from 'react-router';

import * as models from '~/models';
import { insomniaFetch } from '~/ui/insomnia-fetch';
import { invariant } from '~/utils/invariant';
import { createFetcherSubmitHook } from '~/utils/router';

Expand All @@ -18,11 +18,10 @@ export async function clientAction({ request, params }: Route.ClientActionArgs)
try {
const user = await models.userSession.getOrCreate();
const sessionId = user.id;

const response = await insomniaFetch<{ enabled: boolean }>({
method: 'PATCH',
path: `/v1/organizations/${organizationId}/members/${userId}/roles`,
data: { roles: [roleId] },
const response = await updateUserRoles({
organizationId,
userId,
roleId,
sessionId,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { type Billing, type FeatureList, getOrganizationFeatures, type Organization } from 'insomnia-api';
import { href, redirect, type ShouldRevalidateFunctionArgs } from 'react-router';

import { userSession } from '~/models';
import { isScratchpadOrganizationId, type Organization } from '~/models/organization';
import { insomniaFetch } from '~/ui/insomnia-fetch';
import { isScratchpadOrganizationId } from '~/models/organization';
import { createFetcherLoadHook } from '~/utils/router';

import type { Route } from './+types/organization.$organizationId.permissions';
import type { Billing, FeatureList } from './organization';

export const fallbackFeatures = Object.freeze<FeatureList>({
bulkImport: { enabled: false, reason: 'Insomnia API unreachable' },
Expand Down Expand Up @@ -44,11 +43,7 @@ export async function clientLoader({ params }: Route.ClientLoaderArgs) {
}

try {
const featuresResponse = insomniaFetch<{ features: FeatureList; billing: Billing } | undefined>({
method: 'GET',
path: `/v1/organizations/${organizationId}/features`,
sessionId,
});
const featuresResponse = getOrganizationFeatures({ organizationId, sessionId });

return {
featuresPromise: featuresResponse.then(res => res?.features || fallbackFeatures),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { IconName, IconProp } from '@fortawesome/fontawesome-svg-core';
import type { Organization } from 'insomnia-api';
import { Fragment, useEffect, useMemo, useState } from 'react';
import {
Button,
Expand Down Expand Up @@ -42,7 +43,6 @@ import type { ApiSpec } from '~/models/api-spec';
import type { GitRepository } from '~/models/git-repository';
import { sortProjects } from '~/models/helpers/project';
import type { MockServer } from '~/models/mock-server';
import type { Organization } from '~/models/organization';
import { isOwnerOfOrganization, isPersonalOrganization, isScratchpadOrganizationId } from '~/models/organization';
import {
getProjectStorageTypeLabel,
Expand Down
Loading
Loading