@hediet/linkrpc-cli 0.0.3-next.20261025.2 → 0.0.3-next.20261025.4

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/README.md CHANGED
@@ -794,6 +794,7 @@ initial document:
794
794
  | `connection destroy` | Stop the configured connection broker through its local overlay. |
795
795
  | `json-rpc-stdio <serviceId> [--params <json>]` | In the Hub profile, expose `<serviceId>::jsonRpcConnection::connectRaw` as newline-delimited JSON-RPC on stdin/stdout. Diagnostics remain on stderr. |
796
796
  | `json-rpc-export --target-endpoint <uri> --serviceId <id> --path <path> ...` | In the Hub profile, export multiple JSON-RPC factories through one authenticated raw WebSocket listener. |
797
+ | `http-export [--port <n>] [--host <address>]... [--base-domain <domain>]` | In the Hub profile, serve loopback ports of Hub compute nodes at `<port>.<service>.localhost` over HTTP and WebSocket. |
797
798
 
798
799
  The inherited `--schema <path-or-url>` option also overlays static
799
800
  `hubrpc.defaults`, `hubrpc.directory`, and `hubrpc.schemas` reflection for
@@ -834,6 +835,66 @@ Only `listen:ws://` TCP listeners are currently supported, not named pipes or TL
834
835
 
835
836
  The command stays in the foreground until SIGINT/SIGTERM, then closes clients and the listener. Use the existing Hub daemon/service lifecycle to supervise it; this command neither installs a service nor starts remote AHP hosts. Diagnostics and the bound address (including an assigned port when using port 0) go to stderr.
836
837
 
838
+ ### HTTP export
839
+
840
+ `http-export` serves loopback TCP ports of Hub compute nodes (services offering
841
+ `linkrpc.computeNode.tcpForward`) as `*.localhost` sites. `*.localhost` always
842
+ resolves to this machine, so no DNS setup is needed:
843
+
844
+ ```sh
845
+ linkrpc hub http-export # listens on 127.0.0.1:80 and [::1]:80
846
+ ```
847
+
848
+ | URL | Shows |
849
+ | --- | --- |
850
+ | `http://localhost/` | Services that can forward TCP (`?format=json` for JSON). |
851
+ | `http://hediet--tablet--compute.localhost/` | Listening ports of `hediet/tablet/compute` with each owning process's name, command line, and working directory (needs `linkrpc.computeNode.ports`). |
852
+ | `http://3000.hediet--tablet--compute.localhost/` | Port 3000 of that compute node, including WebSocket upgrades. |
853
+
854
+ A service is always exactly one label directly below `localhost`, its
855
+ segments joined with `--`. Browsers treat `localhost` as a public suffix, so
856
+ each service is its own site: pages of one machine get no `SameSite` cookies
857
+ from another machine's apps and cannot set cookies for them, as with direct
858
+ access. The ports of one service share that site, as the ports of `localhost`
859
+ do on its machine. A service ID whose segments are not plain lowercase DNS
860
+ labels, or whose label would exceed 63 characters, is written as a hashed label
861
+ instead, `<slug>--h<hash>`, which the gateway resolves against the services it
862
+ can reach. The index pages link to the right form.
863
+
864
+ There is no gateway credential and no origin check of its own. Instead, the
865
+ gateway translates between a forwarded app's own origins
866
+ (`http://localhost:<port>` on its machine) and their exported names, so the
867
+ app's own Host, Origin, CORS, redirect, and cookie checks behave as they do for
868
+ a browser on its machine:
869
+
870
+ | Header | Translation |
871
+ | --- | --- |
872
+ | `Host` | Always `localhost:<port>`. The gateway itself only accepts `*.localhost` names, so DNS rebinding cannot reach it. |
873
+ | `Origin`, `Referer` | Exported names of the same service map to `http://localhost:<port>`. Other services' exported names and foreign sites pass unchanged. This machine's own loopback origins have no counterpart on the target: `Origin` becomes `null` and `Referer` is dropped. |
874
+ | `Location`, `Access-Control-Allow-Origin` | URLs naming the target machine's loopback map to their exported names. |
875
+ | `Set-Cookie` | Host-only and `Domain=localhost` cookies get the service's domain (e.g. `Domain=hediet--tablet--compute.localhost`), which reaches that machine's exported ports, as such cookies reach all of its ports there. Cookies for other domains are dropped, as a browser there would reject them. `__Host-` cookies stay host-only, so they reach only one exported port. |
876
+
877
+ WebSocket upgrades are translated the same way in both directions. No
878
+ `X-Forwarded-*` headers are added. Absolute loopback URLs inside response
879
+ bodies are not rewritten, so they fail rather than reach this machine. Bind
880
+ loopback only: anyone who can reach the listener can use the export process's
881
+ Hub identity to reach every forwarded port.
882
+
883
+ The gateway's own pages (`localhost` and `<service>.localhost`, HTML and JSON)
884
+ show command lines, which may contain secrets. They answer only top-level
885
+ navigations, same-origin requests, and clients that send no `Sec-Fetch-*` or
886
+ cross-origin `Origin` header (e.g. `curl`); other requests, including fetches,
887
+ scripts, and iframes from forwarded apps of the same machine, get `403`. They
888
+ send no CORS headers, forbid framing (`frame-ancestors 'none'`,
889
+ `X-Frame-Options: DENY`), and set `nosniff`, `no-store`, `no-referrer`, and
890
+ same-origin resource and opener policies. Command lines and working directories
891
+ are only shown for processes the compute node may inspect, e.g. not for
892
+ elevated or other users' processes on Windows.
893
+
894
+ Each service and port shares one `tcpForward.multiplex` session (up to 10
895
+ logical connections, 8 of them for pooled HTTP keep-alive), closed after five
896
+ idle minutes.
897
+
837
898
  ### Participant proxy
