@rivium/sync-node 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Official Node.js SDK for RiviumSync Realtime Database. Designed for server-side
4
4
 
5
5
  ## Features
6
6
 
7
- - **Admin-level access** - Bypasses security rules by default
7
+ - **Server-side access** - Authenticates with your server secret, not an app key
8
8
  - **Full CRUD operations** - Create, read, update, delete documents
9
9
  - **Query support** - Filters, sorting, pagination
10
10
  - **Batch operations** - Atomic writes across multiple documents
@@ -31,8 +31,8 @@ import { RiviumSyncAdmin } from '@rivium/sync-node';
31
31
 
32
32
  // Initialize with your Project API Key and Server Secret
33
33
  const riviumSync = new RiviumSyncAdmin({
34
- apiKey: process.env.RIVIUM_SYNC_API_KEY, // nl_live_xxx or nl_test_xxx
35
- serverSecret: process.env.RIVIUM_SYNC_SERVER_SECRET, // nl_srv_xxx - Required for server-side operations
34
+ apiKey: process.env.RIVIUM_SYNC_API_KEY, // rv_live_xxx
35
+ serverSecret: process.env.RIVIUM_SYNC_SERVER_SECRET, // rv_srv_xxx - Required for server-side operations
36
36
  });
37
37
 
38
38
  // Get a database reference (database must be created via dashboard first)
@@ -42,7 +42,7 @@ const db = riviumSync.database('your-database-id');
42
42
  const users = db.collection('users');
43
43
  ```
44
44
 
45
- > **Note:** Both `apiKey` and `serverSecret` are required for all server-side SDK operations. You can find these credentials in your [AuthLeap Dashboard](https://console.authleap.com) when you create a project. Database creation and deletion is managed via the dashboard, not via SDK.
45
+ > **Note:** Both `apiKey` and `serverSecret` are required for all server-side SDK operations. You can find these credentials in [Rivium Console](https://console.rivium.co) when you create a project. Database creation and deletion is managed via the dashboard, not via SDK.
46
46
 
47
47
  ## CRUD Operations
48
48
 
@@ -201,13 +201,34 @@ unsubscribeAll();
201
201
  unsubscribeQuery();
202
202
  ```
203
203
 
204
+ ## User Tokens
205
+
206
+ Your app's Security Rules check `auth.uid`. A browser or phone cannot be
207
+ trusted to say who the user is - the API key it ships with is public - so your
208
+ backend mints a short-lived token for the user it has already signed in, and
209
+ the client SDK sends it:
210
+
211
+ ```typescript
212
+ // In your backend, behind your own session check:
213
+ app.post('/rivium-sync-token', async (req, res) => {
214
+ const { token, expiresIn } = await riviumSync.createUserToken(req.session.userId);
215
+ res.json({ token, expiresIn });
216
+ });
217
+ ```
218
+
219
+ The client passes that to its `tokenProvider` option. Tokens last an hour by
220
+ default; pass a second argument in seconds to change it, up to 24 hours.
221
+
222
+ This SDK holds the server secret, so it is already trusted and never needs a
223
+ token of its own.
224
+
204
225
  ## Configuration Options
205
226
 
