@naviyra/cloud-panel 0.0.0-stage → 0.0.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.
Files changed (55) hide show
  1. package/.env.example +40 -0
  2. package/README.md +134 -2
  3. package/apps/agent/package.json +13 -0
  4. package/apps/agent/src/cloudflare.mjs +733 -0
  5. package/apps/agent/src/native-worker.mjs +52 -0
  6. package/apps/agent/src/native.mjs +457 -0
  7. package/apps/agent/src/runtime.mjs +760 -0
  8. package/apps/agent/src/server.mjs +195 -0
  9. package/apps/agent/src/static-server.mjs +53 -0
  10. package/apps/panel/AGENTS.md +9 -0
  11. package/apps/panel/CLAUDE.md +1 -0
  12. package/apps/panel/app/api/[...path]/route.js +445 -0
  13. package/apps/panel/app/globals.css +2057 -0
  14. package/apps/panel/app/icon.png +0 -0
  15. package/apps/panel/app/layout.jsx +18 -0
  16. package/apps/panel/app/login/page.jsx +108 -0
  17. package/apps/panel/app/page.jsx +33 -0
  18. package/apps/panel/app/ui/cloudflare.jsx +926 -0
  19. package/apps/panel/app/ui/dashboard.jsx +1487 -0
  20. package/apps/panel/app/ui/databases.jsx +293 -0
  21. package/apps/panel/app/ui/skeleton.jsx +196 -0
  22. package/apps/panel/app/ui/theme.jsx +35 -0
  23. package/apps/panel/lib/agent.mjs +83 -0
  24. package/apps/panel/lib/auth.mjs +48 -0
  25. package/apps/panel/lib/database-runtime.mjs +243 -0
  26. package/apps/panel/lib/databases.mjs +292 -0
  27. package/apps/panel/lib/db.mjs +6 -0
  28. package/apps/panel/next.config.mjs +8 -0
  29. package/apps/panel/package.json +22 -0
  30. package/apps/panel/public/naviyra-logo.png +0 -0
  31. package/bin/naviyra.mjs +177 -0
  32. package/db/001_init.sql +28 -0
  33. package/db/002_native_runtimes.sql +3 -0
  34. package/db/003_databases.sql +8 -0
  35. package/db/004_database_engines.sql +4 -0
  36. package/deploy/naviyra-agent.service +17 -0
  37. package/docker-compose.yml +18 -0
  38. package/docs/managed-runtime.md +52 -0
  39. package/docs/native-hosts.md +48 -0
  40. package/package.json +61 -4
  41. package/packages/runtime/artifacts.json +32 -0
  42. package/packages/runtime/index.cjs +526 -0
  43. package/packages/runtime/index.mjs +10 -0
  44. package/packages/runtime/package.json +6 -0
  45. package/packages/shared/index.mjs +85 -0
  46. package/packages/shared/package.json +9 -0
  47. package/scripts/migrate.mjs +18 -0
  48. package/scripts/postgres-local.mjs +97 -0
  49. package/scripts/runtime-download-manifest.mjs +30 -0
  50. package/scripts/runtime.mjs +20 -0
  51. package/scripts/seed.mjs +35 -0
  52. package/scripts/service-supervisor.mjs +164 -0
  53. package/scripts/setup-local.mjs +215 -0
  54. package/scripts/start-naviyra.mjs +303 -0
  55. package/scripts/windows-boot.mjs +259 -0
