Initial commit: Clean DSS implementation

Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm

Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)

Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability

Migration completed: $(date)
🤖 Clean migration with full functionality preserved
This commit is contained in:
Digital Production Factory
2025-12-09 18:45:48 -03:00
commit 276ed71f31
884 changed files with 373737 additions and 0 deletions

View File

@@ -0,0 +1,668 @@
# DSS Export/Import - Integration Guide for Implementation Teams
## Quick Reference
| Need | Document | Location |
|------|----------|----------|
| **30-second overview** | QUICK_REFERENCE.md | Root directory |
| **Complete feature guide** | DSS_EXPORT_IMPORT_GUIDE.md | Root directory |
| **Architecture overview** | IMPLEMENTATION_SUMMARY.md | Root directory |
| **Production hardening details** | PRODUCTION_READINESS.md | Root directory |
| **Hardening summary** | PRODUCTION_HARDENING_SUMMARY.md | Root directory |
| **API integration** | This file (INTEGRATION_GUIDE.md) | Root directory |
| **Working code examples** | dss/export_import/examples.py | Package |
| **Security utilities** | dss/export_import/security.py | Package |
| **Service layer API** | dss/export_import/service.py | Package |
---
## For Your Implementation Team
### Phase 1: Understanding the System (30 minutes)
```
1. Read: QUICK_REFERENCE.md (5 min)
2. Run: python -m dss.export_import.examples (5 min)
3. Read: PRODUCTION_HARDENING_SUMMARY.md (10 min)
4. Skim: PRODUCTION_READINESS.md (10 min)
```
**Result**: You'll understand what the system does, how to use it, and what production considerations exist.
### Phase 2: API Integration Planning (1 hour)
```
1. Review: dss/export_import/service.py
- Read DSSProjectService docstring and method signatures
- Understand return types: ExportSummary, ImportSummary, MergeSummary
2. Review: dss/export_import/security.py
- Understand what each security class does
- Note configuration options
3. Plan: Where to integrate
- API endpoints for export/import?
- Background job handler (Celery/RQ)?
- CLI commands?
- Web UI buttons?
```
**Deliverable**: Integration plan document with:
- [ ] List of API endpoints needed
- [ ] Error handling strategy
- [ ] Background job approach
- [ ] Monitoring/alerting plan
### Phase 3: API Development (2-4 hours)
Follow the code examples below for your framework.
### Phase 4: Testing (1-2 hours)
```
1. Run examples with real project data
2. Test error scenarios
3. Load test with large projects
4. Test background job handling
```
### Phase 5: Deployment (30 minutes)
Follow production checklist in PRODUCTION_READINESS.md.
---
## API Integration Examples
### Flask
```python
from flask import Flask, request, send_file
from pathlib import Path
from dss.export_import import DSSProjectService
app = Flask(__name__)
service = DSSProjectService(busy_timeout_ms=5000)
@app.route('/api/projects/<int:project_id>/export', methods=['POST'])
def export_project(project_id):
"""Export project to .dss archive"""
try:
# Get project from database
project = db.session.query(Project).get(project_id)
if not project:
return {'error': 'Project not found'}, 404
# Export
output_path = Path(f'/tmp/export_{project_id}.dss')
result = service.export_project(project, output_path)
if not result.success:
return {'error': result.error}, 500
# Return file
return send_file(
result.archive_path,
as_attachment=True,
download_name=f'{project.name}.dss',
mimetype='application/zip'
)
except Exception as e:
app.logger.error(f"Export failed: {e}")
return {'error': 'Export failed'}, 500
@app.route('/api/projects/import', methods=['POST'])
def import_project():
"""Import project from .dss archive"""
try:
if 'file' not in request.files:
return {'error': 'No file provided'}, 400
file = request.files['file']
if not file.filename.endswith('.dss'):
return {'error': 'File must be .dss archive'}, 400
# Save uploaded file
archive_path = Path(f'/tmp/{file.filename}')
file.save(archive_path)
# Import
result = service.import_project(archive_path)
if result.requires_background_job:
# Schedule background import
task_id = import_project_async.delay(str(archive_path))
return {
'status': 'queued',
'job_id': task_id,
'estimated_items': (
result.item_counts.get('tokens', 0) +
result.item_counts.get('components', 0)
)
}, 202
if not result.success:
return {'error': result.error}, 500
# Store in database
new_project = Project(
name=result.project_name,
# ... other fields
)
db.session.add(new_project)
db.session.commit()
return {
'success': True,
'project_name': result.project_name,
'project_id': new_project.id,
'duration_seconds': result.duration_seconds
}, 201
except Exception as e:
app.logger.error(f"Import failed: {e}")
return {'error': 'Import failed'}, 500
@app.route('/api/projects/<int:project_id>/merge', methods=['POST'])
def merge_projects(project_id):
"""Merge imported project with local"""
try:
if 'file' not in request.files:
return {'error': 'No file provided'}, 400
file = request.files['file']
archive_path = Path(f'/tmp/{file.filename}')
file.save(archive_path)
# Get local project
local = db.session.query(Project).get(project_id)
if not local:
return {'error': 'Project not found'}, 404
# Analyze merge
merge_analysis = service.analyze_merge(local, archive_path)
# Perform merge
strategy = request.json.get('strategy', 'keep_local')
result = service.merge_project(local, archive_path, strategy)
if not result.success:
return {'error': result.error}, 500
# Update database
db.session.commit()
return {
'success': True,
'new_items': result.new_items_count,
'updated_items': result.updated_items_count,
'conflicts': result.conflicts_count,
'duration_seconds': result.duration_seconds
}
except Exception as e:
app.logger.error(f"Merge failed: {e}")
return {'error': 'Merge failed'}, 500
```
### FastAPI
```python
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import FileResponse
from pathlib import Path
from dss.export_import import DSSProjectService
app = FastAPI()
service = DSSProjectService(busy_timeout_ms=5000)
@app.post("/api/projects/{project_id}/export")
async def export_project(project_id: int):
"""Export project to .dss archive"""
try:
project = db.get_project(project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
output_path = Path(f"/tmp/export_{project_id}.dss")
result = service.export_project(project, output_path)
if not result.success:
raise HTTPException(status_code=500, detail=result.error)
return FileResponse(
result.archive_path,
media_type="application/zip",
filename=f"{project.name}.dss"
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/projects/import")
async def import_project(file: UploadFile = File(...)):
"""Import project from .dss archive"""
try:
if not file.filename.endswith('.dss'):
raise HTTPException(status_code=400, detail="File must be .dss")
# Save uploaded file
archive_path = Path(f"/tmp/{file.filename}")
with open(archive_path, "wb") as f:
f.write(await file.read())
# Import
result = service.import_project(archive_path)
if result.requires_background_job:
task_id = import_project_async.delay(str(archive_path))
return {
"status": "queued",
"job_id": task_id,
"estimated_items": (
result.item_counts.get('tokens', 0) +
result.item_counts.get('components', 0)
)
}
if not result.success:
raise HTTPException(status_code=500, detail=result.error)
return {
"success": True,
"project_name": result.project_name,
"duration_seconds": result.duration_seconds
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
```
### Django
```python
from django.http import JsonResponse, FileResponse
from django.views.decorators.http import require_http_methods
from pathlib import Path
from dss.export_import import DSSProjectService
service = DSSProjectService(busy_timeout_ms=5000)
@require_http_methods(["POST"])
def export_project(request, project_id):
"""Export project to .dss archive"""
try:
project = Project.objects.get(pk=project_id)
output_path = Path(f"/tmp/export_{project_id}.dss")
result = service.export_project(project, output_path)
if not result.success:
return JsonResponse({'error': result.error}, status=500)
response = FileResponse(
open(result.archive_path, 'rb'),
content_type='application/zip'
)
response['Content-Disposition'] = f'attachment; filename="{project.name}.dss"'
return response
except Project.DoesNotExist:
return JsonResponse({'error': 'Project not found'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
@require_http_methods(["POST"])
def import_project(request):
"""Import project from .dss archive"""
try:
if 'file' not in request.FILES:
return JsonResponse({'error': 'No file provided'}, status=400)
file = request.FILES['file']
if not file.name.endswith('.dss'):
return JsonResponse({'error': 'File must be .dss'}, status=400)
# Save uploaded file
archive_path = Path(f"/tmp/{file.name}")
with open(archive_path, 'wb') as f:
for chunk in file.chunks():
f.write(chunk)
# Import
result = service.import_project(archive_path)
if result.requires_background_job:
task_id = import_project_async.delay(str(archive_path))
return JsonResponse({
'status': 'queued',
'job_id': task_id
}, status=202)
if not result.success:
return JsonResponse({'error': result.error}, status=500)
return JsonResponse({
'success': True,
'project_name': result.project_name
}, status=201)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
```
---
## Background Job Integration
### Celery
```python
# celery_tasks.py
from celery import shared_task
from dss.export_import import DSSProjectService
from django.core.cache import cache
@shared_task(bind=True, time_limit=600)
def import_project_async(self, archive_path):
"""Background task for large imports"""
try:
service = DSSProjectService()
result = service.import_project(archive_path)
# Store result
cache.set(
f"import_job:{self.request.id}",
{
'status': 'completed' if result.success else 'failed',
'success': result.success,
'project_name': result.project_name,
'error': result.error,
'duration_seconds': result.duration_seconds,
},
timeout=3600 # 1 hour
)
if result.success:
# Trigger webhook
notify_user_import_complete(
self.request.id,
result.project_name
)
return {
'job_id': self.request.id,
'success': result.success
}
except Exception as e:
cache.set(
f"import_job:{self.request.id}",
{'status': 'failed', 'error': str(e)},
timeout=3600
)
raise
# In route
@app.post("/api/projects/import/background")
async def import_background(file: UploadFile):
"""Start background import"""
archive_path = Path(f"/tmp/{file.filename}")
with open(archive_path, "wb") as f:
f.write(await file.read())
task = import_project_async.delay(str(archive_path))
return {"job_id": task.id}
@app.get("/api/import/status/{job_id}")
async def import_status(job_id: str):
"""Check background import status"""
result = cache.get(f"import_job:{job_id}")
if not result:
return {"status": "processing"}
return result
```
---
## Error Handling
### Common Error Scenarios
```python
from dss.export_import import DSSArchiveImporter
def handle_import_error(archive_path):
"""Proper error handling with diagnostics"""
# Analyze archive to get detailed errors
importer = DSSArchiveImporter(archive_path)
analysis = importer.analyze()
if not analysis.is_valid:
for error in analysis.errors:
if error.stage == "archive":
if "Zip Slip" in error.message:
# Security alert!
alert_security_team(error.message)
return 403, "Malicious archive rejected"
elif "unsafe paths" in error.message:
return 400, "Invalid archive structure"
else:
return 400, f"Archive error: {error.message}"
elif error.stage == "manifest":
return 400, f"Invalid manifest: {error.message}"
elif error.stage == "schema":
if "newer than app" in error.message:
return 400, "DSS version too old, please update"
else:
return 400, f"Schema error: {error.message}"
elif error.stage == "structure":
return 400, f"Invalid JSON structure: {error.message}"
elif error.stage == "referential":
return 400, f"Invalid references: {error.message}"
# If we got here, archive is valid
return 200, "Archive is valid"
```
---
## Monitoring & Observability
### Metrics to Track
```python
import time
from prometheus_client import Counter, Histogram
# Metrics
export_duration = Histogram(
'dss_export_duration_seconds',
'Time to export project'
)
import_duration = Histogram(
'dss_import_duration_seconds',
'Time to import project'
)
validation_errors = Counter(
'dss_validation_errors_total',
'Validation errors',
['stage']
)
security_alerts = Counter(
'dss_security_alerts_total',
'Security alerts',
['type']
)
# Usage
with export_duration.time():
result = service.export_project(project, path)
if not result.success:
if "Zip Slip" in result.error:
security_alerts.labels(type='zip_slip').inc()
for error in analysis.errors:
validation_errors.labels(stage=error.stage).inc()
```
---
## Testing Strategy
### Unit Tests
```python
import pytest
from dss.export_import import DSSArchiveExporter, DSSArchiveImporter
def test_round_trip():
"""Test export → import = identical"""
# Create test project
project = create_test_project()
# Export
exporter = DSSArchiveExporter(project)
archive_path = exporter.export_to_file(Path("/tmp/test.dss"))
# Import
importer = DSSArchiveImporter(archive_path)
imported = importer.import_replace()
# Verify
assert imported.name == project.name
assert len(imported.theme.tokens) == len(project.theme.tokens)
def test_security_zip_slip():
"""Test Zip Slip protection"""
from dss.export_import.security import ZipSlipValidator
# Malicious paths
unsafe_paths = [
"../../etc/passwd",
"../../../root/.ssh/id_rsa",
"normal_file.json",
]
is_safe, unsafe = ZipSlipValidator.validate_archive_members(unsafe_paths)
assert not is_safe
assert len(unsafe) == 2 # Two unsafe paths
def test_memory_limits():
"""Test memory limit enforcement"""
from dss.export_import.security import MemoryLimitManager
mgr = MemoryLimitManager(max_tokens=100)
ok, error = mgr.check_token_count(101)
assert not ok
assert error is not None
```
### Integration Tests
```python
def test_import_with_large_archive():
"""Test import doesn't OOM on large archive"""
large_archive = create_large_archive(10000) # 10k tokens
result = service.import_project(large_archive)
assert result.success
def test_background_job_scheduling():
"""Test background job detection"""
huge_archive = create_huge_archive(50000) # 50k tokens
result = service.import_project(huge_archive)
assert result.requires_background_job
```
---
## Troubleshooting Guide
### Import Fails with "Archive validation failed"
```python
# Debug:
from dss.export_import import DSSArchiveImporter
importer = DSSArchiveImporter(archive_path)
analysis = importer.analyze()
for error in analysis.errors:
print(f"[{error.stage}] {error.message}")
print(f"Details: {error.details}")
```
### Memory limit exceeded on large archive
```python
# Solution 1: Increase limits
from dss.export_import.security import MemoryLimitManager
memory_mgr = MemoryLimitManager(
max_file_size=500_000_000, # 500MB
max_tokens=50000
)
# Solution 2: Use background job
result = service.import_project(archive, background=True)
if result.requires_background_job:
task_id = celery.send_task('import_project', args=[archive])
```
### Clock skew warnings during merge
```python
# These are informational - system is working correctly
# Warnings indicate clocks are >1 hour apart between systems
# To silence: Sync system clocks
# Or: Increase tolerance in TimestampConflictResolver
from dss.export_import.security import TimestampConflictResolver
from datetime import timedelta
resolver = TimestampConflictResolver(
clock_skew_tolerance=timedelta(hours=2)
)
```
---
## Summary
You now have everything needed to integrate DSS Export/Import:
1. ✅ Code examples for your framework
2. ✅ Background job integration
3. ✅ Error handling patterns
4. ✅ Monitoring setup
5. ✅ Testing strategy
6. ✅ Troubleshooting guide
**Next Steps:**
1. Pick your framework (Flask/FastAPI/Django)
2. Copy the example code
3. Adapt to your database models
4. Add your authentication/authorization
5. Follow production checklist in PRODUCTION_READINESS.md
**Questions?** Refer to the detailed documentation in the files listed at the top of this guide.
---
*Integration Guide v1.0*
*For DSS Export/Import v1.0.1*

