OverviewFilesCommitsIssuesDocsReleasesPeers

hypermosh-shell commits / 6f9993e

name registry: resolve names via the shared hyperident HandleRegistry
Wires step 3 of name resolution (was a TODO): on a local-store/raw-pubkey miss, query the shared hyperident HandleRegistry (the same one hyperident-shell uses) and resolve the mosh server key from the entry's metadata.moshKey (falling back to devicePubkey), caching the hit into the local names store. One handle can serve both the shell and mosh off one entry. - lib/registry.mjs: openRegistry (read replica over Hyperswarm) + resolveEntry / resolveMoshKey / moshKeyFromEntry; registry key defaults to the shared global key, overridable via HYPERMOSH_REGISTRY / ~/.hypermosh/config.json. - lib/names.mjs: resolveAsync (local → raw → registry → cache); registry deps lazy-imported so id/names/offline resolves stay light. - bin: client resolves via resolveAsync (user@name-or-key). - deps: hyper-identity (file:../packages/hyper-identity — now a sibling submodule), corestore, hyperswarm. - test/registry.test.mjs: metadata.moshKey preferred, devicePubkey fallback, unknown→null (deterministic, no network). Added to `npm test`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6f9993ef1566773dafa9f5cd86c2ae7935cf1952
by plankatron <fleeky@gmail.com> · 2026-07-20T03:23:12.000Z

Changed files (7) +522 -8

