@agent-foundry/replay-server 1.0.0 → 1.0.1

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.
Files changed (46) hide show
  1. package/.cursor/dev.mdc +941 -0
  2. package/.cursor/project.mdc +17 -2
  3. package/.env +30 -0
  4. package/Dockerfile +6 -0
  5. package/README.md +153 -12
  6. package/dist/cli/render.js +14 -4
  7. package/dist/cli/render.js.map +1 -1
  8. package/dist/renderer/PuppeteerRenderer.d.ts +12 -2
  9. package/dist/renderer/PuppeteerRenderer.d.ts.map +1 -1
  10. package/dist/renderer/PuppeteerRenderer.js +23 -16
  11. package/dist/renderer/PuppeteerRenderer.js.map +1 -1
  12. package/dist/server/index.d.ts +4 -0
  13. package/dist/server/index.d.ts.map +1 -1
  14. package/dist/server/index.js +200 -46
  15. package/dist/server/index.js.map +1 -1
  16. package/dist/services/BundleManager.d.ts +99 -0
  17. package/dist/services/BundleManager.d.ts.map +1 -0
  18. package/dist/services/BundleManager.js +410 -0
  19. package/dist/services/BundleManager.js.map +1 -0
  20. package/dist/services/OSSClient.d.ts +51 -0
  21. package/dist/services/OSSClient.d.ts.map +1 -0
  22. package/dist/services/OSSClient.js +207 -0
  23. package/dist/services/OSSClient.js.map +1 -0
  24. package/dist/services/index.d.ts +7 -0
  25. package/dist/services/index.d.ts.map +1 -0
  26. package/dist/services/index.js +7 -0
  27. package/dist/services/index.js.map +1 -0
  28. package/dist/services/types.d.ts +73 -0
  29. package/dist/services/types.d.ts.map +1 -0
  30. package/dist/services/types.js +5 -0
  31. package/dist/services/types.js.map +1 -0
  32. package/docker-compose.local.yml +8 -0
  33. package/env.example +30 -0
  34. package/package.json +7 -3
  35. package/restart.sh +5 -0
  36. package/samples/jump_arena_0_ja-mks5um2x-nksbmz.json +1907 -0
  37. package/scripts/render-pipeline.sh +657 -0
  38. package/scripts/test-bundle-preload.sh +20 -0
  39. package/scripts/test-service-sts.sh +176 -0
  40. package/src/cli/render.ts +18 -7
  41. package/src/renderer/PuppeteerRenderer.ts +41 -21
  42. package/src/server/index.ts +249 -68
  43. package/src/services/BundleManager.ts +503 -0
  44. package/src/services/OSSClient.ts +286 -0
  45. package/src/services/index.ts +7 -0
  46. package/src/services/types.ts +78 -0
