@hediet/linkrpc-cli 0.0.3-next.20261022.4 → 0.0.3-next.20261023.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 CHANGED
@@ -9,6 +9,25 @@ interfaces (`hubrpc.defaults`, `hubrpc.directory`, `hubrpc.schemas`) are
9
9
  used to discover what the endpoint exposes. Generic RPC commands work with any
10
10
  reflected interface; compiled-in views add specialized CLI and TUI renderers.
11
11
 
12
+ ## Directory discovery and consent
13
+
14
+ `linkrpc hub ls` first tries directory reads using its current identity and
15
+ capabilities. On `permissionRequired`, it asks the connected hub's approval
16
+ service for short-lived access to `hubrpc.directory::list` on that exact
17
+ directory service, then retries once. Watch mode also requests `watch`;
18
+ schema/member dump modes additionally request `hubrpc.schemas::get`.
19
+ Unsigned connections (`--no-sign`) do not request capabilities.
20
+
21
+ Timeouts, missing methods, and transport errors never trigger consent. Plain
22
+ output shows inaccessible directories and their errors. `--format json` and
23
+ `--format jsonl` include an `inaccessible` report; legacy `--json` preserves its
24
+ flat array and reports failures on stderr.
25
+
26
+ Approval is not automatic authorization: the destination must accept the
27
+ issued capability's root. If the connected hub cannot obtain a grant from an
28
+ accepted issuer, discovery still reports that directory as inaccessible.
29
+ No trust roots are added and no service is made public.
30
+
12
31
  ## Daemon-owned endpoints (`hub services`)
13
32
 
14
33
  `hub services` manages processes and existing connections **through the daemon
