@log0u7/opencode-mesh 0.1.2 → 0.3.0

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +48 -13
  3. package/dist/index.d.ts +6 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +7 -2
  6. package/dist/index.js.map +1 -1
  7. package/dist/lib/db.d.ts.map +1 -1
  8. package/dist/lib/db.js +19 -1
  9. package/dist/lib/db.js.map +1 -1
  10. package/dist/lib/discovery.d.ts +1 -0
  11. package/dist/lib/discovery.d.ts.map +1 -1
  12. package/dist/lib/discovery.js +27 -4
  13. package/dist/lib/discovery.js.map +1 -1
  14. package/dist/lib/identity.d.ts +0 -8
  15. package/dist/lib/identity.d.ts.map +1 -1
  16. package/dist/lib/identity.js +53 -34
  17. package/dist/lib/identity.js.map +1 -1
  18. package/dist/lib/locks.d.ts.map +1 -1
  19. package/dist/lib/locks.js +20 -12
  20. package/dist/lib/locks.js.map +1 -1
  21. package/dist/lib/peers.d.ts +9 -0
  22. package/dist/lib/peers.d.ts.map +1 -1
  23. package/dist/lib/peers.js +49 -11
  24. package/dist/lib/peers.js.map +1 -1
  25. package/dist/lib/serve.d.ts +2 -0
  26. package/dist/lib/serve.d.ts.map +1 -1
  27. package/dist/lib/serve.js +53 -68
  28. package/dist/lib/serve.js.map +1 -1
  29. package/dist/lib/spawn.d.ts +14 -4
  30. package/dist/lib/spawn.d.ts.map +1 -1
  31. package/dist/lib/spawn.js +210 -21
  32. package/dist/lib/spawn.js.map +1 -1
  33. package/dist/lib/sqlite.d.ts.map +1 -1
  34. package/dist/lib/sqlite.js +15 -4
  35. package/dist/lib/sqlite.js.map +1 -1
  36. package/dist/lib/workers.d.ts +2 -0
  37. package/dist/lib/workers.d.ts.map +1 -1
  38. package/dist/lib/workers.js +45 -12
  39. package/dist/lib/workers.js.map +1 -1
  40. package/dist/lib/worktree.d.ts +22 -4
  41. package/dist/lib/worktree.d.ts.map +1 -1
  42. package/dist/lib/worktree.js +54 -6
  43. package/dist/lib/worktree.js.map +1 -1
  44. package/dist/plugin-v2.d.ts +3 -0
  45. package/dist/plugin-v2.d.ts.map +1 -0
  46. package/dist/plugin-v2.js +108 -0
  47. package/dist/plugin-v2.js.map +1 -0
  48. package/dist/plugin.d.ts +12 -0
  49. package/dist/plugin.d.ts.map +1 -1
  50. package/dist/plugin.js +119 -266
  51. package/dist/plugin.js.map +1 -1
  52. package/dist/tools.d.ts +30 -0
  53. package/dist/tools.d.ts.map +1 -0
  54. package/dist/tools.js +308 -0
  55. package/dist/tools.js.map +1 -0
  56. package/package.json +11 -8