View File

@@ -0,0 +1,494 @@
# DSS MCP Tools Specification
## New Tools for Project & Figma Management
Instead of REST endpoints, the following MCP tools should be implemented in `tools/dss_mcp/tools/project_tools.py`:
---
## 1. `dss_create_project`
**Purpose:** Create a new DSS project with empty Figma manifest
**Input Schema:**
```json
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Project name (required)"
},
"root_path": {
"type": "string",
"description": "Project root path (default: '.')"
},
"description": {
"type": "string",
"description": "Optional project description"
}
},
"required": ["name", "root_path"]
}
```
**Implementation:**
```python
async def create_project(self, name: str, root_path: str, description: str = "") -> Dict:
"""Create a new project with empty Figma manifest"""
# 1. Insert into projects table (name, root_path, description, created_at)
# 2. Create figma.json manifest in project folder:
# {
# "version": "1.0",
# "files": [],
# "lastUpdated": "2025-12-05T..."
# }
# 3. Return project metadata (id, name, root_path)
# 4. Emit project-created event
project_id = conn.execute(
"INSERT INTO projects (name, root_path, description, created_at) VALUES (?, ?, ?, ?)",
(name, root_path, description, datetime.now().isoformat())
).lastrowid
# Create manifest file
manifest_path = os.path.join(root_path, "figma.json")
manifest = {
"version": "1.0",
"files": [],
"lastUpdated": datetime.now().isoformat()
}
os.makedirs(root_path, exist_ok=True)
with open(manifest_path, 'w') as f:
json.dump(manifest, f, indent=2)
return {
"project_id": project_id,
"name": name,
"root_path": root_path,
"manifest_path": manifest_path,
"status": "created"
}
```
**Returns:**
```json
{
"project_id": "1",
"name": "my-design-system",
"root_path": "./packages/design",
"manifest_path": "./packages/design/figma.json",
"status": "created"
}
```
---
## 2. `dss_setup_figma_credentials`
**Purpose:** Store Figma API token at user level (encrypted)
**Input Schema:**
```json
{
"type": "object",
"properties": {
"api_token": {
"type": "string",
"description": "Figma Personal Access Token"
}
},
"required": ["api_token"]
}
```
**Implementation:**
```python
async def setup_figma_credentials(self, api_token: str, user_id: str = None) -> Dict:
"""Store and validate Figma API credentials at user level"""
# 1. Validate token by testing Figma API
headers = {"X-Figma-Token": api_token}
async with httpx.AsyncClient() as client:
response = await client.get("https://api.figma.com/v1/me", headers=headers)
if not response.is_success:
raise ValueError("Invalid Figma API token")
user_data = response.json()
# 2. Encrypt and store in project_integrations table (user-scoped)
# Use project_id=NULL for global user credentials
cipher = MCPConfig.get_cipher()
encrypted_config = cipher.encrypt(
json.dumps({"api_token": api_token}).encode()
).decode()
conn.execute(
"""INSERT OR REPLACE INTO project_integrations
(project_id, user_id, integration_type, config, enabled, created_at)
VALUES (NULL, ?, 'figma', ?, 1, ?)""",
(user_id or "anonymous", encrypted_config, datetime.now().isoformat())
)
# 3. Update integration_health
conn.execute(
"""INSERT OR REPLACE INTO integration_health
(integration_type, is_healthy, last_success_at)
VALUES ('figma', 1, ?)""",
(datetime.now().isoformat(),)
)
return {
"status": "configured",
"figma_user": user_data.get("name"),
"workspace": user_data.get("email"),
"message": "Figma credentials stored securely at user level"
}
```
**Returns:**
```json
{
"status": "configured",
"figma_user": "John Designer",
"workspace": "john@company.com",
"message": "Figma credentials stored securely at user level"
}
```
---
## 3. `dss_get_project_manifest`
**Purpose:** Read project's figma.json manifest
**Input Schema:**
```json
{
"type": "object",
"properties": {
"project_id": {
"type": "string",
"description": "Project ID"
}
},
"required": ["project_id"]
}
```
**Implementation:**
```python
async def get_project_manifest(self, project_id: str) -> Dict:
"""Get project's Figma manifest"""
# 1. Get project path from database
project = conn.execute(
"SELECT root_path FROM projects WHERE id = ?", (project_id,)
).fetchone()
if not project:
raise ValueError(f"Project {project_id} not found")
# 2. Read figma.json
manifest_path = os.path.join(project["root_path"], "figma.json")
if os.path.exists(manifest_path):
with open(manifest_path, 'r') as f:
manifest = json.load(f)
else:
manifest = {
"version": "1.0",
"files": [],
"lastUpdated": datetime.now().isoformat()
}
return manifest
```
**Returns:**
```json
{
"version": "1.0",
"files": [
{
"key": "figd_abc123",
"name": "Design Tokens",
"linkedAt": "2025-12-05T15:30:00Z"
}
],
"lastUpdated": "2025-12-05T16:00:00Z"
}
```
---
## 4. `dss_add_figma_file`
**Purpose:** Add Figma file reference to project manifest
**Input Schema:**
```json
{
"type": "object",
"properties": {
"project_id": {
"type": "string",
"description": "Project ID"
},
"file_key": {
"type": "string",
"description": "Figma file key (e.g., figd_abc123 or full URL)"
},
"file_name": {
"type": "string",
"description": "Optional display name for the file"
}
},
"required": ["project_id", "file_key"]
}
```
**Implementation:**
```python
async def add_figma_file(self, project_id: str, file_key: str, file_name: str = None) -> Dict:
"""Add Figma file to project manifest"""
# 1. Extract file key from URL if needed
if "figma.com" in file_key:
match = re.search(r"file/([a-zA-Z0-9]+)", file_key)
file_key = match.group(1) if match else file_key
# 2. Get project and load manifest
project = conn.execute(
"SELECT root_path FROM projects WHERE id = ?", (project_id,)
).fetchone()
manifest_path = os.path.join(project["root_path"], "figma.json")
with open(manifest_path, 'r') as f:
manifest = json.load(f)
# 3. Check if file already linked
existing = next((f for f in manifest["files"] if f["key"] == file_key), None)
if existing:
raise ValueError(f"File {file_key} already linked to project")
# 4. Add file to manifest
manifest["files"].append({
"key": file_key,
"name": file_name or f"Figma File {file_key[:8]}",
"linkedAt": datetime.now().isoformat()
})
manifest["lastUpdated"] = datetime.now().isoformat()
# 5. Write manifest back
with open(manifest_path, 'w') as f:
json.dump(manifest, f, indent=2)
return {
"project_id": project_id,
"file_key": file_key,
"file_name": file_name,
"status": "added",
"files_count": len(manifest["files"])
}
```
**Returns:**
```json
{
"project_id": "1",
"file_key": "figd_abc123",
"file_name": "Design Tokens",
"status": "added",
"files_count": 1
}
```
---
## 5. `dss_discover_figma_files`
**Purpose:** Discover available Figma files and suggest linking
**Input Schema:**
```json
{
"type": "object",
"properties": {
"project_id": {
"type": "string",
"description": "Project ID"
}
},
"required": ["project_id"]
}
```
**Implementation:**
```python
async def discover_figma_files(self, project_id: str, user_id: str = None) -> Dict:
"""Discover available Figma files from user's workspaces"""
# 1. Get user's Figma credentials from project_integrations
creds = conn.execute(
"""SELECT config FROM project_integrations
WHERE integration_type='figma' AND (project_id IS NULL OR project_id=?)
LIMIT 1""",
(project_id,)
).fetchone()
if not creds:
raise ValueError("Figma credentials not configured. Run /setup-figma first.")
# 2. Decrypt credentials
cipher = MCPConfig.get_cipher()
config = json.loads(cipher.decrypt(creds["config"].encode()).decode())
api_token = config["api_token"]
# 3. Fetch user's teams from Figma API
headers = {"X-Figma-Token": api_token}
available_files = []
async with httpx.AsyncClient() as client:
# Get teams
resp = await client.get("https://api.figma.com/v1/teams", headers=headers)
teams = resp.json().get("teams", [])
# Get projects in each team
for team in teams:
team_resp = await client.get(
f"https://api.figma.com/v1/teams/{team['id']}/projects",
headers=headers
)
projects = team_resp.json().get("projects", [])
for project in projects:
# Get files in each project
files_resp = await client.get(
f"https://api.figma.com/v1/projects/{project['id']}/files",
headers=headers
)
files = files_resp.json().get("files", [])
for file in files:
available_files.append({
"key": file["key"],
"name": file["name"],
"team": team["name"],
"project": project["name"]
})
# 4. Get currently linked files
manifest = await self.get_project_manifest(project_id)
linked_keys = {f["key"] for f in manifest["files"]}
# 5. Return available files (excluding already linked)
available = [f for f in available_files if f["key"] not in linked_keys]
return {
"project_id": project_id,
"linked_files": manifest["files"],
"available_files": available[:10], # Top 10 suggestions
"total_available": len(available),
"message": f"Found {len(available)} available Figma files"
}
```
**Returns:**
```json
{
"project_id": "1",
"linked_files": [
{"key": "figd_abc123", "name": "Design Tokens", "linkedAt": "..."}
],
"available_files": [
{"key": "figd_xyz789", "name": "Components", "team": "Design", "project": "Main"},
{"key": "figd_def456", "name": "Icons", "team": "Design", "project": "Main"}
],
"total_available": 2,
"message": "Found 2 available Figma files"
}
```
---
## 6. `dss_list_project_figma_files`
**Purpose:** List all Figma files currently linked to project
**Input Schema:**
```json
{
"type": "object",
"properties": {
"project_id": {
"type": "string",
"description": "Project ID"
}
},
"required": ["project_id"]
}
```
**Implementation:**
```python
async def list_project_figma_files(self, project_id: str) -> Dict:
"""List all Figma files in project manifest"""
manifest = await self.get_project_manifest(project_id)
return {
"project_id": project_id,
"files": manifest["files"],
"count": len(manifest["files"])
}
```
**Returns:**
```json
{
"project_id": "1",
"files": [
{
"key": "figd_abc123",
"name": "Design Tokens",
"linkedAt": "2025-12-05T15:30:00Z"
},
{
"key": "figd_xyz789",
"name": "Components",
"linkedAt": "2025-12-05T16:00:00Z"
}
],
"count": 2
}
```
---
## Implementation Checklist
- [ ] Add 6 new tools to `project_tools.py`
- [ ] Create manifest read/write helper functions
- [ ] Add encryption for Figma tokens in `project_integrations` table
- [ ] Add to `PROJECT_TOOLS` list
- [ ] Register in `handler.py` tool registry
- [ ] Add audit logging to `mcp_tool_usage` table
- [ ] Update integration_health on success/failure
- [ ] Add circuit breaker for Figma API calls
- [ ] Add input validation for file keys and tokens
- [ ] Test with MCP client
---
## Architecture Benefits
**No REST endpoints** - All work via MCP tools
**User-level credentials** - Figma tokens stored per-user in database
**Manifest-driven** - figma.json declares project dependencies
**Versionable** - Manifests can be checked into git
**Discoverable** - Claude can list available Figma files
**Audit trail** - All operations logged in mcp_tool_usage
**Circuit breaker** - Protected against cascading API failures
**Encrypted storage** - Credentials encrypted with Fernet
This is the true **MCP-first architecture** for DSS! 🚀

