agent-shim 8.0.1 → 8.0.2

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/dist/cli.cjs CHANGED
@@ -61039,7 +61039,7 @@ function registerIdentityCommand(program2, deps) {
61039
61039
  // package.json
61040
61040
  var package_default = {
61041
61041
  name: "agent-shim",
61042
- version: "8.0.1",
61042
+ version: "8.0.2",
61043
61043
  description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
61044
61044
  license: "MIT",
61045
61045
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -64665,14 +64665,11 @@ ${request6.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
64665
64665
  };
64666
64666
  const sessions = config2.interceptHosts.map((host) => {
64667
64667
  if (config2.tapHosts.includes(host)) {
64668
- const tlsAcceptor2 = effects.createTlsAcceptor(
64669
- config2.leafFor(host),
64670
- (secure) => {
64671
- pumpTapSession(secure, async (clientAlpn) => await effects.connectTlsUpstream(host, HTTPS_PORT, clientAlpn), config2.capture?.tapStream?.(host));
64672
- },
64673
- TAP_ALPN_PROTOCOLS
64674
- );
64675
- return { host, tlsAcceptor: tlsAcceptor2 };
64668
+ const serveSecure2 = (secure) => {
64669
+ pumpTapSession(secure, async (clientAlpn) => await effects.connectTlsUpstream(host, HTTPS_PORT, clientAlpn), config2.capture?.tapStream?.(host));
64670
+ };
64671
+ const tlsAcceptor2 = effects.createTlsAcceptor(config2.leafFor(host), serveSecure2, TAP_ALPN_PROTOCOLS);
64672
+ return { host, serveSecure: serveSecure2, tlsAcceptor: tlsAcceptor2 };
64676
64673
  }
64677
64674
  const handler = (request6, response) => {
64678
64675
  if (host === config2.routedHost && servedByPipeline(request6.url)) {
@@ -64686,19 +64683,29 @@ ${request6.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
64686
64683
  effects.forwardHttp(config2.upstreamFor(host), request6, response, config2.capture?.observePassthrough(request6));
64687
64684
  };
64688
64685
  const httpSession = effects.createHttpSession(handler, onUpgrade(host));
64689
- const tlsAcceptor = effects.createTlsAcceptor(config2.leafFor(host), (secure, context) => {
64690
- tunnelTokens.set(secure, context);
64686
+ const serveSecure = (secure, capability) => {
64687
+ tunnelTokens.set(secure, capability);
64691
64688
  httpSession.serve(secure);
64692
- });
64693
- return { host, httpSession, tlsAcceptor };
64689
+ };
64690
+ const tlsAcceptor = effects.createTlsAcceptor(config2.leafFor(host), serveSecure);
64691
+ return { host, serveSecure, httpSession, tlsAcceptor };
64694
64692
  });
64695
64693
  const ledger = createConnectionLedger(config2.limits, config2.isLiveCapability);
64696
64694
  const listener = await effects.listenLoopback(preferredPort, (socket) => {
64697
64695
  handleConnect(socket, { config: config2, effects, sessions, ledger });
64698
64696
  });
64699
64697
  const apiSession = sessions.find((session) => session.host === config2.routedHost);
64698
+ const sessionByHost = new Map(sessions.map((session) => [session.host, session]));
64699
+ const sniAcceptor = effects.createSniTlsAcceptor(config2.interceptHosts, config2.leafFor, (secure, host) => {
64700
+ const session = sessionByHost.get(host);
64701
+ if (session === void 0) {
64702
+ secure.destroy();
64703
+ return;
64704
+ }
64705
+ session.serveSecure(secure, config2.transparentCapability ?? "");
64706
+ });
64700
64707
  const transparent = config2.transparentPort === void 0 || apiSession === void 0 ? void 0 : await effects.listenLoopback(config2.transparentPort, (socket) => {
64701
- apiSession.tlsAcceptor.accept(socket, config2.transparentCapability ?? "");
64708
+ sniAcceptor.accept(socket);
64702
64709
  });
64703
64710
  return {
64704
64711
  port: listener.port,
@@ -64707,6 +64714,7 @@ ${request6.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
64707
64714
  ledger.close();
64708
64715
  await listener.close();
64709
64716
  await transparent?.close();
64717
+ sniAcceptor.close();
64710
64718
  for (const { httpSession, tlsAcceptor } of sessions) {
64711
64719
  tlsAcceptor.close();
64712
64720
  httpSession?.close();
@@ -65036,6 +65044,38 @@ function realConnectEffects() {
65036
65044
  }
65037
65045
  };
65038
65046
  },
65047
+ createSniTlsAcceptor: (hosts, leafFor, onSecure) => {
65048
+ const contexts = new Map(
65049
+ hosts.map((host) => {
65050
+ const leaf = leafFor(host);
65051
+ return [host, tls.createSecureContext({ key: leaf.keyPem, cert: leaf.certPem })];
65052
+ })
65053
+ );
65054
+ const server = tls.createServer(
65055
+ {
65056
+ // No default key or cert exists, so the SNI selection is the only source of credentials: an unserved name fails its handshake here, and a client that sent no SNI at all never reaches the selection and fails for want of any certificate, which is exactly the fail-closed shape this surface wants.
65057
+ SNICallback: (servername, callback) => {
65058
+ const context = contexts.get(servername);
65059
+ callback(context === void 0 ? new Error(`the transparent surface serves no host named ${servername}`) : null, context);
65060
+ }
65061
+ },
65062
+ (secure) => {
65063
+ onSecure(secure, typeof secure.servername === "string" ? secure.servername : "");
65064
+ }
65065
+ );
65066
+ server.on("clientError", (_error, socket) => {
65067
+ socket.destroy();
65068
+ });
65069
+ return {
65070
+ // The same hand-off the single-leaf acceptor uses: emitting the connection event runs the TLS server's own connection listener on a socket this process already owns (the redirect half of the connection) rather than one the server accepted itself.
65071
+ accept: (socket) => {
65072
+ server.emit("connection", socket);
65073
+ },
65074
+ close: () => {
65075
+ server.close();
65076
+ }
65077
+ };
65078
+ },
65039
65079
  createHttpSession: (handler, onUpgrade) => {
65040
65080
  const server = http.createServer(handler);
65041
65081
  server.on("clientError", (_error, socket) => {
package/dist/index.cjs CHANGED
@@ -2959,14 +2959,11 @@ ${request3.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
2959
2959
  };
2960
2960
  const sessions = config.interceptHosts.map((host) => {
2961
2961
  if (config.tapHosts.includes(host)) {
2962
- const tlsAcceptor2 = effects.createTlsAcceptor(
2963
- config.leafFor(host),
2964
- (secure) => {
2965
- pumpTapSession(secure, async (clientAlpn) => await effects.connectTlsUpstream(host, HTTPS_PORT, clientAlpn), config.capture?.tapStream?.(host));
2966
- },
2967
- TAP_ALPN_PROTOCOLS
2968
- );
2969
- return { host, tlsAcceptor: tlsAcceptor2 };
2962
+ const serveSecure2 = (secure) => {
2963
+ pumpTapSession(secure, async (clientAlpn) => await effects.connectTlsUpstream(host, HTTPS_PORT, clientAlpn), config.capture?.tapStream?.(host));
2964
+ };
2965
+ const tlsAcceptor2 = effects.createTlsAcceptor(config.leafFor(host), serveSecure2, TAP_ALPN_PROTOCOLS);
2966
+ return { host, serveSecure: serveSecure2, tlsAcceptor: tlsAcceptor2 };
2970
2967
  }
2971
2968
  const handler = (request3, response) => {
2972
2969
  if (host === config.routedHost && servedByPipeline(request3.url)) {
@@ -2980,19 +2977,29 @@ ${request3.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
2980
2977
  effects.forwardHttp(config.upstreamFor(host), request3, response, config.capture?.observePassthrough(request3));
2981
2978
  };
2982
2979
  const httpSession = effects.createHttpSession(handler, onUpgrade(host));
2983
- const tlsAcceptor = effects.createTlsAcceptor(config.leafFor(host), (secure, context) => {
2984
- tunnelTokens.set(secure, context);
2980
+ const serveSecure = (secure, capability) => {
2981
+ tunnelTokens.set(secure, capability);
2985
2982
  httpSession.serve(secure);
2986
- });
2987
- return { host, httpSession, tlsAcceptor };
2983
+ };
2984
+ const tlsAcceptor = effects.createTlsAcceptor(config.leafFor(host), serveSecure);
2985
+ return { host, serveSecure, httpSession, tlsAcceptor };
2988
2986
  });
2989
2987
  const ledger = createConnectionLedger(config.limits, config.isLiveCapability);
2990
2988
  const listener = await effects.listenLoopback(preferredPort, (socket) => {
2991
2989
  handleConnect(socket, { config, effects, sessions, ledger });
2992
2990
  });
2993
2991
  const apiSession = sessions.find((session) => session.host === config.routedHost);
2992
+ const sessionByHost = new Map(sessions.map((session) => [session.host, session]));
2993
+ const sniAcceptor = effects.createSniTlsAcceptor(config.interceptHosts, config.leafFor, (secure, host) => {
2994
+ const session = sessionByHost.get(host);
2995
+ if (session === void 0) {
2996
+ secure.destroy();
2997
+ return;
2998
+ }
2999
+ session.serveSecure(secure, config.transparentCapability ?? "");
3000
+ });
2994
3001
  const transparent = config.transparentPort === void 0 || apiSession === void 0 ? void 0 : await effects.listenLoopback(config.transparentPort, (socket) => {
2995
- apiSession.tlsAcceptor.accept(socket, config.transparentCapability ?? "");
3002
+ sniAcceptor.accept(socket);
2996
3003
  });
2997
3004
  return {
2998
3005
  port: listener.port,
@@ -3001,6 +3008,7 @@ ${request3.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
3001
3008
  ledger.close();
3002
3009
  await listener.close();
3003
3010
  await transparent?.close();
3011
+ sniAcceptor.close();
3004
3012
  for (const { httpSession, tlsAcceptor } of sessions) {
3005
3013
  tlsAcceptor.close();
3006
3014
  httpSession?.close();
@@ -3330,6 +3338,38 @@ function realConnectEffects() {
3330
3338
  }
3331
3339
  };
3332
3340
  },
3341
+ createSniTlsAcceptor: (hosts, leafFor, onSecure) => {
3342
+ const contexts = new Map(
3343
+ hosts.map((host) => {
3344
+ const leaf = leafFor(host);
3345
+ return [host, tls.createSecureContext({ key: leaf.keyPem, cert: leaf.certPem })];
3346
+ })
3347
+ );
3348
+ const server = tls.createServer(
3349
+ {
3350
+ // No default key or cert exists, so the SNI selection is the only source of credentials: an unserved name fails its handshake here, and a client that sent no SNI at all never reaches the selection and fails for want of any certificate, which is exactly the fail-closed shape this surface wants.
3351
+ SNICallback: (servername, callback) => {
3352
+ const context = contexts.get(servername);
3353
+ callback(context === void 0 ? new Error(`the transparent surface serves no host named ${servername}`) : null, context);
3354
+ }
3355
+ },
3356
+ (secure) => {
3357
+ onSecure(secure, typeof secure.servername === "string" ? secure.servername : "");
3358
+ }
3359
+ );
3360
+ server.on("clientError", (_error, socket) => {
3361
+ socket.destroy();
3362
+ });
3363
+ return {
3364
+ // The same hand-off the single-leaf acceptor uses: emitting the connection event runs the TLS server's own connection listener on a socket this process already owns (the redirect half of the connection) rather than one the server accepted itself.
3365
+ accept: (socket) => {
3366
+ server.emit("connection", socket);
3367
+ },
3368
+ close: () => {
3369
+ server.close();
3370
+ }
3371
+ };
3372
+ },
3333
3373
  createHttpSession: (handler, onUpgrade) => {
3334
3374
  const server = http.createServer(handler);
3335
3375
  server.on("clientError", (_error, socket) => {
package/dist/index.mjs CHANGED
@@ -2793,14 +2793,11 @@ ${request3.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
2793
2793
  };
2794
2794
  const sessions = config.interceptHosts.map((host) => {
2795
2795
  if (config.tapHosts.includes(host)) {
2796
- const tlsAcceptor2 = effects.createTlsAcceptor(
2797
- config.leafFor(host),
2798
- (secure) => {
2799
- pumpTapSession(secure, async (clientAlpn) => await effects.connectTlsUpstream(host, HTTPS_PORT, clientAlpn), config.capture?.tapStream?.(host));
2800
- },
2801
- TAP_ALPN_PROTOCOLS
2802
- );
2803
- return { host, tlsAcceptor: tlsAcceptor2 };
2796
+ const serveSecure2 = (secure) => {
2797
+ pumpTapSession(secure, async (clientAlpn) => await effects.connectTlsUpstream(host, HTTPS_PORT, clientAlpn), config.capture?.tapStream?.(host));
2798
+ };
2799
+ const tlsAcceptor2 = effects.createTlsAcceptor(config.leafFor(host), serveSecure2, TAP_ALPN_PROTOCOLS);
2800
+ return { host, serveSecure: serveSecure2, tlsAcceptor: tlsAcceptor2 };
2804
2801
  }
2805
2802
  const handler = (request3, response) => {
2806
2803
  if (host === config.routedHost && servedByPipeline(request3.url)) {
@@ -2814,19 +2811,29 @@ ${request3.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
2814
2811
  effects.forwardHttp(config.upstreamFor(host), request3, response, config.capture?.observePassthrough(request3));
2815
2812
  };
2816
2813
  const httpSession = effects.createHttpSession(handler, onUpgrade(host));
2817
- const tlsAcceptor = effects.createTlsAcceptor(config.leafFor(host), (secure, context) => {
2818
- tunnelTokens.set(secure, context);
2814
+ const serveSecure = (secure, capability) => {
2815
+ tunnelTokens.set(secure, capability);
2819
2816
  httpSession.serve(secure);
2820
- });
2821
- return { host, httpSession, tlsAcceptor };
2817
+ };
2818
+ const tlsAcceptor = effects.createTlsAcceptor(config.leafFor(host), serveSecure);
2819
+ return { host, serveSecure, httpSession, tlsAcceptor };
2822
2820
  });
2823
2821
  const ledger = createConnectionLedger(config.limits, config.isLiveCapability);
2824
2822
  const listener = await effects.listenLoopback(preferredPort, (socket) => {
2825
2823
  handleConnect(socket, { config, effects, sessions, ledger });
2826
2824
  });
2827
2825
  const apiSession = sessions.find((session) => session.host === config.routedHost);
2826
+ const sessionByHost = new Map(sessions.map((session) => [session.host, session]));
2827
+ const sniAcceptor = effects.createSniTlsAcceptor(config.interceptHosts, config.leafFor, (secure, host) => {
2828
+ const session = sessionByHost.get(host);
2829
+ if (session === void 0) {
2830
+ secure.destroy();
2831
+ return;
2832
+ }
2833
+ session.serveSecure(secure, config.transparentCapability ?? "");
2834
+ });
2828
2835
  const transparent = config.transparentPort === void 0 || apiSession === void 0 ? void 0 : await effects.listenLoopback(config.transparentPort, (socket) => {
2829
- apiSession.tlsAcceptor.accept(socket, config.transparentCapability ?? "");
2836
+ sniAcceptor.accept(socket);
2830
2837
  });
2831
2838
  return {
2832
2839
  port: listener.port,
@@ -2835,6 +2842,7 @@ ${request3.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
2835
2842
  ledger.close();
2836
2843
  await listener.close();
2837
2844
  await transparent?.close();
2845
+ sniAcceptor.close();
2838
2846
  for (const { httpSession, tlsAcceptor } of sessions) {
2839
2847
  tlsAcceptor.close();
2840
2848
  httpSession?.close();
@@ -3164,6 +3172,38 @@ function realConnectEffects() {
3164
3172
  }
3165
3173
  };
3166
3174
  },
3175
+ createSniTlsAcceptor: (hosts, leafFor, onSecure) => {
3176
+ const contexts = new Map(
3177
+ hosts.map((host) => {
3178
+ const leaf = leafFor(host);
3179
+ return [host, tls.createSecureContext({ key: leaf.keyPem, cert: leaf.certPem })];
3180
+ })
3181
+ );
3182
+ const server = tls.createServer(
3183
+ {
3184
+ // No default key or cert exists, so the SNI selection is the only source of credentials: an unserved name fails its handshake here, and a client that sent no SNI at all never reaches the selection and fails for want of any certificate, which is exactly the fail-closed shape this surface wants.
3185
+ SNICallback: (servername, callback) => {
3186
+ const context = contexts.get(servername);
3187
+ callback(context === void 0 ? new Error(`the transparent surface serves no host named ${servername}`) : null, context);
3188
+ }
3189
+ },
3190
+ (secure) => {
3191
+ onSecure(secure, typeof secure.servername === "string" ? secure.servername : "");
3192
+ }
3193
+ );
3194
+ server.on("clientError", (_error, socket) => {
3195
+ socket.destroy();
3196
+ });
3197
+ return {
3198
+ // The same hand-off the single-leaf acceptor uses: emitting the connection event runs the TLS server's own connection listener on a socket this process already owns (the redirect half of the connection) rather than one the server accepted itself.
3199
+ accept: (socket) => {
3200
+ server.emit("connection", socket);
3201
+ },
3202
+ close: () => {
3203
+ server.close();
3204
+ }
3205
+ };
3206
+ },
3167
3207
  createHttpSession: (handler, onUpgrade) => {
3168
3208
  const server = http.createServer(handler);
3169
3209
  server.on("clientError", (_error, socket) => {
@@ -1509,6 +1509,13 @@ interface TlsAcceptor {
1509
1509
  /** Drops every session this acceptor is terminating. */
1510
1510
  readonly close: () => void;
1511
1511
  }
1512
+ /** Presents every served host's leaf on one handshake, selected by the SNI name the client sent: the transparent surface's terminator, answering each redirected connection as the host it named. */
1513
+ interface SniTlsAcceptor {
1514
+ /** Starts the TLS handshake on a redirected socket; a handshake that completes for a served host hands its secure session to the acceptor's `onSecure` with that host's name. */
1515
+ readonly accept: (socket: net.Socket) => void;
1516
+ /** Drops every session this acceptor is terminating. */
1517
+ readonly close: () => void;
1518
+ }
1512
1519
  /** Parses HTTP over terminated sockets and serves each request through the handler, keep-alive included. */
1513
1520
  interface HttpParserSession {
1514
1521
  readonly serve: (socket: net.Socket) => void;
@@ -1522,6 +1529,8 @@ interface ConnectEffects {
1522
1529
  readonly listenLoopback: (preferredPort: number | undefined, onSocket: (socket: net.Socket) => void) => Promise<ConnectListenerHandle>;
1523
1530
  /** Builds the TLS terminator for one leaf, handing each successfully handshaked session to `onSecure` together with the context its `accept` call carried. `alpnProtocols`, when given, is advertised during the handshake, which is how a tap host's session can offer HTTP/2 the way its real server does. */
1524
1531
  readonly createTlsAcceptor: (leaf: LeafCert, onSecure: (secure: net.Socket, context: string) => void, alpnProtocols?: readonly string[]) => TlsAcceptor;
1532
+ /** Builds the transparent surface's TLS terminator: one handshake whose SNI name selects which served host's leaf answers it, so a connection redirected from any of the hosts sharing the redirected address is terminated as the host it named. A name the surface does not serve is answered with no certificate at all (a client that sends no SNI never reaches the selection and finds no default certificate either), because the only leaf that could answer such a name is another host's, which the client's own hostname check would reject anyway. No ALPN is offered: node's TLS server negotiates ALPN from the listener's own options and ignores protocols named on an SNI-selected secure context, and the API host's terminated session is parsed as HTTP/1.1, which a negotiated `h2` would strand. */
1533
+ readonly createSniTlsAcceptor: (hosts: readonly string[], leafFor: (host: string) => LeafCert, onSecure: (secure: net.Socket, host: string) => void) => SniTlsAcceptor;
1525
1534
  /** Opens a TLS connection to a real host, presenting its name as SNI, for a tap session's upstream half. `clientAlpn`, when given, is the protocol the client negotiated with the surface and the only one offered upstream, so the tap never changes the channel's protocol. */
1526
1535
  readonly connectTlsUpstream: (host: string, port: number, clientAlpn: string | undefined) => Promise<net.Socket>;
1527
1536
  /** Builds the HTTP parser bound to one request handler. `onUpgrade`, when given, owns every HTTP upgrade request arriving on a terminated session, socket and already-read bytes included; a session without one lets Node destroy the upgrade, its own no-listener behaviour. */
@@ -1550,7 +1559,7 @@ interface ConnectServerConfig {
1550
1559
  /** Whether a capability presented as a CONNECT request's proxy credential belongs to a live launch: the same constant-time check against the same registry the routed pipeline's admission makes, read fresh on every call because launches come and go. */
1551
1560
  readonly isLiveCapability: (token: string) => boolean;
1552
1561
  /**
1553
- * Binds a second listener that serves the intercept hosts' sessions to connections arriving pre-established, with no CONNECT handshake to authenticate: what an operating-system redirect (a pf rule sending the real hosts' address to this port) delivers. Only the API host's session is reachable this way, never a tap host's, because a redirect can name only addresses and the control plane shares the API host's address today; if that ever splits, the transparent listener must not follow it blindly. There is no capability to check on this surface: the connection carried no proxy credential, so every request rides the pipeline anonymously, admitted the way a tunnel's own header-less requests are (by nothing, which is exactly what they present), and the surface exists solely for clients that ignore proxies and would otherwise be invisible to the door entirely.
1562
+ * Binds a second listener that terminates redirected connections per host, with no CONNECT handshake to authenticate: what an operating-system redirect (a pf rule sending the real hosts' address to this port) delivers, pre-established. A redirect names addresses, and several real hosts share the API host's, so the client's TLS SNI name is what says which host arrived: each intercept host is served with its own leaf and its own session exactly as a CONNECT-tunnelled connection to that host is (the API host's `/v1/` rides the routed pipeline, a tap host's stream is pumped to its real host), and a name outside the intercept set, or a connection presenting none, is refused with no certificate at all, because the only leaf that could answer it is another host's. There is no capability to check on this surface: the connection carried no proxy credential, so every request rides the pipeline anonymously, admitted the way a tunnel's own header-less requests are (by nothing, which is exactly what they present), and the surface exists solely for clients that ignore proxies and would otherwise be invisible to the door entirely.
1554
1563
  */
1555
1564
  readonly transparentPort?: number;
1556
1565
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-shim",
3
- "version": "8.0.1",
3
+ "version": "8.0.2",
4
4
  "description": "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
5
5
  "license": "MIT",
6
6
  "author": "Joseph Mearman <joseph@mearman.co.uk>",