@hediet/linkrpc-cli 0.0.3-next.20261025.3 → 0.0.3-next.20261026.1
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 +39 -2
- package/dist/chunks/{cli-D_nbiyek.js → cli-CvfXZUzx.js} +54 -9
- package/dist/chunks/cli-CvfXZUzx.js.map +1 -0
- package/dist/chunks/{runApprovalUi-Diol5P5a.js → runApprovalUi-B8Gb0cg3.js} +3 -3
- package/dist/chunks/{runApprovalUi-Diol5P5a.js.map → runApprovalUi-B8Gb0cg3.js.map} +1 -1
- package/dist/chunks/{runUi-DDJrDeBg.js → runUi-BYS9ZvbD.js} +3 -3
- package/dist/chunks/{runUi-DDJrDeBg.js.map → runUi-BYS9ZvbD.js.map} +1 -1
- package/dist/chunks/{scroll-mMFXvk6E.js → scroll-Csrgv7_4.js} +2 -2
- package/dist/chunks/{scroll-mMFXvk6E.js.map → scroll-Csrgv7_4.js.map} +1 -1
- package/dist/hub.js +1 -1
- package/dist/linkrpc.js +1 -1
- package/dist/rpc.js +1 -1
- package/package.json +7 -7
- package/dist/chunks/cli-D_nbiyek.js.map +0 -1
package/README.md
CHANGED
|
@@ -794,7 +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
|
+
| `http-export [--port <n>] [--host <address>]... [--base-domain <domain>] [--auth browser\|none]` | In the Hub profile, serve loopback ports of Hub compute nodes at `<port>.<service>.localhost` over HTTP and WebSocket; browsers must be approved once unless `--auth none`. |
|
|
798
798
|
|
|
799
799
|
The inherited `--schema <path-or-url>` option also overlays static
|
|
800
800
|
`hubrpc.defaults`, `hubrpc.directory`, and `hubrpc.schemas` reflection for
|
|
@@ -848,7 +848,7 @@ linkrpc hub http-export # listens on 127.0.0.1:80 and [::1]:80
|
|
|
848
848
|
| URL | Shows |
|
|
849
849
|
| --- | --- |
|
|
850
850
|
| `http://localhost/` | Services that can forward TCP (`?format=json` for JSON). |
|
|
851
|
-
| `http://hediet--tablet--compute.localhost/` | Listening ports of `hediet/tablet/compute` (needs `linkrpc.computeNode.ports`). |
|
|
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
852
|
| `http://3000.hediet--tablet--compute.localhost/` | Port 3000 of that compute node, including WebSocket upgrades. |
|
|
853
853
|
|
|
854
854
|
A service is always exactly one label directly below `localhost`, its
|
|
@@ -880,6 +880,43 @@ bodies are not rewritten, so they fail rather than reach this machine. Bind
|
|
|
880
880
|
loopback only: anyone who can reach the listener can use the export process's
|
|
881
881
|
Hub identity to reach every forwarded port.
|
|
882
882
|
|
|
883
|
+
**Browser approval (prototype, on by default).** Browsers must be approved once
|
|
884
|
+
before they can use the gateway; `--auth none` restores the behavior above.
|
|
885
|
+
|
|
886
|
+
- A browser without approval is redirected to `http://localhost/.linkrpc/login`.
|
|
887
|
+
The gateway creates a signing key for that browser and asks the Hub's
|
|
888
|
+
approvers (`hubAccess::requestAccess`) to let it call `tcpForward` and
|
|
889
|
+
`ports::listListening` on the compute nodes. The page shows a code, the first
|
|
890
|
+
eight characters of the browser key (`id:key:AbCdEfGh…` → `AbCd-EfGh`),
|
|
891
|
+
and continues by itself once approved. Approve only requests whose principal
|
|
892
|
+
starts with the code your browser shows.
|
|
893
|
+
- The gateway then signs that browser's calls with the browser's key and
|
|
894
|
+
granted capabilities, so compute nodes authorize the browser, not the
|
|
895
|
+
gateway. A browser approved for some machines only reaches those.
|
|
896
|
+
- `localhost` keeps the browser's session cookie. Each service site
|
|
897
|
+
(`<service>.localhost`) gets its own cookie on first visit through a
|
|
898
|
+
single-use, 30-second ticket from `localhost`, bound to that site and to a
|
|
899
|
+
state cookie set by the site. Cookies are `HttpOnly`, `Secure`
|
|
900
|
+
(Chromium treats `http://*.localhost` as secure), and never reach forwarded
|
|
901
|
+
apps; apps cannot set them either.
|
|
902
|
+
- Requests without `Sec-Fetch-*` and `Origin` headers (curl, scripts, agents)
|
|
903
|
+
are not affected and keep using the gateway's identity. Browser requests
|
|
904
|
+
other than page navigations get `401` until the browser is approved.
|
|
905
|
+
- Approved sessions are kept in `--session-file` (default
|
|
906
|
+
`%LOCALAPPDATA%\linkrpc\http-export\browser-sessions-<port>.json` or
|
|
907
|
+
`$XDG_STATE_HOME/linkrpc/http-export/…`); it contains the browser keys.
|
|
908
|
+
|
|
909
|
+
The gateway's own pages (`localhost` and `<service>.localhost`, HTML and JSON)
|
|
910
|
+
show command lines, which may contain secrets. They answer only top-level
|
|
911
|
+
navigations, same-origin requests, and clients that send no `Sec-Fetch-*` or
|
|
912
|
+
cross-origin `Origin` header (e.g. `curl`); other requests, including fetches,
|
|
913
|
+
scripts, and iframes from forwarded apps of the same machine, get `403`. They
|
|
914
|
+
send no CORS headers, forbid framing (`frame-ancestors 'none'`,
|
|
915
|
+
`X-Frame-Options: DENY`), and set `nosniff`, `no-store`, `no-referrer`, and
|
|
916
|
+
same-origin resource and opener policies. Command lines and working directories
|
|
917
|
+
are only shown for processes the compute node may inspect, e.g. not for
|
|
918
|
+
elevated or other users' processes on Windows.
|
|
919
|
+
|
|
883
920
|
Each service and port shares one `tcpForward.multiplex` session (up to 10
|
|
884
921
|
logical connections, 8 of them for pooled HTTP keep-alive), closed after five
|
|
885
922
|
idle minutes.
|
|
@@ -6,12 +6,13 @@ import { SocketServer } from "@hediet/linkrpc-hub/hub/server/node";
|
|
|
6
6
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
|
-
import
|
|
9
|
+
import { homedir, hostname } from "node:os";
|
|
10
|
+
import path, { dirname, join } from "node:path";
|
|
10
11
|
import { spawnCommand } from "@hediet/linkrpc-hub/spawn";
|
|
11
12
|
import { tapTransport } from "@hediet/linkrpc-hub/hub/server/transit";
|
|
12
13
|
import * as net from "node:net";
|
|
13
14
|
import { mkdir, open, readFile, writeFile } from "node:fs/promises";
|
|
14
|
-
import { DEFAULT_WALK_DEPTH, HubDirectoryExplorer, fetchSchema, findCoveringCapabilities, hubGrantedServiceIdInterface } from "@hediet/linkrpc/hub/common";
|
|
15
|
+
import { DEFAULT_WALK_DEPTH, HubDirectoryExplorer, fetchSchema, findCoveringCapabilities, hubAccessInterface, hubGrantedServiceIdInterface } from "@hediet/linkrpc/hub/common";
|
|
15
16
|
import { CONSOLE_INTERFACE_TAG, ConsoleTemplate, consoleAttachResultSchema, consoleServerMessageSchema, decodeConsoleData, encodeConsoleData } from "@hediet/linkrpc-infra/console";
|
|
16
17
|
import { createInterface } from "node:readline/promises";
|
|
17
18
|
import { Command, InvalidArgumentError, Option } from "commander";
|
|
@@ -3556,12 +3557,54 @@ async function httpExportCommand(options) {
|
|
|
3556
3557
|
port: options.port
|
|
3557
3558
|
})),
|
|
3558
3559
|
baseDomain: options.baseDomain,
|
|
3559
|
-
getTcpForward: (serviceId) => connection.service(serviceId).get(computeNodeTcpForwardInterface),
|
|
3560
|
+
getTcpForward: (serviceId, caller) => connection.service(serviceId).get(computeNodeTcpForwardInterface, signedAs(caller)),
|
|
3560
3561
|
listServices: () => services.get(),
|
|
3561
|
-
listPorts: async (serviceId) => (await connection.service(serviceId).get(computeNodePortsInterface).listListening({})).ports,
|
|
3562
|
+
listPorts: async (serviceId, caller) => (await connection.service(serviceId).get(computeNodePortsInterface, signedAs(caller)).listListening({})).ports,
|
|
3563
|
+
...options.auth === "browser" ? { approval: {
|
|
3564
|
+
gatewayName: hostname(),
|
|
3565
|
+
...options.sessionFile === void 0 ? {} : { stateFile: options.sessionFile },
|
|
3566
|
+
requestAccess: (request) => requestBrowserAccess(connection, request)
|
|
3567
|
+
} } : {},
|
|
3562
3568
|
onError: options.onError
|
|
3563
3569
|
});
|
|
3564
3570
|
}
|
|
3571
|
+
/** Per-user state file for approved browser sessions of a gateway on `port`. */
|
|
3572
|
+
function defaultHttpExportSessionFile(port) {
|
|
3573
|
+
const base = process.platform === "win32" ? process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local") : process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
|
|
3574
|
+
return join(base, "linkrpc", "http-export", `browser-sessions-${port}.json`);
|
|
3575
|
+
}
|
|
3576
|
+
/** Signs a browser's calls with its own key and approved capabilities instead of the gateway's identity. */
|
|
3577
|
+
function signedAs(caller) {
|
|
3578
|
+
return caller ? {
|
|
3579
|
+
signerOverride: caller.signer,
|
|
3580
|
+
capsOverride: caller.capabilities
|
|
3581
|
+
} : {};
|
|
3582
|
+
}
|
|
3583
|
+
/** Asks the Hub's approvers to grant `request.permissions` to the browser's principal; resolves once decided. */
|
|
3584
|
+
async function requestBrowserAccess(connection, request) {
|
|
3585
|
+
const result = await connection.get(hubAccessInterface).requestAccess({
|
|
3586
|
+
consumer: {
|
|
3587
|
+
name: request.name,
|
|
3588
|
+
purpose: request.purpose,
|
|
3589
|
+
principal: request.principal
|
|
3590
|
+
},
|
|
3591
|
+
permissions: request.permissions.map((permission) => ({
|
|
3592
|
+
...permission,
|
|
3593
|
+
target: {
|
|
3594
|
+
...permission.target,
|
|
3595
|
+
members: [...permission.target.members]
|
|
3596
|
+
}
|
|
3597
|
+
})),
|
|
3598
|
+
duration: "persistent"
|
|
3599
|
+
});
|
|
3600
|
+
return result.status === "granted" ? {
|
|
3601
|
+
status: "granted",
|
|
3602
|
+
capabilities: result.capabilities
|
|
3603
|
+
} : {
|
|
3604
|
+
status: result.status,
|
|
3605
|
+
..."reason" in result && result.reason ? { reason: result.reason } : {}
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3565
3608
|
/** Caches the services that implement `tcpForward`; a slow or partial walk returns what it found. */
|
|
3566
3609
|
var ForwardableServices = class {
|
|
3567
3610
|
_channel;
|
|
@@ -9090,7 +9133,7 @@ Batch options:
|
|
|
9090
9133
|
local.close();
|
|
9091
9134
|
}
|
|
9092
9135
|
});
|
|
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) => {
|
|
9136
|
+
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").addOption(new Option("--auth <mode>", "browser: browsers must be approved once through the Hub consent flow (tools without browser headers are not affected); none: no approval").choices(["browser", "none"]).default("browser")).option("--session-file <path>", "Where approved browser sessions are kept across restarts (contains browser keys)").action(async (opts) => {
|
|
9094
9137
|
const localEp = needEndpoint(endpoint);
|
|
9095
9138
|
const local = await connect(localEp, wireLog("hub"));
|
|
9096
9139
|
try {
|
|
@@ -9102,10 +9145,12 @@ Batch options:
|
|
|
9102
9145
|
port: opts.port,
|
|
9103
9146
|
hosts: opts.host ?? ["127.0.0.1", "::1"],
|
|
9104
9147
|
baseDomain: opts.baseDomain,
|
|
9148
|
+
auth: opts.auth,
|
|
9149
|
+
sessionFile: opts.sessionFile ?? defaultHttpExportSessionFile(opts.port),
|
|
9105
9150
|
onError: (error) => process.stderr.write(`linkrpc: ${error.message}\n`)
|
|
9106
9151
|
});
|
|
9107
9152
|
try {
|
|
9108
|
-
process.stderr.write(`linkrpc: HTTP export listening at ${server.urls.join(", ")}\n`);
|
|
9153
|
+
process.stderr.write(`linkrpc: HTTP export listening at ${server.urls.join(", ")}${opts.auth === "browser" ? " (browsers need approval; --auth none disables it)" : ""}\n`);
|
|
9109
9154
|
await _untilSignalled();
|
|
9110
9155
|
} finally {
|
|
9111
9156
|
await server.close();
|
|
@@ -9352,7 +9397,7 @@ Batch options:
|
|
|
9352
9397
|
process.once("SIGINT", onSignal);
|
|
9353
9398
|
process.once("SIGTERM", onSignal);
|
|
9354
9399
|
try {
|
|
9355
|
-
const { runApprovalUi } = await import("./runApprovalUi-
|
|
9400
|
+
const { runApprovalUi } = await import("./runApprovalUi-B8Gb0cg3.js");
|
|
9356
9401
|
await runApprovalUi({
|
|
9357
9402
|
client,
|
|
9358
9403
|
signal: stop.signal
|
|
@@ -9367,7 +9412,7 @@ Batch options:
|
|
|
9367
9412
|
registerViewCommands(program, (action) => withChannel(endpoint, getPrincipalSpec(), action));
|
|
9368
9413
|
program.command("ui").description("Launch the terminal UI (form-based browser of services + methods).").action(async () => {
|
|
9369
9414
|
const acquired = await acquireConnection(endpoint);
|
|
9370
|
-
const { runUi } = await import("./runUi-
|
|
9415
|
+
const { runUi } = await import("./runUi-BYS9ZvbD.js");
|
|
9371
9416
|
try {
|
|
9372
9417
|
await runUi({
|
|
9373
9418
|
endpoint: acquired.ep,
|
|
@@ -9930,4 +9975,4 @@ function safeJson(value) {
|
|
|
9930
9975
|
//#endregion
|
|
9931
9976
|
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 };
|
|
9932
9977
|
|
|
9933
|
-
//# sourceMappingURL=cli-
|
|
9978
|
+
//# sourceMappingURL=cli-CvfXZUzx.js.map
|