signalk-p2p-plugin 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +66 -14
  3. package/deploy/README.md +24 -11
  4. package/deploy/policy.json +30 -0
  5. package/dist/advanced-config.d.ts +11 -0
  6. package/dist/advanced-config.d.ts.map +1 -0
  7. package/dist/advanced-config.js +40 -0
  8. package/dist/advanced-config.js.map +1 -0
  9. package/dist/api.d.ts +14 -1
  10. package/dist/api.d.ts.map +1 -1
  11. package/dist/api.js +41 -3
  12. package/dist/api.js.map +1 -1
  13. package/dist/config.d.ts +5 -0
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +12 -273
  16. package/dist/config.js.map +1 -1
  17. package/dist/data/envelope.d.ts.map +1 -1
  18. package/dist/data/envelope.js +2 -0
  19. package/dist/data/envelope.js.map +1 -1
  20. package/dist/data/types.d.ts +1 -1
  21. package/dist/data/types.d.ts.map +1 -1
  22. package/dist/groups/control.d.ts +8 -0
  23. package/dist/groups/control.d.ts.map +1 -1
  24. package/dist/groups/control.js +30 -0
  25. package/dist/groups/control.js.map +1 -1
  26. package/dist/groups/manager.d.ts +2 -0
  27. package/dist/groups/manager.d.ts.map +1 -1
  28. package/dist/groups/manager.js +29 -2
  29. package/dist/groups/manager.js.map +1 -1
  30. package/dist/groups/types.d.ts +12 -0
  31. package/dist/groups/types.d.ts.map +1 -1
  32. package/dist/groups/types.js +23 -0
  33. package/dist/groups/types.js.map +1 -1
  34. package/dist/live/relay.d.ts +3 -0
  35. package/dist/live/relay.d.ts.map +1 -1
  36. package/dist/live/relay.js +11 -0
  37. package/dist/live/relay.js.map +1 -1
  38. package/dist/peer/cli.js +16 -0
  39. package/dist/peer/cli.js.map +1 -1
  40. package/dist/peer/config.d.ts +3 -0
  41. package/dist/peer/config.d.ts.map +1 -1
  42. package/dist/peer/config.js +5 -0
  43. package/dist/peer/config.js.map +1 -1
  44. package/dist/plugin.d.ts.map +1 -1
  45. package/dist/plugin.js +136 -46
  46. package/dist/plugin.js.map +1 -1
  47. package/dist/policy/operator.d.ts +13 -0
  48. package/dist/policy/operator.d.ts.map +1 -0
  49. package/dist/policy/operator.js +53 -0
  50. package/dist/policy/operator.js.map +1 -0
  51. package/dist/policy/policy.d.ts +49 -0
  52. package/dist/policy/policy.d.ts.map +1 -0
  53. package/dist/policy/policy.js +147 -0
  54. package/dist/policy/policy.js.map +1 -0
  55. package/docs/plans/README.md +2 -2
  56. package/docs/plans/architecture.md +2 -2
  57. package/docs/plans/networks.md +36 -8
  58. package/docs/plans/peer-node.md +7 -2
  59. package/docs/plans/security.md +1 -1
  60. package/docs/plans/shared-data.md +4 -3
  61. package/package.json +3 -2
  62. package/policy/default.json +30 -0
  63. package/policy/operator.pub +2 -0
  64. package/public/index.html +299 -83
