mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-21 03:56:23 +08:00
9b81c6debd
#### PR Dependency Tree * **PR #15221** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added MCP credential management (create/reveal, list, rotate, revoke) with expiration and status tracking. * Introduced read-only vs read/write access modes, with read/write tooling enabled only when permitted. * Added workspace MCP credential configuration UI, including token reveal and setup generation. * Added MCP credential GraphQL APIs to back the UI. * **Changes** * Replaced legacy access-token support with MCP credentials across authentication and realtime updates. * **Bug Fixes** * MCP authentication now reliably rejects revoked, rotated, expired, or disabled-user credentials. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
159 lines
3.6 KiB
TypeScript
159 lines
3.6 KiB
TypeScript
import { BadRequestException } from '@nestjs/common';
|
|
import {
|
|
Args,
|
|
Field,
|
|
ID,
|
|
InputType,
|
|
Int,
|
|
Mutation,
|
|
ObjectType,
|
|
Query,
|
|
registerEnumType,
|
|
Resolver,
|
|
} from '@nestjs/graphql';
|
|
import { McpAccessMode } from '@prisma/client';
|
|
|
|
import { CurrentUser } from '../../../core/auth';
|
|
import { PermissionAccess } from '../../../core/permission';
|
|
import { McpCredentialService } from './credential';
|
|
|
|
registerEnumType(McpAccessMode, { name: 'McpAccessMode' });
|
|
|
|
enum McpCredentialStatus {
|
|
ACTIVE = 'ACTIVE',
|
|
ROTATING = 'ROTATING',
|
|
EXPIRING = 'EXPIRING',
|
|
EXPIRED = 'EXPIRED',
|
|
REVOKED = 'REVOKED',
|
|
}
|
|
|
|
registerEnumType(McpCredentialStatus, { name: 'McpCredentialStatus' });
|
|
|
|
@ObjectType()
|
|
export class McpCredentialType {
|
|
@Field(() => ID)
|
|
id!: string;
|
|
|
|
@Field()
|
|
name!: string;
|
|
|
|
@Field()
|
|
workspaceId!: string;
|
|
|
|
@Field(() => McpAccessMode)
|
|
accessMode!: McpAccessMode;
|
|
|
|
@Field()
|
|
fingerprint!: string;
|
|
|
|
@Field(() => Date)
|
|
createdAt!: Date;
|
|
|
|
@Field(() => Date)
|
|
expiresAt!: Date;
|
|
|
|
@Field(() => Date, { nullable: true })
|
|
lastUsedAt!: Date | null;
|
|
|
|
@Field(() => Date, { nullable: true })
|
|
revokedAt!: Date | null;
|
|
|
|
@Field(() => Date, { nullable: true })
|
|
graceEndsAt!: Date | null;
|
|
|
|
@Field(() => McpCredentialStatus)
|
|
status!: McpCredentialStatus;
|
|
}
|
|
|
|
@ObjectType()
|
|
class RevealedMcpCredentialType {
|
|
@Field(() => McpCredentialType)
|
|
credential!: McpCredentialType;
|
|
|
|
@Field()
|
|
token!: string;
|
|
}
|
|
|
|
@InputType()
|
|
class CreateMcpCredentialInput {
|
|
@Field()
|
|
workspaceId!: string;
|
|
|
|
@Field()
|
|
name!: string;
|
|
|
|
@Field(() => McpAccessMode, { defaultValue: McpAccessMode.READ_ONLY })
|
|
accessMode!: McpAccessMode;
|
|
|
|
@Field(() => Int, { defaultValue: 90 })
|
|
expirationDays!: number;
|
|
}
|
|
|
|
@Resolver()
|
|
export class McpCredentialResolver {
|
|
constructor(
|
|
private readonly credentials: McpCredentialService,
|
|
private readonly ac: PermissionAccess
|
|
) {}
|
|
|
|
@Query(() => [McpCredentialType])
|
|
async mcpCredentials(
|
|
@CurrentUser() user: CurrentUser,
|
|
@Args('workspaceId') workspaceId: string
|
|
) {
|
|
await this.ac.user(user.id).workspace(workspaceId).assert('Workspace.Read');
|
|
return await this.credentials.list(user.id, workspaceId);
|
|
}
|
|
|
|
@Query(() => Boolean)
|
|
mcpCredentialReadWriteAvailable() {
|
|
return env.dev || env.namespaces.canary;
|
|
}
|
|
|
|
@Mutation(() => RevealedMcpCredentialType)
|
|
async createMcpCredential(
|
|
@CurrentUser() user: CurrentUser,
|
|
@Args('input') input: CreateMcpCredentialInput
|
|
) {
|
|
if (
|
|
input.accessMode === McpAccessMode.READ_WRITE &&
|
|
!env.dev &&
|
|
!env.namespaces.canary
|
|
) {
|
|
throw new BadRequestException('MCP write tools are not available');
|
|
}
|
|
await this.ac
|
|
.user(user.id)
|
|
.workspace(input.workspaceId)
|
|
.assert('Workspace.Read');
|
|
return await this.credentials.create({ ...input, userId: user.id });
|
|
}
|
|
|
|
@Mutation(() => RevealedMcpCredentialType)
|
|
async rotateMcpCredential(
|
|
@CurrentUser() user: CurrentUser,
|
|
@Args('id', { type: () => ID }) id: string,
|
|
@Args('workspaceId') workspaceId: string,
|
|
@Args('expirationDays', { type: () => Int, defaultValue: 90 })
|
|
expirationDays: number
|
|
) {
|
|
await this.ac.user(user.id).workspace(workspaceId).assert('Workspace.Read');
|
|
return await this.credentials.rotate(
|
|
id,
|
|
user.id,
|
|
workspaceId,
|
|
expirationDays
|
|
);
|
|
}
|
|
|
|
@Mutation(() => Boolean)
|
|
async revokeMcpCredential(
|
|
@CurrentUser() user: CurrentUser,
|
|
@Args('id', { type: () => ID }) id: string,
|
|
@Args('workspaceId') workspaceId: string
|
|
) {
|
|
await this.ac.user(user.id).workspace(workspaceId).assert('Workspace.Read');
|
|
return await this.credentials.revoke(id, user.id, workspaceId);
|
|
}
|
|
}
|