@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.
- package/.cursor/dev.mdc +941 -0
- package/.cursor/project.mdc +17 -2
- package/.env +30 -0
- package/Dockerfile +6 -0
- package/README.md +297 -27
- package/dist/cli/render.js +14 -4
- package/dist/cli/render.js.map +1 -1
- package/dist/renderer/PuppeteerRenderer.d.ts +28 -2
- package/dist/renderer/PuppeteerRenderer.d.ts.map +1 -1
- package/dist/renderer/PuppeteerRenderer.js +134 -36
- 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 +10 -0
- package/env.example +30 -0
- package/package.json +7 -3
- package/restart.sh +5 -0
- package/samples/jump_arena_5_ja-mksi5fku-qgk5iq.json +1952 -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 +192 -39
- 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,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,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
|
+
}
|