@@ -0,0 +1,147 @@
1
+ import { verifyDetached } from "../identity.js";
2
+ import { canonicalJson } from "../groups/types.js";
3
+ import { PROTOCOL_VERSION } from "../data/envelope.js";
4
+ import { ALL_VESSELS_TOPIC } from "../live/topic.js";
5
+ export const DEFAULT_POLICY_VERSION = 1;
6
+ export const MAX_POLICY_RATE_HZ = 10;
7
+ export function defaultPolicy(version = DEFAULT_POLICY_VERSION, issuedAt = Math.floor(Date.now() / 1000)) {
8
+ return {
9
+ version,
10
+ topic: ALL_VESSELS_TOPIC,
11
+ protocol: PROTOCOL_VERSION,
12
+ issuedAt,
13
+ sharding: {
14
+ maxVesselsPerShard: 500,
15
+ maxVesselsPerInstance: 1000,
16
+ lookAheadKm: 50,
17
+ splitSettleMinutes: 10,
18
+ mergeSettleMinutes: 60,
19
+ mergeBelowRatio: 0.25,
20
+ boundaryHysteresisMeters: 1000,
21
+ },
22
+ election: {
23
+ epochMinutes: 10,
24
+ minUptimeMinutes: 10,
25
+ },
26
+ rates: {
27
+ maxSnapshotHzUnderway: 0.05,
28
+ maxSnapshotHzMoored: 0.01,
29
+ },
30
+ ais: {
31
+ maxTargets: 100,
32
+ maxRateHz: 0.02,
33
+ },
34
+ };
35
+ }
36
+ export function policyBytes(policy) {
37
+ return Buffer.from(canonicalJson(policy));
38
+ }
39
+ export function signPolicy(policy, identity) {
40
+ return {
41
+ policy,
42
+ signature: identity.sign(policyBytes(policy)).toString("base64"),
43
+ };
44
+ }
45
+ export function verifyPolicy(signed, operatorKeys, expectedTopic, now = Math.floor(Date.now() / 1000)) {
46
+ if (!validPolicy(signed.policy, expectedTopic, now))
47
+ return false;
48
+ const bytes = policyBytes(signed.policy);
49
+ const signature = Buffer.from(signed.signature, "base64");
50
+ for (const key of operatorKeys) {
51
+ if (!/^[0-9a-f]{64}$/i.test(key))
52
+ continue;
53
+ if (verifyDetached(Buffer.from(key, "hex"), bytes, signature))
54
+ return true;
55
+ }
56
+ return false;
57
+ }
58
+ export function validPolicy(policy, expectedTopic, now = Math.floor(Date.now() / 1000)) {
59
+ if (!Number.isInteger(policy.version) || policy.version < 1)
60
+ return false;
61
+ if (policy.topic !== expectedTopic)
62
+ return false;
63
+ if (policy.protocol !== PROTOCOL_VERSION)
64
+ return false;
65
+ if (!Number.isInteger(policy.issuedAt) || policy.issuedAt < 0)
66
+ return false;
67
+ if (policy.notBefore !== undefined &&
68
+ (!Number.isInteger(policy.notBefore) || policy.notBefore > now)) {
69
+ return false;
70
+ }
71
+ if (policy.notAfter !== undefined &&
72
+ policy.notAfter > 0 &&
73
+ (!Number.isInteger(policy.notAfter) || policy.notAfter < now)) {
74
+ return false;
75
+ }
76
+ const sharding = policy.sharding;
77
+ if (!sharding || typeof sharding !== "object")
78
+ return false;
79
+ if (!positive(sharding.maxVesselsPerShard))
80
+ return false;
81
+ if (!positive(sharding.maxVesselsPerInstance))
82
+ return false;
83
+ if (!nonNegative(sharding.lookAheadKm))
84
+ return false;
85
+ if (!nonNegative(sharding.splitSettleMinutes))
86
+ return false;
87
+ if (!nonNegative(sharding.mergeSettleMinutes))
88
+ return false;
89
+ if (typeof sharding.mergeBelowRatio !== "number" ||
90
+ sharding.mergeBelowRatio < 0 ||
91
+ sharding.mergeBelowRatio > 1) {
92
+ return false;
93
+ }
94
+ if (!nonNegative(sharding.boundaryHysteresisMeters))
95
+ return false;
96
+ const election = policy.election;
97
+ if (!election || typeof election !== "object")
98
+ return false;
99
+ if (!positive(election.epochMinutes))
100
+ return false;
101
+ if (!nonNegative(election.minUptimeMinutes))
102
+ return false;
103
+ const rates = policy.rates;
104
+ if (!rates || typeof rates !== "object")
105
+ return false;
106
+ if (!rate(rates.maxSnapshotHzUnderway))
107
+ return false;
108
+ if (!rate(rates.maxSnapshotHzMoored))
109
+ return false;
110
+ const ais = policy.ais;
111
+ if (!ais || typeof ais !== "object")
112
+ return false;
113
+ if (!nonNegative(ais.maxTargets))
114
+ return false;
115
+ if (!rate(ais.maxRateHz, true))
116
+ return false;
117
+ return true;
118
+ }
119
+ export function encodePolicy(signed) {
120
+ return Buffer.from(JSON.stringify(signed), "utf8");
121
+ }
122
+ export function decodePolicy(data) {
123
+ const parsed = JSON.parse(data.toString("utf8"));
124
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
125
+ throw new Error("policy: expected an object");
126
+ }
127
+ const signed = parsed;
128
+ if (signed.policy === null ||
129
+ typeof signed.policy !== "object" ||
130
+ typeof signed.signature !== "string") {
131
+ throw new Error("policy: malformed record");
132
+ }
133
+ return signed;
134
+ }
135
+ function positive(value) {
136
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
137
+ }
138
+ function nonNegative(value) {
139
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
140
+ }
141
+ function rate(value, allowZero = false) {
142
+ return (typeof value === "number" &&
143
+ Number.isFinite(value) &&
144
+ (allowZero ? value >= 0 : value > 0) &&
145
+ value <= MAX_POLICY_RATE_HZ);
146
+ }
147
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/policy/policy.ts"],"names":[],"mappings":"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"}
@@ -37,13 +37,13 @@ of private groups.
37
37
  | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
