Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4bc66ab014 | |||
| 971046d568 | |||
| 602350d2e6 | |||
| f20d48fea2 | |||
| c53dc010de | |||
| 03e778a06f | |||
| fa73708d97 | |||
| ee8204532d | |||
| a347403a26 | |||
| 8becfd87bb | |||
| 1b78597649 | |||
| d93b6493c1 | |||
| d581eb3e88 | |||
| 4728ef151c | |||
| eb5f54a6d0 | |||
| c502f1c0bd | |||
| 8f9ac1f9c0 | |||
| 0787989496 | |||
| 9bc25a3dc2 | |||
| 3cc3fd18f4 | |||
| 997fa4af03 | |||
| b864e82318 | |||
| 2f22be2bd7 | |||
| fcfb850168 | |||
| 4a224c1f92 | |||
| d1d17e1288 | |||
| 9055661b51 | |||
| d25a32cdd3 | |||
| 8d65404514 | |||
| 1b6c32d773 | |||
| 352bcfca29 | |||
| 448c678ecc | |||
| e980b96091 | |||
| fabe0fcd54 | |||
| ef27256879 | |||
| c1da41474f | |||
| 815dcd5c80 | |||
| 29936417fc | |||
| 49e32f7d96 | |||
| cd9c242983 | |||
| 3835557e67 | |||
| 69bffab745 | |||
| ef412a3887 | |||
| 2e2b4ca455 | |||
| fb5fe1235c | |||
| e21cdbe6a8 | |||
| 94f33f0a56 | |||
| 5d5e22c8a1 | |||
| b3dbcc2376 |
@@ -1,8 +1,9 @@
|
|||||||
<img src="logoExcaliDash.png" alt="ExcaliDash Logo" width="80" height="88">
|
<img src="logoExcaliDash.png" alt="ExcaliDash Logo" width="80" height="88">
|
||||||
|
|
||||||
# ExcaliDash v0.1.0
|
# ExcaliDash v0.1.6
|
||||||
|
|
||||||
[](LICENSE)
|

|
||||||
|

