@locci/db 1.0.0-alpha-1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 MikeTeddyOmondi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,319 @@
1
+ # Locci DB - [@locci/db]()
2
+
3
+ Lightweight PostgreSQL-compatible database server powered by [PGlite](https://pglite.dev/).
4
+
5
+ ## Features
6
+
7
+ - **Embedded Database**: Full PostgreSQL compatibility with PGlite
8
+ - **Easy to Deploy**: Standalone executable or Docker container
9
+ - **Secure**: Password hashing, connection limiting, IP filtering
10
+ - **Configurable**: Support for config files, environment variables, and CLI flags
11
+ - **CLI Support**: Generate hashes, manage database via command line
12
+ - **Production Ready**: Graceful shutdown, connection pooling, comprehensive logging
13
+
14
+ ## Installation
15
+
16
+ ### As NPM Package
17
+
18
+ ```bash
19
+ npm install @locci/db
20
+ ```
21
+
22
+ ### Global Installation
23
+
24
+ ```bash
25
+ npm install -g @locci/db
26
+ ```
27
+
28
+ ### Using npx
29
+
30
+ ```bash
31
+ npx @locci/db start
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### Start the Server
37
+
38
+ ```bash
39
+ locci-db start
40
+ ```
41
+
42
+ Options:
43
+ - `-p, --port <port>` - Port to listen on (default: 5432)
44
+ - `-h, --host <host>` - Host to bind to (default: 127.0.0.1)
45
+ - `-d, --data-dir <path>` - Data directory path
46
+ - `-l, --log-level <level>` - Log level: trace, debug, info, warn, error, fatal (default: info)
47
+
48
+ ### Generate Password Hash
49
+
50
+ ```bash
51
+ locci-db hash-password "clear-text-password-here"
52
+ ```
53
+
54
+ This generates a secure hash to use in configuration files instead of plaintext passwords.
55
+
56
+ ## Configuration
57
+
58
+ Configuration is loaded from multiple sources in this order:
59
+ 1. CLI flags (highest priority)
60
+ 2. Environment variables
61
+ 3. A `.env` file in the working directory
62
+ 4. Configuration files
63
+ 5. Built-in defaults (lowest priority)
64
+
65
+ A value set at a higher level overrides the same value below it. Levels are
66
+ merged per field, so a config file can supply `auth` while an environment
67
+ variable overrides only the port.
68
+
69
+ ### Environment Variables
70
+
71
+ ```bash
72
+ export LOCCI_DB_PORT=5433
73
+ export LOCCI_DB_HOST=127.0.0.1
74
+ export LOCCI_DB_USERNAME=dbuser
75
+ export LOCCI_DB_PASSWORD=dbpassword
76
+ export LOCCI_DB_PASSWORD_HASH="salt:derivedkey" # If using hash
77
+ export LOCCI_DB_DATA_DIR=/path/to/data
78
+ export LOCCI_DB_LOG_LEVEL=info
79
+ ```
80
+
81
+ These can also live in a `.env` file in the working directory, which is read at
82
+ startup. Real environment variables take precedence over the file, so exporting
83
+ a value still overrides it.
84
+
85
+ ### Configuration File
86
+
87
+ Create `.locci-db.json` in your project directory:
88
+
89
+ ```json
90
+ {
91
+ "port": 5432,
92
+ "host": "127.0.0.1",
93
+ "dataDir": "./my-database",
94
+ "logLevel": "info",
95
+ "auth": {
96
+ "username": "postgres",
97
+ "passwordHash": "salt:derivedkey"
98
+ },
99
+ "security": {
100
+ "allowedHosts": ["127.0.0.1", "localhost"],
101
+ "maxConnections": 10
102
+ }
103
+ }
104
+ ```
105
+
106
+ Supported file formats:
107
+ - `.locci-db.json`
108
+ - `.locci-db.yaml`
109
+ - `.locci-db.yml`
110
+ - `.locci-db.config.js`
111
+ - `locci-db.config.js`
112
+ - `package.json` (under `locci-db` key)
113
+
114
+ ## Single-file binary
115
+
116
+ The server compiles to a standalone executable with Bun. It embeds the Bun
117
+ runtime and PGlite's WASM, so it needs no Node, no Bun, and no `node_modules`
118
+ on the target machine.
119
+
120
+ ```bash
121
+ bun run compile # -> dist-bin/locci-db
122
+ ./dist-bin/locci-db start
123
+ ```
124
+
125
+ Every release publishes prebuilt binaries for Linux (x64, arm64, x64-musl),
126
+ macOS (x64, arm64), and Windows (x64), alongside a `SHA256SUMS` file. Verify a
127
+ download before running it:
128
+
129
+ ```bash
130
+ sha256sum -c SHA256SUMS --ignore-missing
131
+ ```
132
+
133
+ When a signing key is configured for the repository, each artifact also ships a
134
+ detached PGP signature (`.asc`):
135
+
136
+ ```bash
137
+ gpg --verify locci-db-linux-x64.asc locci-db-linux-x64
138
+ ```
139
+
140
+ ## Docker
141
+
142
+ The image ships the compiled binary on `distroless/cc`, so it contains no Node,
143
+ no Bun, and no `node_modules`. Images are built for `linux/amd64` and
144
+ `linux/arm64`.
145
+
146
+
147
+ ### Build
148
+
149
+ ```bash
150
+ docker build -t locci/db:latest .
151
+ ```
152
+
153
+ ### Run
154
+
155
+ ```bash
156
+ docker run -d \
157
+ -p 5432:5432 \
158
+ -e LOCCI_DB_USERNAME=postgres \
159
+ -e LOCCI_DB_PASSWORD=postgres \
160
+ -v locci-data:/app/data \
161
+ locci/db:latest
162
+ ```
163
+
164
+ ### Docker Compose
165
+
166
+ ```bash
167
+ docker compose up -d
168
+ ```
169
+
170
+ ## Security Features
171
+
172
+ - **Password Hashing**: Uses scrypt for secure password hashing
173
+ - **Connection Limiting**: Prevents resource exhaustion
174
+ - **IP Whitelisting**: Restrict connections to authorized hosts
175
+ - **Input Validation**: Zod schema validation for all configurations
176
+ - **Graceful Shutdown**: Properly closes connections on SIGTERM/SIGINT
177
+
178
+ ## Development
179
+
180
+ ### Prerequisites
181
+
182
+ - [Bun](https://bun.sh/) (package manager, and required to compile binaries)
183
+ - Node.js >= 20.0.0 (the published package targets Node)
184
+
185
+ ### Installation
186
+
187
+ Dependencies are managed with Bun (`bun.lock`):
188
+
189
+ ```bash
190
+ bun install
191
+ ```
192
+
193
+ ### Building
194
+
195
+ ```bash
196
+ npm run build
197
+ ```
198
+
199
+ ### Development Mode
200
+
201
+ ```bash
202
+ npm run dev
203
+ ```
204
+
205
+ ### Testing
206
+
207
+ Tests run on [Vitest](https://vitest.dev/) under Node, split into two projects:
208
+
209
+ ```bash
210
+ npm test # unit tests
211
+ npm run test:unit # same, explicit
212
+ npm run test:e2e # builds, then boots the real server and connects to it
213
+ npm run test:all # both projects
214
+ npm run test:watch # unit tests in watch mode
215
+ npm run test:coverage
216
+ ```
217
+
218
+ Unit specs live in `tests/unit`, end-to-end specs in `tests/e2e`. E2E specs bind
219
+ real ports, so they run serially with longer timeouts.
220
+
221
+ ### Linting and type checking
222
+
223
+ ```bash
224
+ npm run lint # oxlint, warnings are errors
225
+ npm run lint:fix # apply autofixes
226
+ npm run typecheck # tsc over src and tests
227
+ ```
228
+
229
+ ### Compiling
230
+
231
+ ```bash
232
+ npm run compile # single-file binary into dist-bin/
233
+ ```
234
+
235
+ ## API
236
+
237
+ ### `startServer(): Promise<net.Server>`
238
+
239
+ Starts the database server with loaded configuration.
240
+
241
+ ```typescript
242
+ import { startServer } from '@locci/db';
243
+
244
+ const server = await startServer();
245
+ ```
246
+
247
+ ### `loadConfig(): Config`
248
+
249
+ Loads configuration from files and environment variables.
250
+
251
+ ```typescript
252
+ import { loadConfig } from '@locci/db/config';
253
+
254
+ const config = loadConfig();
255
+ ```
256
+
257
+ ### `hashPassword(password: string): Promise<string>`
258
+
259
+ Generates a secure password hash.
260
+
261
+ ```typescript
262
+ import { hashPassword } from '@locci/db/config';
263
+
264
+ const hash = await hashPassword('my-password');
265
+ ```
266
+
267
+ ### `verifyPassword(password: string, hash: string): Promise<boolean>`
268
+
269
+ Verifies a password against a hash.
270
+
271
+ ```typescript
272
+ import { verifyPassword } from '@locci/db/config';
273
+
274
+ const isValid = await verifyPassword('my-password', hash);
275
+ ```
276
+
277
+ ## Publishing
278
+
279
+ Releases are automated. Pushing a version tag runs the `Release` workflow,
280
+ which verifies the build, cross-compiles the binaries, creates the GitHub
281
+ release with those binaries and their checksums attached, publishes to npm, and
282
+ pushes multi-arch images to Docker Hub and GHCR.
283
+
284
+ ```bash
285
+ npm version patch|minor|major # updates package.json and creates the tag
286
+ git push --follow-tags
287
+ ```
288
+
289
+ The workflow fails if the tag does not match the version in `package.json`. A
290
+ prerelease version (one containing a hyphen, such as `1.0.0-alpha-1`) publishes
291
+ to npm under the `next` dist-tag and does not move the `latest` Docker tag.
292
+
293
+ Required repository secrets: `NPM_TOKEN`, `DOCKER_USERNAME`, `DOCKER_PASSWORD`.
294
+ GHCR uses the built-in `GITHUB_TOKEN`. Set `GPG_PRIVATE_KEY` and
295
+ `GPG_PASSPHRASE` to sign the release binaries; without them the release still
296
+ completes, with a warning, and the binaries are published unsigned.
297
+
298
+ ## License
299
+
300
+ MIT - See LICENSE file for details
301
+
302
+ ## Dependencies
303
+
304
+ - [PGlite](https://pglite.dev/) - PostgreSQL in WASM
305
+ - [pg-gateway](https://github.com/supabase-community/pg-gateway) - PostgreSQL wire protocol handler
306
+ - [Pino](https://getpino.io/) - Fast logger
307
+ - [Commander](https://github.com/tj/commander.js) - CLI framework
308
+ - [Zod](https://zod.dev/) - TypeScript-first schema validation
309
+ - [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig) - Configuration file discovery
310
+ - [defu](https://github.com/unjs/defu) - Configuration merging
311
+ - [dotenv](https://github.com/motdotla/dotenv) - `.env` file loading
312
+
313
+ ## Contributing
314
+
315
+ Contributions are welcome! Please feel free to submit a Pull Request.
316
+
317
+ ## Support
318
+
319
+ For issues and feature requests, please use the GitHub issues page.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Shim to load the compiled TypeScript CLI
4
+ import('../dist/cli.js').catch((err) => {
5
+ console.error('Failed to start @locci/db:', err);
6
+ process.exit(1);
7
+ });
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ // Must come before any import that reads process.env. Values already present
3
+ // in the real environment win; .env only fills in what is missing.
4
+ import { config as loadEnvFile } from "dotenv";
5
+ loadEnvFile({ quiet: true });
6
+ import { Command } from "commander";
7
+ import { startServer } from "./index.js";
8
+ import { hashPassword } from "./config.js";
9
+ import { VERSION } from "./version.js";
10
+ import { log } from "./utils.js";
11
+ const program = new Command();
12
+ program
13
+ .name("locci-db")
14
+ .description("Lightweight PostgreSQL-compatible database server")
15
+ .version(VERSION);
16
+ program
17
+ .command("start")
18
+ .description("Start the database server")
19
+ .option("-p, --port <port>", "Port to listen on (default: 5432)")
20
+ .option("-h, --host <host>", "Host to bind to (default: 127.0.0.1)")
21
+ .option("-d, --data-dir <path>", "Data directory path")
22
+ .option("-l, --log-level <level>", "Log level (default: info)")
23
+ .action(async (options) => {
24
+ // Only flags the user actually passed become overrides. Setting these
25
+ // unconditionally would make commander defaults outrank the config file.
26
+ if (options.port)
27
+ process.env.LOCCI_DB_PORT = options.port;
28
+ if (options.host)
29
+ process.env.LOCCI_DB_HOST = options.host;
30
+ if (options.dataDir)
31
+ process.env.LOCCI_DB_DATA_DIR = options.dataDir;
32
+ if (options.logLevel)
33
+ process.env.LOCCI_DB_LOG_LEVEL = options.logLevel;
34
+ try {
35
+ await startServer();
36
+ }
37
+ catch (error) {
38
+ log.error({ error }, "Failed to start server:");
39
+ process.exit(1);
40
+ }
41
+ });
42
+ program
43
+ .command("hash-password <password>")
44
+ .description("Generate a password hash for configuration")
45
+ .action(async (password) => {
46
+ try {
47
+ const hash = await hashPassword(password);
48
+ log.info({ hash }, "Password hash (store this in your config) ");
49
+ }
50
+ catch (error) {
51
+ log.error({ error }, "Failed to generate password hash:");
52
+ process.exit(1);
53
+ }
54
+ });
55
+ program.parse();
@@ -0,0 +1,74 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Configuration schema with validation
4
+ */
5
+ export declare const ConfigSchema: z.ZodObject<{
6
+ port: z.ZodDefault<z.ZodNumber>;
7
+ host: z.ZodDefault<z.ZodString>;
8
+ dataDir: z.ZodOptional<z.ZodString>;
9
+ logLevel: z.ZodDefault<z.ZodEnum<["trace", "debug", "info", "warn", "error", "fatal"]>>;
10
+ auth: z.ZodObject<{
11
+ username: z.ZodString;
12
+ password: z.ZodOptional<z.ZodString>;
13
+ passwordHash: z.ZodOptional<z.ZodString>;
14
+ }, "strip", z.ZodTypeAny, {
15
+ username: string;
16
+ password?: string | undefined;
17
+ passwordHash?: string | undefined;
18
+ }, {
19
+ username: string;
20
+ password?: string | undefined;
21
+ passwordHash?: string | undefined;
22
+ }>;
23
+ security: z.ZodOptional<z.ZodObject<{
24
+ allowedHosts: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
25
+ maxConnections: z.ZodDefault<z.ZodNumber>;
26
+ }, "strip", z.ZodTypeAny, {
27
+ allowedHosts: string[];
28
+ maxConnections: number;
29
+ }, {
30
+ allowedHosts?: string[] | undefined;
31
+ maxConnections?: number | undefined;
32
+ }>>;
33
+ }, "strip", z.ZodTypeAny, {
34
+ port: number;
35
+ host: string;
36
+ logLevel: "info" | "fatal" | "error" | "warn" | "debug" | "trace";
37
+ auth: {
38
+ username: string;
39
+ password?: string | undefined;
40
+ passwordHash?: string | undefined;
41
+ };
42
+ dataDir?: string | undefined;
43
+ security?: {
44
+ allowedHosts: string[];
45
+ maxConnections: number;
46
+ } | undefined;
47
+ }, {
48
+ auth: {
49
+ username: string;
50
+ password?: string | undefined;
51
+ passwordHash?: string | undefined;
52
+ };
53
+ port?: number | undefined;
54
+ host?: string | undefined;
55
+ dataDir?: string | undefined;
56
+ logLevel?: "info" | "fatal" | "error" | "warn" | "debug" | "trace" | undefined;
57
+ security?: {
58
+ allowedHosts?: string[] | undefined;
59
+ maxConnections?: number | undefined;
60
+ } | undefined;
61
+ }>;
62
+ export type Config = z.infer<typeof ConfigSchema>;
63
+ /**
64
+ * Hash password securely using scrypt
65
+ */
66
+ export declare function hashPassword(password: string): Promise<string>;
67
+ /**
68
+ * Verify password against hash
69
+ */
70
+ export declare function verifyPassword(password: string, hash: string): Promise<boolean>;
71
+ /**
72
+ * Load configuration from multiple sources
73
+ */
74
+ export declare function loadConfig(): Config;
package/dist/config.js ADDED
@@ -0,0 +1,90 @@
1
+ import { z } from "zod";
2
+ import { cosmiconfigSync } from "cosmiconfig";
3
+ import { defu } from "defu";
4
+ import { randomBytes, scrypt } from "crypto";
5
+ import { promisify } from "util";
6
+ import { log } from "./utils.js";
7
+ const scryptAsync = promisify(scrypt);
8
+ /**
9
+ * Configuration schema with validation
10
+ */
11
+ export const ConfigSchema = z.object({
12
+ port: z.number().int().min(1024).max(65535).default(5432),
13
+ host: z.string().default("127.0.0.1"),
14
+ dataDir: z.string().optional(),
15
+ logLevel: z
16
+ .enum(["trace", "debug", "info", "warn", "error", "fatal"])
17
+ .default("info"),
18
+ auth: z.object({
19
+ username: z.string().min(3).max(63),
20
+ password: z.string().min(8).optional(),
21
+ // Optional: password hash instead of plaintext
22
+ passwordHash: z.string().optional(),
23
+ }),
24
+ security: z
25
+ .object({
26
+ allowedHosts: z.array(z.string()).default(["127.0.0.1", "localhost"]),
27
+ maxConnections: z.number().int().positive().default(10),
28
+ })
29
+ .optional(),
30
+ });
31
+ /**
32
+ * Hash password securely using scrypt
33
+ */
34
+ export async function hashPassword(password) {
35
+ const salt = randomBytes(16).toString("hex");
36
+ const derivedKey = (await scryptAsync(password, salt, 64));
37
+ return `${salt}:${derivedKey.toString("hex")}`;
38
+ }
39
+ /**
40
+ * Verify password against hash
41
+ */
42
+ export async function verifyPassword(password, hash) {
43
+ try {
44
+ const [salt, key] = hash.split(":");
45
+ if (!salt || !key)
46
+ return false;
47
+ const derivedKey = (await scryptAsync(password, salt, 64));
48
+ return key === derivedKey.toString("hex");
49
+ }
50
+ catch (error) {
51
+ log.error({ error }, "Error verifying password:");
52
+ return false;
53
+ }
54
+ }
55
+ /**
56
+ * Load configuration from multiple sources
57
+ */
58
+ export function loadConfig() {
59
+ const explorer = cosmiconfigSync("locci-db", {
60
+ searchPlaces: [
61
+ "package.json",
62
+ ".locci-db.json",
63
+ ".locci-db.yaml",
64
+ ".locci-db.yml",
65
+ ".locci-db.config.js",
66
+ "locci-db.config.js",
67
+ ],
68
+ });
69
+ const result = explorer.search();
70
+ const config = result?.config || {};
71
+ // Merge with environment variables
72
+ const envConfig = {
73
+ port: process.env.LOCCI_DB_PORT
74
+ ? parseInt(process.env.LOCCI_DB_PORT, 10)
75
+ : undefined,
76
+ host: process.env.LOCCI_DB_HOST,
77
+ dataDir: process.env.LOCCI_DB_DATA_DIR,
78
+ logLevel: process.env.LOCCI_DB_LOG_LEVEL,
79
+ auth: {
80
+ username: process.env.LOCCI_DB_USERNAME,
81
+ password: process.env.LOCCI_DB_PASSWORD,
82
+ passwordHash: process.env.LOCCI_DB_PASSWORD_HASH,
83
+ },
84
+ };
85
+ // defu merges right-to-left by falling priority and skips undefined values,
86
+ // so an env var that is not set falls through to the config file.
87
+ const mergedConfig = defu(envConfig, config);
88
+ // Validate with Zod
89
+ return ConfigSchema.parse(mergedConfig);
90
+ }
@@ -0,0 +1,2 @@
1
+ import net from "node:net";
2
+ export declare function startServer(): Promise<net.Server>;
package/dist/index.js ADDED
@@ -0,0 +1,114 @@
1
+ import { PGlite } from "@electric-sql/pglite";
2
+ import net from "node:net";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ import { dirname } from "node:path";
6
+ import { mkdir } from "node:fs/promises";
7
+ import { fromNodeSocket } from "pg-gateway/node";
8
+ import { log } from "./utils.js";
9
+ import { loadConfig, verifyPassword } from "./config.js";
10
+ import { getEmbeddedPGliteAssets } from "./pglite-assets.js";
11
+ export async function startServer() {
12
+ const config = loadConfig();
13
+ // Secure data directory (user's home by default)
14
+ const dataDir = config.dataDir || join(homedir(), ".locci/db", "data");
15
+ // Ensure parent directory exists
16
+ await mkdir(dirname(dataDir), { recursive: true });
17
+ log.info(`Initializing PGlite database at ${dataDir}`);
18
+ const db = new PGlite({ dataDir, ...getEmbeddedPGliteAssets() });
19
+ let activeConnections = 0;
20
+ const server = net.createServer(async (socket) => {
21
+ const clientAddress = socket.remoteAddress;
22
+ // Connection limiting
23
+ if (activeConnections >= (config.security?.maxConnections || 10)) {
24
+ log.warn(`Connection rejected: max connections reached (${activeConnections})`);
25
+ socket.destroy();
26
+ return;
27
+ }
28
+ // IP filtering
29
+ if (config.security?.allowedHosts &&
30
+ !config.security.allowedHosts.includes(clientAddress || "")) {
31
+ log.warn(`Connection rejected from unauthorized host: ${clientAddress}`);
32
+ socket.destroy();
33
+ return;
34
+ }
35
+ activeConnections++;
36
+ log.info(`Client connected from ${clientAddress} (${activeConnections} active)`);
37
+ await fromNodeSocket(socket, {
38
+ serverVersion: "16.3 (PGlite 0.2.0)",
39
+ auth: {
40
+ method: "password",
41
+ validateCredentials: async function (credentials) {
42
+ if (credentials.clearTextPassword) {
43
+ const isValidUser = credentials.username === config.auth.username;
44
+ // Use password hash if available, otherwise compare plaintext
45
+ let isValidPassword;
46
+ if (config.auth.passwordHash) {
47
+ isValidPassword = await verifyPassword(credentials.password, config.auth.passwordHash);
48
+ }
49
+ else if (config.auth.password) {
50
+ isValidPassword = credentials.password === config.auth.password;
51
+ }
52
+ else {
53
+ isValidPassword = false;
54
+ }
55
+ if (!isValidUser || !isValidPassword) {
56
+ log.warn(`Authentication failed for user: ${credentials.username} from ${clientAddress}`);
57
+ }
58
+ return isValidUser && isValidPassword;
59
+ }
60
+ return false;
61
+ },
62
+ getClearTextPassword: async function (credentials) {
63
+ return credentials.username;
64
+ },
65
+ },
66
+ async onStartup() {
67
+ log.debug("Awaiting database to be ready...");
68
+ await db.waitReady;
69
+ },
70
+ async onMessage(data, { isAuthenticated }) {
71
+ if (!isAuthenticated) {
72
+ log.warn("Unauthenticated message rejected");
73
+ return;
74
+ }
75
+ return await db.execProtocolRaw(data);
76
+ },
77
+ });
78
+ socket.on("end", () => {
79
+ activeConnections--;
80
+ log.info(`Client disconnected from ${clientAddress} (${activeConnections} active)`);
81
+ });
82
+ socket.on("error", (err) => {
83
+ activeConnections--;
84
+ log.error(`Socket error from ${clientAddress}: ${err.message}`);
85
+ });
86
+ });
87
+ server.on("error", (err) => {
88
+ log.error(`Server error: ${err.message}`);
89
+ process.exit(1);
90
+ });
91
+ server.listen(config.port, config.host, async () => {
92
+ log.info(`🚀 Server listening on ${config.host}:${config.port}`);
93
+ log.info(`📁 Data directory: ${dataDir}`);
94
+ log.info(`🔐 Authenticated as: ${config.auth.username}`);
95
+ });
96
+ // Graceful shutdown
97
+ process.on("SIGTERM", () => gracefulShutdown(server, db));
98
+ process.on("SIGINT", () => gracefulShutdown(server, db));
99
+ return server;
100
+ }
101
+ async function gracefulShutdown(server, db) {
102
+ log.info("Shutting down gracefully...");
103
+ server.close(() => {
104
+ log.info("Server closed");
105
+ });
106
+ try {
107
+ await db.close();
108
+ log.info("Database closed");
109
+ }
110
+ catch (error) {
111
+ log.error({ error }, "Error closing database:");
112
+ }
113
+ process.exit(0);
114
+ }
@@ -0,0 +1,10 @@
1
+ import type { PGliteOptions } from "@electric-sql/pglite";
2
+ /**
3
+ * PGlite normally fetches postgres.wasm and postgres.data from disk, relative
4
+ * to its own module URL. That does not work inside a compiled single-file
5
+ * binary, so the Bun entry point embeds both and injects them here before the
6
+ * server starts.
7
+ */
8
+ export type EmbeddedPGliteAssets = Pick<PGliteOptions, "wasmModule" | "fsBundle">;
9
+ export declare function setEmbeddedPGliteAssets(assets: EmbeddedPGliteAssets): void;
10
+ export declare function getEmbeddedPGliteAssets(): EmbeddedPGliteAssets;
@@ -0,0 +1,7 @@
1
+ let embedded = {};
2
+ export function setEmbeddedPGliteAssets(assets) {
3
+ embedded = assets;
4
+ }
5
+ export function getEmbeddedPGliteAssets() {
6
+ return embedded;
7
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Type definitions for @locci/db
3
+ */
4
+ export interface ServerConfig {
5
+ port: number;
6
+ host: string;
7
+ dataDir?: string;
8
+ logLevel: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
9
+ auth: AuthConfig;
10
+ security?: SecurityConfig;
11
+ }
12
+ export interface AuthConfig {
13
+ username: string;
14
+ password?: string;
15
+ passwordHash?: string;
16
+ }
17
+ export interface SecurityConfig {
18
+ allowedHosts?: string[];
19
+ maxConnections?: number;
20
+ }
21
+ export interface ConnectionInfo {
22
+ clientAddress?: string;
23
+ username: string;
24
+ timestamp: Date;
25
+ authenticated: boolean;
26
+ }
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Type definitions for @locci/db
3
+ */
4
+ export {};
@@ -0,0 +1,21 @@
1
+ import pino from 'pino';
2
+ /**
3
+ * Create logger instance
4
+ */
5
+ export declare const log: pino.Logger<never, boolean>;
6
+ /**
7
+ * Format bytes for human-readable output
8
+ */
9
+ export declare function formatBytes(bytes: number): string;
10
+ /**
11
+ * Validate port number
12
+ */
13
+ export declare function isValidPort(port: number): boolean;
14
+ /**
15
+ * Validate hostname
16
+ */
17
+ export declare function isValidHost(host: string): boolean;
18
+ /**
19
+ * Sleep for specified milliseconds
20
+ */
21
+ export declare function sleep(ms: number): Promise<void>;
package/dist/utils.js ADDED
@@ -0,0 +1,41 @@
1
+ import pino from 'pino';
2
+ import pretty from 'pino-pretty';
3
+ /**
4
+ * Create logger instance
5
+ */
6
+ export const log = pino({
7
+ level: process.env.LOCCI_DB_LOG_LEVEL || 'info',
8
+ }, pretty({
9
+ colorize: true,
10
+ singleLine: false,
11
+ translateTime: 'SYS:standard',
12
+ }));
13
+ /**
14
+ * Format bytes for human-readable output
15
+ */
16
+ export function formatBytes(bytes) {
17
+ if (bytes === 0)
18
+ return '0 Bytes';
19
+ const k = 1024;
20
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
21
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
22
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
23
+ }
24
+ /**
25
+ * Validate port number
26
+ */
27
+ export function isValidPort(port) {
28
+ return port >= 1024 && port <= 65535 && Number.isInteger(port);
29
+ }
30
+ /**
31
+ * Validate hostname
32
+ */
33
+ export function isValidHost(host) {
34
+ return /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$|^(::)?1?$|^127\.0\.0\.1$|^localhost$|^::$/.test(host);
35
+ }
36
+ /**
37
+ * Sleep for specified milliseconds
38
+ */
39
+ export function sleep(ms) {
40
+ return new Promise((resolve) => setTimeout(resolve, ms));
41
+ }
@@ -0,0 +1 @@
1
+ export declare const VERSION = "1.0.0-alpha-1";
@@ -0,0 +1,2 @@
1
+ // Generated by scripts/gen-version.mjs. Do not edit.
2
+ export const VERSION = "1.0.0-alpha-1";
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@locci/db",
3
+ "version": "1.0.0-alpha-1",
4
+ "description": "Lightweight PostgreSQL-compatible database server powered by PGlite",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "locci-db": "./bin/locci-db.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "prepare": "git config core.hooksPath .githooks || true",
20
+ "dev": "tsx ./src/cli.ts",
21
+ "start": "node ./dist/cli.js",
22
+ "prepublishOnly": "npm run build",
23
+ "test": "vitest run --project unit",
24
+ "lint": "oxlint --deny-warnings",
25
+ "docker:build": "docker build -t locci/db:latest .",
26
+ "docker:run": "docker run -p 5432:5432 --env-file .env locci/db:latest",
27
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
28
+ "test:unit": "vitest run --project unit",
29
+ "test:e2e": "npm run build && vitest run --project e2e",
30
+ "test:all": "npm run build && vitest run",
31
+ "test:watch": "vitest --project unit",
32
+ "test:coverage": "vitest run --project unit --coverage",
33
+ "lint:fix": "oxlint --fix",
34
+ "prebuild": "node scripts/gen-version.mjs",
35
+ "pretypecheck": "node scripts/gen-version.mjs",
36
+ "compile": "bun build ./src/bun.ts --compile --minify --outfile dist-bin/locci-db",
37
+ "precompile": "node scripts/gen-version.mjs"
38
+ },
39
+ "keywords": [
40
+ "postgresql",
41
+ "pglite",
42
+ "database",
43
+ "cli",
44
+ "postgres",
45
+ "embedded-database"
46
+ ],
47
+ "author": "MikeTeddyOmondi <contact@miketeddyomondi.dev>",
48
+ "license": "MIT",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "https://github.com/MikeTeddyOmondi/locci-db.git"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/MikeTeddyOmondi/locci-db/issues"
55
+ },
56
+ "engines": {
57
+ "node": ">=20.0.0"
58
+ },
59
+ "files": [
60
+ "dist",
61
+ "bin",
62
+ "README.md",
63
+ "LICENSE"
64
+ ],
65
+ "dependencies": {
66
+ "@electric-sql/pglite": "^0.2.9",
67
+ "commander": "^12.0.0",
68
+ "cosmiconfig": "^9.0.0",
69
+ "defu": "^6.1.7",
70
+ "dotenv": "^17.4.2",
71
+ "pg-gateway": "0.3.0-beta.3",
72
+ "pino": "^9.5.0",
73
+ "pino-pretty": "^11.3.0",
74
+ "zod": "^3.22.4"
75
+ },
76
+ "devDependencies": {
77
+ "@types/node": "^22.20.1",
78
+ "@vitest/coverage-v8": "^5.0.0",
79
+ "oxlint": "^1.82.0",
80
+ "tsx": "^4.16.2",
81
+ "typescript": "^5.5.3",
82
+ "vitest": "^5.0.0"
83
+ }
84
+ }