@liorandb/core 1.0.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/LICENSE.md +41 -0
- package/README.md +224 -0
- package/package.json +27 -0
- package/src/LioranManager.js +120 -0
- package/src/core/collection.js +160 -0
- package/src/core/database.js +109 -0
- package/src/core/query.js +65 -0
- package/src/index.d.ts +123 -0
- package/src/index.js +4 -0
- package/src/utils/encryption.js +30 -0
- package/src/utils/rootpath.js +77 -0
- package/src/utils/secureKey.js +18 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
LIORANDB LICENSE
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Swaraj Puppalwar
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software") to use, copy, modify, and distribute the Software for PERSONAL, NON-COMMERCIAL PURPOSES ONLY, subject to the following conditions:
|
|
7
|
+
|
|
8
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
SPECIAL CONDITIONS FOR LIORANDB
|
|
15
|
+
|
|
16
|
+
Notwithstanding the general permission above, the following additional conditions apply specifically to LioranDB:
|
|
17
|
+
|
|
18
|
+
1. Personal Use
|
|
19
|
+
Permission is granted to use the Software for personal projects, educational purposes, experimentation, and non-commercial development only.
|
|
20
|
+
|
|
21
|
+
2. Commercial and Production Use
|
|
22
|
+
Any use of the Software for commercial purposes, including but not limited to:
|
|
23
|
+
|
|
24
|
+
* deployment in production environments,
|
|
25
|
+
* integration into paid products or services,
|
|
26
|
+
* hosting for third parties,
|
|
27
|
+
* resale or monetization of any form,
|
|
28
|
+
|
|
29
|
+
is strictly prohibited without obtaining a separate commercial license from the official LioranDB SaaS platform or direct written authorization from Swaraj Puppalwar or Lioran Group.
|
|
30
|
+
|
|
31
|
+
3. No Resale or Redistribution
|
|
32
|
+
You may not sell, sublicense, redistribute, or repackage the Software as a standalone product or as part of any commercial offering without explicit written permission and a valid commercial agreement.
|
|
33
|
+
|
|
34
|
+
4. Ownership
|
|
35
|
+
All intellectual property rights in the Software, including but not limited to source code, architecture, brand identity, and system design, remain the exclusive property of Swaraj Puppalwar and Lioran Group.
|
|
36
|
+
|
|
37
|
+
By using this Software, you agree to comply fully with this license and all associated conditions.
|
|
38
|
+
|
|
39
|
+
This license supersedes and replaces any previous licensing terms related to LioranDB.
|
|
40
|
+
|
|
41
|
+
© 2025 Swaraj Puppalwar & Lioran Group. All rights reserved.
|
package/README.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# @liorandb/core
|
|
2
|
+
|
|
3
|
+
**LioranDB Core Module** – Lightweight, local-first, peer-to-peer database management for Node.js.
|
|
4
|
+
|
|
5
|
+
This is the **core system-level module** of LioranDB. It provides foundational database management functionality, including collections, queries, updates, encryption, and environment setup. **Note:** This is not the final database product, but a core module designed to be used in larger systems.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
* [Installation](#installation)
|
|
12
|
+
* [Overview](#overview)
|
|
13
|
+
* [Getting Started](#getting-started)
|
|
14
|
+
* [API Reference](#api-reference)
|
|
15
|
+
|
|
16
|
+
* [LioranManager](#lioranmanager)
|
|
17
|
+
* [LioranDB](#liorandb)
|
|
18
|
+
* [Collection](#collection)
|
|
19
|
+
* [Query Operators](#query-operators)
|
|
20
|
+
* [Update Operators](#update-operators)
|
|
21
|
+
* [Utilities](#utilities)
|
|
22
|
+
* [Encryption](#encryption)
|
|
23
|
+
* [Environment Setup](#environment-setup)
|
|
24
|
+
* [License](#license)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @liorandb/core
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
> Node.js v18+ recommended.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Overview
|
|
39
|
+
|
|
40
|
+
`@liorandb/core` provides:
|
|
41
|
+
|
|
42
|
+
* Local-first, file-based database directories.
|
|
43
|
+
* MongoDB-style API (`db`, `collection`, `insertOne`, `find`, `updateOne`, etc.).
|
|
44
|
+
* Peer-to-peer-friendly design.
|
|
45
|
+
* Data encryption at rest.
|
|
46
|
+
* Automatic environment configuration.
|
|
47
|
+
* TypeScript typings for full developer support.
|
|
48
|
+
|
|
49
|
+
This module is intended for **Node.js projects** and can serve as the core database engine for larger LioranDB systems.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Getting Started
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
import { LioranManager } from "@liorandb/core";
|
|
57
|
+
|
|
58
|
+
async function main() {
|
|
59
|
+
const manager = new LioranManager();
|
|
60
|
+
const db = await manager.db("myDatabase");
|
|
61
|
+
|
|
62
|
+
const users = db.collection("users");
|
|
63
|
+
|
|
64
|
+
// Insert a document
|
|
65
|
+
const user = await users.insertOne({ name: "Alice", age: 25 });
|
|
66
|
+
|
|
67
|
+
// Query documents
|
|
68
|
+
const results = await users.find({ age: { $gte: 18 } });
|
|
69
|
+
|
|
70
|
+
console.log(results);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
main();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## API Reference
|
|
79
|
+
|
|
80
|
+
### LioranManager
|
|
81
|
+
|
|
82
|
+
Manages databases and provides MongoDB-style client access.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
class LioranManager {
|
|
86
|
+
rootPath: string;
|
|
87
|
+
db(name: string): Promise<LioranDB>;
|
|
88
|
+
createDatabase(name: string): Promise<LioranDB>;
|
|
89
|
+
openDatabase(name: string): Promise<LioranDB>;
|
|
90
|
+
closeDatabase(name: string): Promise<void>;
|
|
91
|
+
renameDatabase(oldName: string, newName: string): Promise<boolean>;
|
|
92
|
+
deleteDatabase(name: string): Promise<boolean>;
|
|
93
|
+
dropDatabase(name: string): Promise<boolean>;
|
|
94
|
+
listDatabases(): Promise<string[]>;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Example:**
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
const manager = new LioranManager();
|
|
102
|
+
await manager.createDatabase("testDB");
|
|
103
|
+
const db = await manager.db("testDB");
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### LioranDB
|
|
107
|
+
|
|
108
|
+
Represents a single database instance with multiple collections.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
class LioranDB {
|
|
112
|
+
basePath: string;
|
|
113
|
+
dbName: string;
|
|
114
|
+
collection<T>(name: string): Collection<T>;
|
|
115
|
+
createCollection(name: string): Promise<boolean>;
|
|
116
|
+
deleteCollection(name: string): Promise<boolean>;
|
|
117
|
+
dropCollection(name: string): Promise<boolean>;
|
|
118
|
+
renameCollection(oldName: string, newName: string): Promise<boolean>;
|
|
119
|
+
listCollections(): Promise<string[]>;
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Example:**
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
const users = db.collection("users");
|
|
127
|
+
await db.createCollection("products");
|
|
128
|
+
const collections = await db.listCollections();
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Collection
|
|
132
|
+
|
|
133
|
+
Handles documents within a database.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
class Collection<T extends { _id?: string }> {
|
|
137
|
+
insertOne(doc: T): Promise<T>;
|
|
138
|
+
insertMany(docs: T[]): Promise<T[]>;
|
|
139
|
+
find(query?: FilterQuery<T>): Promise<T[]>;
|
|
140
|
+
findOne(query?: FilterQuery<T>): Promise<T | null>;
|
|
141
|
+
updateOne(filter: FilterQuery<T>, update: UpdateQuery<T>, options?: { upsert?: boolean }): Promise<T | null>;
|
|
142
|
+
updateMany(filter: FilterQuery<T>, update: UpdateQuery<T>): Promise<T[]>;
|
|
143
|
+
deleteOne(filter: FilterQuery<T>): Promise<boolean>;
|
|
144
|
+
deleteMany(filter: FilterQuery<T>): Promise<number>;
|
|
145
|
+
countDocuments(filter?: FilterQuery<T>): Promise<number>;
|
|
146
|
+
close(): Promise<void>;
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Example:**
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
await users.insertOne({ name: "Bob", age: 30 });
|
|
154
|
+
const adults = await users.find({ age: { $gte: 18 } });
|
|
155
|
+
await users.updateOne({ name: "Bob" }, { $inc: { age: 1 } });
|
|
156
|
+
await users.deleteOne({ name: "Alice" });
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Query Operators
|
|
160
|
+
|
|
161
|
+
* `$gt`, `$gte`, `$lt`, `$lte`, `$ne`, `$eq`, `$in`
|
|
162
|
+
|
|
163
|
+
**Example:**
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
users.find({ age: { $gte: 18, $lt: 65 } });
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Update Operators
|
|
170
|
+
|
|
171
|
+
* `$set` – set field values
|
|
172
|
+
* `$inc` – increment numeric fields
|
|
173
|
+
|
|
174
|
+
**Example:**
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
users.updateOne({ name: "Alice" }, { $set: { city: "Mumbai" }, $inc: { age: 1 } });
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Utilities
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
function getBaseDBFolder(): string;
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Returns the root folder for LioranDB databases. Automatically sets environment variables if missing.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Encryption
|
|
191
|
+
|
|
192
|
+
All documents are encrypted using **AES-256-GCM**.
|
|
193
|
+
|
|
194
|
+
* Uses a master key stored in `.secureKey` within the base folder.
|
|
195
|
+
* Data is encrypted automatically before storage and decrypted on retrieval.
|
|
196
|
+
|
|
197
|
+
**Utility Functions:**
|
|
198
|
+
|
|
199
|
+
* `encryptData(obj)`
|
|
200
|
+
* `decryptData(encStr)`
|
|
201
|
+
|
|
202
|
+
**Master Key Management:**
|
|
203
|
+
|
|
204
|
+
* Managed via `getMasterKey()`
|
|
205
|
+
* Auto-generates 256-bit key if not found.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Environment Setup
|
|
210
|
+
|
|
211
|
+
* `getBaseDBFolder()` ensures `LIORANDB_PATH` is set.
|
|
212
|
+
* Auto-generates scripts for **Windows PowerShell** or **Linux/macOS bash**.
|
|
213
|
+
* Guides users to set system-wide environment variables if missing.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
**Author:** Swaraj Puppalwar
|
|
220
|
+
**License:** LIORANDB LICENSE
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
**Keywords:** p2p-database, lioran, liorandb, p2p-db, peer-to-peer-db, peer-to-peer-database, localfirst-db, localfirst-database
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@liorandb/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "LioranDB is a lightweight, local-first, peer-to-peer, file-based database with a MongoDB-style Node.js API and a simple CLI for seamless distributed development.",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
|
+
"types": "./src/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {},
|
|
9
|
+
"bin": {},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"p2p-database",
|
|
12
|
+
"lioran",
|
|
13
|
+
"liorandb",
|
|
14
|
+
"p2p-db",
|
|
15
|
+
"peer-to-peer-db",
|
|
16
|
+
"peer-to-peer-database",
|
|
17
|
+
"localfirst-db",
|
|
18
|
+
"localfirst-database"
|
|
19
|
+
],
|
|
20
|
+
"author": "Swaraj Puppalwar",
|
|
21
|
+
"license": "LIORANDB LICENSE",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"classic-level": "^1.3.0",
|
|
24
|
+
"crypto": "^1.0.1",
|
|
25
|
+
"uuid": "^9.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { LioranDB } from "./core/database.js";
|
|
4
|
+
|
|
5
|
+
export class LioranManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.rootPath = path.join(
|
|
8
|
+
process.env.LIORANDB_PATH ||
|
|
9
|
+
path.join(process.env.HOME || process.env.USERPROFILE),
|
|
10
|
+
"LioranDB",
|
|
11
|
+
"db"
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(this.rootPath)) {
|
|
15
|
+
fs.mkdirSync(this.rootPath, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
this.openDBs = new Map();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// -----------------------------------------
|
|
22
|
+
// MongoDB-style: client.db("name")
|
|
23
|
+
// Auto-create database if not exists
|
|
24
|
+
// -----------------------------------------
|
|
25
|
+
async db(name) {
|
|
26
|
+
return this.openDatabase(name);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// -----------------------------------------
|
|
30
|
+
// Create a new database (returns db instance)
|
|
31
|
+
// -----------------------------------------
|
|
32
|
+
async createDatabase(name) {
|
|
33
|
+
const dbPath = path.join(this.rootPath, name);
|
|
34
|
+
|
|
35
|
+
if (fs.existsSync(dbPath)) {
|
|
36
|
+
throw new Error(`Database "${name}" already exists`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await fs.promises.mkdir(dbPath, { recursive: true });
|
|
40
|
+
return this.openDatabase(name);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// -----------------------------------------
|
|
44
|
+
// Open DB - AUTO CREATE if missing
|
|
45
|
+
// -----------------------------------------
|
|
46
|
+
async openDatabase(name) {
|
|
47
|
+
const dbPath = path.join(this.rootPath, name);
|
|
48
|
+
|
|
49
|
+
// Auto-create if not exists
|
|
50
|
+
if (!fs.existsSync(dbPath)) {
|
|
51
|
+
await fs.promises.mkdir(dbPath, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (this.openDBs.has(name)) {
|
|
55
|
+
return this.openDBs.get(name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const db = new LioranDB(dbPath, name, this);
|
|
59
|
+
this.openDBs.set(name, db);
|
|
60
|
+
|
|
61
|
+
return db;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// -----------------------------------------
|
|
65
|
+
// Close database
|
|
66
|
+
// -----------------------------------------
|
|
67
|
+
async closeDatabase(name) {
|
|
68
|
+
if (!this.openDBs.has(name)) return;
|
|
69
|
+
|
|
70
|
+
const db = this.openDBs.get(name);
|
|
71
|
+
|
|
72
|
+
for (const [, col] of db.collections.entries()) {
|
|
73
|
+
await col.close();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.openDBs.delete(name);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// -----------------------------------------
|
|
80
|
+
// Rename database
|
|
81
|
+
// -----------------------------------------
|
|
82
|
+
async renameDatabase(oldName, newName) {
|
|
83
|
+
const oldPath = path.join(this.rootPath, oldName);
|
|
84
|
+
const newPath = path.join(this.rootPath, newName);
|
|
85
|
+
|
|
86
|
+
if (!fs.existsSync(oldPath)) throw new Error(`Database "${oldName}" not found`);
|
|
87
|
+
if (fs.existsSync(newPath)) throw new Error(`Database "${newName}" already exists`);
|
|
88
|
+
|
|
89
|
+
await this.closeDatabase(oldName);
|
|
90
|
+
await fs.promises.rename(oldPath, newPath);
|
|
91
|
+
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// -----------------------------------------
|
|
96
|
+
// Delete / Drop database
|
|
97
|
+
// -----------------------------------------
|
|
98
|
+
async deleteDatabase(name) {
|
|
99
|
+
return this.dropDatabase(name);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async dropDatabase(name) {
|
|
103
|
+
const dbPath = path.join(this.rootPath, name);
|
|
104
|
+
|
|
105
|
+
if (!fs.existsSync(dbPath)) return false;
|
|
106
|
+
|
|
107
|
+
await this.closeDatabase(name);
|
|
108
|
+
await fs.promises.rm(dbPath, { recursive: true, force: true });
|
|
109
|
+
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// -----------------------------------------
|
|
114
|
+
// List all databases
|
|
115
|
+
// -----------------------------------------
|
|
116
|
+
async listDatabases() {
|
|
117
|
+
const items = await fs.promises.readdir(this.rootPath, { withFileTypes: true });
|
|
118
|
+
return items.filter(i => i.isDirectory()).map(i => i.name);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { ClassicLevel } from "classic-level";
|
|
2
|
+
import { matchDocument, applyUpdate } from "./query.js";
|
|
3
|
+
import { v4 as uuid } from "uuid";
|
|
4
|
+
import { encryptData, decryptData } from "../utils/encryption.js";
|
|
5
|
+
|
|
6
|
+
export class Collection {
|
|
7
|
+
constructor(dir) {
|
|
8
|
+
this.dir = dir;
|
|
9
|
+
this.db = new ClassicLevel(dir, { valueEncoding: "json" });
|
|
10
|
+
|
|
11
|
+
// Queue system
|
|
12
|
+
this.queue = Promise.resolve(); // start with a resolved promise
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Queue wrapper
|
|
16
|
+
_enqueue(task) {
|
|
17
|
+
// Add task to the queue
|
|
18
|
+
this.queue = this.queue.then(() => task()).catch(console.error);
|
|
19
|
+
return this.queue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async close() {
|
|
23
|
+
if (this.db) {
|
|
24
|
+
try {
|
|
25
|
+
await this.db.close();
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.warn("Warning: close() failed", err);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async insertOne(doc) {
|
|
33
|
+
return this._enqueue(async () => {
|
|
34
|
+
const _id = doc._id ?? uuid();
|
|
35
|
+
const final = { _id, ...doc };
|
|
36
|
+
const encrypted = encryptData(final);
|
|
37
|
+
await this.db.put(String(_id), encrypted);
|
|
38
|
+
return final;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async insertMany(docs = []) {
|
|
43
|
+
return this._enqueue(async () => {
|
|
44
|
+
const ops = [];
|
|
45
|
+
const out = [];
|
|
46
|
+
|
|
47
|
+
for (const d of docs) {
|
|
48
|
+
const _id = d._id ?? uuid();
|
|
49
|
+
const final = { _id, ...d };
|
|
50
|
+
const encrypted = encryptData(final);
|
|
51
|
+
ops.push({ type: "put", key: String(_id), value: encrypted });
|
|
52
|
+
out.push(final);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await this.db.batch(ops);
|
|
56
|
+
return out;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async find(query = {}) {
|
|
61
|
+
return this._enqueue(async () => {
|
|
62
|
+
const out = [];
|
|
63
|
+
for await (const [, encValue] of this.db.iterator()) {
|
|
64
|
+
const value = decryptData(encValue);
|
|
65
|
+
if (matchDocument(value, query)) out.push(value);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async findOne(query = {}) {
|
|
72
|
+
return this._enqueue(async () => {
|
|
73
|
+
for await (const [, encValue] of this.db.iterator()) {
|
|
74
|
+
const value = decryptData(encValue);
|
|
75
|
+
if (matchDocument(value, query)) return value;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async updateOne(filter = {}, update = {}, options = { upsert: false }) {
|
|
82
|
+
return this._enqueue(async () => {
|
|
83
|
+
for await (const [key, encValue] of this.db.iterator()) {
|
|
84
|
+
const value = decryptData(encValue);
|
|
85
|
+
if (matchDocument(value, filter)) {
|
|
86
|
+
const updated = applyUpdate(value, update);
|
|
87
|
+
updated._id = value._id;
|
|
88
|
+
const encrypted = encryptData(updated);
|
|
89
|
+
await this.db.put(key, encrypted);
|
|
90
|
+
return updated;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (options.upsert) {
|
|
95
|
+
const newDoc = applyUpdate(filter, update);
|
|
96
|
+
newDoc._id = newDoc._id ?? uuid();
|
|
97
|
+
const encrypted = encryptData(newDoc);
|
|
98
|
+
await this.db.put(String(newDoc._id), encrypted);
|
|
99
|
+
return newDoc;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async updateMany(filter = {}, update = {}) {
|
|
107
|
+
return this._enqueue(async () => {
|
|
108
|
+
const updated = [];
|
|
109
|
+
for await (const [key, encValue] of this.db.iterator()) {
|
|
110
|
+
const value = decryptData(encValue);
|
|
111
|
+
if (matchDocument(value, filter)) {
|
|
112
|
+
const newDoc = applyUpdate(value, update);
|
|
113
|
+
newDoc._id = value._id;
|
|
114
|
+
const encrypted = encryptData(newDoc);
|
|
115
|
+
await this.db.put(key, encrypted);
|
|
116
|
+
updated.push(newDoc);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return updated;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async deleteOne(filter = {}) {
|
|
124
|
+
return this._enqueue(async () => {
|
|
125
|
+
for await (const [key, encValue] of this.db.iterator()) {
|
|
126
|
+
const value = decryptData(encValue);
|
|
127
|
+
if (matchDocument(value, filter)) {
|
|
128
|
+
await this.db.del(key);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async deleteMany(filter = {}) {
|
|
137
|
+
return this._enqueue(async () => {
|
|
138
|
+
let count = 0;
|
|
139
|
+
for await (const [key, encValue] of this.db.iterator()) {
|
|
140
|
+
const value = decryptData(encValue);
|
|
141
|
+
if (matchDocument(value, filter)) {
|
|
142
|
+
await this.db.del(key);
|
|
143
|
+
count++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return count;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async countDocuments(filter = {}) {
|
|
151
|
+
return this._enqueue(async () => {
|
|
152
|
+
let c = 0;
|
|
153
|
+
for await (const [, encValue] of this.db.iterator()) {
|
|
154
|
+
const value = decryptData(encValue);
|
|
155
|
+
if (matchDocument(value, filter)) c++;
|
|
156
|
+
}
|
|
157
|
+
return c;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { Collection } from "./collection.js";
|
|
4
|
+
|
|
5
|
+
export class LioranDB {
|
|
6
|
+
constructor(basePath, dbName, manager) {
|
|
7
|
+
this.basePath = basePath;
|
|
8
|
+
this.dbName = dbName;
|
|
9
|
+
this.manager = manager;
|
|
10
|
+
this.collections = new Map();
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(basePath)) {
|
|
13
|
+
fs.mkdirSync(basePath, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Get or auto-create collection object */
|
|
18
|
+
collection(name) {
|
|
19
|
+
if (this.collections.has(name)) {
|
|
20
|
+
return this.collections.get(name);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const colPath = path.join(this.basePath, name);
|
|
24
|
+
|
|
25
|
+
// auto-create directory
|
|
26
|
+
if (!fs.existsSync(colPath)) {
|
|
27
|
+
fs.mkdirSync(colPath, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const col = new Collection(colPath);
|
|
31
|
+
this.collections.set(name, col);
|
|
32
|
+
return col;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Create collection manually */
|
|
36
|
+
async createCollection(name) {
|
|
37
|
+
const colPath = path.join(this.basePath, name);
|
|
38
|
+
|
|
39
|
+
if (fs.existsSync(colPath)) {
|
|
40
|
+
throw new Error("Collection already exists");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// create folder
|
|
44
|
+
await fs.promises.mkdir(colPath, { recursive: true });
|
|
45
|
+
|
|
46
|
+
// load into memory map
|
|
47
|
+
const col = new Collection(colPath);
|
|
48
|
+
this.collections.set(name, col);
|
|
49
|
+
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Delete collection fully */
|
|
54
|
+
async deleteCollection(name) {
|
|
55
|
+
const colPath = path.join(this.basePath, name);
|
|
56
|
+
|
|
57
|
+
if (!fs.existsSync(colPath)) {
|
|
58
|
+
throw new Error("Collection does not exist");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// close LevelDB instance if opened
|
|
62
|
+
if (this.collections.has(name)) {
|
|
63
|
+
const col = this.collections.get(name);
|
|
64
|
+
await col.close().catch(() => {});
|
|
65
|
+
await col.db.clear().catch(() => {});
|
|
66
|
+
this.collections.delete(name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// force delete directory
|
|
70
|
+
await fs.promises.rm(colPath, { recursive: true, force: true });
|
|
71
|
+
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Rename collection */
|
|
76
|
+
async renameCollection(oldName, newName) {
|
|
77
|
+
const oldPath = path.join(this.basePath, oldName);
|
|
78
|
+
const newPath = path.join(this.basePath, newName);
|
|
79
|
+
|
|
80
|
+
if (!fs.existsSync(oldPath)) throw new Error("Collection does not exist");
|
|
81
|
+
if (fs.existsSync(newPath)) throw new Error("New collection name exists");
|
|
82
|
+
|
|
83
|
+
if (this.collections.has(oldName)) {
|
|
84
|
+
await this.collections.get(oldName).close().catch(() => {});
|
|
85
|
+
this.collections.delete(oldName);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await fs.promises.rename(oldPath, newPath);
|
|
89
|
+
|
|
90
|
+
const newCol = new Collection(newPath);
|
|
91
|
+
this.collections.set(newName, newCol);
|
|
92
|
+
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Drop a collection (alias deleteCollection) */
|
|
97
|
+
async dropCollection(name) {
|
|
98
|
+
return this.deleteCollection(name);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** List all collections */
|
|
102
|
+
async listCollections() {
|
|
103
|
+
const dirs = await fs.promises.readdir(this.basePath, { withFileTypes: true });
|
|
104
|
+
|
|
105
|
+
return dirs
|
|
106
|
+
.filter(dirent => dirent.isDirectory())
|
|
107
|
+
.map(dirent => dirent.name);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
function getByPath(obj, path) {
|
|
2
|
+
return path.split(".").reduce((o, p) => (o ? o[p] : undefined), obj);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function matchDocument(doc, query) {
|
|
6
|
+
for (const key of Object.keys(query)) {
|
|
7
|
+
const cond = query[key];
|
|
8
|
+
const val = getByPath(doc, key);
|
|
9
|
+
|
|
10
|
+
if (cond && typeof cond === "object" && !Array.isArray(cond)) {
|
|
11
|
+
for (const op of Object.keys(cond)) {
|
|
12
|
+
const v = cond[op];
|
|
13
|
+
if (op === "$gt" && !(val > v)) return false;
|
|
14
|
+
if (op === "$gte" && !(val >= v)) return false;
|
|
15
|
+
if (op === "$lt" && !(val < v)) return false;
|
|
16
|
+
if (op === "$lte" && !(val <= v)) return false;
|
|
17
|
+
if (op === "$ne" && (val === v)) return false;
|
|
18
|
+
if (op === "$eq" && val !== v) return false;
|
|
19
|
+
if (op === "$in" && (!Array.isArray(v) || !v.includes(val))) return false;
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
if (val !== cond) return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function applyUpdate(oldDoc, update) {
|
|
29
|
+
const doc = structuredClone(oldDoc);
|
|
30
|
+
|
|
31
|
+
// $set
|
|
32
|
+
if (update.$set) {
|
|
33
|
+
for (const k in update.$set) {
|
|
34
|
+
const parts = k.split(".");
|
|
35
|
+
let cur = doc;
|
|
36
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
37
|
+
cur[parts[i]] = cur[parts[i]] ?? {};
|
|
38
|
+
cur = cur[parts[i]];
|
|
39
|
+
}
|
|
40
|
+
cur[parts.at(-1)] = update.$set[k];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// $inc
|
|
45
|
+
if (update.$inc) {
|
|
46
|
+
for (const k in update.$inc) {
|
|
47
|
+
const val = getByPath(doc, k) ?? 0;
|
|
48
|
+
const parts = k.split(".");
|
|
49
|
+
let cur = doc;
|
|
50
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
51
|
+
cur[parts[i]] = cur[parts[i]] ?? {};
|
|
52
|
+
cur = cur[parts[i]];
|
|
53
|
+
}
|
|
54
|
+
cur[parts.at(-1)] = val + update.$inc[k];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Direct merge (no operators)
|
|
59
|
+
const hasOp = Object.keys(update).some((k) => k.startsWith("$"));
|
|
60
|
+
if (!hasOp) {
|
|
61
|
+
return { ...doc, ...update };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return doc;
|
|
65
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// ================================================
|
|
2
|
+
// @liorandb/core - Type Definitions
|
|
3
|
+
// ================================================
|
|
4
|
+
|
|
5
|
+
declare module "@liorandb/core" {
|
|
6
|
+
// --------------------------
|
|
7
|
+
// Generic Query Operators
|
|
8
|
+
// --------------------------
|
|
9
|
+
|
|
10
|
+
export interface QueryOperators<T> {
|
|
11
|
+
$gt?: T;
|
|
12
|
+
$gte?: T;
|
|
13
|
+
$lt?: T;
|
|
14
|
+
$lte?: T;
|
|
15
|
+
$ne?: T;
|
|
16
|
+
$in?: T[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type FilterQuery<T> = {
|
|
20
|
+
[K in keyof T]?: T[K] | QueryOperators<T[K]>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// --------------------------
|
|
24
|
+
// Update Operators
|
|
25
|
+
// --------------------------
|
|
26
|
+
|
|
27
|
+
export interface UpdateSet<T> {
|
|
28
|
+
$set?: Partial<T>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface UpdateInc<T> {
|
|
32
|
+
$inc?: {
|
|
33
|
+
[K in keyof T]?: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type UpdateQuery<T> = Partial<T> | (UpdateSet<T> & UpdateInc<T>);
|
|
38
|
+
|
|
39
|
+
// --------------------------
|
|
40
|
+
// Collection Class
|
|
41
|
+
// --------------------------
|
|
42
|
+
|
|
43
|
+
export class Collection<T extends { _id?: string }> {
|
|
44
|
+
constructor(dir: string);
|
|
45
|
+
|
|
46
|
+
close(): Promise<void>;
|
|
47
|
+
|
|
48
|
+
insertOne(doc: T): Promise<T>;
|
|
49
|
+
insertMany(docs: T[]): Promise<T[]>;
|
|
50
|
+
|
|
51
|
+
find(query?: FilterQuery<T>): Promise<T[]>;
|
|
52
|
+
findOne(query?: FilterQuery<T>): Promise<T | null>;
|
|
53
|
+
|
|
54
|
+
updateOne(
|
|
55
|
+
filter: FilterQuery<T>,
|
|
56
|
+
update: UpdateQuery<T>,
|
|
57
|
+
options?: { upsert?: boolean }
|
|
58
|
+
): Promise<T | null>;
|
|
59
|
+
|
|
60
|
+
updateMany(
|
|
61
|
+
filter: FilterQuery<T>,
|
|
62
|
+
update: UpdateQuery<T>
|
|
63
|
+
): Promise<T[]>;
|
|
64
|
+
|
|
65
|
+
deleteOne(filter: FilterQuery<T>): Promise<boolean>;
|
|
66
|
+
deleteMany(filter: FilterQuery<T>): Promise<number>;
|
|
67
|
+
|
|
68
|
+
countDocuments(filter?: FilterQuery<T>): Promise<number>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --------------------------
|
|
72
|
+
// LioranDB: Database Instance
|
|
73
|
+
// --------------------------
|
|
74
|
+
|
|
75
|
+
export class LioranDB {
|
|
76
|
+
basePath: string;
|
|
77
|
+
dbName: string;
|
|
78
|
+
|
|
79
|
+
constructor(basePath: string, dbName: string, manager: LioranManager);
|
|
80
|
+
|
|
81
|
+
collection<T extends { _id?: string }>(name: string): Collection<T>;
|
|
82
|
+
|
|
83
|
+
createCollection(name: string): Promise<boolean>;
|
|
84
|
+
deleteCollection(name: string): Promise<boolean>;
|
|
85
|
+
dropCollection(name: string): Promise<boolean>;
|
|
86
|
+
renameCollection(oldName: string, newName: string): Promise<boolean>;
|
|
87
|
+
|
|
88
|
+
listCollections(): Promise<string[]>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --------------------------
|
|
92
|
+
// LioranManager
|
|
93
|
+
// --------------------------
|
|
94
|
+
|
|
95
|
+
export class LioranManager {
|
|
96
|
+
rootPath: string;
|
|
97
|
+
|
|
98
|
+
constructor();
|
|
99
|
+
|
|
100
|
+
// MongoDB-style
|
|
101
|
+
db(name: string): Promise<LioranDB>;
|
|
102
|
+
|
|
103
|
+
createDatabase(name: string): Promise<LioranDB>;
|
|
104
|
+
openDatabase(name: string): Promise<LioranDB>;
|
|
105
|
+
closeDatabase(name: string): Promise<void>;
|
|
106
|
+
|
|
107
|
+
renameDatabase(oldName: string, newName: string): Promise<boolean>;
|
|
108
|
+
|
|
109
|
+
deleteDatabase(name: string): Promise<boolean>;
|
|
110
|
+
dropDatabase(name: string): Promise<boolean>;
|
|
111
|
+
|
|
112
|
+
listDatabases(): Promise<string[]>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --------------------------
|
|
116
|
+
// Utils
|
|
117
|
+
// --------------------------
|
|
118
|
+
|
|
119
|
+
export function getBaseDBFolder(): string;
|
|
120
|
+
|
|
121
|
+
// Package exports
|
|
122
|
+
export { LioranManager, LioranDB, getBaseDBFolder };
|
|
123
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { getMasterKey } from "./secureKey.js";
|
|
3
|
+
|
|
4
|
+
const algorithm = "aes-256-gcm";
|
|
5
|
+
const key = Buffer.from(getMasterKey(), "hex");
|
|
6
|
+
|
|
7
|
+
export function encryptData(plainObj) {
|
|
8
|
+
const iv = crypto.randomBytes(16); // random 16-byte IV
|
|
9
|
+
const data = Buffer.from(JSON.stringify(plainObj), "utf8");
|
|
10
|
+
|
|
11
|
+
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
|
12
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
13
|
+
const tag = cipher.getAuthTag();
|
|
14
|
+
|
|
15
|
+
// store as iv + tag + data, base64 encoded
|
|
16
|
+
return Buffer.concat([iv, tag, encrypted]).toString("base64");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function decryptData(encStr) {
|
|
20
|
+
const buf = Buffer.from(encStr, "base64");
|
|
21
|
+
const iv = buf.slice(0, 16);
|
|
22
|
+
const tag = buf.slice(16, 32);
|
|
23
|
+
const encrypted = buf.slice(32);
|
|
24
|
+
|
|
25
|
+
const decipher = crypto.createDecipheriv(algorithm, key, iv);
|
|
26
|
+
decipher.setAuthTag(tag);
|
|
27
|
+
|
|
28
|
+
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
29
|
+
return JSON.parse(decrypted.toString("utf8"));
|
|
30
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
|
|
5
|
+
export function getBaseDBFolder() {
|
|
6
|
+
let dbPath = process.env.LIORANDB_PATH;
|
|
7
|
+
|
|
8
|
+
// If LIORANDB_PATH is NOT set system-wide
|
|
9
|
+
if (!dbPath) {
|
|
10
|
+
const homeDir = os.homedir();
|
|
11
|
+
dbPath = path.join(homeDir, "LioranDB");
|
|
12
|
+
|
|
13
|
+
// Ensure directory exists
|
|
14
|
+
if (!fs.existsSync(dbPath)) {
|
|
15
|
+
fs.mkdirSync(dbPath, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Set env var for current process
|
|
19
|
+
process.env.LIORANDB_PATH = dbPath;
|
|
20
|
+
|
|
21
|
+
// Create OS-specific installer scripts
|
|
22
|
+
createSystemEnvInstaller(dbPath);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return dbPath;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createSystemEnvInstaller(dbPath) {
|
|
29
|
+
const platform = os.platform();
|
|
30
|
+
const scriptsDir = path.join(os.tmpdir(), "lioran_env_setup");
|
|
31
|
+
|
|
32
|
+
if (!fs.existsSync(scriptsDir)) {
|
|
33
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (platform === "win32") {
|
|
37
|
+
const winScript = path.join(scriptsDir, "set-lioran-env.ps1");
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
winScript,
|
|
40
|
+
`Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
|
|
41
|
+
[System.Environment]::SetEnvironmentVariable("LIORANDB_PATH", "${dbPath}", "Machine")
|
|
42
|
+
Write-Host "LIORANDB_PATH set system-wide to: ${dbPath}"`,
|
|
43
|
+
"utf8"
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
console.log(`
|
|
47
|
+
⚠️ LIORANDB_PATH is not set system-wide.
|
|
48
|
+
Run this PowerShell script as ADMIN:
|
|
49
|
+
|
|
50
|
+
${winScript}
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (platform === "linux" || platform === "darwin") {
|
|
55
|
+
const bashScript = path.join(scriptsDir, "set-lioran-env.sh");
|
|
56
|
+
fs.writeFileSync(
|
|
57
|
+
bashScript,
|
|
58
|
+
`#!/bin/bash
|
|
59
|
+
echo 'export LIORANDB_PATH="${dbPath}"' >> ~/.bashrc
|
|
60
|
+
echo 'export LIORANDB_PATH="${dbPath}"' >> ~/.zshrc
|
|
61
|
+
echo "LIORANDB_PATH set system-wide to: ${dbPath}"
|
|
62
|
+
source ~/.bashrc 2>/dev/null
|
|
63
|
+
source ~/.zshrc 2>/dev/null
|
|
64
|
+
`,
|
|
65
|
+
"utf8"
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
fs.chmodSync(bashScript, 0o755);
|
|
69
|
+
|
|
70
|
+
console.log(`
|
|
71
|
+
⚠️ LIORANDB_PATH is not set system-wide.
|
|
72
|
+
Run this script:
|
|
73
|
+
|
|
74
|
+
bash ${bashScript}
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import { getBaseDBFolder } from "./rootpath.js";
|
|
5
|
+
|
|
6
|
+
const KEY_FILE = path.join(getBaseDBFolder(), ".secureKey");
|
|
7
|
+
|
|
8
|
+
export function getMasterKey() {
|
|
9
|
+
if (!fs.existsSync(KEY_FILE)) {
|
|
10
|
+
// generate 32-byte random key (256-bit)
|
|
11
|
+
const key = crypto.randomBytes(32).toString("hex");
|
|
12
|
+
fs.writeFileSync(KEY_FILE, key, { encoding: "utf8", mode: 0o600 });
|
|
13
|
+
return key;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const key = fs.readFileSync(KEY_FILE, "utf8").trim();
|
|
17
|
+
return key;
|
|
18
|
+
}
|