Files
zeekay 9cf3ddd45a Join the ZAP router over a paired loopback WebSocket
The extension reached the router through a native-messaging host the
browser had to exec. Snap and Flatpak browsers refuse to exec anything
outside their sandbox, so it never connected there, and the host needed a
manifest per browser plus a daemon someone had to start.

The router is now embedded in hanzo-mcp and the other Hanzo processes, and
one of them serves a WebSocket door on 127.0.0.1:20000+uid%10000.
shared/socket.ts is the one transport: each binary message is one ZAP
router envelope, and ensureZap() is the one reconciler on one clock (a 20 s
heartbeat while registered, which also keeps the MV3 worker alive; 250 ms
doubling to 30 s after a drop, so a router moving to another process is
followed in under a second).

Pairing is explicit. `hanzo-mcp pair` prints ws://127.0.0.1:<port>/#<token>;
the popup takes it once, and shared/zap.ts keeps it in storage.local. The
router proves the token first (HMAC over both nonces) and the extension
says nothing to a door that cannot, so a process squatting on the port is
never obeyed; the token never crosses the wire. The node is
browser/<host>/<engine>-<id>, its name minted once per install.

The nativeMessaging permission, native-zap.ts and the Safari Swift bridge
are gone: WebKit opens ws://127.0.0.1 from an extension's secure context,
so Safari runs the same code. Browser extension 1.9.58.

Claude-Session: https://claude.ai/code/session_01Dc8DXc3fEQCRHUa6XWK4Ku
2026-09-30 06:57:35 -07:00

26 lines
1.0 KiB
TypeScript

import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
/**
* Safari joins the ZAP router the way Chrome and Firefox do: a WebSocket to the
* router's loopback door (shared/socket.ts). WebKit opens ws://127.0.0.1 from a
* secure context, which an extension's pages are, so Safari needs no native
* handler of its own and ships the converter's stock one untouched.
*/
const root = join(__dirname, '..', 'src');
const build = readFileSync(join(root, 'build.js'), 'utf8');
describe('Safari', () => {
it('carries no native bridge of its own', () => {
expect(existsSync(join(root, 'safari'))).toBe(false);
expect(build).not.toMatch(/SafariWebExtensionHandler\.swift/);
});
it('separates a missing converter from a failed conversion', () => {
// One `catch {}` around both reported "Xcode not available" for every
// failure, and the project read as generated when nothing was written.
expect(build).toMatch(/haveConverter/);
});
});