@twick/render-server 0.14.17 ā 0.14.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -21
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +295 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/package.json +11 -9
- package/package.json.bak +11 -9
- package/src/cli.ts +326 -0
- package/src/index.ts +0 -2
package/README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# @twick/render-server
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A Node.js package for rendering videos using Twick. Export the `renderTwickVideo` function for programmatic use, or scaffold a complete server using the CLI.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
This package provides
|
|
7
|
+
This package provides:
|
|
8
|
+
- **`renderTwickVideo` function**: A programmatic API for rendering Twick videos
|
|
9
|
+
- **CLI tool**: Scaffold a complete Express server to run locally on your machine
|
|
8
10
|
|
|
9
11
|
## Installation
|
|
10
12
|
|
|
@@ -14,21 +16,57 @@ npm install @twick/render-server
|
|
|
14
16
|
pnpm add @twick/render-server
|
|
15
17
|
```
|
|
16
18
|
|
|
19
|
+
**Note:** All required dependencies (`@twick/visualizer`, `@twick/media-utils`, and other Twick packages) are automatically installed with `@twick/render-server`.
|
|
20
|
+
|
|
17
21
|
## Quick Start
|
|
18
22
|
|
|
19
|
-
###
|
|
23
|
+
### Option 1: Scaffold a Server (Recommended)
|
|
24
|
+
|
|
25
|
+
Scaffold a complete server with all endpoints configured:
|
|
20
26
|
|
|
21
27
|
```bash
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
npx @twick/render-server init
|
|
29
|
+
```
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
This creates a `twick-render-server` directory with:
|
|
32
|
+
- Express server with POST `/api/render-video` endpoint
|
|
33
|
+
- Rate limiting and security middleware
|
|
34
|
+
- TypeScript configuration
|
|
35
|
+
- Package.json with all dependencies
|
|
36
|
+
|
|
37
|
+
Then navigate to the scaffolded directory and start the server:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd twick-render-server
|
|
41
|
+
npm install
|
|
42
|
+
npm run dev # Development mode
|
|
43
|
+
# or
|
|
44
|
+
npm run build && npm start # Production mode
|
|
28
45
|
```
|
|
29
46
|
|
|
30
47
|
The server will start on port 3001 by default. You can change this by setting the `PORT` environment variable.
|
|
31
48
|
|
|
49
|
+
### Option 2: Use Programmatically
|
|
50
|
+
|
|
51
|
+
Import and use the `renderTwickVideo` function directly:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { renderTwickVideo } from "@twick/render-server";
|
|
55
|
+
|
|
56
|
+
const videoPath = await renderTwickVideo(
|
|
57
|
+
{
|
|
58
|
+
input: {
|
|
59
|
+
properties: { width: 1920, height: 1080 },
|
|
60
|
+
// ... your project variables
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
outFile: "my-video.mp4",
|
|
65
|
+
quality: "high"
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
```
|
|
69
|
+
|
|
32
70
|
## API Endpoints
|
|
33
71
|
|
|
34
72
|
### POST /api/render-video
|
|
@@ -77,17 +115,24 @@ Renders a video using Twick.
|
|
|
77
115
|
```json
|
|
78
116
|
{
|
|
79
117
|
"success": true,
|
|
80
|
-
"downloadUrl": "http://localhost:3001/download/
|
|
118
|
+
"downloadUrl": "http://localhost:3001/download/my-video.mp4"
|
|
81
119
|
}
|
|
82
120
|
```
|
|
83
121
|
|
|
84
|
-
### GET /download/:
|
|
122
|
+
### GET /download/:filename
|
|
123
|
+
|
|
124
|
+
Downloads a rendered video file. This endpoint is rate-limited to prevent abuse.
|
|
85
125
|
|
|
86
|
-
|
|
126
|
+
**Rate Limits:**
|
|
127
|
+
- 100 requests per 15 minutes per IP address
|
|
128
|
+
- Rate limit headers are included in responses:
|
|
129
|
+
- `X-RateLimit-Limit`: Maximum requests allowed
|
|
130
|
+
- `X-RateLimit-Remaining`: Remaining requests in current window
|
|
131
|
+
- `X-RateLimit-Reset`: When the rate limit window resets
|
|
87
132
|
|
|
88
133
|
### GET /health
|
|
89
134
|
|
|
90
|
-
Health check endpoint.
|
|
135
|
+
Health check endpoint. Returns server status and current timestamp.
|
|
91
136
|
|
|
92
137
|
## Configuration
|
|
93
138
|
|
|
@@ -96,23 +141,50 @@ The server uses the following environment variables:
|
|
|
96
141
|
- `PORT`: Server port (default: 3001)
|
|
97
142
|
- `NODE_ENV`: Environment (development/production)
|
|
98
143
|
|
|
99
|
-
## Development
|
|
144
|
+
## Package Development
|
|
145
|
+
|
|
146
|
+
For developing this package itself:
|
|
100
147
|
|
|
101
148
|
```bash
|
|
102
149
|
# Install dependencies
|
|
103
150
|
pnpm install
|
|
104
151
|
|
|
105
|
-
#
|
|
106
|
-
pnpm run dev
|
|
107
|
-
|
|
108
|
-
# Build for production
|
|
152
|
+
# Build the package
|
|
109
153
|
pnpm run build
|
|
110
154
|
|
|
111
|
-
#
|
|
112
|
-
pnpm
|
|
155
|
+
# Clean build artifacts
|
|
156
|
+
pnpm run clean
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## API Reference
|
|
160
|
+
|
|
161
|
+
### `renderTwickVideo(variables, settings)`
|
|
113
162
|
|
|
114
|
-
|
|
115
|
-
|
|
163
|
+
Renders a Twick video with the provided variables and settings.
|
|
164
|
+
|
|
165
|
+
**Parameters:**
|
|
166
|
+
- `variables` (object): Project variables containing input configuration
|
|
167
|
+
- `settings` (object, optional): Render settings to override defaults
|
|
168
|
+
|
|
169
|
+
**Returns:** `Promise<string>` - Path to the rendered video file
|
|
170
|
+
|
|
171
|
+
**Example:**
|
|
172
|
+
```typescript
|
|
173
|
+
import { renderTwickVideo } from "@twick/render-server";
|
|
174
|
+
|
|
175
|
+
const videoPath = await renderTwickVideo(
|
|
176
|
+
{
|
|
177
|
+
input: {
|
|
178
|
+
properties: { width: 1920, height: 1080 },
|
|
179
|
+
tracks: [/* ... */]
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
outFile: "my-video.mp4",
|
|
184
|
+
quality: "high",
|
|
185
|
+
outDir: "./output"
|
|
186
|
+
}
|
|
187
|
+
);
|
|
116
188
|
```
|
|
117
189
|
|
|
118
190
|
> **Note:** This server will work on Linux and macOS only. Windows is not supported.
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
const SERVER_TEMPLATE = `import express from "express";
|
|
8
|
+
import cors from "cors";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { renderTwickVideo } from "@twick/render-server";
|
|
12
|
+
|
|
13
|
+
const PORT = process.env.PORT || 3001;
|
|
14
|
+
const BASE_PATH = \`http://localhost:\${PORT}\`;
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
|
|
19
|
+
// Rate limiting configuration
|
|
20
|
+
const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
|
|
21
|
+
const RATE_LIMIT_MAX_REQUESTS = 100; // Maximum requests per window
|
|
22
|
+
const RATE_LIMIT_CLEANUP_INTERVAL_MS = 60 * 1000; // Cleanup every minute
|
|
23
|
+
|
|
24
|
+
// In-memory store for rate limiting
|
|
25
|
+
interface RateLimitEntry {
|
|
26
|
+
count: number;
|
|
27
|
+
resetTime: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const rateLimitStore = new Map<string, RateLimitEntry>();
|
|
31
|
+
|
|
32
|
+
// Cleanup expired entries periodically
|
|
33
|
+
setInterval(() => {
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
for (const [key, entry] of rateLimitStore.entries()) {
|
|
36
|
+
if (now > entry.resetTime) {
|
|
37
|
+
rateLimitStore.delete(key);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}, RATE_LIMIT_CLEANUP_INTERVAL_MS);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Rate limiting middleware for API endpoints.
|
|
44
|
+
* Tracks request counts per IP address and enforces rate limits
|
|
45
|
+
* to prevent abuse of the render server.
|
|
46
|
+
*
|
|
47
|
+
* @param req - Express request object
|
|
48
|
+
* @param res - Express response object
|
|
49
|
+
* @param next - Express next function
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* \`\`\`js
|
|
53
|
+
* app.use('/api', rateLimitMiddleware);
|
|
54
|
+
* // Applies rate limiting to all /api routes
|
|
55
|
+
* \`\`\`
|
|
56
|
+
*/
|
|
57
|
+
const rateLimitMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
58
|
+
const clientIP = req.ip || req.connection.remoteAddress || 'unknown';
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
|
|
61
|
+
// Get or create rate limit entry for this IP
|
|
62
|
+
let entry = rateLimitStore.get(clientIP);
|
|
63
|
+
|
|
64
|
+
if (!entry || now > entry.resetTime) {
|
|
65
|
+
// New window or expired entry
|
|
66
|
+
entry = {
|
|
67
|
+
count: 1,
|
|
68
|
+
resetTime: now + RATE_LIMIT_WINDOW_MS
|
|
69
|
+
};
|
|
70
|
+
rateLimitStore.set(clientIP, entry);
|
|
71
|
+
} else {
|
|
72
|
+
// Increment count in current window
|
|
73
|
+
entry.count++;
|
|
74
|
+
|
|
75
|
+
if (entry.count > RATE_LIMIT_MAX_REQUESTS) {
|
|
76
|
+
// Rate limit exceeded
|
|
77
|
+
const retryAfter = Math.ceil((entry.resetTime - now) / 1000);
|
|
78
|
+
res.set('Retry-After', retryAfter.toString());
|
|
79
|
+
res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());
|
|
80
|
+
res.set('X-RateLimit-Remaining', '0');
|
|
81
|
+
res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());
|
|
82
|
+
|
|
83
|
+
return res.status(429).json({
|
|
84
|
+
success: false,
|
|
85
|
+
error: 'Too many requests',
|
|
86
|
+
message: \`Rate limit exceeded. Try again in \${retryAfter} seconds.\`,
|
|
87
|
+
retryAfter
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Add rate limit headers
|
|
93
|
+
res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());
|
|
94
|
+
res.set('X-RateLimit-Remaining', (RATE_LIMIT_MAX_REQUESTS - entry.count).toString());
|
|
95
|
+
res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());
|
|
96
|
+
|
|
97
|
+
next();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const app = express();
|
|
101
|
+
|
|
102
|
+
app.use(cors());
|
|
103
|
+
app.use(express.json());
|
|
104
|
+
|
|
105
|
+
// Serve static files from output directory
|
|
106
|
+
app.use("/output", express.static(path.join(__dirname, "output")));
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* POST endpoint for video rendering requests.
|
|
110
|
+
* Accepts project variables and settings, renders the video,
|
|
111
|
+
* and returns a download URL for the completed video.
|
|
112
|
+
*
|
|
113
|
+
* @param req - Express request object containing variables and settings
|
|
114
|
+
* @param res - Express response object
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* \`\`\`js
|
|
118
|
+
* POST /api/render-video
|
|
119
|
+
* Body: { variables: {...}, settings: {...} }
|
|
120
|
+
* Response: { success: true, downloadUrl: "..." }
|
|
121
|
+
* \`\`\`
|
|
122
|
+
*/
|
|
123
|
+
app.post("/api/render-video", async (req, res) => {
|
|
124
|
+
const { variables, settings } = req.body;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const outputPath = await renderTwickVideo(variables, settings);
|
|
128
|
+
res.json({
|
|
129
|
+
success: true,
|
|
130
|
+
downloadUrl: \`\${BASE_PATH}/download/\${path.basename(outputPath)}\`,
|
|
131
|
+
});
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error("Render error:", error);
|
|
134
|
+
res.status(500).json({
|
|
135
|
+
success: false,
|
|
136
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* GET endpoint for downloading rendered videos.
|
|
143
|
+
* Serves video files with rate limiting and security checks
|
|
144
|
+
* to prevent path traversal attacks.
|
|
145
|
+
*
|
|
146
|
+
* @param req - Express request object with filename parameter
|
|
147
|
+
* @param res - Express response object
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* \`\`\`js
|
|
151
|
+
* GET /download/video-123.mp4
|
|
152
|
+
* // Downloads the specified video file
|
|
153
|
+
* \`\`\`
|
|
154
|
+
*/
|
|
155
|
+
app.get("/download/:filename", rateLimitMiddleware, (req, res) => {
|
|
156
|
+
const outputDir = path.resolve(__dirname, "output");
|
|
157
|
+
const requestedPath = path.resolve(outputDir, req.params.filename);
|
|
158
|
+
if (!requestedPath.startsWith(outputDir + path.sep)) {
|
|
159
|
+
// Attempted path traversal or access outside output directory
|
|
160
|
+
res.status(403).json({
|
|
161
|
+
success: false,
|
|
162
|
+
error: "Forbidden",
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
res.download(requestedPath, (err) => {
|
|
167
|
+
if (err) {
|
|
168
|
+
res.status(404).json({
|
|
169
|
+
success: false,
|
|
170
|
+
error: "File not found",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Health check endpoint for monitoring server status.
|
|
178
|
+
* Returns server status and current timestamp for health monitoring.
|
|
179
|
+
*
|
|
180
|
+
* @param req - Express request object
|
|
181
|
+
* @param res - Express response object
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* \`\`\`js
|
|
185
|
+
* GET /health
|
|
186
|
+
* Response: { status: "ok", timestamp: "2024-01-01T00:00:00.000Z" }
|
|
187
|
+
* \`\`\`
|
|
188
|
+
*/
|
|
189
|
+
app.get("/health", (req, res) => {
|
|
190
|
+
res.json({
|
|
191
|
+
status: "ok",
|
|
192
|
+
timestamp: new Date().toISOString(),
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Start the server
|
|
197
|
+
app.listen(PORT, () => {
|
|
198
|
+
console.log(\`Render server running on port \${PORT}\`);
|
|
199
|
+
console.log(\`Health check: \${BASE_PATH}/health\`);
|
|
200
|
+
console.log(\`API endpoint: \${BASE_PATH}/api/render-video\`);
|
|
201
|
+
console.log(\`Download endpoint rate limited: \${RATE_LIMIT_MAX_REQUESTS} requests per \${RATE_LIMIT_WINDOW_MS / 60000} minutes\`);
|
|
202
|
+
});
|
|
203
|
+
`;
|
|
204
|
+
const PACKAGE_JSON_TEMPLATE = `{
|
|
205
|
+
"name": "twick-render-server",
|
|
206
|
+
"version": "1.0.0",
|
|
207
|
+
"type": "module",
|
|
208
|
+
"scripts": {
|
|
209
|
+
"dev": "tsx watch server.ts",
|
|
210
|
+
"build": "tsc",
|
|
211
|
+
"start": "node dist/server.js"
|
|
212
|
+
},
|
|
213
|
+
"dependencies": {
|
|
214
|
+
"@twick/render-server": "^0.14.11",
|
|
215
|
+
"cors": "^2.8.5",
|
|
216
|
+
"express": "^4.18.2"
|
|
217
|
+
},
|
|
218
|
+
"devDependencies": {
|
|
219
|
+
"@types/cors": "^2.8.17",
|
|
220
|
+
"@types/express": "^4.17.21",
|
|
221
|
+
"@types/node": "^20.10.0",
|
|
222
|
+
"tsx": "^4.7.0",
|
|
223
|
+
"typescript": "5.4.2"
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
`;
|
|
227
|
+
const TSCONFIG_TEMPLATE = `{
|
|
228
|
+
"compilerOptions": {
|
|
229
|
+
"target": "ES2022",
|
|
230
|
+
"module": "ESNext",
|
|
231
|
+
"moduleResolution": "bundler",
|
|
232
|
+
"allowSyntheticDefaultImports": true,
|
|
233
|
+
"esModuleInterop": true,
|
|
234
|
+
"allowJs": true,
|
|
235
|
+
"strict": true,
|
|
236
|
+
"skipLibCheck": true,
|
|
237
|
+
"forceConsistentCasingInFileNames": true,
|
|
238
|
+
"outDir": "./dist",
|
|
239
|
+
"rootDir": ".",
|
|
240
|
+
"declaration": true,
|
|
241
|
+
"declarationMap": true,
|
|
242
|
+
"sourceMap": true,
|
|
243
|
+
"resolveJsonModule": true,
|
|
244
|
+
"noEmit": false
|
|
245
|
+
},
|
|
246
|
+
"include": ["server.ts"],
|
|
247
|
+
"exclude": ["node_modules", "dist"]
|
|
248
|
+
}
|
|
249
|
+
`;
|
|
250
|
+
const GITIGNORE_TEMPLATE = `node_modules/
|
|
251
|
+
dist/
|
|
252
|
+
output/
|
|
253
|
+
.env
|
|
254
|
+
*.log
|
|
255
|
+
`;
|
|
256
|
+
function scaffoldServer() {
|
|
257
|
+
const currentDir = process.cwd();
|
|
258
|
+
const serverDir = path.join(currentDir, "twick-render-server");
|
|
259
|
+
// Check if directory already exists
|
|
260
|
+
if (fs.existsSync(serverDir)) {
|
|
261
|
+
console.error(`Error: Directory "${serverDir}" already exists.`);
|
|
262
|
+
console.error("Please remove it or choose a different location.");
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
// Create server directory
|
|
266
|
+
fs.mkdirSync(serverDir, { recursive: true });
|
|
267
|
+
fs.mkdirSync(path.join(serverDir, "output"), { recursive: true });
|
|
268
|
+
// Write server.ts
|
|
269
|
+
fs.writeFileSync(path.join(serverDir, "server.ts"), SERVER_TEMPLATE, "utf-8");
|
|
270
|
+
// Write package.json
|
|
271
|
+
fs.writeFileSync(path.join(serverDir, "package.json"), PACKAGE_JSON_TEMPLATE, "utf-8");
|
|
272
|
+
// Write tsconfig.json
|
|
273
|
+
fs.writeFileSync(path.join(serverDir, "tsconfig.json"), TSCONFIG_TEMPLATE, "utf-8");
|
|
274
|
+
// Write .gitignore
|
|
275
|
+
fs.writeFileSync(path.join(serverDir, ".gitignore"), GITIGNORE_TEMPLATE, "utf-8");
|
|
276
|
+
console.log(`ā
Successfully scaffolded Twick render server!`);
|
|
277
|
+
console.log(`\nš Server created at: ${serverDir}`);
|
|
278
|
+
console.log(`\nš Next steps:`);
|
|
279
|
+
console.log(` 1. cd ${path.basename(serverDir)}`);
|
|
280
|
+
console.log(` 2. npm install`);
|
|
281
|
+
console.log(` 3. npm run dev (for development)`);
|
|
282
|
+
console.log(` 4. npm run build && npm start (for production)`);
|
|
283
|
+
}
|
|
284
|
+
// Parse command line arguments
|
|
285
|
+
const args = process.argv.slice(2);
|
|
286
|
+
const command = args[0];
|
|
287
|
+
if (command === "init") {
|
|
288
|
+
scaffoldServer();
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
console.log("Usage: npx twick-render-server init");
|
|
292
|
+
console.log("\nThis command scaffolds a new Twick render server in the current directory.");
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,YAAY,CAAC
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,YAAY,CAAC
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,YAAY,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twick/render-server",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.20",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"twick-render-server": "./dist/cli.js"
|
|
9
|
+
},
|
|
7
10
|
"scripts": {
|
|
8
11
|
"build": "tsc",
|
|
9
12
|
"dev": "tsx watch src/server.ts",
|
|
@@ -14,13 +17,12 @@
|
|
|
14
17
|
"access": "public"
|
|
15
18
|
},
|
|
16
19
|
"dependencies": {
|
|
17
|
-
"@twick/2d": "0.14.
|
|
18
|
-
"@twick/core": "0.14.
|
|
19
|
-
"@twick/ffmpeg": "0.14.
|
|
20
|
-
"@twick/renderer": "0.14.
|
|
21
|
-
"@twick/ui": "0.14.
|
|
22
|
-
"@twick/visualizer": "0.14.
|
|
23
|
-
"@types/express-rate-limit": "^6.0.2",
|
|
20
|
+
"@twick/2d": "^0.14.20",
|
|
21
|
+
"@twick/core": "^0.14.20",
|
|
22
|
+
"@twick/ffmpeg": "^0.14.20",
|
|
23
|
+
"@twick/renderer": "^0.14.20",
|
|
24
|
+
"@twick/ui": "^0.14.20",
|
|
25
|
+
"@twick/visualizer": "0.14.20",
|
|
24
26
|
"cors": "^2.8.5",
|
|
25
27
|
"express": "^4.18.2",
|
|
26
28
|
"express-rate-limit": "^8.0.1",
|
package/package.json.bak
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twick/render-server",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.20",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"twick-render-server": "./dist/cli.js"
|
|
9
|
+
},
|
|
7
10
|
"scripts": {
|
|
8
11
|
"build": "tsc",
|
|
9
12
|
"dev": "tsx watch src/server.ts",
|
|
@@ -14,13 +17,12 @@
|
|
|
14
17
|
"access": "public"
|
|
15
18
|
},
|
|
16
19
|
"dependencies": {
|
|
17
|
-
"@twick/2d": "0.14.
|
|
18
|
-
"@twick/core": "0.14.
|
|
19
|
-
"@twick/ffmpeg": "0.14.
|
|
20
|
-
"@twick/renderer": "0.14.
|
|
21
|
-
"@twick/ui": "0.14.
|
|
22
|
-
"@twick/visualizer": "0.14.
|
|
23
|
-
"@types/express-rate-limit": "^6.0.2",
|
|
20
|
+
"@twick/2d": "^0.14.20",
|
|
21
|
+
"@twick/core": "^0.14.20",
|
|
22
|
+
"@twick/ffmpeg": "^0.14.20",
|
|
23
|
+
"@twick/renderer": "^0.14.20",
|
|
24
|
+
"@twick/ui": "^0.14.20",
|
|
25
|
+
"@twick/visualizer": "0.14.20",
|
|
24
26
|
"cors": "^2.8.5",
|
|
25
27
|
"express": "^4.18.2",
|
|
26
28
|
"express-rate-limit": "^8.0.1",
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
const SERVER_TEMPLATE = `import express from "express";
|
|
11
|
+
import cors from "cors";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
import { renderTwickVideo } from "@twick/render-server";
|
|
15
|
+
|
|
16
|
+
const PORT = process.env.PORT || 3001;
|
|
17
|
+
const BASE_PATH = \`http://localhost:\${PORT}\`;
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = path.dirname(__filename);
|
|
21
|
+
|
|
22
|
+
// Rate limiting configuration
|
|
23
|
+
const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
|
|
24
|
+
const RATE_LIMIT_MAX_REQUESTS = 100; // Maximum requests per window
|
|
25
|
+
const RATE_LIMIT_CLEANUP_INTERVAL_MS = 60 * 1000; // Cleanup every minute
|
|
26
|
+
|
|
27
|
+
// In-memory store for rate limiting
|
|
28
|
+
interface RateLimitEntry {
|
|
29
|
+
count: number;
|
|
30
|
+
resetTime: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const rateLimitStore = new Map<string, RateLimitEntry>();
|
|
34
|
+
|
|
35
|
+
// Cleanup expired entries periodically
|
|
36
|
+
setInterval(() => {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
for (const [key, entry] of rateLimitStore.entries()) {
|
|
39
|
+
if (now > entry.resetTime) {
|
|
40
|
+
rateLimitStore.delete(key);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}, RATE_LIMIT_CLEANUP_INTERVAL_MS);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Rate limiting middleware for API endpoints.
|
|
47
|
+
* Tracks request counts per IP address and enforces rate limits
|
|
48
|
+
* to prevent abuse of the render server.
|
|
49
|
+
*
|
|
50
|
+
* @param req - Express request object
|
|
51
|
+
* @param res - Express response object
|
|
52
|
+
* @param next - Express next function
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* \`\`\`js
|
|
56
|
+
* app.use('/api', rateLimitMiddleware);
|
|
57
|
+
* // Applies rate limiting to all /api routes
|
|
58
|
+
* \`\`\`
|
|
59
|
+
*/
|
|
60
|
+
const rateLimitMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
61
|
+
const clientIP = req.ip || req.connection.remoteAddress || 'unknown';
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
|
|
64
|
+
// Get or create rate limit entry for this IP
|
|
65
|
+
let entry = rateLimitStore.get(clientIP);
|
|
66
|
+
|
|
67
|
+
if (!entry || now > entry.resetTime) {
|
|
68
|
+
// New window or expired entry
|
|
69
|
+
entry = {
|
|
70
|
+
count: 1,
|
|
71
|
+
resetTime: now + RATE_LIMIT_WINDOW_MS
|
|
72
|
+
};
|
|
73
|
+
rateLimitStore.set(clientIP, entry);
|
|
74
|
+
} else {
|
|
75
|
+
// Increment count in current window
|
|
76
|
+
entry.count++;
|
|
77
|
+
|
|
78
|
+
if (entry.count > RATE_LIMIT_MAX_REQUESTS) {
|
|
79
|
+
// Rate limit exceeded
|
|
80
|
+
const retryAfter = Math.ceil((entry.resetTime - now) / 1000);
|
|
81
|
+
res.set('Retry-After', retryAfter.toString());
|
|
82
|
+
res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());
|
|
83
|
+
res.set('X-RateLimit-Remaining', '0');
|
|
84
|
+
res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());
|
|
85
|
+
|
|
86
|
+
return res.status(429).json({
|
|
87
|
+
success: false,
|
|
88
|
+
error: 'Too many requests',
|
|
89
|
+
message: \`Rate limit exceeded. Try again in \${retryAfter} seconds.\`,
|
|
90
|
+
retryAfter
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Add rate limit headers
|
|
96
|
+
res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());
|
|
97
|
+
res.set('X-RateLimit-Remaining', (RATE_LIMIT_MAX_REQUESTS - entry.count).toString());
|
|
98
|
+
res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());
|
|
99
|
+
|
|
100
|
+
next();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const app = express();
|
|
104
|
+
|
|
105
|
+
app.use(cors());
|
|
106
|
+
app.use(express.json());
|
|
107
|
+
|
|
108
|
+
// Serve static files from output directory
|
|
109
|
+
app.use("/output", express.static(path.join(__dirname, "output")));
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* POST endpoint for video rendering requests.
|
|
113
|
+
* Accepts project variables and settings, renders the video,
|
|
114
|
+
* and returns a download URL for the completed video.
|
|
115
|
+
*
|
|
116
|
+
* @param req - Express request object containing variables and settings
|
|
117
|
+
* @param res - Express response object
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* \`\`\`js
|
|
121
|
+
* POST /api/render-video
|
|
122
|
+
* Body: { variables: {...}, settings: {...} }
|
|
123
|
+
* Response: { success: true, downloadUrl: "..." }
|
|
124
|
+
* \`\`\`
|
|
125
|
+
*/
|
|
126
|
+
app.post("/api/render-video", async (req, res) => {
|
|
127
|
+
const { variables, settings } = req.body;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const outputPath = await renderTwickVideo(variables, settings);
|
|
131
|
+
res.json({
|
|
132
|
+
success: true,
|
|
133
|
+
downloadUrl: \`\${BASE_PATH}/download/\${path.basename(outputPath)}\`,
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error("Render error:", error);
|
|
137
|
+
res.status(500).json({
|
|
138
|
+
success: false,
|
|
139
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* GET endpoint for downloading rendered videos.
|
|
146
|
+
* Serves video files with rate limiting and security checks
|
|
147
|
+
* to prevent path traversal attacks.
|
|
148
|
+
*
|
|
149
|
+
* @param req - Express request object with filename parameter
|
|
150
|
+
* @param res - Express response object
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* \`\`\`js
|
|
154
|
+
* GET /download/video-123.mp4
|
|
155
|
+
* // Downloads the specified video file
|
|
156
|
+
* \`\`\`
|
|
157
|
+
*/
|
|
158
|
+
app.get("/download/:filename", rateLimitMiddleware, (req, res) => {
|
|
159
|
+
const outputDir = path.resolve(__dirname, "output");
|
|
160
|
+
const requestedPath = path.resolve(outputDir, req.params.filename);
|
|
161
|
+
if (!requestedPath.startsWith(outputDir + path.sep)) {
|
|
162
|
+
// Attempted path traversal or access outside output directory
|
|
163
|
+
res.status(403).json({
|
|
164
|
+
success: false,
|
|
165
|
+
error: "Forbidden",
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
res.download(requestedPath, (err) => {
|
|
170
|
+
if (err) {
|
|
171
|
+
res.status(404).json({
|
|
172
|
+
success: false,
|
|
173
|
+
error: "File not found",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Health check endpoint for monitoring server status.
|
|
181
|
+
* Returns server status and current timestamp for health monitoring.
|
|
182
|
+
*
|
|
183
|
+
* @param req - Express request object
|
|
184
|
+
* @param res - Express response object
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* \`\`\`js
|
|
188
|
+
* GET /health
|
|
189
|
+
* Response: { status: "ok", timestamp: "2024-01-01T00:00:00.000Z" }
|
|
190
|
+
* \`\`\`
|
|
191
|
+
*/
|
|
192
|
+
app.get("/health", (req, res) => {
|
|
193
|
+
res.json({
|
|
194
|
+
status: "ok",
|
|
195
|
+
timestamp: new Date().toISOString(),
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Start the server
|
|
200
|
+
app.listen(PORT, () => {
|
|
201
|
+
console.log(\`Render server running on port \${PORT}\`);
|
|
202
|
+
console.log(\`Health check: \${BASE_PATH}/health\`);
|
|
203
|
+
console.log(\`API endpoint: \${BASE_PATH}/api/render-video\`);
|
|
204
|
+
console.log(\`Download endpoint rate limited: \${RATE_LIMIT_MAX_REQUESTS} requests per \${RATE_LIMIT_WINDOW_MS / 60000} minutes\`);
|
|
205
|
+
});
|
|
206
|
+
`;
|
|
207
|
+
|
|
208
|
+
const PACKAGE_JSON_TEMPLATE = `{
|
|
209
|
+
"name": "twick-render-server",
|
|
210
|
+
"version": "1.0.0",
|
|
211
|
+
"type": "module",
|
|
212
|
+
"scripts": {
|
|
213
|
+
"dev": "tsx watch server.ts",
|
|
214
|
+
"build": "tsc",
|
|
215
|
+
"start": "node dist/server.js"
|
|
216
|
+
},
|
|
217
|
+
"dependencies": {
|
|
218
|
+
"@twick/render-server": "^0.14.11",
|
|
219
|
+
"cors": "^2.8.5",
|
|
220
|
+
"express": "^4.18.2"
|
|
221
|
+
},
|
|
222
|
+
"devDependencies": {
|
|
223
|
+
"@types/cors": "^2.8.17",
|
|
224
|
+
"@types/express": "^4.17.21",
|
|
225
|
+
"@types/node": "^20.10.0",
|
|
226
|
+
"tsx": "^4.7.0",
|
|
227
|
+
"typescript": "5.4.2"
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
`;
|
|
231
|
+
|
|
232
|
+
const TSCONFIG_TEMPLATE = `{
|
|
233
|
+
"compilerOptions": {
|
|
234
|
+
"target": "ES2022",
|
|
235
|
+
"module": "ESNext",
|
|
236
|
+
"moduleResolution": "bundler",
|
|
237
|
+
"allowSyntheticDefaultImports": true,
|
|
238
|
+
"esModuleInterop": true,
|
|
239
|
+
"allowJs": true,
|
|
240
|
+
"strict": true,
|
|
241
|
+
"skipLibCheck": true,
|
|
242
|
+
"forceConsistentCasingInFileNames": true,
|
|
243
|
+
"outDir": "./dist",
|
|
244
|
+
"rootDir": ".",
|
|
245
|
+
"declaration": true,
|
|
246
|
+
"declarationMap": true,
|
|
247
|
+
"sourceMap": true,
|
|
248
|
+
"resolveJsonModule": true,
|
|
249
|
+
"noEmit": false
|
|
250
|
+
},
|
|
251
|
+
"include": ["server.ts"],
|
|
252
|
+
"exclude": ["node_modules", "dist"]
|
|
253
|
+
}
|
|
254
|
+
`;
|
|
255
|
+
|
|
256
|
+
const GITIGNORE_TEMPLATE = `node_modules/
|
|
257
|
+
dist/
|
|
258
|
+
output/
|
|
259
|
+
.env
|
|
260
|
+
*.log
|
|
261
|
+
`;
|
|
262
|
+
|
|
263
|
+
function scaffoldServer() {
|
|
264
|
+
const currentDir = process.cwd();
|
|
265
|
+
const serverDir = path.join(currentDir, "twick-render-server");
|
|
266
|
+
|
|
267
|
+
// Check if directory already exists
|
|
268
|
+
if (fs.existsSync(serverDir)) {
|
|
269
|
+
console.error(`Error: Directory "${serverDir}" already exists.`);
|
|
270
|
+
console.error("Please remove it or choose a different location.");
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Create server directory
|
|
275
|
+
fs.mkdirSync(serverDir, { recursive: true });
|
|
276
|
+
fs.mkdirSync(path.join(serverDir, "output"), { recursive: true });
|
|
277
|
+
|
|
278
|
+
// Write server.ts
|
|
279
|
+
fs.writeFileSync(
|
|
280
|
+
path.join(serverDir, "server.ts"),
|
|
281
|
+
SERVER_TEMPLATE,
|
|
282
|
+
"utf-8"
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Write package.json
|
|
286
|
+
fs.writeFileSync(
|
|
287
|
+
path.join(serverDir, "package.json"),
|
|
288
|
+
PACKAGE_JSON_TEMPLATE,
|
|
289
|
+
"utf-8"
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// Write tsconfig.json
|
|
293
|
+
fs.writeFileSync(
|
|
294
|
+
path.join(serverDir, "tsconfig.json"),
|
|
295
|
+
TSCONFIG_TEMPLATE,
|
|
296
|
+
"utf-8"
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// Write .gitignore
|
|
300
|
+
fs.writeFileSync(
|
|
301
|
+
path.join(serverDir, ".gitignore"),
|
|
302
|
+
GITIGNORE_TEMPLATE,
|
|
303
|
+
"utf-8"
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
console.log(`ā
Successfully scaffolded Twick render server!`);
|
|
307
|
+
console.log(`\nš Server created at: ${serverDir}`);
|
|
308
|
+
console.log(`\nš Next steps:`);
|
|
309
|
+
console.log(` 1. cd ${path.basename(serverDir)}`);
|
|
310
|
+
console.log(` 2. npm install`);
|
|
311
|
+
console.log(` 3. npm run dev (for development)`);
|
|
312
|
+
console.log(` 4. npm run build && npm start (for production)`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Parse command line arguments
|
|
316
|
+
const args = process.argv.slice(2);
|
|
317
|
+
const command = args[0];
|
|
318
|
+
|
|
319
|
+
if (command === "init") {
|
|
320
|
+
scaffoldServer();
|
|
321
|
+
} else {
|
|
322
|
+
console.log("Usage: npx twick-render-server init");
|
|
323
|
+
console.log("\nThis command scaffolds a new Twick render server in the current directory.");
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
|