create-planet 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.
Files changed (35) hide show
  1. package/index.js +235 -0
  2. package/package.json +20 -0
  3. package/template/.claude/settings.json +5 -0
  4. package/template/.prettierrc +11 -0
  5. package/template/AGENTS.md +96 -0
  6. package/template/LICENSE +201 -0
  7. package/template/PROTOCOL_DIAGRAMS.md +195 -0
  8. package/template/README.md +51 -0
  9. package/template/astro.config.mjs +20 -0
  10. package/template/package.json +38 -0
  11. package/template/public/favicon.ico +0 -0
  12. package/template/public/map.js +329 -0
  13. package/template/public/planet.css +873 -0
  14. package/template/public/three.min.js +29395 -0
  15. package/template/schema.sql +22 -0
  16. package/template/scripts/inject-do-exports.js +61 -0
  17. package/template/scripts/simulate-universe.js +158 -0
  18. package/template/src/env.d.ts +21 -0
  19. package/template/src/lib/config.ts +52 -0
  20. package/template/src/lib/consensus.ts +127 -0
  21. package/template/src/lib/crypto.ts +89 -0
  22. package/template/src/lib/identity.ts +35 -0
  23. package/template/src/lib/travel.ts +75 -0
  24. package/template/src/pages/api/v1/control-ws.ts +22 -0
  25. package/template/src/pages/api/v1/port.ts +673 -0
  26. package/template/src/pages/control.astro +232 -0
  27. package/template/src/pages/index.astro +1009 -0
  28. package/template/src/pages/manifest.json.ts +37 -0
  29. package/template/src/tests/protocol.test.ts +158 -0
  30. package/template/src/tests/warp-links.test.ts +117 -0
  31. package/template/src/traffic-control.ts +52 -0
  32. package/template/tsconfig.json +9 -0
  33. package/template/vitest.config.ts +12 -0
  34. package/template/wrangler.build.jsonc +36 -0
  35. package/template/wrangler.dev.jsonc +41 -0
