create-planet 1.0.2 → 1.0.5

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.
@@ -35,15 +35,12 @@ const startPlanet = (planet: (typeof allPlanets)[0]) => {
35
35
  const { id, name, url, port } = planet;
36
36
  const inspectorPort = BASE_INSPECTOR_PORT + id;
37
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
- );
38
+ // Clear any state left over from a prior test run
39
+ console.log(`[${name}] Clearing previous state...`);
40
+ execSync(`rm -rf .wrangler/state/test-planet-${id}`, {
41
+ cwd: PROJECT_ROOT,
42
+ stdio: "inherit",
43
+ });
47
44
 
48
45
  const child = spawn(
49
46
  "npx",
@@ -98,6 +95,48 @@ const startPlanet = (planet: (typeof allPlanets)[0]) => {
98
95
  });
99
96
  };
100
97
 
98
+ async function getQuorumCount(planetUrl: string): Promise<number> {
99
+ const res = await fetch(`${planetUrl}/api/v1/control-ws`);
100
+ if (!res.ok) return 0;
101
+ const events = (await res.json()) as any[];
102
+ return events.filter((e) => e.type === "QUORUM_REACHED").length;
103
+ }
104
+
105
+ async function collectQuorumPlanIds(
106
+ planetUrls: string[],
107
+ ): Promise<Set<string>> {
108
+ const ids = new Set<string>();
109
+ for (const url of planetUrls) {
110
+ try {
111
+ const res = await fetch(`${url}/api/v1/control-ws`);
112
+ if (res.ok) {
113
+ const events = (await res.json()) as any[];
114
+ for (const e of events) {
115
+ if (e.type === "QUORUM_REACHED" && e.plan_id) ids.add(e.plan_id);
116
+ }
117
+ }
118
+ } catch {}
119
+ }
120
+ return ids;
121
+ }
122
+
123
+ async function waitForPlanQuorum(
124
+ allUrls: string[],
125
+ planId: string,
126
+ label: string,
127
+ ) {
128
+ for (let attempt = 0; attempt < 30; attempt++) {
129
+ await new Promise((r) => setTimeout(r, 2000));
130
+ const ids = await collectQuorumPlanIds(allUrls);
131
+ if (ids.has(planId)) {
132
+ console.log(`SUCCESS: ${label}`);
133
+ return;
134
+ }
135
+ console.log(`Waiting for ${label}... (attempt ${attempt + 1}/30)`);
136
+ }
137
+ throw new Error(`Timeout waiting for ${label}`);
138
+ }
139
+
101
140
  describe("Federated Planets Protocol", () => {
102
141
  beforeAll(async () => {
103
142
  console.log(`Starting ${NUM_PLANETS} planets...`);
@@ -155,4 +194,118 @@ describe("Federated Planets Protocol", () => {
155
194
 
156
195
  expect(quorumReached).toBe(true);
157
196
  }, 90000); // 90s test timeout
197
+
198
+ it("should reject a third shuttle when the mutual-neighbor limit (2) is reached", async () => {
199
+ // Towel 3 and Towel 4 are mutual neighbors of each other; limit is 2
200
+ const origin = allPlanets[2]; // Towel 3
201
+ const dest = allPlanets[3]; // Towel 4
202
+ const allUrls = allPlanets.map((p) => p.url);
203
+
204
+ const initiate = (shipId: string) =>
205
+ fetch(`${origin.url}/api/v1/port?action=initiate`, {
206
+ method: "POST",
207
+ headers: { "Content-Type": "application/json" },
208
+ body: JSON.stringify({
209
+ ship_id: shipId,
210
+ destination_url: dest.url,
211
+ departure_timestamp: Date.now(),
212
+ }),
213
+ });
214
+
215
+ // Shuttle 1
216
+ console.log("Initiating shuttle 1 from Towel 3 to Towel 4...");
217
+ const res1 = await initiate("SHUTTLE-A");
218
+ expect(res1.ok).toBe(true);
219
+ const plan1Id = ((await res1.json()) as any).plan.id;
220
+ console.log("Shuttle 1 plan:", plan1Id);
221
+ await waitForPlanQuorum(allUrls, plan1Id, "shuttle 1 quorum");
222
+ // Give origin's handleCommit time to write to its own DB after quorum is reached
223
+ await new Promise((r) => setTimeout(r, 3000));
224
+
225
+ // Shuttle 2
226
+ console.log("Initiating shuttle 2 from Towel 3 to Towel 4...");
227
+ const res2 = await initiate("SHUTTLE-B");
228
+ expect(res2.ok).toBe(true);
229
+ const plan2Id = ((await res2.json()) as any).plan.id;
230
+ console.log("Shuttle 2 plan:", plan2Id);
231
+ await waitForPlanQuorum(allUrls, plan2Id, "shuttle 2 quorum");
232
+ // Give origin's handleCommit time to write to its own DB after quorum is reached
233
+ await new Promise((r) => setTimeout(r, 3000));
234
+
235
+ // Shuttle 3 — should be rejected at origin
236
+ console.log("Initiating shuttle 3 (should be rejected)...");
237
+ const res3 = await initiate("SHUTTLE-C");
238
+ expect(res3.status).toBe(422);
239
+ const data3 = (await res3.json()) as any;
240
+ expect(data3.error).toBe("shuttle_limit_exceeded");
241
+ expect(data3.limit).toBe(2);
242
+ expect(data3.relationship).toBe("mutual_neighbors");
243
+ expect(data3.active_shuttles).toBe(2);
244
+ console.log("Shuttle 3 correctly rejected:", data3);
245
+ }, 180000); // 3 minutes for two quorum rounds
246
+
247
+ it("should refuse incoming travel at the destination when its shuttle limit is already reached", async () => {
248
+ // Use Towel 1 → Towel 3 (mutual neighbors, limit = 2).
249
+ // First establish 2 legitimate shuttles so Towel 3's DB is at the limit.
250
+ // Then use the test-only bypass header to skip Towel 1's origin check —
251
+ // the plan proceeds through consensus but Towel 3 refuses registration.
252
+ const origin = allPlanets[0]; // Towel 1
253
+ const dest = allPlanets[2]; // Towel 3
254
+ const allUrls = allPlanets.map((p) => p.url);
255
+
256
+ const initiate = (shipId: string, bypass = false) =>
257
+ fetch(`${origin.url}/api/v1/port?action=initiate`, {
258
+ method: "POST",
259
+ headers: {
260
+ "Content-Type": "application/json",
261
+ ...(bypass ? { "X-Bypass-Shuttle-Limit": "true" } : {}),
262
+ },
263
+ body: JSON.stringify({
264
+ ship_id: shipId,
265
+ destination_url: dest.url,
266
+ departure_timestamp: Date.now(),
267
+ }),
268
+ });
269
+
270
+ // Fill Towel 3's limit with 2 legitimate shuttles from Towel 1
271
+ console.log("Filling Towel 3 limit with 2 shuttles from Towel 1...");
272
+ const fillRes1 = await initiate("FILL-A");
273
+ expect(fillRes1.ok).toBe(true);
274
+ const fillPlan1 = ((await fillRes1.json()) as any).plan.id;
275
+ await waitForPlanQuorum(allUrls, fillPlan1, "fill shuttle 1 quorum");
276
+
277
+ const fillRes2 = await initiate("FILL-B");
278
+ expect(fillRes2.ok).toBe(true);
279
+ const fillPlan2 = ((await fillRes2.json()) as any).plan.id;
280
+ await waitForPlanQuorum(allUrls, fillPlan2, "fill shuttle 2 quorum");
281
+
282
+ // Now both Towel 1 and Towel 3 have count=2 for the 1↔3 pair.
283
+ // Use the bypass header to skip Towel 1's origin check — the plan enters
284
+ // consensus, but Towel 3 independently checks and refuses registration.
285
+ console.log(
286
+ "Initiating with bypass header — origin allows, destination should refuse...",
287
+ );
288
+ const response = await initiate("DEST-REFUSES", true);
289
+ expect(response.ok).toBe(true);
290
+ const planId = ((await response.json()) as any).plan.id;
291
+ console.log("Bypass plan initiated:", planId);
292
+
293
+ // Confirm no QUORUM_REACHED appears for this plan on any planet
294
+ console.log(
295
+ "Confirming destination refusal (no QUORUM_REACHED expected)...",
296
+ );
297
+ for (let attempt = 0; attempt < 10; attempt++) {
298
+ await new Promise((r) => setTimeout(r, 2000));
299
+ const quorumIds = await collectQuorumPlanIds(allUrls);
300
+ if (quorumIds.has(planId)) {
301
+ throw new Error(
302
+ `Plan ${planId} should have been refused by destination but reached quorum`,
303
+ );
304
+ }
305
+ console.log(
306
+ `No QUORUM_REACHED seen (attempt ${attempt + 1}/10) — destination is refusing`,
307
+ );
308
+ }
309
+ console.log("Confirmed: destination refused the travel plan");
310
+ }, 300000); // 5 minutes: two fill quorum rounds + 20s destination refusal confirmation
158
311
  });
@@ -30,14 +30,11 @@ const cleanup = () => {
30
30
 
31
31
  describe("Warp Links Configuration", () => {
32
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
- );
33
+ console.log(`[${TEST_NAME}] Clearing previous state...`);
34
+ execSync(`rm -rf .wrangler/state/warp-test`, {
35
+ cwd: PROJECT_ROOT,
36
+ stdio: "inherit",
37
+ });
41
38
 
42
39
  console.log(`Starting ${TEST_NAME} on http://${TEST_HOST}:${TEST_PORT}...`);
43
40
 
@@ -6,7 +6,38 @@ export default class TrafficControl extends DurableObject {
6
6
 
7
7
  constructor(state: any, env: any) {
8
8
  super(state, env);
9
- console.log("[TrafficControl] Initialized");
9
+ this.ctx.storage.sql.exec(`
10
+ CREATE TABLE IF NOT EXISTS travel_plans (
11
+ id TEXT PRIMARY KEY,
12
+ ship_id TEXT NOT NULL,
13
+ origin_url TEXT NOT NULL,
14
+ destination_url TEXT NOT NULL,
15
+ start_timestamp INTEGER NOT NULL,
16
+ end_timestamp INTEGER NOT NULL,
17
+ status TEXT CHECK(status IN ('PREPARING', 'PLAN_ACCEPTED')) NOT NULL DEFAULT 'PREPARING',
18
+ signatures TEXT NOT NULL
19
+ );
20
+ CREATE INDEX IF NOT EXISTS idx_travel_plans_active ON travel_plans (end_timestamp, origin_url, destination_url);
21
+
22
+ CREATE TABLE IF NOT EXISTS traffic_controllers (
23
+ planet_url TEXT PRIMARY KEY,
24
+ name TEXT NOT NULL,
25
+ space_port_url TEXT NOT NULL,
26
+ last_manifest_fetch INTEGER NOT NULL
27
+ );
28
+
29
+ CREATE TABLE IF NOT EXISTS identity (
30
+ key TEXT PRIMARY KEY,
31
+ value TEXT NOT NULL
32
+ );
33
+
34
+ CREATE TABLE IF NOT EXISTS consensus_plans (
35
+ id TEXT PRIMARY KEY,
36
+ data TEXT NOT NULL,
37
+ expires_at INTEGER NOT NULL
38
+ );
39
+ `);
40
+ console.log("[TrafficControl] Initialized with SQLite storage");
10
41
  }
11
42
 
12
43
  async fetch(request: Request) {
@@ -20,6 +51,10 @@ export default class TrafficControl extends DurableObject {
20
51
  return new Response("OK");
21
52
  }
22
53
 
54
+ if (url.pathname === "/storage" && request.method === "POST") {
55
+ return this.handleStorage(request);
56
+ }
57
+
23
58
  if (request.headers.get("Upgrade") === "websocket") {
24
59
  const pair = new WebSocketPair();
25
60
  const [client, server] = Object.values(pair);
@@ -40,6 +75,74 @@ export default class TrafficControl extends DurableObject {
40
75
  });
41
76
  }
42
77
 
78
+ private async handleStorage(request: Request): Promise<Response> {
79
+ const body: any = await request.json();
80
+ const { action } = body;
81
+
82
+ switch (action) {
83
+ case "getIdentity": {
84
+ const pub = this.ctx.storage.sql
85
+ .exec("SELECT value FROM identity WHERE key = 'identity_public'")
86
+ .toArray();
87
+ const priv = this.ctx.storage.sql
88
+ .exec("SELECT value FROM identity WHERE key = 'identity_private'")
89
+ .toArray();
90
+ return Response.json({
91
+ public: pub.length > 0 ? (pub[0] as any).value : null,
92
+ private: priv.length > 0 ? (priv[0] as any).value : null,
93
+ });
94
+ }
95
+
96
+ case "setIdentity": {
97
+ this.ctx.storage.sql.exec(
98
+ "INSERT OR REPLACE INTO identity (key, value) VALUES ('identity_public', ?), ('identity_private', ?)",
99
+ body.publicKey,
100
+ body.privateKey,
101
+ );
102
+ return Response.json({ success: true });
103
+ }
104
+
105
+ case "savePlan": {
106
+ const expiresAt = Date.now() + 3600 * 1000;
107
+ this.ctx.storage.sql.exec(
108
+ "INSERT OR REPLACE INTO consensus_plans (id, data, expires_at) VALUES (?, ?, ?)",
109
+ body.planId,
110
+ body.data,
111
+ expiresAt,
112
+ );
113
+ return Response.json({ success: true });
114
+ }
115
+
116
+ case "getPlan": {
117
+ const rows = this.ctx.storage.sql
118
+ .exec(
119
+ "SELECT data FROM consensus_plans WHERE id = ? AND expires_at > ?",
120
+ body.planId,
121
+ Date.now(),
122
+ )
123
+ .toArray();
124
+ return Response.json({
125
+ data: rows.length > 0 ? (rows[0] as any).data : null,
126
+ });
127
+ }
128
+
129
+ case "query": {
130
+ const results = this.ctx.storage.sql
131
+ .exec(body.sql, ...(body.params || []))
132
+ .toArray();
133
+ return Response.json({ results });
134
+ }
135
+
136
+ case "exec": {
137
+ this.ctx.storage.sql.exec(body.sql, ...(body.params || []));
138
+ return Response.json({ success: true });
139
+ }
140
+
141
+ default:
142
+ return Response.json({ error: "Unknown action" }, { status: 400 });
143
+ }
144
+ }
145
+
43
146
  broadcast(message: string) {
44
147
  for (const ws of this.sessions) {
45
148
  try {
@@ -5,20 +5,6 @@
5
5
  "directory": "dist/client",
6
6
  "binding": "STATIC_ASSETS",
7
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
8
  "durable_objects": {
23
9
  "bindings": [
24
10
  {
@@ -30,7 +16,7 @@
30
16
  "migrations": [
31
17
  {
32
18
  "tag": "v1",
33
- "new_classes": ["TrafficControl"],
19
+ "new_sqlite_classes": ["TrafficControl"],
34
20
  },
35
21
  ],
36
22
  }
@@ -6,20 +6,6 @@
6
6
  "directory": "dist/client",
7
7
  "binding": "STATIC_ASSETS",
8
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
9
  "durable_objects": {
24
10
  "bindings": [
25
11
  {
@@ -31,11 +17,12 @@
31
17
  "vars": {
32
18
  "WARP_MS_PER_FY": "10000",
33
19
  "DEPARTURE_BUFFER_MS": "5000",
20
+ "ALLOW_TEST_SHUTTLE_BYPASS": "true",
34
21
  },
35
22
  "migrations": [
36
23
  {
37
24
  "tag": "v1",
38
- "new_classes": ["TrafficControl"],
25
+ "new_sqlite_classes": ["TrafficControl"],
39
26
  },
40
27
  ],
41
28
  }
@@ -1,22 +0,0 @@
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
- );