npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@volter/almostcdp

v0.1.33

Published

A useful Chrome DevTools Protocol subset over an existing live DOM surface

Readme

AlmostCDP

AlmostCDP is a browser-side implementation of a useful Chrome DevTools Protocol subset over an existing live DOM surface.

It lets standard CDP clients connect to a page that is already running in a browser, webview, preview frame, or simulated browser. The client still speaks CDP. The surface adapter supplies DOM, navigation, input, capture, network, and console primitives available in that environment.

AlmostCDP is a protocol product with one viewer for any CDP endpoint (@volter/almostcdp/viewer). It does not launch Chrome, retain browser profiles, record sessions, or define an agent action vocabulary.

Intended clients

  • Playwright through chromium.connectOverCDP()
  • Puppeteer
  • Chrome DevTools
  • CDP-based agent and inspection tools
  • @volter/almostcdp/viewer, or another CDP viewer chosen by the embedding product

Intended surfaces

  • a same-realm document
  • an iframe reached through a host bridge
  • Browser Substrate's preview inspector
  • browser and webview hosts that can implement the adapter contract

The project reports support method by method. Unsupported commands return CDP errors; they never succeed as no-ops. Compatibility is measured against pinned client versions and published in the repository.

Download policy needs a host download adapter: pass downloadHost: { write(contextId, path, bytes) } to the server or dispatcher. contextId is undefined for the default context and path is <downloadPath>/<guid> (allowAndName) or <downloadPath>/<suggested filename> (allow), with downloadPath as the client sent it; resolving that path is the host's job. With it, Browser.setDownloadBehavior accepts every policy, and anchor downloads (a[download], including blob:/data: hrefs) are intercepted in the page, fetched there and written through the adapter. A document that arrives as an attachment is the client's download too when the embedding carries documents: the network door's responded answers { download: true }, the embedding keeps the frame where it was, and the body it passes to finished is written through the adapter. Otherwise the host browser downloads it itself. Without the adapter the command refuses every policy, including deny, and pinned Playwright 1.63 must attach with { noDefaults: true }.

Replacing a capture stream suppresses images still pending from the previous stream; stopping that previous stream cannot erase the replacement's pending acknowledgement. Capture remains adapter-provided, with reconstruction limits reported separately from protocol delivery.

See ARCHITECTURE.md for ownership boundaries and ROADMAP.md for the first compatibility target.

Current development build

The first executable slice is on the build/mvp branch. It provides:

  • Chromium discovery routes and browser/page WebSocket endpoints
  • target discovery and flattened sessions
  • Runtime evaluation, handles, calls, properties, bindings, and release
  • DOM documents, queries, node resolution, box models, and content quads
  • page/frame lifecycle and layout metrics
  • Playwright locator click and fill through synthetic Input events
  • styled-DOM screenshot and acknowledged screencast capture via html-to-image
  • host authorization hooks and server-enforced view-only casting
  • a transport-independent dispatcher exported from /core
  • an rrweb event WebSocket with reverse Input.* messages
  • explicit CDP errors for commands the surface cannot perform

An acceptance test opens an ordinary page in the existing normal Chrome containing a same-origin iframe, registers that iframe as the surface, connects unmodified Playwright 1.63 with chromium.connectOverCDP(url, { noDefaults: true }), and evaluates, clicks, fills, captures, receives a screencast frame, consumes a cast event, and sends input back.

Embedding a DOM surface

The package is published to npm as @volter/almostcdp; this repository stays private. Run npm run verify:package inside a World to pack and install an isolated consumer, reinstall from its lock offline, import the core entry points without optional peers, and check server discovery. Install rrweb separately for @volter/almostcdp/rrweb, or html-to-image for @volter/almostcdp/capture. The root entry does not import those adapters.

// Node host
import { createAlmostCdpServer } from "@volter/almostcdp/server";

const server = await createAlmostCdpServer();
console.log(server.httpUrl, server.surfaceWebSocketUrl);

Pass surfaceWebSocketUrl across the embedding application's existing configuration or frame bridge. In the page bundle:

import { connectDomSurface } from "@volter/almostcdp/dom";
import { createRrwebCast } from "@volter/almostcdp/rrweb";
import { createDomCapture } from "@volter/almostcdp/capture";