@@ -0,0 +1,37 @@
1
+ import type { APIRoute } from "astro";
2
+ import { env } from "cloudflare:workers";
3
+ import { PLANET_NAME, PLANET_DESCRIPTION } from "../lib/config";
4
+
5
+ export const GET: APIRoute = async ({ request }) => {
6
+ // Robust helper to get simulation variables
7
+ const getSimVar = (name: string): string | undefined => {
8
+ if (env && (env as any)[name]) return (env as any)[name];
9
+ if (
10
+ typeof process !== "undefined" &&
11
+ process.env &&
12
+ (process.env as any)[name]
13
+ )
14
+ return (process.env as any)[name];
15
+ if (import.meta.env && (import.meta.env as any)[name])
16
+ return (import.meta.env as any)[name];
17
+ return undefined;
18
+ };
19
+
20
+ const simUrl = getSimVar("PUBLIC_SIM_LANDING_SITE");
21
+ const simName = getSimVar("PUBLIC_SIM_PLANET_NAME");
22
+
23
+ const landingSite = simUrl || new URL(request.url).origin;
24
+
25
+ const manifest: any = {
26
+ name: simName || PLANET_NAME,
27
+ description: PLANET_DESCRIPTION,
28
+ landing_site: landingSite,
29
+ space_port: `${landingSite}/api/v1/port`,
30
+ };
31
+
32
+ return new Response(JSON.stringify(manifest), {
33
+ headers: {
34
+ "Content-Type": "application/json",
35
+ },
36
+ });
37
+ };
@@ -0,0 +1,158 @@
1
+ import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
2
+ import { spawn, execSync, type ChildProcess } from "child_process";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const PROJECT_ROOT = path.join(__dirname, "../../");
8
+
9
+ const NUM_PLANETS = 4; // Minimal quorum size (3f + 1 where f=1)
10
+ const BASE_PORT = 4000;
11
+ const BASE_INSPECTOR_PORT = 29229;
12
+
13
+ const allPlanets = Array.from({ length: NUM_PLANETS }, (_, i) => ({
14
+ name: `Towel ${i + 1}`,
15
+ url: `http://towel-${i + 1}.localhost:${BASE_PORT + i}`,
16
+ port: BASE_PORT + i,
17
+ id: i + 1,
18
+ }));
19
+
20
+ const processes: ChildProcess[] = [];
21
+
22
+ const cleanup = () => {
23
+ console.log("Cleaning up processes...");
24
+ processes.forEach((p) => {
25
+ try {
26
+ p.kill();
27
+ } catch (e) {}
28
+ });
29
+ try {
30
+ execSync("pkill -f 'test-planet' || true");
31
+ } catch (e) {}
32
+ };
33
+
34
+ const startPlanet = (planet: (typeof allPlanets)[0]) => {
35
+ const { id, name, url, port } = planet;
36
+ const inspectorPort = BASE_INSPECTOR_PORT + id;
37
+
38
+ // Initialize Database first
39
+ console.log(`[${name}] Initializing database...`);
40
+ execSync(
41
+ `npx wrangler d1 execute planet_db --file=schema.sql -c wrangler.dev.jsonc --local --persist-to=.wrangler/state/test-planet-${id}`,
42
+ {
43
+ cwd: PROJECT_ROOT,
44
+ stdio: "inherit",
45
+ },
46
+ );
47
+
48
+ const child = spawn(
49
+ "npx",
50
+ [
51
+ "wrangler",
52
+ "dev",
53
+ "--port",
54
+ port.toString(),
55
+ "--inspector-port",
56
+ inspectorPort.toString(),
57
+ "-c",
58
+ "wrangler.dev.jsonc",
59
+ "--persist-to",
60
+ `.wrangler/state/test-planet-${id}`,
61
+ "--var",
62
+ `PUBLIC_SIM_PLANET_NAME:"${name}"`,
63
+ "--var",
64
+ `PUBLIC_SIM_LANDING_SITE:"${url}"`,
65
+ "--var",
66
+ `PUBLIC_SIM_WARP_LINKS:'${JSON.stringify(allPlanets.filter((p) => p.url !== url).map((n) => ({ name: n.name, url: n.url })))}'`,
67
+ ],
68
+ {
69
+ cwd: PROJECT_ROOT,
70
+ env: process.env,
71
+ stdio: ["ignore", "pipe", "pipe"],
72
+ shell: true,
73
+ },
74
+ );
75
+
76
+ processes.push(child);
77
+
78
+ return new Promise<void>((resolve, reject) => {
79
+ let isReady = false;
80
+ const timeout = setTimeout(() => {
81
+ if (!isReady)
82
+ reject(new Error(`[${name}] Timed out waiting for readiness`));
83
+ }, 45000); // Increased timeout for CI/slow environments
84
+
85
+ const handleData = (data: Buffer) => {
86
+ const str = data.toString();
87
+ process.stdout.write(`[${name}] ${str}`);
88
+ if (str.includes("Ready on")) {
89
+ isReady = true;
90
+ clearTimeout(timeout);
91
+ resolve();
92
+ }
93
+ };
94
+
95
+ child.stdout?.on("data", handleData);
96
+ child.stderr?.on("data", handleData);
97
+ child.on("error", reject);
98
+ });
99
+ };
100
+
101
+ describe("Federated Planets Protocol", () => {
102
+ beforeAll(async () => {
103
+ console.log(`Starting ${NUM_PLANETS} planets...`);
104
+ const startupPromises = allPlanets.map((p) => startPlanet(p));
105
+ await Promise.all(startupPromises);
106
+ }, 120000); // 2 minute setup timeout
107
+
108
+ afterAll(() => {
109
+ cleanup();
110
+ });
111
+
112
+ it("should reach quorum when initiating a jump", async () => {
113
+ console.log("Initiating jump from Towel 1 to Towel 2...");
114
+ const response = await fetch(
115
+ `${allPlanets[0].url}/api/v1/port?action=initiate`,
116
+ {
117
+ method: "POST",
118
+ headers: { "Content-Type": "application/json" },
119
+ body: JSON.stringify({
120
+ ship_id: "TEST-SHIP",
121
+ destination_url: allPlanets[1].url,
122
+ departure_timestamp: Date.now(),
123
+ }),
124
+ },
125
+ );
126
+
127
+ expect(response.ok).toBe(true);
128
+ const data = (await response.json()) as any;
129
+ expect(data.plan.id).toBeDefined();
130
+ console.log("Plan initiated:", data.plan.id);
131
+
132
+ console.log("Monitoring events for QUORUM_REACHED...");
133
+ let quorumReached = false;
134
+ for (let attempt = 0; attempt < 30; attempt++) {
135
+ await new Promise((r) => setTimeout(r, 2000));
136
+
137
+ const eventsRes = await fetch(`${allPlanets[0].url}/api/v1/control-ws`);
138
+ if (eventsRes.ok) {
139
+ const events = (await eventsRes.json()) as any[];
140
+ const quorumEvent = events.find((e) => e.type === "QUORUM_REACHED");
141
+ const errorEvent = events.find((e) => e.type === "API_ERROR");
142
+
143
+ if (errorEvent) {
144
+ console.error("API ERROR DETECTED:", errorEvent.error);
145
+ }
146
+
147
+ if (quorumEvent) {
148
+ console.log("SUCCESS: Quorum reached!");
149
+ quorumReached = true;
150
+ break;
151
+ }
152
+ }
153
+ console.log(`Waiting for quorum... (attempt ${attempt + 1}/30)`);
154
+ }
155
+
156
+ expect(quorumReached).toBe(true);
157
+ }, 90000); // 90s test timeout
158
+ });
@@ -0,0 +1,117 @@
1
+ import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
2
+ import { spawn, execSync, type ChildProcess } from "child_process";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const PROJECT_ROOT = path.join(__dirname, "../../");
8
+
9
+ const TEST_PORT = 4500;
10
+ const TEST_HOST = "towel-warp-test.localhost";
11
+ const TEST_NAME = "Warp Test Planet";
12
+ const TEST_LINKS = [
13
+ { name: "Test Link Alpha", url: "https://alpha.test" },
14
+ { name: "Test Link Beta", url: "https://beta.test" },
15
+ ];
16
+
17
+ const processes: ChildProcess[] = [];
18
+
19
+ const cleanup = () => {
20
+ console.log("Cleaning up...");
21
+ processes.forEach((p) => {
22
+ try {
23
+ p.kill();
24
+ } catch (e) {}
25
+ });
26
+ try {
27
+ execSync("pkill -f 'warp-test' || true");
28
+ } catch (e) {}
29
+ };
30
+
31
+ describe("Warp Links Configuration", () => {
32
+ beforeAll(async () => {
33
+ console.log(`[${TEST_NAME}] Initializing database...`);
34
+ execSync(
35
+ `npx wrangler d1 execute planet_db --file=schema.sql -c wrangler.dev.jsonc --local --persist-to=.wrangler/state/warp-test`,
36
+ {
37
+ cwd: PROJECT_ROOT,
38
+ stdio: "inherit",
39
+ },
40
+ );
41
+
42
+ console.log(`Starting ${TEST_NAME} on http://${TEST_HOST}:${TEST_PORT}...`);
43
+
44
+ const child = spawn(
45
+ "npx",
46
+ [
47
+ "wrangler",
48
+ "dev",
49
+ "--port",
50
+ TEST_PORT.toString(),
51
+ "-c",
52
+ "wrangler.dev.jsonc",
53
+ "--persist-to",
54
+ ".wrangler/state/warp-test",
55
+ "--var",
56
+ `PUBLIC_SIM_PLANET_NAME:"${TEST_NAME}"`,
57
+ "--var",
58
+ `PUBLIC_SIM_LANDING_SITE:"http://${TEST_HOST}:${TEST_PORT}"`,
59
+ "--var",
60
+ `PUBLIC_SIM_WARP_LINKS:'${JSON.stringify(TEST_LINKS)}'`,
61
+ ],
62
+ {
63
+ cwd: PROJECT_ROOT,
64
+ env: { ...process.env },
65
+ stdio: ["ignore", "pipe", "pipe"],
66
+ shell: true,
67
+ },
68
+ );
69
+
70
+ processes.push(child);
71
+
72
+ await new Promise<void>((resolve, reject) => {
73
+ let isReady = false;
74
+ const timeout = setTimeout(() => {
75
+ if (!isReady)
76
+ reject(new Error(`[${TEST_NAME}] Timed out waiting for readiness`));
77
+ }, 45000);
78
+
79
+ const handleData = (data: Buffer) => {
80
+ const str = data.toString();
81
+ process.stdout.write(`[${TEST_NAME}] ${str}`);
82
+ if (str.includes("Ready on")) {
83
+ isReady = true;
84
+ clearTimeout(timeout);
85
+ resolve();
86
+ }
87
+ };
88
+
89
+ child.stdout?.on("data", handleData);
90
+ child.stderr?.on("data", handleData);
91
+ child.on("error", reject);
92
+ });
93
+ }, 60000);
94
+
95
+ afterAll(() => {
96
+ cleanup();
97
+ });
98
+
99
+ it("should override planet name in manifest", async () => {
100
+ const response = await fetch(
101
+ `http://${TEST_HOST}:${TEST_PORT}/manifest.json`,
102
+ );
103
+ expect(response.ok).toBe(true);
104
+ const manifest = (await response.json()) as any;
105
+ expect(manifest.name).toBe(TEST_NAME);
106
+ });
107
+
108
+ it("should correctly override warp links on homepage", async () => {
109
+ const homeRes = await fetch(`http://${TEST_HOST}:${TEST_PORT}/`);
110
+ expect(homeRes.ok).toBe(true);
111
+ const html = await homeRes.text();
112
+
113
+ expect(html).toContain("Test Link Alpha");
114
+ expect(html).toContain("Test Link Beta");
115
+ expect(html).not.toContain("Aether Reach"); // Default link should be absent
116
+ });
117
+ });
@@ -0,0 +1,52 @@
1
+ import { DurableObject } from "cloudflare:workers";
2
+
3
+ export default class TrafficControl extends DurableObject {
4
+ private sessions: Set<WebSocket> = new Set();
5
+ private events: any[] = [];
6
+
7
+ constructor(state: any, env: any) {
8
+ super(state, env);
9
+ console.log("[TrafficControl] Initialized");
10
+ }
11
+
12
+ async fetch(request: Request) {
13
+ const url = new URL(request.url);
14
+
15
+ if (url.pathname === "/events" && request.method === "POST") {
16
+ const event = await request.json();
17
+ this.events.push({ ...event, timestamp: Date.now() });
18
+ if (this.events.length > 50) this.events.shift();
19
+ this.broadcast(JSON.stringify(event));
20
+ return new Response("OK");
21
+ }
22
+
23
+ if (request.headers.get("Upgrade") === "websocket") {
24
+ const pair = new WebSocketPair();
25
+ const [client, server] = Object.values(pair);
26
+ // @ts-ignore
27
+ server.accept();
28
+ this.sessions.add(server);
29
+
30
+ server.send(JSON.stringify({ type: "history", data: this.events }));
31
+
32
+ server.addEventListener("close", () => this.sessions.delete(server));
33
+ server.addEventListener("error", () => this.sessions.delete(server));
34
+
35
+ return new Response(null, { status: 101, webSocket: client });
36
+ }
37
+
38
+ return new Response(JSON.stringify(this.events), {
39
+ headers: { "Content-Type": "application/json" },
40
+ });
41
+ }
42
+
43
+ broadcast(message: string) {
44
+ for (const ws of this.sessions) {
45
+ try {
46
+ ws.send(message);
47
+ } catch (e) {
48
+ this.sessions.delete(ws);
49
+ }
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "astro/tsconfigs/strict",
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "paths": {
6
+ "@/*": ["src/*"]
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: "node",
7
+ testTimeout: 90000, // Increased for full builds
8
+ hookTimeout: 60000,
9
+ fileParallelism: true,
10
+ include: ["src/tests/**/*.test.{ts,js}"],
11
+ },
12
+ });
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "federated-planet",
3
+ "compatibility_date": "2026-03-31",
4
+ "assets": {
5
+ "directory": "dist/client",
6
+ "binding": "STATIC_ASSETS",
7
+ },
8
+ "d1_databases": [
9
+ {
10
+ "binding": "DB",
11
+ "database_name": "planet_db",
12
+ "database_id": "your-d1-id-here", // To be replaced during deployment
13
+ "migrations_dir": "migrations",
14
+ },
15
+ ],
16
+ "kv_namespaces": [
17
+ {
18
+ "binding": "KV",
19
+ "id": "your-kv-id-here", // To be replaced during deployment
20
+ },
21
+ ],
22
+ "durable_objects": {
23
+ "bindings": [
24
+ {
25
+ "name": "TRAFFIC_CONTROL",
26
+ "class_name": "TrafficControl",
27
+ },
28
+ ],
29
+ },
30
+ "migrations": [
31
+ {
32
+ "tag": "v1",
33
+ "new_classes": ["TrafficControl"],
34
+ },
35
+ ],
36
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "federated-planet",
3
+ "main": "dist/server/entry.mjs",
4
+ "compatibility_date": "2026-03-31",
5
+ "assets": {
6
+ "directory": "dist/client",
7
+ "binding": "STATIC_ASSETS",
8
+ },
9
+ "d1_databases": [
10
+ {
11
+ "binding": "DB",
12
+ "database_name": "planet_db",
13
+ "database_id": "your-d1-id-here",
14
+ "migrations_dir": "migrations",
15
+ },
16
+ ],
17
+ "kv_namespaces": [
18
+ {
19
+ "binding": "KV",
20
+ "id": "your-kv-id-here",
21
+ },
22
+ ],
23
+ "durable_objects": {
24
+ "bindings": [
25
+ {
26
+ "name": "TRAFFIC_CONTROL",
27
+ "class_name": "TrafficControl",
28
+ },
29
+ ],
30
+ },
31
+ "vars": {
32
+ "WARP_MS_PER_FY": "10000",
33
+ "DEPARTURE_BUFFER_MS": "5000",
34
+ },
35
+ "migrations": [
36
+ {
37
+ "tag": "v1",
38
+ "new_classes": ["TrafficControl"],
39
+ },
40
+ ],
41
+ }