feat: harden remote serve and reuse connections
Testing / remote-protocol-compat (0.9.5) (push) Successful in 56s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 59s
Testing / test (push) Successful in 1m1s
Build & Publish Package / publish (push) Successful in 33s
Package Extension / package-extension (push) Successful in 36s

- Gate TCP serve commands with safe-by-default policies, per-key allow tokens, per-key rate limiting, and audit labels.
- Reuse authenticated encrypted remote sessions and parallelize/caches multi-browser fanout to reduce repeated handshake roundtrips.
- Increase paged native-host batch size with extension-side byte budgeting to speed large tab listings safely.
- Point install output at public Chrome Web Store / Firefox AMO listings by default, with --dev preserving unpacked workflows.
- Share search-engine metadata between CLI and SDK and bump the package/extension version to 0.16.0.
- Cover the new security, pooling, paging, install, and fanout behavior with expanded Python and extension tests.
This commit is contained in:
2026-06-18 14:24:15 +02:00
parent 8dece7800f
commit 6fa931aa36
49 changed files with 3407 additions and 1878 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "browser-cli",
"version": "0.15.6",
"version": "0.16.0",
"description": "Control your browser from the terminal or Python SDK",
"browser_specific_settings": {
"gecko": {
+19 -3
View File
@@ -17,17 +17,33 @@ function isCommandSpec(entry: CommandEntry): entry is CommandSpec {
return typeof entry !== "function";
}
// Fill each page up to a byte budget kept safely under the 1MB native-messaging
// limit (extension → host). This makes paging adaptive: many small items pack
// into one page, while a few oversized items (e.g. data-URI favicons) split
// across pages instead of overflowing the limit.
const PAGE_BYTE_BUDGET = 768 * 1024;
export function makePagedData(items: Serializable[], page: PageRequest) {
const total = items.length;
const offset = Math.max(0, Number(page.offset) || 0);
const requestedLimit = Math.max(1, Number(page.limit) || 100);
const limit = Math.min(requestedLimit, 1000);
const end = Math.min(offset + limit, total);
const maxCount = Math.min(requestedLimit, 1000);
let end = offset;
let bytes = 0;
while (end < total && end - offset < maxCount) {
const itemBytes = JSON.stringify(items[end]).length + 1; // +1 ≈ separator
// Always include at least one item so a single oversized item still advances.
if (end > offset && bytes + itemBytes > PAGE_BYTE_BUDGET) break;
bytes += itemBytes;
end++;
}
return {
__browserCliPage: true,
items: items.slice(offset, end),
offset,
limit,
limit: maxCount,
total,
nextOffset: end < total ? end : null,
};
+41
View File
@@ -0,0 +1,41 @@
// @ts-nocheck
import test from 'node:test';
import assert from 'node:assert/strict';
import { makePagedData } from '../src/classes/CommandRegistry';
test('makePagedData packs many small items into one page up to the count cap', () => {
const items = Array.from({ length: 1500 }, (_, i) => ({ id: i, url: 'chrome://newtab/' }));
const page = makePagedData(items, { offset: 0, limit: 1000 });
assert.equal(page.items.length, 1000); // count cap, well under the byte budget
assert.equal(page.nextOffset, 1000);
assert.equal(page.total, 1500);
});
test('makePagedData splits on the byte budget for oversized items', () => {
// Each item ~200KB; only a few fit under the 768KB budget per page.
const big = 'x'.repeat(200 * 1024);
const items = Array.from({ length: 10 }, (_, i) => ({ id: i, favIconUrl: big }));
const page = makePagedData(items, { offset: 0, limit: 1000 });
assert.ok(page.items.length >= 1 && page.items.length < 10, `expected partial page, got ${page.items.length}`);
assert.equal(page.nextOffset, page.items.length);
});
test('makePagedData always advances by at least one item', () => {
// A single item larger than the whole budget must still be returned alone.
const huge = 'x'.repeat(2 * 1024 * 1024);
const items = [{ id: 0, favIconUrl: huge }, { id: 1 }];
const page = makePagedData(items, { offset: 0, limit: 1000 });
assert.equal(page.items.length, 1);
assert.equal(page.nextOffset, 1);
});
test('makePagedData reports null nextOffset on the final page', () => {
const items = [{ id: 0 }, { id: 1 }, { id: 2 }];
const page = makePagedData(items, { offset: 2, limit: 1000 });
assert.equal(page.items.length, 1);
assert.equal(page.nextOffset, null);
});