signalk-siparu 0.2.1 → 0.2.3

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 (39) hide show
  1. package/README.md +29 -24
  2. package/package.json +1 -1
  3. package/plugin/dist/anchor.d.ts +11 -0
  4. package/plugin/dist/anchor.js +103 -0
  5. package/plugin/dist/approval.d.ts +40 -0
  6. package/plugin/dist/approval.js +170 -0
  7. package/plugin/dist/config-routes.d.ts +1 -0
  8. package/plugin/dist/config-routes.js +11 -4
  9. package/plugin/dist/config.d.ts +8 -1
  10. package/plugin/dist/config.js +12 -5
  11. package/plugin/dist/contract.d.ts +16 -62
  12. package/plugin/dist/index.js +72 -52
  13. package/plugin/dist/keystore.js +1 -0
  14. package/plugin/dist/keysync.d.ts +1 -0
  15. package/plugin/dist/keysync.js +31 -5
  16. package/plugin/dist/live.d.ts +2 -7
  17. package/plugin/dist/live.js +2 -58
  18. package/plugin/dist/metrics.js +7 -1
  19. package/plugin/dist/origin-guard.d.ts +5 -0
  20. package/plugin/dist/origin-guard.js +34 -0
  21. package/plugin/dist/pairing.d.ts +10 -0
  22. package/plugin/dist/pairing.js +63 -19
  23. package/plugin/dist/rollup.js +2 -1
  24. package/plugin/dist/sealer.d.ts +5 -8
  25. package/plugin/dist/sealer.js +9 -71
  26. package/plugin/dist/sealing.d.ts +3 -5
  27. package/plugin/dist/voyage-routes.d.ts +1 -0
  28. package/plugin/dist/voyage-routes.js +11 -4
  29. package/public/assets/{Logbook-DKQgrjzR.js → Logbook-CZb9OaA4.js} +1 -1
  30. package/public/assets/{Map-DeGKZhDf.js → Map-CN4rx5A-.js} +4 -4
  31. package/public/assets/Voyage-CzIqnUBA.js +5 -0
  32. package/public/assets/{index-DFYgRoHp.js → index-5gz9-vQd.js} +4 -4
  33. package/public/assets/{style-DhMV3_OD.js → style-CSszoHyM.js} +1 -1
  34. package/public/index.html +1 -1
  35. package/plugin/dist/alarms.d.ts +0 -23
  36. package/plugin/dist/alarms.js +0 -118
  37. package/plugin/dist/alertrules.d.ts +0 -74
  38. package/plugin/dist/alertrules.js +0 -282
  39. package/public/assets/Voyage-csfbROs0.js +0 -5
package/README.md CHANGED
@@ -47,16 +47,24 @@ generator tab. The same gauges are also served over the API (`GET /live`,
47
47
 
48
48
  Signal K ships with security switched off, and nothing in the setup makes you turn
49
49
  it on. With it off, every plugin's HTTP surface is open to anyone who can reach
50
- your boat's network, this one included: the pairing endpoints below answer whoever
51
- asks, so anyone on the network could pair their own screen to her. (The relay
52
- credential itself is out of reach: it lives in a mode-600 file under the plugin's
53
- data directory, not in the config.)
54
-
55
- Little of that is peculiar to Siparu. On an unsecured server `GET /skServer/plugins`
56
- already discloses every plugin's configuration and the App Store will install code.
57
- What is peculiar to Siparu is the consequence: someone on the same marina wifi can
58
- link your boat to **their** account, and your own screen will go on saying "paired"
59
- while they watch her.
50
+ your boat's network, this one included. (The relay credential itself is out of
51
+ reach: it lives in a mode-600 file under the plugin's data directory, not in the
52
+ config. The relay address is not a plugin option at all - it comes from the
53
+ server process's environment, so nothing that writes plugin options can redirect
54
+ the boat.)
55
+
56
+ Because of that, the plugin locks its own writes while security is off: pairing,
57
+ unpairing and log edits refuse until you either add an admin user in Signal K or
58
+ tick "Allow pairing and log edits while Signal K security is off" in the plugin
59
+ settings. The tick exists for installs that cannot easily turn security on; it is
60
+ an acceptance of the risk, not a removal of it.
61
+
62
+ The lock cannot make an unsecured server safe. On such a server
63
+ `GET /skServer/plugins` already discloses every plugin's configuration, the
64
+ server's own config route can change any plugin's settings - including that very
65
+ tick - and the App Store will install code. The consequence peculiar to Siparu is
66
+ what a stranger on the marina wifi could do with an open door: link your boat to
67
+ **their** account while your own screen goes on saying "paired".
60
68
 
