@twick/render-server 0.14.16 → 0.14.18

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 CHANGED
@@ -1,10 +1,12 @@
1
1
  # @twick/render-server
2
2
 
3
- A simple Node.js server for rendering videos using Twick.
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 a server-side rendering solution for Twick video projects. It allows you to render video projects on the server and download the resulting video files.
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
 
@@ -16,19 +18,53 @@ pnpm add @twick/render-server
16
18
 
17
19
  ## Quick Start
18
20
 
19
- ### Starting the server
21
+ ### Option 1: Scaffold a Server (Recommended)
22
+
23
+ Scaffold a complete server with all endpoints configured:
20
24
 
21
25
  ```bash
22
- # Development mode
23
- pnpm run dev
26
+ npx @twick/render-server init
27
+ ```
24
28
 
25
- # Production mode
26
- pnpm run build
27
- pnpm start
29
+ This creates a `twick-render-server` directory with:
30
+ - Express server with POST `/api/render-video` endpoint
31
+ - Rate limiting and security middleware
32
+ - TypeScript configuration
33
+ - Package.json with all dependencies
34
+
35
+ Then navigate to the scaffolded directory and start the server:
36
+
37
+ ```bash
38
+ cd twick-render-server
39
+ npm install
40
+ npm run dev # Development mode
41
+ # or
42
+ npm run build && npm start # Production mode
28
43
  ```
29
44
 
30
45
  The server will start on port 3001 by default. You can change this by setting the `PORT` environment variable.
31
46
 
47
+ ### Option 2: Use Programmatically
48
+
49
+ Import and use the `renderTwickVideo` function directly:
50
+
51
+ ```typescript
52
+ import { renderTwickVideo } from "@twick/render-server";
53
+
54
+ const videoPath = await renderTwickVideo(
55
+ {
56
+ input: {
57
+ properties: { width: 1920, height: 1080 },
58
+ // ... your project variables
59
+ }
60
+ },
61
+ {
62
+ outFile: "my-video.mp4",
63
+ quality: "high"
64
+ }
65
+ );
66
+ ```
67
+
32
68
  ## API Endpoints
33
69
 
34
70
  ### POST /api/render-video
@@ -77,17 +113,24 @@ Renders a video using Twick.
77
113
  ```json
78
114
  {
79
115
  "success": true,
80
- "downloadUrl": "http://localhost:3001/download/output/my-video.mp4"
116
+ "downloadUrl": "http://localhost:3001/download/my-video.mp4"
81
117
  }
82
118
  ```
83
119
 
84
- ### GET /download/:outFile
120
+ ### GET /download/:filename
121
+
122
+ Downloads a rendered video file. This endpoint is rate-limited to prevent abuse.
85
123
 
86
- Downloads a rendered video file.
124
+ **Rate Limits:**
125
+ - 100 requests per 15 minutes per IP address
126
+ - Rate limit headers are included in responses:
127
+ - `X-RateLimit-Limit`: Maximum requests allowed
128
+ - `X-RateLimit-Remaining`: Remaining requests in current window
129
+ - `X-RateLimit-Reset`: When the rate limit window resets
87
130
 
88
131
  ### GET /health
89
132
 
90
- Health check endpoint.
133
+ Health check endpoint. Returns server status and current timestamp.
91
134
 
92
135
  ## Configuration
93
136
 
@@ -96,23 +139,50 @@ The server uses the following environment variables:
96
139
  - `PORT`: Server port (default: 3001)
97
140
  - `NODE_ENV`: Environment (development/production)
98
141
 
99
- ## Development
142
+ ## Package Development
143
+
144
+ For developing this package itself:
100
145
 
101
146
  ```bash
102
147
  # Install dependencies
103
148
  pnpm install
104
149
 
105
- # Start development server
106
- pnpm run dev
107
-
108
- # Build for production
150
+ # Build the package
109
151
  pnpm run build
110
152
 
111
- # Start production server
112
- pnpm start
153
+ # Clean build artifacts
154
+ pnpm run clean
155
+ ```
156
+
157
+ ## API Reference
158
+
159
+ ### `renderTwickVideo(variables, settings)`
113
160
 
114
- # Test
115
- node test.js
161
+ Renders a Twick video with the provided variables and settings.
162
+
163
+ **Parameters:**
164
+ - `variables` (object): Project variables containing input configuration
165
+ - `settings` (object, optional): Render settings to override defaults
166
+
167
+ **Returns:** `Promise<string>` - Path to the rendered video file
168
+
169
+ **Example:**
170
+ ```typescript
171
+ import { renderTwickVideo } from "@twick/render-server";
172
+
173
+ const videoPath = await renderTwickVideo(
174
+ {
175
+ input: {
176
+ properties: { width: 1920, height: 1080 },
177
+ tracks: [/* ... */]
178
+ }
179
+ },
180
+ {
181
+ outFile: "my-video.mp4",
182
+ quality: "high",
183
+ outDir: "./output"
184
+ }
185
+ );
116
186
  ```