View File

@@ -0,0 +1,854 @@
# DSS Coding Standards Migration Guide
This guide shows how to migrate existing code to DSS coding standards defined in `.knowledge/dss-coding-standards.json`.
## Table of Contents
- [Shadow DOM Migration](#shadow-dom-migration)
- [Inline Event Handler Removal](#inline-event-handler-removal)
- [Inline Style Extraction](#inline-style-extraction)
- [Semantic HTML](#semantic-html)
- [Accessibility Improvements](#accessibility-improvements)
- [Logger Migration](#logger-migration)
- [State Management](#state-management)
---
## Shadow DOM Migration
### ❌ Before (No Shadow DOM)
```javascript
export default class MyComponent extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<div class="container">
<h2>Title</h2>
<p>Content</p>
</div>
`;
}
}
```
### ✅ After (With Shadow DOM)
```javascript
export default class MyComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' }); // ✓ Enable Shadow DOM
}
connectedCallback() {
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
.container {
padding: 16px;
background: var(--vscode-sidebar-background);
}
h2 {
color: var(--vscode-foreground);
font-size: 16px;
}
</style>
<div class="container">
<h2>Title</h2>
<p>Content</p>
</div>
`;
}
}
```
**Key Changes:**
- ✓ Add `attachShadow()` in constructor
- ✓ Change `this.innerHTML` to `this.shadowRoot.innerHTML`
- ✓ Extract styles to `<style>` block in Shadow DOM
- ✓ Use VSCode theme CSS variables
---
## Inline Event Handler Removal
### ❌ Before (Inline Events - FORBIDDEN)
```javascript
render() {
this.innerHTML = `
<div
class="card"
onclick="this.getRootNode().host.handleClick()"
onmouseover="this.style.transform='scale(1.02)'"
onmouseout="this.style.transform='scale(1)'">
<button onclick="alert('clicked')">Click me</button>
</div>
`;
}
```
### ✅ After (Event Delegation + CSS)
```javascript
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
this.setupEventListeners(); // ✓ Setup after render
}
disconnectedCallback() {
// ✓ Cleanup happens automatically with AbortController
if (this.abortController) {
this.abortController.abort();
}
}
render() {
this.shadowRoot.innerHTML = `
<style>
.card {
transition: transform 0.2s;
}
.card:hover {
transform: scale(1.02); /* ✓ CSS hover instead of JS */
}
button {
padding: 8px 16px;
}
</style>
<div class="card" data-action="cardClick">
<button data-action="buttonClick" type="button">Click me</button>
</div>
`;
}
setupEventListeners() {
// ✓ Event delegation with AbortController for cleanup
this.abortController = new AbortController();
this.shadowRoot.addEventListener('click', (e) => {
const action = e.target.closest('[data-action]')?.dataset.action;
if (action === 'cardClick') {
this.handleCardClick(e);
} else if (action === 'buttonClick') {
this.handleButtonClick(e);
}
}, { signal: this.abortController.signal });
}
handleCardClick(e) {
console.log('Card clicked');
}
handleButtonClick(e) {
e.stopPropagation();
this.dispatchEvent(new CustomEvent('button-clicked', {
bubbles: true,
composed: true
}));
}
```
**Key Changes:**
- ✓ Remove ALL `onclick`, `onmouseover`, `onmouseout` attributes
- ✓ Use CSS `:hover` for hover effects
- ✓ Event delegation with `data-action` attributes
- ✓ Single event listener using `closest('[data-action]')`
- ✓ AbortController for automatic cleanup
- ✓ Custom events for component communication
---
## Inline Style Extraction
### ❌ Before (Inline Styles Everywhere)
```javascript
render() {
this.innerHTML = `
<div style="background: #1e1e1e; padding: 24px; border-radius: 4px;">
<h2 style="color: #ffffff; font-size: 18px; margin-bottom: 12px;">
${this.title}
</h2>
<button style="padding: 8px 16px; background: #0e639c; color: white; border: none; border-radius: 2px; cursor: pointer;">
Action
</button>
</div>
`;
}
```
### ✅ After (Styles in Shadow DOM)
```javascript
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
.container {
background: var(--vscode-sidebar-background);
padding: 24px;
border-radius: 4px;
}
h2 {
color: var(--vscode-foreground);
font-size: 18px;
margin: 0 0 12px 0;
}
button {
padding: 8px 16px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 2px;
cursor: pointer;
transition: background-color 0.1s;
}
button:hover {
background: var(--vscode-button-hoverBackground);
}
button:focus-visible {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
</style>
<div class="container">
<h2>${this.title}</h2>
<button type="button">Action</button>
</div>
`;
}
```
**Key Changes:**
- ✓ ALL styles moved to `<style>` block
- ✓ Use VSCode theme CSS variables
- ✓ Add hover and focus states in CSS
- ✓ Exception: Dynamic values like `transform: translateX(${x}px)` allowed
---
## Semantic HTML
### ❌ Before (Divs as Buttons)
```javascript
render() {
this.innerHTML = `
<div class="tool-item" onclick="this.selectTool()">
<div class="tool-name">Settings</div>
<div class="tool-desc">Configure options</div>
</div>
<div class="close" onclick="this.close()">×</div>
`;
}
```
### ✅ After (Semantic Elements)
```javascript
render() {
this.shadowRoot.innerHTML = `
<style>
button {
appearance: none;
background: transparent;
border: 1px solid transparent;
padding: 8px;
width: 100%;
text-align: left;
cursor: pointer;
border-radius: 4px;
}
button:hover {
background: var(--vscode-list-hoverBackground);
}
button:focus-visible {
outline: 2px solid var(--vscode-focusBorder);
}
.tool-name {
font-size: 13px;
font-weight: 500;
}
.tool-desc {
font-size: 11px;
color: var(--vscode-descriptionForeground);
}
.close-btn {
padding: 4px 8px;
font-size: 20px;
}
</style>
<button type="button" data-action="selectTool">
<div class="tool-name">Settings</div>
<div class="tool-desc">Configure options</div>
</button>
<button
type="button"
class="close-btn"
data-action="close"
aria-label="Close dialog">
×
</button>
`;
}
```
**Key Changes:**
- ✓ Use `<button type="button">` for interactive elements
- ✓ Add `aria-label` for icon-only buttons
- ✓ Keyboard accessible by default
- ✓ Proper focus management
---
## Accessibility Improvements
### ❌ Before (Poor A11y)
```javascript
render() {
this.innerHTML = `
<div class="modal">
<div class="close" onclick="this.close()">×</div>
<div class="content">${this.content}</div>
</div>
`;
}
```
### ✅ After (WCAG 2.1 AA Compliant)
```javascript
render() {
this.shadowRoot.innerHTML = `
<style>
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--vscode-sidebar-background);
border: 1px solid var(--vscode-widget-border);
border-radius: 4px;
padding: 24px;
max-width: 600px;
}
.close-btn {
position: absolute;
top: 8px;
right: 8px;
background: transparent;
border: none;
font-size: 20px;
cursor: pointer;
padding: 4px 8px;
}
.close-btn:focus-visible {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
</style>
<div
class="modal"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title">
<button
class="close-btn"
type="button"
data-action="close"
aria-label="Close dialog">
×
</button>
<h2 id="modal-title">${this.title}</h2>
<div class="content">${this.content}</div>
</div>
`;
}
connectedCallback() {
this.render();
this.setupEventListeners();
this.trapFocus(); // ✓ Keep focus inside modal
this.previousFocus = document.activeElement; // ✓ Store for restoration
}
disconnectedCallback() {
if (this.previousFocus) {
this.previousFocus.focus(); // ✓ Restore focus on close
}
if (this.abortController) {
this.abortController.abort();
}
}
trapFocus() {
const focusable = this.shadowRoot.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusable[0];
const lastFocusable = focusable[focusable.length - 1];
this.shadowRoot.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
} else if (e.key === 'Escape') {
this.close();
}
});
firstFocusable.focus(); // ✓ Focus first element
}
```
**Key Changes:**
- ✓ Add ARIA attributes (`role`, `aria-modal`, `aria-labelledby`)
- ✓ Semantic `<button>` with `aria-label`
- ✓ Focus trapping for modals
- ✓ Keyboard support (Tab, Shift+Tab, Escape)
- ✓ Focus restoration on close
-`:focus-visible` styling
---
## Logger Migration
### ❌ Before (console.log Everywhere)
```javascript
async loadData() {
console.log('Loading data...');
try {
const response = await fetch('/api/data');
const data = await response.json();
console.log('Data loaded:', data);
this.data = data;
} catch (error) {
console.error('Failed to load data:', error);
}
}
processData() {
console.log('Processing...');
console.warn('This might take a while');
// processing logic
console.log('Done processing');
}
```
### ✅ After (Centralized Logger)
```javascript
import { logger } from '../utils/logger.js';
async loadData() {
const endTimer = logger.time('[MyComponent] Data load'); // ✓ Performance timing
logger.debug('[MyComponent] Starting data load'); // ✓ debug() only in dev
try {
const response = await fetch('/api/data');
const data = await response.json();
logger.info('[MyComponent] Data loaded successfully', {
itemCount: data.length
}); // ✓ Structured data
this.data = data;
endTimer(); // Logs elapsed time
} catch (error) {
logger.error('[MyComponent] Failed to load data', error); // ✓ Proper error logging
throw error;
}
}
processData() {
logger.info('[MyComponent] Starting data processing');
logger.warn('[MyComponent] Heavy processing operation'); // ✓ Use warn for concerns
// processing logic
logger.info('[MyComponent] Processing completed');
}
```
**Key Changes:**
- ✓ Import logger utility
- ✓ Use `logger.debug()` for development-only logs
- ✓ Use `logger.info()` for informational messages
- ✓ Use `logger.warn()` for warnings
- ✓ Use `logger.error()` for errors
- ✓ Add component name prefix `[ComponentName]`
- ✓ Use `logger.time()` for performance measurements
**Logger API:**
```javascript
// Enable debug logs: localStorage.setItem('dss_debug', 'true')
// Or in console: window.dssLogger.enableDebug()
logger.debug('Debug message'); // Only in dev or when debug enabled
logger.info('Info message'); // Always shown
logger.warn('Warning'); // Warning level
logger.error('Error', err); // Error level
const endTimer = logger.time('Operation label');
// ... do work ...
endTimer(); // Logs: [TIME] Operation label: 234ms
```
---
## State Management
### ❌ Before (Direct DOM Manipulation)
```javascript
export default class Counter extends HTMLElement {
connectedCallback() {
this.count = 0;
this.innerHTML = `
<div>Count: <span id="count">0</span></div>
<button onclick="this.getRootNode().host.increment()">+</button>
`;
}
increment() {
this.count++;
document.getElementById('count').textContent = this.count; // ✗ Direct DOM
}
}
```
### ✅ After (Reactive State Updates)
```javascript
import contextStore from '../stores/context-store.js';
export default class Counter extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.state = {
count: 0
};
}
connectedCallback() {
this.render();
this.setupEventListeners();
// ✓ Subscribe to global state changes
this.unsubscribe = contextStore.subscribeToKey('someValue', (newValue) => {
this.setState({ externalValue: newValue });
});
}
disconnectedCallback() {
if (this.unsubscribe) {
this.unsubscribe(); // ✓ Cleanup subscription
}
if (this.abortController) {
this.abortController.abort();
}
}
setState(updates) {
// ✓ Immutable state update
this.state = { ...this.state, ...updates };
this.render(); // ✓ Re-render on state change
}
render() {
this.shadowRoot.innerHTML = `
<style>
.container {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
}
button {
padding: 8px 16px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 2px;
cursor: pointer;
}
</style>
<div class="container">
<div>Count: <span>${this.state.count}</span></div>
<button type="button" data-action="increment">+</button>
<button type="button" data-action="decrement">-</button>
</div>
`;
}
setupEventListeners() {
this.abortController = new AbortController();
this.shadowRoot.addEventListener('click', (e) => {
const action = e.target.closest('[data-action]')?.dataset.action;
if (action === 'increment') {
this.setState({ count: this.state.count + 1 }); // ✓ State update triggers render
} else if (action === 'decrement') {
this.setState({ count: this.state.count - 1 });
}
}, { signal: this.abortController.signal });
}
}
customElements.define('ds-counter', Counter);
```
**Key Changes:**
- ✓ State in `this.state` object
-`setState()` method for immutable updates
- ✓ State changes trigger `render()`
- ✓ No direct DOM manipulation
- ✓ Subscribe to global state via contextStore
- ✓ Cleanup subscriptions in `disconnectedCallback`
---
## Complete Example: Full Migration
### ❌ Before (All Anti-Patterns)
```javascript
export default class OldComponent extends HTMLElement {
connectedCallback() {
this.data = [];
this.render();
}
async loadData() {
console.log('Loading...');
const response = await fetch('/api/data');
this.data = await response.json();
this.render();
}
render() {
this.innerHTML = `
<div style="padding: 24px; background: #1e1e1e;">
<h2 style="color: white; font-size: 18px;">Title</h2>
<div class="item" onclick="alert('clicked')" onmouseover="this.style.background='#333'" onmouseout="this.style.background=''">
Click me
</div>
<div onclick="this.getRootNode().host.loadData()" style="cursor: pointer; padding: 8px;">Load Data</div>
</div>
`;
}
}
customElements.define('old-component', OldComponent);
```
### ✅ After (DSS Standards Compliant)
```javascript
import { logger } from '../utils/logger.js';
import contextStore from '../stores/context-store.js';
export default class NewComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' }); // ✓ Shadow DOM
this.state = {
data: [],
isLoading: false,
error: null
};
}
connectedCallback() {
this.render();
this.setupEventListeners();
this.loadData(); // Initial load
// ✓ Global state subscription
this.unsubscribe = contextStore.subscribeToKey('theme', (newTheme) => {
logger.debug('[NewComponent] Theme changed', { theme: newTheme });
});
}
disconnectedCallback() {
// ✓ Cleanup
if (this.unsubscribe) this.unsubscribe();
if (this.abortController) this.abortController.abort();
}
setState(updates) {
this.state = { ...this.state, ...updates };
this.render();
}
async loadData() {
const endTimer = logger.time('[NewComponent] Data load');
this.setState({ isLoading: true, error: null });
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
logger.info('[NewComponent] Data loaded', { count: data.length });
this.setState({ data, isLoading: false });
endTimer();
} catch (error) {
logger.error('[NewComponent] Failed to load data', error);
this.setState({
error: error.message,
isLoading: false
});
}
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
.container {
padding: 24px;
background: var(--vscode-sidebar-background);
}
h2 {
color: var(--vscode-foreground);
font-size: 18px;
margin: 0 0 16px 0;
}
.item {
padding: 12px;
background: var(--vscode-list-inactiveSelectionBackground);
border-radius: 4px;
margin-bottom: 8px;
cursor: pointer;
transition: background-color 0.1s;
}
.item:hover {
background: var(--vscode-list-hoverBackground);
}
.item:focus-visible {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
button {
padding: 8px 16px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 2px;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
<div class="container">
<h2>Title</h2>
${this.state.data.map((item) => `
<div class="item" tabindex="0" data-action="itemClick" data-item-id="${item.id}">
${item.name}
</div>
`).join('')}
<button
type="button"
data-action="loadData"
?disabled="${this.state.isLoading}">
${this.state.isLoading ? 'Loading...' : 'Load Data'}
</button>
${this.state.error ? `
<div role="alert" style="color: var(--vscode-errorForeground); margin-top: 8px;">
Error: ${this.state.error}
</div>
` : ''}
</div>
`;
}
setupEventListeners() {
this.abortController = new AbortController();
// ✓ Event delegation
this.shadowRoot.addEventListener('click', (e) => {
const action = e.target.closest('[data-action]')?.dataset.action;
if (action === 'itemClick') {
const itemId = e.target.dataset.itemId;
this.handleItemClick(itemId);
} else if (action === 'loadData') {
this.loadData();
}
}, { signal: this.abortController.signal });
// ✓ Keyboard support
this.shadowRoot.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && e.target.hasAttribute('data-action')) {
e.target.click();
}
}, { signal: this.abortController.signal });
}
handleItemClick(itemId) {
logger.debug('[NewComponent] Item clicked', { itemId });
this.dispatchEvent(new CustomEvent('item-selected', {
detail: { itemId },
bubbles: true,
composed: true // ✓ Bubble out of Shadow DOM
}));
}
}
customElements.define('ds-new-component', NewComponent);
```
**All Improvements Applied:**
- ✓ Shadow DOM with encapsulated styles
- ✓ No inline event handlers
- ✓ No inline styles (all in `<style>` block)
- ✓ Semantic `<button>` elements with `type="button"`
- ✓ Event delegation pattern
- ✓ Proper state management with `setState()`
- ✓ Logger utility instead of console.log
- ✓ Accessibility (keyboard support, focus management, ARIA)
- ✓ Error handling and loading states
- ✓ AbortController for cleanup
- ✓ Custom events for component communication
---
## Testing Your Migration
After migrating, verify compliance:
```bash
# Run quality checks
./scripts/verify-quality.sh
# Expected results:
# ✓ No inline event handlers (0)
# ✓ Inline styles ≤10
# ✓ Console statements ≤10
# ✓ All syntax valid
```
## Reference Implementations
Study these files for best practices:
- `admin-ui/js/workdesks/base-workdesk.js`
- `admin-ui/js/components/metrics/ds-frontpage.js`
- `admin-ui/js/components/metrics/ds-metric-card.js`
## Standards Documentation
Full standards: `.knowledge/dss-coding-standards.json`
Need help? Check the coding standards JSON for detailed rules, patterns, and enforcement mechanisms.

View File

@@ -0,0 +1,580 @@
# DSS Export/Import - Production Readiness Guide
## Overview
Based on expert validation from Gemini 3 Pro, this document details the production hardening that has been implemented to address critical operational concerns before wider rollout.
**Current Status**: ✅ **PRODUCTION-READY WITH HARDENING**
All critical security and reliability issues identified in expert review have been addressed and documented.
---
## Security Hardening
### 1. Zip Slip Vulnerability (Path Traversal) ✅
**Issue**: Malicious archives can contain paths like `../../etc/passwd` that extract outside intended directory.
**Solution Implemented**:
- Created `ZipSlipValidator` class in `security.py`
- Validates all archive member paths before processing
- Rejects absolute paths and traversal attempts (`..`)
- Blocks hidden files
- Integrated into `ArchiveValidator.validate_archive_structure()`
**Code Location**: `dss/export_import/security.py:ZipSlipValidator`
**Implementation**:
```python
# Automatic validation on archive open
safe, unsafe_paths = ZipSlipValidator.validate_archive_members(archive.namelist())
if not safe:
raise ImportValidationError(f"Unsafe paths detected: {unsafe_paths}")
```
**Testing**: Archive validation will reject any malicious paths before processing begins.
---
### 2. Manifest Integrity Verification ✅
**Issue**: Archives can be tampered with after creation.
**Solution Implemented**:
- Added `ArchiveIntegrity` class with SHA256 hash verification
- Optional `exportHash` field in manifest
- Detects if manifest has been modified
- Integrated into `ArchiveValidator.validate_manifest()`
**Code Location**: `dss/export_import/security.py:ArchiveIntegrity`
**Implementation**:
```python
# Verify manifest hasn't been tampered with
is_valid, error = ArchiveIntegrity.verify_manifest_integrity(manifest)
if not is_valid:
raise ImportValidationError("Manifest integrity check failed")
```
---
## Resource Management
### 1. Memory Limits ✅
**Issue**: Large archives (10k+ tokens, >100MB JSON) can cause OutOfMemory errors.
**Solution Implemented**:
- Created `MemoryLimitManager` class with configurable limits:
- `DEFAULT_MAX_FILE_SIZE = 100MB`
- `DEFAULT_MAX_TOKENS = 10,000`
- `DEFAULT_MAX_COMPONENTS = 1,000`
- File size checks before loading
- Token count validation during parsing
- Warnings for near-limit conditions
**Code Location**: `dss/export_import/security.py:MemoryLimitManager`
**Configuration**:
```python
# Customize limits as needed
memory_mgr = MemoryLimitManager(
max_file_size=50_000_000, # 50MB
max_tokens=5000, # 5k tokens
max_components=500 # 500 components
)
```
**Integration**: Automatically enforced in `DSSArchiveImporter.analyze()`.
### 2. Streaming JSON Parser ✅
**Issue**: Using `json.load()` loads entire file into memory, causing memory spikes.
**Solution Implemented**:
- Created `StreamingJsonLoader` for memory-efficient parsing
- `load_tokens_streaming()` method validates while loading
- Provides memory footprint estimation
- Graceful degradation if ijson not available
**Code Location**: `dss/export_import/security.py:StreamingJsonLoader`
**Usage**:
```python
# Automatic in importer for tokens.json
parsed, error = StreamingJsonLoader.load_tokens_streaming(
json_content,
max_tokens=10000
)
```
---
## Database Locking Strategy
### 1. SQLite Busy Timeout ✅
**Issue**: SQLite locks entire database file during writes, blocking other operations.
**Solution Implemented**:
- Created `DatabaseLockingStrategy` class
- Configurable `busy_timeout_ms` (default: 5 seconds)
- Recommended SQLite pragmas for concurrent access:
```sql
PRAGMA journal_mode = WAL -- Write-Ahead Logging
PRAGMA busy_timeout = 5000 -- Wait up to 5s for locks
PRAGMA synchronous = NORMAL -- Balance safety vs performance
PRAGMA temp_store = MEMORY -- Use memory for temp tables
```
**Code Location**: `dss/export_import/security.py:DatabaseLockingStrategy`
**Configuration**:
```python
service = DSSProjectService(busy_timeout_ms=10000) # 10 second timeout
```
### 2. Transaction Safety ✅
**Issue**: Large imports can fail mid-operation, leaving database in inconsistent state.
**Solution Implemented**:
- Created `DSSProjectService` with transactional wrapper
- All modifications wrapped in explicit transactions
- Automatic rollback on error
- Comprehensive error handling
**Code Location**: `dss/export_import/service.py:DSSProjectService._transaction()`
**Usage**:
```python
# Automatic transaction management
with service._transaction() as conn:
# All operations automatically committed on success
# Rolled back on exception
project = importer.import_replace()
```
---
## Conflict Resolution with Clock Skew Detection
### 1. Safer Timestamp-Based Resolution ✅
**Issue**: Using wall-clock timestamps for "Last Write Wins" can lose data if clocks are skewed.
**Solution Implemented**:
- Created `TimestampConflictResolver` with drift detection
- Clock skew tolerance: 5 seconds (configurable)
- Drift warning threshold: 1 hour (configurable)
- Safe recommendation method: returns `'local'|'imported'|'unknown'`
- Integrated into `ConflictItem.get_safe_recommendation()`
**Code Location**: `dss/export_import/security.py:TimestampConflictResolver`
**Usage**:
```python
# Get safe recommendation with drift detection
for conflict in merge_analysis.conflicted_items:
winner, warning = conflict.get_safe_recommendation()
if warning:
log.warning(f"Clock skew detected: {warning}")
# Use winner to decide resolution
```
### 2. Future: Logical Timestamps (Lamport) ✅
**Note**: Implemented `compute_logical_version()` method for future use.
**Recommendation**: For future versions, migrate to logical timestamps instead of wall-clock:
```python
# Future enhancement
version = logical_clock.increment() # Instead of datetime.utcnow()
# Eliminates clock skew issues entirely
```
---
## Large Operation Handling
### 1. Background Job Scheduling Detection ✅
**Issue**: Large imports can exceed HTTP request timeouts (typically 30-60s).
**Solution Implemented**:
- `DatabaseLockingStrategy.should_schedule_background()` method
- Estimates operation duration based on item count
- Recommends background job if estimated time > 80% of timeout
- Service layer ready for Celery/RQ integration
**Code Location**: `dss/export_import/security.py:DatabaseLockingStrategy`
**Usage**:
```python
# Service automatically detects if background job needed
result = service.export_project(project, path)
if result.requires_background_job:
job_id = schedule_with_celery(...)
return job_id # Return job ID to client
```
**Integration Points** (for implementing team):
```python
# In your API layer
from celery import shared_task
from dss.export_import.service import DSSProjectService
@shared_task(bind=True)
def import_project_task(self, archive_path, strategy='replace'):
service = DSSProjectService()
result = service.import_project(archive_path, strategy)
return {
'success': result.success,
'project_name': result.project_name,
'error': result.error,
}
# In route handler
result = service.import_project(path, background=True)
if result.requires_background_job:
task = import_project_task.delay(path)
return {'job_id': task.id}
```
---
## Service Layer Architecture
### DSSProjectService
High-level facade for all export/import operations with production guarantees.
**Location**: `dss/export_import/service.py`
**Key Features**:
- ✅ Transactional wrapper with automatic rollback
- ✅ SQLite locking configuration
- ✅ Memory limit enforcement
- ✅ Background job scheduling detection
- ✅ Comprehensive error handling
- ✅ Operation timing and summaries
**Methods**:
```python
service = DSSProjectService(busy_timeout_ms=5000)
# Export
result = service.export_project(project, output_path)
# Returns: ExportSummary(success, archive_path, file_size, item_counts, error, duration)
# Import
result = service.import_project(archive_path, strategy='replace')
# Returns: ImportSummary(success, project_name, item_counts, error, migration_performed, duration, requires_background_job)
# Analyze (safe preview)
analysis = service.analyze_import(archive_path)
# Returns: ImportAnalysis (no modifications)
# Merge
result = service.merge_project(local_project, archive_path, conflict_strategy='keep_local')
# Returns: MergeSummary(success, new_items_count, updated_items_count, conflicts_count, resolution_strategy, duration)
# Merge Analysis (safe preview)
analysis = service.analyze_merge(local_project, archive_path)
# Returns: MergeAnalysis (no modifications)
```
---
## Production Deployment Checklist
### Pre-Deployment
- [ ] Review all security hardening implementations
- [ ] Configure memory limits appropriate for your infrastructure
- [ ] Set SQLite `busy_timeout_ms` based on expected load
- [ ] Test with realistic project sizes (your largest projects)
- [ ] Implement background job handler (Celery/RQ) for large imports
- [ ] Set up monitoring for memory usage during imports
- [ ] Configure database backup before large operations
### Integration
- [ ] Wrap API endpoints with `DSSProjectService`
- [ ] Implement Celery/RQ worker for background imports
- [ ] Add operation result webhooks/notifications
- [ ] Implement progress tracking for large operations
- [ ] Set up error alerting for failed imports
### Monitoring
- [ ] Track export/import duration metrics
- [ ] Monitor memory usage during operations
- [ ] Alert on validation failures
- [ ] Log all merge conflicts
- [ ] Track background job success rate
### Documentation
- [ ] Document supported archive versions
- [ ] Provide user guide for export/import workflows
- [ ] Document clock skew warnings and handling
- [ ] Create troubleshooting guide
- [ ] Document background job status checking
---
## Configuration Examples
### Conservative (Small Projects, High Reliability)
```python
service = DSSProjectService(
busy_timeout_ms=10000 # 10s timeout
)
memory_mgr = MemoryLimitManager(
max_file_size=50 * 1024 * 1024, # 50MB
max_tokens=5000,
max_components=500
)
```
### Balanced (Medium Projects)
```python
service = DSSProjectService(
busy_timeout_ms=5000 # 5s timeout (default)
)
# Uses default memory limits
```
### Aggressive (Large Projects, Background Jobs)
```python
service = DSSProjectService(
busy_timeout_ms=30000 # 30s timeout
)
memory_mgr = MemoryLimitManager(
max_file_size=500 * 1024 * 1024, # 500MB
max_tokens=50000,
max_components=5000
)
# Set background=True for large imports
result = service.import_project(archive_path, background=True)
```
---
## Operational Runbooks
### Handling Import Failures
```python
from dss.export_import.service import DSSProjectService
service = DSSProjectService()
result = service.import_project(archive_path)
if not result.success:
# Check analysis for details
analysis = service.analyze_import(archive_path)
if not analysis.is_valid:
for error in analysis.errors:
print(f"[{error.stage}] {error.message}")
# Stages: archive, manifest, schema, structure, referential
# If Zip Slip or integrity detected
if any("Zip Slip" in e.message for e in analysis.errors):
# Archive is malicious - reject and alert security
pass
# If schema version too new
if any("schema version" in e.message for e in analysis.errors):
# Update DSS and retry
pass
```
### Handling Merge Conflicts
```python
analysis = service.analyze_merge(local_project, archive_path)
if analysis.has_conflicts:
for conflict in analysis.conflicted_items:
winner, warning = conflict.get_safe_recommendation()
if warning:
# Log clock skew warning
log.warning(f"Clock skew detected: {warning}")
print(f"Conflict in {conflict.entity_name}:")
print(f" Recommendation: {winner}")
print(f" Local: {conflict.local_hash} (updated {conflict.local_updated_at})")
print(f" Imported: {conflict.imported_hash} (updated {conflict.imported_updated_at})")
# Apply merge with safe strategy
result = service.merge_project(local_project, archive_path, 'keep_local')
```
### Background Job Integration
```python
# In task handler
from dss.export_import.service import DSSProjectService
def handle_import_job(archive_path, strategy):
service = DSSProjectService()
result = service.import_project(archive_path, strategy)
# Store result for polling
store_job_result(job_id, {
'success': result.success,
'project_name': result.project_name,
'item_counts': result.item_counts,
'error': result.error,
'duration_seconds': result.duration_seconds,
})
# Send webhook notification
notify_user(job_id, result)
```
---
## Known Limitations & Future Work
### Current Limitations
1. **Wall-Clock Timestamps**: Still using `datetime.utcnow()` for conflict resolution
- Mitigation: Clock skew tolerance and warnings in place
- Future: Migrate to Lamport timestamps
2. **Memory Loading**: JSON files loaded into memory
- Mitigation: Memory limits and warnings
- Future: Implement full streaming JSON parser with ijson
3. **No Selective Export**: Always exports everything
- Mitigation: Merge strategy allows selective import
- Future: Add filtering by tags/folders
### Future Enhancements
1. **Logical Timestamps** (Lamport Clocks)
- Eliminates clock skew issues entirely
- Add version field to all entities
- Migration: Auto-initialize version from timestamps
2. **Full Streaming JSON Parser**
- Use ijson for large files
- Process items one-at-a-time
- Constant memory footprint
3. **Selective Export**
- Filter by tags, folders, categories
- Create partial archives
- Enables incremental updates
4. **Dry-Run/Diff View**
- Show exact changes before commit
- Visual diff of token values
- Component structure changes
5. **Asset Bundling**
- Include fonts, images in archives
- Asset deduplication
- CDN-friendly packaging
6. **Audit Trail Export**
- Include change history
- Sync event log
- Activity timeline
7. **Cloud Storage Integration**
- Native S3/GCS upload
- Signed URLs for sharing
- Automatic backups
8. **Encryption Support**
- Encrypt sensitive projects
- Key management
- User-provided keys
---
## Performance Benchmarks
Expected performance on standard hardware:
| Operation | Item Count | Duration | Memory Usage |
|-----------|-----------|----------|--------------|
| Export | 1,000 tokens | 1-2s | 50MB |
| Export | 10,000 tokens | 5-10s | 200MB |
| Import | 1,000 tokens | 2-3s | 75MB |
| Import | 10,000 tokens | 8-15s | 250MB |
| Merge | 5,000 local + 3,000 imported | 3-5s | 150MB |
| Analysis (preview) | 10,000 tokens | 1-2s | 200MB |
**Note**: Background jobs recommended for operations >5 seconds or >200MB memory.
---
## Support & Troubleshooting
### Troubleshooting Guide
**"Zip Slip vulnerability detected"**
→ Archive contains malicious paths. Reject it and alert security team.
**"Manifest integrity check failed"**
→ Archive has been tampered with. Reject and verify source.
**"File size exceeds limit"**
→ Increase `MemoryLimitManager.max_file_size` or split archive.
**"Token count exceeds limit"**
→ Archive has too many tokens. Use selective export or increase limits.
**"Clock skew detected"**
→ System clocks are >1 hour apart. Sync clocks and retry.
**"Database locked"**
→ Increase `busy_timeout_ms` or schedule import during low-traffic windows.
**"Background job required"**
→ Operation too large for synchronous call. Implement Celery/RQ handler.
---
## Security Policy
### Data Integrity
- ✅ Archive validation before any import
- ✅ Manifest integrity verification
- ✅ Referential integrity checks
- ✅ Zip Slip vulnerability protection
- ✅ Transaction safety with automatic rollback
### Confidentiality
- ⚠️ Archives are unencrypted (planned enhancement)
- Recommendation: Store/transmit over HTTPS
- Future: Add encryption support
### Access Control
- Service layer ready for auth integration
- Recommend: Wrap with permission checks
- Audit: Log all import/export operations
---
**Production Status**: ✅ **READY FOR DEPLOYMENT**
All identified security and reliability concerns have been addressed with hardening implementations, configuration options, and documented operational procedures.
For questions about production deployment, refer to the implementation files and inline code documentation.
---
*Generated: December 2025*
*DSS Export/Import System v1.0.1 (Hardened)*