import crypto from 'node:crypto'; const TOKEN_PREFIX = 'msm1'; const TOKEN_AUDIENCE = 'mindspace-mcp'; const DEFAULT_TTL_SECONDS = 15 * 60; const MAX_TTL_SECONDS = 60 * 60; function scopedTokenError(message, code) { return Object.assign(new Error(message), { code }); } function requiredString(value, field) { const normalized = String(value ?? '').trim(); if (!normalized) { throw scopedTokenError( `${field} is required`, 'invalid_mcp_scope', ); } return normalized; } function normalizeTools(tools) { const normalized = [ ...new Set( (Array.isArray(tools) ? tools : []) .map((tool) => String(tool ?? '').trim()) .filter(Boolean), ), ].sort(); if (normalized.length === 0) { throw scopedTokenError( 'tools scope is required', 'invalid_mcp_scope', ); } return normalized; } function normalizeSecret(secret) { const normalized = String(secret ?? '').trim(); if (normalized.length < 16) { throw scopedTokenError( 'MindSpace MCP token secret must contain at least 16 characters', 'invalid_mcp_token_secret', ); } return normalized; } function encodeJson(value) { return Buffer.from( JSON.stringify(value), 'utf8', ).toString('base64url'); } function decodeJson(value) { try { return JSON.parse( Buffer.from( String(value ?? ''), 'base64url', ).toString('utf8'), ); } catch { throw scopedTokenError( 'Malformed MindSpace MCP token payload', 'invalid_mcp_token', ); } } function signPayload(payloadSegment, secret) { return crypto .createHmac('sha256', normalizeSecret(secret)) .update( `${TOKEN_PREFIX}.${payloadSegment}`, 'utf8', ) .digest('base64url'); } function signaturesMatch(left, right) { const leftBuffer = Buffer.from( String(left ?? ''), 'utf8', ); const rightBuffer = Buffer.from( String(right ?? ''), 'utf8', ); return ( leftBuffer.length === rightBuffer.length && crypto.timingSafeEqual( leftBuffer, rightBuffer, ) ); } export function mintMindSpaceMcpScopedToken({ secret, userId, sessionId, packageId, workspaceRef, tools, ttlSeconds = DEFAULT_TTL_SECONDS, now = Date.now(), tokenId = crypto.randomUUID(), } = {}) { const issuedAt = Math.floor(Number(now) / 1000); const normalizedTtl = Math.min( MAX_TTL_SECONDS, Math.max(60, Number(ttlSeconds) || 0), ); const payload = { v: 1, aud: TOKEN_AUDIENCE, sub: requiredString(userId, 'userId'), sessionId: requiredString( sessionId, 'sessionId', ), packageId: requiredString( packageId, 'packageId', ), workspaceRef: requiredString( workspaceRef, 'workspaceRef', ), tools: normalizeTools(tools), iat: issuedAt, exp: issuedAt + normalizedTtl, jti: requiredString(tokenId, 'tokenId'), }; const payloadSegment = encodeJson(payload); const signature = signPayload( payloadSegment, secret, ); return `${TOKEN_PREFIX}.${payloadSegment}.${signature}`; } export function verifyMindSpaceMcpScopedToken({ token, secret, tool = null, now = Date.now(), clockSkewSeconds = 30, } = {}) { const segments = String(token ?? '').split('.'); if ( segments.length !== 3 || segments[0] !== TOKEN_PREFIX ) { throw scopedTokenError( 'Malformed MindSpace MCP token', 'invalid_mcp_token', ); } const expectedSignature = signPayload( segments[1], secret, ); if ( !signaturesMatch( segments[2], expectedSignature, ) ) { throw scopedTokenError( 'Invalid MindSpace MCP token signature', 'invalid_mcp_token', ); } const payload = decodeJson(segments[1]); const currentSeconds = Math.floor(Number(now) / 1000); const skew = Math.max( 0, Number(clockSkewSeconds) || 0, ); if ( payload?.v !== 1 || payload?.aud !== TOKEN_AUDIENCE ) { throw scopedTokenError( 'Invalid MindSpace MCP token audience', 'invalid_mcp_token', ); } if ( !Number.isFinite(Number(payload.iat)) || !Number.isFinite(Number(payload.exp)) || Number(payload.iat) > currentSeconds + skew || Number(payload.exp) < currentSeconds - skew ) { throw scopedTokenError( 'Expired or not-yet-valid MindSpace MCP token', 'expired_mcp_token', ); } const claims = { userId: requiredString(payload.sub, 'userId'), sessionId: requiredString( payload.sessionId, 'sessionId', ), packageId: requiredString( payload.packageId, 'packageId', ), workspaceRef: requiredString( payload.workspaceRef, 'workspaceRef', ), tools: normalizeTools(payload.tools), issuedAt: Number(payload.iat) * 1000, expiresAt: Number(payload.exp) * 1000, tokenId: requiredString( payload.jti, 'tokenId', ), }; const normalizedTool = String(tool ?? '').trim(); if ( normalizedTool && !claims.tools.includes(normalizedTool) ) { throw scopedTokenError( `MindSpace MCP tool is not allowed: ${normalizedTool}`, 'mcp_tool_forbidden', ); } return claims; } export const mindSpaceMcpScopedTokenInternals = { DEFAULT_TTL_SECONDS, MAX_TTL_SECONDS, TOKEN_AUDIENCE, TOKEN_PREFIX, normalizeTools, };