38
38
  | Stack | Holepunch: Hyperswarm + `@hyperswarm/secret-stream` + Protomux + Hypercore/Hyperbee + Autobase. |
39
39
  | Transport | Live plane = generation-rotated Hypercore channels; durable plane = Hypercore/Hyperbee/Autobase. |
40
- | Public network | `all-vessels`, joined and transmitting by default, opt-out, with four consent modes. |
40
+ | Public network | `all-vessels`, joined by default; transmission is opt-in (`receive-only` default), with four consent modes. |
41
41
  | Sharding | Global by default; adaptive geographic shards that subdivide to keep a configured vessel count. |
42
42
  | Relays | No fixed relays; a peer-elected aggregator per shard, gossip fallback, optional standalone bootstrap peer. |
43
43
  | Identity | Persistent Ed25519 keypair per instance; groups keyed by identity, not by a shared secret. |
44
44
  | Groups | Arbitrary number; roles, signed roster, simple invite links, re-key and member removal. |
45
45
  | Injection | Received vessels become real Signal K contexts; local data wins; no bridging between networks. |
46
- | Shared data | Per-data-set toggles (public / selected groups / none); curated defaults (no `electrical.*`). |
46
+ | Shared data | Per-data-set matrix in the webapp (Public + active groups); curated defaults (no `electrical.*`). |
47
47
  | Security | One authorized writer per context; groups approve the member's MMSI; public uses a claim registry + attestors (MMSI ownership unproven). |
48
48
 
49
49
  ## Still open
@@ -13,8 +13,8 @@ of the design must not violate.
13
13
  **Goal.** Let Signal K instances form peer-to-peer meshes and share vessel data in
14
14
  real time:
15
15
 
16
- - one **global public network** (`all-vessels`), joined and transmitting by
17
- default, opt-out;
16
+ - one **global public network** (`all-vessels`), joined with a receive-only
17
+ default; transmission and consent are per-instance;
18
18
  - any number of **private groups**, invite-only and encrypted.
19
19
 
20
20
  A vessel may participate in both at once, with a different view of its data in
@@ -4,8 +4,8 @@
4
4
 
5
5
  Two classes of network, both live at once:
6
6
 
7
- - **`all-vessels`** — one global public network, joined and transmitting by
8
- default, opt-out; implemented as adaptive geographic shards.
7
+ - **`all-vessels`** — one global public network, joined with a receive-only
8
+ default; transmission is opt-in; implemented as adaptive geographic shards.
9
9
  - **Groups** — any number of private, encrypted, invite-only networks with roles,
10
10
  a signed roster, and key rotation.
11
11
 
@@ -39,12 +39,16 @@ according to that network's policy, and ingests what each network carries.
39
39
  `H("signalk-p2p/all-vessels/v1")`, so every instance agrees without coordination.
40
40
  - **Open join.** No membership, no invite, no encryption. Identity is a
41
41
  pseudonymous Ed25519 key.
