Permission Robustness (#591)

<!-- ELLIPSIS_HIDDEN -->


> [!IMPORTANT]
> Enhance permission management by adding unique constraints, handling
duplicate ID errors, and updating frontend and backend logic with
comprehensive tests.
> 
>   - **Database**:
> - Add unique constraint on `Permission` table for `[tenancyId,
queryableId]` in `migration.sql`.
>     - Update `schema.prisma` to reflect new unique constraints.
>   - **Backend**:
> - Update `crud.tsx` files to handle `PERMISSION_ID_ALREADY_EXISTS`
error using `isErrorForNonUniquePermission()`.
> - Add `isPrismaUniqueConstraintViolation()` in `prisma-client.tsx` to
identify unique constraint violations.
>     - Add `PermissionIdAlreadyExists` error in `known-errors.tsx`.
>   - **Frontend**:
> - Update `page-client.tsx` and `permission-table.tsx` to check for
duplicate permission IDs before creation.
>   - **Tests**:
> - Add tests in `project-permission-definitions.test.ts` and
`team-permission-definitions.test.ts` to verify duplicate ID handling.
> - Ensure permissions cannot be created with duplicate IDs across
project and team contexts.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis"
src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup>
for b3ccd15bca. It will automatically
update as commits are pushed.</sup>


<!-- ELLIPSIS_HIDDEN -->

---------

Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
Co-authored-by: Zai Shi <zaishi00@outlook.com>
This commit is contained in:
CactusBlue 2025-04-01 16:12:13 -07:00 committed by GitHub
parent dfe827c77f
commit 306f4e4c67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 450 additions and 41 deletions

View File

@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[tenancyId,queryableId]` on the table `Permission` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "Permission_tenancyId_queryableId_key" ON "Permission"("tenancyId", "queryableId");

View File

@ -211,7 +211,7 @@ model Permission {
description String?
// The scope of the permission. If projectConfigId is set, may be USER or TEAM; if teamId is set, must be TEAM.
// The scope of the permission. If projectConfigId is set, may be PROJECT or TEAM; if teamId is set, must be PROJECT.
scope PermissionScope
projectConfig ProjectConfig? @relation(fields: [projectConfigId], references: [id], onDelete: Cascade)
@ -224,8 +224,9 @@ model Permission {
isDefaultTeamCreatorPermission Boolean @default(false)
isDefaultTeamMemberPermission Boolean @default(false)
isDefaultProjectPermission Boolean @default(false)
isDefaultProjectPermission Boolean @default(false)
@@unique([tenancyId, queryableId])
@@unique([projectConfigId, queryableId])
@@unique([tenancyId, teamId, queryableId])
}

View File

@ -1,31 +1,47 @@
import { createPermissionDefinition, deletePermissionDefinition, listPermissionDefinitions, updatePermissionDefinitions } from "@/lib/permissions";
import { retryTransaction } from "@/prisma-client";
import { createPermissionDefinition, deletePermissionDefinition, isErrorForNonUniquePermission, listPermissionDefinitions, updatePermissionDefinitions } from "@/lib/permissions";
import { isPrismaUniqueConstraintViolation, retryTransaction } from "@/prisma-client";
import { createCrudHandlers } from "@/route-handlers/crud-handler";
import { KnownErrors } from "@stackframe/stack-shared";
import { projectPermissionDefinitionsCrud } from '@stackframe/stack-shared/dist/interface/crud/project-permissions';
import { permissionDefinitionIdSchema, yupObject } from "@stackframe/stack-shared/dist/schema-fields";
import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
export const projectPermissionDefinitionsCrudHandlers = createLazyProxy(() => createCrudHandlers(projectPermissionDefinitionsCrud, {
paramsSchema: yupObject({
permission_id: permissionDefinitionIdSchema.defined(),
}),
async onCreate({ auth, data }) {
return await retryTransaction(async (tx) => {
return await createPermissionDefinition(tx, {
scope: "PROJECT",
tenancy: auth.tenancy,
data,
});
try {
return await createPermissionDefinition(tx, {
scope: "PROJECT",
tenancy: auth.tenancy,
data,
});
} catch (error) {
if (isErrorForNonUniquePermission(error)) {
throw new KnownErrors.PermissionIdAlreadyExists(data.id);
}
throw error;
}
});
},
async onUpdate({ auth, data, params }) {
return await retryTransaction(async (tx) => {
return await updatePermissionDefinitions(tx, {
scope: "PROJECT",
tenancy: auth.tenancy,
permissionId: params.permission_id,
data,
});
try {
return await updatePermissionDefinitions(tx, {
scope: "PROJECT",
tenancy: auth.tenancy,
permissionId: params.permission_id,
data,
});
} catch (error) {
if (isErrorForNonUniquePermission(error)) {
throw new KnownErrors.PermissionIdAlreadyExists(data.id ?? '');
}
throw error;
}
});
},
async onDelete({ auth, params }) {

View File

@ -1,6 +1,7 @@
import { createPermissionDefinition, deletePermissionDefinition, listPermissionDefinitions, updatePermissionDefinitions } from "@/lib/permissions";
import { createPermissionDefinition, deletePermissionDefinition, isErrorForNonUniquePermission, listPermissionDefinitions, updatePermissionDefinitions } from "@/lib/permissions";
import { retryTransaction } from "@/prisma-client";
import { createCrudHandlers } from "@/route-handlers/crud-handler";
import { KnownErrors } from "@stackframe/stack-shared";
import { teamPermissionDefinitionsCrud } from '@stackframe/stack-shared/dist/interface/crud/team-permissions';
import { permissionDefinitionIdSchema, yupObject } from "@stackframe/stack-shared/dist/schema-fields";
import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
@ -11,21 +12,35 @@ export const teamPermissionDefinitionsCrudHandlers = createLazyProxy(() => creat
}),
async onCreate({ auth, data }) {
return await retryTransaction(async (tx) => {
return await createPermissionDefinition(tx, {
scope: "TEAM",
tenancy: auth.tenancy,
data,
});
try {
return await createPermissionDefinition(tx, {
scope: "TEAM",
tenancy: auth.tenancy,
data,
});
} catch (error) {
if (isErrorForNonUniquePermission(error)) {
throw new KnownErrors.PermissionIdAlreadyExists(data.id);
}
throw error;
}
});
},
async onUpdate({ auth, data, params }) {
return await retryTransaction(async (tx) => {
return await updatePermissionDefinitions(tx, {
scope: "TEAM",
tenancy: auth.tenancy,
permissionId: params.permission_id,
data,
});
try {
return await updatePermissionDefinitions(tx, {
scope: "TEAM",
tenancy: auth.tenancy,
permissionId: params.permission_id,
data,
});
} catch (error) {
if (isErrorForNonUniquePermission(error)) {
throw new KnownErrors.PermissionIdAlreadyExists(data.id ?? '');
}
throw error;
}
});
},
async onDelete({ auth, params }) {

View File

@ -1,7 +1,8 @@
import { isPrismaUniqueConstraintViolation } from "@/prisma-client";
import { TeamSystemPermission as DBTeamSystemPermission, Prisma } from "@prisma/client";
import { KnownErrors } from "@stackframe/stack-shared";
import { TeamPermissionDefinitionsCrud, TeamPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/team-permissions";
import { ProjectPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/project-permissions";
import { TeamPermissionDefinitionsCrud, TeamPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/team-permissions";
import { groupBy } from "@stackframe/stack-shared/dist/utils/arrays";
import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
import { stringCompare, typedToLowercase, typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings";
@ -649,3 +650,9 @@ export async function grantDefaultProjectPermissions(
return defaultPermissions.length > 0;
}
export function isErrorForNonUniquePermission(error: unknown): boolean {
return isPrismaUniqueConstraintViolation(error, "Permission", ["tenancyId", "queryableId"]) ||
isPrismaUniqueConstraintViolation(error, "Permission", ["projectConfigId", "queryableId"]) ||
isPrismaUniqueConstraintViolation(error, "Permission", ["tenancyId", "teamId", "queryableId"]);
}

View File

@ -1,7 +1,7 @@
import { Prisma, PrismaClient } from "@prisma/client";
import { withAccelerate } from "@prisma/extension-accelerate";
import { getEnvVariable, getNodeEnvironment } from '@stackframe/stack-shared/dist/utils/env';
import { filterUndefined, typedFromEntries, typedKeys } from "@stackframe/stack-shared/dist/utils/objects";
import { deepPlainEquals, filterUndefined, typedFromEntries, typedKeys } from "@stackframe/stack-shared/dist/utils/objects";
import { Result } from "@stackframe/stack-shared/dist/utils/results";
import { traceSpan } from "./utils/telemetry";
@ -135,3 +135,21 @@ async function rawQueryArray<Q extends RawQuery<any>[]>(queries: Q): Promise<[]
});
}
// not exhaustive
export const PRISMA_ERROR_CODES = {
VALUE_TOO_LONG: "P2000",
RECORD_NOT_FOUND: "P2001",
UNIQUE_CONSTRAINT_VIOLATION: "P2002",
FOREIGN_CONSTRAINT_VIOLATION: "P2003",
GENERIC_CONSTRAINT_VIOLATION: "P2004",
} as const;
export function isPrismaError(error: unknown, code: keyof typeof PRISMA_ERROR_CODES): error is Prisma.PrismaClientKnownRequestError {
return error instanceof Prisma.PrismaClientKnownRequestError && error.code === PRISMA_ERROR_CODES[code];
}
export function isPrismaUniqueConstraintViolation(error: unknown, modelName: string, target: string | string[]): error is Prisma.PrismaClientKnownRequestError {
if (!isPrismaError(error, "UNIQUE_CONSTRAINT_VIOLATION")) return false;
if (!error.meta?.target) return false;
return error.meta.modelName === modelName && deepPlainEquals(error.meta.target, target);
}

View File

@ -40,17 +40,18 @@ function CreateDialog(props: {
onOpenChange: (open: boolean) => void,
}) {
const stackAdminApp = useAdminApp();
const permissions = stackAdminApp.useProjectPermissionDefinitions();
const projectPermissions = stackAdminApp.useProjectPermissionDefinitions();
const combinedPermissions = [...stackAdminApp.useTeamPermissionDefinitions(), ...projectPermissions];
const formSchema = yup.object({
id: yup.string().defined()
.notOneOf(permissions.map((p) => p.id), "ID already exists")
.notOneOf(combinedPermissions.map((p) => p.id), "ID already exists")
.matches(/^[a-z0-9_:]+$/, 'Only lowercase letters, numbers, ":" and "_" are allowed')
.label("ID"),
description: yup.string().label("Description"),
containedPermissionIds: yup.array().of(yup.string().defined()).defined().default([]).meta({
stackFormFieldRender: (props) => (
<PermissionListField {...props} permissions={permissions} type="new" />
<PermissionListField {...props} permissions={projectPermissions} type="new" />
),
}),
});

View File

@ -41,17 +41,18 @@ function CreateDialog(props: {
onOpenChange: (open: boolean) => void,
}) {
const stackAdminApp = useAdminApp();
const permissions = stackAdminApp.useTeamPermissionDefinitions();
const teamPermissions = stackAdminApp.useTeamPermissionDefinitions();
const combinedPermissions = [...teamPermissions, ...stackAdminApp.useProjectPermissionDefinitions()];
const formSchema = yup.object({
id: yup.string().defined()
.notOneOf(permissions.map((p) => p.id), "ID already exists")
.notOneOf(combinedPermissions.map((p) => p.id), "ID already exists")
.matches(/^[a-z0-9_:]+$/, 'Only lowercase letters, numbers, ":" and "_" are allowed')
.label("ID"),
description: yup.string().label("Description"),
containedPermissionIds: yup.array().of(yup.string().defined()).defined().default([]).meta({
stackFormFieldRender: (props) => (
<PermissionListField {...props} permissions={permissions} type="new" />
<PermissionListField {...props} permissions={teamPermissions} type="new" />
),
}),
});

View File

@ -31,9 +31,11 @@ function EditDialog(props: {
permissionType: PermissionType,
}) {
const stackAdminApp = useAdminApp();
const permissions = props.permissionType === 'project'
? stackAdminApp.useProjectPermissionDefinitions()
: stackAdminApp.useTeamPermissionDefinitions();
const teamPermissions = stackAdminApp.useTeamPermissionDefinitions();
const projectPermissions = stackAdminApp.useProjectPermissionDefinitions();
const permissions = props.permissionType === 'project' ? projectPermissions : teamPermissions;
const combinedPermissions = [...teamPermissions, ...projectPermissions];
const currentPermission = permissions.find((p) => p.id === props.selectedPermissionId);
if (!currentPermission) {
return null;
@ -42,7 +44,7 @@ function EditDialog(props: {
const formSchema = yup.object({
id: yup.string()
.defined()
.notOneOf(permissions.map((p) => p.id).filter(p => p !== props.selectedPermissionId), "ID already exists")
.notOneOf(combinedPermissions.map((p) => p.id).filter(p => p !== props.selectedPermissionId), "ID already exists")
.matches(/^[a-z0-9_:]+$/, 'Only lowercase letters, numbers, ":" and "_" are allowed')
.label("ID"),
description: yup.string().label("Description"),

View File

@ -173,3 +173,82 @@ it("creates, updates, and deletes a new user permission", async ({ expect }) =>
}
`);
});
it("handles duplicate permission IDs correctly", async ({ expect }) => {
backendContext.set({ projectKeys: InternalProjectKeys });
const { adminAccessToken } = await Project.createAndGetAdminToken();
// Create first permission
const response1 = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
accessType: "admin",
method: "POST",
body: {
id: 'duplicate_test',
description: "Test permission"
},
headers: {
'x-stack-admin-access-token': adminAccessToken
},
});
expect(response1.status).toBe(201);
// Try to create another permission with the same ID
const response2 = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
accessType: "admin",
method: "POST",
body: {
id: 'duplicate_test',
description: "Another test permission"
},
headers: {
'x-stack-admin-access-token': adminAccessToken
},
});
expect(response2.status).toBe(400);
expect(response2.body).toHaveProperty("code", "PERMISSION_ID_ALREADY_EXISTS");
// Create another permission
const response3 = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
accessType: "admin",
method: "POST",
body: {
id: 'update_test',
description: "Test permission for update"
},
headers: {
'x-stack-admin-access-token': adminAccessToken
},
});
expect(response3.status).toBe(201);
// Update the first permission to have the ID of the second (which should fail)
const response4 = await niceBackendFetch(`/api/v1/project-permission-definitions/duplicate_test`, {
accessType: "admin",
method: "PATCH",
body: {
id: 'update_test',
description: "Updated description"
},
headers: {
'x-stack-admin-access-token': adminAccessToken
},
});
expect(response4.status).toBe(400);
expect(response4.body).toHaveProperty("code", "PERMISSION_ID_ALREADY_EXISTS");
// Clean up
await niceBackendFetch(`/api/v1/project-permission-definitions/duplicate_test`, {
accessType: "admin",
method: "DELETE",
headers: {
'x-stack-admin-access-token': adminAccessToken
},
});
await niceBackendFetch(`/api/v1/project-permission-definitions/update_test`, {
accessType: "admin",
method: "DELETE",
headers: {
'x-stack-admin-access-token': adminAccessToken
},
});
});

