@similie/hyphen-rtsp-tunnel 1.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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +497 -0
  3. package/dist/index.d.ts +4 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +83 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/services/day.d.ts +3 -0
  8. package/dist/services/day.d.ts.map +1 -0
  9. package/dist/services/day.js +26 -0
  10. package/dist/services/day.js.map +1 -0
  11. package/dist/services/device-auth.d.ts +26 -0
  12. package/dist/services/device-auth.d.ts.map +1 -0
  13. package/dist/services/device-auth.js +95 -0
  14. package/dist/services/device-auth.js.map +1 -0
  15. package/dist/services/events.d.ts +29 -0
  16. package/dist/services/events.d.ts.map +1 -0
  17. package/dist/services/events.js +14 -0
  18. package/dist/services/events.js.map +1 -0
  19. package/dist/services/index.d.ts +9 -0
  20. package/dist/services/index.d.ts.map +1 -0
  21. package/dist/services/index.js +9 -0
  22. package/dist/services/index.js.map +1 -0
  23. package/dist/services/notifier.d.ts +20 -0
  24. package/dist/services/notifier.d.ts.map +1 -0
  25. package/dist/services/notifier.js +12 -0
  26. package/dist/services/notifier.js.map +1 -0
  27. package/dist/services/rtsp-tunnel-gateway.d.ts +58 -0
  28. package/dist/services/rtsp-tunnel-gateway.d.ts.map +1 -0
  29. package/dist/services/rtsp-tunnel-gateway.js +532 -0
  30. package/dist/services/rtsp-tunnel-gateway.js.map +1 -0
  31. package/dist/services/sqs-notifier.d.ts +23 -0
  32. package/dist/services/sqs-notifier.d.ts.map +1 -0
  33. package/dist/services/sqs-notifier.js +109 -0
  34. package/dist/services/sqs-notifier.js.map +1 -0
  35. package/dist/services/storage-s3.d.ts +12 -0
  36. package/dist/services/storage-s3.d.ts.map +1 -0
  37. package/dist/services/storage-s3.js +49 -0
  38. package/dist/services/storage-s3.js.map +1 -0
  39. package/dist/services/storage-worker.d.ts +22 -0
  40. package/dist/services/storage-worker.d.ts.map +1 -0
  41. package/dist/services/storage-worker.js +100 -0
  42. package/dist/services/storage-worker.js.map +1 -0
  43. package/dist/services/storage.d.ts +28 -0
  44. package/dist/services/storage.d.ts.map +1 -0
  45. package/dist/services/storage.js +41 -0
  46. package/dist/services/storage.js.map +1 -0
  47. package/package.json +48 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Similie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,497 @@