42
- - **Default on.** The plugin joins and transmits unless the operator opts out.
42
+ - **Default on, receive-only.** The plugin joins immediately but publishes
43
+ nothing until the owner chooses a consent mode. `receive-only`,
44
+ `underway-gated`, `coarse-position` and `opt-out` are available.
43
45
  - **Audiences** follow [`shared-data.md`](shared-data.md): each data set is shared
44
- publicly, with selected groups, or not at all. Consent (`opt-out` by default,
45
- plus `underway-gated`, `receive-only`, `coarse-position`) and rates are per
46
- network.
47
- - **AIS** defaults to send-and-receive publicly, capped and sampled.
46
+ publicly, with selected groups, or not at all, using the webapp matrix.
47
+ - **AIS** defaults to receive-only/private, capped and sampled by policy.
48
+ - **Topology and rates are centrally controlled.** Shard sizes, look-ahead,
49
+ split/merge thresholds, election epoch/min-uptime, public snapshot rate caps
50
+ and AIS caps come from a signed, versioned `NetworkPolicy`; clients cannot
51
+ override them. See §2.1.
48
52
  - **No fixed relays.** The mesh elects a relay/aggregator per shard from
49
53
  reachable peers; bootstrap peers are optional and replaceable.
50
54
  - **Write authorization** is enforced by a replicated claim registry: each context
@@ -55,6 +59,30 @@ according to that network's policy, and ingests what each network carries.
55
59
  The user-visible model is one network. Under the hood it is a set of geographic
56
60
  shards (§3), so no instance replicates the whole world.
57
61
 
62
+ ### 2.1 Network policy
63
+
64
+ A small signed policy carries the network-wide settings:
65
+
66
+ ```
67
+ NetworkPolicy {
68
+ version, issuedAt, notBefore?, notAfter?
69
+ topic, protocol
70
+ sharding { maxVesselsPerShard, maxVesselsPerInstance, lookAheadKm,
71
+ splitSettleMinutes, mergeSettleMinutes, mergeBelowRatio,
72
+ boundaryHysteresisMeters }
73
+ election { epochMinutes, minUptimeMinutes }
74
+ rates { maxSnapshotHzUnderway, maxSnapshotHzMoored }
75
+ ais { maxTargets, maxRateHz }
76
+ }
77
+ ```
78
+
79
+ It is signed by the network operator. A signed default ships with the plugin; a
80
+ newer signed policy received over the mesh overrides it and is cached. Clients
81
+ verify the signature against pinned operator keys, reject rollback and invalid
82
+ values, and fall back to the shipped default if no live policy is available.
83
+ The operator's private key never goes to a relay; the standalone peer can
84
+ republish a signed policy file.
85
+
58
86
  Write authorization, group approval and the public claim registry are specified in
59
87
  [`security.md`](security.md).
60
88
 
@@ -302,7 +330,7 @@ signalk-p2p://join/<base64url>{
302
330
  }>
303
331
  ```
304
332
 
305
- - Anyone holding it can join until `expiresAt` (default 7 days).
333
+ - Anyone holding it can join until `expiresAt` (default 24 hours).
306
334
  - Optionally single-use: the admin records the nonce; a second `join` with the
307
335
  same nonce is rejected.
308
336
  - It is a bearer token: treat it as a secret. The UI should say so.
@@ -62,7 +62,8 @@ npx signalk-p2p-peer \
62
62
  --topic signalk-p2p/all-vessels/v1 \
63
63
  --batch-interval 2000 \
64
64
  --history 10 \
65
- --port 49737
65
+ --port 49737 \
66
+ --policy /etc/signalk-p2p/policy.json
66
67
  ```
67
68
 
68
69
  During development:
@@ -81,10 +82,14 @@ npm run peer:start # compiled dist/peer/cli.js
81
82
  "batchIntervalMs": 2000,
82
83
  "maxBatchBytes": 65536,
83
84
  "historyBatches": 10,
84
- "port": 49737
85
+ "port": 49737,
86
+ "policyFile": "/etc/signalk-p2p/policy.json"
85
87
  }
86
88
  ```
87
89
 
90
+ `policyFile` is optional. When set, the peer verifies and republishes the signed
91
+ network policy every five minutes; it never needs the operator private key.
92
+
88
93
  ```bash
89
94
  npx signalk-p2p-peer --config /etc/signalk-p2p/peer.json
