@modelprofile.com/browser-runtime 1.0.1 → 2.1.0
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/.smartconfig.json +1 -1
- package/changelog.md +19 -0
- package/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/actions.js +12 -3
- package/dist_ts/classes.artifactstore.d.ts +16 -7
- package/dist_ts/classes.artifactstore.js +167 -88
- package/dist_ts/classes.egressproxy.d.ts +2 -0
- package/dist_ts/classes.egressproxy.js +7 -2
- package/dist_ts/classes.flexprovider.js +4 -2
- package/dist_ts/classes.framed.d.ts +30 -3
- package/dist_ts/classes.framed.js +205 -30
- package/dist_ts/classes.runtime.d.ts +86 -22
- package/dist_ts/classes.runtime.js +1060 -230
- package/dist_ts/errors.d.ts +1 -1
- package/dist_ts/errors.js +6 -3
- package/dist_ts/index.d.ts +2 -2
- package/dist_ts/interfaces.d.ts +119 -47
- package/dist_ts/internal.testing.d.ts +10 -1
- package/dist_ts/internal.testing.js +1 -1
- package/dist_ts/mcp.js +45 -11
- package/package.json +2 -2
- package/readme.hints.md +23 -18
- package/readme.md +72 -101
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/actions.ts +11 -2
- package/ts/classes.artifactstore.ts +239 -107
- package/ts/classes.egressproxy.ts +12 -1
- package/ts/classes.flexprovider.ts +5 -1
- package/ts/classes.framed.ts +246 -38
- package/ts/classes.runtime.ts +1350 -320
- package/ts/errors.ts +8 -2
- package/ts/index.ts +19 -6
- package/ts/interfaces.ts +151 -45
- package/ts/internal.testing.ts +12 -0
- package/ts/mcp.ts +67 -16
|
@@ -2,6 +2,7 @@ import * as plugins from './plugins.js';
|
|
|
2
2
|
import type {
|
|
3
3
|
IBrowserArtifactMetadata,
|
|
4
4
|
IBrowserArtifactStoreOptions,
|
|
5
|
+
IBrowserResourceKey,
|
|
5
6
|
} from './interfaces.js';
|
|
6
7
|
import { BrowserRuntimeError } from './errors.js';
|
|
7
8
|
import {
|
|
@@ -16,15 +17,25 @@ interface IStoredArtifact {
|
|
|
16
17
|
digest: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
interface
|
|
20
|
+
interface IResourceArtifacts {
|
|
21
|
+
browserResourceId: string;
|
|
20
22
|
directoryName: string;
|
|
21
23
|
totalBytes: number;
|
|
22
24
|
artifacts: Map<string, IStoredArtifact>;
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
interface IProjectArtifacts {
|
|
28
|
+
directoryName: string;
|
|
29
|
+
totalBytes: number;
|
|
30
|
+
totalArtifacts: number;
|
|
31
|
+
resources: Map<string, IResourceArtifacts>;
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
export class BrowserArtifactStore {
|
|
26
35
|
private readonly rootDirectory: string;
|
|
27
36
|
private readonly maxFileBytes: number;
|
|
37
|
+
private readonly maxArtifactsPerResource: number;
|
|
38
|
+
private readonly maxResourceBytes: number;
|
|
28
39
|
private readonly maxArtifactsPerProject: number;
|
|
29
40
|
private readonly maxProjectBytes: number;
|
|
30
41
|
private readonly maxProjects: number;
|
|
@@ -55,38 +66,46 @@ export class BrowserArtifactStore {
|
|
|
55
66
|
64 * 1024 * 1024,
|
|
56
67
|
8 * 1024 * 1024,
|
|
57
68
|
);
|
|
58
|
-
this.
|
|
59
|
-
options.
|
|
60
|
-
'
|
|
69
|
+
this.maxArtifactsPerResource = validateOptionalInteger(
|
|
70
|
+
options.maxArtifactsPerResource,
|
|
71
|
+
'maxArtifactsPerResource',
|
|
61
72
|
1,
|
|
62
73
|
4096,
|
|
63
74
|
256,
|
|
64
75
|
);
|
|
65
|
-
this.
|
|
66
|
-
options.
|
|
67
|
-
'
|
|
76
|
+
this.maxResourceBytes = validateOptionalInteger(
|
|
77
|
+
options.maxResourceBytes,
|
|
78
|
+
'maxResourceBytes',
|
|
68
79
|
this.maxFileBytes,
|
|
69
80
|
1024 * 1024 * 1024,
|
|
70
81
|
64 * 1024 * 1024,
|
|
71
82
|
);
|
|
72
|
-
this.
|
|
73
|
-
options.
|
|
74
|
-
'
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
83
|
+
this.maxArtifactsPerProject = validateOptionalInteger(
|
|
84
|
+
options.maxArtifactsPerProject,
|
|
85
|
+
'maxArtifactsPerProject',
|
|
86
|
+
this.maxArtifactsPerResource,
|
|
87
|
+
65_536,
|
|
88
|
+
Math.max(1024, this.maxArtifactsPerResource),
|
|
78
89
|
);
|
|
90
|
+
this.maxProjectBytes = validateOptionalInteger(
|
|
91
|
+
options.maxProjectBytes,
|
|
92
|
+
'maxProjectBytes',
|
|
93
|
+
this.maxResourceBytes,
|
|
94
|
+
8 * 1024 * 1024 * 1024,
|
|
95
|
+
Math.max(256 * 1024 * 1024, this.maxResourceBytes),
|
|
96
|
+
);
|
|
97
|
+
this.maxProjects = validateOptionalInteger(options.maxProjects, 'maxProjects', 1, 256, 32);
|
|
79
98
|
this.maxTotalArtifacts = validateOptionalInteger(
|
|
80
99
|
options.maxTotalArtifacts,
|
|
81
100
|
'maxTotalArtifacts',
|
|
82
|
-
|
|
101
|
+
1,
|
|
83
102
|
65_536,
|
|
84
103
|
Math.max(4096, this.maxArtifactsPerProject),
|
|
85
104
|
);
|
|
86
105
|
this.maxTotalBytes = validateOptionalInteger(
|
|
87
106
|
options.maxTotalBytes,
|
|
88
107
|
'maxTotalBytes',
|
|
89
|
-
this.
|
|
108
|
+
this.maxFileBytes,
|
|
90
109
|
8 * 1024 * 1024 * 1024,
|
|
91
110
|
Math.max(512 * 1024 * 1024, this.maxProjectBytes),
|
|
92
111
|
);
|
|
@@ -104,9 +123,7 @@ export class BrowserArtifactStore {
|
|
|
104
123
|
public start(): Promise<void> {
|
|
105
124
|
if (this.started) return Promise.resolve();
|
|
106
125
|
if (this.startPromise) return this.startPromise;
|
|
107
|
-
this.startPromise = this.startInternal().finally(() => {
|
|
108
|
-
this.startPromise = undefined;
|
|
109
|
-
});
|
|
126
|
+
this.startPromise = this.startInternal().finally(() => { this.startPromise = undefined; });
|
|
110
127
|
return this.startPromise;
|
|
111
128
|
}
|
|
112
129
|
|
|
@@ -127,19 +144,19 @@ export class BrowserArtifactStore {
|
|
|
127
144
|
} finally {
|
|
128
145
|
release();
|
|
129
146
|
}
|
|
130
|
-
})().finally(() => {
|
|
131
|
-
this.stopPromise = undefined;
|
|
132
|
-
});
|
|
147
|
+
})().finally(() => { this.stopPromise = undefined; });
|
|
133
148
|
return this.stopPromise;
|
|
134
149
|
}
|
|
135
150
|
|
|
136
151
|
public async store(
|
|
137
152
|
projectIdArg: string,
|
|
153
|
+
browserResourceIdArg: string,
|
|
138
154
|
mimeTypeArg: string,
|
|
139
155
|
data: Uint8Array,
|
|
140
156
|
): Promise<IBrowserArtifactMetadata> {
|
|
141
157
|
this.requireStarted();
|
|
142
|
-
const projectId =
|
|
158
|
+
const projectId = this.validateProjectId(projectIdArg);
|
|
159
|
+
const browserResourceId = this.validateResourceId(browserResourceIdArg);
|
|
143
160
|
const mimeType = validateBoundedString(mimeTypeArg, 'mimeType', 1, 128);
|
|
144
161
|
if (!(data instanceof Uint8Array)) {
|
|
145
162
|
throw new BrowserRuntimeError('INVALID_INPUT', 'data must be Uint8Array');
|
|
@@ -149,7 +166,7 @@ export class BrowserArtifactStore {
|
|
|
149
166
|
const release = await this.mutex.acquire();
|
|
150
167
|
try {
|
|
151
168
|
this.requireStarted();
|
|
152
|
-
await this.purgeExpiredInternal(projectId);
|
|
169
|
+
await this.purgeExpiredInternal({ projectId, browserResourceId });
|
|
153
170
|
const existingProject = this.projects.get(projectId);
|
|
154
171
|
if (!existingProject && this.projects.size >= this.maxProjects) {
|
|
155
172
|
throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
@@ -160,15 +177,22 @@ export class BrowserArtifactStore {
|
|
|
160
177
|
) throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
161
178
|
const project = existingProject ?? await this.createProject(projectId);
|
|
162
179
|
if (
|
|
163
|
-
project.
|
|
180
|
+
project.totalArtifacts >= this.maxArtifactsPerProject
|
|
164
181
|
|| project.totalBytes + bytes.byteLength > this.maxProjectBytes
|
|
165
182
|
) throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
183
|
+
const resource = project.resources.get(browserResourceId)
|
|
184
|
+
?? await this.createResource(project, projectId, browserResourceId);
|
|
185
|
+
if (
|
|
186
|
+
resource.artifacts.size >= this.maxArtifactsPerResource
|
|
187
|
+
|| resource.totalBytes + bytes.byteLength > this.maxResourceBytes
|
|
188
|
+
) throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
166
189
|
|
|
167
190
|
const artifactId = randomId();
|
|
168
191
|
const createdAt = this.now();
|
|
169
192
|
const metadata: IBrowserArtifactMetadata = {
|
|
170
193
|
artifactId,
|
|
171
194
|
projectId,
|
|
195
|
+
browserResourceId,
|
|
172
196
|
mimeType,
|
|
173
197
|
size: bytes.byteLength,
|
|
174
198
|
createdAt,
|
|
@@ -178,9 +202,9 @@ export class BrowserArtifactStore {
|
|
|
178
202
|
metadata,
|
|
179
203
|
digest: plugins.crypto.createHash('sha256').update(bytes).digest('hex'),
|
|
180
204
|
};
|
|
181
|
-
const
|
|
182
|
-
const finalPath = plugins.path.join(
|
|
183
|
-
const temporaryPath = plugins.path.join(
|
|
205
|
+
const resourceDirectory = this.resourcePath(project, resource);
|
|
206
|
+
const finalPath = plugins.path.join(resourceDirectory, artifactId);
|
|
207
|
+
const temporaryPath = plugins.path.join(resourceDirectory, `.${randomId(12)}.tmp`);
|
|
184
208
|
let handle: plugins.fsPromises.FileHandle | undefined;
|
|
185
209
|
let renamed = false;
|
|
186
210
|
try {
|
|
@@ -192,7 +216,9 @@ export class BrowserArtifactStore {
|
|
|
192
216
|
await plugins.fsPromises.rename(temporaryPath, finalPath);
|
|
193
217
|
renamed = true;
|
|
194
218
|
await plugins.fsPromises.chmod(finalPath, 0o600);
|
|
195
|
-
|
|
219
|
+
resource.artifacts.set(artifactId, stored);
|
|
220
|
+
resource.totalBytes += bytes.byteLength;
|
|
221
|
+
project.totalArtifacts += 1;
|
|
196
222
|
project.totalBytes += bytes.byteLength;
|
|
197
223
|
this.totalArtifacts += 1;
|
|
198
224
|
this.totalBytes += bytes.byteLength;
|
|
@@ -201,7 +227,9 @@ export class BrowserArtifactStore {
|
|
|
201
227
|
await handle?.close().catch(() => undefined);
|
|
202
228
|
await plugins.fsPromises.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
203
229
|
if (renamed) await plugins.fsPromises.rm(finalPath, { force: true }).catch(() => undefined);
|
|
204
|
-
if (
|
|
230
|
+
if (resource.artifacts.size === 0) {
|
|
231
|
+
await this.removeEmptyResource(projectId, project, browserResourceId, resource);
|
|
232
|
+
}
|
|
205
233
|
throw error;
|
|
206
234
|
}
|
|
207
235
|
} finally {
|
|
@@ -209,27 +237,36 @@ export class BrowserArtifactStore {
|
|
|
209
237
|
}
|
|
210
238
|
}
|
|
211
239
|
|
|
212
|
-
public async read(
|
|
240
|
+
public async read(
|
|
241
|
+
projectIdArg: string,
|
|
242
|
+
browserResourceIdArg: string,
|
|
243
|
+
artifactIdArg: string,
|
|
244
|
+
): Promise<Uint8Array> {
|
|
213
245
|
this.requireStarted();
|
|
214
|
-
const projectId =
|
|
215
|
-
const
|
|
246
|
+
const projectId = this.validateProjectId(projectIdArg);
|
|
247
|
+
const browserResourceId = this.validateResourceId(browserResourceIdArg);
|
|
248
|
+
const artifactId = this.validateArtifactId(artifactIdArg);
|
|
216
249
|
const release = await this.mutex.acquire();
|
|
217
250
|
let handle: plugins.fsPromises.FileHandle | undefined;
|
|
218
251
|
try {
|
|
219
|
-
const
|
|
220
|
-
if (stored.metadata.expiresAt <= this.now()) {
|
|
221
|
-
await this.deleteArtifact(projectId,
|
|
252
|
+
const resolved = this.resolveArtifact(projectId, browserResourceId, artifactId);
|
|
253
|
+
if (resolved.stored.metadata.expiresAt <= this.now()) {
|
|
254
|
+
await this.deleteArtifact(projectId, browserResourceId, resolved);
|
|
222
255
|
throw new BrowserRuntimeError('INVALID_INPUT', 'artifact is unavailable');
|
|
223
256
|
}
|
|
224
257
|
handle = await plugins.fsPromises.open(
|
|
225
|
-
filePath,
|
|
258
|
+
resolved.filePath,
|
|
226
259
|
plugins.fs.constants.O_RDONLY | plugins.fs.constants.O_NOFOLLOW,
|
|
227
260
|
);
|
|
228
261
|
const stat = await handle.stat();
|
|
229
|
-
if (
|
|
262
|
+
if (
|
|
263
|
+
!stat.isFile()
|
|
264
|
+
|| stat.size !== resolved.stored.metadata.size
|
|
265
|
+
|| stat.size > this.maxFileBytes
|
|
266
|
+
) {
|
|
230
267
|
await handle.close();
|
|
231
268
|
handle = undefined;
|
|
232
|
-
await this.deleteArtifact(projectId,
|
|
269
|
+
await this.deleteArtifact(projectId, browserResourceId, resolved);
|
|
233
270
|
throw new BrowserRuntimeError('FENCED', 'artifact integrity check failed');
|
|
234
271
|
}
|
|
235
272
|
const data = plugins.Buffer.allocUnsafe(stat.size);
|
|
@@ -237,8 +274,8 @@ export class BrowserArtifactStore {
|
|
|
237
274
|
await handle.close();
|
|
238
275
|
handle = undefined;
|
|
239
276
|
const digest = plugins.crypto.createHash('sha256').update(data).digest('hex');
|
|
240
|
-
if (bytesRead !== stat.size || digest !== stored.digest) {
|
|
241
|
-
await this.deleteArtifact(projectId,
|
|
277
|
+
if (bytesRead !== stat.size || digest !== resolved.stored.digest) {
|
|
278
|
+
await this.deleteArtifact(projectId, browserResourceId, resolved);
|
|
242
279
|
throw new BrowserRuntimeError('FENCED', 'artifact integrity check failed');
|
|
243
280
|
}
|
|
244
281
|
return new Uint8Array(data);
|
|
@@ -248,72 +285,90 @@ export class BrowserArtifactStore {
|
|
|
248
285
|
}
|
|
249
286
|
}
|
|
250
287
|
|
|
251
|
-
public async delete(
|
|
288
|
+
public async delete(
|
|
289
|
+
projectIdArg: string,
|
|
290
|
+
browserResourceIdArg: string,
|
|
291
|
+
artifactIdArg: string,
|
|
292
|
+
): Promise<void> {
|
|
252
293
|
this.requireStarted();
|
|
253
|
-
const projectId =
|
|
254
|
-
const
|
|
294
|
+
const projectId = this.validateProjectId(projectIdArg);
|
|
295
|
+
const browserResourceId = this.validateResourceId(browserResourceIdArg);
|
|
296
|
+
const artifactId = this.validateArtifactId(artifactIdArg);
|
|
255
297
|
const release = await this.mutex.acquire();
|
|
256
298
|
try {
|
|
257
|
-
|
|
258
|
-
|
|
299
|
+
await this.deleteArtifact(
|
|
300
|
+
projectId,
|
|
301
|
+
browserResourceId,
|
|
302
|
+
this.resolveArtifact(projectId, browserResourceId, artifactId),
|
|
303
|
+
);
|
|
259
304
|
} finally {
|
|
260
305
|
release();
|
|
261
306
|
}
|
|
262
307
|
}
|
|
263
308
|
|
|
264
|
-
public async purgeExpired(
|
|
309
|
+
public async purgeExpired(resource?: IBrowserResourceKey): Promise<number> {
|
|
265
310
|
this.requireStarted();
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
:
|
|
311
|
+
const validated = resource === undefined ? undefined : {
|
|
312
|
+
projectId: this.validateProjectId(resource.projectId),
|
|
313
|
+
browserResourceId: this.validateResourceId(resource.browserResourceId),
|
|
314
|
+
};
|
|
269
315
|
const release = await this.mutex.acquire();
|
|
270
316
|
try {
|
|
271
|
-
return await this.purgeExpiredInternal(
|
|
317
|
+
return await this.purgeExpiredInternal(validated);
|
|
272
318
|
} finally {
|
|
273
319
|
release();
|
|
274
320
|
}
|
|
275
321
|
}
|
|
276
322
|
|
|
277
|
-
public async
|
|
323
|
+
public async purgeResource(projectIdArg: string, browserResourceIdArg: string): Promise<void> {
|
|
278
324
|
this.requireStarted();
|
|
279
|
-
const projectId =
|
|
325
|
+
const projectId = this.validateProjectId(projectIdArg);
|
|
326
|
+
const browserResourceId = this.validateResourceId(browserResourceIdArg);
|
|
280
327
|
const release = await this.mutex.acquire();
|
|
281
328
|
try {
|
|
282
329
|
const project = this.projects.get(projectId);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
330
|
+
const resource = project?.resources.get(browserResourceId);
|
|
331
|
+
if (!project || !resource) return;
|
|
332
|
+
await plugins.fsPromises.rm(this.resourcePath(project, resource), {
|
|
333
|
+
recursive: true,
|
|
334
|
+
force: true,
|
|
335
|
+
});
|
|
336
|
+
project.totalArtifacts -= resource.artifacts.size;
|
|
337
|
+
project.totalBytes -= resource.totalBytes;
|
|
338
|
+
this.totalArtifacts -= resource.artifacts.size;
|
|
339
|
+
this.totalBytes -= resource.totalBytes;
|
|
340
|
+
project.resources.delete(browserResourceId);
|
|
341
|
+
await this.removeEmptyProject(projectId, project);
|
|
291
342
|
} finally {
|
|
292
343
|
release();
|
|
293
344
|
}
|
|
294
345
|
}
|
|
295
346
|
|
|
296
|
-
public getMetadata(
|
|
347
|
+
public getMetadata(
|
|
348
|
+
projectIdArg: string,
|
|
349
|
+
browserResourceIdArg: string,
|
|
350
|
+
artifactIdArg: string,
|
|
351
|
+
): IBrowserArtifactMetadata {
|
|
297
352
|
this.requireStarted();
|
|
298
|
-
const projectId =
|
|
299
|
-
const
|
|
300
|
-
const
|
|
301
|
-
const
|
|
302
|
-
if (
|
|
353
|
+
const projectId = this.validateProjectId(projectIdArg);
|
|
354
|
+
const browserResourceId = this.validateResourceId(browserResourceIdArg);
|
|
355
|
+
const artifactId = this.validateArtifactId(artifactIdArg);
|
|
356
|
+
const resolved = this.resolveArtifact(projectId, browserResourceId, artifactId);
|
|
357
|
+
if (resolved.stored.metadata.expiresAt <= this.now()) {
|
|
303
358
|
throw new BrowserRuntimeError('INVALID_INPUT', 'artifact is unavailable');
|
|
304
359
|
}
|
|
305
360
|
try {
|
|
306
|
-
const stat = plugins.fs.lstatSync(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
361
|
+
const stat = plugins.fs.lstatSync(resolved.filePath);
|
|
362
|
+
if (
|
|
363
|
+
stat.isSymbolicLink()
|
|
364
|
+
|| !stat.isFile()
|
|
365
|
+
|| stat.size !== resolved.stored.metadata.size
|
|
366
|
+
) throw new BrowserRuntimeError('FENCED', 'artifact integrity check failed');
|
|
312
367
|
} catch (error) {
|
|
313
368
|
if (error instanceof BrowserRuntimeError) throw error;
|
|
314
369
|
throw new BrowserRuntimeError('FENCED', 'artifact integrity check failed');
|
|
315
370
|
}
|
|
316
|
-
return { ...stored.metadata };
|
|
371
|
+
return { ...resolved.stored.metadata };
|
|
317
372
|
}
|
|
318
373
|
|
|
319
374
|
private async startInternal(): Promise<void> {
|
|
@@ -337,8 +392,6 @@ export class BrowserArtifactStore {
|
|
|
337
392
|
|| (uid !== undefined && stat.uid !== uid)
|
|
338
393
|
|| (stat.mode & 0o777) !== 0o700
|
|
339
394
|
) throw new BrowserRuntimeError('FENCED', 'artifact root is not private');
|
|
340
|
-
const entries = await plugins.fsPromises.readdir(this.rootDirectory);
|
|
341
|
-
if (entries.length > 0) throw new BrowserRuntimeError('FENCED', 'artifact root is not empty');
|
|
342
395
|
await plugins.fsPromises.chmod(this.rootDirectory, 0o700);
|
|
343
396
|
this.started = true;
|
|
344
397
|
const sweepIntervalMs = Math.min(60_000, Math.max(1000, Math.floor(this.artifactTtlMs / 2)));
|
|
@@ -357,32 +410,63 @@ export class BrowserArtifactStore {
|
|
|
357
410
|
}
|
|
358
411
|
|
|
359
412
|
private async createProject(projectId: string): Promise<IProjectArtifacts> {
|
|
360
|
-
const directoryName =
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
413
|
+
const directoryName = this.digestDirectory(`project\0${projectId}`);
|
|
414
|
+
await plugins.fsPromises.mkdir(plugins.path.join(this.rootDirectory, directoryName), {
|
|
415
|
+
recursive: false,
|
|
416
|
+
mode: 0o700,
|
|
417
|
+
});
|
|
418
|
+
const project: IProjectArtifacts = {
|
|
419
|
+
directoryName,
|
|
420
|
+
totalBytes: 0,
|
|
421
|
+
totalArtifacts: 0,
|
|
422
|
+
resources: new Map(),
|
|
423
|
+
};
|
|
367
424
|
this.projects.set(projectId, project);
|
|
368
425
|
return project;
|
|
369
426
|
}
|
|
370
427
|
|
|
371
|
-
private async
|
|
372
|
-
|
|
428
|
+
private async createResource(
|
|
429
|
+
project: IProjectArtifacts,
|
|
430
|
+
projectId: string,
|
|
431
|
+
browserResourceId: string,
|
|
432
|
+
): Promise<IResourceArtifacts> {
|
|
433
|
+
const directoryName = this.digestDirectory(`resource\0${projectId}\0${browserResourceId}`);
|
|
434
|
+
await plugins.fsPromises.mkdir(
|
|
435
|
+
plugins.path.join(this.rootDirectory, project.directoryName, directoryName),
|
|
436
|
+
{ recursive: false, mode: 0o700 },
|
|
437
|
+
);
|
|
438
|
+
const resource: IResourceArtifacts = {
|
|
439
|
+
browserResourceId,
|
|
440
|
+
directoryName,
|
|
441
|
+
totalBytes: 0,
|
|
442
|
+
artifacts: new Map(),
|
|
443
|
+
};
|
|
444
|
+
project.resources.set(browserResourceId, resource);
|
|
445
|
+
return resource;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private async purgeExpiredInternal(resourceKey?: IBrowserResourceKey): Promise<number> {
|
|
449
|
+
const projectEntries = resourceKey
|
|
450
|
+
? [[resourceKey.projectId, this.projects.get(resourceKey.projectId)] as const]
|
|
451
|
+
: [...this.projects.entries()];
|
|
373
452
|
let purged = 0;
|
|
374
|
-
for (const
|
|
375
|
-
const project = this.projects.get(currentProjectId);
|
|
453
|
+
for (const [projectId, project] of projectEntries) {
|
|
376
454
|
if (!project) continue;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
)
|
|
384
|
-
|
|
385
|
-
|
|
455
|
+
const resourceEntries = resourceKey
|
|
456
|
+
? [[resourceKey.browserResourceId, project.resources.get(resourceKey.browserResourceId)] as const]
|
|
457
|
+
: [...project.resources.entries()];
|
|
458
|
+
for (const [browserResourceId, resource] of resourceEntries) {
|
|
459
|
+
if (!resource) continue;
|
|
460
|
+
for (const stored of [...resource.artifacts.values()]) {
|
|
461
|
+
if (stored.metadata.expiresAt <= this.now()) {
|
|
462
|
+
await this.deleteArtifact(projectId, browserResourceId, {
|
|
463
|
+
project,
|
|
464
|
+
resource,
|
|
465
|
+
stored,
|
|
466
|
+
filePath: plugins.path.join(this.resourcePath(project, resource), stored.metadata.artifactId),
|
|
467
|
+
});
|
|
468
|
+
purged += 1;
|
|
469
|
+
}
|
|
386
470
|
}
|
|
387
471
|
}
|
|
388
472
|
}
|
|
@@ -391,21 +475,46 @@ export class BrowserArtifactStore {
|
|
|
391
475
|
|
|
392
476
|
private async deleteArtifact(
|
|
393
477
|
projectId: string,
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
filePath: string,
|
|
478
|
+
browserResourceId: string,
|
|
479
|
+
resolved: ReturnType<BrowserArtifactStore['resolveArtifact']>,
|
|
397
480
|
): Promise<void> {
|
|
398
|
-
await plugins.fsPromises.rm(filePath, { force: true });
|
|
399
|
-
if (
|
|
400
|
-
|
|
481
|
+
await plugins.fsPromises.rm(resolved.filePath, { force: true });
|
|
482
|
+
if (resolved.resource.artifacts.delete(resolved.stored.metadata.artifactId)) {
|
|
483
|
+
const size = resolved.stored.metadata.size;
|
|
484
|
+
resolved.resource.totalBytes = Math.max(0, resolved.resource.totalBytes - size);
|
|
485
|
+
resolved.project.totalArtifacts = Math.max(0, resolved.project.totalArtifacts - 1);
|
|
486
|
+
resolved.project.totalBytes = Math.max(0, resolved.project.totalBytes - size);
|
|
401
487
|
this.totalArtifacts = Math.max(0, this.totalArtifacts - 1);
|
|
402
|
-
this.totalBytes = Math.max(0, this.totalBytes -
|
|
488
|
+
this.totalBytes = Math.max(0, this.totalBytes - size);
|
|
403
489
|
}
|
|
490
|
+
await this.removeEmptyResource(
|
|
491
|
+
projectId,
|
|
492
|
+
resolved.project,
|
|
493
|
+
browserResourceId,
|
|
494
|
+
resolved.resource,
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private async removeEmptyResource(
|
|
499
|
+
projectId: string,
|
|
500
|
+
project: IProjectArtifacts,
|
|
501
|
+
browserResourceId: string,
|
|
502
|
+
resource: IResourceArtifacts,
|
|
503
|
+
): Promise<void> {
|
|
504
|
+
if (
|
|
505
|
+
resource.artifacts.size > 0
|
|
506
|
+
|| project.resources.get(browserResourceId) !== resource
|
|
507
|
+
) return;
|
|
508
|
+
await plugins.fsPromises.rm(this.resourcePath(project, resource), {
|
|
509
|
+
recursive: true,
|
|
510
|
+
force: true,
|
|
511
|
+
});
|
|
512
|
+
project.resources.delete(browserResourceId);
|
|
404
513
|
await this.removeEmptyProject(projectId, project);
|
|
405
514
|
}
|
|
406
515
|
|
|
407
516
|
private async removeEmptyProject(projectId: string, project: IProjectArtifacts): Promise<void> {
|
|
408
|
-
if (project.
|
|
517
|
+
if (project.resources.size > 0 || this.projects.get(projectId) !== project) return;
|
|
409
518
|
await plugins.fsPromises.rm(
|
|
410
519
|
plugins.path.join(this.rootDirectory, project.directoryName),
|
|
411
520
|
{ recursive: true, force: true },
|
|
@@ -413,20 +522,43 @@ export class BrowserArtifactStore {
|
|
|
413
522
|
this.projects.delete(projectId);
|
|
414
523
|
}
|
|
415
524
|
|
|
416
|
-
private resolveArtifact(projectId: string, artifactId: string): {
|
|
525
|
+
private resolveArtifact(projectId: string, browserResourceId: string, artifactId: string): {
|
|
417
526
|
project: IProjectArtifacts;
|
|
527
|
+
resource: IResourceArtifacts;
|
|
418
528
|
stored: IStoredArtifact;
|
|
419
529
|
filePath: string;
|
|
420
530
|
} {
|
|
421
531
|
const project = this.projects.get(projectId);
|
|
422
|
-
const
|
|
423
|
-
|
|
532
|
+
const resource = project?.resources.get(browserResourceId);
|
|
533
|
+
const stored = resource?.artifacts.get(artifactId);
|
|
534
|
+
if (!project || !resource || !stored) {
|
|
424
535
|
throw new BrowserRuntimeError('INVALID_INPUT', 'artifact is unavailable');
|
|
425
536
|
}
|
|
426
537
|
return {
|
|
427
538
|
project,
|
|
539
|
+
resource,
|
|
428
540
|
stored,
|
|
429
|
-
filePath: plugins.path.join(this.
|
|
541
|
+
filePath: plugins.path.join(this.resourcePath(project, resource), artifactId),
|
|
430
542
|
};
|
|
431
543
|
}
|
|
544
|
+
|
|
545
|
+
private resourcePath(project: IProjectArtifacts, resource: IResourceArtifacts): string {
|
|
546
|
+
return plugins.path.join(this.rootDirectory, project.directoryName, resource.directoryName);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private digestDirectory(value: string): string {
|
|
550
|
+
return plugins.crypto.createHmac('sha256', this.directoryKey).update(value, 'utf8').digest('hex');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private validateProjectId(value: unknown): string {
|
|
554
|
+
return validateBoundedString(value, 'projectId', 1, 256);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private validateResourceId(value: unknown): string {
|
|
558
|
+
return validateBoundedString(value, 'browserResourceId', 16, 256);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private validateArtifactId(value: unknown): string {
|
|
562
|
+
return validateBoundedString(value, 'artifactId', 1, 128);
|
|
563
|
+
}
|
|
432
564
|
}
|
|
@@ -38,6 +38,7 @@ const hopByHopHeaders = new Set([
|
|
|
38
38
|
|
|
39
39
|
export class BrowserEgressProxy {
|
|
40
40
|
private readonly projectId: string;
|
|
41
|
+
private readonly browserResourceIdValue: string;
|
|
41
42
|
private readonly allowedPorts: ReadonlySet<number>;
|
|
42
43
|
private readonly maxConnections: number;
|
|
43
44
|
private readonly maxActiveRequests: number;
|
|
@@ -69,7 +70,13 @@ export class BrowserEgressProxy {
|
|
|
69
70
|
private rejectedRequests = 0;
|
|
70
71
|
|
|
71
72
|
constructor(options: IBrowserEgressProxyOptions) {
|
|
72
|
-
this.projectId = validateBoundedString(options.projectId, 'projectId', 1,
|
|
73
|
+
this.projectId = validateBoundedString(options.projectId, 'projectId', 1, 256);
|
|
74
|
+
this.browserResourceIdValue = validateBoundedString(
|
|
75
|
+
options.browserResourceId,
|
|
76
|
+
'browserResourceId',
|
|
77
|
+
16,
|
|
78
|
+
256,
|
|
79
|
+
);
|
|
73
80
|
const ports = options.allowedPorts ?? [80, 443];
|
|
74
81
|
if (!Array.isArray(ports) || ports.length < 1 || ports.length > 32) {
|
|
75
82
|
throw new BrowserRuntimeError('INVALID_INPUT', 'allowedPorts must contain 1 to 32 ports');
|
|
@@ -178,6 +185,10 @@ export class BrowserEgressProxy {
|
|
|
178
185
|
return this.projectId;
|
|
179
186
|
}
|
|
180
187
|
|
|
188
|
+
public get browserResourceId(): string {
|
|
189
|
+
return this.browserResourceIdValue;
|
|
190
|
+
}
|
|
191
|
+
|
|
181
192
|
public get allowedTargetPorts(): number[] {
|
|
182
193
|
return [...this.allowedPorts];
|
|
183
194
|
}
|
|
@@ -36,7 +36,11 @@ implements plugins.flexharness.IFlexToolProvider<TScope> {
|
|
|
36
36
|
this.state = 'provisioning';
|
|
37
37
|
const scopeId = validateBoundedString(context.scopeId, 'scopeId', 1, 128);
|
|
38
38
|
const sessionId = validateBoundedString(context.sessionId, 'sessionId', 1, 128);
|
|
39
|
-
if (
|
|
39
|
+
if (
|
|
40
|
+
this.client.scopeId !== scopeId
|
|
41
|
+
|| this.client.sessionId.harnessId !== 'flex'
|
|
42
|
+
|| this.client.sessionId.nativeId !== sessionId
|
|
43
|
+
) {
|
|
40
44
|
this.state = 'closed';
|
|
41
45
|
throw new BrowserRuntimeError('CAPABILITY_INVALID');
|
|
42
46
|
}
|