61
69
  So before you pair, add an admin user in Signal K (Security > Users). It takes a
62
70
  minute and it is the difference between a boat you share and a boat you leak.
@@ -111,6 +119,13 @@ commit, names each write route, and an eighth one fails the build.
111
119
  | `POST /voyages/:id/merge-previous` | Join this passage to the one before it, when one trip was recorded as two |
112
120
  | `POST /voyages/:id/undo-merge` | Separate a passage back into what it was made of |
113
121
 
122
+ Those seven, and `GET /pair/status` with them, answer only requests that came from the
123
+ boat's own screen. A browser marks every request with its origin, and one from a page on
124
+ another site gets 403: on a server with security off, which is the default, an advert
125
+ frame in any tab on the boat's network could otherwise unpair her or lift the pairing
126
+ code off the helm. A caller with no browser headers, curl or a script on her own network,
127
+ is not affected. Neither is the reading surface above.
128
+
114
129
  The two voyage edits are reachable from the boat's own network only. What the
115
130
  shore can ask her is a closed set of five questions, all of them reads
116
131
  (`history`, `snapshots`, `voyages`, `track`, `phases`); nothing on that socket
@@ -141,20 +156,10 @@ that sends a position in the clear, because a boat whose privacy depends on the
141
156
  state of a list held elsewhere does not have any.
142
157
 
143
158
  Once a device is named, the report is encrypted to it and the relay carries a
144
- block it cannot read. Three things stay legible beside it, and they are worth
145
- stating plainly because they are what a carrier can see:
146
-
147
- - Her identifier and the time she sent the frame, so it can be routed at all.
148
- - How loud she is: `normal`, `warning` or `alarm`, and nothing about what is
149
- wrong. A carrier has to know a notification is due, and this field is inside
150
- her signature so that it cannot decide one is not.
151
- - When the newest audible condition began, as a moment in time. Without it a
152
- second alarm raised while a first one stands is indistinguishable from the
153
- first one still standing, and the second would never be announced. It says
154
- that an event is a different event; what the event is stays encrypted.
155
-
156
- Taken together, a carrier can see that something is wrong aboard, roughly how
157
- serious, and when each one started. It cannot see what any of them is.
159
+ block it cannot read. Two things stay legible beside it, and they are worth
160
+ stating plainly because they are what a carrier can see: her identifier and
161
+ the time she sent the frame, so it can be routed at all. Both are inside her
162
+ signature, so neither can be rewritten in transit.
158
163
 
159
164
  Voyage detection opens after sustained movement, closes after sustained