@@ -0,0 +1,176 @@
1
+ #!/bin/bash
2
+ #
3
+ # Test script for BFF /studio/service/sts endpoint
4
+ #
5
+ # This script tests the service-to-service authentication for obtaining
6
+ # STS credentials from the BFF service.
7
+ #
8
+ # Usage:
9
+ # ./scripts/test-service-sts.sh
10
+ #
11
+ # Environment variables:
12
+ # BFF_BASE_URL - BFF service URL (default: http://localhost:11001)
13
+ # BFF_SERVICE_TOKEN - Set to SUPABASE_JWT_SECRET from BFF's .env
14
+ #
15
+ # The script will:
16
+ # 1. Call POST /studio/service/sts with the service token
17
+ # 2. Display the STS credentials (masked for security)
18
+ # 3. Verify the response structure
19
+ #
20
+
21
+ set -e
22
+
23
+ # Configuration
24
+ BFF_BASE_URL="${BFF_BASE_URL:-http://localhost:11001}"
25
+ BFF_SERVICE_TOKEN="${BFF_SERVICE_TOKEN:-}"
26
+
27
+ echo "=============================================="
28
+ echo "Testing BFF /studio/service/sts endpoint"
29
+ echo "=============================================="
30
+ echo ""
31
+ echo "BFF URL: $BFF_BASE_URL"
32
+ echo "Service Token: ${BFF_SERVICE_TOKEN:0:20}..."
33
+ echo ""
34
+
35
+ # Check if service token is configured
36
+ if [ -z "$BFF_SERVICE_TOKEN" ]; then
37
+ echo "ERROR: BFF_SERVICE_TOKEN is not set"
38
+ echo ""
39
+ echo "Please set BFF_SERVICE_TOKEN to the value of SUPABASE_JWT_SECRET from BFF's .env file:"
40
+ echo ""
41
+ echo " export BFF_SERVICE_TOKEN=your-supabase-jwt-secret"
42
+ echo ""
43
+ echo "Or add it to your .env file:"
44
+ echo ""
45
+ echo " BFF_SERVICE_TOKEN=your-supabase-jwt-secret"
46
+ echo ""
47
+ exit 1
48
+ fi
49
+
50
+ # Test the endpoint
51
+ echo "Calling POST $BFF_BASE_URL/studio/service/sts..."
52
+ echo ""
53
+
54
+ response=$(curl -s -w "\n%{http_code}" -X POST "$BFF_BASE_URL/studio/service/sts" \
55
+ -H "Content-Type: application/json" \
56
+ -H "Authorization: Bearer $BFF_SERVICE_TOKEN")
57
+
58
+ # Extract HTTP status code (last line)
59
+ http_code=$(echo "$response" | tail -n 1)
60
+ # Extract body (all but last line)
61
+ body=$(echo "$response" | sed '$d')
62
+
63
+ echo "HTTP Status: $http_code"
64
+ echo ""
65
+
66
+ if [ "$http_code" -eq 200 ]; then
67
+ echo "SUCCESS! STS credentials received."
68
+ echo ""
69
+
70
+ # Parse and display response (with masked secrets)
71
+ if command -v jq &> /dev/null; then
72
+ echo "Response:"
73
+ echo "$body" | jq '{
74
+ bucket: .bucket,
75
+ region: .region,
76
+ credentials: {
77
+ accessKeyId: (.credentials.accessKeyId | .[0:10] + "..."),
78
+ accessKeySecret: "***MASKED***",
79
+ securityToken: (.credentials.securityToken | .[0:20] + "..."),
80
+ expiration: .credentials.expiration
81
+ }
82
+ }'
83
+
84
+ # Validate response structure
85
+ echo ""
86
+ echo "Validation:"
87
+
88
+ has_bucket=$(echo "$body" | jq -r '.bucket // empty')
89
+ has_region=$(echo "$body" | jq -r '.region // empty')
90
+ has_access_key=$(echo "$body" | jq -r '.credentials.accessKeyId // empty')
91
+ has_secret=$(echo "$body" | jq -r '.credentials.accessKeySecret // empty')
92
+ has_token=$(echo "$body" | jq -r '.credentials.securityToken // empty')
93
+ has_expiration=$(echo "$body" | jq -r '.credentials.expiration // empty')
94
+
95
+ all_valid=true
96
+
97
+ if [ -n "$has_bucket" ]; then
98
+ echo " [OK] bucket: $has_bucket"
99
+ else
100
+ echo " [FAIL] bucket: missing"
101
+ all_valid=false
102
+ fi
103
+
104
+ if [ -n "$has_region" ]; then
105
+ echo " [OK] region: $has_region"
106
+ else
107
+ echo " [FAIL] region: missing"
108
+ all_valid=false
109
+ fi
110
+
111
+ if [ -n "$has_access_key" ]; then
112
+ echo " [OK] credentials.accessKeyId: present"
113
+ else
114
+ echo " [FAIL] credentials.accessKeyId: missing"
115
+ all_valid=false
116
+ fi
117
+
118
+ if [ -n "$has_secret" ]; then
119
+ echo " [OK] credentials.accessKeySecret: present"
120
+ else
121
+ echo " [FAIL] credentials.accessKeySecret: missing"
122
+ all_valid=false
123
+ fi
124
+
125
+ if [ -n "$has_token" ]; then
126
+ echo " [OK] credentials.securityToken: present"
127
+ else
128
+ echo " [FAIL] credentials.securityToken: missing"
129
+ all_valid=false
130
+ fi
131
+
132
+ if [ -n "$has_expiration" ]; then
133
+ echo " [OK] credentials.expiration: $has_expiration"
134
+ else
135
+ echo " [FAIL] credentials.expiration: missing"
136
+ all_valid=false
137
+ fi
138
+
139
+ echo ""
140
+ if [ "$all_valid" = true ]; then
141
+ echo "All validations passed!"
142
+ exit 0
143
+ else
144
+ echo "Some validations failed!"
145
+ exit 1
146
+ fi
147
+ else
148
+ echo "Response (raw):"
149
+ echo "$body"
150
+ echo ""
151
+ echo "Note: Install 'jq' for prettier output"
152
+ fi
153
+ else
154
+ echo "FAILED! Error response:"
155
+ echo ""
156
+ if command -v jq &> /dev/null; then
157
+ echo "$body" | jq .
158
+ else
159
+ echo "$body"
160
+ fi
161
+ echo ""
162
+
163
+ case $http_code in
164
+ 401)
165
+ echo "Authentication failed. Check that BFF_SERVICE_TOKEN matches SUPABASE_JWT_SECRET in BFF."
166
+ ;;
167
+ 500)
168
+ echo "Server error. Check BFF logs for details."
169
+ ;;
170
+ *)
171
+ echo "Unexpected error."
172
+ ;;
173
+ esac
174
+
175
+ exit 1
176
+ fi
package/src/cli/render.ts CHANGED
@@ -9,8 +9,7 @@
9
9
 