@@ -0,0 +1,243 @@
1
+ import { managedRuntime } from "@naviyra/runtime";
2
+ import { randomBytes } from "node:crypto";
3
+ import { setTimeout as delay } from "node:timers/promises";
4
+
5
+ export const databaseEngines = Object.freeze({
6
+ postgresql: { label: "PostgreSQL", image: "postgres:17", port: 5432 },
7
+ mysql: { label: "MySQL", image: "mysql:8.4", port: 3306 },
8
+ mariadb: { label: "MariaDB", image: "mariadb:11.4", port: 3306 },
9
+ });
10
+ const failure = (message) => Object.assign(Error(message), { status: 503 });
11
+ export function databaseEngine(value) {
12
+ if (typeof value !== "string" || !Object.hasOwn(databaseEngines, value))
13
+ throw Object.assign(Error("Choose PostgreSQL, MySQL, or MariaDB"), {
14
+ status: 400,
15
+ });
16
+ return databaseEngines[value];
17
+ }
18
+
19
+ // Never include command arguments or database output in public errors: they can contain secrets.
20
+ function docker(args, options) {
21
+ return managedRuntime().run(args, options);
22
+ }
23
+
24
+ export function createDatabaseRuntime(run = docker, sleep = delay) {
25
+ async function inspect(id) {
26
+ try {
27
+ return JSON.parse(await run(["inspect", id]))[0];
28
+ } catch {
29
+ throw failure("Could not inspect the managed database");
30
+ }
31
+ }
32
+ async function owned(row) {
33
+ if (!/^naviyra-db-[0-9a-f-]{36}$/.test(row.runtime_id || ""))
34
+ throw failure("Invalid managed database identity");
35
+ const info = await inspect(row.runtime_id);
36
+ if (info?.Config?.Labels?.["naviyra.database"] !== row.id)
37
+ throw failure("Managed database ownership does not match");
38
+ return info;
39
+ }
40
+ async function sql(row, statement) {
41
+ databaseEngine(row.engine);
42
+ const args =
43
+ row.engine === "postgresql"
44
+ ? ["psql", "-U", "postgres", "-d", "postgres", "-v", "ON_ERROR_STOP=1"]
45
+ : [
46
+ "sh",
47
+ "-c",
48
+ row.engine === "mysql"
49
+ ? 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot --protocol=socket'
50
+ : 'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" exec mariadb -uroot --protocol=socket',
51
+ ];
52
+ return run(["exec", "-i", row.runtime_id, ...args], { input: statement });
53
+ }
54
+ return {
55
+ async provision({ id, engine, physical, password }) {
56
+ const spec = databaseEngine(engine);
57
+ if (
58
+ !/^nv_[a-z][a-z0-9_]{1,24}$/.test(physical) ||
59
+ !/^[A-Za-z0-9_-]+$/.test(password) ||
60
+ !/^[0-9a-f-]{36}$/.test(id)
61
+ )
62
+ throw failure("Invalid database configuration");
63
+ try {
64
+ if ((await run(["info", "--format", "{{.OSType}}"])) !== "linux")
65
+ throw Error();
66
+ } catch (error) {
67
+ throw error.status
68
+ ? error
69
+ : failure(
70
+ "Naviyra's managed Docker runtime is unavailable. Run npm run runtime:setup on the server.",
71
+ );
72
+ }
73
+ try {
74
+ await run(["network", "inspect", "naviyra-databases"]);
75
+ } catch {
76
+ try {
77
+ await run([
78
+ "network",
79
+ "create",
80
+ "--label",
81
+ "naviyra.managed=true",
82
+ "naviyra-databases",
83
+ ]);
84
+ } catch {
85
+ await run(["network", "inspect", "naviyra-databases"]);
86
+ }
87
+ }
88
+ try {
89
+ await run(["image", "inspect", spec.image]);
90
+ } catch {
91
+ try {
92
+ await run(["pull", spec.image], { timeout: 600000 });
93
+ } catch {
94
+ throw failure(
95
+ `Could not download ${spec.label}. Check Docker and its registry network access, then retry.`,
96
+ );
97
+ }
98
+ }
99
+ const row = {
100
+ id,
101
+ engine,
102
+ runtime_id: `naviyra-db-${id}`,
103
+ db_user: physical,
104
+ };
105
+ const adminPassword = randomBytes(24).toString("base64url");
106
+ const prefix = engine === "mariadb" ? "MARIADB" : "MYSQL";
107
+ const env =
108
+ engine === "postgresql"
109
+ ? {
110
+ POSTGRES_PASSWORD: adminPassword,
111
+ POSTGRES_INITDB_ARGS:
112
+ "--auth-host=scram-sha-256 --auth-local=trust",
113
+ }
114
+ : {
115
+ [`${prefix}_ROOT_PASSWORD`]: adminPassword,
116
+ [`${prefix}_DATABASE`]: physical,
117
+ [`${prefix}_USER`]: physical,
118
+ [`${prefix}_PASSWORD`]: password,
119
+ };
120
+ // Create separately so cleanup can never remove a pre-existing container on name collision.
121
+ await run(
122
+ [
123
+ "create",
124
+ "--name",
125
+ row.runtime_id,
126
+ "--label",
127
+ `naviyra.database=${id}`,
128
+ "--restart",
129
+ "unless-stopped",
130
+ "--network",
131
+ "naviyra-databases",
132
+ "-p",
133
+ `127.0.0.1::${spec.port}`,
134
+ ...Object.keys(env).flatMap((key) => ["-e", key]),
135
+ spec.image,
136
+ ],
137
+ { env },
138
+ );
139
+ try {
140
+ await run(["start", row.runtime_id]);
141
+ let ready = false;
142
+ for (let attempt = 0; attempt < 90; attempt++) {
143
+ try {
144
+ // TCP probes avoid accepting the temporary socket-only initialization server.
145
+ if (engine === "postgresql")
146
+ await run([
147
+ "exec",
148
+ row.runtime_id,
149
+ "pg_isready",
150
+ "-h",
151
+ "127.0.0.1",
152
+ "-U",
153
+ "postgres",
154
+ ]);
155
+ else
156
+ await run([
157
+ "exec",
158
+ row.runtime_id,
159
+ "sh",
160
+ "-c",
161
+ engine === "mysql"
162
+ ? 'MYSQL_PWD="$MYSQL_PASSWORD" exec mysql -h127.0.0.1 -u"$MYSQL_USER" -D"$MYSQL_DATABASE" -e "SELECT 1"'
163
+ : 'MYSQL_PWD="$MARIADB_PASSWORD" exec mariadb -h127.0.0.1 -u"$MARIADB_USER" -D"$MARIADB_DATABASE" -e "SELECT 1"',
164
+ ]);
165
+ ready = true;
166
+ break;
167
+ } catch {
168
+ await sleep(2000);
169
+ }
170
+ }
171
+ if (!ready)
172
+ throw failure(
173
+ `${spec.label} did not become ready. Check server resources and retry.`,
174
+ );
175
+ if (engine === "postgresql") {
176
+ await sql(
177
+ row,
178
+ `CREATE ROLE "${physical}" LOGIN PASSWORD '${password}';\nCREATE DATABASE "${physical}" OWNER "${physical}";\nREVOKE ALL ON DATABASE "${physical}" FROM PUBLIC;\n`,
179
+ );
180
+ await run(
181
+ [
182
+ "exec",
183
+ "-e",
184
+ "PGPASSWORD",
185
+ row.runtime_id,
186
+ "psql",
187
+ "-h",
188
+ "127.0.0.1",
189
+ "-U",
190
+ physical,
191
+ "-d",
192
+ physical,
193
+ "-v",
194
+ "ON_ERROR_STOP=1",
195
+ "-c",
196
+ "SELECT 1",
197
+ ],
198
+ { env: { PGPASSWORD: password } },
199
+ );
200
+ }
201
+ const info = await owned(row);
202
+ const binding = info.NetworkSettings?.Ports?.[`${spec.port}/tcp`]?.[0];
203
+ const port = Number(binding?.HostPort);
204
+ if (
205
+ binding?.HostIp !== "127.0.0.1" ||
206
+ !Number.isInteger(port) ||
207
+ port < 1 ||
208
+ port > 65535
209
+ )
210
+ throw failure("Database did not receive a private port");
211
+ return { runtime_id: row.runtime_id, host: "127.0.0.1", port };
212
+ } catch (error) {
213
+ try {
214
+ await run(["rm", "-f", "-v", row.runtime_id]);
215
+ } catch {
216
+ throw failure(
217
+ `Database setup failed; remove the incomplete container ${row.runtime_id} on the panel server before retrying.`,
218
+ );
219
+ }
220
+ throw error;
221
+ }
222
+ },
223
+ async reset(row, password) {
224
+ await owned(row);
225
+ if (
226
+ !/^nv_[a-z][a-z0-9_]{1,24}$/.test(row.db_user) ||
227
+ !/^[A-Za-z0-9_-]+$/.test(password)
228
+ )
229
+ throw failure("Invalid database credentials");
230
+ await sql(
231
+ row,
232
+ row.engine === "postgresql"
233
+ ? `ALTER ROLE "${row.db_user}" PASSWORD '${password}';`
234
+ : `ALTER USER '${row.db_user}'@'%' IDENTIFIED BY '${password}';`,
235
+ );
236
+ },
237
+ async remove(row) {
238
+ await owned(row);
239
+ await run(["rm", "-f", "-v", row.runtime_id]);
240
+ },
241
+ };
242
+ }
243
+ export const databaseRuntime = createDatabaseRuntime();
@@ -0,0 +1,292 @@
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import pg from "pg";
3
+ import { db } from "./db.mjs";
4
+ import {
5
+ databaseEngine,
6
+ databaseEngines,
7
+ databaseRuntime,
8
+ } from "./database-runtime.mjs";
9
+
10
+ const NAME = /^[a-z][a-z0-9_]{1,24}$/;
11
+ const IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
12
+
13
+ function fail(message, status) {
14
+ return Object.assign(Error(message), { status });
15
+ }
16
+
17
+ function connectionParts() {
18
+ let url;
19
+ try {
20
+ url = new URL(process.env.DATABASE_URL || "");
21
+ } catch {
22
+ throw fail("Database server is not available", 500);
23
+ }
24
+ if (!["localhost", "127.0.0.1"].includes(url.hostname))
25
+ throw fail(
26
+ "Databases can only be created on the local PostgreSQL server",
27
+ 400,
28
+ );
29
+ const user = decodeURIComponent(url.username);
30
+ const database = decodeURIComponent(url.pathname.replace(/^\//, ""));
31
+ const port = Number(url.port || 5432);
32
+ if (!IDENT.test(user) || !IDENT.test(database) || !port)
33
+ throw fail("Database server is not available", 500);
34
+ return { host: "127.0.0.1", port, user, database, url };
35
+ }
36
+
37
+ export function databaseName(value, native = true) {
38
+ const name = String(value || "")
39
+ .trim()
40
+ .toLowerCase();
41
+ if (!NAME.test(name))
42
+ throw fail(
43
+ "Use 2–25 characters: lowercase letters, numbers, and underscores",
44
+ 400,
45
+ );
46
+ const physical = `nv_${name}`;
47
+ if (native && process.env.DATABASE_URL) {
48
+ const { database } = connectionParts();
49
+ if (["postgres", "template0", "template1", database].includes(physical))
50
+ throw fail("Choose a different database name", 400);
51
+ }
52
+ return { name, physical };
53
+ }
54
+
55
+ async function withAdmin(run) {
56
+ const { url } = connectionParts();
57
+ const admin = new URL(url.href);
58
+ admin.pathname = "/postgres";
59
+ const client = new pg.Client({ connectionString: admin.href });
60
+ await client.connect();
61
+ try {
62
+ await client.query("SET statement_timeout = '15s'");
63
+ return await run(client);
64
+ } finally {
65
+ await client.end();
66
+ }
67
+ }
68
+
69
+ async function runFormat(client, template, values) {
70
+ let statement;
71
+ try {
72
+ const placeholders = values
73
+ .map((_, index) => `$${index + 2}::text`)
74
+ .join(", ");
75
+ const { rows } = await client.query(
76
+ `SELECT format($1::text, ${placeholders}) AS statement`,
77
+ [template, ...values],
78
+ );
79
+ statement = rows[0]?.statement;
80
+ } catch {
81
+ throw fail("Could not update the database", 500);
82
+ }
83
+ if (typeof statement !== "string" || statement.length > 400)
84
+ throw fail("Could not update the database", 500);
85
+ try {
86
+ await client.query(statement);
87
+ } catch (error) {
88
+ if (error.code === "42710" || error.code === "42P04")
89
+ throw fail("A database with that name already exists", 409);
90
+ throw fail("Could not update the database", 500);
91
+ }
92
+ }
93
+
94
+ async function isolate(client) {
95
+ const { user, database } = connectionParts();
96
+ for (const name of [database, "postgres"]) {
97
+ await runFormat(client, "REVOKE CONNECT ON DATABASE %I FROM PUBLIC", [
98
+ name,
99
+ ]);
100
+ await runFormat(client, "GRANT CONNECT ON DATABASE %I TO %I", [name, user]);
101
+ }
102
+ }
103
+
104
+ async function dropPhysical(client, dbName, dbUser) {
105
+ await client.query(
106
+ "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname=$1 AND pid <> pg_backend_pid()",
107
+ [dbName],
108
+ );
109
+ await runFormat(client, "DROP DATABASE IF EXISTS %I", [dbName]);
110
+ await runFormat(client, "DROP ROLE IF EXISTS %I", [dbUser]);
111
+ }
112
+
113
+ function view(row, parts, password) {
114
+ return {
115
+ id: row.id,
116
+ name: row.name,
117
+ database: row.db_name,
118
+ user: row.db_user,
119
+ engine: row.engine || "postgresql",
120
+ host: row.host || parts.host,
121
+ port: row.port || parts.port,
122
+ created_at: row.created_at,
123
+ ...(password ? { password } : {}),
124
+ };
125
+ }
126
+
127
+ export async function listDatabases(userId) {
128
+ const rows = (
129
+ await db.query(
130
+ "SELECT * FROM app_databases WHERE user_id=$1 ORDER BY created_at DESC",
131
+ [userId],
132
+ )
133
+ ).rows;
134
+ return {
135
+ engines: Object.entries(databaseEngines).map(([value, spec]) => ({
136
+ value,
137
+ label: spec.label,
138
+ })),
139
+ databases: rows.map((row) =>
140
+ view(row, row.runtime_id ? row : connectionParts()),
141
+ ),
142
+ };
143
+ }
144
+
145
+ export async function createDatabase(userId, requested, engine) {
146
+ // Requests from older clients retain native PostgreSQL behavior.
147
+ if (engine !== undefined)
148
+ return createManagedDatabase(userId, requested, engine);
149
+ const { name, physical } = databaseName(requested);
150
+ const parts = connectionParts();
151
+ const password = randomBytes(18).toString("base64url");
152
+ const id = randomUUID();
153
+ await withAdmin(async (client) => {
154
+ await isolate(client);
155
+ let roleCreated = false;
156
+ let databaseCreated = false;
157
+ try {
158
+ await runFormat(client, "CREATE ROLE %I LOGIN PASSWORD %L", [
159
+ physical,
160
+ password,
161
+ ]);
162
+ roleCreated = true;
163
+ await runFormat(client, "CREATE DATABASE %I OWNER %I", [
164
+ physical,
165
+ physical,
166
+ ]);
167
+ databaseCreated = true;
168
+ await runFormat(client, "REVOKE ALL ON DATABASE %I FROM PUBLIC", [
169
+ physical,
170
+ ]);
171
+ await runFormat(client, "GRANT ALL PRIVILEGES ON DATABASE %I TO %I", [
172
+ physical,
173
+ physical,
174
+ ]);
175
+ } catch (error) {
176
+ try {
177
+ if (databaseCreated) await dropPhysical(client, physical, physical);
178
+ else if (roleCreated)
179
+ await runFormat(client, "DROP ROLE %I", [physical]);
180
+ } catch {}
181
+ throw error;
182
+ }
183
+ });
184
+ try {
185
+ const probeUrl = new URL(parts.url.href);
186
+ probeUrl.username = physical;
187
+ probeUrl.password = password;
188
+ probeUrl.pathname = `/${physical}`;
189
+ const probe = new pg.Client({ connectionString: probeUrl.href });
190
+ await probe.connect();
191
+ try {
192
+ await probe.query("SELECT 1");
193
+ } finally {
194
+ await probe.end();
195
+ }
196
+ const row = (
197
+ await db.query(
198
+ "INSERT INTO app_databases(id,user_id,name,db_name,db_user) VALUES($1,$2,$3,$4,$4) RETURNING id, name, db_name, db_user, created_at",
199
+ [id, userId, name, physical],
200
+ )
201
+ ).rows[0];
202
+ return view(row, parts, password);
203
+ } catch (error) {
204
+ await withAdmin((client) => dropPhysical(client, physical, physical));
205
+ if (error.code === "23505")
206
+ throw fail("A database with that name already exists", 409);
207
+ throw error.status ? error : fail("Could not create the database", 500);
208
+ }
209
+ }
210
+
211
+ async function createManagedDatabase(userId, requested, engine) {
212
+ databaseEngine(engine);
213
+ const { name, physical } = databaseName(requested, false);
214
+ if (
215
+ (await db.query("SELECT 1 FROM app_databases WHERE name=$1", [name]))
216
+ .rowCount
217
+ )
218
+ throw fail("A database with that name already exists", 409);
219
+ const id = randomUUID();
220
+ const password = randomBytes(18).toString("base64url");
221
+ const runtime = await databaseRuntime.provision({
222
+ id,
223
+ engine,
224
+ physical,
225
+ password,
226
+ });
227
+ try {
228
+ const row = (
229
+ await db.query(
230
+ "INSERT INTO app_databases(id,user_id,name,db_name,db_user,engine,runtime_id,host,port) VALUES($1,$2,$3,$4,$4,$5,$6,$7,$8) RETURNING *",
231
+ [
232
+ id,
233
+ userId,
234
+ name,
235
+ physical,
236
+ engine,
237
+ runtime.runtime_id,
238
+ runtime.host,
239
+ runtime.port,
240
+ ],
241
+ )
242
+ ).rows[0];
243
+ return view(row, runtime, password);
244
+ } catch (error) {
245
+ await databaseRuntime.remove({ id, engine, ...runtime });
246
+ if (error.code === "23505")
247
+ throw fail("A database with that name already exists", 409);
248
+ throw fail("Could not save the database", 500);
249
+ }
250
+ }
251
+
252
+ async function owned(userId, id) {
253
+ const row = (
254
+ await db.query("SELECT * FROM app_databases WHERE id=$1 AND user_id=$2", [
255
+ id,
256
+ userId,
257
+ ])
258
+ ).rows[0];
259
+ if (!row) throw fail("Database not found", 404);
260
+ if (!row.db_name.startsWith("nv_") || row.db_name !== row.db_user)
261
+ throw fail("Could not update the database", 500);
262
+ return row;
263
+ }
264
+
265
+ export async function resetDatabasePassword(userId, id) {
266
+ const row = await owned(userId, id);
267
+ const password = randomBytes(18).toString("base64url");
268
+ if (row.runtime_id) {
269
+ await databaseRuntime.reset(row, password);
270
+ return view(row, row, password);
271
+ }
272
+ const parts = connectionParts();
273
+ await withAdmin((client) =>
274
+ runFormat(client, "ALTER ROLE %I WITH PASSWORD %L", [
275
+ row.db_user,
276
+ password,
277
+ ]),
278
+ );
279
+ return view(row, parts, password);
280
+ }
281
+
282
+ export async function deleteDatabase(userId, id) {
283
+ const row = await owned(userId, id);
284
+ if (row.runtime_id) await databaseRuntime.remove(row);
285
+ else
286
+ await withAdmin((client) => dropPhysical(client, row.db_name, row.db_user));
287
+ await db.query("DELETE FROM app_databases WHERE id=$1 AND user_id=$2", [
288
+ id,
289
+ userId,
290
+ ]);
291
+ return { ok: true };
292
+ }
@@ -0,0 +1,6 @@
1
+ import pg from "pg";
2
+ const globalDb = globalThis;
3
+ export const db =
4
+ globalDb.naviyraDb ||
5
+ new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 10 });
6
+ if (process.env.NODE_ENV !== "production") globalDb.naviyraDb = db;
@@ -0,0 +1,8 @@
1
+ import dotenv from "dotenv";
2
+ import path from "node:path";
3
+ dotenv.config({ path: path.resolve(process.cwd(), "../../.env"), quiet: true });
4
+ export default {
5
+ poweredByHeader: false,
6
+ devIndicators: false,
7
+ serverExternalPackages: ["pg", "bcryptjs", "@naviyra/runtime"],
8
+ };
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@naviyra/panel",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "next dev --hostname 127.0.0.1",
8
+ "build": "next build",
9
+ "start": "next start --hostname 127.0.0.1"
10
+ },
11
+ "dependencies": {
12
+ "@naviyra/runtime": "1.0.0",
13
+ "@naviyra/shared": "1.0.0",
14
+ "bcryptjs": "^3.0.2",
15
+ "jose": "^6.1.0",
16
+ "lucide-react": "^0.468.0",
17
+ "next": "^16.0.0",
18
+ "pg": "^8.16.3",
19
+ "react": "^19.2.0",
20
+ "react-dom": "^19.2.0"
21
+ }
22
+ }