@@ -2239,7 +2239,7 @@ async function lsCommand(channel, opts = {}) {
2239
2239
  validateLsOptions(opts);
2240
2240
  const legacyOutput = opts.format === void 0;
2241
2241
  if (opts.dumpPath !== void 0) {
2242
- const dump = await createHubDump(channel, { maxDepth: opts.maxDepth });
2242
+ const dump = await createHubDump(channel, opts);
2243
2243
  const dumpPath = path.resolve(opts.dumpPath);
2244
2244
  await mkdir(path.dirname(dumpPath), { recursive: true });
2245
2245
  await writeFile(dumpPath, `${formatJson(dump)}\n`, "utf8");
@@ -2254,9 +2254,38 @@ async function lsCommand(channel, opts = {}) {
2254
2254
  const state = createLsState(explorer.graphSnapshot, opts);
2255
2255
  explorer.dispose();
2256
2256
  const all = state.listings;
2257
- const output = opts.withMembers ? await addMembers(channel, all, (listing) => listing.discoveredFrom) : all;
2258
- if (format === "json") return formatJson(legacyOutput ? output : state);
2259
- return renderTree(output, opts.interfaceId);
2257
+ const output = opts.withMembers ? await addMembers(createSchemaFetcher(channel, opts), all, (listing) => listing.discoveredFrom) : all;
2258
+ if (format === "json") {
2259
+ for (const item of state.inaccessible) opts.log?.(`Inaccessible directory ${item.serviceId || "(root)"}: ${item.reason}`);
2260
+ return formatJson(legacyOutput ? output : state);
2261
+ }
2262
+ return [renderTree(output, opts.interfaceId), ...state.inaccessible.map((item) => `! Inaccessible directory ${item.serviceId || "(root)"}: ${item.reason}`)].join("\n");
2263
+ }
2264
+ async function requestDirectoryAccess(session, serviceId, options) {
2265
+ const result = await session.requestAccess({
2266
+ consumer: {
2267
+ name: "linkrpc-cli",
2268
+ purpose: `Discover interfaces in ${serviceId}.`
2269
+ },
2270
+ permissions: [{
2271
+ target: {
2272
+ serviceId: { exact: serviceId },
2273
+ interfaceId: { exact: "hubrpc.directory" },
2274
+ members: [{ exact: "list" }, ...options.watch ? [{ exact: "watch" }] : []]
2275
+ },
2276
+ canInvoke: true
2277
+ }, ...options.withMembers || options.dumpPath !== void 0 || options.dumpPatchesPath !== void 0 ? [{
2278
+ target: {
2279
+ serviceId: { exact: serviceId },
2280
+ interfaceId: { exact: "hubrpc.schemas" },
2281
+ members: [{ exact: "get" }]
2282
+ },
2283
+ canInvoke: true
2284
+ }] : []],
2285
+ duration: "shortLived"
2286
+ });
2287
+ if (result.status !== "granted") throw new Error(`Directory access ${result.status}${"reason" in result && result.reason ? `: ${result.reason}` : ""}`);
2288
+ return true;
2260
2289
  }
2261
2290
  function validateLsOptions(opts) {
2262
2291
  validateSearchExpression(opts.search, "ls");
@@ -2282,6 +2311,8 @@ function validateSearchExpression(search, command) {
2282
2311
  }
2283
2312
  function explorerOptions(opts) {
2284
2313
  return {
2314
+ unlockGatedDirectory: opts.requestDirectoryAccess,
2315
+ log: opts.log,
2285
2316
  maxDepth: opts.serviceId === void 0 ? opts.maxDepth ?? DEFAULT_WALK_DEPTH : 0,
2286
2317
  ...opts.interfaceId !== void 0 ? { interfaceId: opts.interfaceId } : {},
2287
2318
  ...opts.interfacePrefix !== void 0 ? { interfaceIdPrefix: opts.interfacePrefix } : {},
@@ -2426,14 +2457,31 @@ function directoryTargetLabel(target) {
2426
2457
  function formatScopes(scopes) {
2427
2458
  return scopes.length === 0 ? "" : ` via ${scopes.join(", ")}`;
2428
2459
  }
2429
- async function addMembers(channel, listings, getTarget) {
2460
+ function createSchemaFetcher(channel, opts) {
2461
+ const approvals = /* @__PURE__ */ new Map();
2462
+ return async (interfaceId, hash, target) => {
2463
+ try {
2464
+ return await fetchSchema(channel, interfaceId, hash, target);
2465
+ } catch (error) {
2466
+ if (typeof error !== "object" || error === null || !("code" in error) || error.code !== ErrorCode.permissionRequired || !target || !opts.requestDirectoryAccess) throw error;
2467
+ let approval = approvals.get(target);
2468
+ if (!approval) {
2469
+ approval = opts.requestDirectoryAccess(target);
2470
+ approvals.set(target, approval);
2471
+ }
2472
+ if (!await approval) throw error;
2473
+ return fetchSchema(channel, interfaceId, hash, target);
2474
+ }
2475
+ };
2476
+ }
2477
+ async function addMembers(getSchema, listings, getTarget) {
2430
2478
  const cache = /* @__PURE__ */ new Map();
2431
2479
  return Promise.all(listings.map(async (listing) => {
2432
2480
  const target = getTarget(listing);
2433
2481
  const cacheKey = `${target ?? ""}\0${listing.interfaceId}\0${listing.hash}`;
2434
2482
  let members = cache.get(cacheKey);
2435
2483
  if (members === void 0) {
2436
- members = fetchSchema(channel, listing.interfaceId, listing.hash, target === "" ? void 0 : target).then((schema) => Object.entries(schema.methods).map(([name, method]) => ({
2484
+ members = getSchema(listing.interfaceId, listing.hash, target === "" ? void 0 : target).then((schema) => Object.entries(schema.methods).map(([name, method]) => ({
2437
2485
  name,
2438
2486
  kind: method.result === void 0 ? "notification" : "request"
2439
2487
  })).sort((a, b) => a.name.localeCompare(b.name)));
@@ -2469,14 +2517,14 @@ function createInitialHubDump() {
2469
2517
  };
2470
2518
  }
2471
2519
  async function createHubDump(channel, opts = {}) {
2472
- const explorer = new HubDirectoryExplorer(channel, { maxDepth: opts.maxDepth ?? DEFAULT_WALK_DEPTH });
2520
+ const explorer = new HubDirectoryExplorer(channel, explorerOptions(opts));
2473
2521
  await explorer.explore();
2474
- const cache = await fetchDumpSchemas(channel, explorer.graphSnapshot);
2522
+ const cache = await fetchDumpSchemas(createSchemaFetcher(channel, opts), explorer.graphSnapshot);
2475
2523
  const dump = buildHubDump(explorer.graphSnapshot, cache, true);
2476
2524
  explorer.dispose();
2477
2525
  return dump;
2478
2526
  }
2479
- async function fetchDumpSchemas(channel, snapshot, cache = {
2527
+ async function fetchDumpSchemas(getSchema, snapshot, cache = {
2480
2528
  schemas: /* @__PURE__ */ new Map(),
2481
2529
  errors: /* @__PURE__ */ new Map()
2482
2530
  }) {
@@ -2486,7 +2534,7 @@ async function fetchDumpSchemas(channel, snapshot, cache = {
2486
2534
  cache.errors.delete(hash);
2487
2535
  const attempts = [];
2488
2536
  for (const candidate of candidates) try {
2489
- const schema = await fetchSchema(channel, candidate.interfaceId, hash, candidate.target);
2537
+ const schema = await getSchema(candidate.interfaceId, hash, candidate.target);
2490
2538
  cache.schemas.set(hash, schema);
2491
2539
  break;
2492
2540
  } catch (error) {
@@ -2542,6 +2590,7 @@ function collectSchemaCandidates(snapshot) {
2542
2590
  return result;
2543
2591
  }
2544
2592
  async function runStreamingLs(channel, opts) {
2593
+ const getSchema = createSchemaFetcher(channel, opts);
2545
2594
  const explorer = new HubDirectoryExplorer(channel, explorerOptions(opts));
2546
2595
  const patchMode = opts.dumpPatchesPath !== void 0;
2547
2596
  const patchPath = patchMode && opts.dumpPatchesPath !== "-" ? path.resolve(opts.dumpPatchesPath) : void 0;
@@ -2565,7 +2614,7 @@ async function runStreamingLs(channel, opts) {
2565
2614
  const unsubscribe = explorer.subscribe((event) => {
2566
2615
  queue = queue.then(async () => {
2567
2616
  if (patchMode) {
2568
- if (event.type === "settled") await fetchDumpSchemas(channel, event.snapshot, schemaCache);
2617
+ if (event.type === "settled") await fetchDumpSchemas(getSchema, event.snapshot, schemaCache);
2569
2618
  const schemasComplete = event.snapshot.complete && [...collectSchemaCandidates(event.snapshot).keys()].every((hash) => schemaCache.schemas.has(hash) || schemaCache.errors.has(hash));
2570
2619
  const next = buildHubDump(event.snapshot, schemaCache, schemasComplete);
2571
2620
  for (const operation of createJsonPatch(toJsonValue(currentDump), toJsonValue(next))) await writeLine(JSON.stringify(operation));
@@ -8476,7 +8525,7 @@ async function main(rawArgv, executableName = "linkrpc") {
8476
8525
  }, { autoNegotiatePerCall: true });
8477
8526
  });
8478
8527
  program.command("ls").description("List services and interfaces. Walks the bus recursively, following `hubrpc.directory` references.").option("--interface <id>", "filter by interfaceId").option("--interface-prefix <prefix>", "filter by interfaceId prefix").option("--service <id>", "list a single service (no recursion)").option("--service-prefix <prefix>", "filter by serviceId prefix").option("--search <regexp>", "case-insensitive regexp over listings").option("--depth <n>", "max recursion depth (default 5)", (v) => Number.parseInt(v, 10)).option("--with-members", "include request and notification members from interface schemas").option("--json", "legacy flat listing JSON; use --format json for graph state").option("--format <format>", "pretty, json, or jsonl").option("--dump <file>", "write topology and all schemas to one JSON fixture file").option("--dump-patches <file>", "write progressive RFC 6902 operations as JSON Lines; use - for stdout").option("--stream", "legacy patch-log JSONL; use --format jsonl for reconstructable graph state").option("--watch", "continue observing directory changes until interrupted").action(async (opts) => {
8479
- await withChannel(endpoint, getPrincipalSpec(), async (channel) => {
8528
+ await withChannel(endpoint, getPrincipalSpec(), async (channel, session) => {
8480
8529
  const format = resolveGraphFormat(opts.format, {
8481
8530
  json: opts.json === true,
8482
8531
  stream: opts.stream === true,
@@ -8484,6 +8533,15 @@ async function main(rawArgv, executableName = "linkrpc") {
8484
8533
  });
8485
8534
  const legacyOutput = opts.format === void 0 && opts.watch !== true;
8486
8535
  const out = await lsCommand(channel, {
8536
+ requestDirectoryAccess: session === void 0 ? void 0 : (serviceId) => requestDirectoryAccess(session, serviceId, {
8537
+ watch: opts.watch,
8538
+ withMembers: opts.withMembers,
8539
+ dumpPath: opts.dump,
8540
+ dumpPatchesPath: opts.dumpPatches
8541
+ }),
8542
+ log: (message) => {
8543
+ process.stderr.write(`linkrpc: ${message}\n`);
8544
+ },
8487
8545
  interfaceId: opts.interface,
8488
8546
  interfacePrefix: opts.interfacePrefix,
8489
8547
  serviceId: opts.service,
@@ -8506,6 +8564,9 @@ async function main(rawArgv, executableName = "linkrpc") {
8506
8564
  maxDepth: typeof opts.depth === "number" && !Number.isNaN(opts.depth) ? opts.depth : void 0
8507
8565
  });
8508
8566
  if (out.length > 0) process.stdout.write(out + "\n");
8567
+ }, {
8568
+ requestReflectionAccess: false,
8569
+ autoNegotiatePerCall: false
8509
8570
  });
8510
8571
  });
8511
8572
  if (profile === "hub") {
@@ -9125,7 +9186,7 @@ Batch options:
9125
9186
  process.once("SIGINT", onSignal);
9126
9187
  process.once("SIGTERM", onSignal);
9127
9188
  try {
9128
- const { runApprovalUi } = await import("./runApprovalUi-CdC5_Qoo.js");
9189
+ const { runApprovalUi } = await import("./runApprovalUi-CNp2DhKG.js");
9129
9190
  await runApprovalUi({
9130
9191
  client,
9131
9192
  signal: stop.signal
@@ -9140,7 +9201,7 @@ Batch options:
9140
9201
  registerViewCommands(program, (action) => withChannel(endpoint, getPrincipalSpec(), action));
9141
9202
  program.command("ui").description("Launch the terminal UI (form-based browser of services + methods).").action(async () => {
9142
9203
  const acquired = await acquireConnection(endpoint);
9143
- const { runUi } = await import("./runUi-Bb6VATfB.js");
9204
+ const { runUi } = await import("./runUi-B4rV3ZfF.js");
9144
9205
  try {
9145
9206
  await runUi({
9146
9207
  endpoint: acquired.ep,
@@ -9676,4 +9737,4 @@ function safeJson(value) {
9676
9737
  //#endregion
9677
9738
  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 };
9678
9739
 
9679
- //# sourceMappingURL=cli-DJREb0Dm.js.map
9740
+ //# sourceMappingURL=cli-B93twHKF.js.map