README.md +11 -3
……@@ -96,9 +96,17 @@ hypermosh add-name <name> <pubkey> # optional: remember a name
9696hypermosh names # list known names
9797```
9898
99- **Names are optional.** A raw public key always works; `add-name` records a
100 `name -> pubkey` mapping in `~/.hypermosh/names` (SSH-known_hosts style) so you can
101 say `hypermosh you@myserver`.
99- **Names are optional.** A raw public key always works. Resolution order:
100 1. **local names store** — `~/.hypermosh/names` (SSH-known_hosts style; `add-name` writes it), instant/offline.
101 2. **raw z-base32/hex pubkey** — used directly.
102 3. **shared hyperident registry** — the P2P `HandleRegistry` (same one hyperident-shell
103 uses). A name resolves to the mosh server key via the entry's **`metadata.moshKey`**
104 (falling back to `devicePubkey`), and the hit is cached into the local names store.
105 So one handle (e.g. `celltron`) can serve both `hyperident-shell celltron` and
106 `hypermosh celltron` off the same entry.
107
108 The registry key defaults to the shared global registry; override per-machine with
109 `HYPERMOSH_REGISTRY=<64-hex>` or `~/.hypermosh/config.json` `{ "registryKey": "…" }`.
102110
103111## How it works (default SSH mode)
104112
bin/hypermosh.mjs +2 -2
……@@ -22,7 +22,7 @@ import cp from '#node:child_process'
2222import { serveSession, connectSession } from '../lib/session.mjs'
2323import { serveSSH, connectSSH } from '../lib/ssh-mode.mjs'
2424import { loadOrCreateKey, loadDeviceKeyPair, readAllowed, HypercoreId } from '../lib/identity.mjs'
25import { resolve as resolveName } from '../lib/names.mjs'
25import { resolveAsync as resolveName } from '../lib/names.mjs'
2626import { addName, loadNames } from '../lib/names.mjs'
2727
2828function flags (argv) {
……@@ -108,7 +108,7 @@ async function main () {
108108 // one-command client
109109 const { user, host } = splitTarget(cmd)
110110 let remoteKey
111 try { remoteKey = resolveName(host) } catch (e) { console.error('hypermosh: ' + e.message); process.exit(1) }
111 try { remoteKey = await resolveName(host) } catch (e) { console.error('hypermosh: ' + e.message); process.exit(1) }
112112
113113 let keyPair = null
114114 try { keyPair = loadDeviceKeyPair() } catch { /* ephemeral identity is fine */ }
lib/names.mjs +23 -0
……@@ -50,3 +50,26 @@ export function resolve (nameOrKey, file = namesFile()) {
5050 throw new Error('cannot resolve "' + nameOrKey + '" — not a known name or a valid pubkey.\n' +
5151 ' add it with: hypermosh add-name ' + nameOrKey + ' <pubkey>')
5252}
53
54// Async resolve: local store / raw pubkey (offline, instant) → the shared
55// hyperident HandleRegistry (reads metadata.moshKey), caching a hit into the
56// local names store. The registry deps are lazy-imported so `id`/`names`/offline
57// resolves never pay for corestore/hyperswarm.
58export async function resolveAsync (nameOrKey, file = namesFile()) {
59 const map = loadNames(file)
60 if (map.has(nameOrKey)) return HypercoreId.decode(map.get(nameOrKey))
61 try { return HypercoreId.decode(nameOrKey) } catch {}
62 let keyStr = null
63 try {
64 const { resolveMoshKey } = await import('./registry.mjs')
65 keyStr = await resolveMoshKey(nameOrKey)
66 } catch (e) { /* registry unreachable → fall through to the helpful error */ }
67 if (keyStr) {
68 let key
69 try { key = HypercoreId.decode(keyStr) } catch { throw new Error('shared registry returned an invalid key for "' + nameOrKey + '": ' + keyStr) }
70 try { addName(nameOrKey, HypercoreId.encode(key), file) } catch {} // cache normalized
71 return key
72 }
73 throw new Error('cannot resolve "' + nameOrKey + '" — not a known name, a valid pubkey, or in the shared registry.\n' +
74 ' add it with: hypermosh add-name ' + nameOrKey + ' <pubkey>')
75}
lib/registry.mjs +97 -0
……@@ -0,0 +1,97 @@
1// P2P handle-registry integration for hypermosh — resolve a name to a mosh
2// server key via the SHARED hyperident HandleRegistry (the same registry
3// hyperident-shell uses). This is the identity-layer name registry; hypermosh
4// is a READ consumer.
5//
6// The shared registry maps `name -> { identity:{identityRoot,devicePubkey}, metadata }`.
7// hyperident-shell claims a name pointing devicePubkey at the shell address; a
8// hypermosh server runs on its OWN key (≠ the device key), so we read the mosh
9// address from `metadata.moshKey` (falling back to devicePubkey if a handle only
10// carries a single address). One handle (e.g. `celltron`) can thus serve both
11// `hyperident-shell celltron` and `hypermosh celltron` off the same entry.
12//
13// Dual-runtime (node + bare): corestore/hyperswarm/autobase all run on both.
14import Corestore from 'corestore'
15import Hyperswarm from 'hyperswarm'
16import fs from '#node:fs'
17import os from '#node:os'
18import path from '#node:path'
19import process from '#node:process'
20import { HandleRegistry } from 'hyper-identity/handle-registry'
21import { mkLog } from './log.mjs'
22
23const log = mkLog('registry')
24
25// The shared/global hyperident HandleRegistry key (what hyperident-shell uses).
26// Override per-machine via HYPERMOSH_REGISTRY or ~/.hypermosh/config.json.
27const DEFAULT_REGISTRY_KEY = 'ba3197256a5928a802c79cc824eaa26b7d5745212820e67f4f5a1d989d86d580'
28
29function homedir () { return (os.homedir && os.homedir()) || process.env.HOME || process.env.USERPROFILE || '.' }
30function configFile () { return path.join(homedir(), '.hypermosh', 'config.json') }
31function storeDir () { return path.join(homedir(), '.hypermosh', 'registry-store') }
32
33export function isRegistryKey (s) { return typeof s === 'string' && /^[0-9a-fA-F]{64}$/.test(s) }
34
35// env → config.json → shared default.
36export function loadRegistryKey () {
37 if (isRegistryKey(process.env.HYPERMOSH_REGISTRY)) return process.env.HYPERMOSH_REGISTRY
38 try {
39 const k = JSON.parse(fs.readFileSync(configFile(), 'utf8')).registryKey
40 if (isRegistryKey(k)) return k
41 } catch {}
42 return DEFAULT_REGISTRY_KEY
43}
44
45// Open a read replica of the shared registry and replicate it over Hyperswarm.
46async function openRegistry (registryKey) {
47 const store = new Corestore(storeDir())
48 const reg = new HandleRegistry(store, { bootstrap: registryKey })
49 await reg.ready()
50 const swarm = new Hyperswarm()
51 swarm.on('connection', (conn) => reg.replicate(conn))
52 swarm.join(reg.discoveryKey, { server: false, client: true })
53 await swarm.flush()
54 return {
55 reg, swarm, store,
56 async close () {
57 try { await swarm.destroy() } catch {}
58 try { await reg.close() } catch {}
59 try { await store.close() } catch {}
60 }
61 }
62}
63
64// Resolve a name to its full registry entry (or null). Joins the swarm, syncs,
65// and polls until the name resolves or the timeout elapses.
66export async function resolveEntry (name, { registryKey = loadRegistryKey(), timeoutMs = 8000 } = {}) {
67 if (!isRegistryKey(registryKey)) return null
68 log('resolving', name, 'via registry', registryKey.slice(0, 8))
69 const r = await openRegistry(registryKey)
70 try {
71 const deadline = Date.now() + timeoutMs
72 let entry = null
73 while (!entry && Date.now() < deadline) {
74 await r.reg.update()
75 entry = await r.reg.resolve(name)
76 if (!entry) await new Promise((res) => setTimeout(res, 200))
77 }
78 return entry
79 } finally {
80 await r.close()
81 }
82}
83
84// Extract the mosh server key from a registry entry: prefer metadata.moshKey,
85// fall back to the entry's devicePubkey (a handle that carries only one address).
86export function moshKeyFromEntry (entry) {
87 if (!entry) return null
88 return (entry.metadata && entry.metadata.moshKey) || (entry.identity && entry.identity.devicePubkey) || null
89}
90
91// High-level: resolve a name to the mosh SERVER key (z-base32/hex string), or null.
92export async function resolveMoshKey (name, opts = {}) {
93 const entry = await resolveEntry(name, opts)
94 const key = moshKeyFromEntry(entry)
95 if (key) log('resolved', name, '->', String(key).slice(0, 8), entry.metadata && entry.metadata.moshKey ? '(moshKey)' : '(devicePubkey fallback)')
96 return key
97}
package-lock.json +341 -2
……@@ -19,14 +19,48 @@
1919 "bare-runtime": "^1.30.3",
2020 "bare-subprocess": "^6.1.0",
2121 "compact-encoding": "^2.11.0",
22 "corestore": "^7.9.2",
23 "hyper-identity": "file:../packages/hyper-identity",
2224 "hypercore-id-encoding": "^1.2.0",
2325 "hyperdht": "^6.6.0",
26 "hyperswarm": "^4.16.0",
2427 "protomux": "^3.4.0"
2528 },
2629 "bin": {
2730 "hypermosh": "bin/hypermosh.mjs"
2831 }
2932 },
33 "../packages/hyper-identity": {
34 "version": "1.1.0",
35 "license": "MIT",
36 "dependencies": {
37 "autobase": "7.28.1",
38 "b4a": "^1.6.4",
39 "bare-fs": "^4.0.0",
40 "bare-net": "^1.0.0",
41 "bare-os": "^3.9.1",
42 "bare-path": "^3.0.0",
43 "bare-process": "^4.0.0",
44 "bip39-mnemonic": "^2.5.0",
45 "blind-pairing": "^2.3.1",
46 "hyperbee": "^2.27.3",
47 "hypercore": "^11.24.0",
48 "hypercore-crypto": "^3.0.0",
49 "hyperident": "file:../../repos/hyperident",
50 "hyperswarm": "^4.16.0",
51 "keet-identity-key": "github:holepunchto/keet-identity-key",
52 "sodium-native": "^5.0.0"
53 },
54 "devDependencies": {
55 "brittle": "^3.19.1",
56 "corestore": "^7.9.2",
57 "random-access-memory": "^6.2.1"
58 },
59 "engines": {
60 "bare": ">=1.0",
61 "node": ">=18"
62 }
63 },
3064 "node_modules/@hyperswarm/secret-stream": {
3165 "version": "6.9.1",
3266 "resolved": "https://registry.npmjs.org/@hyperswarm/secret-stream/-/secret-stream-6.9.1.tgz",
……@@ -122,7 +156,6 @@
122156 "resolved": "https://registry.npmjs.org/bare-buffer/-/bare-buffer-3.6.2.tgz",
123157 "integrity": "sha512-WT9xx12FJvWbBCkgfjuAeWfY40RW6BKVr2fK9UGrTEYKbvqhcaW0J9AMkA3Sj36Wgi+DUuGXYkZPxn5jVH61LA==",
124158 "license": "Apache-2.0",
125 "peer": true,
126159 "engines": {
127160 "bare": ">=1.20.0"
128161 }
……@@ -728,11 +761,16 @@
728761 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz",
729762 "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==",
730763 "license": "Apache-2.0",
731 "peer": true,
732764 "dependencies": {
733765 "bare-path": "^3.0.0"
734766 }
735767 },
768 "node_modules/big-sparse-array": {
769 "version": "1.0.3",
770 "resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz",
771 "integrity": "sha512-6RjV/3mSZORlMdpUaQ6rUSpG637cZm0//E54YYGtQg1c1O+AbZP8UTdJ/TchsDZcTVLmyWZcseBfp2HBeXUXOQ==",
772 "license": "MIT"
773 },
736774 "node_modules/bits-to-bytes": {
737775 "version": "1.3.0",
738776 "resolved": "https://registry.npmjs.org/bits-to-bytes/-/bits-to-bytes-1.3.0.tgz",
……@@ -831,6 +869,38 @@
831869 "b4a": "^1.3.0"
832870 }
833871 },
872 "node_modules/corestore": {
873 "version": "7.11.1",
874 "resolved": "https://registry.npmjs.org/corestore/-/corestore-7.11.1.tgz",
875 "integrity": "sha512-X6UsGFyHeAcqffTqgoDV7WDZjhfvolWabwzws6GbdqeXQY0CTM6gNpNMGEi6RGvAdZDPnFaboF5jFBy1lw1rDA==",
876 "license": "MIT",
877 "dependencies": {
878 "b4a": "^1.6.7",
879 "bare-events": "^2.8.3",
880 "hypercore": "^11.32.0",
881 "hypercore-crypto": "^3.4.2",
882 "hypercore-errors": "^1.4.0",
883 "hypercore-id-encoding": "^1.3.0",
884 "ready-resource": "^1.1.1",
885 "sodium-universal": "^5.0.1",
886 "streamx": "^2.26.0",
887 "which-runtime": "^1.2.1"
888 }
889 },
890 "node_modules/device-file": {
891 "version": "2.3.1",
892 "resolved": "https://registry.npmjs.org/device-file/-/device-file-2.3.1.tgz",
893 "integrity": "sha512-bmON44lwxJPle9N2OcH4tqM44pMGZKT8G6OkzXkz0urvqQ9LKkoQdTS6w0ztYfmLdUE27R4UUmx0+y2gEz4Jug==",
894 "license": "Apache-2.0",
895 "dependencies": {
896 "b4a": "^1.6.7",
897 "bare-fs": "^4.0.1",
898 "bare-path": "^3.0.0",
899 "fd-lock": "^2.1.0",
900 "fs-native-extensions": "^1.4.0",
901 "ready-resource": "^1.2.0"
902 }
903 },
834904 "node_modules/dht-rpc": {
835905 "version": "6.27.0",
836906 "resolved": "https://registry.npmjs.org/dht-rpc/-/dht-rpc-6.27.0.tgz",
……@@ -875,6 +945,34 @@
875945 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
876946 "license": "MIT"
877947 },
948 "node_modules/fd-lock": {
949 "version": "2.2.0",
950 "resolved": "https://registry.npmjs.org/fd-lock/-/fd-lock-2.2.0.tgz",
951 "integrity": "sha512-Il4jWBhjjgvUg+z8d0bC7ncXqB42caqKTUpnZ9jpcqiPmw8bkIixt7Kic1czbZPMxAQD/9kEqfZ5Dq77nSll0w==",
952 "license": "Apache-2.0",
953 "dependencies": {
954 "bare-fs": "^4.5.0",
955 "fs-native-extensions": "^1.4.4",
956 "ready-resource": "^1.2.0",
957 "resource-on-exit": "^1.0.0"
958 }
959 },
960 "node_modules/flat-tree": {
961 "version": "1.13.0",
962 "resolved": "https://registry.npmjs.org/flat-tree/-/flat-tree-1.13.0.tgz",
963 "integrity": "sha512-fT3HIuCPwHhFgJ20QYzDHgUG0zMmFg5cHvFiFo5h+QMSJ28TihsEVY0f8HGliuO+pOzmvjMx1odToeaEWkTnyQ==",
964 "license": "MIT"
965 },
966 "node_modules/fs-native-extensions": {
967 "version": "1.5.0",
968 "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.5.0.tgz",
969 "integrity": "sha512-nuZLFm9mGCxvyi7Llww/J4OyifKCS21nEUTAmnlTZp3FObPOvA32aCedCmt4Z+8yk+caqfClNaSCfn/P7T7FLQ==",
970 "license": "Apache-2.0",
971 "dependencies": {
972 "require-addon": "^1.1.0",
973 "which-runtime": "^1.2.0"
974 }
975 },
878976 "node_modules/generate-object-property": {
879977 "version": "2.0.0",
880978 "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-2.0.0.tgz",
……@@ -890,6 +988,39 @@
890988 "integrity": "sha512-IfTY0dKZM43ACyGvXkbG7De7WY7MxTS5VO6Juhe8oJKpCmrYYXoqp/cJMskkpi0k9H8wuXq0H+eI898/BCqvXg==",
891989 "license": "MIT"
892990 },
991 "node_modules/hyper-identity": {
992 "resolved": "../packages/hyper-identity",
993 "link": true
994 },
995 "node_modules/hypercore": {
996 "version": "11.34.1",
997 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-11.34.1.tgz",
998 "integrity": "sha512-nlrbXjI4x59akT8AwVEFLN54OicB53JDnH1xYP4j9zKDksHUVgqJFXnFwOkRr4Nr3kwCU0b9Rh3eIsU0JX5HEA==",
999 "license": "MIT",
1000 "dependencies": {
1001 "@hyperswarm/secret-stream": "^6.0.0",
1002 "b4a": "^1.1.0",
1003 "bare-events": "^2.2.0",
1004 "big-sparse-array": "^1.0.3",
1005 "compact-encoding": "^3.0.0",
1006 "fast-fifo": "^1.3.0",
1007 "flat-tree": "^1.9.0",
1008 "hypercore-crypto": "^3.2.1",
1009 "hypercore-errors": "^1.5.0",
1010 "hypercore-id-encoding": "^1.2.0",
1011 "hypercore-storage": "^3.2.0",
1012 "is-options": "^1.0.1",
1013 "nanoassert": "^2.0.0",
1014 "protomux": "^3.5.0",
1015 "quickbit-universal": "^2.2.0",
1016 "random-array-iterator": "^1.0.0",
1017 "safety-catch": "^1.0.1",
1018 "sodium-universal": "^5.0.1",
1019 "streamx": "^2.12.4",
1020 "unslab": "^1.3.0",
1021 "z32": "^1.0.0"
1022 }
1023 },
8931024 "node_modules/hypercore-crypto": {
8941025 "version": "3.7.0",
8951026 "resolved": "https://registry.npmjs.org/hypercore-crypto/-/hypercore-crypto-3.7.0.tgz",
……@@ -910,6 +1041,15 @@
9101041 "b4a": "^1.3.0"
9111042 }
9121043 },
1044 "node_modules/hypercore-errors": {
1045 "version": "1.5.0",
1046 "resolved": "https://registry.npmjs.org/hypercore-errors/-/hypercore-errors-1.5.0.tgz",
1047 "integrity": "sha512-5KQ/SuDxsvet+7qWA35Ay6zdD9WyAHQoyWHGcPUTbmJBd300gvNIJoi3oma7kp4TTCSzii6qYumNZe/s0j/saQ==",
1048 "license": "Apache-2.0",
1049 "dependencies": {
1050 "hypercore-id-encoding": "^1.3.0"
1051 }
1052 },
9131053 "node_modules/hypercore-id-encoding": {
9141054 "version": "1.3.0",
9151055 "resolved": "https://registry.npmjs.org/hypercore-id-encoding/-/hypercore-id-encoding-1.3.0.tgz",
……@@ -920,6 +1060,45 @@
9201060 "z32": "^1.0.0"
9211061 }
9221062 },
1063 "node_modules/hypercore-storage": {
1064 "version": "3.2.0",
1065 "resolved": "https://registry.npmjs.org/hypercore-storage/-/hypercore-storage-3.2.0.tgz",
1066 "integrity": "sha512-i5O5ZMkdZaNAWGuNRXUZjuzv7B41OIhDHUwLCZPjucgVmywY0ZE5PDafu6e5oPuhtghjl6S8q6Jn+POVKN2BvQ==",
1067 "license": "Apache-2.0",
1068 "dependencies": {
1069 "b4a": "^1.6.7",
1070 "bare-path": "^3.0.0",
1071 "compact-encoding": "^3.1.0",
1072 "device-file": "^2.1.2",
1073 "flat-tree": "^1.12.1",
1074 "hypercore-crypto": "^3.4.2",
1075 "hyperschema": "^1.21.0",
1076 "index-encoder": "^3.3.2",
1077 "resolve-reject-promise": "^1.0.0",
1078 "rocksdb-native": "^3.11.0",
1079 "scope-lock": "^1.2.4",
1080 "streamx": "^2.21.1",
1081 "xache": "^1.2.1"
1082 }
1083 },
1084 "node_modules/hypercore-storage/node_modules/compact-encoding": {
1085 "version": "3.3.0",
1086 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-3.3.0.tgz",
1087 "integrity": "sha512-e64XyzlBvTRJ3iScuU/U2w25Dglkm7fhrRbrGaHIwuTlNnzjRKMaQBCVl7mJRvZg7wI5a9wX1IVTXCuaAANNkw==",
1088 "license": "Apache-2.0",
1089 "dependencies": {
1090 "b4a": "^1.3.0"
1091 }
1092 },
1093 "node_modules/hypercore/node_modules/compact-encoding": {
1094 "version": "3.3.0",
1095 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-3.3.0.tgz",
1096 "integrity": "sha512-e64XyzlBvTRJ3iScuU/U2w25Dglkm7fhrRbrGaHIwuTlNnzjRKMaQBCVl7mJRvZg7wI5a9wX1IVTXCuaAANNkw==",
1097 "license": "Apache-2.0",
1098 "dependencies": {
1099 "b4a": "^1.3.0"
1100 }
1101 },
9231102 "node_modules/hyperdht": {
9241103 "version": "6.33.0",
9251104 "resolved": "https://registry.npmjs.org/hyperdht/-/hyperdht-6.33.0.tgz",
……@@ -999,6 +1178,39 @@
9991178 "b4a": "^1.3.0"
10001179 }
10011180 },
1181 "node_modules/hyperswarm": {
1182 "version": "4.17.0",
1183 "resolved": "https://registry.npmjs.org/hyperswarm/-/hyperswarm-4.17.0.tgz",
1184 "integrity": "sha512-oe86sK961Ueg7rvDN/veFwG8xH+Iv6vObPhGDkPJcDVxk/NduW41ZhAcVDnHzRbm7S0eLU7WaDUvehOYoKSpRQ==",
1185 "license": "MIT",
1186 "dependencies": {
1187 "b4a": "^1.3.1",
1188 "bare-events": "^2.2.0",
1189 "hyperdht": "^6.21.0",
1190 "safety-catch": "^1.0.2",
1191 "shuffled-priority-queue": "^2.1.0",
1192 "streamx": "^2.22.1",
1193 "unslab": "^1.3.0"
1194 }
1195 },
1196 "node_modules/index-encoder": {
1197 "version": "3.5.0",
1198 "resolved": "https://registry.npmjs.org/index-encoder/-/index-encoder-3.5.0.tgz",
1199 "integrity": "sha512-idZ1cxtZz2dRV6rUiaP9Xo99UjXbSzjcMacoQmxUMu/A7fEQcNPngvwDJYeWelQUS5XFlY71/or70lKn6XnwbQ==",
1200 "license": "Apache-2.0",
1201 "dependencies": {
1202 "b4a": "^1.6.4"
1203 }
1204 },
1205 "node_modules/is-options": {
1206 "version": "1.0.2",
1207 "resolved": "https://registry.npmjs.org/is-options/-/is-options-1.0.2.tgz",
1208 "integrity": "sha512-u+Ai74c8Q74aS8BuHwPdI1jptGOT1FQXgCq8/zv0xRuE+wRgSMEJLj8lVO8Zp9BeGb29BXY6AsNPinfqjkr7Fg==",
1209 "license": "MIT",
1210 "dependencies": {
1211 "b4a": "^1.1.1"
1212 }
1213 },
10021214 "node_modules/is-property": {
10031215 "version": "1.0.2",
10041216 "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
……@@ -1076,6 +1288,44 @@
10761288 "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
10771289 "license": "MIT"
10781290 },
1291 "node_modules/quickbit-native": {
1292 "version": "2.4.8",
1293 "resolved": "https://registry.npmjs.org/quickbit-native/-/quickbit-native-2.4.8.tgz",
1294 "integrity": "sha512-FcCcqI+nIAWGknqhtrYT5TSD7t/N+Xd8ctM+2PrIIBuwOi5hx0SxAvuPtzLIEMfT/2h9+fhBakUe2uALOHX6yw==",
1295 "license": "Apache-2.0",
1296 "optional": true,
1297 "dependencies": {
1298 "require-addon": "^1.1.0"
1299 }
1300 },
1301 "node_modules/quickbit-universal": {
1302 "version": "2.2.0",
1303 "resolved": "https://registry.npmjs.org/quickbit-universal/-/quickbit-universal-2.2.0.tgz",
1304 "integrity": "sha512-w02i1R8n7+6pEKTud8DfF8zbFY9o7RtPlUc3jWbtCkDKvhbx/AvV7oNnz4/TcmsPGpSJS+fq5Ud6RH6+YPvSGg==",
1305 "license": "ISC",
1306 "dependencies": {
1307 "b4a": "^1.6.0",
1308 "simdle-universal": "^1.1.0"
1309 },
1310 "optionalDependencies": {
1311 "quickbit-native": "^2.2.0"
1312 }
1313 },
1314 "node_modules/random-array-iterator": {
1315 "version": "1.0.0",
1316 "resolved": "https://registry.npmjs.org/random-array-iterator/-/random-array-iterator-1.0.0.tgz",
1317 "integrity": "sha512-u7xCM93XqKEvPTP6xZp2ehttcAemKnh73oKNf1FvzuVCfpt6dILDt1Kxl1LeBjm2iNIeR49VGFhy4Iz3yOun+Q==",
1318 "license": "MIT"
1319 },
1320 "node_modules/ready-resource": {
1321 "version": "1.2.0",
1322 "resolved": "https://registry.npmjs.org/ready-resource/-/ready-resource-1.2.0.tgz",
1323 "integrity": "sha512-nfcco/8iAFV0M+2PYnmIc+/xY0iRb35d42HFHQ7AfjulbGEAFa+XWpByfwSyeVeiBoMLLFVMv1HixxNCqzSQ1g==",
1324 "license": "MIT",
1325 "dependencies": {
1326 "bare-events": "^2.2.0"
1327 }
1328 },
10791329 "node_modules/record-cache": {
10801330 "version": "1.2.0",
10811331 "resolved": "https://registry.npmjs.org/record-cache/-/record-cache-1.2.0.tgz",
……@@ -1085,6 +1335,12 @@
10851335 "b4a": "^1.3.1"
10861336 }
10871337 },
1338 "node_modules/refcounter": {
1339 "version": "1.0.0",
1340 "resolved": "https://registry.npmjs.org/refcounter/-/refcounter-1.0.0.tgz",
1341 "integrity": "sha512-1WosVzUy0kPUaPMEtlNDwm99UsteALIhXXR8rerELoa63WkYIXAl0hxgwPFrIYBRWZPGUyekQ04FRtPJ7dHk9w==",
1342 "license": "Apache-2.0"
1343 },
10881344 "node_modules/require-addon": {
10891345 "version": "1.2.0",
10901346 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
……@@ -1111,18 +1367,95 @@
11111367 "node": ">=18"
11121368 }
11131369 },
1370 "node_modules/resolve-reject-promise": {
1371 "version": "1.1.0",
1372 "resolved": "https://registry.npmjs.org/resolve-reject-promise/-/resolve-reject-promise-1.1.0.tgz",
1373 "integrity": "sha512-LWsTOA91AqzBTjSGgX79Tc130pwcBK6xjpJEO+qRT5IKZ6bGnHKcc8QL3upUBcWuU8OTIDzKK2VNSwmmlqvAVg==",
1374 "license": "MIT"
1375 },
1376 "node_modules/resource-on-exit": {
1377 "version": "1.0.0",
1378 "resolved": "https://registry.npmjs.org/resource-on-exit/-/resource-on-exit-1.0.0.tgz",
1379 "integrity": "sha512-ViJwJAknCkLRJRPR+9SISQQ7R5eRgtdIHLJsM2hHx1MweAJbJxJ5XnMjjq0Lc7ZGv44ufzAqds1nKxiVkdy4ag==",
1380 "license": "Apache-2.0"
1381 },
1382 "node_modules/rocksdb-native": {
1383 "version": "3.17.2",
1384 "resolved": "https://registry.npmjs.org/rocksdb-native/-/rocksdb-native-3.17.2.tgz",
1385 "integrity": "sha512-6kijLkb7NrUFCyWQsIxKIU/iQASxdqLDWrteAeXVEq1MJWrJUcXP6MDPRh8Ux7im2sBereDiI/D4vxYV2ia90A==",
1386 "license": "Apache-2.0",
1387 "dependencies": {
1388 "compact-encoding": "^3.0.0",
1389 "ready-resource": "^1.0.0",
1390 "refcounter": "^1.0.0",
1391 "require-addon": "^1.0.2",
1392 "resolve-reject-promise": "^1.1.0",
1393 "signal-promise": "^1.0.3",
1394 "streamx": "^2.16.1"
1395 },
1396 "engines": {
1397 "bare": ">=1.16.0"
1398 }
1399 },
1400 "node_modules/rocksdb-native/node_modules/compact-encoding": {
1401 "version": "3.3.0",
1402 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-3.3.0.tgz",
1403 "integrity": "sha512-e64XyzlBvTRJ3iScuU/U2w25Dglkm7fhrRbrGaHIwuTlNnzjRKMaQBCVl7mJRvZg7wI5a9wX1IVTXCuaAANNkw==",
1404 "license": "Apache-2.0",
1405 "dependencies": {
1406 "b4a": "^1.3.0"
1407 }
1408 },
11141409 "node_modules/safety-catch": {
11151410 "version": "1.0.3",
11161411 "resolved": "https://registry.npmjs.org/safety-catch/-/safety-catch-1.0.3.tgz",
11171412 "integrity": "sha512-Zq+J1TefpoEq/HTUabo0YXX5MNvttjWYODGohgPBO2jfko8Wqx3JYMgE823szDFVamdH5PlpByvfiWScTdSYDA==",
11181413 "license": "MIT"
11191414 },
1415 "node_modules/scope-lock": {
1416 "version": "1.2.4",
1417 "resolved": "https://registry.npmjs.org/scope-lock/-/scope-lock-1.2.4.tgz",
1418 "integrity": "sha512-BpSd8VCuCxW9ZitcdIC/vjs3gMaP9bRBL5nkHcyfX2VrS52n13/rHuBA2xJ/S/4DPuRdAO/Bk8pWd8eD/gHCIA==",
1419 "license": "Apache-2.0"
1420 },
1421 "node_modules/shuffled-priority-queue": {
1422 "version": "2.1.0",
1423 "resolved": "https://registry.npmjs.org/shuffled-priority-queue/-/shuffled-priority-queue-2.1.0.tgz",
1424 "integrity": "sha512-xhdh7fHyMsr0m/w2kDfRJuBFRS96b9l8ZPNWGaQ+PMvnUnZ/Eh+gJJ9NsHBd7P9k0399WYlCLzsy18EaMfyadA==",
1425 "license": "MIT",
1426 "dependencies": {
1427 "unordered-set": "^2.0.1"
1428 }
1429 },
11201430 "node_modules/signal-promise": {
11211431 "version": "1.0.3",
11221432 "resolved": "https://registry.npmjs.org/signal-promise/-/signal-promise-1.0.3.tgz",
11231433 "integrity": "sha512-WBgv0UnIq2C+Aeh0/n+IRpP6967eIx9WpynTUoiW3isPpfe1zu2LJzyfXdo9Tgef8yR/sGjcMvoUXD7EYdiz+g==",
11241434 "license": "MIT"
11251435 },
1436 "node_modules/simdle-native": {
1437 "version": "1.3.9",
1438 "resolved": "https://registry.npmjs.org/simdle-native/-/simdle-native-1.3.9.tgz",
1439 "integrity": "sha512-Isc8sP4OiiIU0mpslD4GHEnR0VQWvR/54WN7YtwEDkNdTJVWtpmvsSvsgRlw5BNGxdYXlVRegdnrSu10H/PhvA==",
1440 "license": "Apache-2.0",
1441 "optional": true,
1442 "dependencies": {
1443 "b4a": "^1.6.0",
1444 "require-addon": "^1.1.0"
1445 }
1446 },
1447 "node_modules/simdle-universal": {
1448 "version": "1.1.2",
1449 "resolved": "https://registry.npmjs.org/simdle-universal/-/simdle-universal-1.1.2.tgz",
1450 "integrity": "sha512-3n3w1bs+uwgHKQjt6arez83EywNlhZzYvNOhvAASTl/8KqNIcqr6aHyGt3JRlfuUC7iB0tomJRPlJ2cRGIpBzA==",
1451 "license": "ISC",
1452 "dependencies": {
1453 "b4a": "^1.6.0"
1454 },
1455 "optionalDependencies": {
1456 "simdle-native": "^1.1.1"
1457 }
1458 },
11261459 "node_modules/sodium-native": {
11271460 "version": "5.1.0",
11281461 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz",
……@@ -1220,6 +1553,12 @@
12201553 "bare": ">=1.17.4"
12211554 }
12221555 },
1556 "node_modules/unordered-set": {
1557 "version": "2.0.1",
1558 "resolved": "https://registry.npmjs.org/unordered-set/-/unordered-set-2.0.1.tgz",
1559 "integrity": "sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg==",
1560 "license": "MIT"
1561 },
12231562 "node_modules/unslab": {
12241563 "version": "1.3.0",
12251564 "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz",
package.json +4 -1
……@@ -52,7 +52,7 @@
5252 }
5353 },
5454 "scripts": {
55 "test": "node test/frames.test.mjs && node test/firewall.test.mjs && node test/ssh-mode.test.mjs",
55 "test": "node test/frames.test.mjs && node test/registry.test.mjs && node test/firewall.test.mjs && node test/ssh-mode.test.mjs",
5656 "test:p2p": "node test/tunnel.test.mjs && node test/session.test.mjs"
5757 },
5858 "dependencies": {
……@@ -66,8 +66,11 @@
6666 "bare-runtime": "^1.30.3",
6767 "bare-subprocess": "^6.1.0",
6868 "compact-encoding": "^2.11.0",
69 "corestore": "^7.9.2",
70 "hyper-identity": "file:../packages/hyper-identity",
6971 "hypercore-id-encoding": "^1.2.0",
7072 "hyperdht": "^6.6.0",
73 "hyperswarm": "^4.16.0",
7174 "protomux": "^3.4.0"
7275 },
7376 "license": "Apache-2.0",
test/registry.test.mjs +44 -0
……@@ -0,0 +1,44 @@
1// registry test — the name→moshKey read path against a real HandleRegistry.
2// Creates a local registry (writer), claims handles, and asserts resolveMoshKey's
3// extraction: prefer metadata.moshKey, fall back to devicePubkey. No network.
4import Corestore from 'corestore'
5import { HandleRegistry } from 'hyper-identity/handle-registry'
6import { moshKeyFromEntry } from '../lib/registry.mjs'
7import fs from 'fs'
8import os from 'os'
9import path from 'path'
10
11let failures = 0
12const ok = (cond, msg) => { if (cond) console.log(' ✅ ' + msg); else { console.log(' ❌ ' + msg); failures++ } }
13
14console.log('[registry] name -> moshKey extraction (metadata.moshKey, devicePubkey fallback)')
15
16const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hm-reg-'))
17const store = new Corestore(dir)
18const reg = new HandleRegistry(store) // no bootstrap → this node is the writer
19await reg.ready()
20
21const ROOT = 'a'.repeat(64)
22const DEVICE = 'b'.repeat(64)
23const MOSH = 'c'.repeat(64)
24
25// 1. handle WITH metadata.moshKey → moshKey wins
26await reg.claim({ name: 'nodeA', identity: { identityRoot: ROOT, devicePubkey: DEVICE }, metadata: { kind: 'shell', moshKey: MOSH } })
27const a = await reg.resolve('nodeA')
28ok(a && a.metadata && a.metadata.moshKey === MOSH, 'claim stored metadata.moshKey')
29ok(moshKeyFromEntry(a) === MOSH, 'resolveMoshKey prefers metadata.moshKey over devicePubkey')
30
31// 2. handle WITHOUT moshKey → fall back to devicePubkey
32await reg.claim({ name: 'nodeB', identity: { identityRoot: ROOT, devicePubkey: DEVICE } })
33const b = await reg.resolve('nodeB')
34ok(moshKeyFromEntry(b) === DEVICE, 'falls back to devicePubkey when no metadata.moshKey')
35
36// 3. unknown name → null
37ok(moshKeyFromEntry(await reg.resolve('nope')) === null, 'unknown name resolves to null')
38
39await reg.close()
40try { await store.close() } catch {}
41try { fs.rmSync(dir, { recursive: true, force: true }) } catch {}
42
43console.log(failures === 0 ? '\n✅ registry test passed' : '\n❌ registry test failed')
44process.exit(failures === 0 ? 0 : 1)