10
10
  import * as fs from 'fs';
11
11
  import * as path from 'path';
12
- import { PuppeteerRenderer, RenderProgress } from '../renderer/PuppeteerRenderer.js';
13
- import type { ReplayManifestV1 } from '@agent-foundry/replay';
12
+ import { PuppeteerRenderer, RenderProgress, GenericReplayManifest } from '../renderer/PuppeteerRenderer.js';
14
13
 
15
14
  interface CliArgs {
16
15
  manifest?: string;
@@ -130,7 +129,7 @@ Examples:
130
129
  `);
131
130
  }
132
131
 
133
- async function loadManifest(args: CliArgs): Promise<ReplayManifestV1> {
132
+ async function loadManifest(args: CliArgs): Promise<GenericReplayManifest> {
134
133
  if (args.manifest) {
135
134
  const manifestPath = path.resolve(args.manifest);
136
135
  if (!fs.existsSync(manifestPath)) {
@@ -183,12 +182,24 @@ async function main(): Promise<void> {
183
182
  console.log('📄 Loading manifest...');
184
183
  const manifest = await loadManifest(args);
185
184
  console.log(` Game ID: ${manifest.gameId}`);
186
- console.log(` Timeline: ${manifest.timeline.length} ages`);
187
- console.log(` Highlights: ${manifest.highlights.length}`);
185
+
186
+ // Handle optional timeline
187
+ const timelineLength = Array.isArray(manifest.timeline) ? manifest.timeline.length : 0;
188
+ if (timelineLength > 0) {
189
+ console.log(` Timeline: ${timelineLength} ages`);
190
+ }
191
+
192
+ // Handle optional highlights
193
+ const highlightsLength = Array.isArray((manifest as any).highlights) ? (manifest as any).highlights.length : 0;
194
+ if (highlightsLength > 0) {
195
+ console.log(` Highlights: ${highlightsLength}`);
196
+ }
188
197
 
189
198
  // Estimate duration
190
- const estimatedDuration = manifest.timeline.length * args.secondsPerAge;
191
- console.log(` Estimated video duration: ${estimatedDuration}s`);
199
+ const estimatedDuration = timelineLength * args.secondsPerAge;
200
+ if (estimatedDuration > 0) {
201
+ console.log(` Estimated video duration: ${estimatedDuration}s`);
202
+ }
192
203
  console.log('');
193
204
 
194
205
  // Create output directory
@@ -12,7 +12,18 @@
12
12
  import puppeteer, { Browser, Page } from 'puppeteer';
13
13
  import { spawn, ChildProcess } from 'child_process';
14
14
  import * as path from 'path';
15
- import type { ReplayManifestV1 } from '@agent-foundry/replay';
15
+
16
+ /**
17
+ * Generic replay manifest interface
18
+ * Supports any game type - games interpret their own schemas
19
+ */
20
+ export interface GenericReplayManifest {
21
+ schema: string;
22
+ bundleId?: string;
23
+ gameId: string;
24
+ timeline?: unknown[]; // Optional - games may not have timelines
25
+ [key: string]: unknown;
26
+ }
16
27
 
17
28
  export interface RenderConfig {
18
29
  /** URL of the game (can be local file:// or http://) */
@@ -76,7 +87,7 @@ export class PuppeteerRenderer {
76
87
  /**
77
88
  * Render a replay manifest to video
78
89
  */
79
- async render(manifest: ReplayManifestV1, config: RenderConfig): Promise<RenderResult> {
90
+ async render(manifest: GenericReplayManifest, config: RenderConfig): Promise<RenderResult> {
80
91
  const startTime = Date.now();
81
92
  const fullConfig: RenderConfig & typeof DEFAULT_CONFIG = {
82
93
  ...DEFAULT_CONFIG,
@@ -100,7 +111,9 @@ export class PuppeteerRenderer {
100
111
  await this.loadGame(fullConfig.gameUrl, manifest);
101
112
 
102
113
  // Capture and encode frames in streaming mode
103
- const frameCount = manifest.timeline.length * fullConfig.secondsPerAge * fullConfig.fps;
114
+ // Handle optional timeline - games may not have timelines
115
+ const timelineLength = Array.isArray(manifest.timeline) ? manifest.timeline.length : 1;
116
+ const frameCount = timelineLength * fullConfig.secondsPerAge * fullConfig.fps;
104
117
  this.reportProgress(config.onProgress, 'capture', 0, frameCount, 'Starting capture & encode...');
105
118
  await this.captureAndEncodeFrames(manifest, fullConfig, config.onProgress);
106
119
 
@@ -178,7 +191,7 @@ export class PuppeteerRenderer {
178
191
  });
179
192
  }
180
193
 
181
- private async loadGame(gameUrl: string, manifest: ReplayManifestV1): Promise<void> {
194
+ private async loadGame(gameUrl: string, manifest: GenericReplayManifest): Promise<void> {
182
195
  if (!this.page) throw new Error('Browser not initialized');
183
196
 
184
197
  // Navigate to game with replay mode query parameter
@@ -194,23 +207,28 @@ export class PuppeteerRenderer {
194
207
 
195
208
  console.log('📼 Page loaded, waiting for React to initialize...');
196
209
 
197
- // Wait for React to initialize (check for the app container)
210
+ // Wait for React to initialize
211
+ // Try to detect common app containers, but don't fail if not found
198
212
  try {
199
213
  await this.page.waitForFunction(() => {
200
- // Wait for the app to be mounted and initial loading to complete
201
- const app = document.querySelector('ion-app');
202
- const loading = document.querySelector('ion-spinner');
203
- return app !== null && loading === null;
204
- }, { timeout: 15000 });
214
+ // Check for common app container patterns
215
+ const ionApp = document.querySelector('ion-app');
216
+ const rootDiv = document.querySelector('#root');
217
+ const appDiv = document.querySelector('[id*="app"], [class*="app"]');
218
+
219
+ // If using Ionic, wait for loading to complete
220
+ if (ionApp) {
221
+ const loading = document.querySelector('ion-spinner');
222
+ return loading === null;
223
+ }
224
+
225
+ // Otherwise, just check if root is mounted
226
+ return rootDiv !== null || appDiv !== null;
227
+ }, { timeout: 10000 });
228
+ console.log('📼 App container detected');
205
229
  } catch (error) {
206
- // Log errors before failing
207
- if (this.pageErrors.length > 0) {
208
- console.error('📼 Page errors detected:', this.pageErrors);
209
- }
210
- if (this.failedRequests.length > 0) {
211
- console.error('📼 Failed requests:', this.failedRequests);
212
- }
213
- throw error;
230
+ // Non-fatal - continue anyway
231
+ console.log('📼 App container detection timed out, continuing anyway...');
214
232
  }
215
233
 
216
234
  // Log any errors that occurred (but didn't prevent initialization)
@@ -275,7 +293,7 @@ export class PuppeteerRenderer {
275
293
  * Frames are captured as JPEG buffers and streamed directly to FFmpeg
276
294
  */
277
295
  private async captureAndEncodeFrames(
278
- manifest: ReplayManifestV1,
296
+ manifest: GenericReplayManifest,
279
297
  config: RenderConfig & typeof DEFAULT_CONFIG,
280
298
  onProgress?: (progress: RenderProgress) => void
281
299
  ): Promise<void> {
@@ -290,14 +308,16 @@ export class PuppeteerRenderer {
290
308
  * Internal method to capture and encode with a specific encoder
291
309
  */
292
310
  private async captureAndEncodeFramesWithEncoder(
293
- manifest: ReplayManifestV1,
311
+ manifest: GenericReplayManifest,
294
312
  config: RenderConfig & typeof DEFAULT_CONFIG,
295
313
  encoder: 'h264_nvenc' | 'libx264',
296
314
  onProgress?: (progress: RenderProgress) => void
297
315
  ): Promise<void> {
298
316
  if (!this.page) throw new Error('Browser not initialized');
299
317
 
300
- const totalAges = manifest.timeline.length;
318
+ // Handle optional timeline - games may not have timelines
319
+ const timelineLength = Array.isArray(manifest.timeline) ? manifest.timeline.length : 1;
320
+ const totalAges = timelineLength;
301
321
  const framesPerAge = config.secondsPerAge * config.fps;
302
322
  const totalFrames = totalAges * framesPerAge;
303
323