signalk-backup 0.1.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 (43) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +71 -0
  3. package/assets/icon.png +0 -0
  4. package/assets/icon.svg +46 -0
  5. package/package.json +91 -0
  6. package/plugin/backup-client.d.ts +60 -0
  7. package/plugin/backup-client.d.ts.map +1 -0
  8. package/plugin/backup-client.js +87 -0
  9. package/plugin/backup-client.js.map +1 -0
  10. package/plugin/config/schema.d.ts +32 -0
  11. package/plugin/config/schema.d.ts.map +1 -0
  12. package/plugin/config/schema.js +83 -0
  13. package/plugin/config/schema.js.map +1 -0
  14. package/plugin/database-export/index.d.ts +33 -0
  15. package/plugin/database-export/index.d.ts.map +1 -0
  16. package/plugin/database-export/index.js +55 -0
  17. package/plugin/database-export/index.js.map +1 -0
  18. package/plugin/database-export/questdb.d.ts +41 -0
  19. package/plugin/database-export/questdb.d.ts.map +1 -0
  20. package/plugin/database-export/questdb.js +143 -0
  21. package/plugin/database-export/questdb.js.map +1 -0
  22. package/plugin/database-export/types.d.ts +30 -0
  23. package/plugin/database-export/types.d.ts.map +1 -0
  24. package/plugin/database-export/types.js +10 -0
  25. package/plugin/database-export/types.js.map +1 -0
  26. package/plugin/index.d.ts +4 -0
  27. package/plugin/index.d.ts.map +1 -0
  28. package/plugin/index.js +477 -0
  29. package/plugin/index.js.map +1 -0
  30. package/plugin/proxy.d.ts +13 -0
  31. package/plugin/proxy.d.ts.map +1 -0
  32. package/plugin/proxy.js +100 -0
  33. package/plugin/proxy.js.map +1 -0
  34. package/plugin/types.d.ts +121 -0
  35. package/plugin/types.d.ts.map +1 -0
  36. package/plugin/types.js +2 -0
  37. package/plugin/types.js.map +1 -0
  38. package/public/assets/index-B7P78qlq.js +2 -0
  39. package/public/assets/index-B7P78qlq.js.map +1 -0
  40. package/public/assets/main-DQRPJ7oa.js +11 -0
  41. package/public/assets/main-DQRPJ7oa.js.map +1 -0
  42. package/public/icon.svg +46 -0
  43. package/public/index.html +27 -0
