mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-16 09:36:17 +08:00
fix(server): mcp api visibility (#15247)
fix #15246 #### PR Dependency Tree * **PR #15247** 👈 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 * **Bug Fixes** * Improved consistency when issuing and validating MCP credential tokens by using a shared token prefix across issuance and parsing. * Preserved correct recognition of standard JWT-based authentication tokens. * **Tests** * Updated MCP credentials coverage to validate behavior through the HTTP API response (instead of direct controller invocation). * Adjusted workspace quota e2e setup to derive restricted limits via entitlements before reconciling quota state. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -5,7 +5,6 @@ import { ProjectRoot } from '@affine-tools/utils/path';
|
||||
import { McpAccessMode, PrismaClient } from '@prisma/client';
|
||||
import type { TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
import type { Request, Response } from 'express';
|
||||
import { nanoid } from 'nanoid';
|
||||
import Sinon from 'sinon';
|
||||
import { z } from 'zod';
|
||||
@@ -43,7 +42,6 @@ import {
|
||||
CopilotEmbeddingJob,
|
||||
MockEmbeddingClient,
|
||||
} from '../../plugins/copilot/embedding';
|
||||
import { WorkspaceMcpController } from '../../plugins/copilot/mcp/controller';
|
||||
import { McpCredentialService } from '../../plugins/copilot/mcp/credential';
|
||||
import { WorkspaceMcpProvider } from '../../plugins/copilot/mcp/provider';
|
||||
import { PromptService } from '../../plugins/copilot/prompt';
|
||||
@@ -84,12 +82,12 @@ import { SubscriptionService } from '../../plugins/payment/service';
|
||||
import { SubscriptionStatus } from '../../plugins/payment/types';
|
||||
import { installMockCopilotRuntime, MockCopilotProvider } from '../mocks';
|
||||
import { TestingPromptService } from '../mocks/prompt-service.mock';
|
||||
import { createTestingModule, TestingModule } from '../utils';
|
||||
import { createTestingApp, TestingApp } from '../utils';
|
||||
import { singleUserPromptMessages, systemPrompt } from './prompt-test-helper';
|
||||
|
||||
type Context = {
|
||||
auth: AuthService;
|
||||
module: TestingModule;
|
||||
module: TestingApp;
|
||||
db: PrismaClient;
|
||||
event: EventBus;
|
||||
models: Models;
|
||||
@@ -143,7 +141,7 @@ let restoreMockCopilotNativeRuntime: (() => void) | undefined;
|
||||
|
||||
test.before(async t => {
|
||||
restoreMockCopilotNativeRuntime = installMockCopilotRuntime();
|
||||
const module = await createTestingModule({
|
||||
const module = await createTestingApp({
|
||||
imports: [
|
||||
ConfigModule.override({
|
||||
copilot: {
|
||||
@@ -278,28 +276,12 @@ test('MCP credentials stay bound to their endpoint, workspace, and profile', asy
|
||||
t.is(authenticated.userId, userId);
|
||||
await t.throwsAsync(mcpCredentials.authenticate(issued.token, other.id));
|
||||
|
||||
let responseStatus: number | undefined;
|
||||
let responseBody: unknown;
|
||||
const request = {
|
||||
body: { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} },
|
||||
get: () => `Bearer ${issued.token}`,
|
||||
on: () => request,
|
||||
} as unknown as Request;
|
||||
const response = {
|
||||
status: (status: number) => {
|
||||
responseStatus = status;
|
||||
return response;
|
||||
},
|
||||
json: (body: unknown) => {
|
||||
responseBody = body;
|
||||
return response;
|
||||
},
|
||||
} as unknown as Response;
|
||||
await t.context.module
|
||||
.get(WorkspaceMcpController)
|
||||
.mcp(request, response, ws.id);
|
||||
t.is(responseStatus, 200);
|
||||
t.like(responseBody as object, {
|
||||
const response = await t.context.module
|
||||
.POST(`/api/workspaces/${ws.id}/mcp`)
|
||||
.set('Authorization', `Bearer ${issued.token}`)
|
||||
.send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} })
|
||||
.expect(200);
|
||||
t.like(response.body, {
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import test from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { ConfigFactory } from '../../base';
|
||||
import { EntitlementService } from '../../core/entitlement';
|
||||
import { QuotaStateService } from '../../core/quota/state';
|
||||
import { WorkspaceBlobStorage } from '../../core/storage/wrappers/blob';
|
||||
import { StorageRuntimeProvider } from '../../core/storage-runtime';
|
||||
@@ -170,22 +171,20 @@ test.after.always(async () => {
|
||||
|
||||
async function withRestrictedWorkspaceQuota(workspaceId: string) {
|
||||
const quotaState = app.get(QuotaStateService);
|
||||
const blobModel = app.get(BlobModel);
|
||||
const base = await quotaState.reconcileWorkspaceQuotaState(workspaceId);
|
||||
return Sinon.stub(quotaState, 'reconcileWorkspaceQuotaState').callsFake(
|
||||
async id => {
|
||||
if (id !== workspaceId) {
|
||||
return base;
|
||||
}
|
||||
|
||||
const entitlement = app.get(EntitlementService);
|
||||
const resolveUserEntitlement =
|
||||
entitlement.resolveUserEntitlement.bind(entitlement);
|
||||
const stub = Sinon.stub(entitlement, 'resolveUserEntitlement').callsFake(
|
||||
async userId => {
|
||||
const resolved = await resolveUserEntitlement(userId);
|
||||
return {
|
||||
...base,
|
||||
blobLimit: BigInt(RESTRICTED_QUOTA.blobLimit),
|
||||
storageQuota: BigInt(RESTRICTED_QUOTA.storageQuota),
|
||||
usedStorageQuota: BigInt(await blobModel.totalSize(workspaceId)),
|
||||
...resolved,
|
||||
quota: { ...resolved.quota, ...RESTRICTED_QUOTA },
|
||||
};
|
||||
}
|
||||
);
|
||||
await quotaState.reconcileWorkspaceQuotaState(workspaceId);
|
||||
return stub;
|
||||
}
|
||||
|
||||
test('should set blobs', async t => {
|
||||
|
||||
@@ -115,7 +115,3 @@ export class AccessTokenService {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function isLikelyJwt(token: string) {
|
||||
return token.split('.').length === 3;
|
||||
}
|
||||
|
||||
@@ -23,16 +23,13 @@ import {
|
||||
UnsupportedClientVersion,
|
||||
} from '../../base';
|
||||
import { WEBSOCKET_OPTIONS } from '../../base/websocket';
|
||||
import {
|
||||
AccessTokenService,
|
||||
isLikelyJwt,
|
||||
SessionAccessTokenError,
|
||||
} from './access-token';
|
||||
import { AccessTokenService, SessionAccessTokenError } from './access-token';
|
||||
import { AuthSessionService } from './auth-session';
|
||||
import { extractTokenFromHeader } from './input';
|
||||
import { AuthService } from './service';
|
||||
import { AuthSessionPrincipal, Session } from './session';
|
||||
import { AuthSessionHttpError } from './session-exchange';
|
||||
import { isLikelyJwt } from './token';
|
||||
|
||||
const PUBLIC_ENTRYPOINT_SYMBOL = Symbol('public');
|
||||
const INTERNAL_ENTRYPOINT_SYMBOL = Symbol('internal');
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export const MCP_CREDENTIAL_TOKEN_PREFIX = 'aff_mcp_v1';
|
||||
|
||||
export function isLikelyJwt(token: string) {
|
||||
return (
|
||||
!token.startsWith(`${MCP_CREDENTIAL_TOKEN_PREFIX}.`) &&
|
||||
token.split('.').length === 3
|
||||
);
|
||||
}
|
||||
@@ -9,9 +9,9 @@ import { Transactional } from '@nestjs-cls/transactional';
|
||||
import type { McpAccessMode } from '@prisma/client';
|
||||
|
||||
import { CryptoHelper, EventBus } from '../../../base';
|
||||
import { MCP_CREDENTIAL_TOKEN_PREFIX } from '../../../core/auth/token';
|
||||
import { Models } from '../../../models';
|
||||
|
||||
const TOKEN_PREFIX = 'aff_mcp_v1';
|
||||
const ALLOWED_EXPIRATION_DAYS = new Set([30, 90, 365]);
|
||||
const ROTATION_GRACE_MS = 24 * 60 * 60 * 1000;
|
||||
const LAST_USED_WRITE_INTERVAL_MS = 5 * 60 * 1000;
|
||||
@@ -220,7 +220,7 @@ export class McpCredentialService {
|
||||
});
|
||||
return {
|
||||
credential: { ...credential, status: this.status(credential) },
|
||||
token: `${TOKEN_PREFIX}.${id}.${secret}`,
|
||||
token: `${MCP_CREDENTIAL_TOKEN_PREFIX}.${id}.${secret}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ export class McpCredentialService {
|
||||
const parts = token.split('.');
|
||||
if (
|
||||
parts.length !== 3 ||
|
||||
parts[0] !== TOKEN_PREFIX ||
|
||||
parts[0] !== MCP_CREDENTIAL_TOKEN_PREFIX ||
|
||||
!parts[1] ||
|
||||
!parts[2]
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user