@dollhousemcp/mcp-server 1.9.4 → 1.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/dist/config/ConfigWizardDisplay.d.ts +64 -0
- package/dist/config/ConfigWizardDisplay.d.ts.map +1 -0
- package/dist/config/ConfigWizardDisplay.js +150 -0
- package/dist/config/WizardFirstResponse.d.ts +25 -0
- package/dist/config/WizardFirstResponse.d.ts.map +1 -0
- package/dist/config/WizardFirstResponse.js +118 -0
- package/dist/elements/memories/MemoryManager.d.ts.map +1 -1
- package/dist/elements/memories/MemoryManager.js +54 -23
- package/dist/generated/version.d.ts +2 -2
- package/dist/generated/version.js +3 -3
- package/dist/scripts/scripts/run-config-wizard.js +57 -0
- package/dist/scripts/src/config/ConfigManager.js +799 -0
- package/dist/scripts/src/config/ConfigWizard.js +368 -0
- package/dist/scripts/src/errors/SecurityError.js +47 -0
- package/dist/scripts/src/security/constants.js +28 -0
- package/dist/scripts/src/security/contentValidator.js +415 -0
- package/dist/scripts/src/security/errors.js +32 -0
- package/dist/scripts/src/security/regexValidator.js +217 -0
- package/dist/scripts/src/security/secureYamlParser.js +272 -0
- package/dist/scripts/src/security/securityMonitor.js +111 -0
- package/dist/scripts/src/security/validators/unicodeValidator.js +315 -0
- package/dist/scripts/src/utils/logger.js +288 -0
- package/dist/tools/getWelcomeMessage.d.ts +41 -0
- package/dist/tools/getWelcomeMessage.d.ts.map +1 -0
- package/dist/tools/getWelcomeMessage.js +109 -0
- package/package.json +1 -1
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Secure YAML Parser for DollhouseMCP - For Markdown Files with YAML Frontmatter
|
|
4
|
+
*
|
|
5
|
+
* IMPORTANT: This parser is specifically designed for Markdown files with YAML frontmatter
|
|
6
|
+
* (the format used by personas, skills, templates, and other elements).
|
|
7
|
+
*
|
|
8
|
+
* USE THIS FOR:
|
|
9
|
+
* - Persona files (e.g., creative-writer.md)
|
|
10
|
+
* - Skill files (e.g., code-review.md)
|
|
11
|
+
* - Template files (e.g., meeting-notes.md)
|
|
12
|
+
* - Any Markdown file with YAML frontmatter between --- markers
|
|
13
|
+
*
|
|
14
|
+
* DO NOT USE THIS FOR:
|
|
15
|
+
* - Pure YAML configuration files (use js-yaml directly with FAILSAFE_SCHEMA)
|
|
16
|
+
* - JSON files
|
|
17
|
+
* - Plain text files without frontmatter
|
|
18
|
+
*
|
|
19
|
+
* FILE FORMAT EXPECTED:
|
|
20
|
+
* ```
|
|
21
|
+
* ---
|
|
22
|
+
* name: Element Name
|
|
23
|
+
* description: Element description
|
|
24
|
+
* version: 1.0.0
|
|
25
|
+
* ---
|
|
26
|
+
*
|
|
27
|
+
* # Markdown content here
|
|
28
|
+
* The actual content/instructions go here...
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Provides safe YAML parsing that prevents deserialization attacks
|
|
32
|
+
* by using a restricted schema and pre-validation.
|
|
33
|
+
*
|
|
34
|
+
* Security: SEC-003 - YAML parsing vulnerability protection
|
|
35
|
+
*/
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.SecureYamlParser = void 0;
|
|
38
|
+
const yaml = require("js-yaml");
|
|
39
|
+
const gray_matter_1 = require("gray-matter");
|
|
40
|
+
const SecurityError_js_1 = require("../errors/SecurityError.js");
|
|
41
|
+
const contentValidator_js_1 = require("./contentValidator.js");
|
|
42
|
+
const securityMonitor_js_1 = require("./securityMonitor.js");
|
|
43
|
+
class SecureYamlParser {
|
|
44
|
+
/**
|
|
45
|
+
* Parse a Markdown file with YAML frontmatter (Securely)
|
|
46
|
+
*
|
|
47
|
+
* @param input - The full content of a Markdown file with YAML frontmatter
|
|
48
|
+
* @param options - Parsing options for security and validation
|
|
49
|
+
* @returns ParsedContent with separated YAML data and Markdown content
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* // For a persona file:
|
|
54
|
+
* const personaFile = `---
|
|
55
|
+
* name: Creative Writer
|
|
56
|
+
* description: A creative writing assistant
|
|
57
|
+
* ---
|
|
58
|
+
* You are a creative writer...`;
|
|
59
|
+
*
|
|
60
|
+
* const result = SecureYamlParser.parse(personaFile);
|
|
61
|
+
* // result.data = { name: 'Creative Writer', description: '...' }
|
|
62
|
+
* // result.content = 'You are a creative writer...'
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
static parse(input, options = {}) {
|
|
66
|
+
const opts = { ...this.DEFAULT_OPTIONS, ...options };
|
|
67
|
+
// 1. Size validation
|
|
68
|
+
if (input.length > (opts.maxContentSize || this.DEFAULT_OPTIONS.maxContentSize)) {
|
|
69
|
+
throw new SecurityError_js_1.SecurityError('Content exceeds maximum allowed size', 'medium');
|
|
70
|
+
}
|
|
71
|
+
// 2. Extract frontmatter boundaries
|
|
72
|
+
const frontmatterMatch = input.match(/^---\n([\s\S]*?)\n---/);
|
|
73
|
+
if (!frontmatterMatch) {
|
|
74
|
+
// No frontmatter, return empty data
|
|
75
|
+
return {
|
|
76
|
+
data: {},
|
|
77
|
+
content: input
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const yamlContent = frontmatterMatch[1];
|
|
81
|
+
const markdownContent = input.substring(frontmatterMatch[0].length);
|
|
82
|
+
// 3. Validate YAML size
|
|
83
|
+
if (yamlContent.length > (opts.maxYamlSize || this.DEFAULT_OPTIONS.maxYamlSize)) {
|
|
84
|
+
throw new SecurityError_js_1.SecurityError('YAML frontmatter exceeds maximum allowed size', 'medium');
|
|
85
|
+
}
|
|
86
|
+
// 4. Pre-parse security validation
|
|
87
|
+
if (!contentValidator_js_1.ContentValidator.validateYamlContent(yamlContent)) {
|
|
88
|
+
securityMonitor_js_1.SecurityMonitor.logSecurityEvent({
|
|
89
|
+
type: 'YAML_INJECTION_ATTEMPT',
|
|
90
|
+
severity: 'CRITICAL',
|
|
91
|
+
source: 'secure_yaml_parser',
|
|
92
|
+
details: 'Malicious YAML pattern detected during parsing'
|
|
93
|
+
});
|
|
94
|
+
throw new SecurityError_js_1.SecurityError('Malicious YAML content detected', 'critical');
|
|
95
|
+
}
|
|
96
|
+
// 5. Parse with safe schema
|
|
97
|
+
let data;
|
|
98
|
+
try {
|
|
99
|
+
data = yaml.load(yamlContent, {
|
|
100
|
+
schema: this.SAFE_SCHEMA,
|
|
101
|
+
json: false, // Don't allow JSON-specific types
|
|
102
|
+
onWarning: (warning) => {
|
|
103
|
+
securityMonitor_js_1.SecurityMonitor.logSecurityEvent({
|
|
104
|
+
type: 'YAML_PARSING_WARNING',
|
|
105
|
+
severity: 'LOW',
|
|
106
|
+
source: 'secure_yaml_parser',
|
|
107
|
+
details: `YAML warning: ${warning.message}`
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
throw new SecurityError_js_1.SecurityError(`YAML parsing failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'high');
|
|
114
|
+
}
|
|
115
|
+
// 6. Ensure data is an object
|
|
116
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
|
117
|
+
throw new SecurityError_js_1.SecurityError('YAML must contain an object at root level', 'medium');
|
|
118
|
+
}
|
|
119
|
+
// 7. Validate allowed keys if specified
|
|
120
|
+
if (opts.allowedKeys) {
|
|
121
|
+
const invalidKeys = Object.keys(data).filter(key => !opts.allowedKeys.includes(key));
|
|
122
|
+
if (invalidKeys.length > 0) {
|
|
123
|
+
throw new SecurityError_js_1.SecurityError(`Invalid YAML keys detected: ${invalidKeys.join(', ')}`, 'medium');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// 8. Validate field types and content
|
|
127
|
+
for (const [key, value] of Object.entries(data)) {
|
|
128
|
+
// Check field-specific validators only if field validation is enabled
|
|
129
|
+
if (opts.validateFields && this.FIELD_VALIDATORS[key] && !this.FIELD_VALIDATORS[key](value)) {
|
|
130
|
+
throw new SecurityError_js_1.SecurityError(`Invalid value for field '${key}'`, 'medium');
|
|
131
|
+
}
|
|
132
|
+
// Validate string fields for injection patterns
|
|
133
|
+
if (typeof value === 'string' && opts.validateContent) {
|
|
134
|
+
const validation = contentValidator_js_1.ContentValidator.validateAndSanitize(value);
|
|
135
|
+
if (!validation.isValid && validation.severity === 'critical') {
|
|
136
|
+
throw new SecurityError_js_1.SecurityError(`Security threat detected in field '${key}'`, 'critical');
|
|
137
|
+
}
|
|
138
|
+
// Replace with sanitized content
|
|
139
|
+
data[key] = validation.sanitizedContent;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// 9. Validate markdown content if requested
|
|
143
|
+
let finalContent = markdownContent;
|
|
144
|
+
if (opts.validateContent) {
|
|
145
|
+
const contentValidation = contentValidator_js_1.ContentValidator.validateAndSanitize(markdownContent);
|
|
146
|
+
if (!contentValidation.isValid && contentValidation.severity === 'critical') {
|
|
147
|
+
throw new SecurityError_js_1.SecurityError('Security threat detected in content', 'critical');
|
|
148
|
+
}
|
|
149
|
+
finalContent = contentValidation.sanitizedContent || markdownContent;
|
|
150
|
+
}
|
|
151
|
+
securityMonitor_js_1.SecurityMonitor.logSecurityEvent({
|
|
152
|
+
type: 'YAML_PARSE_SUCCESS',
|
|
153
|
+
severity: 'LOW',
|
|
154
|
+
source: 'secure_yaml_parser',
|
|
155
|
+
details: `Successfully parsed YAML with ${Object.keys(data).length} fields`
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
data,
|
|
159
|
+
content: finalContent
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Create a secure gray-matter compatible parser
|
|
164
|
+
*/
|
|
165
|
+
static createSecureMatterParser() {
|
|
166
|
+
return {
|
|
167
|
+
parse: (input) => {
|
|
168
|
+
const result = this.parse(input);
|
|
169
|
+
return {
|
|
170
|
+
data: result.data,
|
|
171
|
+
content: result.content,
|
|
172
|
+
excerpt: result.excerpt,
|
|
173
|
+
orig: input
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
stringify: (content, data) => {
|
|
177
|
+
// Validate data before stringifying
|
|
178
|
+
const validation = contentValidator_js_1.ContentValidator.validateMetadata(data);
|
|
179
|
+
if (!validation.isValid) {
|
|
180
|
+
throw new SecurityError_js_1.SecurityError('Cannot stringify content with security threats', 'high');
|
|
181
|
+
}
|
|
182
|
+
// Use safe YAML dump
|
|
183
|
+
const yamlStr = yaml.dump(data, {
|
|
184
|
+
schema: this.SAFE_SCHEMA,
|
|
185
|
+
skipInvalid: true,
|
|
186
|
+
noRefs: true,
|
|
187
|
+
noCompatMode: true
|
|
188
|
+
});
|
|
189
|
+
return `---\n${yamlStr}---\n${content}`;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Safe wrapper for gray-matter with security validations
|
|
195
|
+
*/
|
|
196
|
+
static safeMatter(input, options) {
|
|
197
|
+
// First, use our secure parser
|
|
198
|
+
const secureParsed = this.parse(input);
|
|
199
|
+
// Then use gray-matter with custom engines
|
|
200
|
+
return (0, gray_matter_1.default)(input, {
|
|
201
|
+
...options,
|
|
202
|
+
engines: {
|
|
203
|
+
yaml: {
|
|
204
|
+
parse: (str) => {
|
|
205
|
+
// Use our secure YAML parsing
|
|
206
|
+
const parsed = yaml.load(str, {
|
|
207
|
+
schema: this.SAFE_SCHEMA,
|
|
208
|
+
json: false
|
|
209
|
+
});
|
|
210
|
+
// Ensure it's an object
|
|
211
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
212
|
+
return {};
|
|
213
|
+
}
|
|
214
|
+
return parsed;
|
|
215
|
+
},
|
|
216
|
+
stringify: (obj) => {
|
|
217
|
+
return yaml.dump(obj, {
|
|
218
|
+
schema: this.SAFE_SCHEMA,
|
|
219
|
+
skipInvalid: true,
|
|
220
|
+
noRefs: true
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
exports.SecureYamlParser = SecureYamlParser;
|
|
229
|
+
SecureYamlParser.DEFAULT_OPTIONS = {
|
|
230
|
+
maxYamlSize: 64 * 1024, // 64KB for YAML
|
|
231
|
+
maxContentSize: 1024 * 1024, // 1MB for content
|
|
232
|
+
validateContent: true,
|
|
233
|
+
validateFields: true // By default, apply field validators
|
|
234
|
+
};
|
|
235
|
+
// Allowed YAML types - using CORE_SCHEMA (safe subset with basic types like booleans and integers)
|
|
236
|
+
SecureYamlParser.SAFE_SCHEMA = yaml.CORE_SCHEMA;
|
|
237
|
+
// Additional validation for specific persona fields
|
|
238
|
+
SecureYamlParser.FIELD_VALIDATORS = {
|
|
239
|
+
name: (v) => typeof v === 'string' && v.length <= 100,
|
|
240
|
+
description: (v) => typeof v === 'string' && v.length <= 500,
|
|
241
|
+
author: (v) => typeof v === 'string' && v.length <= 100,
|
|
242
|
+
version: (v) => typeof v === 'string' && /^\d+\.\d+(\.\d+)?(-[a-zA-Z0-9.-]+)?$/.test(v),
|
|
243
|
+
category: (v) => typeof v === 'string' && v.length <= 50,
|
|
244
|
+
age_rating: (v) => ['all', '13+', '18+'].includes(v),
|
|
245
|
+
price: (v) => typeof v === 'string' && (v === 'free' || /^\$\d+\.\d{2}$/.test(v)),
|
|
246
|
+
ai_generated: (v) => typeof v === 'boolean' || v === 'true' || v === 'false',
|
|
247
|
+
generation_method: (v) => ['human', 'ChatGPT', 'Claude', 'hybrid'].includes(v),
|
|
248
|
+
created_date: (v) => {
|
|
249
|
+
if (typeof v !== 'string')
|
|
250
|
+
return false;
|
|
251
|
+
// More flexible date validation - accept common formats
|
|
252
|
+
// ISO8601, US format, European format, simple dates
|
|
253
|
+
const datePatterns = [
|
|
254
|
+
/^\d{4}-\d{2}-\d{2}$/, // YYYY-MM-DD
|
|
255
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, // ISO8601 with time
|
|
256
|
+
/^\d{1,2}\/\d{1,2}\/\d{4}$/, // MM/DD/YYYY or M/D/YYYY
|
|
257
|
+
/^\d{1,2}-\d{1,2}-\d{4}$/, // MM-DD-YYYY or M-D-YYYY
|
|
258
|
+
/^\d{1,2}\.\d{1,2}\.\d{4}$/, // DD.MM.YYYY (European)
|
|
259
|
+
/^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2},?\s+\d{4}$/i // Month DD, YYYY
|
|
260
|
+
];
|
|
261
|
+
// Check if it matches common patterns first
|
|
262
|
+
const matchesPattern = datePatterns.some(pattern => pattern.test(v.trim()));
|
|
263
|
+
if (!matchesPattern) {
|
|
264
|
+
// Fall back to Date.parse for other formats, but be more lenient
|
|
265
|
+
const parsed = Date.parse(v);
|
|
266
|
+
return !isNaN(parsed) && parsed > 0; // Ensure it's a valid positive timestamp
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
},
|
|
270
|
+
triggers: (v) => Array.isArray(v) && v.every(t => typeof t === 'string' && t.length <= 50),
|
|
271
|
+
content_flags: (v) => Array.isArray(v) && v.every(f => typeof f === 'string' && f.length <= 50)
|
|
272
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Security Monitor for DollhouseMCP
|
|
4
|
+
*
|
|
5
|
+
* Centralized security event logging and monitoring system
|
|
6
|
+
* for tracking and alerting on security-related events.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.SecurityMonitor = void 0;
|
|
10
|
+
const logger_js_1 = require("../utils/logger.js");
|
|
11
|
+
class SecurityMonitor {
|
|
12
|
+
/**
|
|
13
|
+
* Logs a security event
|
|
14
|
+
*/
|
|
15
|
+
static logSecurityEvent(event) {
|
|
16
|
+
const logEntry = {
|
|
17
|
+
...event,
|
|
18
|
+
timestamp: new Date().toISOString(),
|
|
19
|
+
id: `SEC-${Date.now()}-${++this.eventCount}`,
|
|
20
|
+
};
|
|
21
|
+
// Store in memory (circular buffer)
|
|
22
|
+
this.events.push(logEntry);
|
|
23
|
+
if (this.events.length > this.MAX_EVENTS) {
|
|
24
|
+
this.events.shift();
|
|
25
|
+
}
|
|
26
|
+
// In MCP servers, we cannot write to stderr/stdout as it breaks the JSON-RPC protocol
|
|
27
|
+
// Security events are stored in memory and can be retrieved via API
|
|
28
|
+
// Only send critical alerts via the proper channel
|
|
29
|
+
if (event.severity === 'CRITICAL') {
|
|
30
|
+
this.sendSecurityAlert(logEntry);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Sends security alerts for critical events
|
|
35
|
+
*/
|
|
36
|
+
static sendSecurityAlert(event) {
|
|
37
|
+
// In a production environment, this would integrate with:
|
|
38
|
+
// - Slack webhooks
|
|
39
|
+
// - Email alerts
|
|
40
|
+
// - PagerDuty
|
|
41
|
+
// - Security Information and Event Management (SIEM) systems
|
|
42
|
+
// Log critical security alerts with structured data
|
|
43
|
+
// DO NOT use console.error in MCP servers as it breaks the JSON-RPC protocol
|
|
44
|
+
logger_js_1.logger.error('🚨 CRITICAL SECURITY ALERT 🚨', {
|
|
45
|
+
type: event.type,
|
|
46
|
+
details: event.details,
|
|
47
|
+
timestamp: event.timestamp,
|
|
48
|
+
id: event.id
|
|
49
|
+
});
|
|
50
|
+
// If in production mode with proper config, send actual alerts
|
|
51
|
+
if (process.env.DOLLHOUSE_SECURITY_ALERTS === 'true') {
|
|
52
|
+
// TODO: Implement actual alert mechanisms
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Gets recent security events for analysis
|
|
57
|
+
*/
|
|
58
|
+
static getRecentEvents(count = 100) {
|
|
59
|
+
return this.events.slice(-count);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Gets events by severity
|
|
63
|
+
*/
|
|
64
|
+
static getEventsBySeverity(severity) {
|
|
65
|
+
return this.events.filter(event => event.severity === severity);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Gets events by type
|
|
69
|
+
*/
|
|
70
|
+
static getEventsByType(type) {
|
|
71
|
+
return this.events.filter(event => event.type === type);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Generates a security report
|
|
75
|
+
*/
|
|
76
|
+
static generateSecurityReport() {
|
|
77
|
+
const eventsBySeverity = {
|
|
78
|
+
CRITICAL: 0,
|
|
79
|
+
HIGH: 0,
|
|
80
|
+
MEDIUM: 0,
|
|
81
|
+
LOW: 0,
|
|
82
|
+
};
|
|
83
|
+
const eventsByType = {};
|
|
84
|
+
for (const event of this.events) {
|
|
85
|
+
eventsBySeverity[event.severity]++;
|
|
86
|
+
eventsByType[event.type] = (eventsByType[event.type] || 0) + 1;
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
totalEvents: this.events.length,
|
|
90
|
+
eventsBySeverity,
|
|
91
|
+
eventsByType,
|
|
92
|
+
recentCriticalEvents: this.getEventsBySeverity('CRITICAL').slice(-10),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Clears old events (for memory management)
|
|
97
|
+
*/
|
|
98
|
+
static clearOldEvents(daysToKeep = 7) {
|
|
99
|
+
const cutoffDate = new Date();
|
|
100
|
+
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
|
101
|
+
const cutoffTimestamp = cutoffDate.toISOString();
|
|
102
|
+
const index = this.events.findIndex(event => event.timestamp >= cutoffTimestamp);
|
|
103
|
+
if (index > 0) {
|
|
104
|
+
this.events.splice(0, index);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.SecurityMonitor = SecurityMonitor;
|
|
109
|
+
SecurityMonitor.eventCount = 0;
|
|
110
|
+
SecurityMonitor.events = [];
|
|
111
|
+
SecurityMonitor.MAX_EVENTS = 1000; // Keep last 1000 events in memory
|