@browserbridge/bbx 1.8.0 → 1.9.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/PRIVACY.md +29 -3
- package/README.md +10 -3
- package/SECURITY.md +12 -0
- package/package.json +7 -1
- package/packages/agent-client/src/cli-args.js +35 -0
- package/packages/agent-client/src/cli-batch.js +121 -58
- package/packages/agent-client/src/cli-output.js +8 -1
- package/packages/agent-client/src/cli.js +227 -19
- package/packages/agent-client/src/client.js +156 -3
- package/packages/agent-client/src/command-registry.js +8 -2
- package/packages/agent-client/src/config.js +195 -0
- package/packages/agent-client/src/npm-self-update.js +305 -0
- package/packages/agent-client/src/remotes.js +4 -2
- package/packages/agent-client/src/runtime.js +97 -5
- package/packages/agent-client/src/types.ts +38 -0
- package/packages/mcp-server/src/handlers-capture.js +29 -2
- package/packages/mcp-server/src/handlers-dom.js +54 -3
- package/packages/mcp-server/src/handlers-page.js +180 -60
- package/packages/mcp-server/src/handlers-utils.js +35 -4
- package/packages/mcp-server/src/handlers.js +10 -3
- package/packages/mcp-server/src/server.js +75 -6
- package/packages/native-host/src/artifact-store.js +353 -0
- package/packages/native-host/src/config.js +9 -1
- package/packages/native-host/src/daemon-logger.js +3 -1
- package/packages/native-host/src/daemon.js +543 -27
- package/packages/native-host/src/native-host.js +25 -2
- package/packages/protocol/src/capability-values.js +2 -0
- package/packages/protocol/src/defaults.js +28 -0
- package/packages/protocol/src/errors.js +48 -5
- package/packages/protocol/src/incidental-sanitizer.js +197 -0
- package/packages/protocol/src/index.js +2 -0
- package/packages/protocol/src/protocol.js +316 -6
- package/packages/protocol/src/recovery-telemetry.js +302 -0
- package/packages/protocol/src/registry.js +143 -6
- package/packages/protocol/src/summary.js +116 -25
- package/packages/protocol/src/types.ts +489 -4
- package/skills/browser-bridge/SKILL.md +22 -8
- package/skills/browser-bridge/references/protocol.md +117 -13
- package/skills/browser-bridge/references/token-efficiency.md +17 -7
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
handleLogTool,
|
|
20
20
|
handleNavigationTool,
|
|
21
21
|
handlePageTool,
|
|
22
|
+
handleSensitiveReadTool,
|
|
22
23
|
handlePatchTool,
|
|
23
24
|
handleRawCallTool,
|
|
24
25
|
handleSetupTool,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
BUDGET_PRESETS,
|
|
34
35
|
DEFAULT_CONSOLE_LIMIT,
|
|
35
36
|
DEFAULT_EVAL_TIMEOUT_MS,
|
|
37
|
+
DEFAULT_HAR_LIMIT,
|
|
36
38
|
DEFAULT_LOG_TAIL_LIMIT,
|
|
37
39
|
DEFAULT_MAX_DEPTH,
|
|
38
40
|
DEFAULT_MAX_HTML_LENGTH,
|
|
@@ -41,6 +43,7 @@ import {
|
|
|
41
43
|
DEFAULT_TEXT_BUDGET,
|
|
42
44
|
DEFAULT_NAV_TIMEOUT_MS,
|
|
43
45
|
DEFAULT_WAIT_TIMEOUT_MS,
|
|
46
|
+
MAX_HAR_ENTRIES,
|
|
44
47
|
getMethodsByMaxComplexity,
|
|
45
48
|
} from '../../protocol/src/index.js';
|
|
46
49
|
import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
|
|
@@ -215,6 +218,10 @@ export function createBridgeMcpServer() {
|
|
|
215
218
|
action: z
|
|
216
219
|
.enum([
|
|
217
220
|
'query',
|
|
221
|
+
'baseline_create',
|
|
222
|
+
'baseline_compare',
|
|
223
|
+
'baseline_describe',
|
|
224
|
+
'baseline_release',
|
|
218
225
|
'describe',
|
|
219
226
|
'text',
|
|
220
227
|
'attributes',
|
|
@@ -237,6 +244,14 @@ export function createBridgeMcpServer() {
|
|
|
237
244
|
.describe(
|
|
238
245
|
'CSS selector (used if no elementRef; accessibility_tree requires exactly one match)'
|
|
239
246
|
),
|
|
247
|
+
baselineId: z.string().optional().describe('Opaque semantic DOM baseline identifier'),
|
|
248
|
+
maxChanges: z
|
|
249
|
+
.number()
|
|
250
|
+
.int()
|
|
251
|
+
.positive()
|
|
252
|
+
.max(200)
|
|
253
|
+
.optional()
|
|
254
|
+
.describe('Maximum returned change examples; exact counts are always retained'),
|
|
240
255
|
elementRef: z
|
|
241
256
|
.string()
|
|
242
257
|
.optional()
|
|
@@ -361,12 +376,31 @@ export function createBridgeMcpServer() {
|
|
|
361
376
|
handleStylesLayoutTool
|
|
362
377
|
);
|
|
363
378
|
|
|
379
|
+
server.registerTool(
|
|
380
|
+
'browser_sensitive_read',
|
|
381
|
+
{
|
|
382
|
+
title: 'Sensitive Browser Read',
|
|
383
|
+
description:
|
|
384
|
+
'High-risk exact-value read for one local or session storage key. This operation is logged as Sensitive access, is never batched or retried, and returns the whole value or fails atomically.',
|
|
385
|
+
inputSchema: {
|
|
386
|
+
source: z.enum(['local_storage', 'session_storage']).describe('Exact storage source'),
|
|
387
|
+
key: z
|
|
388
|
+
.string()
|
|
389
|
+
.max(2_048)
|
|
390
|
+
.describe('Exact storage key, including an empty key if intended'),
|
|
391
|
+
tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
|
|
392
|
+
destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
handleSensitiveReadTool
|
|
396
|
+
);
|
|
397
|
+
|
|
364
398
|
server.registerTool(
|
|
365
399
|
'browser_page',
|
|
366
400
|
{
|
|
367
401
|
title: 'Browser Page State',
|
|
368
402
|
description:
|
|
369
|
-
'Read page-level data, wait for load/URL conditions, or explicitly inspect/handle JavaScript dialogs. For element-level reads, use browser_dom. evaluate, performance, handle_dialog, and source=cdp network capture are debugger-backed.',
|
|
403
|
+
'Read page-level data, export captured network evidence as HAR 1.2, wait for load/URL conditions, or explicitly inspect/handle JavaScript dialogs. The performance action returns a raw CDP Performance.getMetrics point sample: Chrome defines names and units, which vary, while BBX sets no navigation observation window and does not measure LCP, CLS, or INP. For element-level reads, use browser_dom. evaluate, performance, handle_dialog, and source=cdp network capture are debugger-backed.',
|
|
370
404
|
inputSchema: {
|
|
371
405
|
action: z
|
|
372
406
|
.enum([
|
|
@@ -379,9 +413,12 @@ export function createBridgeMcpServer() {
|
|
|
379
413
|
'text',
|
|
380
414
|
'extract_content',
|
|
381
415
|
'network',
|
|
416
|
+
'har',
|
|
382
417
|
'performance',
|
|
383
418
|
])
|
|
384
|
-
.describe(
|
|
419
|
+
.describe(
|
|
420
|
+
'Page operation to perform; performance reads raw Chrome/CDP counters, not LCP, CLS, or INP'
|
|
421
|
+
),
|
|
385
422
|
tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
|
|
386
423
|
destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
|
|
387
424
|
budgetPreset: z
|
|
@@ -434,7 +471,9 @@ export function createBridgeMcpServer() {
|
|
|
434
471
|
.int()
|
|
435
472
|
.positive()
|
|
436
473
|
.optional()
|
|
437
|
-
.describe(
|
|
474
|
+
.describe(
|
|
475
|
+
`Maximum entries to return (console default: ${DEFAULT_CONSOLE_LIMIT}; HAR default: ${DEFAULT_HAR_LIMIT}, max: ${MAX_HAR_ENTRIES})`
|
|
476
|
+
),
|
|
438
477
|
type: z
|
|
439
478
|
.enum(['local', 'session'])
|
|
440
479
|
.optional()
|
|
@@ -468,7 +507,11 @@ export function createBridgeMcpServer() {
|
|
|
468
507
|
.positive()
|
|
469
508
|
.optional()
|
|
470
509
|
.describe('Maximum semantic extraction settlement wait (default: 2000)'),
|
|
471
|
-
urlPattern: z
|
|
510
|
+
urlPattern: z
|
|
511
|
+
.string()
|
|
512
|
+
.max(2_048)
|
|
513
|
+
.optional()
|
|
514
|
+
.describe('Bounded URL pattern used to filter network entries or HAR export evidence'),
|
|
472
515
|
source: z
|
|
473
516
|
.enum(['fetch-xhr', 'cdp'])
|
|
474
517
|
.optional()
|
|
@@ -477,6 +520,10 @@ export function createBridgeMcpServer() {
|
|
|
477
520
|
.enum(['read', 'start', 'clear', 'stop'])
|
|
478
521
|
.optional()
|
|
479
522
|
.describe('Explicit CDP capture lifecycle action (default: read)'),
|
|
523
|
+
delivery: z
|
|
524
|
+
.enum(['auto', 'inline', 'artifact'])
|
|
525
|
+
.optional()
|
|
526
|
+
.describe('HAR delivery mode: auto chooses inline or a daemon-owned artifact'),
|
|
480
527
|
waitForLoad: z
|
|
481
528
|
.boolean()
|
|
482
529
|
.optional()
|
|
@@ -626,7 +673,7 @@ export function createBridgeMcpServer() {
|
|
|
626
673
|
.int()
|
|
627
674
|
.nonnegative()
|
|
628
675
|
.optional()
|
|
629
|
-
.describe('
|
|
676
|
+
.describe('Delay after hover dispatch in ms; does not end hover (default: 0)'),
|
|
630
677
|
sourceElementRef: z.string().optional().describe('Drag source element (for drag action)'),
|
|
631
678
|
sourceSelector: z
|
|
632
679
|
.string()
|
|
@@ -674,7 +721,14 @@ export function createBridgeMcpServer() {
|
|
|
674
721
|
.describe('CSS property: value pairs (for apply_styles)'),
|
|
675
722
|
important: z.boolean().optional().describe('Add !important flag (default: false)'),
|
|
676
723
|
operation: z
|
|
677
|
-
.enum([
|
|
724
|
+
.enum([
|
|
725
|
+
'setAttribute',
|
|
726
|
+
'removeAttribute',
|
|
727
|
+
'addClass',
|
|
728
|
+
'removeClass',
|
|
729
|
+
'toggleClass',
|
|
730
|
+
'setTextContent',
|
|
731
|
+
])
|
|
678
732
|
.optional()
|
|
679
733
|
.describe('DOM mutation type'),
|
|
680
734
|
value: z.unknown().optional().describe('Value for the DOM operation'),
|
|
@@ -728,6 +782,11 @@ export function createBridgeMcpServer() {
|
|
|
728
782
|
.positive()
|
|
729
783
|
.optional()
|
|
730
784
|
.describe('CDP node id for cdp_box_model/cdp_computed_styles'),
|
|
785
|
+
computedStyles: z
|
|
786
|
+
.array(z.string().min(1).max(128))
|
|
787
|
+
.max(100)
|
|
788
|
+
.optional()
|
|
789
|
+
.describe('CSS property names to include for cdp_dom_snapshot'),
|
|
731
790
|
rect: z
|
|
732
791
|
.object({
|
|
733
792
|
x: z.number().nonnegative().describe('Region left edge (viewport pixels)'),
|
|
@@ -748,6 +807,16 @@ export function createBridgeMcpServer() {
|
|
|
748
807
|
.max(100)
|
|
749
808
|
.optional()
|
|
750
809
|
.describe('JPEG/WebP quality from 0 to 100; ignored for PNG'),
|
|
810
|
+
delivery: z
|
|
811
|
+
.enum(['auto', 'inline', 'artifact'])
|
|
812
|
+
.optional()
|
|
813
|
+
.describe('Delivery mode: auto chooses inline or a daemon-owned artifact'),
|
|
814
|
+
scale: z
|
|
815
|
+
.number()
|
|
816
|
+
.min(0.1)
|
|
817
|
+
.max(4)
|
|
818
|
+
.optional()
|
|
819
|
+
.describe('Screenshot capture scale (0.1 to 4)'),
|
|
751
820
|
},
|
|
752
821
|
},
|
|
753
822
|
handleCaptureTool
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ARTIFACT_CHUNK_BYTES,
|
|
9
|
+
ARTIFACT_TTL_MS,
|
|
10
|
+
BridgeError,
|
|
11
|
+
ERROR_CODES,
|
|
12
|
+
MAX_ARTIFACT_BYTES,
|
|
13
|
+
MAX_ARTIFACT_CLIENT_BYTES,
|
|
14
|
+
MAX_ARTIFACT_TOTAL_BYTES,
|
|
15
|
+
MAX_ARTIFACTS_PER_CLIENT,
|
|
16
|
+
} from '../../protocol/src/index.js';
|
|
17
|
+
|
|
18
|
+
/** @typedef {import('../../protocol/src/types.js').ArtifactKind} ArtifactKind */
|
|
19
|
+
/** @typedef {{ artifactId: string, requestId: string, ownerId: string, extensionId: string, kind: ArtifactKind, mimeType: string, totalBytes: number, sha256: string, chunkCount: number, createdAt: string, expiresAt: string, fd: number, temporaryPath: string, finalPath: string, written: number, nextChunk: number, hash: import('node:crypto').Hash }} ArtifactTransfer */
|
|
20
|
+
/** @typedef {{ artifactId: string, requestId: string, ownerId: string, extensionId: string, kind: ArtifactKind, mimeType: string, byteLength: number, sha256: string, chunkCount: number, createdAt: string, expiresAt: string, filePath: string }} ArtifactRecord */
|
|
21
|
+
|
|
22
|
+
export class ArtifactStore {
|
|
23
|
+
/** @param {string} rootPath @param {() => number} [now] */
|
|
24
|
+
constructor(rootPath, now = Date.now) {
|
|
25
|
+
this.rootPath = rootPath;
|
|
26
|
+
this.now = now;
|
|
27
|
+
/** @type {Map<string, ArtifactTransfer>} */
|
|
28
|
+
this.transfers = new Map();
|
|
29
|
+
/** @type {Map<string, ArtifactRecord>} */
|
|
30
|
+
this.artifacts = new Map();
|
|
31
|
+
/** @type {Map<string, ReturnType<typeof setTimeout>>} */
|
|
32
|
+
this.expiryTimers = new Map();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
reset() {
|
|
36
|
+
for (const transfer of this.transfers.values()) {
|
|
37
|
+
try {
|
|
38
|
+
fs.closeSync(transfer.fd);
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
for (const timer of this.expiryTimers.values()) clearTimeout(timer);
|
|
42
|
+
fs.rmSync(this.rootPath, { recursive: true, force: true });
|
|
43
|
+
fs.mkdirSync(this.rootPath, { recursive: true, mode: 0o700 });
|
|
44
|
+
if (process.platform !== 'win32') fs.chmodSync(this.rootPath, 0o700);
|
|
45
|
+
this.transfers.clear();
|
|
46
|
+
this.artifacts.clear();
|
|
47
|
+
this.expiryTimers.clear();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @param {Omit<ArtifactTransfer, 'fd' | 'temporaryPath' | 'finalPath' | 'written' | 'nextChunk' | 'hash'>} input */
|
|
51
|
+
begin(input) {
|
|
52
|
+
this.pruneExpired();
|
|
53
|
+
if (!/^art_[A-Za-z0-9_-]{32,64}$/u.test(input.artifactId)) {
|
|
54
|
+
throw new BridgeError(
|
|
55
|
+
ERROR_CODES.ARTIFACT_TRANSFER_INVALID,
|
|
56
|
+
'Artifact identifier is invalid.'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (
|
|
60
|
+
!Number.isInteger(input.totalBytes) ||
|
|
61
|
+
input.totalBytes < 1 ||
|
|
62
|
+
input.totalBytes > MAX_ARTIFACT_BYTES ||
|
|
63
|
+
input.chunkCount !== Math.ceil(input.totalBytes / ARTIFACT_CHUNK_BYTES)
|
|
64
|
+
) {
|
|
65
|
+
throw new BridgeError(ERROR_CODES.ARTIFACT_TRANSFER_INVALID, 'Artifact size is invalid.');
|
|
66
|
+
}
|
|
67
|
+
const now = this.now();
|
|
68
|
+
const createdAt = Date.parse(input.createdAt);
|
|
69
|
+
const expiresAt = Date.parse(input.expiresAt);
|
|
70
|
+
if (
|
|
71
|
+
!/^[a-f0-9]{64}$/u.test(input.sha256) ||
|
|
72
|
+
!isValidKindAndMimeType(input.kind, input.mimeType) ||
|
|
73
|
+
!Number.isFinite(createdAt) ||
|
|
74
|
+
!Number.isFinite(expiresAt) ||
|
|
75
|
+
createdAt > now + 10_000 ||
|
|
76
|
+
createdAt < now - ARTIFACT_TTL_MS ||
|
|
77
|
+
expiresAt <= now ||
|
|
78
|
+
expiresAt <= createdAt ||
|
|
79
|
+
expiresAt > now + ARTIFACT_TTL_MS
|
|
80
|
+
) {
|
|
81
|
+
throw new BridgeError(ERROR_CODES.ARTIFACT_TRANSFER_INVALID, 'Artifact metadata is invalid.');
|
|
82
|
+
}
|
|
83
|
+
if (this.transfers.has(input.artifactId) || this.artifacts.has(input.artifactId)) {
|
|
84
|
+
throw new BridgeError(
|
|
85
|
+
ERROR_CODES.ARTIFACT_TRANSFER_INVALID,
|
|
86
|
+
'Artifact identifier is already in use.'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const ownedArtifacts = [...this.artifacts.values()].filter(
|
|
90
|
+
(item) => item.ownerId === input.ownerId
|
|
91
|
+
);
|
|
92
|
+
const ownedTransfers = [...this.transfers.values()].filter(
|
|
93
|
+
(item) => item.ownerId === input.ownerId
|
|
94
|
+
);
|
|
95
|
+
const clientBytes =
|
|
96
|
+
ownedArtifacts.reduce((total, item) => total + item.byteLength, 0) +
|
|
97
|
+
ownedTransfers.reduce((total, item) => total + item.totalBytes, 0);
|
|
98
|
+
const totalBytes =
|
|
99
|
+
[...this.artifacts.values()].reduce((total, item) => total + item.byteLength, 0) +
|
|
100
|
+
[...this.transfers.values()].reduce((total, item) => total + item.totalBytes, 0);
|
|
101
|
+
if (
|
|
102
|
+
ownedArtifacts.length + ownedTransfers.length >= MAX_ARTIFACTS_PER_CLIENT ||
|
|
103
|
+
clientBytes + input.totalBytes > MAX_ARTIFACT_CLIENT_BYTES ||
|
|
104
|
+
totalBytes + input.totalBytes > MAX_ARTIFACT_TOTAL_BYTES
|
|
105
|
+
) {
|
|
106
|
+
throw new BridgeError(ERROR_CODES.ARTIFACT_QUOTA_EXCEEDED, 'Artifact quota exceeded.');
|
|
107
|
+
}
|
|
108
|
+
const temporaryPath = path.join(this.rootPath, `${input.artifactId}.part`);
|
|
109
|
+
const finalPath = path.join(this.rootPath, `${input.artifactId}.bin`);
|
|
110
|
+
const fd = fs.openSync(temporaryPath, 'wx', 0o600);
|
|
111
|
+
this.transfers.set(input.artifactId, {
|
|
112
|
+
...input,
|
|
113
|
+
fd,
|
|
114
|
+
temporaryPath,
|
|
115
|
+
finalPath,
|
|
116
|
+
written: 0,
|
|
117
|
+
nextChunk: 0,
|
|
118
|
+
hash: createHash('sha256'),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** @param {string} artifactId @param {number} chunkIndex @param {string} data */
|
|
123
|
+
writeChunk(artifactId, chunkIndex, data) {
|
|
124
|
+
const transfer = this.transfers.get(artifactId);
|
|
125
|
+
if (!transfer || chunkIndex !== transfer.nextChunk) {
|
|
126
|
+
throw new BridgeError(
|
|
127
|
+
ERROR_CODES.ARTIFACT_TRANSFER_INVALID,
|
|
128
|
+
'Artifact chunk order is invalid.'
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const bytes = decodeBase64(data);
|
|
132
|
+
if (bytes.length < 1 || bytes.length > ARTIFACT_CHUNK_BYTES) {
|
|
133
|
+
throw new BridgeError(
|
|
134
|
+
ERROR_CODES.ARTIFACT_TRANSFER_INVALID,
|
|
135
|
+
'Artifact chunk size is invalid.'
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (transfer.written + bytes.length > transfer.totalBytes) {
|
|
139
|
+
throw new BridgeError(
|
|
140
|
+
ERROR_CODES.ARTIFACT_TRANSFER_INVALID,
|
|
141
|
+
'Artifact exceeds declared size.'
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
fs.writeSync(transfer.fd, bytes);
|
|
145
|
+
transfer.hash.update(bytes);
|
|
146
|
+
transfer.written += bytes.length;
|
|
147
|
+
transfer.nextChunk += 1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** @param {string} artifactId @returns {ArtifactRecord} */
|
|
151
|
+
commit(artifactId) {
|
|
152
|
+
const transfer = this.transfers.get(artifactId);
|
|
153
|
+
if (!transfer) {
|
|
154
|
+
throw new BridgeError(ERROR_CODES.ARTIFACT_TRANSFER_INVALID, 'Artifact transfer is missing.');
|
|
155
|
+
}
|
|
156
|
+
const digest = transfer.hash.digest('hex');
|
|
157
|
+
if (
|
|
158
|
+
transfer.written !== transfer.totalBytes ||
|
|
159
|
+
transfer.nextChunk !== transfer.chunkCount ||
|
|
160
|
+
digest !== transfer.sha256
|
|
161
|
+
) {
|
|
162
|
+
this.abort(artifactId);
|
|
163
|
+
throw new BridgeError(ERROR_CODES.ARTIFACT_TRANSFER_INVALID, 'Artifact checksum is invalid.');
|
|
164
|
+
}
|
|
165
|
+
fs.closeSync(transfer.fd);
|
|
166
|
+
fs.renameSync(transfer.temporaryPath, transfer.finalPath);
|
|
167
|
+
if (process.platform !== 'win32') fs.chmodSync(transfer.finalPath, 0o600);
|
|
168
|
+
const record = {
|
|
169
|
+
artifactId: transfer.artifactId,
|
|
170
|
+
requestId: transfer.requestId,
|
|
171
|
+
ownerId: transfer.ownerId,
|
|
172
|
+
extensionId: transfer.extensionId,
|
|
173
|
+
kind: transfer.kind,
|
|
174
|
+
mimeType: transfer.mimeType,
|
|
175
|
+
byteLength: transfer.totalBytes,
|
|
176
|
+
sha256: digest,
|
|
177
|
+
chunkCount: transfer.chunkCount,
|
|
178
|
+
createdAt: transfer.createdAt,
|
|
179
|
+
expiresAt: transfer.expiresAt,
|
|
180
|
+
filePath: transfer.finalPath,
|
|
181
|
+
};
|
|
182
|
+
this.transfers.delete(artifactId);
|
|
183
|
+
this.artifacts.set(artifactId, record);
|
|
184
|
+
const timer = setTimeout(
|
|
185
|
+
() => this.removeCommitted(artifactId),
|
|
186
|
+
Math.max(0, Date.parse(record.expiresAt) - this.now())
|
|
187
|
+
);
|
|
188
|
+
timer.unref?.();
|
|
189
|
+
this.expiryTimers.set(artifactId, timer);
|
|
190
|
+
return record;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** @param {string} artifactId @param {string} ownerId @param {number} offset @param {number} maxBytes */
|
|
194
|
+
read(artifactId, ownerId, offset, maxBytes) {
|
|
195
|
+
const record = this.getOwned(artifactId, ownerId);
|
|
196
|
+
const start = Math.min(offset, record.byteLength);
|
|
197
|
+
const byteLength = Math.min(maxBytes, record.byteLength - start);
|
|
198
|
+
const bytes = Buffer.alloc(byteLength);
|
|
199
|
+
const fd = fs.openSync(record.filePath, 'r');
|
|
200
|
+
try {
|
|
201
|
+
fs.readSync(fd, bytes, 0, byteLength, start);
|
|
202
|
+
} finally {
|
|
203
|
+
fs.closeSync(fd);
|
|
204
|
+
}
|
|
205
|
+
const nextOffset = start + byteLength < record.byteLength ? start + byteLength : null;
|
|
206
|
+
return {
|
|
207
|
+
artifactId,
|
|
208
|
+
data: bytes.toString('base64'),
|
|
209
|
+
offset: start,
|
|
210
|
+
byteLength,
|
|
211
|
+
chunkIndex: Math.floor(start / ARTIFACT_CHUNK_BYTES),
|
|
212
|
+
chunkCount: record.chunkCount,
|
|
213
|
+
nextOffset,
|
|
214
|
+
totalBytes: record.byteLength,
|
|
215
|
+
sha256: record.sha256,
|
|
216
|
+
expiresAt: record.expiresAt,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** @param {string} artifactId @param {string} ownerId */
|
|
221
|
+
delete(artifactId, ownerId) {
|
|
222
|
+
this.getOwned(artifactId, ownerId);
|
|
223
|
+
this.removeCommitted(artifactId);
|
|
224
|
+
return { artifactId, deleted: true };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** @param {string} artifactId @param {string} ownerId @param {string} requestId @param {ArtifactKind} [kind] */
|
|
228
|
+
ownsCommitted(artifactId, ownerId, requestId, kind) {
|
|
229
|
+
const record = this.artifacts.get(artifactId);
|
|
230
|
+
return (
|
|
231
|
+
record?.ownerId === ownerId &&
|
|
232
|
+
record.requestId === requestId &&
|
|
233
|
+
(kind === undefined || record.kind === kind)
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @param {string} artifactId
|
|
239
|
+
* @param {string} ownerId
|
|
240
|
+
* @param {string} requestId
|
|
241
|
+
* @param {Record<string, unknown>} descriptor
|
|
242
|
+
* @param {ArtifactKind | undefined} expectedKind
|
|
243
|
+
*/
|
|
244
|
+
matchesCommitted(artifactId, ownerId, requestId, descriptor, expectedKind) {
|
|
245
|
+
const record = this.artifacts.get(artifactId);
|
|
246
|
+
return Boolean(
|
|
247
|
+
record &&
|
|
248
|
+
record.ownerId === ownerId &&
|
|
249
|
+
record.requestId === requestId &&
|
|
250
|
+
(expectedKind === undefined || record.kind === expectedKind) &&
|
|
251
|
+
descriptor.kind === record.kind &&
|
|
252
|
+
descriptor.mimeType === record.mimeType &&
|
|
253
|
+
descriptor.byteLength === record.byteLength &&
|
|
254
|
+
descriptor.sha256 === record.sha256 &&
|
|
255
|
+
descriptor.chunkSize === ARTIFACT_CHUNK_BYTES &&
|
|
256
|
+
descriptor.chunkCount === record.chunkCount &&
|
|
257
|
+
descriptor.createdAt === record.createdAt &&
|
|
258
|
+
descriptor.expiresAt === record.expiresAt
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** @param {string} requestId */
|
|
263
|
+
abortRequest(requestId) {
|
|
264
|
+
for (const transfer of this.transfers.values()) {
|
|
265
|
+
if (transfer.requestId === requestId) this.abort(transfer.artifactId);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** @param {string} requestId */
|
|
270
|
+
deleteRequest(requestId) {
|
|
271
|
+
this.abortRequest(requestId);
|
|
272
|
+
for (const record of [...this.artifacts.values()]) {
|
|
273
|
+
if (record.requestId === requestId) this.removeCommitted(record.artifactId);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** @param {string} extensionId */
|
|
278
|
+
deleteByExtension(extensionId) {
|
|
279
|
+
for (const transfer of [...this.transfers.values()]) {
|
|
280
|
+
if (transfer.extensionId === extensionId) this.abort(transfer.artifactId);
|
|
281
|
+
}
|
|
282
|
+
for (const record of this.artifacts.values()) {
|
|
283
|
+
if (record.extensionId === extensionId) {
|
|
284
|
+
this.removeCommitted(record.artifactId);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
pruneExpired() {
|
|
290
|
+
const now = this.now();
|
|
291
|
+
for (const transfer of [...this.transfers.values()]) {
|
|
292
|
+
if (Date.parse(transfer.expiresAt) <= now) this.abort(transfer.artifactId);
|
|
293
|
+
}
|
|
294
|
+
for (const record of this.artifacts.values()) {
|
|
295
|
+
if (Date.parse(record.expiresAt) <= now) this.removeCommitted(record.artifactId);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** @param {string} artifactId */
|
|
300
|
+
abort(artifactId) {
|
|
301
|
+
const transfer = this.transfers.get(artifactId);
|
|
302
|
+
if (!transfer) return;
|
|
303
|
+
this.transfers.delete(artifactId);
|
|
304
|
+
try {
|
|
305
|
+
fs.closeSync(transfer.fd);
|
|
306
|
+
} catch {}
|
|
307
|
+
fs.rmSync(transfer.temporaryPath, { force: true });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** @param {string} artifactId */
|
|
311
|
+
removeCommitted(artifactId) {
|
|
312
|
+
const record = this.artifacts.get(artifactId);
|
|
313
|
+
if (!record) return;
|
|
314
|
+
this.artifacts.delete(artifactId);
|
|
315
|
+
const timer = this.expiryTimers.get(artifactId);
|
|
316
|
+
if (timer) clearTimeout(timer);
|
|
317
|
+
this.expiryTimers.delete(artifactId);
|
|
318
|
+
fs.rmSync(record.filePath, { force: true });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** @param {string} artifactId @param {string} ownerId */
|
|
322
|
+
getOwned(artifactId, ownerId) {
|
|
323
|
+
this.pruneExpired();
|
|
324
|
+
const record = this.artifacts.get(artifactId);
|
|
325
|
+
if (!record || record.ownerId !== ownerId) {
|
|
326
|
+
throw new BridgeError(ERROR_CODES.ARTIFACT_NOT_FOUND, 'Artifact was not found.');
|
|
327
|
+
}
|
|
328
|
+
return record;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** @param {ArtifactKind} kind @param {string} mimeType */
|
|
333
|
+
function isValidKindAndMimeType(kind, mimeType) {
|
|
334
|
+
return (
|
|
335
|
+
(kind === 'screenshot' && mimeType.startsWith('image/')) ||
|
|
336
|
+
(kind === 'har' && mimeType === 'application/json')
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** @param {string} value */
|
|
341
|
+
function decodeBase64(value) {
|
|
342
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value)) {
|
|
343
|
+
throw new BridgeError(ERROR_CODES.ARTIFACT_TRANSFER_INVALID, 'Artifact chunk is not base64.');
|
|
344
|
+
}
|
|
345
|
+
const bytes = Buffer.from(value, 'base64');
|
|
346
|
+
if (bytes.toString('base64') !== value) {
|
|
347
|
+
throw new BridgeError(
|
|
348
|
+
ERROR_CODES.ARTIFACT_TRANSFER_INVALID,
|
|
349
|
+
'Artifact chunk is not canonical base64.'
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return bytes;
|
|
353
|
+
}
|
|
@@ -4,6 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
|
+
import { sanitizeIncidentalPath, sanitizeIncidentalText } from '../../protocol/src/index.js';
|
|
7
8
|
|
|
8
9
|
export const APP_NAME = 'com.browserbridge.browser_bridge';
|
|
9
10
|
export const BRIDGE_HOME_ENV = 'BROWSER_BRIDGE_HOME';
|
|
@@ -58,6 +59,11 @@ export function getBridgeDir(env = process.env) {
|
|
|
58
59
|
return path.join(xdgDataHome, 'browser-bridge');
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/** @param {NodeJS.ProcessEnv} [env=process.env] */
|
|
63
|
+
export function getArtifactStorePath(env = process.env) {
|
|
64
|
+
return path.join(getBridgeDir(env), 'artifacts');
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
/**
|
|
62
68
|
* Resolve the IPC endpoint the daemon listens on and the CLI / native host
|
|
63
69
|
* connect to. On Windows we use a Named Pipe by default because Node's AF_UNIX
|
|
@@ -125,7 +131,9 @@ function normalizePort(value) {
|
|
|
125
131
|
* @returns {void}
|
|
126
132
|
*/
|
|
127
133
|
function warnProxyConfigIgnored(configPath, reason) {
|
|
128
|
-
console.error(
|
|
134
|
+
console.error(
|
|
135
|
+
`browser-bridge: ignoring proxy config at ${sanitizeIncidentalPath(configPath)}: ${sanitizeIncidentalText(reason)}`
|
|
136
|
+
);
|
|
129
137
|
}
|
|
130
138
|
|
|
131
139
|
/**
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
+
import { sanitizeIncidentalValue } from '../../protocol/src/index.js';
|
|
4
|
+
|
|
3
5
|
/** @typedef {{ write: (chunk: string) => void }} LogStream */
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -62,7 +64,7 @@ export class DaemonLogger {
|
|
|
62
64
|
message,
|
|
63
65
|
...extra,
|
|
64
66
|
};
|
|
65
|
-
this.#stream.write(`${JSON.stringify(entry)}\n`);
|
|
67
|
+
this.#stream.write(`${JSON.stringify(sanitizeIncidentalValue(entry))}\n`);
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/**
|