@hivedev/hivesdk 1.0.34 → 1.0.36

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 (2) hide show
  1. package/README.md +987 -1
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1 +1,987 @@
1
- Test
1
+ # @hivedev/hivesdk
2
+
3
+ Software development kit for microservices that integrate with the **Hive** system — a hub-and-spoke platform for educational institutions.
4
+
5
+ Every service in the Hive ecosystem (NoteHive, and any future service) is built around this SDK. It handles authentication, configuration management, service registration, database connectivity, push notifications, and client-side session management — so each service only has to implement its own domain logic.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ - [Concepts](#concepts)
12
+ - [Installation](#installation)
13
+ - [Server SDK](#server-sdk)
14
+ - [1. Initialising the Server](#1-initialising-the-server)
15
+ - [2. Registering the Service](#2-registering-the-service)
16
+ - [3. Mounting the Middleware](#3-mounting-the-middleware)
17
+ - [4. Loading Configuration](#4-loading-configuration)
18
+ - [5. Connecting to the Database](#5-connecting-to-the-database)
19
+ - [6. Guarding Routes](#6-guarding-routes)
20
+ - [7. Sending Mail](#7-sending-mail)
21
+ - [8. Utility Functions](#8-utility-functions)
22
+ - [Full Server Bootstrap Example](#full-server-bootstrap-example)
23
+ - [Client SDK](#client-sdk)
24
+ - [1. Loading the Client Bundle](#1-loading-the-client-bundle)
25
+ - [2. Initialising the Client](#2-initialising-the-client)
26
+ - [3. Login](#3-login)
27
+ - [4. Checking Login State](#4-checking-login-state)
28
+ - [Full Client Bootstrap Example](#full-client-bootstrap-example)
29
+ - [Enumerations](#enumerations)
30
+ - [Environment Variables Reference](#environment-variables-reference)
31
+ - [How HivePortal Fits In](#how-hiveportal-fits-in)
32
+ - [Known Limitations](#known-limitations)
33
+
34
+ ---
35
+
36
+ ## Concepts
37
+
38
+ **HivePortal** is the central hub. It owns all user accounts, sessions, permissions, and the service registry. Every microservice is a spoke — it delegates all authentication and configuration to HivePortal via this SDK.
39
+
40
+ **Service registration** is a two-step process:
41
+ 1. The service announces itself to HivePortal with its name and local/remote URLs.
42
+ 2. A HivePortal administrator approves it. Once approved, HivePortal sends back an encrypted configuration payload containing database credentials, Firebase credentials, permissions, and SMTP credentials.
43
+
44
+ **Configuration** is stored encrypted on disk (AES-256-CBC, key derived via scrypt from `SERVER_PASSWORD`). It is decrypted at startup and cached in `process.env`. Credentials never sit on disk in plaintext.
45
+
46
+ **Authentication** is always proxied. A service never validates a session token itself — it forwards the check to HivePortal and acts on the response.
47
+
48
+ ---
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ npm install @hivedev/hivesdk
54
+ ```
55
+
56
+ The SDK ships three pre-built files:
57
+ - `hive-server.js` — ESM bundle for Node.js backends
58
+ - `hive-server.cjs` — CJS bundle for Node.js backends
59
+ - `hive-client.js` — ESM bundle for browser frontends
60
+
61
+ ---
62
+
63
+ ## Server SDK
64
+
65
+ ### 1. Initialising the Server
66
+
67
+ Call `initServer` once at the very start of your service, before anything else. It sets the service's identity and resolves the server password.
68
+
69
+ ```js
70
+ // ESM
71
+ import { initServer } from "@hivedev/hivesdk/server";
72
+
73
+ await initServer({
74
+ serviceName: "NoteHive", // Must match the name registered in HivePortal
75
+ servicePort: 49161, // The port this service listens on
76
+ remoteUrl: "", // Public-facing URL if accessible from outside LAN, empty string if LAN-only
77
+ serverPassword: "" // Optional — see password resolution below
78
+ });
79
+ ```
80
+
81
+ ```js
82
+ // CJS
83
+ const { initServer } = require("@hivedev/hivesdk/server");
84
+
85
+ await initServer({
86
+ serviceName: "NoteHive",
87
+ servicePort: 49161,
88
+ remoteUrl: "",
89
+ serverPassword: ""
90
+ });
91
+ ```
92
+
93
+ **Password resolution order:**
94
+
95
+ 1. The `serverPassword` argument, if provided.
96
+ 2. The `SERVER_PASSWORD` environment variable, if set.
97
+ 3. An interactive terminal prompt, if neither of the above is available — it will ask the user to create a password on first run, or enter their existing password on subsequent runs.
98
+
99
+ The password is used as the encryption key for all `.dat` configuration files. Losing it means losing access to the stored configuration.
100
+
101
+ > **Important:** `initServer` must only be called once. Calling it a second time is a no-op with a console warning. In a clustered setup, call it only from the primary process.
102
+
103
+ ---
104
+
105
+ ### 2. Registering the Service
106
+
107
+ After `initServer`, call `registerService` to announce this service to HivePortal and receive configuration.
108
+
109
+ ```js
110
+ // ESM
111
+ import { registerService } from "@hivedev/hivesdk/server";
112
+
113
+ await registerService();
114
+ ```
115
+
116
+ ```js
117
+ // CJS
118
+ const { registerService } = require("@hivedev/hivesdk/server");
119
+
120
+ await registerService();
121
+ ```
122
+
123
+ What this does internally:
124
+
125
+ 1. Sets the service's local URL (`http://<hostIp>:<servicePort>`) and remote URL in the internal registry.
126
+ 2. POSTs to HivePortal's `/RegisterService` endpoint.
127
+ 3. If HivePortal accepts the request, begins polling HivePortal's `/Configuration` endpoint every 2 seconds.
128
+ 4. Once an administrator approves the service in HivePortal, the configuration payload is received, decrypted, and saved to disk via `saveConfiguration`.
129
+
130
+ **On first run**, the terminal will show:
131
+
132
+ ```
133
+ Registering service...
134
+ Service registration hasn't been approved yet. Please contact administrators!
135
+ Service registration hasn't been approved yet. Please contact administrators!
136
+ ...
137
+ Configuration received.
138
+ Saving configuration...
139
+ Service registered.
140
+ ```
141
+
142
+ Once registered and approved, subsequent startups load configuration from disk directly via `loadConfiguration` and do not need to poll again, unless credentials change in HivePortal.
143
+
144
+ > In a clustered setup, call `registerService` only from the primary process, after a short delay to allow workers to start up first.
145
+
146
+ ```js
147
+ // ESM
148
+ import cluster from "cluster";
149
+ import os from "os";
150
+ import { initServer, registerService } from "@hivedev/hivesdk/server";
151
+
152
+ if (cluster.isPrimary)
153
+ {
154
+ await initServer({ serviceName: "NoteHive", servicePort: 49161, remoteUrl: "" });
155
+
156
+ setTimeout(async () =>
157
+ {
158
+ await registerService();
159
+ }, 3000);
160
+
161
+ for (let i = 0; i < os.cpus().length; i++)
162
+ {
163
+ cluster.fork({ ...process.env });
164
+ }
165
+ }
166
+ else
167
+ {
168
+ // Start your HTTP server in workers
169
+ }
170
+ ```
171
+
172
+ ```js
173
+ // CJS
174
+ const cluster = require("cluster");
175
+ const os = require("os");
176
+ const { initServer, registerService } = require("@hivedev/hivesdk/server");
177
+
178
+ if (cluster.isPrimary)
179
+ {
180
+ await initServer({ serviceName: "NoteHive", servicePort: 49161, remoteUrl: "" });
181
+
182
+ setTimeout(async () =>
183
+ {
184
+ await registerService();
185
+ }, 3000);
186
+
187
+ for (let i = 0; i < os.cpus().length; i++)
188
+ {
189
+ cluster.fork({ ...process.env });
190
+ }
191
+ }
192
+ else
193
+ {
194
+ // Start your HTTP server in workers
195
+ }
196
+ ```
197
+
198
+ ---
199
+
200
+ ### 3. Mounting the Middleware
201
+
202
+ `handleHiveRequests` is an Express-compatible middleware that intercepts all Hive-related routes. Mount it before your own route handlers.
203
+
204
+ ```js
205
+ // ESM
206
+ import express from "express";
207
+ import cookieParser from "cookie-parser";
208
+ import { handleHiveRequests } from "@hivedev/hivesdk/server";
209
+
210
+ const app = express();
211
+
212
+ app.use(express.json());
213
+ app.use(cookieParser());
214
+ app.use(handleHiveRequests); // Must come before your own routes
215
+
216
+ app.get("/my-route", (request, response) => { ... });
217
+ ```
218
+
219
+ ```js
220
+ // CJS
221
+ const express = require("express");
222
+ const cookieParser = require("cookie-parser");
223
+ const { handleHiveRequests } = require("@hivedev/hivesdk/server");
224
+
225
+ const app = express();
226
+
227
+ app.use(express.json());
228
+ app.use(cookieParser());
229
+ app.use(handleHiveRequests);
230
+
231
+ app.get("/my-route", (request, response) => { ... });
232
+ ```
233
+
234
+ **Routes handled automatically by this middleware:**
235
+
236
+ | Method | Route | What it does |
237
+ |--------|-------|--------------|
238
+ | `POST` | `/Login` | Proxies login credentials to HivePortal, sets session cookie and/or `x-session-token` header |
239
+ | `POST` | `/Logout` | Proxies logout to HivePortal, clears session |
240
+ | `GET` | `/ServiceUrls` | Returns local or remote URL for a named service |
241
+ | `POST` | `/IsLoggedIn` | Checks if the current session is valid with HivePortal |
242
+ | `POST` | `/IsLoggedInWithPermission` | Checks session validity and a specific named permission |
243
+ | `POST` | `/SaveUserFilter` | Saves a user-defined data filter (requires `FILTER_OPERATIONS` permission) |
244
+ | `POST` | `/PushNotificationToken` | Registers a push notification token for the current device |
245
+ | `GET` | `/hive-client.js` | Serves the built client-side SDK bundle (see [Client SDK](#client-sdk)) |
246
+
247
+ You do not need to implement any of these routes yourself.
248
+
249
+ ---
250
+
251
+ ### 4. Loading Configuration
252
+
253
+ On startup (after `initServer`), call `loadConfiguration` to decrypt and load credentials from disk into `process.env`.
254
+
255
+ ```js
256
+ // ESM
257
+ import { loadConfiguration } from "@hivedev/hivesdk/server";
258
+
259
+ await loadConfiguration();
260
+ ```
261
+
262
+ ```js
263
+ // CJS
264
+ const { loadConfiguration } = require("@hivedev/hivesdk/server");
265
+
266
+ await loadConfiguration();
267
+ ```
268
+
269
+ This populates the following environment variables (only those with saved `.dat` files on disk are loaded):
270
+
271
+ | Variable | Contents |
272
+ |----------|----------|
273
+ | `FIREBASE_ADMIN_CREDENTIALS` | JSON string of Firebase Admin SDK credentials |
274
+ | `DATABASE_CREDENTIALS` | JSON string of database credentials, keyed by service name |
275
+ | `PERMISSIONS` | JSON string of permission definitions |
276
+ | `SMTP_CREDENTIALS` | JSON string of SMTP credentials |
277
+ | `SMTP_PASSWORD` | Extracted SMTP password, ready for use in nodemailer |
278
+
279
+ It also calls `DatabaseConnector.setCredentials` internally using the credentials stored under your service's name, so the database is ready to connect after this call.
280
+
281
+ If a `.dat` file does not exist yet (e.g. first run before registration is approved), that credential is silently skipped.
282
+
283
+ > Call `loadConfiguration` before `registerService`. On first run the files won't exist yet and it will skip gracefully. Once `registerService` completes and the administrator approves it, files are written to disk and will be fully loaded on the next startup.
284
+
285
+ ---
286
+
287
+ ### 5. Connecting to the Database
288
+
289
+ After `loadConfiguration`, use `DatabaseConnector` to establish a MongoDB connection.
290
+
291
+ ```js
292
+ // ESM — import directly from the SDK's built file
293
+ import DatabaseConnector from "./node_modules/@hivedev/hivesdk/DatabaseConnector.js";
294
+
295
+ const connected = await DatabaseConnector.connect();
296
+
297
+ if (!connected)
298
+ {
299
+ console.error("Failed to connect to database.");
300
+ process.exit(1);
301
+ }
302
+
303
+ // DatabaseConnector.databaseObject is now a live MongoDB db instance
304
+ const collection = DatabaseConnector.databaseObject.collection("my_collection");
305
+ ```
306
+
307
+ ```js
308
+ // CJS
309
+ const DatabaseConnector = require("./node_modules/@hivedev/hivesdk/DatabaseConnector.js");
310
+
311
+ const connected = await DatabaseConnector.connect();
312
+
313
+ if (!connected)
314
+ {
315
+ console.error("Failed to connect to database.");
316
+ process.exit(1);
317
+ }
318
+
319
+ const collection = DatabaseConnector.databaseObject.collection("my_collection");
320
+ ```
321
+
322
+ If `loadConfiguration` was not called first, `DatabaseConnector.connect()` will attempt to call it automatically.
323
+
324
+ **Disconnecting:**
325
+
326
+ ```js
327
+ await DatabaseConnector.disconnect();
328
+ ```
329
+
330
+ ---
331
+
332
+ ### 6. Guarding Routes
333
+
334
+ Use `isLoggedIn` and `isLoggedInWithPermission` to protect your own route handlers. Both accept the request and response objects and a `bSendResponse` boolean.
335
+
336
+ When `bSendResponse` is `false`, the function returns a boolean and leaves the response untouched — use this to guard logic inside your own handlers.
337
+
338
+ When `bSendResponse` is `true`, the function writes the result directly to the response — used internally by `handleHiveRequests` for the `/IsLoggedIn` and `/IsLoggedInWithPermission` endpoints.
339
+
340
+ Both functions handle **session refresh transparently** — if HivePortal signals that the session token should be refreshed, a new cookie and/or `x-session-token` header is set automatically before your handler continues.
341
+
342
+ **Guarding a route with a login check:**
343
+
344
+ ```js
345
+ // ESM
346
+ import { isLoggedIn } from "@hivedev/hivesdk/server";
347
+
348
+ app.get("/my-protected-route", async (request, response) =>
349
+ {
350
+ const loggedIn = await isLoggedIn(request, response, false);
351
+
352
+ if (!loggedIn)
353
+ {
354
+ response.statusCode = 401;
355
+ response.end("Unauthorized");
356
+ return;
357
+ }
358
+
359
+ response.end("Here is your protected data.");
360
+ });
361
+ ```
362
+
363
+ ```js
364
+ // CJS
365
+ const { isLoggedIn } = require("@hivedev/hivesdk/server");
366
+
367
+ app.get("/my-protected-route", async (request, response) =>
368
+ {
369
+ const loggedIn = await isLoggedIn(request, response, false);
370
+
371
+ if (!loggedIn)
372
+ {
373
+ response.statusCode = 401;
374
+ response.end("Unauthorized");
375
+ return;
376
+ }
377
+
378
+ response.end("Here is your protected data.");
379
+ });
380
+ ```
381
+
382
+ **Guarding a route with a permission check:**
383
+
384
+ The permission name must be set on `request.body.permissionName` before calling `isLoggedInWithPermission`.
385
+
386
+ ```js
387
+ // ESM
388
+ import { isLoggedInWithPermission } from "@hivedev/hivesdk/server";
389
+
390
+ app.post("/admin-action", async (request, response) =>
391
+ {
392
+ request.body.permissionName = "ADMIN_OPERATIONS";
393
+
394
+ const permitted = await isLoggedInWithPermission(request, response, false);
395
+
396
+ if (!permitted)
397
+ {
398
+ response.statusCode = 403;
399
+ response.end("Forbidden");
400
+ return;
401
+ }
402
+
403
+ response.end("Admin action performed.");
404
+ });
405
+ ```
406
+
407
+ ```js
408
+ // CJS
409
+ const { isLoggedInWithPermission } = require("@hivedev/hivesdk/server");
410
+
411
+ app.post("/admin-action", async (request, response) =>
412
+ {
413
+ request.body.permissionName = "ADMIN_OPERATIONS";
414
+
415
+ const permitted = await isLoggedInWithPermission(request, response, false);
416
+
417
+ if (!permitted)
418
+ {
419
+ response.statusCode = 403;
420
+ response.end("Forbidden");
421
+ return;
422
+ }
423
+
424
+ response.end("Admin action performed.");
425
+ });
426
+ ```
427
+
428
+ ---
429
+
430
+ ### 7. Sending Mail
431
+
432
+ ```js
433
+ // ESM
434
+ import { sendMail } from "@hivedev/hivesdk/server";
435
+
436
+ await sendMail(
437
+ "sender@yourdomain.com",
438
+ "recipient@example.com",
439
+ "Subject line here",
440
+ "<p>HTML body here</p>"
441
+ );
442
+ ```
443
+
444
+ ```js
445
+ // CJS
446
+ const { sendMail } = require("@hivedev/hivesdk/server");
447
+
448
+ await sendMail(
449
+ "sender@yourdomain.com",
450
+ "recipient@example.com",
451
+ "Subject line here",
452
+ "<p>HTML body here</p>"
453
+ );
454
+ ```
455
+
456
+ Uses Gmail SMTP via nodemailer. The SMTP password is read from `process.env.SMTP_PASSWORD`, which is populated automatically by `loadConfiguration`. The sender Gmail address is currently hardcoded in `SendMail.js` — edit it there if needed.
457
+
458
+ ---
459
+
460
+ ### 8. Utility Functions
461
+
462
+ All utilities are exported from `@hivedev/hivesdk/server`.
463
+
464
+ **`getAppDataDirectory()`**
465
+
466
+ Returns the platform-appropriate data directory for your service, creating it if it doesn't exist. On Windows this is `%APPDATA%\<ServiceName>`, on macOS `~/Library/Application Support/<ServiceName>`, on Linux `~/.config/<ServiceName>`.
467
+
468
+ ```js
469
+ // ESM
470
+ import { getAppDataDirectory } from "@hivedev/hivesdk/server";
471
+ const dataDir = getAppDataDirectory();
472
+ // e.g. "/home/user/.config/NoteHive"
473
+ ```
474
+
475
+ ```js
476
+ // CJS
477
+ const { getAppDataDirectory } = require("@hivedev/hivesdk/server");
478
+ const dataDir = getAppDataDirectory();
479
+ ```
480
+
481
+ **`getHostIp()`**
482
+
483
+ Returns the first non-loopback IPv4 address of the host machine.
484
+
485
+ ```js
486
+ // ESM
487
+ import { getHostIp } from "@hivedev/hivesdk/server";
488
+ const ip = getHostIp(); // e.g. "192.168.1.42"
489
+ ```
490
+
491
+ ```js
492
+ // CJS
493
+ const { getHostIp } = require("@hivedev/hivesdk/server");
494
+ const ip = getHostIp();
495
+ ```
496
+
497
+ **`findService(serviceName)`**
498
+
499
+ Discovers another Hive service on the LAN via UDP to port 49153. Returns `{ name, urls: { local, remote } }` or `null` if not found within 3 seconds. Requires `HIVE_PORTAL_LAN_IP` environment variable to be set.
500
+
501
+ ```js
502
+ // ESM
503
+ import { findService } from "@hivedev/hivesdk/server";
504
+
505
+ const service = await findService("NoteHive");
506
+
507
+ if (service)
508
+ {
509
+ console.log(service.urls.local); // "http://192.168.1.42:49161"
510
+ }
511
+ ```
512
+
513
+ ```js
514
+ // CJS
515
+ const { findService } = require("@hivedev/hivesdk/server");
516
+
517
+ const service = await findService("NoteHive");
518
+
519
+ if (service)
520
+ {
521
+ console.log(service.urls.local);
522
+ }
523
+ ```
524
+
525
+ **`extractConfigurationForService()`**
526
+
527
+ Parses `process.env` and returns the configuration as structured objects, extracting the correct database credentials for the current service name.
528
+
529
+ ```js
530
+ // ESM
531
+ import { extractConfigurationForService } from "@hivedev/hivesdk/server";
532
+
533
+ const { firebaseAdminCredentials, databaseCredentials, permissions, smtpCredentials } = extractConfigurationForService();
534
+ ```
535
+
536
+ ```js
537
+ // CJS
538
+ const { extractConfigurationForService } = require("@hivedev/hivesdk/server");
539
+
540
+ const { firebaseAdminCredentials, databaseCredentials, permissions, smtpCredentials } = extractConfigurationForService();
541
+ ```
542
+
543
+ **`encryptAndStoreFile(data, fullPath, password)`**
544
+
545
+ Encrypts a string or Buffer with AES-256-CBC and writes it to disk. Creates the directory if it doesn't exist.
546
+
547
+ ```js
548
+ // ESM
549
+ import { encryptAndStoreFile } from "@hivedev/hivesdk/server";
550
+
551
+ await encryptAndStoreFile("my secret data", "/path/to/file.dat", process.env.SERVER_PASSWORD);
552
+ ```
553
+
554
+ ```js
555
+ // CJS
556
+ const { encryptAndStoreFile } = require("@hivedev/hivesdk/server");
557
+
558
+ await encryptAndStoreFile("my secret data", "/path/to/file.dat", process.env.SERVER_PASSWORD);
559
+ ```
560
+
561
+ **`decryptFileWithPassword(fullPath, password)`**
562
+
563
+ Reads and decrypts an encrypted `.dat` file. Returns the decrypted string, or `null` on failure.
564
+
565
+ ```js
566
+ // ESM
567
+ import { decryptFileWithPassword } from "@hivedev/hivesdk/server";
568
+
569
+ const contents = await decryptFileWithPassword("/path/to/file.dat", process.env.SERVER_PASSWORD);
570
+ ```
571
+
572
+ ```js
573
+ // CJS
574
+ const { decryptFileWithPassword } = require("@hivedev/hivesdk/server");
575
+
576
+ const contents = await decryptFileWithPassword("/path/to/file.dat", process.env.SERVER_PASSWORD);
577
+ ```
578
+
579
+ **`saveConfiguration(configurationObject)` / `loadConfiguration()`**
580
+
581
+ Normally called automatically by `registerService` and during startup respectively. Available for manual use if needed. `saveConfiguration` only writes keys that are present in the object — missing keys do not overwrite existing files.
582
+
583
+ ```js
584
+ // ESM
585
+ import { saveConfiguration, loadConfiguration } from "@hivedev/hivesdk/server";
586
+
587
+ await saveConfiguration({
588
+ databaseCredentials: { NoteHive: { host: "127.0.0.1", username: "admin", password: "pass", name: "notehive" } },
589
+ smtpCredentials: { password: "smtp-password" }
590
+ });
591
+
592
+ await loadConfiguration();
593
+ ```
594
+
595
+ ```js
596
+ // CJS
597
+ const { saveConfiguration, loadConfiguration } = require("@hivedev/hivesdk/server");
598
+
599
+ await saveConfiguration({
600
+ databaseCredentials: { NoteHive: { host: "127.0.0.1", username: "admin", password: "pass", name: "notehive" } },
601
+ smtpCredentials: { password: "smtp-password" }
602
+ });
603
+
604
+ await loadConfiguration();
605
+ ```
606
+
607
+ ---
608
+
609
+ ### Full Server Bootstrap Example
610
+
611
+ This is the recommended startup sequence for any Hive microservice.
612
+
613
+ ```js
614
+ // ESM
615
+ import express from "express";
616
+ import cookieParser from "cookie-parser";
617
+ import cluster from "cluster";
618
+ import os from "os";
619
+ import { handleHiveRequests, initServer, registerService, loadConfiguration } from "@hivedev/hivesdk/server";
620
+ import DatabaseConnector from "./node_modules/@hivedev/hivesdk/DatabaseConnector.js";
621
+
622
+ const SERVICE_NAME = "NoteHive";
623
+ const SERVICE_PORT = 49161;
624
+
625
+ const app = express();
626
+
627
+ app.use(express.json());
628
+ app.use(cookieParser());
629
+ app.use(handleHiveRequests);
630
+
631
+ app.get("/", (request, response) => response.redirect("/Client/Pages/HomePage.html"));
632
+
633
+ if (cluster.isPrimary)
634
+ {
635
+ await initServer({
636
+ serviceName: SERVICE_NAME,
637
+ servicePort: SERVICE_PORT,
638
+ remoteUrl: process.env.REMOTE_URL || ""
639
+ });
640
+
641
+ await loadConfiguration();
642
+
643
+ setTimeout(async () =>
644
+ {
645
+ await registerService();
646
+ }, 3000);
647
+
648
+ for (let i = 0; i < os.cpus().length; i++)
649
+ {
650
+ cluster.fork({ ...process.env });
651
+ }
652
+
653
+ cluster.on("exit", (worker) =>
654
+ {
655
+ console.log(`Worker ${worker.process.pid} died. Restarting...`);
656
+ cluster.fork();
657
+ });
658
+ }
659
+ else
660
+ {
661
+ await DatabaseConnector.connect();
662
+
663
+ app.listen(SERVICE_PORT, () =>
664
+ {
665
+ console.log(`Worker ${process.pid} listening on port ${SERVICE_PORT}`);
666
+ });
667
+ }
668
+ ```
669
+
670
+ ```js
671
+ // CJS
672
+ const express = require("express");
673
+ const cookieParser = require("cookie-parser");
674
+ const cluster = require("cluster");
675
+ const os = require("os");
676
+ const { handleHiveRequests, initServer, registerService, loadConfiguration } = require("@hivedev/hivesdk/server");
677
+ const DatabaseConnector = require("./node_modules/@hivedev/hivesdk/DatabaseConnector.js");
678
+
679
+ const SERVICE_NAME = "NoteHive";
680
+ const SERVICE_PORT = 49161;
681
+
682
+ const app = express();
683
+
684
+ app.use(express.json());
685
+ app.use(cookieParser());
686
+ app.use(handleHiveRequests);
687
+
688
+ app.get("/", (request, response) => response.redirect("/Client/Pages/HomePage.html"));
689
+
690
+ (async () =>
691
+ {
692
+ if (cluster.isPrimary)
693
+ {
694
+ await initServer({
695
+ serviceName: SERVICE_NAME,
696
+ servicePort: SERVICE_PORT,
697
+ remoteUrl: process.env.REMOTE_URL || ""
698
+ });
699
+
700
+ await loadConfiguration();
701
+
702
+ setTimeout(async () =>
703
+ {
704
+ await registerService();
705
+ }, 3000);
706
+
707
+ for (let i = 0; i < os.cpus().length; i++)
708
+ {
709
+ cluster.fork({ ...process.env });
710
+ }
711
+
712
+ cluster.on("exit", (worker) =>
713
+ {
714
+ console.log(`Worker ${worker.process.pid} died. Restarting...`);
715
+ cluster.fork();
716
+ });
717
+ }
718
+ else
719
+ {
720
+ await DatabaseConnector.connect();
721
+
722
+ app.listen(SERVICE_PORT, () =>
723
+ {
724
+ console.log(`Worker ${process.pid} listening on port ${SERVICE_PORT}`);
725
+ });
726
+ }
727
+ })();
728
+ ```
729
+
730
+ ---
731
+
732
+ ## Client SDK
733
+
734
+ The client SDK is a browser-only ESM bundle (`hive-client.js`). There is no CJS variant — it runs exclusively in the browser.
735
+
736
+ ### 1. Loading the Client Bundle
737
+
738
+ The `handleHiveRequests` middleware automatically serves the client bundle at `/hive-client.js` by reading it from `node_modules/@hivedev/hivesdk/hive-client.js`. **This works as long as your service's Node.js process is started from the root of the package** (i.e. the directory that contains `node_modules`), which is the recommended setup.
739
+
740
+ If your service is started from a different working directory, the automatic route will fail to locate the file. In that case, serve it yourself:
741
+
742
+ ```js
743
+ import path from "path";
744
+ import { createReadStream } from "fs";
745
+
746
+ app.get("/hive-client.js", (request, response) =>
747
+ {
748
+ const filePath = path.join("/absolute/path/to/node_modules/@hivedev/hivesdk/hive-client.js");
749
+ response.setHeader("Content-Type", "application/javascript");
750
+ createReadStream(filePath).pipe(response);
751
+ });
752
+ ```
753
+
754
+ Include it in your HTML pages as a module script:
755
+
756
+ ```html
757
+ <script type="module" src="/hive-client.js"></script>
758
+ ```
759
+
760
+ Or import from it in another module script on the page:
761
+
762
+ ```html
763
+ <script type="module">
764
+ import { login } from "/hive-client.js";
765
+ </script>
766
+ ```
767
+
768
+ ---
769
+
770
+ ### 2. Initialising the Client
771
+
772
+ Call `initClient` once on page load. It sets up in-app WebView detection, signals cookie support to the server, and starts the periodic login state checker.
773
+
774
+ ```js
775
+ import { initClient } from "/hive-client.js";
776
+
777
+ await initClient({});
778
+ ```
779
+
780
+ `initClient` accepts one optional parameter:
781
+
782
+ | Parameter | Type | Default | Description |
783
+ |-----------|------|---------|-------------|
784
+ | `loginTokenStorageMethod` | `sessionStorageMethod` enum | `HTTP_ONLY_COOKIE` | How session tokens are stored on the client |
785
+
786
+ ---
787
+
788
+ ### 3. Login
789
+
790
+ Call `login()` to open the HivePortal login page. The SDK handles the entire flow — the popup opens, the user enters their credentials, and on success the page reloads at `/`.
791
+
792
+ ```js
793
+ import { login } from "/hive-client.js";
794
+
795
+ login();
796
+ ```
797
+
798
+ You can wire it to any element:
799
+
800
+ ```js
801
+ import { login } from "/hive-client.js";
802
+
803
+ document.getElementById("login-button").addEventListener("click", login);
804
+ ```
805
+
806
+ When running inside the native mobile app (detected automatically), the login popup is rendered as a full-screen iframe overlay instead of a real popup window, since WebViews block `window.open`.
807
+
808
+ ---
809
+
810
+ ### 4. Checking Login State
811
+
812
+ `initClient` automatically starts a periodic login check (every 30 seconds, clamped between 10 and 60 seconds). Each check fires a `login-state-changed` custom event on `window`.
813
+
814
+ ```js
815
+ window.addEventListener("login-state-changed", (event) =>
816
+ {
817
+ const isLoggedIn = event.detail;
818
+
819
+ document.getElementById("login-prompt").style.display = isLoggedIn ? "none" : "block";
820
+ document.getElementById("dashboard").style.display = isLoggedIn ? "block" : "none";
821
+ });
822
+ ```
823
+
824
+ The current login state is also available synchronously at any time:
825
+
826
+ ```js
827
+ if (window.IS_LOGGED_IN)
828
+ {
829
+ // Safe to load user-specific content
830
+ }
831
+ ```
832
+
833
+ ---
834
+
835
+ ### Full Client Bootstrap Example
836
+
837
+ ```html
838
+ <!DOCTYPE html>
839
+ <html lang="en">
840
+ <head>
841
+ <meta charset="UTF-8">
842
+ <title>NoteHive</title>
843
+ </head>
844
+ <body>
845
+
846
+ <div id="login-prompt">
847
+ <button id="login-button">Sign In</button>
848
+ </div>
849
+
850
+ <div id="dashboard" style="display: none;">
851
+ <h1>Welcome to NoteHive</h1>
852
+ </div>
853
+
854
+ <script type="module">
855
+ import { initClient, login } from "/hive-client.js";
856
+
857
+ await initClient({});
858
+
859
+ window.addEventListener("login-state-changed", (event) =>
860
+ {
861
+ const isLoggedIn = event.detail;
862
+ document.getElementById("login-prompt").style.display = isLoggedIn ? "none" : "block";
863
+ document.getElementById("dashboard").style.display = isLoggedIn ? "block" : "none";
864
+ });
865
+
866
+ document.getElementById("login-button").addEventListener("click", login);
867
+ </script>
868
+
869
+ </body>
870
+ </html>
871
+ ```
872
+
873
+ ---
874
+
875
+ ## Enumerations
876
+
877
+ The SDK exports several enumerations used throughout the Hive ecosystem.
878
+
879
+ **`userPrivileges`** — Numeric privilege levels assigned to users.
880
+
881
+ | Key | Value |
882
+ |-----|-------|
883
+ | `STUDENT` | 0 |
884
+ | `CLASS_REPRESENTATIVE` | 10 |
885
+ | `STUDENT_COORDINATOR` | 20 |
886
+ | `ASSISTANT` | 30 |
887
+ | `TEACHER` | 40 |
888
+ | `COURSE_COORDINATOR` | 50 |
889
+ | `HEAD_OF_DEPARTMENT` | 70 |
890
+ | `MANAGEMENT` | 80 |
891
+ | `SUPER_USER` | 100 |
892
+
893
+ **`sessionStorageMethod`** — How session tokens are stored on the client.
894
+
895
+ | Key | Value |
896
+ |-----|-------|
897
+ | `HTTP_ONLY_COOKIE` | 0 |
898
+ | `LOCAL_STORAGE` | 1 |
899
+ | `SESSION_STORAGE` | 2 |
900
+
901
+ **`clientConnectionTypeFlags`** — Bitmask flags describing how a client is connected.
902
+
903
+ | Key | Value |
904
+ |-----|-------|
905
+ | `LAN` | 1 |
906
+ | `REMOTE` | 2 |
907
+ | `SAME_SYSTEM` | 4 |
908
+ | `UNKNOWN` | 8 |
909
+
910
+ **`appWebViewInteractions`** — Message types exchanged between the web page and the native app WebView layer.
911
+
912
+ | Key | Value |
913
+ |-----|-------|
914
+ | `REQUEST_NOTIFICATION_TOKEN` | 0 |
915
+ | `BACK_PRESSED` | 1 |
916
+
917
+ **`approvalTypes`** — Types of approval requests sent to HivePortal administrators.
918
+
919
+ | Key | Value |
920
+ |-----|-------|
921
+ | `USER_REGISTERATION` | 0 |
922
+ | `SERVICE_REGISTERATION` | 1 |
923
+
924
+ **`filterTypes`**, **`filterOperators`**, **`userDataFetchFilterComparision`**, **`userFieldTypes`**, **`userFieldFormats`**, **`fileTreeNodeTypes`** — Domain-specific enumerations used for data querying, user field definitions, and file tree structures. Refer to their respective source files for values.
925
+
926
+ ---
927
+
928
+ ## Environment Variables Reference
929
+
930
+ | Variable | Set by | Description |
931
+ |----------|--------|-------------|
932
+ | `SERVER_PASSWORD` | External / `setupPassword()` | Master password for encrypting/decrypting config files |
933
+ | `SERVICE_NAME` | `initServer` | Name of this service as registered with HivePortal |
934
+ | `SERVICE_PORT` | `initServer` | Port this service listens on |
935
+ | `REMOTE_URL` | `initServer` | Public-facing URL of this service (empty if LAN-only) |
936
+ | `HIVE_PORTAL_LAN_IP` | External | LAN IP of HivePortal, used by `findService` for UDP discovery |
937
+ | `FIREBASE_ADMIN_CREDENTIALS` | `loadConfiguration` | JSON string of Firebase Admin credentials |
938
+ | `DATABASE_CREDENTIALS` | `loadConfiguration` | JSON string of DB credentials keyed by service name |
939
+ | `PERMISSIONS` | `loadConfiguration` | JSON string of permission definitions |
940
+ | `SMTP_CREDENTIALS` | `loadConfiguration` | JSON string of SMTP credentials |
941
+ | `SMTP_PASSWORD` | `loadConfiguration` | Extracted SMTP password, ready for nodemailer |
942
+
943
+ ---
944
+
945
+ ## How HivePortal Fits In
946
+
947
+ ```
948
+ ┌─────────────────────┐
949
+ │ HivePortal │
950
+ │ (hub — port 49152) │
951
+ │ │
952
+ │ - User accounts │
953
+ │ - Sessions │
954
+ │ - Permissions │
955
+ │ - Service registry │
956
+ │ - Configuration │
957
+ └────────┬─────────────┘
958
+
959
+ ┌────────────────────┼────────────────────┐
960
+ │ │ │
961
+ ┌──────────▼──────────┐ │ ┌──────────▼──────────┐
962
+ │ NoteHive │ ... │ FutureService │
963
+ │ (port 49161) │ │ (port 49162) │
964
+ │ │ │ │
965
+ │ uses @hivedev/ │ │ uses @hivedev/ │
966
+ │ hivesdk │ │ hivesdk │
967
+ └──────────────────────┘ └─────────────────────┘
968
+
969
+ Each service:
970
+ 1. initServer() — establish identity and password
971
+ 2. loadConfiguration() — decrypt credentials from disk
972
+ 3. registerService() — announce to HivePortal, receive config
973
+ 4. handleHiveRequests — mount middleware
974
+ 5. Own domain logic — notes, files, etc.
975
+ ```
976
+
977
+ Every authentication check a service performs is a proxied call to HivePortal. HivePortal is the single source of truth for all session and permission data. Services only store and serve their own domain data.
978
+
979
+ ---
980
+
981
+ ## Known Limitations
982
+
983
+ - **`ScheduleServiceUrlUpdate`** — This function is currently a stub and does nothing. It is intended for future use to periodically re-broadcast service URLs to HivePortal if the host IP changes.
984
+ - **`loginTokenStorageMethod`** — The `LOCAL_STORAGE` and `SESSION_STORAGE` options in the `sessionStorageMethod` enum are defined but not yet implemented. Only `HTTP_ONLY_COOKIE` (with the `x-session-token` header fallback for native app clients) is fully functional.
985
+ - **SMTP sender address** — The sender Gmail address in `SendMail.js` is hardcoded. Edit it in the SDK source if your service needs a different address.
986
+ - **MongoDB only** — `DatabaseConnector` is built for MongoDB. There is no support for other databases.
987
+ - **MongoDB port hardcoded** — The MongoDB port is hardcoded to `27017`. The host is configurable via credentials but the port is not.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hivedev/hivesdk",
3
3
  "type": "module",
4
- "version": "1.0.34",
4
+ "version": "1.0.36",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
7
7
  "scripts": {