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,22 @@
1
+ -- Travel Plans Table: Tracks all journeys (active and historical)
2
+ CREATE TABLE IF NOT EXISTS travel_plans (
3
+ id TEXT PRIMARY KEY,
4
+ ship_id TEXT NOT NULL,
5
+ origin_url TEXT NOT NULL,
6
+ destination_url TEXT NOT NULL,
7
+ start_timestamp INTEGER NOT NULL,
8
+ end_timestamp INTEGER NOT NULL,
9
+ status TEXT CHECK(status IN ('PREPARING', 'PLAN_ACCEPTED')) NOT NULL DEFAULT 'PREPARING',
10
+ signatures TEXT NOT NULL -- JSON array of cryptographic signatures
11
+ );
12
+
13
+ -- Index for fast retrieval of active plans (not yet arrived)
14
+ CREATE INDEX IF NOT EXISTS idx_travel_plans_active ON travel_plans (end_timestamp, origin_url, destination_url);
15
+
16
+ -- Traffic Controllers Cache: Known neighbors with space ports
17
+ CREATE TABLE IF NOT EXISTS traffic_controllers (
18
+ planet_url TEXT PRIMARY KEY,
19
+ name TEXT NOT NULL,
20
+ space_port_url TEXT NOT NULL,
21
+ last_manifest_fetch INTEGER NOT NULL
22
+ );
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Post-build script that patches dist/server/entry.mjs to export Durable Object
3
+ * classes required by Cloudflare Workers. Astro's build doesn't re-export these
4
+ * from the entrypoint, causing "Worker depends on Durable Objects not exported
5
+ * in entrypoint" errors.
6
+ */
7
+ import { readFileSync, writeFileSync, readdirSync } from "fs";
8
+ import { join } from "path";
9
+
10
+ const serverDir = "dist/server";
11
+ const chunksDir = join(serverDir, "chunks");
12
+ const entryFile = join(serverDir, "entry.mjs");
13
+
14
+ // Map of DO class names to search for in chunks
15
+ const durableObjects = ["TrafficControl"];
16
+
17
+ for (const className of durableObjects) {
18
+ // Find the chunk containing this DO class
19
+ const chunks = readdirSync(chunksDir);
20
+ let targetChunk = null;
21
+
22
+ for (const chunk of chunks) {
23
+ if (!chunk.endsWith(".mjs")) continue;
24
+ const content = readFileSync(join(chunksDir, chunk), "utf-8");
25
+ if (content.includes(`class ${className} extends DurableObject`)) {
26
+ targetChunk = chunk;
27
+ break;
28
+ }
29
+ }
30
+
31
+ if (!targetChunk) {
32
+ console.error(
33
+ `[inject-do-exports] Could not find ${className} in any chunk`,
34
+ );
35
+ process.exit(1);
36
+ }
37
+
38
+ // Add named export to the chunk file
39
+ const chunkPath = join(chunksDir, targetChunk);
40
+ let chunkContent = readFileSync(chunkPath, "utf-8");
41
+ if (!chunkContent.includes(`export { ${className}`)) {
42
+ chunkContent = chunkContent.replace(
43
+ /export \{\s*page\s*\};/,
44
+ `export {\n page,\n ${className}\n};`,
45
+ );
46
+ writeFileSync(chunkPath, chunkContent);
47
+ console.log(
48
+ `[inject-do-exports] Exported ${className} from chunks/${targetChunk}`,
49
+ );
50
+ }
51
+
52
+ // Re-export from entry.mjs
53
+ let entryContent = readFileSync(entryFile, "utf-8");
54
+ if (!entryContent.includes(className)) {
55
+ entryContent += `export { ${className} } from './chunks/${targetChunk}';\n`;
56
+ writeFileSync(entryFile, entryContent);
57
+ console.log(`[inject-do-exports] Re-exported ${className} in entry.mjs`);
58
+ }
59
+ }
60
+
61
+ console.log("[inject-do-exports] Done");
@@ -0,0 +1,158 @@
1
+ import { spawn, execSync } from "child_process";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ const NUM_PLANETS = 10;
8
+ const BASE_PORT = 3000;
9
+ const BASE_INSPECTOR_PORT = 19229;
10
+ const OPEN_BROWSER = process.argv.includes("--open");
11
+
12
+ const allPlanets = Array.from({ length: NUM_PLANETS }, (_, i) => ({
13
+ name: `Towel ${i + 1}`,
14
+ url: `http://towel-${i + 1}.localhost:${BASE_PORT + i}`,
15
+ port: BASE_PORT + i,
16
+ id: i + 1,
17
+ }));
18
+
19
+ const processes = [];
20
+
21
+ const cleanup = () => {
22
+ console.log("Cleaning up existing processes...");
23
+ processes.forEach((p) => {
24
+ try {
25
+ p.kill();
26
+ } catch (e) {}
27
+ });
28
+ try {
29
+ execSync("pkill -f 'wrangler dev' || true");
30
+ } catch (e) {}
31
+ };
32
+
33
+ const startPlanet = (planet) => {
34
+ const { id, name, url, port } = planet;
35
+ const inspectorPort = BASE_INSPECTOR_PORT + id;
36
+
37
+ // Initialize Database first
38
+ console.log(`[${name}] Initializing database...`);
39
+ execSync(
40
+ `npx wrangler d1 execute planet_db --file=schema.sql -c wrangler.dev.jsonc --local --persist-to=.wrangler/state/planet-${id}`,
41
+ {
42
+ cwd: path.join(__dirname, ".."),
43
+ stdio: "inherit",
44
+ },
45
+ );
46
+
47
+ const env = {
48
+ ...process.env,
49
+ PUBLIC_SIM_PLANET_NAME: name,
50
+ PUBLIC_SIM_LANDING_SITE: url,
51
+ PUBLIC_SIM_WARP_LINKS: JSON.stringify(
52
+ allPlanets
53
+ .filter((p) => p.url !== url)
54
+ .sort(() => 0.5 - Math.random())
55
+ .slice(0, 5)
56
+ .map((n) => ({ name: n.name, url: n.url })),
57
+ ),
58
+ };
59
+
60
+ const child = spawn(
61
+ "npx",
62
+ [
63
+ "wrangler",
64
+ "dev",
65
+ "--port",
66
+ port,
67
+ "--ip",
68
+ "0.0.0.0",
69
+ "--inspector-port",
70
+ inspectorPort,
71
+ "-c",
72
+ "wrangler.dev.jsonc",
73
+ "--persist-to",
74
+ `.wrangler/state/planet-${id}`,
75
+ "--var",
76
+ `PUBLIC_SIM_PLANET_NAME:"${name}"`,
77
+ "--var",
78
+ `PUBLIC_SIM_LANDING_SITE:"${url}"`,
79
+ "--var",
80
+ `PUBLIC_SIM_WARP_LINKS:'${env.PUBLIC_SIM_WARP_LINKS}'`,
81
+ ],
82
+ {
83
+ cwd: path.join(__dirname, ".."),
84
+ env: process.env,
85
+ stdio: ["ignore", "pipe", "pipe"],
86
+ shell: true,
87
+ },
88
+ );
89
+
90
+ processes.push(child);
91
+
92
+ return new Promise((resolve, reject) => {
93
+ let isReady = false;
94
+ const timeout = setTimeout(() => {
95
+ if (!isReady)
96
+ reject(new Error(`[${name}] Timed out waiting for readiness`));
97
+ }, 30000);
98
+
99
+ const handleData = (data) => {
100
+ const str = data.toString();
101
+ process.stdout.write(`[${name}] ${str}`);
102
+ if (str.includes("Ready on")) {
103
+ isReady = true;
104
+ clearTimeout(timeout);
105
+ resolve();
106
+ }
107
+ };
108
+
109
+ child.stdout.on("data", handleData);
110
+ child.stderr.on("data", handleData);
111
+ child.on("error", reject);
112
+ });
113
+ };
114
+
115
+ const run = async () => {
116
+ try {
117
+ cleanup();
118
+ console.log("Building project for wrangler...");
119
+ execSync("npm run build", {
120
+ cwd: path.join(__dirname, ".."),
121
+ stdio: "inherit",
122
+ });
123
+
124
+ console.log(
125
+ `--- SIMULATING FEDERATED UNIVERSE (${NUM_PLANETS} PLANETS) ---`,
126
+ );
127
+
128
+ // Start planets sequentially to avoid overwhelming the system, but wait for readiness
129
+ for (const planet of allPlanets) {
130
+ await startPlanet(planet);
131
+ }
132
+
133
+ if (OPEN_BROWSER) {
134
+ console.log("All planets are ready! Opening browser windows...");
135
+ const urlsToOpen = [
136
+ allPlanets[0].url, // First planet landing site
137
+ ...allPlanets.map((p) => `${p.url}/control`), // All planets control centers
138
+ ];
139
+ const urls = urlsToOpen.join(" ");
140
+ // On macOS, 'open' with multiple URLs opens them in tabs
141
+ execSync(`open ${urls}`);
142
+ } else {
143
+ console.log(`All planets are ready! (pass --open to launch browser)`);
144
+ allPlanets.forEach((p) => console.log(` ${p.name}: ${p.url}`));
145
+ }
146
+
147
+ console.log("Simulation running. Press Ctrl+C to stop.");
148
+ } catch (e) {
149
+ console.error("Simulation failed to start:", e.message);
150
+ cleanup();
151
+ process.exit(1);
152
+ }
153
+ };
154
+
155
+ process.on("SIGINT", cleanup);
156
+ process.on("SIGTERM", cleanup);
157
+
158
+ run();
@@ -0,0 +1,21 @@
1
+ /// <reference types="astro/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly KV: import("@cloudflare/workers-types").KVNamespace;
5
+ readonly DB: import("@cloudflare/workers-types").D1Database;
6
+ readonly TRAFFIC_CONTROL: import("@cloudflare/workers-types").DurableObjectNamespace;
7
+ readonly PUBLIC_SIM_PLANET_NAME?: string;
8
+ readonly PUBLIC_SIM_PLANET_DESCRIPTION?: string;
9
+ readonly PUBLIC_SIM_LANDING_SITE?: string;
10
+ readonly PUBLIC_SIM_WARP_LINKS?: string;
11
+ }
12
+
13
+ interface ImportMeta {
14
+ readonly env: ImportMetaEnv;
15
+ }
16
+
17
+ type Runtime = import("@astrojs/cloudflare").Runtime<ImportMetaEnv>;
18
+
19
+ declare namespace App {
20
+ interface Locals extends Runtime {}
21
+ }
@@ -0,0 +1,52 @@
1
+ // Baked in at scaffold time by `npm create planet`
2
+ export const PLANET_NAME = "Towel 42 Space Outpost";
3
+ export const PLANET_DESCRIPTION =
4
+ "A remote outpost in the Federated Planets. A quiet spot for deep-space weary travelers. Don't panic, but bring a towel.";
5
+
6
+ export const DEFAULT_WARP_LINKS = [
7
+ {
8
+ name: "Federation Prime",
9
+ url: "https://prime.federatedplanets.com/",
10
+ },
11
+ { name: "Waystation Meridian", url: "https://waystation.federatedplanets.com/" },
12
+ { name: "The Interchange", url: "https://interchange.federatedplanets.com/" },
13
+ { name: "Port Cassini", url: "https://port-cassini.federatedplanets.com/" },
14
+ { name: "Terminus Reach", url: "https://terminus.federatedplanets.com/" },
15
+ { name: "Driftyard Seven", url: "https://driftyard.federatedplanets.com/" },
16
+ { name: "Towel 42 Space Outpost", url: "https://towel-42.federatedplanets.com/" },
17
+ { name: "Explorers Outpost", url: "https://www.nasa.gov/" },
18
+ ];
19
+
20
+ import { env as cloudflareEnv } from "cloudflare:workers";
21
+
22
+ // Robust helper to get simulation variables from any available environment source
23
+ const getSimVar = (name: string): string | undefined => {
24
+ // 1. Check Cloudflare env object (for wrangler dev --var)
25
+ const env = cloudflareEnv as any;
26
+ if (env && env[name]) return env[name];
27
+
28
+ // 2. Check process.env (traditional node/dev env)
29
+ if (typeof process !== "undefined" && process.env && process.env[name])
30
+ return process.env[name];
31
+
32
+ // 3. Check import.meta.env (build-time or astro-provided)
33
+ if (import.meta.env && import.meta.env[name]) return import.meta.env[name];
34
+
35
+ return undefined;
36
+ };
37
+
38
+ export const getWarpLinks = () => {
39
+ const landingSite = getSimVar("PUBLIC_SIM_LANDING_SITE")?.replace(/\/$/, "");
40
+ try {
41
+ const simLinks = getSimVar("PUBLIC_SIM_WARP_LINKS");
42
+ if (simLinks) {
43
+ return JSON.parse(simLinks);
44
+ }
45
+ } catch (e) {}
46
+ if (!landingSite) return DEFAULT_WARP_LINKS;
47
+ return DEFAULT_WARP_LINKS.filter(
48
+ (l) => l.url.replace(/\/$/, "") !== landingSite,
49
+ );
50
+ };
51
+
52
+ export const WARP_LINKS = getWarpLinks();
@@ -0,0 +1,127 @@
1
+ import { CryptoCore } from "./crypto";
2
+ import type { PlanetManifest } from "./travel";
3
+ import { env as cloudflareEnv } from "cloudflare:workers";
4
+ import { PLANET_NAME } from "./config";
5
+
6
+ // Robust helper to get simulation variables from any available environment source
7
+ const getSimVar = (name: string): string | undefined => {
8
+ // 1. Check Cloudflare env object (for wrangler dev --var)
9
+ const env = cloudflareEnv as any;
10
+ if (env && env[name]) return env[name];
11
+
12
+ // 2. Check process.env (traditional node/dev env)
13
+ if (
14
+ typeof process !== "undefined" &&
15
+ process.env &&
16
+ (process.env as any)[name]
17
+ )
18
+ return (process.env as any)[name];
19
+
20
+ // 3. Check import.meta.env (build-time or astro-provided)
21
+ if (import.meta.env && (import.meta.env as any)[name])
22
+ return (import.meta.env as any)[name];
23
+
24
+ return undefined;
25
+ };
26
+
27
+ export interface TravelPlan {
28
+ id: string;
29
+ ship_id: string;
30
+ origin_url: string;
31
+ destination_url: string;
32
+ start_timestamp: number;
33
+ end_timestamp: number;
34
+ status: "PREPARING" | "PLAN_ACCEPTED";
35
+ traffic_controllers: string[]; // List of landing site URLs elected
36
+ signatures: Record<string, string>; // planet_url -> signature
37
+ }
38
+
39
+ export class ConsensusEngine {
40
+ /**
41
+ * Broadcasts a message to all elected Traffic Controllers
42
+ */
43
+ static async broadcast(
44
+ plan: TravelPlan,
45
+ action: "prepare" | "commit",
46
+ controllers: PlanetManifest[],
47
+ ) {
48
+ const localName = getSimVar("PUBLIC_SIM_PLANET_NAME") || PLANET_NAME;
49
+ console.log(
50
+ `[${localName}] Broadcasting ${action} for plan ${plan.id} to ${controllers.length} controllers`,
51
+ );
52
+
53
+ const promises = controllers.map(async (tc) => {
54
+ if (!tc.space_port) {
55
+ console.warn(
56
+ `[${localName}] Skipping broadcast to ${tc.landing_site}: No space_port found in manifest`,
57
+ );
58
+ return;
59
+ }
60
+ try {
61
+ const localUrl =
62
+ getSimVar("PUBLIC_SIM_LANDING_SITE") || "http://unknown";
63
+ console.log(`[${localName}] Sending ${action} to ${tc.space_port}...`);
64
+ const res = await fetch(`${tc.space_port}?action=${action}`, {
65
+ method: "POST",
66
+ headers: {
67
+ "Content-Type": "application/json",
68
+ "X-Planet-Origin": localUrl,
69
+ },
70
+ body: JSON.stringify(plan),
71
+ });
72
+ if (!res.ok) {
73
+ console.error(
74
+ `[${localName}] Broadcast ${action} to ${tc.landing_site} failed: ${res.status} ${await res.text()}`,
75
+ );
76
+ } else {
77
+ console.log(
78
+ `[${localName}] Broadcast ${action} to ${tc.landing_site} succeeded`,
79
+ );
80
+ }
81
+ } catch (e: any) {
82
+ console.error(
83
+ `[${localName}] Failed to broadcast ${action} to ${tc.landing_site}:`,
84
+ e.message,
85
+ );
86
+ }
87
+ });
88
+ return Promise.all(promises);
89
+ }
90
+
91
+ /**
92
+ * Checks if we have enough signatures for consensus
93
+ * N = 3f + 1, we need 2f + 1 signatures
94
+ */
95
+ static hasQuorum(plan: TravelPlan): boolean {
96
+ const localName = getSimVar("PUBLIC_SIM_PLANET_NAME") || PLANET_NAME;
97
+ const n = plan.traffic_controllers.length;
98
+ const f = Math.floor((n - 1) / 3);
99
+ const required = 2 * f + 1;
100
+ const current = Object.keys(plan.signatures).length;
101
+
102
+ console.log(
103
+ `[${localName}] Quorum check for plan ${plan.id}: ${current}/${required} signatures (N=${n}, f=${f})`,
104
+ );
105
+ return current >= required;
106
+ }
107
+
108
+ /**
109
+ * Saves plan state to KV for active consensus tracking
110
+ */
111
+ static async savePlanState(KV: KVNamespace, plan: TravelPlan) {
112
+ await KV.put(`consensus_plan_${plan.id}`, JSON.stringify(plan), {
113
+ expirationTtl: 3600,
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Retrieves plan state from KV
119
+ */
120
+ static async getPlanState(
121
+ KV: KVNamespace,
122
+ planId: string,
123
+ ): Promise<TravelPlan | null> {
124
+ const data = await KV.get(`consensus_plan_${planId}`);
125
+ return data ? JSON.parse(data) : null;
126
+ }
127
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Cryptography Utility for Space Travel Protocol
3
+ * Uses Web Crypto API (Ed25519) for signing and verification.
4
+ */
5
+
6
+ export class CryptoCore {
7
+ private static KEY_ALGO = {
8
+ name: "Ed25519",
9
+ };
10
+
11
+ /**
12
+ * Generates a new Ed25519 KeyPair
13
+ */
14
+ static async generateKeyPair(): Promise<CryptoKeyPair> {
15
+ return (await crypto.subtle.generateKey(this.KEY_ALGO, true, [
16
+ "sign",
17
+ "verify",
18
+ ])) as CryptoKeyPair;
19
+ }
20
+
21
+ /**
22
+ * Exports a key to Base64 format
23
+ */
24
+ static async exportKey(key: CryptoKey): Promise<string> {
25
+ const exported = await crypto.subtle.exportKey(
26
+ key.type === "public" ? "spki" : "pkcs8",
27
+ key,
28
+ );
29
+ return btoa(String.fromCharCode(...new Uint8Array(exported)));
30
+ }
31
+
32
+ /**
33
+ * Imports a key from Base64 format
34
+ */
35
+ static async importKey(
36
+ base64Key: string,
37
+ type: "public" | "private",
38
+ ): Promise<CryptoKey> {
39
+ const binary = atob(base64Key);
40
+ const bytes = new Uint8Array(binary.length);
41
+ for (let i = 0; i < binary.length; i++) {
42
+ bytes[i] = binary.charCodeAt(i);
43
+ }
44
+
45
+ return await crypto.subtle.importKey(
46
+ type === "public" ? "spki" : "pkcs8",
47
+ bytes,
48
+ this.KEY_ALGO,
49
+ true,
50
+ type === "public" ? ["verify"] : ["sign"],
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Signs data string using a private key
56
+ */
57
+ static async sign(data: string, privateKey: CryptoKey): Promise<string> {
58
+ const encoder = new TextEncoder();
59
+ const signature = await crypto.subtle.sign(
60
+ this.KEY_ALGO,
61
+ privateKey,
62
+ encoder.encode(data),
63
+ );
64
+ return btoa(String.fromCharCode(...new Uint8Array(signature)));
65
+ }
66
+
67
+ /**
68
+ * Verifies signature for data string using a public key
69
+ */
70
+ static async verify(
71
+ data: string,
72
+ signatureBase64: string,
73
+ publicKey: CryptoKey,
74
+ ): Promise<boolean> {
75
+ const encoder = new TextEncoder();
76
+ const signatureBinary = atob(signatureBase64);
77
+ const signatureBytes = new Uint8Array(signatureBinary.length);
78
+ for (let i = 0; i < signatureBinary.length; i++) {
79
+ signatureBytes[i] = signatureBinary.charCodeAt(i);
80
+ }
81
+
82
+ return await crypto.subtle.verify(
83
+ this.KEY_ALGO,
84
+ publicKey,
85
+ signatureBytes,
86
+ encoder.encode(data),
87
+ );
88
+ }
89
+ }
@@ -0,0 +1,35 @@
1
+ import { CryptoCore } from "./crypto";
2
+
3
+ export class PlanetIdentity {
4
+ /**
5
+ * Retrieves or generates the planet's Ed25519 identity keys from KV.
6
+ */
7
+ static async getIdentity(KV: KVNamespace): Promise<{
8
+ publicKey: CryptoKey;
9
+ privateKey: CryptoKey;
10
+ publicKeyBase64: string;
11
+ }> {
12
+ const existingPublic = await KV.get("identity_public");
13
+ const existingPrivate = await KV.get("identity_private");
14
+
15
+ if (existingPublic && existingPrivate) {
16
+ const publicKey = await CryptoCore.importKey(existingPublic, "public");
17
+ const privateKey = await CryptoCore.importKey(existingPrivate, "private");
18
+ return { publicKey, privateKey, publicKeyBase64: existingPublic };
19
+ }
20
+
21
+ // Generate new if not found
22
+ const keyPair = await CryptoCore.generateKeyPair();
23
+ const publicB64 = await CryptoCore.exportKey(keyPair.publicKey);
24
+ const privateB64 = await CryptoCore.exportKey(keyPair.privateKey);
25
+
26
+ await KV.put("identity_public", publicB64);
27
+ await KV.put("identity_private", privateB64);
28
+
29
+ return {
30
+ publicKey: keyPair.publicKey,
31
+ privateKey: keyPair.privateKey,
32
+ publicKeyBase64: publicB64,
33
+ };
34
+ }
35
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Travel Logic for Federated Planets
3
+ * Handles distance, time, and controller election.
4
+ */
5
+ import md5 from "md5";
6
+
7
+ export interface Coordinates {
8
+ x: number;
9
+ y: number;
10
+ z: number;
11
+ }
12
+
13
+ export interface PlanetManifest {
14
+ name: string;
15
+ landing_site: string;
16
+ space_port?: string;
17
+ }
18
+
19
+ export class TravelCalculator {
20
+ /**
21
+ * Calculates 3D coordinates from a URL deterministically
22
+ */
23
+ static calculateCoordinates(url: string): Coordinates {
24
+ const domain = new URL(url).hostname.toLowerCase();
25
+ const hash = md5(domain);
26
+
27
+ const xHex = hash.slice(0, 6);
28
+ const yHex = hash.slice(6, 12);
29
+ const zHex = hash.slice(12, 18);
30
+
31
+ const x = (parseInt(xHex, 16) % 100000) / 100;
32
+ const y = (parseInt(yHex, 16) % 100000) / 100;
33
+ const z = (parseInt(zHex, 16) % 100000) / 100;
34
+
35
+ return { x, y, z };
36
+ }
37
+
38
+ /**
39
+ * Calculates 3D Euclidean distance between two points
40
+ */
41
+ static calculateDistance(p1: Coordinates, p2: Coordinates): number {
42
+ return Math.sqrt(
43
+ Math.pow(p2.x - p1.x, 2) +
44
+ Math.pow(p2.y - p1.y, 2) +
45
+ Math.pow(p2.z - p1.z, 2),
46
+ );
47
+ }
48
+
49
+ /**
50
+ * Calculates travel time in Earth hours (Flight-Years)
51
+ * 1 FY per 100 sparsecs.
52
+ */
53
+ static calculateTravelTime(distance: number): number {
54
+ return distance / 100;
55
+ }
56
+
57
+ /**
58
+ * Elects Traffic Controllers based on proximity sortition
59
+ */
60
+ static electControllers(
61
+ seed: string,
62
+ eligibleNeighbors: PlanetManifest[],
63
+ n: number = 4, // Default 3f + 1 where f=1
64
+ ): PlanetManifest[] {
65
+ const scored = eligibleNeighbors.map((neighbor) => {
66
+ const score = md5(seed + neighbor.landing_site);
67
+ return { neighbor, score };
68
+ });
69
+
70
+ // Deterministic sort by score
71
+ scored.sort((a, b) => a.score.localeCompare(b.score));
72
+
73
+ return scored.slice(0, n).map((s) => s.neighbor);
74
+ }
75
+ }
@@ -0,0 +1,22 @@
1
+ import type { APIRoute } from "astro";
2
+ import { env } from "cloudflare:workers";
3
+ import TrafficControl from "../../../traffic-control";
4
+ export { TrafficControl };
5
+
6
+ export const GET: APIRoute = async ({ request }) => {
7
+ const { TRAFFIC_CONTROL } = env as any;
8
+
9
+ if (!TRAFFIC_CONTROL) {
10
+ return new Response("Durable Object binding not found", { status: 500 });
11
+ }
12
+
13
+ const isWebSocket = request.headers.get("Upgrade") === "websocket";
14
+ const id = TRAFFIC_CONTROL.idFromName("global");
15
+ const obj = TRAFFIC_CONTROL.get(id);
16
+
17
+ try {
18
+ return await obj.fetch(request);
19
+ } catch (e: any) {
20
+ return new Response(`DO fetch error: ${e.message}`, { status: 500 });
21
+ }
22
+ };