206
227
  ```typescript
207
228
  const riviumSync = new RiviumSyncAdmin({
208
229
  // Required
209
- apiKey: 'nl_live_xxxxxxxxxxxxxxxxxxxxx', // Required - from AuthLeap Dashboard
210
- serverSecret: 'nl_srv_xxxxxxxxxxxxxxxxxxxxx', // Required - from AuthLeap Dashboard
230
+ apiKey: 'rv_live_xxxxxxxxxxxxxxxxxxxxx', // Required - from Rivium Console
231
+ serverSecret: 'rv_srv_xxxxxxxxxxxxxxxxxxxxx', // Required - from Rivium Console
211
232
 
212
233
  // Optional
213
234
  enableRealtime: false, // Enable MQTT subscriptions
@@ -220,10 +241,10 @@ const riviumSync = new RiviumSyncAdmin({
220
241
 
221
242
  | Credential | Format | Description |
222
243
  |------------|--------|-------------|
223
- | **API Key** | `nl_live_xxx` or `nl_test_xxx` | Used for client-side SDKs and server-side SDKs |
224
- | **Server Secret** | `nl_srv_xxx` | **Required** for server-side operations. Never expose in client-side code. |
244
+ | **API Key** | `rv_live_xxx` | Used for client-side SDKs and server-side SDKs |
245
+ | **Server Secret** | `rv_srv_xxx` | **Required** for server-side operations. Never expose in client-side code. |
225
246
 
226
- Both credentials are generated when you create a project in the [AuthLeap Dashboard](https://console.authleap.com). Store them securely and never commit them to version control.
247
+ Both credentials are generated when you create a project in [Rivium Console](https://console.rivium.co). Store them securely and never commit them to version control.
227
248
 
228
249
  ### Log Levels
229
250
 
package/dist/index.d.mts CHANGED
@@ -55,7 +55,7 @@ declare enum RiviumSyncLogLevel {
55
55
  interface RiviumSyncAdminConfig {
56
56
  /** Your Project API Key (rv_live_xxx or rv_test_xxx) - REQUIRED */
57
57
  apiKey: string;
58
- /** Server secret for server-side authentication (nl_srv_xxx) - REQUIRED for all server operations */
58
+ /** Server secret for server-side authentication (rv_srv_xxx) - REQUIRED for all server operations */
59
59
  serverSecret: string;
60
60
  /** Optional user identifier for Security Rules (used as auth.uid when acting on behalf of a user) */
61
61
  userId?: string;
@@ -303,6 +303,32 @@ declare class RiviumSyncAdmin {
303
303
  constructor(config: RiviumSyncAdminConfig);
304
304
  private log;
305
305
  setLogLevel(level: RiviumSyncLogLevel): void;
306
+ /**
307
+ * Mint a user token so your app can prove who the acting user is.
308
+ *
309
+ * Security Rules read `auth.uid`. A client cannot set that itself - the API
310
+ * key it ships with is public, so the server would have no reason to believe
311
+ * it. Your backend, which holds the server secret, calls this and hands the
312
+ * token to the app; the client SDKs take it through their `tokenProvider`
313
+ * option and send it on every request.
314
+ *
315
+ * Never ship the server secret (or this call) inside an app.
316
+ *
317
+ * @param userId Your own id for the signed-in user - whatever your rules expect.
318
+ * @param expiresIn Lifetime in seconds. Default 1 hour, maximum 24 hours.
319
+ *
320
+ * @example
321
+ * // In your Express backend, behind your own session check:
322
+ * app.post('/rivium-sync-token', async (req, res) => {
323
+ * const { token, expiresIn } = await sync.createUserToken(req.session.userId);
324
+ * res.json({ token, expiresIn });
325
+ * });
326
+ */
327
+ createUserToken(userId: string, expiresIn?: number): Promise<{
328
+ token: string;
329
+ userId: string;
330
+ expiresIn: number;
331
+ }>;
306
332
  private request;
307
333
  /**
308
334
  * Get a database reference
@@ -319,6 +345,13 @@ declare class RiviumSyncAdmin {
319
345
  updateDocument<T>(databaseId: string, collectionId: string, documentId: string, data: Partial<T>): Promise<void>;
320
346
  deleteDocument(databaseId: string, collectionId: string, documentId: string): Promise<void>;
321
347
  executeBatch(operations: BatchOperation[]): Promise<void>;
348
+ /** Project the API key belongs to, from POST /connections/token. */
349
+ private projectId;
350
+ /**
351
+ * Topic for a collection or a single document.
352
+ * `rivium_sync/{projectId}/{databaseName}/{collectionName}[/{documentId}]`
353
+ */
354
+ private topicFor;
322
355
  private initRealtime;
323
356
  private connectMqtt;
324
357
  listenDocument<T>(databaseId: string, collectionId: string, documentId: string, callback: DocumentListener<T>): Unsubscribe;
package/dist/index.d.ts CHANGED
@@ -55,7 +55,7 @@ declare enum RiviumSyncLogLevel {
55
55
  interface RiviumSyncAdminConfig {
56
56
  /** Your Project API Key (rv_live_xxx or rv_test_xxx) - REQUIRED */
57
57
  apiKey: string;
58
- /** Server secret for server-side authentication (nl_srv_xxx) - REQUIRED for all server operations */
58
+ /** Server secret for server-side authentication (rv_srv_xxx) - REQUIRED for all server operations */
59
59
  serverSecret: string;
60
60
  /** Optional user identifier for Security Rules (used as auth.uid when acting on behalf of a user) */
61
61
  userId?: string;
@@ -303,6 +303,32 @@ declare class RiviumSyncAdmin {
303
303
  constructor(config: RiviumSyncAdminConfig);
304
304
  private log;
305
305
  setLogLevel(level: RiviumSyncLogLevel): void;
306
+ /**
307
+ * Mint a user token so your app can prove who the acting user is.
308
+ *
309
+ * Security Rules read `auth.uid`. A client cannot set that itself - the API
310
+ * key it ships with is public, so the server would have no reason to believe
311
+ * it. Your backend, which holds the server secret, calls this and hands the
312
+ * token to the app; the client SDKs take it through their `tokenProvider`
313
+ * option and send it on every request.
314
+ *
315
+ * Never ship the server secret (or this call) inside an app.
316
+ *
317
+ * @param userId Your own id for the signed-in user - whatever your rules expect.
318
+ * @param expiresIn Lifetime in seconds. Default 1 hour, maximum 24 hours.
319
+ *
320
+ * @example
321
+ * // In your Express backend, behind your own session check:
322
+ * app.post('/rivium-sync-token', async (req, res) => {
323
+ * const { token, expiresIn } = await sync.createUserToken(req.session.userId);
324
+ * res.json({ token, expiresIn });
325
+ * });
326
+ */
327
+ createUserToken(userId: string, expiresIn?: number): Promise<{
328
+ token: string;
329
+ userId: string;
330
+ expiresIn: number;
331
+ }>;
306
332
  private request;
307
333
  /**
308
334
  * Get a database reference
@@ -319,6 +345,13 @@ declare class RiviumSyncAdmin {
319
345
  updateDocument<T>(databaseId: string, collectionId: string, documentId: string, data: Partial<T>): Promise<void>;
320
346
  deleteDocument(databaseId: string, collectionId: string, documentId: string): Promise<void>;
321
347
  executeBatch(operations: BatchOperation[]): Promise<void>;
348
+ /** Project the API key belongs to, from POST /connections/token. */
349
+ private projectId;
350
+ /**
351
+ * Topic for a collection or a single document.
352
+ * `rivium_sync/{projectId}/{databaseName}/{collectionName}[/{documentId}]`
353
+ */
354
+ private topicFor;
322
355
  private initRealtime;
323
356
  private connectMqtt;
324
357
  listenDocument<T>(databaseId: string, collectionId: string, documentId: string, callback: DocumentListener<T>): Unsubscribe;
package/dist/index.js CHANGED
@@ -379,6 +379,11 @@ var _RiviumSyncAdmin = class _RiviumSyncAdmin {
379
379
  this.documentListeners = /* @__PURE__ */ new Map();
380
380
  this.collectionListeners = /* @__PURE__ */ new Map();
381
381
  this.cachedCollections = /* @__PURE__ */ new Map();
382
+ // ==========================================================================
383
+ // Realtime (Optional)
384
+ // ==========================================================================
385
+ /** Project the API key belongs to, from POST /connections/token. */
386
+ this.projectId = null;
382
387
  if (!config.apiKey) {
383
388
  throw new RiviumSyncError(1301 /* MISSING_API_KEY */);
384
389
  }
@@ -425,6 +430,40 @@ var _RiviumSyncAdmin = class _RiviumSyncAdmin {
425
430
  // ==========================================================================
426
431
  // HTTP Client
427
432
  // ==========================================================================
433
+ /**
434
+ * Mint a user token so your app can prove who the acting user is.
435
+ *
436
+ * Security Rules read `auth.uid`. A client cannot set that itself - the API
437
+ * key it ships with is public, so the server would have no reason to believe
438
+ * it. Your backend, which holds the server secret, calls this and hands the
439
+ * token to the app; the client SDKs take it through their `tokenProvider`
440
+ * option and send it on every request.
441
+ *
442
+ * Never ship the server secret (or this call) inside an app.
443
+ *
444
+ * @param userId Your own id for the signed-in user - whatever your rules expect.
445
+ * @param expiresIn Lifetime in seconds. Default 1 hour, maximum 24 hours.
446
+ *
447
+ * @example
448
+ * // In your Express backend, behind your own session check:
449
+ * app.post('/rivium-sync-token', async (req, res) => {
450
+ * const { token, expiresIn } = await sync.createUserToken(req.session.userId);
451
+ * res.json({ token, expiresIn });
452
+ * });
453
+ */
454
+ async createUserToken(userId, expiresIn) {
455
+ if (!userId) {
456
+ throw new RiviumSyncError(
457
+ 1300 /* INVALID_CONFIG */,
458
+ "createUserToken requires a userId"
459
+ );
460
+ }
461
+ return this.request(
462
+ "POST",
463
+ "/users/token",
464
+ expiresIn === void 0 ? { userId } : { userId, expiresIn }
465
+ );
466
+ }
428
467
  async request(method, path, body) {
429
468
  const url = `${this.config.baseUrl}${path}`;
430
469
  const headers = {
@@ -598,13 +637,19 @@ var _RiviumSyncAdmin = class _RiviumSyncAdmin {
598
637
  }
599
638
  }
600
639
  }
601
- // ==========================================================================
602
- // Realtime (Optional)
603
- // ==========================================================================
640
+ /**
641
+ * Topic for a collection or a single document.
642
+ * `rivium_sync/{projectId}/{databaseName}/{collectionName}[/{documentId}]`
643
+ */
644
+ topicFor(databaseId, collectionId, documentId) {
645
+ const base = `rivium_sync/${this.projectId ?? "unknown"}/${databaseId}/${collectionId}`;
646
+ return documentId === void 0 ? base : `${base}/${documentId}`;
647
+ }
604
648
  async initRealtime() {
605
649
  try {
606
650
  this.log(4 /* DEBUG */, "Fetching MQTT token...");
607
651
  const tokenData = await this.request("POST", "/connections/token");
652
+ this.projectId = tokenData.projectId ?? null;
608
653
  this.mqttConfig = {
609
654
  host: tokenData.mqtt.host,
610
655
  port: tokenData.mqtt.port,
@@ -661,7 +706,7 @@ var _RiviumSyncAdmin = class _RiviumSyncAdmin {
661
706
  };
662
707
  }
663
708
  const path = `/${databaseId}/${collectionId}/${documentId}`;
664
- const mqttTopic = `rivium_sync/${this.config.apiKey.substring(0, 16)}/db/${databaseId}/${collectionId}/${documentId}`;
709
+ const mqttTopic = this.topicFor(databaseId, collectionId, documentId);
665
710
  if (!this.documentListeners.has(path)) {
666
711
  this.documentListeners.set(path, /* @__PURE__ */ new Set());
667
712
  }
@@ -693,7 +738,7 @@ var _RiviumSyncAdmin = class _RiviumSyncAdmin {
693
738
  };
694
739
  }
695
740
  const path = `/${databaseId}/${collectionId}`;
696
- const mqttTopic = `rivium_sync/${this.config.apiKey.substring(0, 16)}/db/${databaseId}/${collectionId}/+`;
741
+ const mqttTopic = this.topicFor(databaseId, collectionId, "changes");
697
742
  if (!this.collectionListeners.has(path)) {
698
743
  this.collectionListeners.set(path, /* @__PURE__ */ new Set());
699
744
  }
@@ -725,12 +770,11 @@ var _RiviumSyncAdmin = class _RiviumSyncAdmin {
725
770
  }
726
771
  resubscribeAll() {
727
772
  if (!this.mqttClient?.connected) return;
728
- const appId = this.config.apiKey.substring(0, 16);
729
773
  this.documentListeners.forEach((_, path) => {
730
774
  const parts = path.split("/").filter((p) => p);
731
775
  if (parts.length === 3) {
732
776
  const [databaseId, collectionId, documentId] = parts;
733
- const topic = `rivium_sync/${appId}/db/${databaseId}/${collectionId}/${documentId}`;
777
+ const topic = this.topicFor(databaseId, collectionId, documentId);
734
778
  this.mqttClient.subscribe(topic, { qos: 1 });
735
779
  }
736
780
  });
@@ -738,7 +782,7 @@ var _RiviumSyncAdmin = class _RiviumSyncAdmin {
738
782
  const parts = path.split("/").filter((p) => p);
739
783
  if (parts.length === 2) {
740
784
  const [databaseId, collectionId] = parts;
741
- const topic = `rivium_sync/${appId}/db/${databaseId}/${collectionId}/+`;
785
+ const topic = this.topicFor(databaseId, collectionId, "changes");
742
786
  this.mqttClient.subscribe(topic, { qos: 1 });
743
787
  }
744
788
  });
package/dist/index.mjs CHANGED
@@ -336,6 +336,11 @@ var _RiviumSyncAdmin = class _RiviumSyncAdmin {
336
336
  this.documentListeners = /* @__PURE__ */ new Map();
337
337
  this.collectionListeners = /* @__PURE__ */ new Map();
338
338
  this.cachedCollections = /* @__PURE__ */ new Map();
339
+ // ==========================================================================
340
+ // Realtime (Optional)
341
+ // ==========================================================================
342
+ /** Project the API key belongs to, from POST /connections/token. */
343
+ this.projectId = null;
339
344
  if (!config.apiKey) {
340
345
  throw new RiviumSyncError(1301 /* MISSING_API_KEY */);
341
346
  }
@@ -382,6 +387,40 @@ var _RiviumSyncAdmin = class _RiviumSyncAdmin {
382
387
  // ==========================================================================
383
388
  // HTTP Client
384
389
  // ==========================================================================
390
+ /**
391
+ * Mint a user token so your app can prove who the acting user is.
392
+ *
393
+ * Security Rules read `auth.uid`. A client cannot set that itself - the API
394
+ * key it ships with is public, so the server would have no reason to believe
395
+ * it. Your backend, which holds the server secret, calls this and hands the
396
+ * token to the app; the client SDKs take it through their `tokenProvider`
397
+ * option and send it on every request.
398
+ *
399
+ * Never ship the server secret (or this call) inside an app.
400
+ *
401
+ * @param userId Your own id for the signed-in user - whatever your rules expect.
402
+ * @param expiresIn Lifetime in seconds. Default 1 hour, maximum 24 hours.
403
+ *
404
+ * @example
405
+ * // In your Express backend, behind your own session check:
406
+ * app.post('/rivium-sync-token', async (req, res) => {
407
+ * const { token, expiresIn } = await sync.createUserToken(req.session.userId);
408
+ * res.json({ token, expiresIn });
409
+ * });
410
+ */
411
+ async createUserToken(userId, expiresIn) {
412
+ if (!userId) {
413
+ throw new RiviumSyncError(
414
+ 1300 /* INVALID_CONFIG */,
415
+ "createUserToken requires a userId"
416
+ );
417
+ }
418
+ return this.request(
419
+ "POST",
420
+ "/users/token",
421
+ expiresIn === void 0 ? { userId } : { userId, expiresIn }
422
+ );
423
+ }
385
424
  async request(method, path, body) {
386
425
  const url = `${this.config.baseUrl}${path}`;
387
426
  const headers = {
@@ -555,13 +594,19 @@ var _RiviumSyncAdmin = class _RiviumSyncAdmin {
555
594
  }
556
595
  }
557
596
  }
558
- // ==========================================================================
559
- // Realtime (Optional)
560
- // ==========================================================================
597
+ /**
598
+ * Topic for a collection or a single document.
599
+ * `rivium_sync/{projectId}/{databaseName}/{collectionName}[/{documentId}]`
600
+ */
601
+ topicFor(databaseId, collectionId, documentId) {
602
+ const base = `rivium_sync/${this.projectId ?? "unknown"}/${databaseId}/${collectionId}`;
603
+ return documentId === void 0 ? base : `${base}/${documentId}`;
604
+ }
561
605
  async initRealtime() {
562
606
  try {
563
607
  this.log(4 /* DEBUG */, "Fetching MQTT token...");
564
608
  const tokenData = await this.request("POST", "/connections/token");
609
+ this.projectId = tokenData.projectId ?? null;
565
610
  this.mqttConfig = {
566
611
  host: tokenData.mqtt.host,
567
612
  port: tokenData.mqtt.port,
@@ -618,7 +663,7 @@ var _RiviumSyncAdmin = class _RiviumSyncAdmin {
618
663
  };
619
664
  }
620
665
  const path = `/${databaseId}/${collectionId}/${documentId}`;
621
- const mqttTopic = `rivium_sync/${this.config.apiKey.substring(0, 16)}/db/${databaseId}/${collectionId}/${documentId}`;
666
+ const mqttTopic = this.topicFor(databaseId, collectionId, documentId);
622
667
  if (!this.documentListeners.has(path)) {
623
668
  this.documentListeners.set(path, /* @__PURE__ */ new Set());
624
669
  }
@@ -650,7 +695,7 @@ var _RiviumSyncAdmin = class _RiviumSyncAdmin {
650
695
  };
651
696
  }
652
697
  const path = `/${databaseId}/${collectionId}`;
653
- const mqttTopic = `rivium_sync/${this.config.apiKey.substring(0, 16)}/db/${databaseId}/${collectionId}/+`;
698
+ const mqttTopic = this.topicFor(databaseId, collectionId, "changes");
654
699
  if (!this.collectionListeners.has(path)) {
655
700
  this.collectionListeners.set(path, /* @__PURE__ */ new Set());
656
701
  }
@@ -682,12 +727,11 @@ var _RiviumSyncAdmin = class _RiviumSyncAdmin {
682
727
  }
683
728
  resubscribeAll() {
684
729
  if (!this.mqttClient?.connected) return;
685
- const appId = this.config.apiKey.substring(0, 16);
686
730
  this.documentListeners.forEach((_, path) => {
687
731
  const parts = path.split("/").filter((p) => p);
688
732
  if (parts.length === 3) {
689
733
  const [databaseId, collectionId, documentId] = parts;
690
- const topic = `rivium_sync/${appId}/db/${databaseId}/${collectionId}/${documentId}`;
734
+ const topic = this.topicFor(databaseId, collectionId, documentId);
691
735
  this.mqttClient.subscribe(topic, { qos: 1 });
692
736
  }
693
737
  });
@@ -695,7 +739,7 @@ var _RiviumSyncAdmin = class _RiviumSyncAdmin {
695
739
  const parts = path.split("/").filter((p) => p);
696
740
  if (parts.length === 2) {
697
741
  const [databaseId, collectionId] = parts;
698
- const topic = `rivium_sync/${appId}/db/${databaseId}/${collectionId}/+`;
742
+ const topic = this.topicFor(databaseId, collectionId, "changes");
699
743
  this.mqttClient.subscribe(topic, { qos: 1 });
700
744
  }
701
745
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivium/sync-node",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "RiviumSync Node.js SDK - Server-side SDK for RiviumSync Realtime Database",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",