@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.
- package/.cursor/dev.mdc +941 -0
- package/.cursor/project.mdc +17 -2
- package/.env +30 -0
- package/Dockerfile +6 -0
- package/README.md +153 -12
- package/dist/cli/render.js +14 -4
- package/dist/cli/render.js.map +1 -1
- package/dist/renderer/PuppeteerRenderer.d.ts +12 -2
- package/dist/renderer/PuppeteerRenderer.d.ts.map +1 -1
- package/dist/renderer/PuppeteerRenderer.js +23 -16
- package/dist/renderer/PuppeteerRenderer.js.map +1 -1
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +200 -46
- package/dist/server/index.js.map +1 -1
- package/dist/services/BundleManager.d.ts +99 -0
- package/dist/services/BundleManager.d.ts.map +1 -0
- package/dist/services/BundleManager.js +410 -0
- package/dist/services/BundleManager.js.map +1 -0
- package/dist/services/OSSClient.d.ts +51 -0
- package/dist/services/OSSClient.d.ts.map +1 -0
- package/dist/services/OSSClient.js +207 -0
- package/dist/services/OSSClient.js.map +1 -0
- package/dist/services/index.d.ts +7 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +7 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/types.d.ts +73 -0
- package/dist/services/types.d.ts.map +1 -0
- package/dist/services/types.js +5 -0
- package/dist/services/types.js.map +1 -0
- package/docker-compose.local.yml +8 -0
- package/env.example +30 -0
- package/package.json +7 -3
- package/restart.sh +5 -0
- package/samples/jump_arena_0_ja-mks5um2x-nksbmz.json +1907 -0
- package/scripts/render-pipeline.sh +657 -0
- package/scripts/test-bundle-preload.sh +20 -0
- package/scripts/test-service-sts.sh +176 -0
- package/src/cli/render.ts +18 -7
- package/src/renderer/PuppeteerRenderer.ts +41 -21
- package/src/server/index.ts +249 -68
- package/src/services/BundleManager.ts +503 -0
- package/src/services/OSSClient.ts +286 -0
- package/src/services/index.ts +7 -0
- 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<
|
|
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
|
-
|
|
187
|
-
|
|
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 =
|
|
191
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
//
|
|
207
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|