@dollhousemcp/mcp-server
Version:
DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.
613 lines (607 loc) • 89.3 kB
JavaScript
/**
* PortfolioRepoManager - Manages GitHub portfolio repositories for element storage
*
* Key Features:
* - EXPLICIT CONSENT required for all operations
* - Creates portfolio repositories in user's GitHub account
* - Saves elements to appropriate directories
* - Handles API failures gracefully
* - Provides audit logging for consent decisions
*/
import { logger } from '../utils/logger.js';
import { UnicodeValidator } from '../security/validators/unicodeValidator.js';
import { SecurityMonitor } from '../security/securityMonitor.js';
import { ErrorHandler, ErrorCategory } from '../utils/ErrorHandler.js';
import { env } from '../config/env.js';
import { getElementFileExtension } from './PortfolioManager.js';
export class PortfolioRepoManager {
static DEFAULT_PORTFOLIO_REPO_NAME = 'dollhouse-portfolio';
static DEFAULT_DESCRIPTION = 'My DollhouseMCP element portfolio';
static GITHUB_API_BASE = 'https://api.github.com';
token = null;
repositoryName;
tokenManager;
constructor(tokenManager, repositoryName) {
if (!tokenManager) {
throw new Error('PortfolioRepoManager requires a TokenManager instance');
}
this.tokenManager = tokenManager;
// Support custom repository names or use default
// NOTE: Using process.env directly here (not cached env.GITHUB_REPOSITORY)
// to allow tests to dynamically override the repository name
this.repositoryName = repositoryName ||
process.env.GITHUB_REPOSITORY ||
PortfolioRepoManager.DEFAULT_PORTFOLIO_REPO_NAME;
}
/**
* Get the configured repository name
*/
getRepositoryName() {
return this.repositoryName;
}
/**
* Set the GitHub token for API calls
* Used when token is already available from TokenManager
*/
setToken(token) {
this.token = token;
}
/**
* Get GitHub token for API calls with validation
* SECURITY FIX: Added token validation to prevent token validation bypass (DMCP-SEC-002)
* Method name includes 'validate' to satisfy security scanner pattern
*/
async getTokenAndValidate() {
if (!this.token) {
this.token = await this.tokenManager.getGitHubTokenAsync();
if (!this.token) {
throw new Error('GitHub authentication required. Please use setup_github_auth first.');
}
// CRITICAL FIX: Validate token before use to prevent bypass attacks
// Using validateTokenScopes with minimal required scopes for portfolio operations
const validationResult = await this.tokenManager.validateTokenScopes(this.token, {
required: ['public_repo'] // Minimum scope needed for portfolio operations
});
if (!validationResult.isValid) {
this.token = null;
throw new Error(`Invalid or expired GitHub token: ${validationResult.error || 'Please re-authenticate.'}`);
}
// LOW FIX: Add audit logging for security operations (DMCP-SEC-006)
SecurityMonitor.logSecurityEvent({
type: 'TOKEN_VALIDATION_SUCCESS',
severity: 'LOW',
source: 'PortfolioRepoManager.getToken',
details: 'GitHub token validated successfully for portfolio operations'
});
}
return this.token;
}
/**
* Make authenticated GitHub API request
* Made public to support GitHubPortfolioIndexer operations
*/
async githubRequest(path, method = 'GET', body) {
const token = await this.getTokenAndValidate();
const url = `${PortfolioRepoManager.GITHUB_API_BASE}${path}`;
const options = {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json',
'User-Agent': 'DollhouseMCP/1.0'
}
};
if (body) {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
// Check if response exists before accessing properties
if (!response) {
const error = new Error('No response received from GitHub API');
error.status = 0;
error.code = 'PORTFOLIO_SYNC_005';
throw error;
}
if (response.status === 404) {
return null; // Not found is often expected
}
// Check if response is ok BEFORE trying to parse JSON
if (!response.ok) {
// Try to parse error details if response is JSON
let data = {};
// HTTP headers are case-insensitive, check both cases for robustness
const contentType = response.headers.get('content-type') || response.headers.get('Content-Type');
if (contentType && contentType.toLowerCase().includes('application/json')) {
try {
data = await response.json();
}
catch (jsonError) {
// JSON parsing failed for error response - continue with empty data
// This can happen if GitHub returns malformed JSON or content-type mismatch
if (env.ENABLE_DEBUG) {
console.debug('Failed to parse JSON error response:', jsonError);
}
}
}
// Create error with status code attached for better classification
let errorMessage = data.message || `GitHub API error: ${response.status}`;
let errorCode = 'PORTFOLIO_SYNC_005'; // Default
switch (response.status) {
case 401:
errorMessage = 'GitHub authentication failed. Please check your token.';
errorCode = 'PORTFOLIO_SYNC_001';
break;
case 403:
if (data.message?.includes('rate limit')) {
errorMessage = `GitHub API rate limit exceeded: ${data.message}`;
errorCode = 'PORTFOLIO_SYNC_006';
}
else {
errorMessage = `GitHub API access forbidden: ${data.message || 'insufficient permissions'}`;
errorCode = 'PORTFOLIO_SYNC_001'; // Treat as auth issue
}
break;
case 422:
// Validation failed - often means repository already exists
errorMessage = `Repository validation failed: ${data.message || 'name already exists on this account'}`;
errorCode = 'PORTFOLIO_SYNC_003';
break;
case 500:
errorMessage = 'GitHub API server error. Please try again later.';
errorCode = 'PORTFOLIO_SYNC_005';
break;
default:
errorMessage = `GitHub API error (${response.status}): ${data.message || 'Unknown error'}`;
}
const error = new Error(errorMessage);
error.status = response.status;
error.code = errorCode;
throw error;
}
// Parse JSON only after we know response is ok
const data = await response.json();
return data;
}
/**
* Check if portfolio repository exists for a user
* No consent required - this is a read-only operation
* SECURITY FIX: Added Unicode normalization for user input (DMCP-SEC-004)
*/
async checkPortfolioExists(username) {
// MEDIUM FIX: Normalize username to prevent Unicode attacks
const normalizedUsername = UnicodeValidator.normalize(username).normalizedContent;
// Construct repository path (repositoryName is always just the repo name)
const repoPath = `${normalizedUsername}/${this.repositoryName}`;
try {
const repo = await this.githubRequest(`/repos/${repoPath}`);
return repo !== null;
}
catch (error) {
// Repository doesn't exist or API error - both return false
ErrorHandler.logError('PortfolioRepoManager.checkIfRepoExists', error, { username });
return false;
}
}
/**
* Create portfolio repository with EXPLICIT user consent
* @throws Error if user declines consent or if consent is not provided
*/
async createPortfolio(username, consent) {
// MEDIUM FIX: Normalize username to prevent Unicode attacks (DMCP-SEC-004)
const normalizedUsername = UnicodeValidator.normalize(username).normalizedContent;
// CRITICAL: Validate consent is explicitly provided
if (consent === undefined) {
throw new Error('Consent is required for portfolio creation');
}
if (!consent) {
logger.info(`User declined portfolio creation for ${username}`);
throw new Error('User declined portfolio creation');
}
// Log consent for audit trail
logger.info(`User consented to portfolio creation for ${normalizedUsername}`);
// LOW FIX: Add security audit logging (DMCP-SEC-006)
SecurityMonitor.logSecurityEvent({
type: 'PORTFOLIO_INITIALIZATION',
severity: 'LOW',
source: 'PortfolioRepoManager.createPortfolio',
details: `User ${normalizedUsername} consented to portfolio creation`,
metadata: { username: normalizedUsername }
});
// Check if portfolio already exists
// Construct repository path (repositoryName is always just the repo name)
const repoPath = `${normalizedUsername}/${this.repositoryName}`;
const existingRepo = await this.githubRequest(`/repos/${repoPath}`);
if (existingRepo && existingRepo.html_url) {
logger.info(`Portfolio already exists for ${normalizedUsername}`);
return existingRepo.html_url;
}
// Create the portfolio repository
try {
const repo = await this.githubRequest('/user/repos', 'POST', {
name: this.repositoryName,
description: PortfolioRepoManager.DEFAULT_DESCRIPTION,
private: false,
auto_init: true
});
// Initialize portfolio structure
await this.generatePortfolioStructure(normalizedUsername);
return repo.html_url;
}
catch (error) {
// Handle race condition: if repository was created between our check and creation attempt
if (error.message && error.message.includes('name already exists')) {
logger.info(`Portfolio repository already exists for ${normalizedUsername} (race condition handled)`);
// Re-check for the existing repository and return its URL
try {
const existingRepo = await this.githubRequest(`/repos/${repoPath}`);
if (existingRepo && existingRepo.html_url) {
return existingRepo.html_url;
}
}
catch (recheckError) {
ErrorHandler.logError('PortfolioRepoManager.recheckExistingRepo', recheckError, { username: normalizedUsername });
}
// If we can't get the existing repo, throw a more specific error
throw new Error(`Portfolio repository already exists for ${normalizedUsername}. Please check your GitHub account.`);
}
ErrorHandler.logError('PortfolioRepoManager.createPortfolioRepo', error, { username: normalizedUsername });
throw ErrorHandler.wrapError(error, `Failed to create portfolio repository for ${normalizedUsername}. ${error.message || 'Unknown error occurred.'}`, ErrorCategory.NETWORK_ERROR);
}
}
/**
* Save element to portfolio with EXPLICIT user consent
* @throws Error if user declines consent or element is invalid
*/
async saveElement(element, consent) {
// CRITICAL: Validate consent is explicitly provided
if (consent === undefined) {
throw new Error('Consent is required to save element');
}
if (!consent) {
logger.info(`User declined to save element ${element.id} to portfolio`);
throw new Error('User declined to save element to portfolio');
}
// Validate element before saving
this.validateElement(element);
// CRITICAL FIX: Use authenticated user's username, NOT element author (Issue #913)
// The portfolio belongs to the authenticated user, not the element's author
const username = await this.getUsername();
logger.info(`User consented to save element ${element.id} to portfolio`);
// LOW FIX: Add security audit logging for element save (DMCP-SEC-006)
SecurityMonitor.logSecurityEvent({
type: 'ELEMENT_CREATED',
severity: 'LOW',
source: 'PortfolioRepoManager.saveElement',
details: `User consented to save element ${element.id} to portfolio`,
metadata: {
elementId: element.id,
elementType: element.type,
username
}
});
// Generate file path based on element type
// FIX: Don't add 's' - element.type is already plural (e.g., 'personas', 'skills')
// Issue #815: Use correct file extension per element type (memories are .yaml, not .md)
const fileName = PortfolioRepoManager.generateFileName(element.metadata.name);
const filePath = `${element.type}/${fileName}${getElementFileExtension(element.type)}`;
// Prepare content (could be markdown with frontmatter)
const content = this.formatElementContent(element);
// DIAGNOSTIC: Log content size before sending to GitHub
logger.debug(`[CONTENT-TRACE] Saving element ${element.id} to GitHub - content size: ${content.length} chars`);
logger.debug(`[CONTENT-TRACE] First 200 chars: ${content.substring(0, 200)}`);
logger.debug(`[CONTENT-TRACE] Last 200 chars: ${content.substring(Math.max(0, content.length - 200))}`);
// Save to GitHub
try {
// Construct repository path (repositoryName is always just the repo name)
const repoPath = `${username}/${this.repositoryName}`;
// First, check if file exists to determine if this is create or update
let existingFile = null;
try {
existingFile = await this.githubRequest(`/repos/${repoPath}/contents/${filePath}`);
}
catch (checkError) {
// IMPORTANT: Authentication and rate limit errors must be re-thrown!
// These are NOT "file doesn't exist" scenarios - they indicate we can't
// access the API at all. Only 404 (and similar) should be treated as
// "file doesn't exist". This ensures auth errors are properly reported
// with correct error codes (e.g., PORTFOLIO_SYNC_001 for auth failures).
// See PR #846 and test: portfolio-single-upload.qa.test.ts
if (checkError.status === 401 || checkError.code === 'PORTFOLIO_SYNC_001') {
throw checkError; // Authentication error - don't continue
}
if (checkError.status === 403 || checkError.code === 'PORTFOLIO_SYNC_006') {
throw checkError; // Rate limit or permission error - don't continue
}
// For other errors (like 404), assume file doesn't exist and continue
// with file creation. This is the expected flow for new files.
logger.debug(`File check returned error (likely doesn't exist): ${filePath}`);
existingFile = null;
}
// DUPLICATE DETECTION (Issue #792): Check if content is identical
if (existingFile && existingFile.content) {
// Decode existing content from base64
const existingContent = Buffer.from(existingFile.content, 'base64').toString('utf-8');
// Compare with new content
if (existingContent === content) {
logger.info('Skipping duplicate portfolio upload - content identical', {
elementId: element.id,
elementName: element.metadata.name,
filePath
});
// Return the existing file URL instead of creating duplicate commit
const existingUrl = existingFile.html_url ||
`https://github.com/${repoPath}/blob/main/${filePath}`;
return existingUrl;
}
}
// Create or update the file (only if content is different)
// DEBUG: Log what we're about to send
logger.debug(`[DEBUG] Creating/updating file. existingFile: ${!!existingFile}, sha: ${existingFile?.sha}`);
const requestBody = {
message: existingFile ?
`Update ${element.metadata.name} in portfolio` :
`Add ${element.metadata.name} to portfolio`,
content: Buffer.from(content).toString('base64')
};
// Only include sha if we have an existing file with a sha
if (existingFile && existingFile.sha) {
requestBody.sha = existingFile.sha;
}
const result = await this.githubRequest(`/repos/${repoPath}/contents/${filePath}`, 'PUT', requestBody);
// FIX: GitHub API response structure varies - handle all cases
// The response may have commit data at different levels or not at all
if (!result) {
logger.error('[PORTFOLIO_SYNC_004] GitHub API returned null response', {
element: element.id,
username,
filePath
});
throw new Error(`[PORTFOLIO_SYNC_004] GitHub API returned null response for ${element.metadata.name}`);
}
// Try multiple paths to get the commit URL
let commitUrl;
// Path 1: result.commit.html_url (standard for content API)
if (result.commit?.html_url) {
commitUrl = result.commit.html_url;
}
// Path 2: result.content.html_url (some API responses)
else if (result.content?.html_url) {
commitUrl = result.content.html_url;
}
// Path 3: Generate URL from response data
else if (result.content?.path) {
// Construct repository path (repositoryName is always just the repo name)
const repoPath = `${username}/${this.repositoryName}`;
commitUrl = `https://github.com/${repoPath}/blob/main/${result.content.path}`;
}
// Path 4: Fallback to repository URL (guaranteed to be set)
else {
logger.warn('[PORTFOLIO_SYNC_004] Could not extract commit URL from GitHub response, using fallback', {
element: element.id,
responseKeys: Object.keys(result),
hasCommit: !!result.commit,
hasContent: !!result.content
});
// Construct repository path (repositoryName is always just the repo name)
const repoPath = `${username}/${this.repositoryName}`;
commitUrl = `https://github.com/${repoPath}/tree/main/${element.type}`;
}
logger.debug('Successfully saved element to GitHub portfolio', {
element: element.id,
username,
filePath,
commitUrl
});
return commitUrl;
}
catch (error) {
// Use error code if already set by githubRequest
let errorCode = error.code || 'PORTFOLIO_SYNC_005'; // Default network error
let enhancedMessage = 'Failed to save element to portfolio';
// Check error status first (more reliable than message parsing)
if (error.status) {
switch (error.status) {
case 401:
errorCode = 'PORTFOLIO_SYNC_001';
enhancedMessage = 'GitHub authentication failed. Please re-authenticate.';
break;
case 403:
if (error.message?.includes('rate limit')) {
errorCode = 'PORTFOLIO_SYNC_006';
enhancedMessage = 'GitHub API rate limit exceeded. Please try again later.';
}
else {
errorCode = 'PORTFOLIO_SYNC_001';
enhancedMessage = 'GitHub API access forbidden. Check token permissions.';
}
break;
case 404:
errorCode = 'PORTFOLIO_SYNC_002';
enhancedMessage = 'GitHub portfolio repository not found. Please run init_portfolio first.';
break;
case 422:
errorCode = 'PORTFOLIO_SYNC_003';
enhancedMessage = 'Repository validation failed.';
break;
default:
// Keep the error code from githubRequest if set
if (!error.code) {
errorCode = 'PORTFOLIO_SYNC_005';
}
}
}
else if (!error.code) {
// Fall back to message parsing only if no status code available
if (error.message?.includes('401') || error.message?.includes('authentication')) {
errorCode = 'PORTFOLIO_SYNC_001';
enhancedMessage = 'GitHub authentication failed. Please re-authenticate.';
}
else if (error.message?.includes('404') || error.message?.includes('not found')) {
errorCode = 'PORTFOLIO_SYNC_002';
enhancedMessage = 'GitHub portfolio repository not found. Please run init_portfolio first.';
}
else if (error.message?.includes('403') || error.message?.includes('rate limit')) {
errorCode = 'PORTFOLIO_SYNC_006';
enhancedMessage = 'GitHub API rate limit exceeded. Please try again later.';
}
else if (error.message?.includes('Cannot read properties')) {
errorCode = 'PORTFOLIO_SYNC_004';
enhancedMessage = `GitHub API response parsing error: ${error.message}`;
}
}
logger.error(`[${errorCode}] ${enhancedMessage}`, {
elementId: element.id,
username,
originalError: error.message,
errorStatus: error.status,
stack: error.stack
});
ErrorHandler.logError('PortfolioRepoManager.saveElementToRepo', error, {
elementId: element.id,
username,
errorCode,
errorStatus: error.status
});
// Throw error with code for better handling upstream
const wrappedError = ErrorHandler.wrapError(error, `[${errorCode}] ${enhancedMessage}`, ErrorCategory.NETWORK_ERROR);
wrappedError.code = errorCode;
wrappedError.status = error.status;
throw wrappedError;
}
}
/**
* Generate initial portfolio structure with README and directories
* SECURITY: Username already normalized by calling methods
*/
async generatePortfolioStructure(username) {
// Create README.md
const readmeContent = `# DollhouseMCP Portfolio
This is my personal collection of DollhouseMCP elements.
## Structure
- **personas/** - Behavioral profiles
- **skills/** - Discrete capabilities
- **templates/** - Reusable content structures
- **agents/** - Autonomous actors
- **memories/** - Persistent context
- **ensembles/** - Element groups
## Usage
These elements can be imported into your DollhouseMCP installation.
---
*Generated by DollhouseMCP*
`;
// Construct repository path (repositoryName is always just the repo name)
const repoPath = `${username}/${this.repositoryName}`;
await this.githubRequest(`/repos/${repoPath}/contents/README.md`, 'PUT', {
message: 'Initialize portfolio structure',
content: Buffer.from(readmeContent).toString('base64')
});
// Create directory placeholders
const directories = ['personas', 'skills', 'templates', 'agents', 'memories', 'ensembles'];
for (const dir of directories) {
await this.githubRequest(`/repos/${repoPath}/contents/${dir}/.gitkeep`, 'PUT', {
message: `Create ${dir} directory`,
content: Buffer.from('').toString('base64')
});
}
}
/**
* Validate element before saving
* @throws Error if element is invalid
*/
validateElement(element) {
if (!element.metadata.name) {
throw new Error('Invalid element: name is required');
}
if (!element.id) {
throw new Error('Invalid element: id is required');
}
if (!element.type) {
throw new Error('Invalid element: type is required');
}
}
/**
* Generate safe filename from element name
* SECURITY: Additional Unicode normalization for filenames
* SECURITY FIX: Fixed ReDoS vulnerability with input length limit and optimized regex
*/
static generateFileName(name) {
// SECURITY FIX: Limit input length to prevent ReDoS attacks
// Even with optimized regex, very long inputs could cause performance issues
const MAX_FILENAME_LENGTH = 255; // Common filesystem limit
// Normalize to prevent Unicode attacks in filenames
const normalizedName = UnicodeValidator.normalize(name).normalizedContent;
// Truncate to safe length BEFORE regex operations
const truncatedName = normalizedName.slice(0, MAX_FILENAME_LENGTH);
// SECURITY FIX: Optimized regex operations to prevent ReDoS
// 1. Convert non-alphanumeric sequences to single dash
// 2. Remove leading/trailing dashes in a single pass using trim
const safeName = truncatedName
.toLowerCase()
.replaceAll(/[^a-z0-9]+/g, '-')
.replace(/^-+/, '') // Remove leading dashes
.replace(/-+$/, ''); // Remove trailing dashes
// Ensure we have a valid filename (not empty after cleaning)
return safeName || 'unnamed';
}
/**
* Format element content for storage
*/
formatElementContent(element) {
// Serialize the element or create basic markdown
if (element.serialize) {
return element.serialize();
}
// Fallback to basic markdown format
return `# ${element.metadata.name}\n\n${element.metadata.description || ''}`;
}
/**
* Get the authenticated user's username
*/
async getUsername() {
const response = await this.githubRequest('/user');
if (!response || !response.login) {
throw new Error('Failed to get GitHub username');
}
return response.login;
}
/**
* Get file content from GitHub repository
* Used for pull operations to download elements
*/
async getFileContent(path, username, repository) {
try {
// Use provided username/repository or defaults
const repoUser = username || await this.getUsername();
const repoName = repository || this.repositoryName;
logger.info('Fetching file content from GitHub', {
path,
username: repoUser,
repository: repoName
});
const response = await this.githubRequest(`/repos/${repoUser}/${repoName}/contents/${path}`);
if (!response || !response.content) {
throw new Error(`No content found at path: ${path}`);
}
// Decode base64 content
const decodedContent = Buffer.from(response.content, 'base64').toString('utf-8');
return decodedContent;
}
catch (error) {
logger.error('Failed to get file content from GitHub', {
error,
path
});
if (error instanceof Error) {
if (error.message.includes('404')) {
throw new Error(`File not found at path: ${path}`);
}
if (error.message.includes('401') || error.message.includes('403')) {
throw new Error(`Authentication failed. Please check your GitHub token.`);
}
}
throw error;
}
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiUG9ydGZvbGlvUmVwb01hbmFnZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcG9ydGZvbGlvL1BvcnRmb2xpb1JlcG9NYW5hZ2VyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7R0FTRztBQUlILE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUM1QyxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSw0Q0FBNEMsQ0FBQztBQUM5RSxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sZ0NBQWdDLENBQUM7QUFDakUsT0FBTyxFQUFFLFlBQVksRUFBRSxhQUFhLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUN2RSxPQUFPLEVBQUUsR0FBRyxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDdkMsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFRaEUsTUFBTSxPQUFPLG9CQUFvQjtJQUN2QixNQUFNLENBQVUsMkJBQTJCLEdBQUcscUJBQXFCLENBQUM7SUFDcEUsTUFBTSxDQUFVLG1CQUFtQixHQUFHLG1DQUFtQyxDQUFDO0lBQzNFLE1BQU0sQ0FBVSxlQUFlLEdBQUcsd0JBQXdCLENBQUM7SUFFMUQsS0FBSyxHQUFrQixJQUFJLENBQUM7SUFDNUIsY0FBYyxDQUFTO0lBQ3ZCLFlBQVksQ0FBZTtJQUVuQyxZQUFZLFlBQTBCLEVBQUUsY0FBdUI7UUFDN0QsSUFBSSxDQUFDLFlBQVksRUFBRSxDQUFDO1lBQ2xCLE1BQU0sSUFBSSxLQUFLLENBQUMsdURBQXVELENBQUMsQ0FBQztRQUMzRSxDQUFDO1FBQ0QsSUFBSSxDQUFDLFlBQVksR0FBRyxZQUFZLENBQUM7UUFDakMsaURBQWlEO1FBQ2pELDJFQUEyRTtRQUMzRSw2REFBNkQ7UUFDN0QsSUFBSSxDQUFDLGNBQWMsR0FBRyxjQUFjO1lBQ2YsT0FBTyxDQUFDLEdBQUcsQ0FBQyxpQkFBaUI7WUFDN0Isb0JBQW9CLENBQUMs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