117
187
 
118
188
  > **Note:** This server will work on Linux and macOS only. Windows is not supported.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAE3C,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoMvB,CAAC;AAEF,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;CAsB7B,CAAC;AAEF,MAAM,iBAAiB,GAAG;;;;;;;;;;;;;;;;;;;;;;CAsBzB,CAAC;AAEF,MAAM,kBAAkB,GAAG;;;;;CAK1B,CAAC;AAEF,SAAS,cAAc;IACrB,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IACjC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,qBAAqB,CAAC,CAAC;IAE/D,oCAAoC;IACpC,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,KAAK,CAAC,qBAAqB,SAAS,mBAAmB,CAAC,CAAC;QACjE,OAAO,CAAC,KAAK,CAAC,kDAAkD,CAAC,CAAC;QAClE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,0BAA0B;IAC1B,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAElE,kBAAkB;IAClB,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,EACjC,eAAe,EACf,OAAO,CACR,CAAC;IAEF,qBAAqB;IACrB,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,EACpC,qBAAqB,EACrB,OAAO,CACR,CAAC;IAEF,sBAAsB;IACtB,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,EACrC,iBAAiB,EACjB,OAAO,CACR,CAAC;IAEF,mBAAmB;IACnB,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAClC,kBAAkB,EAClB,OAAO,CACR,CAAC;IAEF,OAAO,CAAC,GAAG,CAAC,gDAAgD,CAAC,CAAC;IAC9D,OAAO,CAAC,GAAG,CAAC,2BAA2B,SAAS,EAAE,CAAC,CAAC;IACpD,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAChC,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACpD,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IACjC,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;IACnD,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC,CAAC;AACnE,CAAC;AAED,+BAA+B;AAC/B,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAExB,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,CAAC;AACnB,CAAC;KAAM,CAAC;IACN,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;IACnD,OAAO,CAAC,GAAG,CAAC,8EAA8E,CAAC,CAAC;IAC5F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  export { default as renderTwickVideo } from "./renderer";
2
- export { default as createServer } from "./server";
3
2
  //# sourceMappingURL=index.d.ts.map
@@ -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;AAGzD,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,UAAU,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
@@ -1,5 +1,3 @@
1
1
  // Export the renderer function
2
2
  export { default as renderTwickVideo } from "./renderer";
3
- // Re-export the server for programmatic usage
4
- export { default as createServer } from "./server";
5
3
  //# sourceMappingURL=index.js.map
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;AAEzD,8CAA8C;AAC9C,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,UAAU,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.16",
3
+ "version": "0.14.18",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "type": "module",
6
- "main": "dist/server.js",
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,12 +17,12 @@
14
17
  "access": "public"
15
18
  },
16
19
  "dependencies": {
17
- "@twick/2d": "0.14.16",
18
- "@twick/core": "0.14.16",
19
- "@twick/ffmpeg": "0.14.16",
20
- "@twick/renderer": "0.14.16",
21
- "@twick/ui": "0.14.16",
22
- "@twick/visualizer": "0.14.16",
20
+ "@twick/2d": "0.14.18",
21
+ "@twick/core": "0.14.18",
22
+ "@twick/ffmpeg": "0.14.18",
23
+ "@twick/renderer": "0.14.18",
24
+ "@twick/ui": "0.14.18",
25
+ "@twick/visualizer": "0.14.18",
23
26
  "@types/express-rate-limit": "^6.0.2",
24
27
  "cors": "^2.8.5",
25
28
  "express": "^4.18.2",
package/package.json.bak CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@twick/render-server",
3
- "version": "0.14.16",
3
+ "version": "0.14.18",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "type": "module",
6
- "main": "dist/server.js",
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,12 +17,12 @@
14
17
  "access": "public"
15
18
  },
16
19
  "dependencies": {
17
- "@twick/2d": "0.14.16",
18
- "@twick/core": "0.14.16",
19
- "@twick/ffmpeg": "0.14.16",
20
- "@twick/renderer": "0.14.16",
21
- "@twick/ui": "0.14.16",
22
- "@twick/visualizer": "0.14.16",
20
+ "@twick/2d": "0.14.18",
21
+ "@twick/core": "0.14.18",
22
+ "@twick/ffmpeg": "0.14.18",
23
+ "@twick/renderer": "0.14.18",
24
+ "@twick/ui": "0.14.18",
25
+ "@twick/visualizer": "0.14.18",
23
26
  "@types/express-rate-limit": "^6.0.2",
24
27
  "cors": "^2.8.5",
25
28
  "express": "^4.18.2",
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
+
package/src/index.ts CHANGED
@@ -1,5 +1,3 @@
1
1
  // Export the renderer function
2
2
  export { default as renderTwickVideo } from "./renderer";
3
3
 
4
- // Re-export the server for programmatic usage
5
- export { default as createServer } from "./server";