@naviyra/cloud-panel 0.0.0-stage → 0.0.2

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 (56) hide show
  1. package/.env.example +40 -0
  2. package/README.md +145 -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 +215 -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/panel-control.mjs +195 -0
  49. package/scripts/postgres-local.mjs +97 -0
  50. package/scripts/runtime-download-manifest.mjs +30 -0
  51. package/scripts/runtime.mjs +20 -0
  52. package/scripts/seed.mjs +35 -0
  53. package/scripts/service-supervisor.mjs +164 -0
  54. package/scripts/setup-local.mjs +215 -0
  55. package/scripts/start-naviyra.mjs +303 -0
  56. package/scripts/windows-boot.mjs +259 -0
@@ -0,0 +1,195 @@
1
+ import http from "node:http";
2
+ import { timingSafeEqual, createHash } from "node:crypto";
3
+ import { pathToFileURL } from "node:url";
4
+ import { Runtime } from "./runtime.mjs";
5
+ import {
6
+ projectInput,
7
+ idSchema,
8
+ deploymentInput,
9
+ MAX_ZIP_BYTES,
10
+ } from "@naviyra/shared";
11
+ import {
12
+ cloudflareStatus,
13
+ startCloudflareLogin,
14
+ cloudflareLoginStatus,
15
+ startCloudflareApiLogin,
16
+ cloudflareApiLoginStatus,
17
+ listDns,
18
+ getDnsRecord,
19
+ updateDnsRecord,
20
+ emailOverview,
21
+ setEmailRouting,
22
+ createEmailRule,
23
+ updateEmailRule,
24
+ deleteEmailRule,
25
+ addDestination,
26
+ deleteDestination,
27
+ } from "./cloudflare.mjs";
28
+
29
+ export function createServer(runtime, token) {
30
+ if (!token || token.length < 32)
31
+ throw Error("AGENT_TOKEN must contain at least 32 characters");
32
+ const digest = (value) => createHash("sha256").update(value).digest();
33
+ return http.createServer(async (req, res) => {
34
+ const send = (status, data) => {
35
+ res.writeHead(status, {
36
+ "Content-Type": "application/json",
37
+ "Cache-Control": "no-store",
38
+ });
39
+ res.end(JSON.stringify(data));
40
+ };
41
+ try {
42
+ if (
43
+ !timingSafeEqual(
44
+ digest(req.headers.authorization || ""),
45
+ digest(`Bearer ${token}`),
46
+ )
47
+ )
48
+ return send(401, { error: "Unauthorized" });
49
+ const url = new URL(req.url, "http://localhost");
50
+ if (req.method === "GET" && url.pathname === "/health")
51
+ return send(200, { ok: true, mode: runtime.mode });
52
+ if (req.method === "GET" && url.pathname === "/system/metrics")
53
+ return send(200, runtime.systemMetrics());
54
+ if (url.pathname.startsWith("/cloudflare")) {
55
+ const readJson = async () => {
56
+ let size = 0;
57
+ const chunks = [];
58
+ for await (const chunk of req) {
59
+ size += chunk.length;
60
+ if (size > 8192) {
61
+ const error = Error("Payload too large");
62
+ error.status = 413;
63
+ throw error;
64
+ }
65
+ chunks.push(chunk);
66
+ }
67
+ const text = Buffer.concat(chunks).toString();
68
+ return text ? JSON.parse(text) : {};
69
+ };
70
+ const cf = url.pathname.match(
71
+ /^\/cloudflare\/(?:(status|login|api-login|dns|email)|dns\/([a-f0-9]{32})|email\/(routing|rules|addresses)(?:\/([a-f0-9]{32}))?)$/i,
72
+ );
73
+ if (!cf) return send(404, { error: "Not found" });
74
+ const [, root, dnsId, emailAction, emailId] = cf;
75
+ if (req.method === "GET" && root === "status")
76
+ return send(200, await cloudflareStatus());
77
+ if (req.method === "GET" && root === "login")
78
+ return send(200, cloudflareLoginStatus());
79
+ if (req.method === "POST" && root === "login") {
80
+ await readJson();
81
+ return send(202, await startCloudflareLogin());
82
+ }
83
+ if (req.method === "GET" && root === "api-login")
84
+ return send(200, cloudflareApiLoginStatus());
85
+ if (req.method === "POST" && root === "api-login") {
86
+ await readJson();
87
+ return send(202, await startCloudflareApiLogin());
88
+ }
89
+ if (req.method === "GET" && root === "dns")
90
+ return send(200, await listDns());
91
+ if (req.method === "GET" && dnsId)
92
+ return send(200, await getDnsRecord(dnsId));
93
+ if (req.method === "PUT" && dnsId)
94
+ return send(200, await updateDnsRecord(dnsId, await readJson()));
95
+ if (req.method === "GET" && root === "email")
96
+ return send(200, await emailOverview());
97
+ if (req.method === "POST" && emailAction === "routing")
98
+ return send(
99
+ 200,
100
+ await setEmailRouting(Boolean((await readJson()).enabled)),
101
+ );
102
+ if (req.method === "POST" && emailAction === "rules" && !emailId)
103
+ return send(201, await createEmailRule(await readJson()));
104
+ if (req.method === "PUT" && emailAction === "rules" && emailId)
105
+ return send(200, await updateEmailRule(emailId, await readJson()));
106
+ if (req.method === "DELETE" && emailAction === "rules" && emailId)
107
+ return send(
108
+ 200,
109
+ await deleteEmailRule(emailId, url.searchParams.get("domain") || ""),
110
+ );
111
+ if (req.method === "POST" && emailAction === "addresses" && !emailId)
112
+ return send(201, await addDestination((await readJson()).email));
113
+ if (req.method === "DELETE" && emailAction === "addresses" && emailId)
114
+ return send(200, await deleteDestination(emailId));
115
+ return send(405, { error: "Method not allowed" });
116
+ }
117
+ const match = url.pathname.match(
118
+ /^\/projects(?:\/([^/]+))?(?:\/(deploy|source|restart|logs|metrics))?$/,
119
+ );
120
+ if (!match) return send(404, { error: "Not found" });
121
+ const id = match[1] ? idSchema.parse(match[1]) : null;
122
+ const action = match[2];
123
+ if (req.method === "GET" && id) {
124
+ if (action === "logs") return send(200, await runtime.logs(id));
125
+ if (action === "metrics") return send(200, await runtime.metrics(id));
126
+ if (!action) return send(200, runtime.get(id));
127
+ }
128
+ let size = 0;
129
+ const chunks = [];
130
+ const limit = action === "source" ? MAX_ZIP_BYTES : 8192;
131
+ for await (const chunk of req) {
132
+ size += chunk.length;
133
+ if (size > limit) return send(413, { error: "Payload too large" });
134
+ chunks.push(chunk);
135
+ }
136
+ const body = Buffer.concat(chunks);
137
+ if (req.method === "POST" && !id) {
138
+ const input = JSON.parse(body.toString());
139
+ return send(
140
+ 201,
141
+ await runtime.serial(() =>
142
+ runtime.create(idSchema.parse(input.id), projectInput.parse(input)),
143
+ ),
144
+ );
145
+ }
146
+ if (req.method === "PUT" && action === "source") {
147
+ await runtime.serial(() => runtime.upload(id, body));
148
+ return send(200, { ok: true });
149
+ }
150
+ if (req.method === "POST" && action === "deploy") {
151
+ const input = deploymentInput.parse(JSON.parse(body.toString()));
152
+ return send(
153
+ 202,
154
+ await runtime.serial(() => runtime.deploy(id, input.deployment_id)),
155
+ );
156
+ }
157
+ if (req.method === "POST" && action === "restart")
158
+ return send(200, await runtime.serial(() => runtime.restart(id)));
159
+ if (req.method === "DELETE" && id && !action) {
160
+ await runtime.serial(() => runtime.remove(id));
161
+ return send(200, { ok: true });
162
+ }
163
+ return send(405, { error: "Method not allowed" });
164
+ } catch (error) {
165
+ send(
166
+ error.status ||
167
+ (error.name === "ZodError" || error instanceof SyntaxError
168
+ ? 400
169
+ : 500),
170
+ { error: error.message },
171
+ );
172
+ }
173
+ });
174
+ }
175
+ if (
176
+ process.argv[1] &&
177
+ import.meta.url === pathToFileURL(process.argv[1]).href
178
+ ) {
179
+ const runtime = await new Runtime({
180
+ root: process.env.AGENT_ROOT || ".runtime",
181
+ mode: process.env.AGENT_MODE || "mock",
182
+ portMin: Number(process.env.PORT_MIN || 3100),
183
+ portMax: Number(process.env.PORT_MAX || 3999),
184
+ }).init();
185
+ for (const signal of ["SIGINT", "SIGTERM"])
186
+ process.once(signal, async () => {
187
+ await runtime.supervisor.shutdown();
188
+ process.exit(0);
189
+ });
190
+ createServer(runtime, process.env.AGENT_TOKEN).listen(
191
+ Number(process.env.AGENT_PORT || 4001),
192
+ process.env.AGENT_HOST || "127.0.0.1",
193
+ () => console.log(`Naviyra agent listening (${runtime.mode})`),
194
+ );
195
+ }
@@ -0,0 +1,53 @@
1
+ import http from "node:http";
2
+ import { realpath, stat, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ const root = await realpath(process.argv[2]);
5
+ const types = {
6
+ ".html": "text/html; charset=utf-8",
7
+ ".js": "text/javascript",
8
+ ".css": "text/css",
9
+ ".json": "application/json",
10
+ ".svg": "image/svg+xml",
11
+ ".png": "image/png",
12
+ ".jpg": "image/jpeg",
13
+ ".ico": "image/x-icon",
14
+ ".woff2": "font/woff2",
15
+ ".txt": "text/plain",
16
+ ".webp": "image/webp",
17
+ };
18
+ http
19
+ .createServer(async (req, res) => {
20
+ try {
21
+ if (!["GET", "HEAD"].includes(req.method)) {
22
+ res.writeHead(405);
23
+ res.end();
24
+ return;
25
+ }
26
+ const requested = decodeURIComponent(
27
+ new URL(req.url, "http://localhost").pathname,
28
+ );
29
+ if (
30
+ requested.split("/").some((part) => part.startsWith(".")) ||
31
+ requested.includes("\\") ||
32
+ requested.includes("\0")
33
+ )
34
+ throw Error();
35
+ let file = await realpath(path.join(root, requested));
36
+ let relative = path.relative(root, file);
37
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw Error();
38
+ if ((await stat(file)).isDirectory())
39
+ file = await realpath(path.join(file, "index.html"));
40
+ relative = path.relative(root, file);
41
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw Error();
42
+ const data = await readFile(file);
43
+ res.writeHead(200, {
44
+ "Content-Type": types[path.extname(file)] || "application/octet-stream",
45
+ "X-Content-Type-Options": "nosniff",
46
+ });
47
+ res.end(req.method === "HEAD" ? undefined : data);
48
+ } catch {
49
+ res.writeHead(404);
50
+ res.end("Not found");
51
+ }
52
+ })
53
+ .listen(Number(process.env.PORT), "127.0.0.1");
@@ -0,0 +1,9 @@
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+
3
+ # This is NOT the Next.js you know
4
+
5
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
6
+
7
+ This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
8
+
9
+ <!-- END:nextjs-agent-rules -->
@@ -0,0 +1 @@
1
+ @AGENTS.md