package/CHANGELOG.md CHANGED
@@ -7,6 +7,142 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-10-14
11
+
12
+ ### Added
13
+
14
+ - Documentation: `docs/CONFIGURATION.md` (the full options reference), and new
15
+ architecture and wire protocol docs detailing the process model, worker
16
+ lifecycle, data-dir layout, and the HTTP/mDNS/pairing protocol. The README
17
+ links them and documents the plugin options inline.
18
+ - Plugin options are validated at setup. A `port` outside `[0, 65535]` or not an
19
+ integer, a `bind` other than `loopback`/`lan`, a non-boolean `advertise`, or an
20
+ empty/non-string `dataDir` now fails fast with a clear message instead of
21
+ surfacing later as `EADDRINUSE`, a bind on the wrong interface, or a data dir
22
+ at the wrong path. `tsconfig.test.json` and a `pnpm typecheck:tests` step
23
+ typecheck the test suite, which `tsconfig.json` (src only) never did.
24
+
25
+ ### Changed
26
+
27
+ - `engines.node` is now `>=22.5.0` (the first release with `node:sqlite`), and
28
+ `@types/node` is pinned to the max Node major tested in CI (24) instead of 26.
29
+ `scripts/smoke-dist.mjs` now asserts the v2 (`setup`) entrypoint, and
30
+ `biome.json` is migrated to the 2.5.x schema.
31
+ - `mesh_lock` is documented and advertised as local to this machine: it is
32
+ enforced between sessions sharing this host's data directory and is not part
33
+ of the wire protocol. A guard test fails the build if a lock route reappears
34
+ on the server.
35
+ - Removed the Ed25519 keypair ceremony in node identity: nothing signed or
36
+ verified with it, so the short fingerprint is derived from `node_id`.
37
+ Existing node ids are preserved on upgrade; the fingerprint value changes,
38
+ which may require re-pairing peers.
39
+ - Workers spawned by `mesh_spawn` run with `OPENCODE_MESH_CHILD=1` and skip the
40
+ server, mDNS and all mesh tools, so a pool of N workers no longer costs N
41
+ mesh nodes.
42
+ - `mesh_worker_logs` reads only the tail of a worker log instead of loading the
43
+ whole file on the host's event loop; `mesh_status` caches `tailscale status`
44
+ for 60s and bounds the subprocess with a 2s timeout.
45
+
46
+ ### Fixed
47
+
48
+ - The docs no longer claim security behaviour the code does not implement:
49
+ `SECURITY.md` drops the "refuses entries that look like API-key formats"
50
+ promise (no such filtering exists) and the umask-077 claim (the data
51
+ *directory* is created `0700`; the SQLite file itself is not `chmod`ed), and
52
+ the README drops the non-existent `"allow_tailnet"` option while updating its
53
+ stale install pin.
54
+ - `mesh_pair` now rejects an empty `node_id` or `token` instead of completing a
55
+ pairing with a credential nobody can present (or that clobbers a peer record).
56
+ A partially-supplied pair (`node_id` without `token`, or vice versa) is an
57
+ error rather than being silently ignored.
58
+ - The mesh server reports a body read failure as `400` (malformed request) rather
59
+ than `413` (too large): a client that drops the connection mid-body no longer
60
+ looks like an oversized payload.
61
+ - Peer addresses that carry an IPv6 zone id (`[fe80::1%eth0]:4399`) are accepted,
62
+ so a link-local peer is no longer silently unreachable.
63
+ - `defaultDataDir()` no longer resolves to a filesystem-root path
64
+ (`/.local/share/opencode-mesh`) when `HOME` is unset or blank: it falls back to
65
+ `os.homedir()`, then `os.tmpdir()`.
66
+ - The node identity migration now finishes on disk: a `node.json` written by
67
+ <= 0.2.0 is rewritten without its dead Ed25519 keypair, so the stale key
68
+ material no longer lingers after the id is loaded. A corrupt or `node_id`-less
69
+ `node.json` now fails loudly with a recovery path instead of silently minting a
70
+ new node id, which would break every peer that recognised the node. Closes #31.
71
+ - `mesh_lock` no longer throws a raw `UNIQUE constraint failed` error when two
72
+ opencode sessions sharing the same data directory race for the same path. The
73
+ lock claim is a single atomic `INSERT ... ON CONFLICT DO NOTHING`, and the
74
+ worker status transition is a single guarded `UPDATE`, so a stale
75
+ read-then-write can neither raise nor overwrite a concurrently finalized row.
76
+ - The mesh database now records a schema version (`PRAGMA user_version`) and
77
+ creates indexes for its hot lookups (unread messages, pending pairing tokens,
78
+ peer tokens, running workers, expired locks).
79
+ - On OpenCode 2.x hosts, `mesh_spawn` and `mesh_worker_remove` no longer hand the
80
+ raw `ctx.worktree` domain to the REST-shaped worktree client. The in-process
81
+ domain (which resolves payloads directly, without the v1 `{ data, error }`
82
+ envelope) is wrapped back into that shape, and an injected
83
+ `options.worktreeClient` now takes precedence. Closes #29.
84
+ - Worktree create/remove calls on the 1.x REST client are bounded by a 15s
85
+ timeout. A create response with no directory, or a remove response that is
86
+ not a boolean, now raises a clear error instead of persisting an `undefined`
87
+ directory. When the 2.x domain reports only a directory (no branch), the
88
+ worker's branch falls back to that directory's name.
89
+ - A spawn that fails before the child runs no longer leaks its worktree:
90
+ `mesh_spawn` removes the worktree and the dead registry row when the child
91
+ never starts or `runWorker` throws. `mesh_worker_remove` keeps the registry
92
+ row when the worktree was not actually removed, instead of orphaning it.
93
+ - Node runtime support: the published package no longer calls `require()` from
94
+ ESM modules. The Node fallback paths (`node:sqlite` engine, `node:crypto`)
95
+ now use static and dynamic imports and actually run. Vitest's module runner
96
+ injects a `require`, which hid the `ReferenceError` from the suite, so
97
+ `scripts/smoke-dist.mjs` loads the built package under plain Node and is part
98
+ of `pnpm verify`.
99
+ - `mesh_send` no longer derives the peer's port from the local node. Discovered
100
+ addresses now carry the port from the mDNS SRV record (`[ipv6]:port` for IPv6
101
+ literals), and a portless address is refused instead of resolving to a wrong
102
+ socket. Peers that fall back to an ephemeral port are now reachable.
103
+ - Workers killed through `mesh_worker_stop` no longer orphan their own child
104
+ processes: workers are detached process-group leaders and a stop takes the
105
+ group down. The late `exit` event after a deliberate stop no longer throws;
106
+ a host restart reconciles `running` rows whose pid is gone.
107
+ - A worker that fails to start (binary missing, `error` event with no `exit`)
108
+ is now finalized as `failed` instead of staying `running` forever with a
109
+ leaked live entry; a `-1` pid is never persisted. `mesh_spawn` reports the
110
+ failure instead of a success envelope.
111
+ - A worker's stderr is drained into its log; a child writing more than the pipe
112
+ buffer (~64 KB) to stderr no longer blocks. `mesh_worker_stop` escalates
113
+ `SIGTERM` to `SIGKILL` after 3s so a worker ignoring the signal cannot linger.
114
+ - Worker logs are capped at 512 KB (rotated to `<id>.log.1`) and deleted by
115
+ `mesh_worker_remove`, so a chatty worker cannot fill the disk.
116
+ - A JSON line split across two stdout chunks is now reassembled before parsing,
117
+ so a worker's `session_id` is captured instead of silently dropping to null.
118
+ A session id arriving after the row is terminal is ignored.
119
+
120
+ ### Security
121
+
122
+ - The mesh HTTP server binds to `127.0.0.1` by default. `"bind": "lan"` opts
123
+ into listening on all interfaces, and `mesh_status` / the `mesh_pair` offer
124
+ report the address actually bound instead of a hardcoded loopback string.
125
+ - Pairing offers expire after 5 minutes, are pruned on check, and a pending
126
+ token alone no longer installs a permanent peer: the claimed node must
127
+ already be known to this host (mDNS discovery or explicit `mesh_pair`).
128
+ Auto-pairing a previously unknown node id is gone.
129
+ - Discovered peers keep their hostname and fingerprint through pairing instead
130
+ of collapsing to `"pending"`, so `mesh_status` still shows a real identity.
131
+
132
+ ### Removed
133
+
134
+ - `/announce` endpoint, its parser and `touchAddress` (no client ever called
135
+ the route).
136
+
137
+ ## [0.2.0] - 2026-10-12
138
+
139
+ ### Added
140
+
141
+ - Dual-export plugin entry for OpenCode 2: one published package runs on both
142
+ OpenCode 1.18 and 2.x (official migration pattern, "Support V1 and V2 from
143
+ one package"). v1 hosts call `server()` (unchanged); v2 hosts call `setup()`
144
+ (`Plugin.define`). Both entries drive the same engine.
145
+
10
146
  ## [0.1.2] - 2026-09-23
11
147
 
12
148
  ### Changed
package/README.md CHANGED
@@ -2,12 +2,16 @@
2
2
 
3
3
  [![CI](https://github.com/log0u7/opencode-mesh/actions/workflows/ci.yml/badge.svg)](https://github.com/log0u7/opencode-mesh/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/@log0u7/opencode-mesh)](https://www.npmjs.com/package/@log0u7/opencode-mesh)
5
+ [![Node.js](https://img.shields.io/node/v/@log0u7/opencode-mesh?logo=node.js&logoColor=white)](https://www.npmjs.com/package/@log0u7/opencode-mesh)
6
+ [![TypeScript](https://img.shields.io/github/package-json/dependency-version/log0u7/opencode-mesh/dev/typescript?logo=typescript&logoColor=white)](https://github.com/log0u7/opencode-mesh/blob/main/package.json)
5
7
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
6
8
 
7
9
  Coordination for [OpenCode](https://opencode.ai): multiple sessions working together on one machine, and a peer pool across a LAN or a Tailscale tailnet.
8
10
 
9
11
  Each node advertises itself over mDNS, pairs with explicit operator approval (per-peer bearer tokens, deny by default), then sessions can exchange messages (`mesh_send` / `mesh_inbox`) and coordinate on shared work (`mesh_lock` / `mesh_unlock`). Tailscale IPs are picked up automatically when `tailscale status` works, for cross-subnet pools.
10
12
 
13
+ `mesh_lock` is **local to your machine**: it is enforced between OpenCode sessions sharing this host's data directory (`~/.local/share/opencode-mesh/`). Locks are not part of the wire protocol, so a peer on another node does not see or respect them. Remote nodes coordinate through messages (`mesh_send`), not locks.
14
+
11
15
  ## Install
12
16
 
13
17
  Register the plugin in `opencode.json` (project or global `~/.config/opencode/opencode.json`):
@@ -22,12 +26,36 @@ Restart OpenCode. The package is installed automatically at startup into opencod
22
26
 
23
27
  ```json
24
28
  {
25
- "plugin": ["@log0u7/opencode-mesh@0.1.1"]
29
+ "plugin": ["@log0u7/opencode-mesh@0.3.0"]
30
+ }
31
+ ```
32
+
33
+ ### Configuration
34
+
35
+ All options are optional; see [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for the reference.
36
+
37
+ On OpenCode 1.x hosts, options travel in a package-and-options tuple:
38
+
39
+ ```json
40
+ {
41
+ "plugin": [["@log0u7/opencode-mesh", { "port": 4399, "bind": "loopback", "advertise": true }]]
42
+ }
43
+ ```
44
+
45
+ On 2.x hosts the key is `plugins` and each entry is an object:
46
+
47
+ ```json
48
+ {
49
+ "plugins": [
50
+ { "package": "@log0u7/opencode-mesh", "options": { "port": 4399, "bind": "lan" } }
51
+ ]
26
52
  }
27
53
  ```
28
54
 
29
55
  Node identity and mailbox live under `~/.local/share/opencode-mesh/` (honors `XDG_DATA_HOME`), created with restrictive permissions.
30
56
 
57
+ Workers spawned with `mesh_spawn` run with `OPENCODE_MESH_CHILD=1`: they skip the mesh server, mDNS and all mesh tools, so a pool of N workers costs N worktrees, not N mesh nodes.
58
+
31
59
  Note (npm 12): the package ships no lifecycle install scripts and needs no `allowScripts` / allowlist on install (its runtime dependency `bonjour-service` has none either).
32
60
 
33
61
  ## Usage
@@ -36,10 +64,10 @@ Same machine: start two OpenCode instances in the same repository.
36
64
 
37
65
  ```
38
66
  mesh_status() # local sessions + paired peers + active locks
39
- mesh_pair() # list discovered peers, approve pairing
67
+ mesh_pair() # emit a pairing offer (5 min) / complete the peer's offer
40
68
  mesh_send(peer="laptop", subject="handoff", body="...")
41
69
  mesh_inbox() # pull new messages, ack
42
- mesh_lock(path="src/api.ts", ttl=300) # claim a file before editing
70
+ mesh_lock(path="src/api.ts", ttl=300) # claim a file before editing (this machine only)
43
71
  mesh_unlock(path="src/api.ts")
44
72
  ```
45
73
 
@@ -48,10 +76,10 @@ mesh_unlock(path="src/api.ts")
48
76
  | Tool | Args | Description |
49
77
  |---|---|---|
50
78
  | `mesh_status` | : local session registry, own address, paired peers, active locks. |
51
- | `mesh_pair` | `query?` | List discovered peers; operator approves by hostname/fingerprint. |
79
+ | `mesh_pair` | `node_id?`, `token?`, `address?` | No args: emit a pairing offer (5 min ttl). With args: complete the peer's offer. |
52
80
  | `mesh_send` | `peer`, `subject`, `body` | Deliver a message to a peer's mailbox. |
53
81
  | `mesh_inbox` | `ack?` | Fetch unread messages; ack marks them read. |
54
- | `mesh_lock` | `path`, `ttl?` | Exclusive claim on a file or worktree path (default ttl 600s). |
82
+ | `mesh_lock` | `path`, `ttl?` | Exclusive claim on a file or path, enforced only between sessions on this machine (default ttl 600s). Not visible to remote peers. |
55
83
  | `mesh_unlock` | `path` | Release a claim. |
56
84
 
57
85
  ### Orchestrator
@@ -70,22 +98,29 @@ Workers run `opencode run --dir <worktree> --format json` headless with `--auto`
70
98
 
71
99
  ### Security model
72
100
 
73
- - Every HTTP endpoint requires a per-peer bearer token; nothing is open by default.
74
- - All peers, same machine or remote, pair through `mesh_pair`: one side emits a one-time token, the other completes the pairing with it.
75
- - mDNS TXT records carry node id + key fingerprint only, never secrets.
76
- - LAN-only by default; tailnet addresses are surfaced by `mesh_status` when `tailscale` runs, for cross-subnet pools (`"allow_tailnet": true` plugin option, planned).
101
+ - The HTTP mesh server binds to `127.0.0.1` by default. Set `"bind": "lan"` in the plugin options to listen on all interfaces for a LAN pool.
102
+ - Every endpoint requires a per-peer bearer token; nothing is open by default.
103
+ - Pairing offers expire after 5 minutes. A pending token alone grants nothing: the claimed node must already be known to this host (discovered over mDNS, or paired out of band with `mesh_pair`). Both sides run `mesh_pair` to complete a pairing, so a leaked offer cannot install a permanent peer on its own.
104
+ - Peer addresses always carry the advertised port; a portless address is refused instead of being completed with the local port.
105
+ - mDNS TXT records carry node id + fingerprint only, never secrets.
106
+ - Tailnet addresses are surfaced by `mesh_status` when `tailscale` runs, for cross-subnet pools.
107
+ - Messages travel over plain HTTP: there is no TLS layer. Loopback and trusted networks only.
77
108
 
78
109
  ## Development
79
110
 
80
- Requires Node >= 22 (mise: `mise install`) and pnpm (corepack).
111
+ Requires Node >= 22.5 (`mise install`) and pnpm (corepack).
81
112
 
82
113
  ```sh
83
114
  pnpm install
84
- pnpm verify # biome + typecheck + vitest
85
- pnpm build:check # tsc build + pack dry-run
115
+ pnpm verify # biome + typecheck + vitest + Node smoke of dist/
116
+ pnpm typecheck:tests # typecheck the test suite (also enforced in CI)
117
+ pnpm build:check # tsc build + pack dry-run
118
+ pnpm smoke:dist # build, then load dist/ under plain Node (no vitest shims)
86
119
  ```
87
120
 
88
- See [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow.
121
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow, and the [docs/](docs/) folder for
122
+ [configuration](docs/CONFIGURATION.md), [architecture](docs/ARCHITECTURE.md), and the
123
+ wire [protocol](docs/PROTOCOL.md).
89
124
 
90
125
  ## License
91
126
 
package/dist/index.d.ts CHANGED
@@ -5,8 +5,13 @@
5
5
  *
6
6
  * @packageDocumentation
7
7
  */
8
- declare const pluginModule: {
8
+ /**
9
+ * Dual entrypoint: v1 hosts call `server()`, v2 hosts call `setup()`
10
+ * (see https://opencode.ai/v2/docs/build/plugins/migrate-v1).
11
+ */
12
+ export declare const pluginModule: {
9
13
  id: string;
14
+ setup: (context: import("@opencode/plugin/promise/plugin").Context) => Promise<import("@opencode/plugin/promise/plugin").Cleanup | void> | import("@opencode/plugin/promise/plugin").Cleanup | void;
10
15
  server: import("@opencode-ai/plugin").Plugin;
11
16
  };
12
17
  export default pluginModule;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,QAAA,MAAM,YAAY;;;CAGmC,CAAC;eAEvC,YAAY;AAE3B,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE/D,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH;;;GAGG;AACH,eAAO,MAAM,YAAY;;;;CAG6D,CAAC;eAExE,YAAY;AAE3B,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE/D,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -5,9 +5,14 @@
5
5
  *
6
6
  * @packageDocumentation
7
7
  */
8
+ import { MeshV2 } from "./plugin-v2.js";
8
9
  import { MeshPlugin } from "./plugin.js";
9
- const pluginModule = {
10
- id: "@log0u7/opencode-mesh",
10
+ /**
11
+ * Dual entrypoint: v1 hosts call `server()`, v2 hosts call `setup()`
12
+ * (see https://opencode.ai/v2/docs/build/plugins/migrate-v1).
13
+ */
14
+ export const pluginModule = {
15
+ ...MeshV2,
11
16
  server: MeshPlugin,
12
17
  };
13
18
  export default pluginModule;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,YAAY,GAAG;IACnB,EAAE,EAAE,uBAAuB;IAC3B,MAAM,EAAE,UAAU;CACiC,CAAC;AAEtD,eAAe,YAAY,CAAC;AAI5B,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,GAAG,MAAM;IACT,MAAM,EAAE,UAAU;CACkE,CAAC;AAEvF,eAAe,YAAY,CAAC;AAI5B,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAE1D,MAAM,MAAM,MAAM,GAAG;IACnB,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;AA4CF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAW/C"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAE1D,MAAM,MAAM,MAAM,GAAG;IACnB,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;AAgEF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAW/C"}
package/dist/lib/db.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { mkdirSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { openSqlite } from "./sqlite.js";
4
+ // Bump when a schema change needs a migration step; the version anchors future
5
+ // migrations that cannot be expressed as idempotent `CREATE ... IF NOT EXISTS`.
6
+ const SCHEMA_VERSION = 1;
4
7
  const SCHEMA = `
5
8
  CREATE TABLE IF NOT EXISTS peers (
6
9
  node_id TEXT PRIMARY KEY,
@@ -41,7 +44,22 @@ CREATE TABLE IF NOT EXISTS workers (
41
44
  started_at INTEGER NOT NULL,
42
45
  finished_at INTEGER
43
46
  );
47
+ CREATE INDEX IF NOT EXISTS idx_messages_status_created ON messages(status, created_at);
48
+ CREATE INDEX IF NOT EXISTS idx_pending_tokens_created ON pending_tokens(created_at);
49
+ CREATE INDEX IF NOT EXISTS idx_peers_token ON peers(token);
50
+ CREATE INDEX IF NOT EXISTS idx_workers_status ON workers(status);
51
+ CREATE INDEX IF NOT EXISTS idx_locks_expires ON locks(expires_at);
44
52
  `;
53
+ function migrate(conn) {
54
+ const current = conn.get("PRAGMA user_version")?.user_version ?? 0;
55
+ if (current >= SCHEMA_VERSION) {
56
+ return;
57
+ }
58
+ // Every statement in SCHEMA is idempotent, so a version-0 database (fresh or
59
+ // pre-versioning) is brought up to date by re-running it.
60
+ conn.exec(SCHEMA);
61
+ conn.run(`PRAGMA user_version = ${SCHEMA_VERSION}`);
62
+ }
45
63
  export function openMeshDb(path) {
46
64
  if (path !== ":memory:") {
47
65
  mkdirSync(dirname(path), { recursive: true });
@@ -49,7 +67,7 @@ export function openMeshDb(path) {
49
67
  const conn = openSqlite(path);
50
68
  conn.run("PRAGMA journal_mode = WAL");
51
69
  conn.run("PRAGMA busy_timeout = 5000");
52
- conn.exec(SCHEMA);
70
+ migrate(conn);
53
71
  return { conn, close: () => conn.close() };
54
72
  }
55
73
  //# sourceMappingURL=db.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,UAAU,EAAmB,MAAM,aAAa,CAAC;AAO1D,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCd,CAAC;AAEF,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QACxB,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,CAAC;IAED,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;IACtC,IAAI,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;IACvC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAElB,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC;AAC7C,CAAC"}
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,UAAU,EAAmB,MAAM,aAAa,CAAC;AAO1D,+EAA+E;AAC/E,gFAAgF;AAChF,MAAM,cAAc,GAAG,CAAC,CAAC;AAEzB,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6Cd,CAAC;AAEF,SAAS,OAAO,CAAC,IAAgB;IAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAA2B,qBAAqB,CAAC,EAAE,YAAY,IAAI,CAAC,CAAC;IAC7F,IAAI,OAAO,IAAI,cAAc,EAAE,CAAC;QAC9B,OAAO;IACT,CAAC;IACD,6EAA6E;IAC7E,0DAA0D;IAC1D,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAClB,IAAI,CAAC,GAAG,CAAC,yBAAyB,cAAc,EAAE,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QACxB,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,CAAC;IAED,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;IACtC,IAAI,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;IACvC,OAAO,CAAC,IAAI,CAAC,CAAC;IAEd,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC;AAC7C,CAAC"}
@@ -21,5 +21,6 @@ export declare function browseMesh(options: {
21
21
  }): {
22
22
  close(): void;
23
23
  };
24
+ export declare function resetTailnetCache(): void;
24
25
  export declare function tailnetAddresses(): Promise<string[]>;
25
26
  //# sourceMappingURL=discovery.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../src/lib/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE/C,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAEjD,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;AAIF,wBAAgB,aAAa,CAAC,OAAO,EAAE;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,eAAe,CAalB;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC;AAEF,wBAAgB,UAAU,CAAC,OAAO,EAAE;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;CAC9C,GAAG;IAAE,KAAK,IAAI,IAAI,CAAA;CAAE,CAoBpB;AAID,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAkB1D"}
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../src/lib/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE/C,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAEjD,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;AAIF,wBAAgB,aAAa,CAAC,OAAO,EAAE;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,eAAe,CAalB;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC;AAEF,wBAAgB,UAAU,CAAC,OAAO,EAAE;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;CAC9C,GAAG;IAAE,KAAK,IAAI,IAAI,CAAA;CAAE,CAoBpB;AAsBD,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAgC1D"}
@@ -25,7 +25,7 @@ export function browseMesh(options) {
25
25
  node_id: id,
26
26
  hostname: service.host ?? service.name,
27
27
  fingerprint: fp,
28
- addresses: service.addresses ?? [],
28
+ addresses: socketAddresses(service.addresses, service.port),
29
29
  });
30
30
  });
31
31
  return {
@@ -34,12 +34,33 @@ export function browseMesh(options) {
34
34
  },
35
35
  };
36
36
  }
37
+ // An mDNS A/AAAA record is a bare IP: the peer's listening port comes from the
38
+ // SRV record in the same service. Guessing the port from the local node is
39
+ // wrong whenever the peer fell back to an ephemeral port, so a record without
40
+ // a port yields no address at all rather than a wrong one.
41
+ function socketAddresses(ips, port) {
42
+ if (port === undefined) {
43
+ return [];
44
+ }
45
+ return (ips ?? []).map((ip) => (ip.includes(":") ? `[${ip}]:${port}` : `${ip}:${port}`));
46
+ }
37
47
  // Tailnet addresses let peers connect across subnets when tailscale runs.
38
- // Absent CLI = empty list, never an error.
48
+ // Absent CLI = empty list, never an error. The result is cached: mesh_status is
49
+ // a hot tool call and `tailscale status --json` is a subprocess we must not pay
50
+ // on every invocation.
51
+ const TAILNET_TTL_MS = 60_000;
52
+ const TAILNET_TIMEOUT_MS = 2000;
53
+ let tailnetCache = null;
54
+ export function resetTailnetCache() {
55
+ tailnetCache = null;
56
+ }
39
57
  export async function tailnetAddresses() {
58
+ if (tailnetCache !== null && tailnetCache.expiresAt > Date.now()) {
59
+ return tailnetCache.addresses;
60
+ }
40
61
  const { execFile } = await import("node:child_process");
41
- return new Promise((resolve) => {
42
- execFile("tailscale", ["status", "--json"], (error, stdout) => {
62
+ const addresses = await new Promise((resolve) => {
63
+ execFile("tailscale", ["status", "--json"], { timeout: TAILNET_TIMEOUT_MS }, (error, stdout) => {
43
64
  if (error) {
44
65
  resolve([]);
45
66
  return;
@@ -54,5 +75,7 @@ export async function tailnetAddresses() {
54
75
  }
55
76
  });
56
77
  });
78
+ tailnetCache = { addresses, expiresAt: Date.now() + TAILNET_TTL_MS };
79
+ return addresses;
57
80
  }
58
81
  //# sourceMappingURL=discovery.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/lib/discovery.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAC;AAMjD,uEAAuE;AACvE,8BAA8B;AAC9B,MAAM,UAAU,aAAa,CAAC,OAK7B;IACC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC;QACtC,IAAI,EAAE,OAAO,CAAC,OAAO;QACrB,IAAI,EAAE,iBAAiB;QACvB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,GAAG,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,OAAO,EAAE,EAAE,EAAE,OAAO,CAAC,WAAW,EAAE;KACtD,CAAC,CAAC;IAEH,OAAO;QACL,KAAK;YACH,OAAO,CAAC,IAAI,EAAE,CAAC;QACjB,CAAC;KACF,CAAC;AACJ,CAAC;AASD,MAAM,UAAU,UAAU,CAAC,OAG1B;IACC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,EAAE,CAAC,OAAO,EAAE,EAAE;QAC5E,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC;QAC3B,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC;QAC3B,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxE,OAAO;QACT,CAAC;QACD,OAAO,CAAC,YAAY,CAAC;YACnB,OAAO,EAAE,EAAE;YACX,QAAQ,EAAE,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,IAAI;YACtC,WAAW,EAAE,EAAE;YACf,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,EAAE;SACnC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,KAAK;YACH,OAAO,CAAC,IAAI,EAAE,CAAC;QACjB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,0EAA0E;AAC1E,2CAA2C;AAC3C,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAExD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,QAAQ,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC,KAAmB,EAAE,MAAc,EAAE,EAAE;YAClF,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO,CAAC,EAAE,CAAC,CAAC;gBACZ,OAAO;YACT,CAAC;YACD,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAA2C,CAAC;gBAC5E,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC;gBACtC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC9F,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,CAAC,EAAE,CAAC,CAAC;YACd,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/lib/discovery.ts"],"names":[],"mappings":"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"}
@@ -1,15 +1,7 @@
1
- type Jwk = {
2
- kty?: string;
3
- x?: string;
4
- [key: string]: unknown;
5
- };
6
1
  export type NodeIdentity = {
7
2
  node_id: string;
8
3
  hostname: string;
9
4
  fingerprint: string;
10
- public_key: Jwk;
11
- private_key: Jwk;
12
5
  };
13
6
  export declare function loadOrCreateIdentity(dataDir: string): NodeIdentity;
14
- export {};
15
7
  //# sourceMappingURL=identity.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/lib/identity.ts"],"names":[],"mappings":"AAKA,KAAK,GAAG,GAAG;IACT,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,GAAG,CAAC;IAChB,WAAW,EAAE,GAAG,CAAC;CAClB,CAAC;AASF,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CASlE"}
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/lib/identity.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAIF,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CAgBlE"}
@@ -1,58 +1,77 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
3
  import { hostname as osHostname } from "node:os";
4
- import { join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
5
  export function loadOrCreateIdentity(dataDir) {
6
6
  const file = join(dataDir, "node.json");
7
- const existing = tryRead(file);
7
+ const existing = readIdentity(file);
8
8
  if (existing) {
9
- return withFingerprint(existing);
9
+ // A file written by <= 0.2.0 carries an Ed25519 keypair nothing ever used.
10
+ // Only the node_id matters, so the migration finishes by rewriting the file
11
+ // without the key material: the node keeps its id and its peers stay
12
+ // recognised, and the dead keys stop lingering on disk.
13
+ if (existing.migrated)
14
+ writeIdentity(file, existing.identity);
15
+ return withFingerprint(existing.identity);
10
16
  }
11
- return withFingerprint(generate(dataDir, file));
17
+ const identity = { node_id: randomUUID(), hostname: hostName() };
18
+ writeIdentity(file, identity);
19
+ return withFingerprint(identity);
12
20
  }
13
- function tryRead(file) {
21
+ function readIdentity(file) {
22
+ let raw;
14
23
  try {
15
- const raw = JSON.parse(readFileSync(file, "utf8"));
16
- if (typeof raw.node_id === "string" && raw.public_key && raw.private_key) {
17
- return raw;
18
- }
19
- return null;
24
+ raw = readFileSync(file, "utf8");
25
+ }
26
+ catch (error) {
27
+ if (error.code === "ENOENT")
28
+ return null;
29
+ throw new Error(`cannot read identity file ${file}: ${errorMessage(error)}`);
30
+ }
31
+ let parsed;
32
+ try {
33
+ parsed = JSON.parse(raw);
20
34
  }
21
35
  catch {
22
- return null;
36
+ throw corrupt(file, "is not valid JSON");
23
37
  }
24
- }
25
- function generate(dataDir, file) {
26
- // Bun implements node:crypto, so this works under OpenCode's runtime and Node.
27
- const { generateKeyPairSync } = require("node:crypto");
28
- const { publicKey, privateKey } = generateKeyPairSync("ed25519");
29
- const publicJwk = publicKey.export({ format: "jwk" });
30
- const privateJwk = privateKey.export({ format: "jwk" });
31
- const identity = {
32
- node_id: randomUUID(),
33
- hostname: hostName(),
34
- public_key: publicJwk,
35
- private_key: privateJwk,
38
+ const record = (parsed ?? {});
39
+ if (typeof record.node_id !== "string" || record.node_id.length === 0) {
40
+ throw corrupt(file, "has no node_id");
41
+ }
42
+ return {
43
+ identity: { node_id: record.node_id, hostname: hostName(record.hostname) },
44
+ migrated: Object.keys(record).some((key) => key !== "node_id" && key !== "hostname"),
36
45
  };
37
- mkdirSync(dataDir, { recursive: true, mode: 0o700 });
38
- // Atomic write: temp file + rename, so a crash never leaves a truncated keypair.
46
+ }
47
+ // A corrupt identity file would make the node silently mint a new id, breaking
48
+ // every peer that recognises it and orphaning queued mail. Refuse to start and
49
+ // hand the operator a recovery path instead of reminting behind their back.
50
+ function corrupt(file, why) {
51
+ return new Error(`identity file ${file} ${why}; refusing to mint a new node id. ` +
52
+ `Move it aside to start with a fresh identity: mv '${file}' '${file}.bak'`);
53
+ }
54
+ function writeIdentity(file, identity) {
55
+ mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
56
+ // Atomic write: temp file + rename, so a crash never leaves a truncated record.
39
57
  const tmp = `${file}.tmp-${process.pid}`;
40
58
  writeFileSync(tmp, JSON.stringify(identity, null, 2), { mode: 0o600 });
41
59
  renameSync(tmp, file);
42
60
  chmodSync(file, 0o600);
43
- return identity;
44
61
  }
62
+ // The keypair this replaces produced a fingerprint nobody verified. Deriving it
63
+ // from the node_id keeps the same short, operator-readable label for `mesh_status`
64
+ // without storing or protecting key material.
45
65
  function withFingerprint(identity) {
46
- const { createHash } = require("node:crypto");
47
- // x is the raw public key coordinate in an Ed25519 JWK; hash it for a short
48
- // operator-verifiable fingerprint.
49
- const x = identity.public_key.x ?? "";
50
66
  return {
51
67
  ...identity,
52
- fingerprint: createHash("sha256").update(x).digest("hex").slice(0, 16),
68
+ fingerprint: createHash("sha256").update(identity.node_id).digest("hex").slice(0, 16),
53
69
  };
54
70
  }
55
- function hostName() {
56
- return osHostname();
71
+ function hostName(recorded) {
72
+ return typeof recorded === "string" && recorded.length > 0 ? recorded : osHostname();
73
+ }
74
+ function errorMessage(error) {
75
+ return error instanceof Error ? error.message : String(error);
57
76
  }
58
77
  //# sourceMappingURL=identity.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/lib/identity.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/lib/identity.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"locks.d.ts","sourceRoot":"","sources":["../../src/lib/locks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE7C,wBAAgB,OAAO,CACrB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC9F,OAAO,CAwBT;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAM5F;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EAAE,CAGpE"}
1
+ {"version":3,"file":"locks.d.ts","sourceRoot":"","sources":["../../src/lib/locks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE7C,wBAAgB,OAAO,CACrB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC9F,OAAO,CAiCT;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAM5F;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EAAE,CAGpE"}