|
||||||
[](https://hub.docker.com)
|
[](https://hub.docker.com)
|
||||||
|
|
||||||
A self-hosted dashboard and organizer for [Excalidraw](https://github.com/excalidraw/excalidraw) with live collaboration features.
|
A self-hosted dashboard and organizer for [Excalidraw](https://github.com/excalidraw/excalidraw) with live collaboration features.
|
||||||
@@ -74,7 +75,10 @@ See [release notes](https://github.com/ZimengXiong/ExcaliDash/releases) for a sp
|
|||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> NOT for production use. This is just a side project (and also the first release), and it likely contains some bugs. DO NOT open ports to the internet (e.g. CORS is set to allow all)
|
> NOT for production use. While attempts have been made at hardening (XSS/dompurify, CORS, rate-limiting, sanitization), they are inadequate for public deployment. Do not expose any ports. Currently lacking CSRF.
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> ExcaliDash is in BETA. Please backup your data regularly (e.g. with cron).
|
||||||
|
|
||||||
## Docker Hub (Recommended)
|
## Docker Hub (Recommended)
|
||||||
|
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
# ExcaliDash v0.1.5
|
||||||
|
|
||||||
|
Date: 2025-11-23
|
||||||
|
|
||||||
|
Compatibility: v0.1.x (Backward Compatible)
|
||||||
|
|
||||||
|
# Security
|
||||||
|
|
||||||
|
- RCE: implemented strict Zod schema validation and input sanitization on file uploads; added path traversal guards to file handling logic
|
||||||
|
|
||||||
|
- XSS: used DOMPurify for HTML sanitization; blocked execution-capable SVG attributes and enforces CSP headers.
|
||||||
|
|
||||||
|
- DoS: moved CPU-intensive operations to worker threads to prevent event loop blocking; request rate limiting (1,000 req/15 min per IP) and streaming for large files
|
||||||
|
|
||||||
|
# Infras & Deployment
|
||||||
|
|
||||||
|
- non-root execution (uid 1001) in containers
|
||||||
|
- migrated to multi-stage Docker builds
|
||||||
|
|
||||||
|
# Database
|
||||||
|
|
||||||
|
- migrated to better-sqlite3, converted all DB interactions to non-blocking async operations and offloaded integrity checks to worker threads.
|
||||||
|
|
||||||
|
- implemented SQLite magic header validation; added automatic backup triggers preceding data import
|
||||||
|
|
||||||
|
- input validation logic
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
|
||||||
|
- updated Settings UI to show version
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
# Security Fixes Implementation Summary
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document summarizes the comprehensive security fixes implemented to address two critical security vulnerabilities identified in ExcaliDash:
|
|
||||||
|
|
||||||
1. **Stored XSS Vector (High Severity)** - Data sanitization negligence
|
|
||||||
2. **Root Execution Privilege (Critical Severity)** - Container escape risk
|
|
||||||
|
|
||||||
## Security Issues Fixed
|
|
||||||
|
|
||||||
### Issue 1: Stored XSS Vector (High Severity) ✅ FIXED
|
|
||||||
|
|
||||||
**Problem**: Backend used lazy `z.object({}).passthrough()` validation for elements and appState, allowing arbitrary JSON storage without sanitization.
|
|
||||||
|
|
||||||
**Attack Vectors**:
|
|
||||||
|
|
||||||
- Malicious `.excalidraw` files containing `<script>` tags in element properties
|
|
||||||
- `javascript:` URIs in link attributes
|
|
||||||
- SVG previews with embedded malicious code
|
|
||||||
- Compromised clients sending XSS payloads
|
|
||||||
|
|
||||||
**Solution Implemented**:
|
|
||||||
|
|
||||||
- **Strict Zod Schemas**: Replaced `.passthrough()` with detailed validation schemas for elements and appState
|
|
||||||
- **HTML/JS Sanitization**: Implemented comprehensive sanitization layer removing script tags, event handlers, and malicious URLs
|
|
||||||
- **SVG Sanitization**: Special handling for SVG content to prevent script execution
|
|
||||||
- **URL Validation**: Whitelist-only approach for URL schemes (http, https, mailto, relative paths only)
|
|
||||||
- **Input Sanitization**: All string inputs are sanitized before database persistence
|
|
||||||
- **Import Validation**: Additional security checks for imported .excalidraw files with `X-Imported-File` header
|
|
||||||
|
|
||||||
### Issue 2: Root Execution Privilege (Critical Severity) ✅ FIXED
|
|
||||||
|
|
||||||
**Problem**: Container ran Node.js process as root without USER directive, providing immediate root access in case of RCE.
|
|
||||||
|
|
||||||
**Attack Vectors**:
|
|
||||||
|
|
||||||
- RCE vulnerabilities in `better-sqlite3` native bindings
|
|
||||||
- File upload processing vulnerabilities
|
|
||||||
- Import functionality exploits
|
|
||||||
|
|
||||||
**Solution Implemented**:
|
|
||||||
|
|
||||||
- **Non-Root User**: Created dedicated `nodejs` user with UID 1001
|
|
||||||
- **Permission Management**: Proper ownership and permissions for data directories
|
|
||||||
- **Dockerfile Security**: Added USER directive to switch to non-root execution
|
|
||||||
- **Entry Point Security**: Updated docker-entrypoint.sh to handle permissions correctly
|
|
||||||
|
|
||||||
### Additional Security Hardening ✅ IMPLEMENTED
|
|
||||||
|
|
||||||
**Security Headers**:
|
|
||||||
|
|
||||||
- Content Security Policy (CSP) with strict source restrictions
|
|
||||||
- X-Frame-Options: DENY (prevents clickjacking)
|
|
||||||
- X-Content-Type-Options: nosniff
|
|
||||||
- X-XSS-Protection: 1; mode=block
|
|
||||||
- Referrer-Policy: strict-origin-when-cross-origin
|
|
||||||
- Permissions-Policy: geolocation=(), microphone=(), camera=()
|
|
||||||
|
|
||||||
**Rate Limiting**:
|
|
||||||
|
|
||||||
- Implemented basic rate limiting (1000 requests per 15-minute window)
|
|
||||||
- Per-IP tracking to prevent DoS attacks
|
|
||||||
|
|
||||||
**Request Validation**:
|
|
||||||
|
|
||||||
- Maintained existing 50MB request size limits
|
|
||||||
- Enhanced validation for file imports
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
### Backend Changes
|
|
||||||
|
|
||||||
1. **`backend/src/security.ts`** - New security utilities module
|
|
||||||
|
|
||||||
- HTML/JS sanitization functions
|
|
||||||
- SVG sanitization functions
|
|
||||||
- Strict Zod schemas for elements and appState
|
|
||||||
- Drawing data validation and sanitization
|
|
||||||
- URL sanitization with whitelist validation
|
|
||||||
|
|
||||||
2. **`backend/src/index.ts`** - Updated backend security
|
|
||||||
|
|
||||||
- Replaced lazy `.passthrough()` schemas with strict validation
|
|
||||||
- Added security middleware with headers and rate limiting
|
|
||||||
- Enhanced POST /drawings endpoint with import validation
|
|
||||||
- Added malicious content detection and rejection
|
|
||||||
|
|
||||||
3. **`backend/Dockerfile`** - Container security hardening
|
|
||||||
|
|
||||||
- Created non-root `nodejs` user (UID 1001)
|
|
||||||
- Added USER directive for non-root execution
|
|
||||||
- Set proper file ownership and permissions
|
|
||||||
|
|
||||||
4. **`backend/docker-entrypoint.sh`** - Permission management
|
|
||||||
- Added proper directory permission setup
|
|
||||||
- User-aware permission handling
|
|
||||||
- Database file permission management
|
|
||||||
|
|
||||||
### Frontend Changes
|
|
||||||
|
|
||||||
5. **`frontend/src/utils/importUtils.ts`** - Import security marking
|
|
||||||
- Added `X-Imported-File: true` header for imported files
|
|
||||||
- Enables additional backend validation for imported content
|
|
||||||
|
|
||||||
## Security Testing
|
|
||||||
|
|
||||||
### Test Coverage
|
|
||||||
|
|
||||||
**XSS Prevention Tests** (`backend/src/securityTest.ts`):
|
|
||||||
|
|
||||||
- ✅ HTML/JS injection prevention
|
|
||||||
- ✅ SVG malicious content blocking
|
|
||||||
- ✅ URL scheme validation (javascript:, data:, vbscript: blocked)
|
|
||||||
- ✅ Text sanitization with length limits
|
|
||||||
- ✅ Malicious drawing rejection
|
|
||||||
- ✅ Legitimate content preservation
|
|
||||||
|
|
||||||
**Container Security Tests**:
|
|
||||||
|
|
||||||
- ✅ Docker container runs as `uid=1001(nodejs)` instead of root
|
|
||||||
- ✅ Proper file permissions for data directories
|
|
||||||
- ✅ Non-root user execution verified
|
|
||||||
|
|
||||||
### Test Results
|
|
||||||
|
|
||||||
```
|
|
||||||
🧪 Security Test Suite Results:
|
|
||||||
|
|
||||||
✅ HTML/JS injection prevention - WORKING
|
|
||||||
✅ SVG malicious content blocking - WORKING
|
|
||||||
✅ URL scheme validation - WORKING
|
|
||||||
✅ Text sanitization with limits - WORKING
|
|
||||||
✅ Malicious drawing rejection - WORKING
|
|
||||||
✅ Legitimate content preservation - WORKING
|
|
||||||
✅ Container runs as non-root (uid=1001) - WORKING
|
|
||||||
|
|
||||||
🔒 XSS Prevention: IMPLEMENTED & FUNCTIONAL
|
|
||||||
🔒 Container Security: IMPLEMENTED & FUNCTIONAL
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Benefits
|
|
||||||
|
|
||||||
### Before Fixes
|
|
||||||
|
|
||||||
- ❌ Any malicious script in drawing data would be stored and executed
|
|
||||||
- ❌ Container escape possible with immediate root access
|
|
||||||
- ❌ No protection against XSS, CSRF, or clickjacking attacks
|
|
||||||
- ❌ Unrestricted file uploads and imports
|
|
||||||
|
|
||||||
### After Fixes
|
|
||||||
|
|
||||||
- ✅ All drawing data is sanitized before storage
|
|
||||||
- ✅ Malicious content is detected and rejected
|
|
||||||
- ✅ Container runs with minimal privileges (UID 1001)
|
|
||||||
- ✅ Comprehensive security headers protect against common attacks
|
|
||||||
- ✅ Rate limiting prevents DoS attacks
|
|
||||||
- ✅ Strict validation for all imported content
|
|
||||||
|
|
||||||
## Security Impact
|
|
||||||
|
|
||||||
### Risk Reduction
|
|
||||||
|
|
||||||
- **XSS Risk**: High → **Eliminated**
|
|
||||||
- **Container Escape**: Critical → **Mitigated**
|
|
||||||
- **RCE Impact**: High → **Reduced** (non-root execution)
|
|
||||||
- **DoS Risk**: Medium → **Reduced** (rate limiting)
|
|
||||||
|
|
||||||
### Compliance
|
|
||||||
|
|
||||||
- Implements defense-in-depth security principles
|
|
||||||
- Follows secure coding practices
|
|
||||||
- Adheres to container security best practices
|
|
||||||
- Protects against OWASP Top 10 vulnerabilities
|
|
||||||
|
|
||||||
## Maintenance Notes
|
|
||||||
|
|
||||||
### Regular Security Tasks
|
|
||||||
|
|
||||||
1. **Security Test Suite**: Run `npm run security-test` to verify XSS prevention
|
|
||||||
2. **Container Security**: Verify non-root execution in production
|
|
||||||
3. **Dependency Updates**: Keep dependencies updated for security patches
|
|
||||||
4. **Security Audit**: Review and update sanitization rules as needed
|
|
||||||
|
|
||||||
### Monitoring
|
|
||||||
|
|
||||||
- Monitor rate limiting logs for DoS attempts
|
|
||||||
- Track validation failures for potential attack patterns
|
|
||||||
- Review container logs for permission-related issues
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
Both critical security issues have been successfully addressed with comprehensive fixes that:
|
|
||||||
|
|
||||||
1. **Eliminate XSS vulnerabilities** through strict validation and sanitization
|
|
||||||
2. **Reduce container escape risk** through non-root execution
|
|
||||||
3. **Add defense-in-depth** security measures
|
|
||||||
4. **Maintain full functionality** while improving security posture
|
|
||||||
|
|
||||||
The implementation includes thorough testing to ensure security measures work correctly while preserving legitimate functionality.
|
|
||||||
|
|
||||||
**Security Status**: ✅ **RESOLVED**
|
|
||||||
@@ -2,3 +2,4 @@
|
|||||||
PORT=8000
|
PORT=8000
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
DATABASE_URL=file:/app/prisma/dev.db
|
DATABASE_URL=file:/app/prisma/dev.db
|
||||||
|
FRONTEND_URL=http://localhost:6767
|
||||||
+10
-10
@@ -8,7 +8,10 @@ COPY package*.json ./
|
|||||||
COPY tsconfig.json ./
|
COPY tsconfig.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm ci
|
# Install build deps required for compiling native modules like better-sqlite3
|
||||||
|
RUN apk add --no-cache python3 make g++ build-base sqlite-dev && \
|
||||||
|
npm ci
|
||||||
|
ENV PYTHON=/usr/bin/python3
|
||||||
|
|
||||||
# Copy prisma schema
|
# Copy prisma schema
|
||||||
COPY prisma ./prisma/
|
COPY prisma ./prisma/
|
||||||
@@ -25,8 +28,8 @@ RUN npx tsc
|
|||||||
# Production stage
|
# Production stage
|
||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
# Install OpenSSL for Prisma and create non-root user
|
# Install OpenSSL for Prisma and su-exec, create non-root user
|
||||||
RUN apk add --no-cache openssl && \
|
RUN apk add --no-cache openssl su-exec sqlite-libs && \
|
||||||
addgroup -g 1001 -S nodejs && \
|
addgroup -g 1001 -S nodejs && \
|
||||||
adduser -S nodejs -u 1001
|
adduser -S nodejs -u 1001
|
||||||
|
|
||||||
@@ -51,17 +54,14 @@ COPY --from=builder /app/src/generated ./dist/generated
|
|||||||
# Generate Prisma Client in production (updates node_modules)
|
# Generate Prisma Client in production (updates node_modules)
|
||||||
RUN npx prisma generate
|
RUN npx prisma generate
|
||||||
|
|
||||||
# Create necessary directories and set proper ownership
|
# Create necessary directories (ownership will be set in entrypoint)
|
||||||
RUN mkdir -p /app/uploads /app/prisma && \
|
RUN mkdir -p /app/uploads /app/prisma
|
||||||
chown -R nodejs:nodejs /app
|
|
||||||
|
|
||||||
# Copy and set permissions for entrypoint script
|
# Copy and set permissions for entrypoint script
|
||||||
COPY docker-entrypoint.sh ./
|
COPY docker-entrypoint.sh ./
|
||||||
RUN chmod +x docker-entrypoint.sh && \
|
RUN chmod +x docker-entrypoint.sh
|
||||||
chown nodejs:nodejs docker-entrypoint.sh
|
|
||||||
|
|
||||||
# Switch to non-root user
|
# REMOVED: USER nodejs (We must stay root to fix permissions in entrypoint)
|
||||||
USER nodejs
|
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,34 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Auto-hydrate prisma directory when bind-mounted volume is empty
|
# 1. Hydrate volume if empty (Running as root)
|
||||||
if [ ! -f "/app/prisma/schema.prisma" ]; then
|
if [ ! -f "/app/prisma/schema.prisma" ]; then
|
||||||
echo "Mount is empty. Hydrating /app/prisma from /app/prisma_template..."
|
echo "Mount is empty. Hydrating /app/prisma..."
|
||||||
cp -R /app/prisma_template/. /app/prisma/
|
cp -R /app/prisma_template/. /app/prisma/
|
||||||
|
else
|
||||||
|
# Volume exists but may be missing new migrations from an upgrade
|
||||||
|
# Always sync schema and migrations from template to ensure upgrades work
|
||||||
|
echo "Syncing schema and migrations from template..."
|
||||||
|
cp /app/prisma_template/schema.prisma /app/prisma/schema.prisma
|
||||||
|
cp -R /app/prisma_template/migrations/. /app/prisma/migrations/
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ensure proper ownership and permissions for data directories
|
# 2. Fix permissions unconditionally (Running as root)
|
||||||
echo "Setting up data directory permissions..."
|
echo "Fixing filesystem permissions..."
|
||||||
mkdir -p /app/uploads
|
chown -R nodejs:nodejs /app/uploads
|
||||||
mkdir -p /app/prisma
|
chown -R nodejs:nodejs /app/prisma
|
||||||
|
chmod 755 /app/uploads
|
||||||
# Set ownership to the node user (UID 1000)
|
|
||||||
if [ "$(id -u)" = "0" ]; then
|
|
||||||
# If running as root (for some reason), fix ownership
|
|
||||||
chown -R nodejs:nodejs /app/uploads
|
|
||||||
chown -R nodejs:nodejs /app/prisma
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure database file has proper permissions
|
# Ensure database file has proper permissions
|
||||||
if [ -f "/app/prisma/dev.db" ]; then
|
if [ -f "/app/prisma/dev.db" ]; then
|
||||||
chmod 664 /app/prisma/dev.db 2>/dev/null || true
|
echo "Database file found, ensuring write permissions..."
|
||||||
|
chmod 666 /app/prisma/dev.db
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Set appropriate permissions for uploads directory
|
# 3. Run Migrations (Drop privileges to nodejs)
|
||||||
chmod 755 /app/uploads
|
|
||||||
|
|
||||||
# Run migrations as the current user
|
|
||||||
echo "Running database migrations..."
|
echo "Running database migrations..."
|
||||||
npx prisma migrate deploy
|
su-exec nodejs npx prisma migrate deploy
|
||||||
|
|
||||||
# Start the application
|
# 4. Start Application (Drop privileges to nodejs)
|
||||||
echo "Starting application as user $(whoami) (UID: $(id -u))"
|
echo "Starting application as nodejs..."
|
||||||
node dist/index.js
|
exec su-exec nodejs node dist/index.js
|
||||||
|
|||||||
Generated
+1
-8
@@ -22,6 +22,7 @@
|
|||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"jsdom": "^27.2.0",
|
"jsdom": "^27.2.0",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
|
"prisma": "^5.22.0",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
@@ -30,7 +31,6 @@
|
|||||||
"@types/express": "^5.0.5",
|
"@types/express": "^5.0.5",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"nodemon": "^3.1.11",
|
"nodemon": "^3.1.11",
|
||||||
"prisma": "^5.22.0",
|
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
@@ -312,14 +312,12 @@
|
|||||||
"version": "5.22.0",
|
"version": "5.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
|
||||||
"integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==",
|
"integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "5.22.0",
|
"version": "5.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
|
||||||
"integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
|
"integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
|
||||||
"devOptional": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -333,14 +331,12 @@
|
|||||||
"version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
"version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
|
||||||
"integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
|
"integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/fetch-engine": {
|
"node_modules/@prisma/fetch-engine": {
|
||||||
"version": "5.22.0",
|
"version": "5.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
|
||||||
"integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
|
"integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "5.22.0",
|
"@prisma/debug": "5.22.0",
|
||||||
@@ -352,7 +348,6 @@
|
|||||||
"version": "5.22.0",
|
"version": "5.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
|
||||||
"integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
|
"integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "5.22.0"
|
"@prisma/debug": "5.22.0"
|
||||||
@@ -1747,7 +1742,6 @@
|
|||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -2677,7 +2671,6 @@
|
|||||||
"version": "5.22.0",
|
"version": "5.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
|
||||||
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
|
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
|
||||||
"devOptional": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "1.0.0",
|
"version": "0.1.6",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"jsdom": "^27.2.0",
|
"jsdom": "^27.2.0",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
|
"prisma": "^5.22.0",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
@@ -33,7 +34,6 @@
|
|||||||
"@types/express": "^5.0.5",
|
"@types/express": "^5.0.5",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"nodemon": "^3.1.11",
|
"nodemon": "^3.1.11",
|
||||||
"prisma": "^5.22.0",
|
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,7 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Library" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY DEFAULT 'default',
|
||||||
|
"items" TEXT NOT NULL DEFAULT '[]',
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "PrivateVault" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY DEFAULT 'vault',
|
||||||
|
"passwordHash" TEXT NOT NULL,
|
||||||
|
"salt" TEXT NOT NULL,
|
||||||
|
"hint" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA defer_foreign_keys=ON;
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Drawing" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"elements" TEXT NOT NULL,
|
||||||
|
"appState" TEXT NOT NULL,
|
||||||
|
"files" TEXT NOT NULL DEFAULT '{}',
|
||||||
|
"preview" TEXT,
|
||||||
|
"version" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"collectionId" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"isPrivate" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"encryptedData" TEXT,
|
||||||
|
"iv" TEXT,
|
||||||
|
CONSTRAINT "Drawing_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "Collection" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Drawing" ("appState", "collectionId", "createdAt", "elements", "files", "id", "name", "preview", "updatedAt", "version") SELECT "appState", "collectionId", "createdAt", "elements", "files", "id", "name", "preview", "updatedAt", "version" FROM "Drawing";
|
||||||
|
DROP TABLE "Drawing";
|
||||||
|
ALTER TABLE "new_Drawing" RENAME TO "Drawing";
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
PRAGMA defer_foreign_keys=OFF;
|
||||||
Binary file not shown.
@@ -32,4 +32,26 @@ model Drawing {
|
|||||||
collection Collection? @relation(fields: [collectionId], references: [id])
|
collection Collection? @relation(fields: [collectionId], references: [id])
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// Privacy/Encryption fields
|
||||||
|
isPrivate Boolean @default(false)
|
||||||
|
encryptedData String? // Encrypted blob containing elements, appState, files when isPrivate=true
|
||||||
|
iv String? // Initialization vector for AES-GCM decryption
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton model for storing vault password hash and settings
|
||||||
|
model PrivateVault {
|
||||||
|
id String @id @default("vault") // Singleton pattern
|
||||||
|
passwordHash String // bcrypt hash for password verification
|
||||||
|
salt String // Salt for client-side key derivation (hex encoded)
|
||||||
|
hint String? // Optional password hint
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Library {
|
||||||
|
id String @id @default("default") // Singleton pattern - use "default" ID
|
||||||
|
items String @default("[]") // Stored as JSON string array of library items
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -136,6 +136,25 @@ exports.Prisma.DrawingScalarFieldEnum = {
|
|||||||
version: 'version',
|
version: 'version',
|
||||||
collectionId: 'collectionId',
|
collectionId: 'collectionId',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt',
|
||||||
|
isPrivate: 'isPrivate',
|
||||||
|
encryptedData: 'encryptedData',
|
||||||
|
iv: 'iv'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.PrivateVaultScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
passwordHash: 'passwordHash',
|
||||||
|
salt: 'salt',
|
||||||
|
hint: 'hint',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.LibraryScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
items: 'items',
|
||||||
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -152,7 +171,9 @@ exports.Prisma.NullsOrder = {
|
|||||||
|
|
||||||
exports.Prisma.ModelName = {
|
exports.Prisma.ModelName = {
|
||||||
Collection: 'Collection',
|
Collection: 'Collection',
|
||||||
Drawing: 'Drawing'
|
Drawing: 'Drawing',
|
||||||
|
PrivateVault: 'PrivateVault',
|
||||||
|
Library: 'Library'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+2363
-3
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "prisma-client-6afe3d9baa793154c8d01c79f8418d91423e5ccaec794547bf848a451459cf53",
|
"name": "prisma-client-2894d2d911743eb6e6a7a673efae7513c10b6ce609edeb9caf76ed42f4728682",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"browser": "index-browser.js",
|
"browser": "index-browser.js",
|
||||||
|
|||||||
@@ -32,4 +32,26 @@ model Drawing {
|
|||||||
collection Collection? @relation(fields: [collectionId], references: [id])
|
collection Collection? @relation(fields: [collectionId], references: [id])
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// Privacy/Encryption fields
|
||||||
|
isPrivate Boolean @default(false)
|
||||||
|
encryptedData String? // Encrypted blob containing elements, appState, files when isPrivate=true
|
||||||
|
iv String? // Initialization vector for AES-GCM decryption
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton model for storing vault password hash and settings
|
||||||
|
model PrivateVault {
|
||||||
|
id String @id @default("vault") // Singleton pattern
|
||||||
|
passwordHash String // bcrypt hash for password verification
|
||||||
|
salt String // Salt for client-side key derivation (hex encoded)
|
||||||
|
hint String? // Optional password hint
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Library {
|
||||||
|
id String @id @default("default") // Singleton pattern - use "default" ID
|
||||||
|
items String @default("[]") // Stored as JSON string array of library items
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,25 @@ exports.Prisma.DrawingScalarFieldEnum = {
|
|||||||
version: 'version',
|
version: 'version',
|
||||||
collectionId: 'collectionId',
|
collectionId: 'collectionId',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt',
|
||||||
|
isPrivate: 'isPrivate',
|
||||||
|
encryptedData: 'encryptedData',
|
||||||
|
iv: 'iv'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.PrivateVaultScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
passwordHash: 'passwordHash',
|
||||||
|
salt: 'salt',
|
||||||
|
hint: 'hint',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.LibraryScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
items: 'items',
|
||||||
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -152,7 +171,9 @@ exports.Prisma.NullsOrder = {
|
|||||||
|
|
||||||
exports.Prisma.ModelName = {
|
exports.Prisma.ModelName = {
|
||||||
Collection: 'Collection',
|
Collection: 'Collection',
|
||||||
Drawing: 'Drawing'
|
Drawing: 'Drawing',
|
||||||
|
PrivateVault: 'PrivateVault',
|
||||||
|
Library: 'Library'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+624
-42
File diff suppressed because it is too large
Load Diff
+120
-93
@@ -257,133 +257,160 @@ export const sanitizeUrl = (url: unknown): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strict Zod schema for Excalidraw elements with validation
|
* Very flexible Zod schema for Excalidraw elements
|
||||||
*/
|
*/
|
||||||
export const elementSchema = z
|
export const elementSchema = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string().min(1).max(100),
|
id: z.string().min(1).max(200).optional().nullable(),
|
||||||
type: z.enum([
|
type: z.string().optional().nullable(),
|
||||||
"rectangle",
|
x: z.number().optional().nullable(),
|
||||||
"ellipse",
|
y: z.number().optional().nullable(),
|
||||||
"diamond",
|
width: z.number().optional().nullable(),
|
||||||
"arrow",
|
height: z.number().optional().nullable(),
|
||||||
"line",
|
angle: z.number().optional().nullable(),
|
||||||
"text",
|
strokeColor: z.string().optional().nullable(),
|
||||||
"image",
|
backgroundColor: z.string().optional().nullable(),
|
||||||
"frame",
|
fillStyle: z.string().optional().nullable(),
|
||||||
"embed",
|
strokeWidth: z.number().optional().nullable(),
|
||||||
"selection",
|
strokeStyle: z.string().optional().nullable(),
|
||||||
"text-container",
|
roundness: z.any().optional().nullable(),
|
||||||
]),
|
boundElements: z.array(z.any()).optional().nullable(),
|
||||||
x: z.number().finite().min(-100000).max(100000),
|
groupIds: z.array(z.string()).optional().nullable(),
|
||||||
y: z.number().finite().min(-100000).max(100000),
|
frameId: z.string().optional().nullable(),
|
||||||
width: z.number().finite().min(0).max(100000),
|
seed: z.number().optional().nullable(),
|
||||||
height: z.number().finite().min(0).max(100000),
|
version: z.number().optional().nullable(),
|
||||||
angle: z
|
versionNonce: z.number().optional().nullable(),
|
||||||
.number()
|
isDeleted: z.boolean().optional().nullable(),
|
||||||
.finite()
|
opacity: z.number().optional().nullable(),
|
||||||
.min(-2 * Math.PI)
|
link: z.string().optional().nullable(),
|
||||||
.max(2 * Math.PI),
|
locked: z.boolean().optional().nullable(),
|
||||||
strokeColor: z.string().optional(),
|
text: z.string().optional().nullable(),
|
||||||
backgroundColor: z.string().optional(),
|
fontSize: z.number().optional().nullable(),
|
||||||
fillStyle: z.enum(["solid", "hachure", "cross-hatch", "dots"]).optional(),
|
fontFamily: z.number().optional().nullable(),
|
||||||
strokeWidth: z.number().finite().min(0).max(10).optional(),
|
textAlign: z.string().optional().nullable(),
|
||||||
strokeStyle: z.enum(["solid", "dashed", "dotted"]).optional(),
|
verticalAlign: z.string().optional().nullable(),
|
||||||
roundness: z
|
customData: z.record(z.string(), z.any()).optional().nullable(),
|
||||||
.object({
|
|
||||||
type: z.enum(["round", "sharp"]),
|
|
||||||
value: z.number().finite().min(0).max(1),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
boundElements: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
id: z.string(),
|
|
||||||
type: z.string(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
groupIds: z.array(z.string()).optional(),
|
|
||||||
frameId: z.string().optional(),
|
|
||||||
seed: z.number().finite().optional(),
|
|
||||||
version: z.number().finite().min(0).max(100000),
|
|
||||||
versionNonce: z.number().finite().min(0).max(100000),
|
|
||||||
isDeleted: z.boolean().optional(),
|
|
||||||
opacity: z.number().finite().min(0).max(1).optional(),
|
|
||||||
link: z.string().optional().transform(sanitizeUrl),
|
|
||||||
locked: z.boolean().optional(),
|
|
||||||
// Text-specific properties
|
|
||||||
text: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.transform((val) => sanitizeText(val, 5000)),
|
|
||||||
fontSize: z.number().finite().min(1).max(200).optional(),
|
|
||||||
fontFamily: z.number().finite().min(1).max(5).optional(),
|
|
||||||
textAlign: z.enum(["left", "center", "right"]).optional(),
|
|
||||||
verticalAlign: z.enum(["top", "middle", "bottom"]).optional(),
|
|
||||||
// Custom properties - whitelist only known safe properties
|
|
||||||
customData: z.record(z.string(), z.any()).optional(),
|
|
||||||
})
|
})
|
||||||
.strict();
|
.passthrough()
|
||||||
|
.transform((element) => {
|
||||||
|
// Apply basic sanitization to string values only
|
||||||
|
const sanitized = { ...element };
|
||||||
|
|
||||||
|
if (typeof sanitized.text === "string") {
|
||||||
|
sanitized.text = sanitizeText(sanitized.text, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof sanitized.link === "string") {
|
||||||
|
sanitized.link = sanitizeUrl(sanitized.link);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitized;
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strict Zod schema for Excalidraw app state with validation
|
* Flexible Zod schema for Excalidraw app state with validation
|
||||||
*/
|
*/
|
||||||
export const appStateSchema = z
|
export const appStateSchema = z
|
||||||
.object({
|
.object({
|
||||||
gridSize: z.number().finite().min(0).max(100).optional(),
|
gridSize: z.number().finite().min(0).max(1000).optional().nullable(),
|
||||||
gridStep: z.number().finite().min(1).max(100).optional(),
|
gridStep: z.number().finite().min(1).max(1000).optional().nullable(),
|
||||||
viewBackgroundColor: z.string().optional(),
|
viewBackgroundColor: z.string().optional().nullable(),
|
||||||
currentItemStrokeColor: z.string().optional(),
|
currentItemStrokeColor: z.string().optional().nullable(),
|
||||||
currentItemBackgroundColor: z.string().optional(),
|
currentItemBackgroundColor: z.string().optional().nullable(),
|
||||||
currentItemFillStyle: z
|
currentItemFillStyle: z
|
||||||
.enum(["solid", "hachure", "cross-hatch", "dots"])
|
.enum(["solid", "hachure", "cross-hatch", "dots"])
|
||||||
.optional(),
|
.optional()
|
||||||
currentItemStrokeWidth: z.number().finite().min(0).max(10).optional(),
|
.nullable(),
|
||||||
currentItemStrokeStyle: z.enum(["solid", "dashed", "dotted"]).optional(),
|
currentItemStrokeWidth: z
|
||||||
|
.number()
|
||||||
|
.finite()
|
||||||
|
.min(0)
|
||||||
|
.max(50)
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
|
currentItemStrokeStyle: z
|
||||||
|
.enum(["solid", "dashed", "dotted"])
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
currentItemRoundness: z
|
currentItemRoundness: z
|
||||||
.object({
|
.object({
|
||||||
type: z.enum(["round", "sharp"]),
|
type: z.enum(["round", "sharp"]),
|
||||||
value: z.number().finite().min(0).max(1),
|
value: z.number().finite().min(0).max(1),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional()
|
||||||
currentItemFontSize: z.number().finite().min(1).max(200).optional(),
|
.nullable(),
|
||||||
currentItemFontFamily: z.number().finite().min(1).max(5).optional(),
|
currentItemFontSize: z
|
||||||
currentItemTextAlign: z.enum(["left", "center", "right"]).optional(),
|
.number()
|
||||||
currentItemVerticalAlign: z.enum(["top", "middle", "bottom"]).optional(),
|
.finite()
|
||||||
scrollX: z.number().finite().min(-1000000).max(1000000).optional(),
|
.min(1)
|
||||||
scrollY: z.number().finite().min(-1000000).max(1000000).optional(),
|
.max(500)
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
|
currentItemFontFamily: z
|
||||||
|
.number()
|
||||||
|
.finite()
|
||||||
|
.min(1)
|
||||||
|
.max(10)
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
|
currentItemTextAlign: z
|
||||||
|
.enum(["left", "center", "right"])
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
|
currentItemVerticalAlign: z
|
||||||
|
.enum(["top", "middle", "bottom"])
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
|
scrollX: z
|
||||||
|
.number()
|
||||||
|
.finite()
|
||||||
|
.min(-10000000)
|
||||||
|
.max(10000000)
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
|
scrollY: z
|
||||||
|
.number()
|
||||||
|
.finite()
|
||||||
|
.min(-10000000)
|
||||||
|
.max(10000000)
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
zoom: z
|
zoom: z
|
||||||
.object({
|
.object({
|
||||||
value: z.number().finite().min(0.1).max(10),
|
value: z.number().finite().min(0.01).max(100),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional()
|
||||||
selection: z.array(z.string()).optional(),
|
.nullable(),
|
||||||
selectedElementIds: z.record(z.string(), z.boolean()).optional(),
|
selection: z.array(z.string()).optional().nullable(),
|
||||||
selectedGroupIds: z.record(z.string(), z.boolean()).optional(),
|
selectedElementIds: z.record(z.string(), z.boolean()).optional().nullable(),
|
||||||
|
selectedGroupIds: z.record(z.string(), z.boolean()).optional().nullable(),
|
||||||
activeEmbeddable: z
|
activeEmbeddable: z
|
||||||
.object({
|
.object({
|
||||||
elementId: z.string(),
|
elementId: z.string(),
|
||||||
state: z.string(),
|
state: z.string(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional()
|
||||||
|
.nullable(),
|
||||||
activeTool: z
|
activeTool: z
|
||||||
.object({
|
.object({
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
customType: z.string().optional(),
|
customType: z.string().optional().nullable(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional()
|
||||||
cursorX: z.number().finite().optional(),
|
.nullable(),
|
||||||
cursorY: z.number().finite().optional(),
|
cursorX: z.number().finite().optional().nullable(),
|
||||||
// Sanitize any string values in appState
|
cursorY: z.number().finite().optional().nullable(),
|
||||||
|
// Add common Excalidraw app state properties
|
||||||
|
collaborators: z.record(z.string(), z.any()).optional().nullable(),
|
||||||
})
|
})
|
||||||
.strict()
|
// Allow any additional properties
|
||||||
.catchall(
|
.catchall(
|
||||||
z.any().refine((val) => {
|
z.any().refine((val) => {
|
||||||
// Recursively sanitize any string values found in the object
|
// Sanitize string values, but be more permissive for other types
|
||||||
if (typeof val === "string") {
|
if (typeof val === "string") {
|
||||||
return sanitizeText(val, 1000);
|
return sanitizeText(val, 1000);
|
||||||
}
|
}
|
||||||
|
// Allow numbers, booleans, objects, arrays, null, undefined
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user