90
95
  ```
@@ -329,7 +329,7 @@ context to be dropped. Observations dedupe by MMSI and timestamp.
329
329
 
330
330
  - An invite is a **bearer token** (`skp2p://join/...`): whoever holds it can
331
331
  request to join until it expires. It is not a proof of MMSI ownership.
332
- - Default expiry: 7 days. Optional single-use via a nonce recorded by the roster.
332
+ - Default expiry: 24 hours. Optional single-use via a nonce recorded by the roster.
333
333
  - The admin's approval of the join request is the actual admission; the invite
334
334
  only gets the request in the door.
335
335
  - Treat invites as secrets; the UI says so and offers revoke/expire controls.
@@ -237,7 +237,8 @@ A group that is removed or left is dropped from every data set's group list.
237
237
 
238
238
  ## 4. Consent modes (public audience)
239
239
 
240
- The public network is joined and transmitting by default, but "transmitting" is
240
+ The public network is joined by default but **receive-only** until the owner opts
241
+ in; "transmitting" is
241
242
  configurable. The mode applies to position-bearing data sets (Position and Depth)
242
243
  when they are shared publicly.
243
244
 
@@ -248,7 +249,7 @@ when they are shared publicly.
248
249
  | `receive-only` | Join and ingest, but publish nothing of your own. | Maximum privacy; no contribution. |
249
250
  | `coarse-position` | Publish position rounded to a grid (default 5 km) instead of the exact fix. | Approximate only; not anonymity. |
250
251
 
251
- The mode is per network. Groups use the same modes and default to `opt-out` (a
252
+ The mode is per network. Groups use the same modes and default to `receive-only` (a
252
253
  group is already a trusted audience). A boat can be `opt-out` to its club and
253
254
  `underway-gated` to the world.
254
255
 
@@ -311,7 +312,7 @@ The AIS data set uses the same three-way control as the others:
311
312
  - **Share with groups** — publish into the selected groups;
312
313
  - **Don't share** — no AIS over P2P.
313
314
 
314
- Defaults: public **on**, groups **off**.
315
+ Defaults: public **off**, groups **off**; AIS is receive-only/private by default.
315
316
 
316
317
  ### 6.2 Direction
317
318
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-p2p-plugin",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Private and public peer-to-peer mesh networks for Signal K vessels.",
5
5
  "keywords": [
6
6
  "signalk-node-server-plugin",
@@ -46,7 +46,8 @@
46
46
  "deploy",
47
47
  "public",
48
48
  "icon.svg",
49
- "CHANGELOG.md"
49
+ "CHANGELOG.md",
50
+ "policy"
50
51
  ],
51
52
  "scripts": {
52
53
  "build": "tsc -b",
@@ -0,0 +1,30 @@
1
+ {
2
+ "policy": {
3
+ "version": 1,
4
+ "topic": "signalk-p2p/all-vessels/v1",
5
+ "protocol": 1,
6
+ "issuedAt": 1794023095,
7
+ "sharding": {
8
+ "maxVesselsPerShard": 500,
9
+ "maxVesselsPerInstance": 1000,
10
+ "lookAheadKm": 50,
11
+ "splitSettleMinutes": 10,
12
+ "mergeSettleMinutes": 60,
13
+ "mergeBelowRatio": 0.25,
14
+ "boundaryHysteresisMeters": 1000
15
+ },
16
+ "election": {
17
+ "epochMinutes": 10,
18
+ "minUptimeMinutes": 10
19
+ },
20
+ "rates": {
21
+ "maxSnapshotHzUnderway": 0.05,
22
+ "maxSnapshotHzMoored": 0.01
23
+ },
24
+ "ais": {
25
+ "maxTargets": 100,
26
+ "maxRateHz": 0.02
27
+ }
28
+ },
29
+ "signature": "bnGdM4p1nOlYh9oudCtSt5dBvhlQFQwkg3gYedh+/SothLJKuhDdy7qoKxLTTYp6RaVp8XTL2Dj4BDcR12l9CA=="
30
+ }
@@ -0,0 +1,2 @@
1
+ publicKey: b393c24608f23cdb389cfb0cbc8c9de619345fa110cdddcb14ab6afdb494db09
2
+ fingerprint: 254375e336e45f93bb5691e49fdb4e758fefed7981940d98dc9773b8b23390bf