toeverything/AFFiNE · error · BadRequestException
Unsupported MCP credential expiration
Error message
Unsupported MCP credential expiration
What it means
BadRequestException('Unsupported MCP credential expiration') thrown in the private issue() helper when input.expirationDays is not in ALLOWED_EXPIRATION_DAYS = {30, 90, 365}. Both create and rotate paths funnel through issue(), so any create/rotate request with a different lifetime (7, 60, 180, 0, negative, undefined) is rejected before anything is stored.
Source
Thrown at packages/backend/server/src/plugins/copilot/mcp/credential.ts:196
const now = new Date();
await this.models.mcpCredential.touch(
credential.id,
new Date(now.getTime() - LAST_USED_WRITE_INTERVAL_MS),
now
);
return credential;
}
private async issue(
input: IssueMcpCredential & {
familyId?: string;
generation?: number;
graceEndsAt?: Date;
}
) {
if (!ALLOWED_EXPIRATION_DAYS.has(input.expirationDays)) {
throw new BadRequestException('Unsupported MCP credential expiration');
}
const name = input.name.trim();
if (!name || name.length > 64) {
throw new BadRequestException('MCP credential name is required');
}
const id = randomUUID();
const secret = this.crypto.randomBytes(32).toString('base64url');
const secretHash = this.crypto.sha256(secret).toString('hex');
const credential = await this.models.mcpCredential.create({
id,
familyId: input.familyId ?? id,
generation: input.generation ?? 0,
name,
secretHash,
fingerprint: secretHash.slice(0, 12),
userId: input.userId,
workspaceId: input.workspaceId,View on GitHub (pinned to b4c8548c09)
Solutions
- Set expirationDays to exactly 30, 90, or 365
- Update the client to source allowed values from server docs/schema instead of hardcoding
- As a maintainer, move the allowed set into GraphQL validation so clients get schema errors earlier
Example fix
// before
await credentials.create({ userId, workspaceId, name, accessMode, expirationDays: 60 });
// after
const ALLOWED = [30, 90, 365] as const;
await credentials.create({
userId, workspaceId, name, accessMode,
expirationDays: ALLOWED.includes(reqDays) ? reqDays : 90,
}); Defensive patterns
Strategy: validation
Validate before calling
const ALLOWED_EXPIRATION_DAYS = new Set([30, 90, 365]);
const expirationDays = ALLOWED_EXPIRATION_DAYS.has(input.expirationDays)
? input.expirationDays
: 90;
await credentials.create({ ...input, expirationDays }); Type guard
const isAllowedExpiration = (d: unknown): d is 30 | 90 | 365 => d === 30 || d === 90 || d === 365;
Try / catch
try {
await credentials.create(input);
} catch (e) {
if (e instanceof BadRequestException && /expiration/.test(e.message)) {
input.expirationDays = 90;
await credentials.create(input);
} else throw e;
} Prevention
- Drive the expiration dropdown from the same {30, 90, 365} set as the server
- Pin the allowed set in a shared constant/package so client and server cannot drift
- Add schema-level enum validation on the GraphQL input to fail fast
When it happens
Trigger: createMcpCredential or rotateMcpCredential with expirationDays outside {30,90,365}; passing 0 or a negative number to mean 'no expiry'; enum desync where a client offers 7 or 60 days.
Common situations: Client UI dropdown drifts from server-allowed values after a version change; scripts hardcode a lifetime the server later removed; GraphQL input validated loosely and hits this server-side guard.
Related errors
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/e353f8122e34742d.
Report an issue: GitHub.