View File

@ -315,3 +315,86 @@ it("should trigger project permission webhook when a permission is revoked from
}
`);
});
it("should not be able to create a permission with the same name as an existing project permission", async ({ expect }) => {
await Auth.Otp.signIn();
const { adminAccessToken } = await Project.createAndGetAdminToken();
// First, create a project permission definition
const createTeamPermissionResponse = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
accessType: "admin",
method: "POST",
body: {
id: 'custom_project_permission',
description: 'A custom project permission',
},
headers: {
'x-stack-admin-access-token': adminAccessToken
},
});
expect(createTeamPermissionResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 201,
"body": {
"contained_permission_ids": [],
"description": "A custom project permission",
"id": "custom_project_permission",
},
"headers": Headers { <some fields may have been hidden> },
}
`);
// Try creating another team permission with the same name
const createAnotherTeamPermissionResponse = await niceBackendFetch(`/api/v1/team-permission-definitions`, {
accessType: "admin",
method: "POST",
body: { id: 'custom_project_permission' },
headers: { 'x-stack-admin-access-token': adminAccessToken },
});
expect(createAnotherTeamPermissionResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "PERMISSION_ID_ALREADY_EXISTS",
"details": { "permission_id": "custom_project_permission" },
"error": "Permission with ID \\"custom_project_permission\\" already exists. Choose a different ID.",
},
"headers": Headers {
"x-stack-known-error": "PERMISSION_ID_ALREADY_EXISTS",
<some fields may have been hidden>,
},
}
`);
// Now try to create a project permission with the same name
const createProjectPermissionResponse = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
accessType: "admin",
method: "POST",
body: {
id: 'custom_project_permission',
description: 'Attempt to create a project permission with same name as team permission',
},
headers: {
'x-stack-admin-access-token': adminAccessToken
},
});
// TODO: P2002 postgres codes should automatically be converted into duplicate key error
expect(createProjectPermissionResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "PERMISSION_ID_ALREADY_EXISTS",
"details": { "permission_id": "custom_project_permission" },
"error": "Permission with ID \\"custom_project_permission\\" already exists. Choose a different ID.",
},
"headers": Headers {
"x-stack-known-error": "PERMISSION_ID_ALREADY_EXISTS",
<some fields may have been hidden>,
},
}
`);
});

