@log0u7/opencode-mesh 0.2.0 → 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.
- package/CHANGELOG.md +127 -0
- package/README.md +46 -13
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +19 -1
- package/dist/lib/db.js.map +1 -1
- package/dist/lib/discovery.d.ts +1 -0
- package/dist/lib/discovery.d.ts.map +1 -1
- package/dist/lib/discovery.js +27 -4
- package/dist/lib/discovery.js.map +1 -1
- package/dist/lib/identity.d.ts +0 -8
- package/dist/lib/identity.d.ts.map +1 -1
- package/dist/lib/identity.js +53 -34
- package/dist/lib/identity.js.map +1 -1
- package/dist/lib/locks.d.ts.map +1 -1
- package/dist/lib/locks.js +20 -12
- package/dist/lib/locks.js.map +1 -1
- package/dist/lib/peers.d.ts +9 -0
- package/dist/lib/peers.d.ts.map +1 -1
- package/dist/lib/peers.js +49 -11
- package/dist/lib/peers.js.map +1 -1
- package/dist/lib/serve.d.ts +2 -0
- package/dist/lib/serve.d.ts.map +1 -1
- package/dist/lib/serve.js +53 -68
- package/dist/lib/serve.js.map +1 -1
- package/dist/lib/spawn.d.ts +14 -4
- package/dist/lib/spawn.d.ts.map +1 -1
- package/dist/lib/spawn.js +210 -21
- package/dist/lib/spawn.js.map +1 -1
- package/dist/lib/sqlite.d.ts.map +1 -1
- package/dist/lib/sqlite.js +15 -4
- package/dist/lib/sqlite.js.map +1 -1
- package/dist/lib/workers.d.ts +2 -0
- package/dist/lib/workers.d.ts.map +1 -1
- package/dist/lib/workers.js +45 -12
- package/dist/lib/workers.js.map +1 -1
- package/dist/lib/worktree.d.ts +22 -4
- package/dist/lib/worktree.d.ts.map +1 -1
- package/dist/lib/worktree.js +54 -6
- package/dist/lib/worktree.js.map +1 -1
- package/dist/plugin-v2.d.ts.map +1 -1
- package/dist/plugin-v2.js +64 -337
- package/dist/plugin-v2.js.map +1 -1
- package/dist/plugin.d.ts +2 -6
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +118 -265
- package/dist/plugin.js.map +1 -1
- package/dist/tools.d.ts +30 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +308 -0
- package/dist/tools.js.map +1 -0
- package/package.json +10 -8
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,133 @@ 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
|
+
|
|
10
137
|
## [0.2.0] - 2026-10-12
|
|
11
138
|
|
|
12
139
|
### Added
|
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ Coordination for [OpenCode](https://opencode.ai): multiple sessions working toge
|
|
|
10
10
|
|
|
11
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.
|
|
12
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
|
+
|
|
13
15
|
## Install
|
|
14
16
|
|
|
15
17
|
Register the plugin in `opencode.json` (project or global `~/.config/opencode/opencode.json`):
|
|
@@ -24,12 +26,36 @@ Restart OpenCode. The package is installed automatically at startup into opencod
|
|
|
24
26
|
|
|
25
27
|
```json
|
|
26
28
|
{
|
|
27
|
-
"plugin": ["@log0u7/opencode-mesh@0.
|
|
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
|
+
]
|
|
28
52
|
}
|
|
29
53
|
```
|
|
30
54
|
|
|
31
55
|
Node identity and mailbox live under `~/.local/share/opencode-mesh/` (honors `XDG_DATA_HOME`), created with restrictive permissions.
|
|
32
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
|
+
|
|
33
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).
|
|
34
60
|
|
|
35
61
|
## Usage
|
|
@@ -38,10 +64,10 @@ Same machine: start two OpenCode instances in the same repository.
|
|
|
38
64
|
|
|
39
65
|
```
|
|
40
66
|
mesh_status() # local sessions + paired peers + active locks
|
|
41
|
-
mesh_pair() #
|
|
67
|
+
mesh_pair() # emit a pairing offer (5 min) / complete the peer's offer
|
|
42
68
|
mesh_send(peer="laptop", subject="handoff", body="...")
|
|
43
69
|
mesh_inbox() # pull new messages, ack
|
|
44
|
-
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)
|
|
45
71
|
mesh_unlock(path="src/api.ts")
|
|
46
72
|
```
|
|
47
73
|
|
|
@@ -50,10 +76,10 @@ mesh_unlock(path="src/api.ts")
|
|
|
50
76
|
| Tool | Args | Description |
|
|
51
77
|
|---|---|---|
|
|
52
78
|
| `mesh_status` | : local session registry, own address, paired peers, active locks. |
|
|
53
|
-
| `mesh_pair` | `
|
|
79
|
+
| `mesh_pair` | `node_id?`, `token?`, `address?` | No args: emit a pairing offer (5 min ttl). With args: complete the peer's offer. |
|
|
54
80
|
| `mesh_send` | `peer`, `subject`, `body` | Deliver a message to a peer's mailbox. |
|
|
55
81
|
| `mesh_inbox` | `ack?` | Fetch unread messages; ack marks them read. |
|
|
56
|
-
| `mesh_lock` | `path`, `ttl?` | Exclusive claim on a file or
|
|
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. |
|
|
57
83
|
| `mesh_unlock` | `path` | Release a claim. |
|
|
58
84
|
|
|
59
85
|
### Orchestrator
|
|
@@ -72,22 +98,29 @@ Workers run `opencode run --dir <worktree> --format json` headless with `--auto`
|
|
|
72
98
|
|
|
73
99
|
### Security model
|
|
74
100
|
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
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.
|
|
79
108
|
|
|
80
109
|
## Development
|
|
81
110
|
|
|
82
|
-
Requires Node >= 22 (
|
|
111
|
+
Requires Node >= 22.5 (`mise install`) and pnpm (corepack).
|
|
83
112
|
|
|
84
113
|
```sh
|
|
85
114
|
pnpm install
|
|
86
|
-
pnpm verify
|
|
87
|
-
pnpm
|
|
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)
|
|
88
119
|
```
|
|
89
120
|
|
|
90
|
-
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).
|
|
91
124
|
|
|
92
125
|
## License
|
|
93
126
|
|
package/dist/lib/db.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
70
|
+
migrate(conn);
|
|
53
71
|
return { conn, close: () => conn.close() };
|
|
54
72
|
}
|
|
55
73
|
//# sourceMappingURL=db.js.map
|
package/dist/lib/db.js.map
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/lib/discovery.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/lib/discovery.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
1
|
+
{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/lib/discovery.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/identity.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/lib/identity.js
CHANGED
|
@@ -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 =
|
|
7
|
+
const existing = readIdentity(file);
|
|
8
8
|
if (existing) {
|
|
9
|
-
|
|
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
|
-
|
|
17
|
+
const identity = { node_id: randomUUID(), hostname: hostName() };
|
|
18
|
+
writeIdentity(file, identity);
|
|
19
|
+
return withFingerprint(identity);
|
|
12
20
|
}
|
|
13
|
-
function
|
|
21
|
+
function readIdentity(file) {
|
|
22
|
+
let raw;
|
|
14
23
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
36
|
+
throw corrupt(file, "is not valid JSON");
|
|
23
37
|
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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(
|
|
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
|
package/dist/lib/identity.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/lib/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/lib/identity.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/locks.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/lib/locks.js
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
export function tryLock(db, input) {
|
|
2
2
|
const expiresAt = input.now + input.ttl_ms;
|
|
3
3
|
db.conn.run("DELETE FROM locks WHERE expires_at <= ?", [input.now]);
|
|
4
|
+
// Claim atomically: a second opencode process sharing this dataDir that races
|
|
5
|
+
// for the same path loses the insert (ON CONFLICT DO NOTHING) instead of
|
|
6
|
+
// throwing a UNIQUE constraint error out of mesh_lock.
|
|
7
|
+
db.conn.run("INSERT INTO locks (path, owner_node, owner_session, expires_at) VALUES (?, ?, ?, ?) ON CONFLICT(path) DO NOTHING", [input.path, input.owner_node, input.owner_session, expiresAt]);
|
|
8
|
+
const claimed = (db.conn.get("SELECT changes() AS changes")?.changes ?? 0) > 0;
|
|
9
|
+
if (claimed) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
4
12
|
const existing = db.conn.get("SELECT * FROM locks WHERE path = ?", [input.path]);
|
|
5
|
-
if (existing) {
|
|
6
|
-
|
|
7
|
-
// Same owner refreshes its claim.
|
|
8
|
-
db.conn.run("UPDATE locks SET expires_at = ?, owner_session = ? WHERE path = ?", [
|
|
9
|
-
expiresAt,
|
|
10
|
-
input.owner_session,
|
|
11
|
-
input.path,
|
|
12
|
-
]);
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
13
|
+
if (!existing) {
|
|
14
|
+
// Raced with a release between the insert and this read; not granted.
|
|
15
15
|
return false;
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
if (existing.owner_node === input.owner_node) {
|
|
18
|
+
// Same owner refreshes its claim.
|
|
19
|
+
db.conn.run("UPDATE locks SET expires_at = ?, owner_session = ? WHERE path = ?", [
|
|
20
|
+
expiresAt,
|
|
21
|
+
input.owner_session,
|
|
22
|
+
input.path,
|
|
23
|
+
]);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
19
27
|
}
|
|
20
28
|
export function releaseLock(db, input) {
|
|
21
29
|
db.conn.run("DELETE FROM locks WHERE path = ? AND owner_node = ?", [
|
package/dist/lib/locks.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locks.js","sourceRoot":"","sources":["../../src/lib/locks.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,OAAO,CACrB,EAAU,EACV,KAA+F;IAE/F,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC;IAE3C,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,yCAAyC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAEpE,
|
|
1
|
+
{"version":3,"file":"locks.js","sourceRoot":"","sources":["../../src/lib/locks.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,OAAO,CACrB,EAAU,EACV,KAA+F;IAE/F,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC;IAE3C,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,yCAAyC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAEpE,8EAA8E;IAC9E,yEAAyE;IACzE,uDAAuD;IACvD,EAAE,CAAC,IAAI,CAAC,GAAG,CACT,kHAAkH,EAClH,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,aAAa,EAAE,SAAS,CAAC,CAC/D,CAAC;IACF,MAAM,OAAO,GACX,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAsB,6BAA6B,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IACtF,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,QAAQ,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAU,oCAAoC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1F,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,sEAAsE;QACtE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,QAAQ,CAAC,UAAU,KAAK,KAAK,CAAC,UAAU,EAAE,CAAC;QAC7C,kCAAkC;QAClC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,mEAAmE,EAAE;YAC/E,SAAS;YACT,KAAK,CAAC,aAAa;YACnB,KAAK,CAAC,IAAI;SACX,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,EAAU,EAAE,KAA2C;IACjF,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,qDAAqD,EAAE;QACjE,KAAK,CAAC,IAAI;QACV,KAAK,CAAC,UAAU;KACjB,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAsB,6BAA6B,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;AAC7F,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,EAAU,EAAE,KAAsB;IAC1D,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,yCAAyC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACpE,OAAO,EAAE,CAAC,IAAI,CAAC,GAAG,CAAU,mCAAmC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AAClF,CAAC;AASD,SAAS,SAAS,CAAC,GAAY;IAC7B,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,KAAK,EAAE,GAAG,CAAC,aAAa;QACxB,OAAO,EAAE,GAAG,CAAC,UAAU;QACvB,UAAU,EAAE,GAAG,CAAC,UAAU;KAC3B,CAAC;AACJ,CAAC"}
|
package/dist/lib/peers.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MeshDb } from "./db.js";
|
|
2
2
|
import type { Peer } from "../types/peer.js";
|
|
3
|
+
export declare const PAIRING_TOKEN_TTL_MS: number;
|
|
3
4
|
export type PeerInput = {
|
|
4
5
|
node_id: string;
|
|
5
6
|
hostname: string;
|
|
@@ -15,8 +16,16 @@ export declare function pairWithToken(db: MeshDb, peer: {
|
|
|
15
16
|
}): void;
|
|
16
17
|
export declare function createPairingToken(db: MeshDb): string;
|
|
17
18
|
export declare function authenticateToken(db: MeshDb, token: string, from: string | null): Peer | null;
|
|
19
|
+
export declare function prunePendingTokens(db: MeshDb): void;
|
|
18
20
|
export declare function listPeers(db: MeshDb): Peer[];
|
|
21
|
+
export declare function findPeerById(db: MeshDb, nodeId: string): Peer | null;
|
|
19
22
|
export declare function findPeerByToken(db: MeshDb, token: string): Peer | null;
|
|
23
|
+
export declare function findSendablePeer(db: MeshDb, query: string): {
|
|
24
|
+
node_id: string;
|
|
25
|
+
addresses: string[];
|
|
26
|
+
token: string | null;
|
|
27
|
+
} | null;
|
|
28
|
+
export declare function resolvePeerUrl(db: MeshDb, query: string): string | null;
|
|
20
29
|
export declare function upsertDiscoveredPeer(db: MeshDb, peer: {
|
|
21
30
|
node_id: string;
|
|
22
31
|
hostname: string;
|