838
899
 
839
900
  `proxy` stands in for a participant on the hub: the participant is either this
@@ -940,7 +1001,7 @@ Value-taking options support both `--option=value` and `--option value`.
940
1001
  Parameter assignment itself still uses `key=value`, e.g.
941
1002
  `--param path=/docs/a` or `--param=path=/docs/a`. Use the equals form for
942
1003
  dash-prefixed values. Quote JSON and strings containing spaces.
943
- `--cap:request` and `--cap:allow-any` are switches with no value.
1004
+ `--cap:request`, `--cap:allow-any`, and `--cap:delegate` are switches with no value.
944
1005
  The older `--request-cap`, `--allow-param`, and `--duration` spellings have
945
1006
  been replaced by the `--cap:` options.
946
1007
 
@@ -950,6 +1011,9 @@ Schema validation is mandatory for explicit requests: `--no-validate` and
950
1011
  the permission; signed calls include that hash in their signed metadata.
951
1012
  The default requested duration is `persistent`; `--cap:duration shortLived` and
952
1013
  `--cap:duration longLived` are also supported. The Hub still decides what to grant.
1014
+ `--cap:delegate` also requests `canDelegate`, so the identity can issue narrower
1015
+ capabilities for that scope to other identities, e.g. a service it runs; a
1016
+ stored grant without delegation does not satisfy such a request.
953
1017
  One-shot approval cannot satisfy a reusable scope and is rejected.
954
1018
 
955
1019
  Fresh durable grants for the selected principal are reused when they cover