1
+ # @similie/hyphen-rtsp-tunnel
2
+
3
+ **Secure, low-cost RTSP snapshot tunneling for distributed environmental monitoring**
4
+
5
+ ---
6
+
7
+ ## Why this exists
8
+
9
+ At **Similie**, we’re always looking for ways to make **low-cost technology have research-grade impact**.
10
+
11
+ Many communities—especially in emerging and climate-vulnerable regions—already have access to **consumer-grade IP cameras**. These cameras are affordable, reliable, and widely available, but they’re rarely designed to operate in:
12
+
13
+ - Intermittent connectivity environments
14
+ - Outside local area networks (LAN) infrastructures
15
+ - Horizontally scaled server architectures
16
+ - Scientific or humanitarian data pipelines
17
+
18
+ The outputs are often pushed to proprietary/paid services for home monitoring and security use cases. Similie seeks to bypass these limitations, by tapping into the RTSP outputs found on many consumer-grade cameras.
19
+
20
+ This project bridges the gap between cost, access, deployability, and open operability.
21
+
22
+ **`@similie/hyphen-rtsp-tunnel`** allows low-power edge devices (for example, ESP32-based gateways) to securely tunnel RTSP camera streams over WebSockets to a centralized server, capture snapshots, and feed them into modern processing pipelines—**without exposing cameras directly to the internet**.
23
+
24
+ The result:
25
+
26
+ > **Consumer hardware, safely integrated into professional-grade monitoring systems**
27
+
28
+ ---
29
+
30
+ ## What this module does
31
+
32
+ This package implements a **Hyphen Command Center plugin** that:
33
+
34
+ - Accepts **secure WebSocket connections** from edge devices
35
+ - Authenticates devices using a challenge-response handshake (pluggable)
36
+ - Creates a **temporary RTSP tunnel** over WebSockets
37
+ - Captures a snapshot using `ffmpeg`
38
+ - Emits structured events for downstream processing (storage, queues, AI, etc.)
39
+
40
+ Design constraints:
41
+
42
+ - The gateway **does not assume storage or cloud providers**
43
+ - The gateway **does not block on uploads**
44
+ - The gateway is **leader-aware** for horizontal scaling
45
+
46
+ ---
47
+
48
+ ## High-level architecture
49
+
50
+ ```
51
+ ┌─────────────┐
52
+ │ IP Camera │
53
+ │ (RTSP) │
54
+ └─────┬───────┘
55
+ │ 192.168.4.x (private AP)
56
+ ┌─────▼───────┐
57
+ │ HyphenOS. |
58
+ | Enabled │
59
+ │ Device │
60
+ │ (ESP32) │
61
+ │ │
62
+ │ RTSP ↔ WSS │
63
+ └─────┬───────┘
64
+ │ Secure WebSocket
65
+
66
+ ┌───────────────────────┐
67
+ │ Command Center API │
68
+ │ │
69
+ │ RTSP Tunnel Gateway │
70
+ │ - ffmpeg snapshot │
71
+ │ - auth handshake │
72
+ │ - leader-aware │
73
+ └─────────┬─────────────┘
74
+ │ events
75
+
76
+ ┌────────────────────────────┐
77
+ │ Storage / Queue / AI │
78
+ │ (pluggable, async) │
79
+ └────────────────────────────┘
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Key design principles
85
+
86
+ ### 🔐 Secure by default
87
+
88
+ - Cameras are **never exposed** to the public internet
89
+ - RTSP traffic only flows inside a **temporary authenticated tunnel**
90
+ - Device authentication is **pluggable** (RSA, certificates, future identity systems)
91
+
92
+ ### 🌍 Designed for constrained environments
93
+
94
+ - Works with **low-power devices**
95
+ - Handles **intermittent connectivity**
96
+ - Snapshot windows automatically clean up on failure
97
+
98
+ ### ⚖️ Horizontally scalable
99
+
100
+ - Leader-aware via Redis-based leader election
101
+ - Only one gateway handles RTSP capture at a time
102
+ - Multiple servers can process downstream jobs
103
+
104
+ ### 🧩 Modular & extensible
105
+
106
+ - Storage is event-driven, not hard-coded
107
+ - Queue notification is optional and pluggable
108
+ - Designed to integrate with broader data pipelines
109
+
110
+ ---
111
+
112
+ ## Installation
113
+
114
+ ```bash
115
+ npm install @similie/hyphen-rtsp-tunnel
116
+ ```
117
+
118
+ This package is designed to be used inside a Hyphen Command Center API instance.
119
+ It is not a standalone server binary.
120
+
121
+ ---
122
+
123
+ ## Requirements
124
+
125
+ This module spans **device firmware** (HyphenOS) and **server infrastructure** (Hyphen Command Center + ffmpeg). Make sure the full stack meets the minimum versions below.
126
+
127
+ ### 1) Device Firmware (HyphenOS)
128
+
129
+ You need **HyphenOS v1.0.19+** with the IPCamera + WiFi AP feature enabled.
130
+
131
+ At minimum, your firmware build must include these `build_flags` (example shown using PlatformIO-style flags):
132
+
133
+ ```ini
134
+ ; --- Enable WiFi AP mode (camera LAN) ---
135
+ -D HYPHEN_WIFI_AP_ENABLE=1
136
+ -D HYPHEN_WIFI_AP_SSID=\"HyphenCam-001\"
137
+ -D HYPHEN_WIFI_AP_PASS=\"hyphen1234\"
138
+ -D HYPHEN_WIFI_AP_CHANNEL=6
139
+ -D HYPHEN_WIFI_AP_MAX_CLIENTS=4
140
+ -D HYPHEN_HIDE_WIFI_AP=0
141
+ ; --- Camera endpoint on AP LAN ---
142
+ -D HYPHEN_CAM_HOST=\"192.168.4.216\"
143
+ -D HYPHEN_CAM_PORT=554
144
+
145
+ ; --- WSS tunnel endpoint (Hyphen Command Center API) ---
146
+ -D HYPHEN_WSS_HOST=\"192.168.18.215\"
147
+ -D HYPHEN_WSS_PORT=7443
148
+ -D HYPHEN_WSS_PATH=\"/\"
149
+
150
+ ; --- Snapshot cadence ---
151
+ -D HYPHEN_IPCAM_OFFSET_DEFAULT=1 ; (publish() cadence multiplier)
152
+ -D HYPHEN_IPCAM_TUNNEL_TIMEOUT_MS=45000
153
+
154
+ ; --- AP static network (optional but recommended for determinism) ---
155
+ -D HYPHEN_WIFI_AP_IP_0=192
156
+ -D HYPHEN_WIFI_AP_IP_1=168
157
+ -D HYPHEN_WIFI_AP_IP_2=4
158
+ -D HYPHEN_WIFI_AP_IP_3=1
159
+
160
+ -D HYPHEN_WIFI_AP_MASK_0=255
161
+ -D HYPHEN_WIFI_AP_MASK_1=255
162
+ -D HYPHEN_WIFI_AP_MASK_2=255
163
+ -D HYPHEN_WIFI_AP_MASK_3=0
164
+ ```
165
+
166
+ Notes:
167
+
168
+ - The camera is expected to be reachable on the Hyphen device’s AP LAN (e.g. 192.168.4.x).
169
+ - The WSS host/port/path are compiled into the device firmware, so the server does not “discover” the camera endpoint.
170
+
171
+ ### 2) Hyphen Command Center Versions
172
+
173
+ This module is designed to run inside the Hyphen Command Center stack.
174
+
175
+ Minimum versions:
176
+
177
+ - Hyphen Command Center API v1.1.1+
178
+ - Hyphen Command Center (UI) v1.1.2+
179
+
180
+ You can override env variable defaults if you include the following in the IP Camera "Sensor" metadata in Command Center:
181
+
182
+ - CAM_PASS=admin
183
+ - CAM_USER=mycamerapassword
184
+ - RTSP_PATH=/stream2 # the RTSP stream to pull
185
+
186
+ ### 3) Server Runtime: ffmpeg
187
+
188
+ The server process that hosts this plugin must have ffmpeg installed and available on the PATH (or wrapped in your runtime container image).
189
+ Quick install:
190
+
191
+ #### Ubuntu/Debian
192
+
193
+ ```bash
194
+ sudo apt-get update
195
+ sudo apt-get install -y ffmpeg
196
+ ```
197
+
198
+ #### Fedora
199
+
200
+ ```bash
201
+ sudo dnf install -y ffmpeg
202
+ ```
203
+
204
+ #### macOS (Home)
205
+
206
+ ```bash
207
+ brew install ffmpeg
208
+ ```
209
+
210
+ #### Docker
211
+
212
+ If you run Command Center in containers, ensure your image includes ffmpeg (for Debian-based images):
213
+
214
+ ```dockerfile
215
+ RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
216
+ ```
217
+
218
+ ### 4) Optional: AWS SQS (Downstream notifications)
219
+
220
+ If you want the gateway to emit snapshot events to SQS, you must configure AWS credentials + region and provide:
221
+ • AWS_REGION
222
+ • SQS_QUEUE_URL
223
+
224
+ If these are not set, the SQS notifier remains disabled and the module still works normally.
225
+
226
+ ---
227
+
228
+ ## Quick Debugging Checklist
229
+
230
+ Before debugging code, verify these basics — **90% of issues show up here**.
231
+
232
+ ### Device (HyphenOS)
233
+
234
+ - [ ] Device is running **HyphenOS v1.0.19+**
235
+ - [ ] WiFi AP is visible (e.g. `HyphenCam-001`)
236
+ - [ ] Camera is connected to the device AP
237
+ - [ ] Camera responds from the device:
238
+ - RTSP reachable at `rtsp://<CAM_HOST>:<CAM_PORT><RTSP_PATH>`
239
+ - [ ] Device can resolve and reach the Command Center WSS host
240
+ - [ ] `HELLO` and `AUTH_OK` appear in Command Center logs
241
+
242
+ ### Network
243
+
244
+ - [ ] **WSS port is reachable** from the device network
245
+ - [ ] No firewall blocking:
246
+ - WebSocket port (`WS_PORT`)
247
+ - Local proxy port (`PROXY_PORT`) on the server
248
+ - [ ] TLS certs are valid if `WS_TLS=1`
249
+
250
+ ### Command Center API
251
+
252
+ - [ ] Running **Hyphen Command Center API v1.1.1+**
253
+ - [ ] Module `@similie/hyphen-rtsp-tunnel` is loaded at startup
254
+ - [ ] Redis is reachable (leader election + caching)
255
+ - [ ] Device identity exists and resolves correctly
256
+
257
+ ### ffmpeg
258
+
259
+ - [ ] `ffmpeg` is installed and on `PATH`
260
+ - [ ] Running `ffmpeg -version` works in the same environment
261
+ - [ ] `ffmpeg` can open the RTSP proxy URL:
262
+ ```bash
263
+ ffmpeg -rtsp_transport tcp -i rtsp://127.0.0.1:8554/stream2 -frames:v 1 test.jpg
264
+ ```
265
+ - No other capture is already in progress (single-capture lock)
266
+
267
+ ### Storage / Output
268
+
269
+ - OUT_DIR exists and is writable
270
+ - Snapshot files appear on successful capture
271
+ - Downstream listeners (SQS, workers, etc.) are optional and non-blocking
272
+
273
+ ## Usage: Command Center Plugin
274
+
275
+ ```bash
276
+ # .env
277
+ HYPHEN_MODULES=@similie/hyphen-rtsp-tunnel-module # comma-separated for additional modules
278
+ ```
279
+
280
+ ## Environment Variables
281
+
282
+ The gateway is fully configured via environment variables.
283
+
284
+ ## Environment Variables
285
+
286
+ All configuration for `@similie/hyphen-rtsp-tunnel` is provided via environment variables.
287
+
288
+ ### Gateway & Networking
289
+
290
+ | Variable | Default | Description |
291
+ | ------------ | ------- | ------------------------------------------------- |
292
+ | `WS_PORT` | `7443` | Port for the WebSocket gateway |
293
+ | `WS_TLS` | `0` | Enable TLS for WebSocket server (`1` = HTTPS/WSS) |
294
+ | `TLS_CERT` | — | Path to TLS certificate (required if `WS_TLS=1`) |
295
+ | `TLS_KEY` | — | Path to TLS private key (required if `WS_TLS=1`) |
296
+ | `PROXY_PORT` | `8554` | Local TCP proxy port used by FFmpeg |
297
+
298
+ ---
299
+
300
+ ### RTSP / Camera
301
+
302
+ | Variable | Default | Description |
303
+ | ----------- | ---------- | -------------------- |
304
+ | `CAM_USER` | `admin` | RTSP camera username |
305
+ | `CAM_PASS` | — | RTSP camera password |
306
+ | `RTSP_PATH` | `/stream2` | RTSP stream path |
307
+
308
+ > **Note:**
309
+ > The camera **host and port are NOT configured server-side**.
310
+ > The ESP32 device determines the camera endpoint via its own build flags.
311
+
312
+ ---
313
+
314
+ ### Capture Behavior
315
+
316
+ | Variable | Default | Description |
317
+ | -------------------- | ------- | -------------------------------------------- |
318
+ | `AUTO_CAPTURE` | `1` | Automatically capture on device connection |
319
+ | `CAPTURE_TIMEOUT_MS` | `45000` | Maximum capture duration before abort |
320
+ | `HELLO_WAIT_MS` | `2000` | Time to wait for HELLO before closing socket |
321
+
322
+ ---
323
+
324
+ ### Authentication
325
+
326
+ | Variable | Default | Description |
327
+ | -------------- | ------- | ------------------------------------- |
328
+ | `REQUIRE_AUTH` | `1` | Require AUTH handshake before capture |
329
+
330
+ Authentication is currently based on Command Center/Device Certificate Signatures.
331
+
332
+ ---
333
+
334
+ ### Storage
335
+
336
+ | Variable | Default | Description |
337
+ | --------- | ----------------- | ------------------------------------ |
338
+ | `OUT_DIR` | OS temp directory | Local directory for snapshot storage |
339
+
340
+ ---
341
+
342
+ ### AWS / SQS (Optional)
343
+
344
+ These are only required if using the SQS notifier.
345
+
346
+ | Variable | Description |
347
+ | --------------- | ------------------------------ |
348
+ | `AWS_REGION` | AWS region |
349
+ | `SQS_QUEUE_URL` | SQS queue URL (FIFO supported) |
350
+
351
+ If these variables are not present, the notifier is automatically disabled.
352
+
353
+ ---
354
+
355
+ ## Device Handshake Flow
356
+
357
+ ```
358
+ Server → READY
359
+ Device → HELLO [payloadId] deviceId
360
+ Server → CHAL
361
+ Device → AUTH deviceId
362
+ Server → AUTH_OK
363
+ Server → OPEN
364
+ ```
365
+
366
+ ---
367
+
368
+ ## Events Emitted
369
+
370
+ The RTSP tunnel gateway exposes an internal event emitter so that **capture, storage, and downstream processing can be fully decoupled**.
371
+
372
+ ### `snapshot:captured`
373
+
374
+ Emitted when a snapshot is successfully captured and written to local temporary storage.
375
+
376
+ ```ts
377
+ {
378
+ sessionId: string;
379
+ deviceId: string;
380
+ payloadId: string | null;
381
+ localPath: string;
382
+ capturedAt: string;
383
+ }
384
+ ```
385
+
386
+ Typical consumers of this event include:
387
+
388
+ - Storage adapters (S3, NFS, local disk)
389
+ - Queue notifiers (SQS, BullMQ, Redis)
390
+ - Video stitching pipelines
391
+ - AI / image-processing workers (e.g. 4Shadow)
392
+
393
+ ### `snapshot:failed`
394
+
395
+ Emitted whenever a snapshot attempt fails at any stage.
396
+
397
+ ```ts
398
+ {
399
+ stage: "auth" | "capture" | "timeout" | "ffmpeg";
400
+ error: string;
401
+ }
402
+ ```
403
+
404
+ This allows downstream systems to log, alert, retry, or back off without blocking the gateway.
405
+
406
+ ---
407
+
408
+ ## Storage Responsibility (Intentional Design)
409
+
410
+ We have stubbed out this plugin to work well for our workflows, but out of the box you may need to implement logic to
411
+ support your own workflows
412
+
413
+ This module does not:
414
+
415
+ - Upload images to S3 (without configuration)
416
+ - Push messages to SQS or Redis (without configuration)
417
+ - Persist database records
418
+ - Perform AI or video processing
419
+
420
+ The gateway’s responsibility ends at secure capture and local persistence.
421
+
422
+ This keeps the system:
423
+
424
+ - Non-blocking
425
+ - Fault tolerant
426
+ - Horizontally scalable
427
+ - Easy to extend with new pipelines
428
+
429
+ All heavy or slow work is expected to run in downstream workers.
430
+
431
+ ---
432
+
433
+ ## Scaling & High Availability
434
+
435
+ The RTSP tunnel is designed to operate in clustered environments:
436
+
437
+ - Uses leader election so only one instance captures
438
+ - Other nodes remain idle and ready for failover
439
+ - Safe to run behind load balancers
440
+ - Compatible with Redis-backed horizontal scaling
441
+
442
+ This avoids duplicate captures while preserving resilience.
443
+
444
+ ---
445
+
446
+ ## Security Model
447
+
448
+ Security is enforced through layered constraints:
449
+
450
+ - TLS-secured WebSocket transport
451
+ - Device-initiated outbound connections only
452
+ - Optional cryptographic authentication (nonce + signature)
453
+ - No inbound RTSP exposure
454
+ - Camera network remains isolated behind the device
455
+
456
+ This design minimizes attack surface and removes the need to expose cameras directly to the internet.
457
+
458
+ ---
459
+
460
+ ### Who This Is For
461
+
462
+ - Environmental sensing networks
463
+ - Flood and climate early-warning systems
464
+ - Research deployments using consumer-grade cameras
465
+ - Edge-to-cloud ingestion pipelines
466
+ - Teams who need secure camera access without RTSP exposure
467
+
468
+ ---
469
+
470
+ ## Philosophy
471
+
472
+ At Similie, we believe that low-cost, open technology can deliver research-grade impact.
473
+
474
+ This module is part of a broader effort to make climate monitoring, early warning, and environmental intelligence:
475
+
476
+ - Accessible
477
+ - Affordable
478
+ - Open
479
+ - Locally deployable
480
+ - Globally scalable
481
+
482
+ ---
483
+
484
+ ### License
485
+
486
+ MIT © Similie
487
+
488
+ ### Hyphen Ecosystem
489
+
490
+ | Project | Description | Repository |
491
+ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
492
+ | **HyphenOS** | The device runtime environment for ultra-reliable IoT deployments. Includes OTA, telemetry queues, watchdogs, and sensor drivers. | https://github.com/similie/hyphen-os |
493
+ | **HyphenConnect** | Network + MQTT abstraction layer for ESP32 / Cellular devices. Enables function calls, variable access, and secure OTA. | https://github.com/similie/hyphen-connect |
494
+ | **Hyphen Command Center** | SvelteKit UI for managing your global device fleet, OTA updates, telemetry, and configuration. | https://github.com/similie/hyphen-command-center |
495
+ | **Hyphen Elemental** | The hardware schematics Similie uses to for our Hyphen Elemental 4 line of Products. | https://github.com/similie/hyphen-elemental |
496
+ | **Hyphen Video Encode** | A video stream processor for the RTSP Camera Workflow. Turn snaps into daily images. | https://github.com/similie/hyphen-videoencoder |
497
+ | **Ellipsies** | Workflow + routing engine powering the API: device identity, build pipeline, users, orgs, storage, and message routing. | https://github.com/similie/ellipsies |
@@ -0,0 +1,4 @@
1
+ import type { HyphenModule } from "@similie/hyphen-command-server-types";
2
+ declare const moduleImpl: HyphenModule;
3
+ export default moduleImpl;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AAoBzE,QAAA,MAAM,UAAU,EAAE,YAqFjB,CAAC;AAEF,eAAe,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,83 @@
1
+ import { RtspTunnelGateway, S3StorageAdapter, LocalStorageAdapter, StorageWorker, SqsNotifier, NoopNotifier, } from "./services/index.js";
2
+ function buildNotifierFromEnv() {
3
+ // Not implementing SQS yet — just leaving the slot.
4
+ // Example rule later:
5
+ if (process.env.SQS_QUEUE_URL && process.env.AWS_REGION)
6
+ return new SqsNotifier();
7
+ return new NoopNotifier();
8
+ }
9
+ const moduleImpl = {
10
+ name: "rtsp-tunnel",
11
+ version: "1.0.0",
12
+ async init(ctx) {
13
+ ctx.log("[rtsp-tunnel] init");
14
+ ctx.ellipsies.pgManager.datasource;
15
+ // --- choose storage adapter by env ---
16
+ const mode = process.env.STORAGE_MODE ?? "local";
17
+ const storage = mode === "s3"
18
+ ? new S3StorageAdapter(process.env.S3_BUCKET ?? "", process.env.S3_PREFIX ?? "hyphen/rtsp")
19
+ : new LocalStorageAdapter();
20
+ const gateway = new RtspTunnelGateway(ctx);
21
+ const storageWorker = new StorageWorker(ctx, gateway.events, storage);
22
+ const notifier = buildNotifierFromEnv();
23
+ await notifier.init(ctx);
24
+ // event -> notifier (does nothing if NoopNotifier)
25
+ gateway.events.on("snapshot:stored", async (e) => {
26
+ try {
27
+ ctx.log("[rtsp-tunnel] snapshot stored event", { event: e });
28
+ await notifier.send("snapshot:stored", e);
29
+ }
30
+ catch (err) {
31
+ ctx.log("[rtsp-tunnel] notifier error (stored)", err);
32
+ }
33
+ });
34
+ gateway.events.on("snapshot:failed", async (e) => {
35
+ try {
36
+ ctx.log("[rtsp-tunnel] snapshot failed event", { event: e });
37
+ await notifier.send("snapshot:failed", e);
38
+ }
39
+ catch (err) {
40
+ ctx.log("[rtsp-tunnel] notifier error (failed)", err);
41
+ }
42
+ });
43
+ const startAll = async () => {
44
+ // leader gating
45
+ if (ctx.leader && !ctx.leader.amLeader()) {
46
+ ctx.log("[rtsp-tunnel] not leader; gateway not started");
47
+ return;
48
+ }
49
+ // IMPORTANT: start worker first so we never miss captured events
50
+ storageWorker.start();
51
+ await gateway.start();
52
+ };
53
+ const stopAll = async () => {
54
+ await gateway.stop();
55
+ await storageWorker.stop();
56
+ };
57
+ // // Leader hooks (if present)
58
+ if (ctx.leader) {
59
+ ctx.leader.on("elected", async () => {
60
+ ctx.log("[rtsp-tunnel] leader elected; starting gateway+worker");
61
+ await startAll();
62
+ });
63
+ ctx.leader.on("revoked", async () => {
64
+ ctx.log("[rtsp-tunnel] leader revoked; stopping gateway+worker");
65
+ await stopAll();
66
+ });
67
+ ctx.leader.on("error", (err) => {
68
+ ctx.log("[rtsp-tunnel] leader error", err?.message ?? err);
69
+ // do not stop automatically; your choice
70
+ });
71
+ }
72
+ // Start now (leader or single-node)
73
+ await startAll();
74
+ return {
75
+ shutdown: async () => {
76
+ ctx.log("[rtsp-tunnel] shutdown");
77
+ await stopAll();
78
+ },
79
+ };
80
+ },
81
+ };
82
+ export default moduleImpl;
83
+ //# 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,3 @@
1
+ export declare function sanitizeTzOffsetHours(v: any): number | null;
2
+ export declare function dayFromCapturedAt(capturedAtIso: string, tzOffsetHours?: number | null): string;
3
+ //# sourceMappingURL=day.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"day.d.ts","sourceRoot":"","sources":["../../src/services/day.ts"],"names":[],"mappings":"AAAA,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,GAAG,IAAI,CAM3D;AAED,wBAAgB,iBAAiB,CAC/B,aAAa,EAAE,MAAM,EACrB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,GAC5B,MAAM,CAmBR"}
@@ -0,0 +1,26 @@
1
+ export function sanitizeTzOffsetHours(v) {
2
+ const n = typeof v === "number" ? v : Number(v);
3
+ if (!Number.isFinite(n))
4
+ return null;
5
+ // sanity bounds (common tz offsets)
6
+ if (n < -12 || n > 14)
7
+ return null;
8
+ return n;
9
+ }
10
+ export function dayFromCapturedAt(capturedAtIso, tzOffsetHours) {
11
+ const d = new Date(capturedAtIso);
12
+ if (Number.isNaN(d.getTime()))
13
+ throw new Error(`Invalid capturedAt ${capturedAtIso}`);
14
+ const hasOffset = process.env.USE_DEVICE_TZ_OFFSET === "1";
15
+ const off = hasOffset &&
16
+ typeof tzOffsetHours === "number" &&
17
+ Number.isFinite(tzOffsetHours)
18
+ ? tzOffsetHours
19
+ : 0;
20
+ const shifted = new Date(d.getTime() + Math.round(off * 3600 * 1000));
21
+ const yyyy = shifted.getUTCFullYear();
22
+ const mm = String(shifted.getUTCMonth() + 1).padStart(2, "0");
23
+ const dd = String(shifted.getUTCDate()).padStart(2, "0");
24
+ return `${yyyy}-${mm}-${dd}`;
25
+ }
26
+ //# sourceMappingURL=day.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"day.js","sourceRoot":"","sources":["../../src/services/day.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,qBAAqB,CAAC,CAAM;IAC1C,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAChD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,oCAAoC;IACpC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,aAAqB,EACrB,aAA6B;IAE7B,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,aAAa,CAAC,CAAC;IAClC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,sBAAsB,aAAa,EAAE,CAAC,CAAC;IAEzD,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,KAAK,GAAG,CAAC;IAE3D,MAAM,GAAG,GACP,SAAS;QACT,OAAO,aAAa,KAAK,QAAQ;QACjC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC;QAC5B,CAAC,CAAC,aAAa;QACf,CAAC,CAAC,CAAC,CAAC;IACR,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC;IAEtE,MAAM,IAAI,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IACtC,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC9D,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACzD,OAAO,GAAG,IAAI,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { ModuleContext } from "@similie/hyphen-command-server-types";
2
+ export type AuthPayload = {
3
+ deviceId: string;
4
+ sigB64: string;
5
+ };
6
+ export declare class DeviceAuth {
7
+ private readonly ctx;
8
+ constructor(ctx: ModuleContext);
9
+ deviceSensors(identity: string): Promise<{
10
+ results: any;
11
+ values: Record<string, string>;
12
+ }>;
13
+ device(identity: string): Promise<import("@similie/ellipsies").ObjectLiteral | null>;
14
+ private ds;
15
+ /**
16
+ * Parse "AUTH <deviceId> <sigB64>"
17
+ * Returns null if invalid.
18
+ */
19
+ parseAuthLine(line: string): AuthPayload | null;
20
+ /**
21
+ * Lookup device certificate by identity and verify signature
22
+ * over `${deviceId}.${nonceB64}` using RSA-SHA256.
23
+ */
24
+ verify(deviceId: string, nonceB64: string, sigB64: string): Promise<boolean>;
25
+ }
26
+ //# sourceMappingURL=device-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-auth.d.ts","sourceRoot":"","sources":["../../src/services/device-auth.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAE1E,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,aAAa;IAElC,aAAa,CAAC,QAAQ,EAAE,MAAM;;;;IAU9B,MAAM,CAAC,QAAQ,EAAE,MAAM;IAMpC,OAAO,CAAC,EAAE;IAQV;;;OAGG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAY/C;;;OAGG;IACG,MAAM,CACV,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC;CAqDpB"}