@agent-foundry/replay-server 1.0.0 → 1.0.2

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 +297 -27
  6. package/dist/cli/render.js +14 -4
  7. package/dist/cli/render.js.map +1 -1
  8. package/dist/renderer/PuppeteerRenderer.d.ts +28 -2
  9. package/dist/renderer/PuppeteerRenderer.d.ts.map +1 -1
  10. package/dist/renderer/PuppeteerRenderer.js +134 -36
  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 +10 -0
  33. package/env.example +30 -0
  34. package/package.json +7 -3
  35. package/restart.sh +5 -0
  36. package/samples/jump_arena_5_ja-mksi5fku-qgk5iq.json +1952 -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 +192 -39
  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,286 @@
1
+ /**
2
+ * OSS Client
3
+ *
4
+ * Handles authenticated downloads from Alibaba Cloud OSS using STS credentials
5
+ * obtained from the BFF API.
6
+ */
7
+
8
+ import OSS from 'ali-oss';
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import type { STSCredentials, OSSClientConfig } from './types.js';
12
+
13
+ /**
14
+ * Error thrown by OSS client operations
15
+ */
16
+ export class OSSClientError extends Error {
17
+ constructor(
18
+ message: string,
19
+ public readonly code: string,
20
+ public readonly statusCode?: number
21
+ ) {
22
+ super(message);
23
+ this.name = 'OSSClientError';
24
+ }
25
+ }
26
+
27
+ /**
28
+ * OSS Client for downloading bundles with STS authentication
29
+ */
30
+ export class OSSClient {
31
+ private readonly bffBaseUrl: string;
32
+ private readonly serviceToken?: string;
33
+ private readonly timeout: number;
34
+
35
+ private cachedCredentials: STSCredentials | null = null;
36
+ private credentialsExpiresAt: Date | null = null;
37
+
38
+ constructor(config: OSSClientConfig) {
39
+ this.bffBaseUrl = config.bffBaseUrl.replace(/\/$/, '');
40
+ this.serviceToken = config.serviceToken;
41
+ this.timeout = config.timeout ?? 30000;
42
+ }
43
+
44
+ /**
45
+ * Get STS credentials from BFF API
46
+ * Caches credentials until they expire (with 5 minute buffer)
47
+ *
48
+ * Uses the /studio/service/sts endpoint which requires BFF_SERVICE_TOKEN
49
+ * (set to SUPABASE_JWT_SECRET) for service-to-service authentication.
50
+ */
51
+ async getSTSCredentials(): Promise<STSCredentials> {
52
+ // Check if cached credentials are still valid (with 5 minute buffer)
53
+ if (this.cachedCredentials && this.credentialsExpiresAt) {
54
+ const bufferMs = 5 * 60 * 1000; // 5 minutes
55
+ if (new Date().getTime() + bufferMs < this.credentialsExpiresAt.getTime()) {
56
+ return this.cachedCredentials;
57
+ }
58
+ }
59
+
60
+ if (!this.serviceToken) {
61
+ throw new OSSClientError(
62
+ 'BFF_SERVICE_TOKEN is required for STS credentials. Set it to the value of SUPABASE_JWT_SECRET from BFF.',
63
+ 'SERVICE_TOKEN_MISSING'
64
+ );
65
+ }
66
+
67
+ console.log('[OSSClient] Requesting STS credentials from BFF...');
68
+
69
+ const controller = new AbortController();
70
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
71
+
72
+ try {
73
+ const headers: Record<string, string> = {
74
+ 'Content-Type': 'application/json',
75
+ 'Authorization': `Bearer ${this.serviceToken}`,
76
+ };
77
+
78
+ // Use the service-to-service endpoint
79
+ const response = await fetch(`${this.bffBaseUrl}/studio/service/sts`, {
80
+ method: 'POST',
81
+ headers,
82
+ signal: controller.signal,
83
+ });
84
+
85
+ clearTimeout(timeoutId);
86
+
87
+ if (!response.ok) {
88
+ let detail = response.statusText;
89
+ try {
90
+ const error = await response.json();
91
+ detail = error.detail || error.message || response.statusText;
92
+ } catch {
93
+ // Ignore JSON parse error
94
+ }
95
+ throw new OSSClientError(
96
+ `Failed to get STS credentials: ${detail}`,
97
+ 'STS_REQUEST_FAILED',
98
+ response.status
99
+ );
100
+ }
101
+
102
+ const data = await response.json();
103
+
104
+ // Extract credentials from response
105
+ // BFF /studio/service/sts returns: { credentials: { accessKeyId, ... }, bucket, region }
106
+ const creds = data.credentials || data;
107
+ const credentials: STSCredentials = {
108
+ accessKeyId: creds.accessKeyId || creds.access_key_id,
109
+ accessKeySecret: creds.accessKeySecret || creds.access_key_secret,
110
+ securityToken: creds.securityToken || creds.security_token,
111
+ expiration: creds.expiration,
112
+ bucket: data.bucket,
113
+ region: data.region,
114
+ };
115
+
116
+ // Validate credentials
117
+ if (!credentials.accessKeyId || !credentials.accessKeySecret || !credentials.securityToken) {
118
+ throw new OSSClientError(
119
+ 'Invalid STS credentials response from BFF',
120
+ 'INVALID_STS_RESPONSE'
121
+ );
122
+ }
123
+
124
+ // Validate bucket and region
125
+ if (!credentials.bucket || !credentials.region) {
126
+ throw new OSSClientError(
127
+ 'Invalid STS credentials response from BFF: missing bucket or region',
128
+ 'INVALID_STS_RESPONSE'
129
+ );
130
+ }
131
+
132
+ // Cache credentials
133
+ this.cachedCredentials = credentials;
134
+ this.credentialsExpiresAt = new Date(credentials.expiration);
135
+
136
+ console.log('[OSSClient] STS credentials obtained, expires:', credentials.expiration);
137
+
138
+ return credentials;
139
+ } catch (error) {
140
+ clearTimeout(timeoutId);
141
+
142
+ if (error instanceof OSSClientError) {
143
+ throw error;
144
+ }
145
+
146
+ if (error instanceof Error && error.name === 'AbortError') {
147
+ throw new OSSClientError(
148
+ 'STS credentials request timeout',
149
+ 'STS_TIMEOUT',
150
+ 408
151
+ );
152
+ }
153
+
154
+ throw new OSSClientError(
155
+ `Failed to get STS credentials: ${error instanceof Error ? error.message : String(error)}`,
156
+ 'STS_REQUEST_ERROR'
157
+ );
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Download file from OSS URL with STS authentication
163
+ *
164
+ * @param url - OSS URL to download from
165
+ * @param destPath - Local destination path
166
+ * @param onProgress - Optional progress callback (0-100)
167
+ */
168
+ async downloadFile(
169
+ url: string,
170
+ destPath: string,
171
+ onProgress?: (percent: number) => void
172
+ ): Promise<void> {
173
+ console.log(`[OSSClient] Downloading ${url} to ${destPath}`);
174
+
175
+ // Get STS credentials
176
+ const credentials = await this.getSTSCredentials();
177
+
178
+ if (!credentials.bucket || !credentials.region) {
179
+ throw new OSSClientError(
180
+ 'Missing bucket or region in STS credentials',
181
+ 'INVALID_CREDENTIALS'
182
+ );
183
+ }
184
+
185
+ // Parse URL to extract object key
186
+ // URL format: https://bucket.oss-region.aliyuncs.com/path/to/object
187
+ // or: https://oss-region.aliyuncs.com/bucket/path/to/object
188
+ const parsedUrl = new URL(url);
189
+ let objectKey = parsedUrl.pathname.slice(1); // Remove leading /
190
+
191
+ // Handle case where bucket is in the path (not subdomain)
192
+ if (parsedUrl.hostname.includes('aliyuncs.com') && !parsedUrl.hostname.startsWith(credentials.bucket)) {
193
+ // Format: https://oss-region.aliyuncs.com/bucket/path/to/object
194
+ const pathParts = objectKey.split('/');
195
+ if (pathParts[0] === credentials.bucket) {
196
+ objectKey = pathParts.slice(1).join('/');
197
+ }
198
+ }
199
+
200
+ console.log(`[OSSClient] Extracted object key: ${objectKey}`);
201
+
202
+ try {
203
+ // Create OSS client with STS credentials
204
+ const client = new OSS({
205
+ accessKeyId: credentials.accessKeyId,
206
+ accessKeySecret: credentials.accessKeySecret,
207
+ stsToken: credentials.securityToken,
208
+ bucket: credentials.bucket,
209
+ region: credentials.region,
210
+ timeout: 10 * 60 * 1000, // 10 minutes
211
+ });
212
+
213
+ // Ensure destination directory exists
214
+ const destDir = path.dirname(destPath);
215
+ fs.mkdirSync(destDir, { recursive: true });
216
+
217
+ // Download file
218
+ // Note: ali-oss client.get() for Node.js doesn't support progress callbacks
219
+ // Progress tracking would require streaming the response manually
220
+ const result = await client.get(objectKey, destPath, {
221
+ timeout: 10 * 60 * 1000,
222
+ });
223
+
224
+ if (onProgress) {
225
+ onProgress(100);
226
+ }
227
+
228
+ console.log(`[OSSClient] Download complete: ${destPath}`);
229
+ } catch (error: any) {
230
+ // Clean up partial download
231
+ if (fs.existsSync(destPath)) {
232
+ try {
233
+ fs.unlinkSync(destPath);
234
+ } catch {
235
+ // Ignore cleanup error
236
+ }
237
+ }
238
+
239
+ // Handle OSS SDK errors
240
+ if (error.code === 'ConnectionTimeoutError' || error.code === 'RequestTimeout') {
241
+ throw new OSSClientError(
242
+ 'Download timeout',
243
+ 'DOWNLOAD_TIMEOUT',
244
+ 408
245
+ );
246
+ }
247
+
248
+ if (error.status === 403 || error.code === 'AccessDenied') {
249
+ throw new OSSClientError(
250
+ 'Access denied - check STS credentials and permissions',
251
+ 'DOWNLOAD_FORBIDDEN',
252
+ 403
253
+ );
254
+ }
255
+
256
+ if (error.status === 404 || error.code === 'NoSuchKey') {
257
+ throw new OSSClientError(
258
+ `Object not found: ${objectKey}`,
259
+ 'OBJECT_NOT_FOUND',
260
+ 404
261
+ );
262
+ }
263
+
264
+ throw new OSSClientError(
265
+ `Download failed: ${error.message || String(error)}`,
266
+ 'DOWNLOAD_ERROR',
267
+ error.status
268
+ );
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Clear cached credentials
274
+ */
275
+ clearCredentialsCache(): void {
276
+ this.cachedCredentials = null;
277
+ this.credentialsExpiresAt = null;
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Create an OSS client instance
283
+ */
284
+ export function createOSSClient(config: OSSClientConfig): OSSClient {
285
+ return new OSSClient(config);
286
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Services exports
3
+ */
4
+
5
+ export * from './types.js';
6
+ export * from './OSSClient.js';
7
+ export * from './BundleManager.js';
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Shared types for replay-server services
3
+ */
4
+
5
+ /**
6
+ * STS credentials from BFF API for OSS access
7
+ */
8
+ export interface STSCredentials {
9
+ accessKeyId: string;
10
+ accessKeySecret: string;
11
+ securityToken: string;
12
+ expiration: string;
13
+ bucket?: string;
14
+ region?: string;
15
+ }
16
+
17
+ /**
18
+ * OSS client configuration
19
+ */
20
+ export interface OSSClientConfig {
21
+ /** BFF API base URL for STS credentials */
22
+ bffBaseUrl: string;
23
+ /** Optional service-to-service auth token */
24
+ serviceToken?: string;
25
+ /** Request timeout in milliseconds (default: 30000) */
26
+ timeout?: number;
27
+ }
28
+
29
+ /**
30
+ * Bundle information
31
+ */
32
+ export interface BundleInfo {
33
+ bundleId: string;
34
+ bundleUrl?: string;
35
+ status: 'pending' | 'downloading' | 'ready' | 'error';
36
+ size?: number;
37
+ cachedAt?: Date;
38
+ lastAccessedAt?: Date;
39
+ error?: string;
40
+ /** Download progress percentage (0-100) */
41
+ progress?: number;
42
+ }
43
+
44
+ /**
45
+ * Bundle manager configuration
46
+ */
47
+ export interface BundleManagerConfig {
48
+ /** Directory to store bundles (default: /tmp/bundles) */
49
+ bundlesDir: string;
50
+ /** Maximum cache size in bytes (default: 10GB) */
51
+ maxCacheSize: number;
52
+ }
53
+
54
+ /**
55
+ * Cache statistics
56
+ */
57
+ export interface CacheStats {
58
+ /** Used cache size in bytes */
59
+ used: number;
60
+ /** Maximum cache size in bytes */
61
+ max: number;
62
+ /** Usage percentage (0-100) */
63
+ usedPercent: number;
64
+ /** Number of cached bundles */
65
+ bundleCount: number;
66
+ }
67
+
68
+ /**
69
+ * Download statistics
70
+ */
71
+ export interface DownloadStats {
72
+ /** Number of active downloads */
73
+ active: number;
74
+ /** Total completed downloads */
75
+ completed: number;
76
+ /** Total failed downloads */
77
+ failed: number;
78
+ }