@@ -1,4 +1,4 @@
1
- import { $ as requestTopologyAccess, B as proxyCommand, Ct as MethodRefWithOptHash, F as disconnectBroker, G as parseDuration, H as withStaticHubReflection, I as getBrokerStatus, L as readBrokerNotifications, M as readEndpointDiscovery, N as ContextStore, P as formatContextReference, Q as requestReflectionAccess, R as stopConnectionBroker, St as parseParamOverride, U as parseConnectionLimit, V as tunnelCommand, W as parseConnectionLimitMilliseconds, _t as describeSchema, a as resolveInvocationEndpoint, c as resolveLocalEndpoint, ct as readCliIdentity, dt as connect, et as setupSigning, ft as connectViaRootOverlay, gt as describeObjectParams, ht as resolvedEndpointToConfig, i as resolveInvocationContext, it as walkHub, lt as formatPrincipalSource, m as managedEndpointsInterface, mt as resolveTargetEndpoint, n as loadStaticHubSchema, nt as fetchSchema$1, o as resolveSigning, ot as cliIdentityTarget, pt as connectViaTransport, q as resolveCliInvocation, r as resolveStaticHubSchemaSource, rt as findMethodInSchema, s as validationDefault, st as deleteCliIdentity, t as completeForLine, tt as fetchDefaults, ut as parsePrincipalSpec, vt as explainValidation, wt as mcpForwardInterface, xt as mergeParams, y as HubHome, yt as validateValueAgainstSchema, z as ConnectionBroker } from "./runComplete-1DjMsU_0.js";
1
+ import { $ as requestTopologyAccess, B as proxyCommand, Ct as MethodRefWithOptHash, F as disconnectBroker, G as parseDuration, H as withStaticHubReflection, I as getBrokerStatus, L as readBrokerNotifications, M as readEndpointDiscovery, N as ContextStore, P as formatContextReference, Q as requestReflectionAccess, R as stopConnectionBroker, St as parseParamOverride, U as parseConnectionLimit, V as tunnelCommand, W as parseConnectionLimitMilliseconds, _t as describeSchema, a as resolveInvocationEndpoint, c as resolveLocalEndpoint, ct as readCliIdentity, dt as connect, et as setupSigning, ft as connectViaRootOverlay, gt as describeObjectParams, ht as resolvedEndpointToConfig, i as resolveInvocationContext, it as walkHub, lt as formatPrincipalSource, m as managedEndpointsInterface, mt as resolveTargetEndpoint, n as loadStaticHubSchema, nt as fetchSchema$1, o as resolveSigning, ot as cliIdentityTarget, pt as connectViaTransport, q as resolveCliInvocation, r as resolveStaticHubSchemaSource, rt as findMethodInSchema, s as validationDefault, st as deleteCliIdentity, t as completeForLine, tt as fetchDefaults, ut as parsePrincipalSpec, vt as explainValidation, wt as mcpForwardInterface, xt as mergeParams, y as HubHome, yt as validateValueAgainstSchema, z as ConnectionBroker } from "./runComplete-bFjlzR3Y.js";
2
2
  import { ErrorCode, INTERFACE_TEMPLATES_EXTENSION, LinkRpcConnection, RpcError, computeInterfaceHash, connectTransports, createSchemaToZod, directoryInterface, exportStaticHubSchema, generateTsContract, generateTsInterface, interfaceTemplateArgumentsEquivalent, interfaceTemplateMemberName, interfaceTemplatesEqual, isAssignable, jcsCanonicalize, matchParams, schemaToZod, validateInterfaceTemplates } from "@hediet/linkrpc";
3
3
  import { z } from "zod";
4
4
  import { WebSocketTransport, connectNdjson, connectToHub, formatEndpointUri, isHubEndpoint, openWebSocket, parseEndpointUri, runInitializeHandshake, traceMessageTransport as traceMessageTransport$1 } from "@hediet/linkrpc/node";
@@ -27,6 +27,8 @@ import { TrafficFlowAggregator, createTerminalConsentPrompt, formatFlowSummary }
27
27
  import { loadHubConfig, printHubConfigSchema } from "@hediet/linkrpc-hub/config-file";
28
28
  import { connectJsonRpcTransports, connectRawJsonRpcTransport, createNdjsonJsonRpcTransport, jsonRpcConnectionInterface } from "@hediet/linkrpc-infra/json-rpc";
29
29
  import { startJsonRpcWebSocketExport, validateJsonRpcExport } from "@hediet/linkrpc-infra/json-rpc/node";
30
+ import { computeNodePortsInterface, computeNodeTcpForwardInterface } from "@hediet/linkrpc-infra/compute-node";
31
+ import { startHttpExport } from "@hediet/linkrpc-infra/http-export/node";
30
32
  import { GRAPH_INTERFACE_TAG, IMMUTABLE_GRAPH_INTERFACE_ID, ROOT_WATCH_INTERFACE_ID, evaluateGraphPresentation, graphPresentationInterface, graphPresentationSchema, isGraphRef, standardGraphRuntimeOptions, validateGraphInterfaceSchema } from "@hediet/linkrpc-infra/graph";