// Run this bundle in the document being recorded (including inside an iframe).
connectDomSurface({
  endpoint: configuredSurfaceWebSocketUrl,
  id: "preview",
  cast: createRrwebCast(),
  capture: createDomCapture(),
});

Back in the Node host:

// Standard CDP:
await chromium.connectOverCDP(server.httpUrl, { noDefaults: true });

// rrweb events out; JSON CDP Input.* messages back:
server.castWebSocketUrl("preview");

Two optional host hooks complete the page surface: close() answers Page.close, and files.read(path) supplies the bytes for DOM.setFileInputFiles. Without them those commands refuse by name. A server whose clients name paths on its own disk passes the dispatcher a fileHost instead: read(path) gives a file's bytes, name, type and time, and the optional list(path) a directory's files by their relative paths, so a folder upload arrives with each file's webkitRelativePath. The surface is given the bytes alone.

An embedding that carries the page's own requests through the surface's network door (a service worker's fetches, reported and routed) passes carriedDocuments: true, and Page.navigate then answers as Chromium's does: once the document's response or failure is known, with errorText for a navigation that failed (net::ERR_ABORTED for a 204, the carried failure otherwise). A relative URL is refused ("Cannot navigate to invalid URL").

A host that serves the page under a different URL than its clients use passes urls: { toSurface, fromSurface }. toSurface(clientUrl) returns the frame's real URL (or undefined to refuse) and is applied to every URL a client sends, including Page.navigate before its same-origin check; the navigation adapter then receives the real URL. fromSurface(realUrl) returns the client-visible URL and is applied to every URL the surface reports (target, frame tree, navigation and network events, history, console and exceptions). A URL the mapping cannot relate should pass through unchanged. Page script keeps seeing the frame's real location. Cookie domains are hosts, not URLs: optional hostToSurface/hostFromSurface map them, and without them the host is mapped as http://<host>/.

The server and DOM agent normally live on opposite sides of a process or frame boundary, so an embedding application passes the surface endpoint into its page bundle. The bare server binds to loopback by default. A host that publishes it remotely must add authentication around both CDP and cast WebSockets.

An embedding can instead pass transport to connectDomSurface, omitting endpoint. MessagePortTransport from @volter/almostcdp/message-port wraps an owned, transferred port and works with both the DOM agent and core dispatcher. This lets a sandbox carry protocol messages through its existing Host bridge without opening a network connection from the preview. The embedding owns port transfer, origin validation, and closure; unexpected peer disappearance still requires host lifecycle notification. The acceptance harness exercises the same public Playwright/CDP behavior over direct WebSocket and transferred MessagePort paths. Browser Substrate integration itself remains W52 work.

A host with no Node server uses createSocketEndpoint from @volter/almostcdp/socket instead. It runs the same dispatcher and serves Chromium's HTTP discovery and DevTools WebSockets over any accepted byte socket ({ onData, onClose, write, close }), so a listener inside the tab publishes the endpoint an unmodified client reaches at http://<address>. attachSurface(peer) registers a surface transport, such as a MessagePortTransport, without a surface WebSocket. A host that received the transport from its own platform, and so knows which target it belongs to, passes attachSurface(peer, { id }): the surface is refused unless it announces that id, and for a successor document the host's word replaces the succession token, so a page cannot claim another tab's target by presenting its id and token. With requireExpect: true in the endpoint's options, every top-level surface must be one its host named this way. A subsurface (a dedicated worker a page runs) is always named by the dispatcher, <parent id>.<n>, and is always a worker target, whatever its agent announces. A successor document resumes with only the init scripts and bindings clients registered on the target through their own commands (Page.addScriptToEvaluateOnNewDocument, Runtime.addBinding), never ones the previous page reported; a host that also vouches for each document's origin, attachSurface(peer, { id, origin }), has the surface modules' state (emulation, permissions, interception) dropped when the origin changes, so no page can seed another origin's document. connectDomSurface({ automation: false }) leaves navigator.webdriver as the browser reports it (by default a driven page reports true, as Chromium launched for automation does). address is the host:port advertised in discovery; authorize sees each request's method, path and headers.

An agent can also run inside the document it controls, as a browser extension instruments every document of a tab. Such a document is replaced on navigation, agent and all. With succession: { token } (at least 16 characters, held by the embedding host) each document's agent connects with the same id and token:

connectDomSurface({ endpoint, id: tabId, succession: { token: tabToken } });

The first hello binds the token to the target. When a document goes, the server suspends the target instead of closing it: sessions and client sockets remain, commands the old document never answered fail as Chromium fails them on navigation, and new commands wait. The next document's hello with the same id and token resumes the target; its agent restores the state clients set that outlives a document (enabled domains, init scripts, bindings, isolated world, emulation, permission grants, request interception, published by each agent as it changes) and announces itself as a navigation, with the loaderId a client's Page.navigate was promised. A host that calls preloadSuccession() as soon as a document's script executes (before connectDomSurface) runs the client's init scripts there, without waiting for the resume, when the predecessor was of the same origin: the predecessor hands them over in the tab's session storage, and binding calls they make wait for the resume, which announces their context. Without a successor within successionGraceMs (default 15 s) the target is destroyed. Cross-origin Page.navigate is allowed for these surfaces. The token authorizes a successor, not a client: a page that can read its agent's options can claim its own target, so publish the endpoint only under the host's access policy. test/succession.test.mjs drives one normal-Chrome tab with unmodified Playwright across two origins, by goto and by a link click in the tab; its receipt is test/results/acceptance-succession.json.

The optional navigation adapter supplies navigate(url) and reload() so an embedding can retain its routing checks. DOM surfaces without succession reject cross-origin navigation before invoking that adapter, including non-HTTP opaque destinations; inherited-origin srcdoc frames retain their embedding origin. Protocol control still grants JavaScript execution in the live page; these checks do not replace the embedding's sandbox and network policy.

Watching and taking over: the viewer

mountViewer(element, { endpoint, … }) from @volter/almostcdp/viewer fills an element with a live view of one page and returns { state, takeOver(), release(), close() }. It works against any CDP endpoint: real Chrome, a Volter Browsers publication, or AlmostCDP. It is one ES module with no runtime dependency; rrweb's Replayer is imported only in DOM-cast mode.

  • endpoint: an HTTP discovery URL (the /json/list base, such as Volter Browsers' cdpUrl), a page or browser WebSocket URL, { url, headers, target }, or an already-connected CDP session (anything with send(method, params) and on(event, listener), as Playwright's and Puppeteer's are). headers go to discovery. A browser WebSocket cannot send headers: a query string on the discovery URL (Volter Browsers' ?token=) is carried to the socket, and a socket factory can open it with headers where the platform allows.
  • Pixel mode (always available): Page.startScreencast frames drawn to a canvas, each acknowledged after it is drawn, sized to the element and restarted when it is resized. Use it for canvas, WebGL and video pages.
  • DOM-cast mode: when the endpoint is AlmostCDP (Browser.getVersion answers revision @almostcdp, or refuses on a direct page socket) and its /casts/<target> socket answers within castTimeoutMs, the rrweb cast is replayed instead: crisp at any scale and lighter than frames. cast names the cast socket explicitly or disables it; mode: 'pixel' | 'dom' forces one.
  • Input: the viewer starts watching. A badge offers Take over; while taken over the view is outlined and the badge reads "You are controlling this browser" with Release. Input goes through bindCastInput to CDP Input.* (mouse, wheel, keys, text and IME, touch), mapped from the displayed rectangle to the page's viewport. control: false makes a watch-only viewer; takeOver: true starts in control.
  • controls: true adds back, forward, reload and an address field through Page.*; the address field accepts only http(s) URLs.
  • The viewer never records. A dropped socket it opened is reopened with backoff; a session it was given stays its caller's.

The minified module, with cast-input and cast-client, is about 17 KB (7 KB gzipped) without rrweb.

Playwright in the page

@volter/almostcdp/playwright is unmodified playwright-core 1.63.0, client and CDP half in-process, bundled for a browser document (3.8 MB minified, about 970 KB gzipped). It drives an AlmostCDP endpoint with no Node process:

import { connectPlaywright, evalPlaywright, pageFor } from "@volter/almostcdp/playwright";

const browser = await connectPlaywright(port); // a MessagePort, a transport, or a createSocketEndpoint() endpoint
await evalPlaywright("async (page, args) => { await page.getByLabel('Name').fill(args.name); return page.title(); }", { name: "Ada" });
const page = await pageFor(browser, "tab-7"); // the page whose surface id is tab-7

connectPlaywright attaches with noDefaults and writes nothing to disk; the browser it returns is evalPlaywright's default page source unless a { page } is passed. evalPlaywright compiles its source with eval, so it runs in a realm that allows it: a plain page, or a browser extension's sandboxed page (Manifest V3 forbids eval in content scripts and ordinary extension pages). The surface a script drives answers Runtime.evaluate in the page's main world, under the page's own Content-Security-Policy.

The bundle also exports Playwright's own chromium, errors, devices, selectors and request. A bundler that aliases playwright-core to @volter/almostcdp/playwright runs code written against Playwright on this one instance. Node builtins are replaced at build time (scripts/bundle-playwright.mjs): events, buffer and path-browserify, small shims for os, util, crypto, async_hooks and process, and no-op modules for the builtins Playwright loads but never calls over a connectOverCDP transport.

playwrightMcpTools() and createPlaywrightMcpBackend(context, tools) are Playwright's own MCP browser tools, the core set @playwright/mcp serves, with the schemas it publishes: a host that speaks MCP answers tools/call with the backend over a context this realm drives, serving the subset of tools it chooses. There is no file system: actions answer without the page snapshot @playwright/mcp would save to a file (browser_snapshot answers it inline), and nothing a tool would save is written.

Verification

The acceptance harness uses the existing normal-Chrome eval server through ALMOSTCDP_BROWSER_EVAL_URL; it never launches a separate browser. Run tests inside a World. test/world/config.json selects no vendors because these fixtures use only local HTTP and WebSockets. Supply an empty temporary live-env file to the World operator; generated env files are ignored.

For a real rrweb/capture proof, build and run node test/browser/serve.mjs inside the World, register its printed app URL, and open it in normal Chrome. Then run node test/browser/drive.mjs <printed-cdp-url> in the same World. The driver checks actual decoded pixels, acknowledgement pacing, late-join snapshots, masked input, and reverse input. The page displays an upstream rrweb replayer beside the source; it is a test consumer. The product viewer is @volter/almostcdp/viewer (above).

For the embedded Chromium DevTools fixture, run node test/browser/install-devtools.mjs inside the World, then set ALMOSTCDP_DEVTOOLS_ROOT to the printed directory when starting the fixture. The installer verifies the selected upstream archive and its documented patch against test/browser/devtools-candidate.json; the historical unpatched snapshot remains in test/browser/devtools.json. This older frontend is pinned for compatibility testing; a maintained production frontend selection and distribution-notice review remain release work. There is no Browserless service or dependency.

The rrweb consumer sets triggerFocus: false: replayed focus otherwise steals native focus from an embedded DevTools canvas or another input controller. On 2026-09-23, the pinned embedded frontend displayed live frames, delivered a click to the source input, and typed devtools through its canvas; the source value became private devtools, and the rrweb reconstruction remained masked. This was a manual normal-Chrome check, recorded in test/results/embedded-viewer.json; DevTools scroll and navigation remain outstanding. A subsequent rrweb viewer check exercised overlay clicks, typing, half-scale coordinates, privacy masking, and rendered reconnect; its receipt is test/results/rrweb-viewer.json.

To repeat the rendered reconnect check, use the fixture's Drop cast and change fixture button. It closes only the fixture's cast socket and adds ten to the synthetic source count while disconnected. Inspect the replayed count and the viewer body's data-cast-epoch and data-cast-state: a new epoch must reach ready with the changed count and masked input. The fault route requires a same-origin POST and exists only in the local test server.

RuntimeDomain from @volter/almostcdp/core owns remote values and object handles independently of DOM access. Embedders supply realm evaluation, invocation, descriptions, and property descriptors. The Node VM acceptance test exercises this adapter through the public CDP endpoint; it is not yet a complete second implementation of Page, DOM, or Input.

Remote object handles and object groups belong to one CDP session. Console objects receive separate handles for each subscribed session; disconnecting or detaching releases that session's handles and pending commands. Sessions still share the live page and its JavaScript realm. Frontend DOM node IDs belong to one session; backend node IDs identify the same node across sessions. Navigation invalidates both, and disabling DOM invalidates that session's frontend IDs. Execution-world isolation still needs work before full session conformance can be claimed.

Rejected or timed-out domain enable/disable commands restore event routing, including overlapping commands whose responses arrive out of order. When a surface disconnects, its direct page sockets close; an old connection cannot silently start controlling a replacement surface that reuses the target ID.

A failed or timed-out screencast start releases its ownership and requests surface cleanup. Screencast frame events must carry the internal audience from the start command's sessionId; the dispatcher strips it from direct-client events and rejects frames from a previous owner. DOM capture callbacks also carry a generation check so stopped or superseded work cannot emit late frames.

For an embedded viewer, AlmostCDP retains the target's logical input focus when the viewer takes native browser focus to collect keys. Input remains synthetic; this does not grant trusted events or browser-level keyboard privileges.

The rrweb recorder must run in the document it records. Input values are masked by default; consumers can explicitly override rrweb recording options. New cast connections request a fresh snapshot. Cast data is not retained on the server; slow consumers are disconnected once queued output exceeds 8 MiB.

bindCastInput from @volter/almostcdp/cast-input binds a viewer-owned overlay to the cast input wire. Supply a sender (method, params) and a viewport callback with the displayed rectangle and original source dimensions. It maps mouse, wheel and touch coordinates through display scaling and forwards keyboard and text input. Keys carry their legacy key code, as Chromium's editing keys need; Enter carries \r; a soft keyboard's deletions become Backspace and Delete; the paste shortcut is left to the overlay's paste event, which carries the viewer's clipboard. The returned function removes listeners and restores the overlay's editable and tab-index attributes. test/browser/viewer.ts uses this with the rrweb replayer. Scaled click and typing are covered through the public cast endpoint. Dragging beyond the viewport and full viewer IME behavior remain acceptance work.

connectCast from @volter/almostcdp/cast-client manages the viewer connection. It calls onReset on each new connection so the consumer can discard the old replayer, then waits for rrweb metadata and a full snapshot before enabling sendInput. Early incremental events are dropped; offline input is rejected without queueing. Retries use bounded exponential backoff. Policy failures stop retrying, and missing targets are explicitly transient. The rrweb fixture uses this client; the endpoint test proves recovery after surface replacement. Full rendered-replayer recovery still needs browser acceptance.

createAlmostCdpServer({ authorize(request) { ... } }) accepts a host callback returning "control", "view", or false. It gates discovery and WebSocket upgrades. View-only sockets can receive casts but cannot send input or open CDP control sockets. Hosts own credential storage and origin policy.

connectDomSurface({ inputPolicy({ method, element }) { ... } }) is the embedding's policy for what a control client's input may act on. It is asked before a pointer press, a wheel turn, a touch start, a key down or text is dispatched, and before a file input is given files, with the element the input lands on (the one under the point, or the focused one for keys and text); and before Page.navigate, Page.reload or Page.navigateToHistoryEntry, with no element and the address when the command names one. A returned reason refuses the command with that message and nothing is dispatched. An extension that lends a person's own tab uses it to keep password fields and controls that pay, send or delete out of a client's reach.

connectDomSurface({ dialogs: "person" }) leaves the page's alert, confirm and prompt to the person while a client observes the page: the browser shows each one, the page gets the person's answer, and the client hears Page.javascriptDialogOpening (with hasBrowserHandler: true) and Page.javascriptDialogClosed with that answer; Page.handleJavaScriptDialog is refused. By default ("client") a surface dismisses them at once and reports them.

Chromium debugging-pipe bridge (in progress)

The private package installs almostcdp-chromium. Set ALMOSTCDP_ENDPOINT to an existing AlmostCDP HTTP discovery base or browser WebSocket URL; optionally set ALMOSTCDP_TOKEN for bearer authentication. The host must already be running. The executable accepts Chromium's --remote-debugging-pipe, reads NUL-delimited JSON on descriptor 3, and writes responses/events on descriptor 4. It preserves protocol messages and errors. Discovery redirects and foreign-origin sockets are refused. Messages and queued upstream traffic are bounded to 16 MiB.

Pinned Playwright can launch this transport using executablePath pointing to node_modules/.bin/almostcdp-chromium. No Chrome is launched and no profile is created by the executable. Other Chromium launch flags do not configure the existing host: profile, network, rendering, and isolation remain host-owned. Browser.close ends only this relay and its CDP connection; it leaves the host and existing surfaces running. Pipe EOF and process termination also detach.

This is a transport proof, not yet support for unchanged page tests. browser.newContext() still rejects Target.createBrowserContext; creating pages, enforcing downloads and applying launch options need host lifecycle adapters. test/pipe.test.mjs exercises the public endpoint with the pinned unmodified Playwright launcher and records that explicit limitation.

For the outstanding DevTools navigation proof, the same fixture server serves /devtools-proof. Its host keeps the AlmostCDP connection alive while the 600×350 source iframe navigates between /navigation-first and /navigation-second; capture reconstructs that iframe's current document. Open the embedded frontend with Open upstream DevTools. This fixture is separate from the rrweb proof. The manual checks in test/results/devtools-navigation.json passed canvas click, keyboard input, wheel scrolling, address-bar navigation, and canvas clicking on the new document against revision 825e079. The host retained its protocol connection across navigation. This remains a manual proof on the pinned historical frontend; automatic reruns and a maintained production frontend are still outstanding.

The receipt also preserves earlier intermittent controller timeouts. Thirty capture probes completed in 32–179 ms while the source remained responsive, and timeouts also occurred without a screencast; capture cost was not established as the cause. capture-probe.js exports measureCapture(targetWindow) for a single measurement without starting a stream. No browser restart or source reload was used to resolve those observations.

Current DevTools candidate

The selected fixture is recorded in test/browser/devtools-candidate.json, which pins Chromium snapshot 1703215, its exact DevTools source revision, and SHA-256 digests for both upstream archives. Install it inside the World with:

node test/browser/install-devtools.mjs --pin test/browser/devtools-candidate.json

For offline installation, pass the frontend ZIP as the positional argument and --source-archive <source.zip>. Both archives must match their pinned digests. The installer retains the source archive's license, notice, copyright and README.chromium files in the cache's notices/ directory, with an inventory in artifact.json. Redistributing the frontend must include these notices; the printed frontend directory alone is not a complete distribution.

The unpatched native failure is preserved in test/results/devtools-current-native.json. DevTools supplied the DOM keycode as a platform-native keycode; the original r keydown stalled isolated macOS Chrome. The selected artifact omits that field, as Playwright does. The pin records the exact one-line patch and hashes before/after; a patch-specific cache key keeps it separate from the pristine archive. No Volter Browsers protocol rewriting is involved.

The patched native and AlmostCDP walks passed click, full rapid typing, wheel, address-bar navigation and post-navigation input through authenticated Volter Browsers publication. Native Chrome uses the consumer's explicit Activate target button when hidden. The receipt is test/results/devtools-patched-viewer.json. Automatic viewer reruns remain open; this is a manual compatibility proof.

Host-owned browser contexts

An embedding host can supply contextHost to createAlmostCdpServer or createDispatcher. Its createContext(options, signal) must create an isolated storage/network realm using the supplied ID, honor the requested proxy/origin options or reject them, and observe cancellation. disposeContext(id) must close that realm's pages and release storage; it must tolerate partial creation.

AlmostCDP maps these hooks to Target.createBrowserContext, Target.disposeBrowserContext, and Target.getBrowserContexts. Without a host, creation/disposal remain explicit unsupported errors.

A pageHost adds createPage({ id, url, browserContextId? }, signal) and closePage(id). Creation must register a live surface with that ID before resolving (the public discovery endpoint is a registration barrier). AlmostCDP then returns Target.createTarget success and reports the assigned context in target events. Closing a managed page releases it through the host and detaches its sessions. Unmanaged live surfaces are not owned by this lifecycle API. Unsupported launch options fail rather than being silently ignored.

This is not yet a full Playwright browser: download policy and a concrete isolation/page adapter with real-browser proofs remain required. The acceptance tests for this seam use an in-memory allocation host and do not certify storage isolation.