@@ -0,0 +1,477 @@
1
+ import path from 'node:path';
2
+ import { BackupClient } from './backup-client.js';
3
+ import { registerProxy } from './proxy.js';
4
+ import { ConfigSchema, SCHEMA_DEFAULTS } from './config/schema.js';
5
+ import { runAllExports } from './database-export/index.js';
6
+ const BACKUP_IMAGE = 'ghcr.io/dirkwa/signalk-backup-server';
7
+ const CONTAINER_NAME = 'signalk-backup-server';
8
+ const PLUGIN_ID = 'signalk-backup';
9
+ const SK_MOUNT = '/signalk-data';
10
+ const API_PORT = 3010;
11
+ const OAUTH_PORT = 53682;
12
+ const SAFE_TAG = /^[a-zA-Z0-9._-]+$/;
13
+ /**
14
+ * Sensible default resource limits for the backup-server container.
15
+ * Backup work is mostly I/O bound (Kopia is content-addressable so it
16
+ * reads more than it CPUs); rclone uploads are network-bound. 1 CPU
17
+ * and 256MB is enough for typical SignalK installs (a few GB of config).
18
+ *
19
+ * Users can override any field via signalk-container's plugin config
20
+ * under "Per-container resource overrides", keyed by the unprefixed
21
+ * container name `signalk-backup-server`. See:
22
+ * signalk-container/doc/plugin-developer-guide.md §"Resource Limits"
23
+ */
24
+ const DEFAULT_RESOURCES = {
25
+ cpus: 1,
26
+ memory: '256m',
27
+ memorySwap: '256m',
28
+ pidsLimit: 100
29
+ };
30
+ function getContainerManager() {
31
+ return globalThis.__signalk_containerManager;
32
+ }
33
+ /**
34
+ * Wait for signalk-container's API to be FULLY ready on globalThis —
35
+ * both the manager object exposed AND its runtime detection complete.
36
+ *
37
+ * signalk-container publishes `__signalk_containerManager` synchronously
38
+ * during its own start() but kicks off `detectRuntime` async, so there's
39
+ * a ~1-2s window where getRuntime() returns null. signalk-backup loads
40
+ * BEFORE signalk-container alphabetically and races into that window
41
+ * unless we wait for both signals.
42
+ */
43
+ async function waitForContainerManager(maxMs, intervalMs = 500) {
44
+ const deadline = Date.now() + maxMs;
45
+ while (Date.now() < deadline) {
46
+ const m = getContainerManager();
47
+ if (m && m.getRuntime())
48
+ return m;
49
+ await new Promise((r) => setTimeout(r, intervalMs));
50
+ }
51
+ return getContainerManager();
52
+ }
53
+ function errMsg(err) {
54
+ return err instanceof Error ? err.message : String(err);
55
+ }
56
+ /**
57
+ * Resolve the actual host:port the backup-server container is reachable at.
58
+ *
59
+ * signalk-container exposes `resolveContainerAddress`, but its result comes
60
+ * from a process-local port-allocation cache that can drift from the live
61
+ * podman binding (TOCTOU between the in-process port probe and the actual
62
+ * `podman create`). When that drift happens, the returned address points at
63
+ * a port that nothing is listening on — every subsequent proxy request
64
+ * returns ECONNREFUSED.
65
+ *
66
+ * This helper queries `listContainers()` (which signalk-container reads
67
+ * directly from podman/docker) and parses the live `Ports` field for the
68
+ * binding mapped from the container's API_PORT. That's authoritative.
69
+ *
70
+ * Falls back to `resolveContainerAddress` if the live binding can't be
71
+ * parsed, so this is a strict superset of the documented contract.
72
+ */
73
+ async function resolveActualAddress(containers) {
74
+ try {
75
+ const list = await containers.listContainers();
76
+ const found = list.find((c) => c.name === `sk-${CONTAINER_NAME}`);
77
+ if (found && Array.isArray(found.ports)) {
78
+ // The shape ContainerInfo we mirror only declares { name, image, state },
79
+ // but the runtime object includes a `ports` array of strings like
80
+ // "127.0.0.1:3010->3010/tcp". Parse the binding for our API_PORT.
81
+ const ports = found.ports;
82
+ const wanted = `->${API_PORT}/tcp`;
83
+ for (const entry of ports) {
84
+ if (!entry.endsWith(wanted))
85
+ continue;
86
+ const hostPart = entry.slice(0, -wanted.length);
87
+ // hostPart looks like "127.0.0.1:3010" or "0.0.0.0:3010".
88
+ if (hostPart.includes(':'))
89
+ return hostPart;
90
+ }
91
+ }
92
+ }
93
+ catch {
94
+ // fall through to the documented API
95
+ }
96
+ return containers.resolveContainerAddress(CONTAINER_NAME, API_PORT);
97
+ }
98
+ export default function (app) {
99
+ let client = null;
100
+ let currentSettings = null;
101
+ let containerAddress = null;
102
+ let dbExportTimer = null;
103
+ let dbExportInFlight = false;
104
+ const buildContainerConfig = (tag, logLevel) => ({
105
+ image: BACKUP_IMAGE,
106
+ tag,
107
+ // signalkConfigRootMount mounts the entire SignalK config root
108
+ // (`~/.signalk/` typically) — settings.json, security.json, the
109
+ // whole plugin-config-data tree. signalkDataMount would only mount
110
+ // this plugin's own subdir, which isn't enough for backup/restore.
111
+ // Requires signalk-container >= 1.5.0.
112
+ signalkConfigRootMount: SK_MOUNT,
113
+ signalkAccessiblePorts: [API_PORT, OAUTH_PORT],
114
+ env: {
115
+ PORT: String(API_PORT),
116
+ // The backup engine's own state lives in a plugin-config-data subdir
117
+ // so it travels with the SignalK config (snapshotted by backup, included in restore).
118
+ DATA_DIR: `${SK_MOUNT}/plugin-config-data/${PLUGIN_ID}`,
119
+ SIGNALK_DATA_PATH: SK_MOUNT,
120
+ SIGNALK_VERSION: getSignalKVersion(app),
121
+ LOG_LEVEL: logLevel
122
+ },
123
+ resources: DEFAULT_RESOURCES,
124
+ restart: 'unless-stopped'
125
+ });
126
+ const plugin = {
127
+ id: PLUGIN_ID,
128
+ name: 'Backup',
129
+ description: 'Scheduled deduplicated backups of SignalK config to local storage and Google Drive',
130
+ schema: ConfigSchema,
131
+ start(config) {
132
+ app.debug('Starting signalk-backup');
133
+ // CRITICAL: Signal K does not seed schema defaults into the runtime
134
+ // config — when the plugin is auto-enabled (or enabled without
135
+ // saving the form), `config` is `{}`. Merge defaults so callers can
136
+ // rely on every field being present.
137
+ const merged = { ...SCHEMA_DEFAULTS, ...config };
138
+ currentSettings = merged;
139
+ void asyncStart(merged).catch((err) => {
140
+ app.setPluginError(`Startup failed: ${errMsg(err)}`);
141
+ });
142
+ },
143
+ async stop() {
144
+ app.debug('Stopping signalk-backup');
145
+ stopDbExportTimer();
146
+ client = null;
147
+ containerAddress = null;
148
+ const containers = getContainerManager();
149
+ if (containers && currentSettings?.managedContainer !== false) {
150
+ try {
151
+ containers.updates.unregister(PLUGIN_ID);
152
+ }
153
+ catch (err) {
154
+ app.debug(`Error unregistering update tracker: ${errMsg(err)}`);
155
+ }
156
+ try {
157
+ await containers.stop(CONTAINER_NAME);
158
+ }
159
+ catch (err) {
160
+ app.debug(`Error stopping ${CONTAINER_NAME}: ${errMsg(err)}`);
161
+ }
162
+ }
163
+ app.setPluginStatus('Stopped');
164
+ },
165
+ registerWithRouter(router) {
166
+ // Lightweight readiness signal for admin/webapp badges.
167
+ router.get('/status', async (_req, res) => {
168
+ const containers = getContainerManager();
169
+ let containerState = 'unknown';
170
+ let containerImage = '';
171
+ if (containers) {
172
+ try {
173
+ containerState = await containers.getState(CONTAINER_NAME);
174
+ }
175
+ catch (err) {
176
+ app.debug(`status: getState failed: ${errMsg(err)}`);
177
+ }
178
+ if (containers.getRuntime()) {
179
+ try {
180
+ const list = await containers.listContainers();
181
+ const found = list.find((c) => c.name === `sk-${CONTAINER_NAME}`);
182
+ if (found)
183
+ containerImage = found.image;
184
+ }
185
+ catch (err) {
186
+ app.debug(`status: listContainers failed: ${errMsg(err)}`);
187
+ }
188
+ }
189
+ }
190
+ if (!containerImage) {
191
+ containerImage = `${BACKUP_IMAGE}:${currentSettings?.imageTag ?? 'latest'}`;
192
+ }
193
+ res.json({
194
+ container: {
195
+ state: containerState,
196
+ image: containerImage,
197
+ managed: currentSettings?.managedContainer !== false
198
+ },
199
+ ready: client !== null
200
+ });
201
+ });
202
+ // Update detection — delegated to signalk-container's centralized
203
+ // update service. Same pattern as mayara.
204
+ router.get('/api/update/check', async (_req, res) => {
205
+ const containers = getContainerManager();
206
+ if (!containers) {
207
+ res.status(503).json({ error: 'signalk-container not available' });
208
+ return;
209
+ }
210
+ try {
211
+ const result = await containers.updates.checkOne(PLUGIN_ID);
212
+ res.json(result);
213
+ }
214
+ catch (err) {
215
+ res.status(500).json({ error: errMsg(err) });
216
+ }
217
+ });
218
+ router.post('/api/update/apply', async (req, res) => {
219
+ const containers = getContainerManager();
220
+ if (!containers) {
221
+ res.status(503).json({ error: 'signalk-container not available' });
222
+ return;
223
+ }
224
+ const body = (req.body ?? {});
225
+ const tag = (typeof body.tag === 'string' ? body.tag : undefined) ??
226
+ currentSettings?.imageTag ??
227
+ 'latest';
228
+ if (!SAFE_TAG.test(tag)) {
229
+ res.status(400).json({ error: 'Invalid tag format' });
230
+ return;
231
+ }
232
+ try {
233
+ app.setPluginStatus(`Pulling ${BACKUP_IMAGE}:${tag}...`);
234
+ await containers.pullImage(`${BACKUP_IMAGE}:${tag}`);
235
+ app.setPluginStatus('Recreating signalk-backup-server container...');
236
+ await containers.remove(CONTAINER_NAME);
237
+ try {
238
+ await containers.ensureRunning(CONTAINER_NAME, buildContainerConfig(tag, currentSettings?.logLevel ?? 'info'));
239
+ }
240
+ catch (recreateErr) {
241
+ const msg = `Container removed but recreation failed: ${errMsg(recreateErr)}. Click Update again to retry.`;
242
+ app.setPluginError(msg);
243
+ res.status(500).json({ error: msg });
244
+ return;
245
+ }
246
+ if (currentSettings) {
247
+ currentSettings.imageTag = tag;
248
+ await new Promise((resolve) => {
249
+ app.savePluginOptions({ ...currentSettings }, (err) => {
250
+ if (err) {
251
+ app.error(`Failed to persist new tag: ${errMsg(err)}. Container is running with ${tag} but a plugin restart will revert.`);
252
+ }
253
+ resolve();
254
+ });
255
+ });
256
+ }
257
+ app.setPluginStatus(`Updated to ${BACKUP_IMAGE}:${tag}`);
258
+ res.json({ success: true, tag });
259
+ }
260
+ catch (err) {
261
+ app.setPluginError(`Update failed: ${errMsg(err)}`);
262
+ res.status(500).json({ error: errMsg(err) });
263
+ }
264
+ });
265
+ // Proxy /api/* to the backup-server. Registered LAST so the
266
+ // explicit /api/update/{check,apply} above match first.
267
+ // containerAddress includes the scheme (http:// or https://) so
268
+ // external-mode HTTPS upstreams aren't downgraded.
269
+ registerProxy(router, {
270
+ getUpstreamBase: () => containerAddress,
271
+ log: (msg) => {
272
+ app.debug(msg);
273
+ }
274
+ });
275
+ }
276
+ };
277
+ async function asyncStart(settings) {
278
+ if (!settings.managedContainer) {
279
+ // External-server mode: skip the container, point at user-provided URL.
280
+ const url = settings.externalUrl.trim();
281
+ if (!url) {
282
+ app.setPluginError('managedContainer is disabled but externalUrl is empty. Set externalUrl in plugin config.');
283
+ return;
284
+ }
285
+ client = new BackupClient(url);
286
+ // Keep the scheme so the proxy can route HTTPS external upstreams.
287
+ containerAddress = url;
288
+ try {
289
+ await client.waitForReady(15_000);
290
+ app.setPluginStatus(`Connected to external backup-server at ${url}`);
291
+ await seedFirstRunSchedule(client);
292
+ if (settings.databaseExport.questdb) {
293
+ app.setPluginStatus(`Connected to external backup-server. Note: database export ` +
294
+ `requires managed-container mode and was skipped.`);
295
+ }
296
+ }
297
+ catch (err) {
298
+ app.setPluginError(`External backup-server unreachable: ${errMsg(err)}`);
299
+ }
300
+ return;
301
+ }
302
+ // signalk-container exposes its API via globalThis only after its own
303
+ // start() has finished. Plugin-load order is alphabetical, and
304
+ // "signalk-backup" comes before "signalk-container" — so on a cold
305
+ // server start, getContainerManager() is undefined for the first few
306
+ // hundred ms. Poll for up to 30s before giving up.
307
+ const containers = await waitForContainerManager(120_000);
308
+ if (!containers) {
309
+ app.setPluginError('signalk-container plugin not available after 120s. Install and enable it, then restart this plugin.');
310
+ return;
311
+ }
312
+ if (!containers.getRuntime()) {
313
+ app.setPluginError('No container runtime detected (Podman or Docker). Install one and restart signalk-container.');
314
+ return;
315
+ }
316
+ if (!SAFE_TAG.test(settings.imageTag)) {
317
+ app.setPluginError(`Invalid imageTag "${settings.imageTag}"`);
318
+ return;
319
+ }
320
+ try {
321
+ app.setPluginStatus(`Starting ${BACKUP_IMAGE}:${settings.imageTag}...`);
322
+ await containers.ensureRunning(CONTAINER_NAME, buildContainerConfig(settings.imageTag, settings.logLevel));
323
+ // Register with the central update service so users see "update available"
324
+ // badges in the signalk-container config panel without us writing custom
325
+ // GitHub-poll logic.
326
+ try {
327
+ containers.updates.register({
328
+ pluginId: PLUGIN_ID,
329
+ containerName: CONTAINER_NAME,
330
+ image: BACKUP_IMAGE,
331
+ currentTag: () => currentSettings?.imageTag ?? settings.imageTag,
332
+ versionSource: containers.updates.sources.githubReleases('dirkwa/signalk-backup-server')
333
+ });
334
+ }
335
+ catch (err) {
336
+ app.debug(`updates.register failed (non-fatal): ${errMsg(err)}`);
337
+ }
338
+ const addr = await resolveActualAddress(containers);
339
+ if (!addr) {
340
+ throw new Error('Could not resolve container address');
341
+ }
342
+ // Container is always plain HTTP on the host loopback; storing the
343
+ // full URL keeps the format consistent with external-mode (which
344
+ // may be HTTPS) so the proxy can use the same value as-is.
345
+ containerAddress = `http://${addr}`;
346
+ // `client` stays null until /api/health succeeds, so /status's
347
+ // `ready: client !== null` reports the truthful upstream-reachable
348
+ // signal rather than just "we know the address."
349
+ const pending = new BackupClient(containerAddress);
350
+ app.setPluginStatus('Waiting for backup-server to become ready...');
351
+ await pending.waitForReady(60_000);
352
+ client = pending;
353
+ await seedFirstRunSchedule(client);
354
+ startDbExportTimer();
355
+ }
356
+ catch (err) {
357
+ app.setPluginError(`Container startup failed: ${errMsg(err)}`);
358
+ }
359
+ }
360
+ /**
361
+ * Start the database-export interval, if any DB exporter is enabled in
362
+ * settings. The first export fires after one full interval — not at
363
+ * startup — to avoid hammering the disk during server boot.
364
+ */
365
+ function startDbExportTimer() {
366
+ stopDbExportTimer();
367
+ const dbCfg = currentSettings?.databaseExport;
368
+ if (!dbCfg?.questdb) {
369
+ app.debug('Database export: no exporters enabled, scheduler idle');
370
+ return;
371
+ }
372
+ const intervalMs = Math.max(5, dbCfg.intervalMinutes) * 60 * 1000;
373
+ app.debug(`Database export: scheduling every ${dbCfg.intervalMinutes}m`);
374
+ dbExportTimer = setInterval(() => {
375
+ void runDbExportTick();
376
+ }, intervalMs);
377
+ }
378
+ function stopDbExportTimer() {
379
+ if (dbExportTimer) {
380
+ clearInterval(dbExportTimer);
381
+ dbExportTimer = null;
382
+ }
383
+ }
384
+ async function runDbExportTick() {
385
+ // Coalesce: if a previous tick is still running (e.g. a slow large
386
+ // table on a Pi), skip this one entirely rather than queuing.
387
+ if (dbExportInFlight) {
388
+ app.debug('Database export: previous tick still running, skipping');
389
+ return;
390
+ }
391
+ dbExportInFlight = true;
392
+ try {
393
+ const results = await runAllExports({
394
+ signalkConfigRoot: resolveSignalkConfigRoot(),
395
+ signalkBaseUrl: resolveSignalkBaseUrl(),
396
+ log: (msg) => {
397
+ app.debug(msg);
398
+ }
399
+ });
400
+ const totalTables = results.reduce((acc, r) => acc + r.tables.length, 0);
401
+ const totalBytes = results.reduce((acc, r) => acc + r.totalBytes, 0);
402
+ app.debug(`Database export tick: ${results.length} sources, ` +
403
+ `${totalTables} tables, ${totalBytes} bytes`);
404
+ }
405
+ catch (err) {
406
+ app.error(`Database export tick failed: ${errMsg(err)}`);
407
+ }
408
+ finally {
409
+ dbExportInFlight = false;
410
+ }
411
+ }
412
+ /**
413
+ * Resolve the host-visible SignalK config root. Prefer the env var
414
+ * (authoritative when set) and fall back to walking up from
415
+ * getDataDirPath(), which signalk-server guarantees returns
416
+ * `<configRoot>/plugin-config-data/<pluginId>`.
417
+ */
418
+ function resolveSignalkConfigRoot() {
419
+ const fromEnv = process.env['SIGNALK_NODE_CONFIG_DIR'];
420
+ if (fromEnv && fromEnv.length > 0)
421
+ return fromEnv;
422
+ // dirname twice: <root>/plugin-config-data/signalk-backup → <root>
423
+ return path.dirname(path.dirname(app.getDataDirPath()));
424
+ }
425
+ /**
426
+ * Resolve the SignalK loopback URL — used to talk to source plugins
427
+ * (e.g. signalk-questdb) over HTTP. Same convention used by
428
+ * signalk-questdb itself: PORT env var with 3000 as default.
429
+ */
430
+ function resolveSignalkBaseUrl() {
431
+ const port = process.env['PORT'] ?? '3000';
432
+ return `http://127.0.0.1:${port}`;
433
+ }
434
+ async function seedFirstRunSchedule(c) {
435
+ try {
436
+ const existing = await c.getSettings();
437
+ const alreadyConfigured = existing.scheduler?.configured === true;
438
+ if (alreadyConfigured) {
439
+ app.setPluginStatus(`Backup engine ready at ${containerAddress}`);
440
+ return;
441
+ }
442
+ // First run: opt-in safe defaults — daily local-only backup, no cloud.
443
+ // The user must explicitly enable cloud sync from the GUI.
444
+ await c.putSettings({
445
+ scheduler: {
446
+ configured: true,
447
+ daily: { enabled: true, retain: 7 },
448
+ hourly: { enabled: false, retain: 24 },
449
+ weekly: { enabled: false, retain: 4 },
450
+ startup: { enabled: true, retain: 3 }
451
+ },
452
+ cloud: { mode: 'off' }
453
+ });
454
+ app.setPluginStatus(`First-run: seeded daily local backup schedule. Open Backup Console to configure.`);
455
+ }
456
+ catch (err) {
457
+ // Don't fail startup if seeding fails — the container is up and the
458
+ // user can configure manually from the UI.
459
+ app.debug(`First-run seed failed (non-fatal): ${errMsg(err)}`);
460
+ app.setPluginStatus(`Backup engine ready at ${containerAddress}`);
461
+ }
462
+ }
463
+ return plugin;
464
+ }
465
+ /**
466
+ * Best-effort SignalK version detection. Plumbed into the container so
467
+ * backups can be tagged with the running SignalK version. Falls back to
468
+ * "unknown" — never throws.
469
+ */
470
+ function getSignalKVersion(app) {
471
+ // ServerAPI doesn't expose a stable .version field across versions; many
472
+ // builds make it available, but treating it as optional is safer.
473
+ const candidate = app.signalk?.version ??
474
+ app.version;
475
+ return typeof candidate === 'string' && candidate.length > 0 ? candidate : 'unknown';
476
+ }
477
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { IRouter } from 'express';
2
+ export interface ProxyOptions {
3
+ /**
4
+ * Lazy accessor for the upstream base URL — must include the scheme
5
+ * (e.g. `http://127.0.0.1:3010` or `https://server:3010`). Returns
6
+ * null when the backup-server isn't ready; those requests get a 503.
7
+ */
8
+ getUpstreamBase: () => string | null;
9
+ /** Optional debug logger. */
10
+ log?: (msg: string) => void;
11
+ }
12
+ export declare function registerProxy(router: IRouter, opts: ProxyOptions): void;
13
+ //# sourceMappingURL=proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,OAAO,EAA0D,MAAM,SAAS,CAAA;AA2B9F,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,eAAe,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IACpC,6BAA6B;IAC7B,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAC5B;AAID,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CA+DvE"}
@@ -0,0 +1,100 @@
1
+ // Reverse proxy from /plugins/signalk-backup/api/* to the backup-server's
2
+ // loopback API. Container is loopback-only so a remote browser can't
3
+ // reach it directly; routing through the SignalK origin also avoids CORS.
4
+ // Both directions stream via Readable.fromWeb + stream/promises::pipeline
5
+ // so multi-GB ZIP downloads/uploads don't allocate the body in memory.
6
+ import { Readable } from 'node:stream';
7
+ import { pipeline } from 'node:stream/promises';
8
+ // Hop-by-hop headers per RFC 7230 §6.1 (must NOT cross a proxy hop), plus
9
+ // host (gets rewritten to upstream) and content-length (recomputed by
10
+ // undici from the streamed body).
11
+ const HOP_BY_HOP_REQUEST_HEADERS = new Set([
12
+ 'host',
13
+ 'connection',
14
+ 'keep-alive',
15
+ 'proxy-authorization',
16
+ 'te',
17
+ 'trailer',
18
+ 'transfer-encoding',
19
+ 'upgrade',
20
+ 'content-length'
21
+ ]);
22
+ // Same list, response side. content-encoding stays — upstream may have
23
+ // already gzipped and we pass that through unchanged.
24
+ const HOP_BY_HOP_RESPONSE_HEADERS = new Set([
25
+ 'connection',
26
+ 'keep-alive',
27
+ 'transfer-encoding',
28
+ 'upgrade',
29
+ 'proxy-authenticate'
30
+ ]);
31
+ // Catches /api/*; the explicit /api/update/{check,apply} routes must
32
+ // register BEFORE this — Express matches in registration order.
33
+ export function registerProxy(router, opts) {
34
+ router.all(/^\/api\/.*/, async (req, res) => {
35
+ const base = opts.getUpstreamBase();
36
+ if (!base) {
37
+ res.status(503).json({ error: 'backup-server not ready' });
38
+ return;
39
+ }
40
+ // Express's router-mount prefix has already been stripped, so
41
+ // req.url is e.g. `/api/backups?type=manual`.
42
+ const upstreamUrl = base.replace(/\/$/, '') + req.url;
43
+ const headers = new Headers();
44
+ for (const [name, value] of Object.entries(req.headers)) {
45
+ if (value === undefined)
46
+ continue;
47
+ if (HOP_BY_HOP_REQUEST_HEADERS.has(name.toLowerCase()))
48
+ continue;
49
+ if (Array.isArray(value)) {
50
+ for (const v of value)
51
+ headers.append(name, v);
52
+ }
53
+ else {
54
+ headers.set(name, value);
55
+ }
56
+ }
57
+ // duplex: 'half' is required by undici when body is a Node Readable;
58
+ // both fields are absent from the DOM RequestInit we'd otherwise import.
59
+ const init = {
60
+ method: req.method,
61
+ headers
62
+ };
63
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
64
+ init.body = req;
65
+ init.duplex = 'half';
66
+ }
67
+ let upstreamRes;
68
+ try {
69
+ upstreamRes = await fetch(upstreamUrl, init);
70
+ }
71
+ catch (err) {
72
+ opts.log?.(`proxy ${req.method} ${req.url} → upstream error: ${errMsg(err)}`);
73
+ res.status(502).json({ error: 'backup-server unreachable', detail: errMsg(err) });
74
+ return;
75
+ }
76
+ res.status(upstreamRes.status);
77
+ for (const [name, value] of upstreamRes.headers.entries()) {
78
+ if (HOP_BY_HOP_RESPONSE_HEADERS.has(name.toLowerCase()))
79
+ continue;
80
+ res.setHeader(name, value);
81
+ }
82
+ if (!upstreamRes.body) {
83
+ res.end();
84
+ return;
85
+ }
86
+ try {
87
+ await pipeline(Readable.fromWeb(upstreamRes.body), res);
88
+ }
89
+ catch (err) {
90
+ opts.log?.(`proxy ${req.method} ${req.url} → stream error: ${errMsg(err)}`);
91
+ if (!res.writableEnded) {
92
+ res.end();
93
+ }
94
+ }
95
+ });
96
+ }
97
+ function errMsg(err) {
98
+ return err instanceof Error ? err.message : String(err);
99
+ }
100
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,qEAAqE;AACrE,0EAA0E;AAC1E,0EAA0E;AAC1E,uEAAuE;AAEvE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAG/C,0EAA0E;AAC1E,sEAAsE;AACtE,kCAAkC;AAClC,MAAM,0BAA0B,GAAG,IAAI,GAAG,CAAC;IACzC,MAAM;IACN,YAAY;IACZ,YAAY;IACZ,qBAAqB;IACrB,IAAI;IACJ,SAAS;IACT,mBAAmB;IACnB,SAAS;IACT,gBAAgB;CACjB,CAAC,CAAA;AAEF,uEAAuE;AACvE,sDAAsD;AACtD,MAAM,2BAA2B,GAAG,IAAI,GAAG,CAAC;IAC1C,YAAY;IACZ,YAAY;IACZ,mBAAmB;IACnB,SAAS;IACT,oBAAoB;CACrB,CAAC,CAAA;AAaF,qEAAqE;AACrE,gEAAgE;AAChE,MAAM,UAAU,aAAa,CAAC,MAAe,EAAE,IAAkB;IAC/D,MAAM,CAAC,GAAG,CAAC,YAAY,EAAE,KAAK,EAAE,GAAmB,EAAE,GAAoB,EAAE,EAAE;QAC3E,MAAM,IAAI,GAAG,IAAI,CAAC,eAAe,EAAE,CAAA;QACnC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC,CAAA;YAC1D,OAAM;QACR,CAAC;QAED,8DAA8D;QAC9D,8CAA8C;QAC9C,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAA;QAErD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;QAC7B,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACxD,IAAI,KAAK,KAAK,SAAS;gBAAE,SAAQ;YACjC,IAAI,0BAA0B,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;gBAAE,SAAQ;YAChE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBACzB,KAAK,MAAM,CAAC,IAAI,KAAK;oBAAE,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;YAChD,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;YAC1B,CAAC;QACH,CAAC;QAED,qEAAqE;QACrE,yEAAyE;QACzE,MAAM,IAAI,GAA4B;YACpC,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,OAAO;SACR,CAAA;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAClD,IAAI,CAAC,IAAI,GAAG,GAAG,CAAA;YACf,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACtB,CAAC;QAED,IAAI,WAA8C,CAAA;QAClD,IAAI,CAAC;YACH,WAAW,GAAG,MAAM,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC,CAAA;QAC9C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,GAAG,sBAAsB,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YAC7E,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,2BAA2B,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YACjF,OAAM;QACR,CAAC;QAED,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAA;QAC9B,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,WAAW,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;YAC1D,IAAI,2BAA2B,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;gBAAE,SAAQ;YACjE,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;QAC5B,CAAC;QAED,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;YACtB,GAAG,CAAC,GAAG,EAAE,CAAA;YACT,OAAM;QACR,CAAC;QAED,IAAI,CAAC;YACH,MAAM,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,IAAa,CAAC,EAAE,GAAG,CAAC,CAAA;QAClE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,GAAG,oBAAoB,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YAC3E,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;gBACvB,GAAG,CAAC,GAAG,EAAE,CAAA;YACX,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,MAAM,CAAC,GAAY;IAC1B,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;AACzD,CAAC"}