View File

@ -307,3 +307,82 @@ it("creates, updates, and delete a new team permission", async ({ expect }) => {
}
`);
});
it("handles duplicate permission IDs correctly", async ({ expect }) => {
backendContext.set({ projectKeys: InternalProjectKeys });
const { adminAccessToken } = await Project.createAndGetAdminToken();
// Create first permission
const response1 = await niceBackendFetch(`/api/v1/team-permission-definitions`, {
accessType: "admin",
method: "POST",
body: {
id: 'duplicate_test',
description: "Test permission"
},
headers: {
'x-stack-admin-access-token': adminAccessToken
},
});
expect(response1.status).toBe(201);
// Try to create another permission with the same ID
const response2 = await niceBackendFetch(`/api/v1/team-permission-definitions`, {
accessType: "admin",
method: "POST",
body: {
id: 'duplicate_test',
description: "Another test permission"
},
headers: {
'x-stack-admin-access-token': adminAccessToken
},
});
expect(response2.status).toBe(400);
expect(response2.body).toHaveProperty("code", "PERMISSION_ID_ALREADY_EXISTS");
// Create another permission
const response3 = await niceBackendFetch(`/api/v1/team-permission-definitions`, {
accessType: "admin",
method: "POST",
body: {
id: 'update_test',
description: "Test permission for update"
},
headers: {
'x-stack-admin-access-token': adminAccessToken
},
});
expect(response3.status).toBe(201);
// Update the first permission to have the ID of the second (which should fail)
const response4 = await niceBackendFetch(`/api/v1/team-permission-definitions/duplicate_test`, {
accessType: "admin",
method: "PATCH",
body: {
id: 'update_test',
description: "Updated description"
},
headers: {
'x-stack-admin-access-token': adminAccessToken
},
});
expect(response4.status).toBe(400);
expect(response4.body).toHaveProperty("code", "PERMISSION_ID_ALREADY_EXISTS");
// Clean up
await niceBackendFetch(`/api/v1/team-permission-definitions/duplicate_test`, {
accessType: "admin",
method: "DELETE",
headers: {
'x-stack-admin-access-token': adminAccessToken
},
});
await niceBackendFetch(`/api/v1/team-permission-definitions/update_test`, {
accessType: "admin",
method: "DELETE",
headers: {
'x-stack-admin-access-token': adminAccessToken
},
});
});

View File

@ -300,3 +300,86 @@ it("should trigger team permission webhook when a permission is revoked from a u
}
`);
});
it("should not be able to create a permission with the same name as an existing team permission", async ({ expect }) => {
await Auth.Otp.signIn();
const { adminAccessToken } = await Project.createAndGetAdminToken();
// First, create a team permission definition
const createTeamPermissionResponse = await niceBackendFetch(`/api/v1/team-permission-definitions`, {
accessType: "admin",
method: "POST",
body: {
id: 'custom_team_permission',
description: 'A custom team permission',
},
headers: {
'x-stack-admin-access-token': adminAccessToken
},
});
expect(createTeamPermissionResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 201,
"body": {
"contained_permission_ids": [],
"description": "A custom team permission",
"id": "custom_team_permission",
},
"headers": Headers { <some fields may have been hidden> },
}
`);
// Try creating another team permission with the same name
const createAnotherTeamPermissionResponse = await niceBackendFetch(`/api/v1/team-permission-definitions`, {
accessType: "admin",
method: "POST",
body: { id: 'custom_team_permission' },
headers: { 'x-stack-admin-access-token': adminAccessToken },
});
expect(createAnotherTeamPermissionResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "PERMISSION_ID_ALREADY_EXISTS",
"details": { "permission_id": "custom_team_permission" },
"error": "Permission with ID \\"custom_team_permission\\" already exists. Choose a different ID.",
},
"headers": Headers {
"x-stack-known-error": "PERMISSION_ID_ALREADY_EXISTS",
<some fields may have been hidden>,
},
}
`);
// Now try to create a project permission with the same name
const createProjectPermissionResponse = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
accessType: "admin",
method: "POST",
body: {
id: 'custom_team_permission',
description: 'Attempt to create a project permission with same name as team permission',
},
headers: {
'x-stack-admin-access-token': adminAccessToken
},
});
// TODO: P2002 postgres codes should automatically be converted into duplicate key error
expect(createProjectPermissionResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "PERMISSION_ID_ALREADY_EXISTS",
"details": { "permission_id": "custom_team_permission" },
"error": "Permission with ID \\"custom_team_permission\\" already exists. Choose a different ID.",
},
"headers": Headers {
"x-stack-known-error": "PERMISSION_ID_ALREADY_EXISTS",
<some fields may have been hidden>,
},
}
`);
});

View File

@ -39,7 +39,8 @@ To check whether a user has a specific permission, use the `getPermission` metho
export function CheckUserPermission() {
const user = useUser({ or: 'redirect' });
const permission = user.usePermission('read');
const team = user.useTeam('some-team-id');
const permission = user.usePermission(team, 'read');
// Don't rely on client-side permission checks for business logic.
return (
@ -57,7 +58,8 @@ To check whether a user has a specific permission, use the `getPermission` metho
export default async function CheckUserPermission() {
const user = await stackServerApp.getUser({ or: 'redirect' });
const permission = await user.getPermission('read');
const team = await stackServerApp.getTeam('some-team-id');
const permission = await user.getPermission(team, 'read');
// This is a server-side check, so it's secure.
return (

View File

@ -1218,6 +1218,19 @@ const InvalidPollingCodeError = createKnownErrorConstructor(
(json: any) => [json] as const,
);
const PermissionIdAlreadyExists = createKnownErrorConstructor(
KnownError,
"PERMISSION_ID_ALREADY_EXISTS",
(permissionId: string) => [
400,
`Permission with ID "${permissionId}" already exists. Choose a different ID.`,
{
permission_id: permissionId,
},
] as const,
(json: any) => [json.permission_id] as const,
);
export type KnownErrors = {
[K in keyof typeof KnownErrors]: InstanceType<typeof KnownErrors[K]>;
};
@ -1229,6 +1242,7 @@ export const KnownErrors = {
SchemaError,
AllOverloadsFailed,
ProjectAuthenticationError,
PermissionIdAlreadyExists,
InvalidProjectAuthentication,
ProjectKeyWithoutAccessType,
InvalidAccessType,