160
165
  stillness, and folds short docking manoeuvres into the preceding voyage. Its
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-siparu",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Kept aboard, proven ashore. An impartial, timestamped record of every voyage: position, wind, depth and logbook, written on the boat and readable from anywhere. A read-only Signal K plugin with a built-in dashboard.",
5
5
  "keywords": [
6
6
  "signalk-node-server-plugin",
@@ -0,0 +1,11 @@
1
+ import type { DeviceAnchor } from './approval';
2
+ export declare class AnchorStore {
3
+ private readonly file;
4
+ private writeChain;
5
+ constructor(dataDir: string);
6
+ load(boatId: string | undefined, pairedAt: string | undefined): DeviceAnchor | null | 'broken';
7
+ set(boatId: string, pairedAt: string, anchor: DeviceAnchor): Promise<void>;
8
+ clear(): Promise<void>;
9
+ flush(): Promise<void>;
10
+ private write;
11
+ }
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AnchorStore = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const FILE_VERSION = 1;
40
+ const PUB_SHAPE = /^[A-Za-z0-9_-]{43}$/;
41
+ class AnchorStore {
42
+ file;
43
+ writeChain = Promise.resolve();
44
+ constructor(dataDir) {
45
+ this.file = path.join(dataDir, 'anchor.json');
46
+ }
47
+ load(boatId, pairedAt) {
48
+ let text;
49
+ try {
50
+ text = fs.readFileSync(this.file, 'utf8');
51
+ }
52
+ catch (e) {
53
+ return e?.code === 'ENOENT' ? null : 'broken';
54
+ }
55
+ let raw;
56
+ try {
57
+ raw = JSON.parse(text);
58
+ }
59
+ catch {
60
+ return 'broken';
61
+ }
62
+ if (raw.v !== FILE_VERSION ||
63
+ typeof raw.boat !== 'string' ||
64
+ typeof raw.paired_at !== 'string' ||
65
+ typeof raw.kid !== 'string' ||
66
+ raw.kid.length === 0 ||
67
+ typeof raw.pub !== 'string' ||
68
+ !PUB_SHAPE.test(raw.pub)) {
69
+ return 'broken';
70
+ }
71
+ if (!boatId || raw.boat !== boatId)
72
+ return null;
73
+ if (!pairedAt || raw.paired_at !== pairedAt)
74
+ return null;
75
+ return { kid: raw.kid, pub: raw.pub };
76
+ }
77
+ set(boatId, pairedAt, anchor) {
78
+ return this.write(async () => {
79
+ const tmp = `${this.file}.tmp`;
80
+ const shape = {
81
+ v: FILE_VERSION,
82
+ boat: boatId,
83
+ paired_at: pairedAt,
84
+ kid: anchor.kid,
85
+ pub: anchor.pub
86
+ };
87
+ await fs.promises.writeFile(tmp, JSON.stringify(shape, null, 2), { mode: 0o600 });
88
+ await fs.promises.chmod(tmp, 0o600);
89
+ await fs.promises.rename(tmp, this.file);
90
+ });
91
+ }
92
+ clear() {
93
+ return this.write(() => fs.promises.rm(this.file, { force: true }));
94
+ }
95
+ flush() {
96
+ return this.writeChain.catch(() => undefined);
97
+ }
98
+ write(run) {
99
+ this.writeChain = this.writeChain.then(run, run);
100
+ return this.writeChain;
101
+ }
102
+ }
103
+ exports.AnchorStore = AnchorStore;
@@ -0,0 +1,40 @@
1
+ import { type KeyObject } from 'crypto';
2
+ import type { DevicePublicKey } from './contract';
3
+ export declare const MAC_BYTES = 32;
4
+ export interface DeviceAnchor {
5
+ kid: string;
6
+ pub: string;
7
+ }
8
+ export declare function approvalInput(boat: string, approverKid: string, subjectKid: string, subjectPub: Buffer): Buffer;
9
+ export declare function computeApproval(opts: {
10
+ approverPriv: KeyObject;
11
+ inboxPub: Buffer;
12
+ boat: string;
13
+ approverKid: string;
14
+ subjectKid: string;
15
+ subjectPub: Buffer;
16
+ }): Buffer;
17
+ export declare function verifyApproval(opts: {
18
+ inboxPriv: KeyObject;
19
+ approverPub: Buffer;
20
+ boat: string;
21
+ approverKid: string;
22
+ subjectKid: string;
23
+ subjectPub: Buffer;
24
+ mac: Buffer;
25
+ }): boolean;
26
+ export interface SkippedDevice {
27
+ kid: string;
28
+ reason: string;
29
+ }
30
+ export interface AcceptResult {
31
+ accepted: DevicePublicKey[];
32
+ skipped: SkippedDevice[];
33
+ pinned: boolean;
34
+ }
35
+ export declare function acceptDevices(opts: {
36
+ anchor: DeviceAnchor | null | 'broken';
37
+ entries: DevicePublicKey[];
38
+ inboxPriv: KeyObject | undefined;
39
+ boat: string | undefined;
40
+ }): AcceptResult;
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAC_BYTES = void 0;
4
+ exports.approvalInput = approvalInput;
5
+ exports.computeApproval = computeApproval;
6
+ exports.verifyApproval = verifyApproval;
7
+ exports.acceptDevices = acceptDevices;
8
+ const crypto_1 = require("crypto");
9
+ const sealing_1 = require("./sealing");
10
+ const LABEL = 'siparu/device-approval/v1';
11
+ const APPROVAL_VERSION = 1;
12
+ const KEY_BYTES = 32;
13
+ exports.MAC_BYTES = 32;
14
+ function approvalInput(boat, approverKid, subjectKid, subjectPub) {
15
+ return Buffer.concat([
16
+ Buffer.from(LABEL, 'utf8'),
17
+ (0, sealing_1.u16be)(APPROVAL_VERSION),
18
+ (0, sealing_1.lp)(Buffer.from(boat, 'utf8')),
19
+ (0, sealing_1.lp)(Buffer.from(approverKid, 'utf8')),
20
+ (0, sealing_1.lp)(Buffer.from(subjectKid, 'utf8')),
21
+ (0, sealing_1.lp)(subjectPub)
22
+ ]);
23
+ }
24
+ function approvalKey(shared, approverPub, boat, approverKid, subjectKid) {
25
+ const info = `${LABEL}/${boat}/${approverKid}/${subjectKid}`;
26
+ return Buffer.from((0, crypto_1.hkdfSync)('sha256', shared, approverPub, info, KEY_BYTES));
27
+ }
28
+ function computeApproval(opts) {
29
+ const shared = (0, crypto_1.diffieHellman)({
30
+ privateKey: opts.approverPriv,
31
+ publicKey: (0, sealing_1.x25519PublicFromRaw)(opts.inboxPub)
32
+ });
33
+ const approverPub = (0, sealing_1.publicFromPrivate)(opts.approverPriv);
34
+ const key = approvalKey(shared, approverPub, opts.boat, opts.approverKid, opts.subjectKid);
35
+ return (0, crypto_1.createHmac)('sha256', key)
36
+ .update(approvalInput(opts.boat, opts.approverKid, opts.subjectKid, opts.subjectPub))
37
+ .digest();
38
+ }
39
+ function verifyApproval(opts) {
40
+ if (opts.mac.length !== exports.MAC_BYTES)
41
+ return false;
42
+ let expected;
43
+ try {
44
+ const shared = (0, crypto_1.diffieHellman)({
45
+ privateKey: opts.inboxPriv,
46
+ publicKey: (0, sealing_1.x25519PublicFromRaw)(opts.approverPub)
47
+ });
48
+ const key = approvalKey(shared, opts.approverPub, opts.boat, opts.approverKid, opts.subjectKid);
49
+ expected = (0, crypto_1.createHmac)('sha256', key)
50
+ .update(approvalInput(opts.boat, opts.approverKid, opts.subjectKid, opts.subjectPub))
51
+ .digest();
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ return (0, crypto_1.timingSafeEqual)(expected, opts.mac);
57
+ }
58
+ function acceptDevices(opts) {
59
+ const { anchor, entries, inboxPriv, boat } = opts;
60
+ if (anchor === null) {
61
+ return { accepted: [...entries], skipped: [], pinned: false };
62
+ }
63
+ const skipped = [];
64
+ if (anchor === 'broken' || !boat) {
65
+ return {
66
+ accepted: [],
67
+ skipped: entries.map((e) => ({
68
+ kid: e.kid,
69
+ reason: anchor === 'broken'
70
+ ? 'the anchor on disk cannot be read; re-pair to pin the screens again'
71
+ : 'the boat is not paired'
72
+ })),
73
+ pinned: true
74
+ };
75
+ }
76
+ const trusted = new Map();
77
+ const anchorPub = decodePub(anchor.pub);
78
+ if (anchorPub)
79
+ trusted.set(anchor.kid, anchorPub);
80
+ const accepted = [];
81
+ const pending = new Map();
82
+ for (const entry of entries) {
83
+ if (entry.kid === anchor.kid) {
84
+ const entryPub = decodePub(entry.pub);
85
+ if (anchorPub && entryPub?.equals(anchorPub)) {
86
+ accepted.push(entry);
87
+ }
88
+ else {
89
+ skipped.push({
90
+ kid: entry.kid,
91
+ reason: 'carries the anchor id over a different key'
92
+ });
93
+ }
94
+ continue;
95
+ }
96
+ pending.set(entry, '');
97
+ }
98
+ let grew = true;
99
+ while (grew) {
100
+ grew = false;
101
+ for (const [entry] of pending) {
102
+ const verdict = admissible(entry, trusted, inboxPriv, boat);
103
+ if (verdict === true) {
104
+ accepted.push(entry);
105
+ const pub = decodePub(entry.pub);
106
+ if (pub)
107
+ trusted.set(entry.kid, pub);
108
+ pending.delete(entry);
109
+ grew = true;
110
+ }
111
+ else {
112
+ pending.set(entry, verdict);
113
+ }
114
+ }
115
+ }
116
+ for (const [entry, reason] of pending) {
117
+ skipped.push({ kid: entry.kid, reason });
118
+ }
119
+ return { accepted, skipped, pinned: true };
120
+ }
121
+ function admissible(entry, trusted, inboxPriv, boat) {
122
+ const approvals = entry.approvals ?? [];
123
+ if (approvals.length === 0) {
124
+ return 'carries no approval from a device this boat trusts';
125
+ }
126
+ if (!inboxPriv) {
127
+ return 'the boat holds no inbox key yet to verify the approval with';
128
+ }
129
+ const subjectPub = decodePub(entry.pub);
130
+ const refusals = [];
131
+ for (const { by, mac } of approvals) {
132
+ const approverPub = trusted.get(by);
133
+ if (!approverPub) {
134
+ refusals.push({ rank: 2, text: `approved by "${by}", which this boat does not trust` });
135
+ continue;
136
+ }
137
+ const raw = decodeMac(mac);
138
+ if (!subjectPub || !raw) {
139
+ refusals.push({ rank: 1, text: 'the approval is malformed' });
140
+ continue;
141
+ }
142
+ const ok = verifyApproval({
143
+ inboxPriv,
144
+ approverPub,
145
+ boat,
146
+ approverKid: by,
147
+ subjectKid: entry.kid,
148
+ subjectPub,
149
+ mac: raw
150
+ });
151
+ if (ok)
152
+ return true;
153
+ refusals.push({ rank: 0, text: 'the approval does not verify' });
154
+ }
155
+ return worst(refusals);
156
+ }
157
+ function worst(refusals) {
158
+ const first = refusals.reduce((best, r) => (r.rank < best.rank ? r : best));
159
+ return refusals.length === 1
160
+ ? first.text
161
+ : `${first.text} (${refusals.length} approvals, none accepted)`;
162
+ }
163
+ function decodePub(pub) {
164
+ const raw = Buffer.from(pub, 'base64url');
165
+ return raw.length === 32 ? raw : null;
166
+ }
167
+ function decodeMac(mac) {
168
+ const raw = Buffer.from(mac, 'base64url');
169
+ return raw.length === exports.MAC_BYTES ? raw : null;
170
+ }
@@ -2,6 +2,7 @@ import type { ServerAPI } from '@signalk/server-api';
2
2
  import type { IRouter } from 'express';
3
3
  interface ConfigDeps {
4
4
  app: ServerAPI;
5
+ acceptOpenNetwork: () => boolean;
5
6
  getConfig: () => object;
6
7
  fuelPathsView: () => {
7
8
  available: string[];
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.allowConfigure = allowConfigure;
4
4
  exports.registerConfigRoutes = registerConfigRoutes;
5
5
  const pairing_1 = require("./pairing");
6
+ const origin_guard_1 = require("./origin-guard");
6
7
  function allowConfigure(app, req) {
7
8
  try {
8
9
  const ss = app.securityStrategy;
@@ -39,12 +40,18 @@ function readJsonBody(req) {
39
40
  });
40
41
  }
41
42
  function registerConfigRoutes(router, deps) {
42
- const { app, getConfig, fuelPathsView, restart } = deps;
43
+ const { app, acceptOpenNetwork, getConfig, fuelPathsView, restart } = deps;
43
44
  router.get('/config/fuel-paths', (_req, res) => {
44
45
  res.json(fuelPathsView());
45
46
  });
46
- router.post('/config/fuel-paths', (req, res) => {
47
- if (!(0, pairing_1.securityOff)(app, req) && !allowConfigure(app, req)) {
47
+ router.post('/config/fuel-paths', (0, origin_guard_1.sameOrigin)((req, res) => {
48
+ if ((0, pairing_1.securityOff)(app, req)) {
49
+ if ((0, pairing_1.writeLocked)(app, req, acceptOpenNetwork())) {
50
+ res.status(403).json({ error: 'security_off', message: pairing_1.WRITE_LOCKED_MESSAGE });
51
+ return;
52
+ }
53
+ }
54
+ else if (!allowConfigure(app, req)) {
48
55
  res.status(403).json({ error: 'admin_required' });
49
56
  return;
50
57
  }
@@ -66,5 +73,5 @@ function registerConfigRoutes(router, deps) {
66
73
  if (!res.headersSent)
67
74
  res.status(500).json({ error: 'internal' });
68
75
  });
69
- });
76
+ }));
70
77
  }
@@ -21,10 +21,10 @@ export interface Options {
21
21
  maxStorageMB: number;
22
22
  chartsRemoteUrl: string;
23
23
  chartsBasemapUrl: string;
24
- relayUrl: string;
25
24
  ports: PortEntry[];
26
25
  voyage: VoyageOptions;
27
26
  fuelRatePaths: string[];
27
+ acceptOpenNetwork: boolean;
28
28
  }
29
29
  export declare const DEFAULTS: Options;
30
30
  export declare const INTERNAL: {
@@ -37,6 +37,7 @@ export declare const INTERNAL: {
37
37
  export declare const KN_TO_MS = 0.514444;
38
38
  export declare function isCalendarMonthDay(s: string): boolean;
39
39
  export declare function safeRelayUrl(raw: unknown): string | undefined;
40
+ export declare const RELAY_URL: string;
40
41
  export declare function resolveOptions(raw: unknown): Options;
41
42
  export declare const CONFIG_SCHEMA: {
42
43
  type: string;
@@ -165,6 +166,12 @@ export declare const CONFIG_SCHEMA: {
165
166
  };
166
167
  };
167
168
  };
169
+ acceptOpenNetwork: {
170
+ type: string;
171
+ title: string;
172
+ description: string;
173
+ default: boolean;
174
+ };
168
175
  fuelRatePaths: {
169
176
  type: string;
170
177
  title: string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CONFIG_SCHEMA = exports.KN_TO_MS = exports.INTERNAL = exports.DEFAULTS = void 0;
3
+ exports.CONFIG_SCHEMA = exports.RELAY_URL = exports.KN_TO_MS = exports.INTERNAL = exports.DEFAULTS = void 0;
4
4
  exports.isCalendarMonthDay = isCalendarMonthDay;
5
5
  exports.safeRelayUrl = safeRelayUrl;
6
6
  exports.resolveOptions = resolveOptions;
@@ -12,7 +12,6 @@ exports.DEFAULTS = {
12
12
  maxStorageMB: 500,
13
13
  chartsRemoteUrl: 'https://tiles.siparu.app',
14
14
  chartsBasemapUrl: 'https://tiles.openfreemap.org/planet',
15
- relayUrl: 'https://relay.siparu.app',
16
15
  ports: [],
17
16
  voyage: {
18
17
  openKnots: 1.5,
@@ -23,7 +22,8 @@ exports.DEFAULTS = {
23
22
  mergeShortNm: 1.0,
24
23
  phaseMinMinutes: 10
25
24
  },
26
- fuelRatePaths: []
25
+ fuelRatePaths: [],
26
+ acceptOpenNetwork: false
27
27
  };
28
28
  exports.INTERNAL = {
29
29
  samplePeriodMs: 2000,
@@ -49,6 +49,7 @@ function safeRelayUrl(raw) {
49
49
  return undefined;
50
50
  return raw.replace(/\/+$/, '');
51
51
  }
52
+ exports.RELAY_URL = safeRelayUrl(process.env.SIPARU_RELAY_URL) ?? 'https://relay.siparu.app';
52
53
  function resolveOptions(raw) {
53
54
  const c = (raw ?? {});
54
55
  const v = (c.voyage ?? {});
@@ -65,7 +66,6 @@ function resolveOptions(raw) {
65
66
  chartsBasemapUrl: /^https?:\/\/\S+$/.test(c.chartsBasemapUrl ?? '')
66
67
  ? c.chartsBasemapUrl.replace(/\/+$/, '')
67
68
  : exports.DEFAULTS.chartsBasemapUrl,
68
- relayUrl: safeRelayUrl(c.relayUrl) ?? exports.DEFAULTS.relayUrl,
69
69
  ports: Array.isArray(c.ports)
70
70
  ? c.ports
71
71
  .filter((p) => !!p &&
@@ -89,7 +89,8 @@ function resolveOptions(raw) {
89
89
  },
90
90
  fuelRatePaths: Array.isArray(c.fuelRatePaths)
91
91
  ? c.fuelRatePaths.filter((p) => typeof p === 'string' && p.trim().length > 0).map((p) => p.trim())
92
- : exports.DEFAULTS.fuelRatePaths
92
+ : exports.DEFAULTS.fuelRatePaths,
93
+ acceptOpenNetwork: c.acceptOpenNetwork === true
93
94
  };
94
95
  }
95
96
  exports.CONFIG_SCHEMA = {
@@ -206,6 +207,12 @@ exports.CONFIG_SCHEMA = {
206
207
  }
207
208
  }
208
209
  },
210
+ acceptOpenNetwork: {
211
+ type: 'boolean',
212
+ title: 'Allow pairing and log edits while Signal K security is off',
213
+ description: 'With Signal K security off, anyone on the boat\'s network can pair this boat to their own account, unpair her or edit her log, so Siparu refuses all three until you answer for it here. It cannot protect this page: the server writes these settings itself, and on an unsecured server anyone can change them, this tick included. The real fix is an admin user in Signal K (Security > Users). Tick this only on a network you trust.',
214
+ default: exports.DEFAULTS.acceptOpenNetwork
215
+ },
209
216
  fuelRatePaths: {
210
217
  type: 'array',
211
218
  title: 'Fuel consumption source (advanced)',
@@ -1,3 +1,4 @@
1
+ export type DepthDatum = 'belowTransducer' | 'belowKeel' | 'belowSurface';
1
2
  export interface Snapshot {
2
3
  ts: number;
3
4
  lat: number | null;
@@ -18,6 +19,7 @@ export interface Snapshot {
18
19
  air_temp_k: number | null;
19
20
  air_pressure_pa: number | null;
20
21
  depth: number | null;
22
+ depth_datum?: DepthDatum | null;
21
23
  water_temp_k: number | null;
22
24
  gps_satellites: number | null;
23
25
  ais_class: string | null;
@@ -221,6 +223,15 @@ export interface HealthResult {
221
223
  mode: 'sealed' | 'blocked' | 'none';
222
224
  reason: string | null;
223
225
  screens: string[];
226
+ screens_pinned: boolean;
227
+ screens_skipped: {
228
+ kid: string;
229
+ reason: string;
230
+ }[];
231
+ screens_rejected: {
232
+ kid: string;
233
+ reason: string;
234
+ }[];
224
235
  };
225
236
  }
226
237
  export type PhaseKind = 'underway' | 'anchored' | 'moored' | 'stopped';
@@ -323,73 +334,16 @@ export interface SealedFrame {
323
334
  sig: string;
324
335
  [extension: string]: unknown;
325
336
  }
326
- export type AlertLevel = 'normal' | 'warning' | 'alarm';
327
- export interface StandingCondition {
328
- path: string;
329
- state: Exclude<AlertLevel, 'normal'>;
330
- message: string | null;
331
- since: number;
332
- }
333
- export interface AlertNote {
334
- path: string;
335
- state: Exclude<AlertLevel, 'normal'>;
336
- message: string | null;
337
- since: number;
338
- }
339
- export type AlertRing = 'warning' | 'alarm' | 'never';
340
- export interface AlertRule {
341
- path: string;
342
- ring_from: AlertRing;
343
- }
344
- export interface SetAlertRulesRequest {
345
- type: 'setalertrules';
346
- v: number;
347
- id: string;
348
- kid: string;
349
- ts: number;
350
- rules: AlertRule[];
351
- proof: string;
352
- }
353
- export interface AlertRulesResult {
354
- rules: AlertRule[];
355
- ts: number;
356
- }
357
- export type SetAlertRulesResponse = {
358
- type: 'setalertrules';
359
- id: string;
360
- result: AlertRulesResult;
361
- } | {
362
- type: 'setalertrules';
363
- id: string;
364
- error: {
365
- code: string;
366
- message: string;
367
- };
368
- };
369
- export interface AlertRulesRequest {
370
- type: 'alertrules';
371
- id: string;
372
- }
373
- export interface AlertRulesReadResult extends AlertRulesResult {
374
- known: string[];
375
- }
376
- export type AlertRulesResponse = {
377
- type: 'alertrules';
378
- id: string;
379
- result: AlertRulesReadResult;
380
- } | {
381
- type: 'alertrules';
382
- id: string;
383
- error: {
384
- code: string;
385
- message: string;
386
- };
387
- };
388
337
  export interface BoatPublicKeys {
389
338
  identity: string;
390
339
  inbox: string;
391
340
  }
341
+ export interface DeviceApproval {
342
+ by: string;
343
+ mac: string;
344
+ }
392
345
  export interface DevicePublicKey {
393
346
  kid: string;
394
347
  pub: string;
348
+ approvals?: DeviceApproval[];
395
349
  }