31
33
  import React from "react";
32
34
  import { Box, Text, measureElement, render } from "ink";
@@ -407,7 +409,7 @@ function contextNamespace(reference) {
407
409
  async function useLocalManagement(start, action) {
408
410
  const home = HubHome.resolve();
409
411
  if (start) {
410
- const { startDaemon, nodeDaemonLaunch } = await import("./src-DPg65Jd1.js");
412
+ const { startDaemon, nodeDaemonLaunch } = await import("./src-B71xYgEO.js");
411
413
  if (await startDaemon({
412
414
  home,
413
415
  launch: nodeDaemonLaunch(home)
@@ -427,7 +429,7 @@ async function useLocalManagement(start, action) {
427
429
  }
428
430
  }
429
431
  async function restartLocalDaemon() {
430
- const { startDaemon, stopDaemon, nodeDaemonLaunch } = await import("./src-DPg65Jd1.js");
432
+ const { startDaemon, stopDaemon, nodeDaemonLaunch } = await import("./src-B71xYgEO.js");
431
433
  const home = HubHome.resolve();
432
434
  process.stderr.write("Restarting the local Hub daemon: clients will disconnect and managed workloads will stop. OS autostart settings are unchanged.\n");
433
435
  await stopDaemon(home);
@@ -947,6 +949,7 @@ async function requestCallCapability(channel, session, ref, params, schema, opts
947
949
  ...ref.hash !== void 0 ? { interfaceHash: ref.hash } : {}
948
950
  },
949
951
  canInvoke: true,
952
+ ...opts.delegate === true ? { canDelegate: true } : {},
950
953
  params: matchers
951
954
  };
952
955
  const coverageOptions = {
@@ -3541,6 +3544,72 @@ async function jsonRpcExportCommand(options) {
3541
3544
  });
3542
3545
  }
3543
3546
  //#endregion
3547
+ //#region src/commands/httpExport.ts
3548
+ const SERVICE_LIST_TTL_MS = 3e4;
3549
+ const SERVICE_LIST_TIMEOUT_MS = 2e4;
3550
+ async function httpExportCommand(options) {
3551
+ const connection = new LinkRpcConnection(options.local.rpcChannel);
3552
+ const services = new ForwardableServices(options.local.channel, options.requestDirectoryAccess, options.onError);
3553
+ return startHttpExport({
3554
+ listen: options.hosts.map((host) => ({
3555
+ host,
3556
+ port: options.port
3557
+ })),
3558
+ baseDomain: options.baseDomain,
3559
+ getTcpForward: (serviceId) => connection.service(serviceId).get(computeNodeTcpForwardInterface),
3560
+ listServices: () => services.get(),
3561
+ listPorts: async (serviceId) => (await connection.service(serviceId).get(computeNodePortsInterface).listListening({})).ports,
3562
+ onError: options.onError
3563
+ });
3564
+ }
3565
+ /** Caches the services that implement `tcpForward`; a slow or partial walk returns what it found. */
3566
+ var ForwardableServices = class {
3567
+ _channel;
3568
+ _requestDirectoryAccess;
3569
+ _onError;
3570
+ _cached;
3571
+ _pending;
3572
+ constructor(_channel, _requestDirectoryAccess, _onError) {
3573
+ this._channel = _channel;
3574
+ this._requestDirectoryAccess = _requestDirectoryAccess;
3575
+ this._onError = _onError;
3576
+ }
3577
+ get() {
3578
+ if (this._cached && Date.now() - this._cached.at < SERVICE_LIST_TTL_MS) return Promise.resolve(this._cached.value);
3579
+ this._pending ??= this._load().finally(() => {
3580
+ this._pending = void 0;
3581
+ });
3582
+ return this._pending;
3583
+ }
3584
+ async _load() {
3585
+ const explorer = new HubDirectoryExplorer(this._channel, {
3586
+ interfaceId: computeNodeTcpForwardInterface.info.id,
3587
+ timeoutMs: 5e3,
3588
+ unlockGatedDirectory: this._requestDirectoryAccess,
3589
+ log: (message) => this._onError(new Error(message))
3590
+ });
3591
+ let timer;
3592
+ try {
3593
+ const walked = await Promise.race([explorer.explore(), new Promise((resolve) => {
3594
+ timer = setTimeout(() => resolve(void 0), SERVICE_LIST_TIMEOUT_MS);
3595
+ })]);
3596
+ const listings = (walked?.listings ?? explorer.result.listings).filter((listing) => listing.serviceId !== "");
3597
+ const value = {
3598
+ serviceIds: [...new Set(listings.map((listing) => listing.serviceId))],
3599
+ complete: walked !== void 0 && walked.inaccessible.length === 0
3600
+ };
3601
+ this._cached = {
3602
+ value,
3603
+ at: Date.now()
3604
+ };
3605
+ return value;
3606
+ } finally {
3607
+ clearTimeout(timer);
3608
+ explorer.dispose();
3609
+ }
3610
+ }
3611
+ };
3612
+ //#endregion
3544
3613
  //#region ../../node_modules/.pnpm/@vscode+observables@0.1.1-0/node_modules/@vscode/observables/dist/observableInternal/logging/logging.js
3545
3614
  let globalObservableLogger;
3546
3615
  function addLogger(logger) {
@@ -8779,7 +8848,7 @@ async function main(rawArgv, executableName = "linkrpc") {
8779
8848
  "shortLived",
8780
8849
  "longLived",
8781
8850
  "persistent"
8782
- ]).default("persistent"));
8851
+ ]).default("persistent")).option("--cap:delegate", "also request the right to delegate this authority (canDelegate) to other identities");
8783
8852
  const configureCall = (cmd, capabilityOnly) => {
8784
8853
  cmd.description((capabilityOnly ? "Request a capability without executing a call. " : "Invoke a request. ") + "`<methodRef>` is `[svc::][iface::]method[@hash]`. Use `--p:<name> <value>` (or `--p:<name>=<value>`) as a shortcut for `--param <name>=<value>`. Run `hub call <methodRef> --help` to see the method-specific `--p:` flags derived from the live schema.").option("--params <json>", "inline params JSON, or \"-\" to read stdin").option("--param <kv...>", "param override, key=value (JSON or string)").option("--no-validate", "skip schema-based param validation").helpOption(false).option("-h, --help", "show help (includes method params when reachable)").action(async (methodRef, opts, cmd) => {
8785
8854
  if (opts.help) {
@@ -8795,6 +8864,7 @@ async function main(rawArgv, executableName = "linkrpc") {
8795
8864
  const allowParams = opts["cap:allowParam"];
8796
8865
  if (!capabilityOnly && allowParams !== void 0 && !requestCap) throw new Error("--cap:allow-param requires --cap:request.");
8797
8866
  if (!capabilityOnly && cmd.getOptionValueSource("cap:duration") === "cli" && !requestCap) throw new Error("--cap:duration requires --cap:request.");
8867
+ if (!capabilityOnly && opts["cap:delegate"] === true && !requestCap) throw new Error("--cap:delegate requires --cap:request.");
8798
8868
  resolveSigning(profile, g_invocation?.values ?? {}, capabilityOnly || requestCap);
8799
8869
  const validation = opts.validate === false ? "off" : effectiveValidation(profile);
8800
8870
  if ((capabilityOnly || requestCap) && validation === "off") throw new Error("Capability requests require schema validation; remove --no-validate/--validation off.");
@@ -8808,7 +8878,8 @@ async function main(rawArgv, executableName = "linkrpc") {
8808
8878
  requestCap,
8809
8879
  capabilityOnly,
8810
8880
  signingSession: session,
8811
- duration: opts["cap:duration"]
8881
+ duration: opts["cap:duration"],
8882
+ delegate: opts["cap:delegate"] === true
8812
8883
  });
8813
8884
  process.stdout.write(out + "\n");
8814
8885
  }, {
@@ -9019,6 +9090,30 @@ Batch options:
9019
9090
  local.close();
9020
9091
  }
9021
9092
  });
9093
+ program.command("http-export").description("Serve loopback ports of Hub compute nodes at <port>.<service>.localhost over HTTP and WebSocket.").option("--port <n>", "Local port to listen on", (value) => parsePositiveInteger(value, "--port"), 80).option("--host <address>", "Local address to bind (repeatable; default 127.0.0.1 and ::1)", collectOption).option("--base-domain <domain>", "Base domain of exported hostnames", "localhost").action(async (opts) => {
9094
+ const localEp = needEndpoint(endpoint);
9095
+ const local = await connect(localEp, wireLog("hub"));
9096
+ try {
9097
+ const session = await setupSigning(local.channel, local.signing, getPrincipalSpec(), { negotiateHubCaps: isHubEndpoint(localEp) });
9098
+ reportIdentity(session);
9099
+ const server = await httpExportCommand({
9100
+ local,
9101
+ requestDirectoryAccess: (serviceId) => requestDirectoryAccess(session, serviceId, {}),
9102
+ port: opts.port,
9103
+ hosts: opts.host ?? ["127.0.0.1", "::1"],
9104
+ baseDomain: opts.baseDomain,
9105
+ onError: (error) => process.stderr.write(`linkrpc: ${error.message}\n`)
9106
+ });
9107
+ try {
9108
+ process.stderr.write(`linkrpc: HTTP export listening at ${server.urls.join(", ")}\n`);
9109
+ await _untilSignalled();
9110
+ } finally {
9111
+ await server.close();
9112
+ }
9113
+ } finally {
9114
+ local.close();
9115
+ }
9116
+ });
9022
9117
  program.command("json-rpc-stdio <serviceId>").description("Expose <serviceId>::jsonRpcConnection::connectRaw as a newline-delimited JSON-RPC endpoint on this process's stdin/stdout.").option("--params <json>", "JSON value passed to the remote transport factory").action(async (serviceId, opts) => {
9023
9118
  const localEp = needEndpoint(endpoint);
9024
9119
  let params;
@@ -9257,7 +9352,7 @@ Batch options:
9257
9352
  process.once("SIGINT", onSignal);
9258
9353
  process.once("SIGTERM", onSignal);
9259
9354
  try {
9260
- const { runApprovalUi } = await import("./runApprovalUi-DTSdmhod.js");
9355
+ const { runApprovalUi } = await import("./runApprovalUi-Diol5P5a.js");
9261
9356
  await runApprovalUi({
9262
9357
  client,
9263
9358
  signal: stop.signal
@@ -9272,7 +9367,7 @@ Batch options:
9272
9367
  registerViewCommands(program, (action) => withChannel(endpoint, getPrincipalSpec(), action));
9273
9368
  program.command("ui").description("Launch the terminal UI (form-based browser of services + methods).").action(async () => {
9274
9369
  const acquired = await acquireConnection(endpoint);
9275
- const { runUi } = await import("./runUi-C8VfjCWT.js");
9370
+ const { runUi } = await import("./runUi-DDJrDeBg.js");
9276
9371
  try {
9277
9372
  await runUi({
9278
9373
  endpoint: acquired.ep,
@@ -9835,4 +9930,4 @@ function safeJson(value) {
9835
9930
  //#endregion
9836
9931
  export { formatCondition as _, formatCommandLegend as a, targetId as b, safeTerminalText as c, ObservablePromise as d, observableValue as f, resolveViewTarget as g, Disposable as h, dispatchHostKey as i, approvalDetailLines as l, derived as m, views as n, hostCommands as o, autorun as p, ViewFrame as r, matchesKey as s, runCli as t, ObservableLazyPromise as u, interfaceTarget as v, matchesTarget as y };
9837
9932
 
9838
- //# sourceMappingURL=cli-BZ-4lHyj.js.map
9933
+ //# sourceMappingURL=cli-D_nbiyek.js.map