@spacemolt/lib 4.6.0 → 7.1.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.
- package/COMMANDS.md +19 -13
- package/README.md +61 -6
- package/dist/account.d.ts +44 -15
- package/dist/auth/clerk.d.ts +45 -18
- package/dist/auth/credentials.d.ts +1 -0
- package/dist/auth/file-store.d.ts +1 -1
- package/dist/client.d.ts +22 -2
- package/dist/data/catalog.d.ts +37 -4
- package/dist/events/emitter.d.ts +1 -2
- package/dist/generated/actions.gen.d.ts +2 -2
- package/dist/generated/commands.gen.d.ts +79 -35
- package/dist/generated/notifications.gen.d.ts +4 -3
- package/dist/generated/openapi/types.gen.d.ts +1736 -344
- package/dist/index.d.ts +4 -4
- package/dist/index.js +447 -157
- package/dist/node.js +66 -4
- package/dist/protocol.d.ts +7 -0
- package/dist/transport/correlator.d.ts +2 -1
- package/dist/transport/socket.d.ts +10 -8
- package/dist/validation.d.ts +4 -0
- package/package.json +15 -4
package/dist/index.js
CHANGED
|
@@ -1,4 +1,38 @@
|
|
|
1
|
+
// src/validation.ts
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function requireRecord(value, source) {
|
|
6
|
+
if (!isRecord(value))
|
|
7
|
+
throw new TypeError(`${source} must be a JSON object`);
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
1
11
|
// src/protocol.ts
|
|
12
|
+
function hasPayload(frame) {
|
|
13
|
+
return isRecord(frame.payload);
|
|
14
|
+
}
|
|
15
|
+
function isResultFrame(frame) {
|
|
16
|
+
return frame.type === "result" && hasPayload(frame) && "result" in frame.payload && (frame.payload.structuredContent === undefined || isRecord(frame.payload.structuredContent));
|
|
17
|
+
}
|
|
18
|
+
function isActionResultFrame(frame) {
|
|
19
|
+
return frame.type === "action_result" && hasPayload(frame) && typeof frame.payload.command === "string" && typeof frame.payload.tick === "number" && isRecord(frame.payload.result) && (frame.payload.auto_docked === undefined || typeof frame.payload.auto_docked === "boolean") && (frame.payload.auto_undocked === undefined || typeof frame.payload.auto_undocked === "boolean");
|
|
20
|
+
}
|
|
21
|
+
function isActionErrorFrame(frame) {
|
|
22
|
+
return frame.type === "action_error" && hasPayload(frame) && typeof frame.payload.command === "string" && typeof frame.payload.tick === "number" && typeof frame.payload.code === "string" && typeof frame.payload.message === "string" && (frame.payload.details === undefined || isRecord(frame.payload.details));
|
|
23
|
+
}
|
|
24
|
+
function isErrorFrame(frame) {
|
|
25
|
+
return frame.type === "error" && hasPayload(frame) && typeof frame.payload.code === "string" && typeof frame.payload.message === "string" && (frame.payload.details === undefined || isRecord(frame.payload.details)) && (frame.payload.pending_command === undefined || typeof frame.payload.pending_command === "string");
|
|
26
|
+
}
|
|
27
|
+
function isWelcomeFrame(frame) {
|
|
28
|
+
return frame.type === "welcome" && hasPayload(frame) && typeof frame.payload.version === "string" && typeof frame.payload.release_date === "string" && Array.isArray(frame.payload.release_notes) && frame.payload.release_notes.every((note) => typeof note === "string") && typeof frame.payload.tick_rate === "number" && typeof frame.payload.current_tick === "number" && typeof frame.payload.server_time === "number" && (frame.payload.motd === undefined || typeof frame.payload.motd === "string") && typeof frame.payload.game_info === "string" && typeof frame.payload.website === "string" && typeof frame.payload.help_text === "string" && typeof frame.payload.terms === "string";
|
|
29
|
+
}
|
|
30
|
+
function isLoggedInFrame(frame) {
|
|
31
|
+
return frame.type === "logged_in" && hasPayload(frame);
|
|
32
|
+
}
|
|
33
|
+
function isRegisteredFrame(frame) {
|
|
34
|
+
return frame.type === "registered" && hasPayload(frame) && typeof frame.payload.password === "string" && typeof frame.payload.player_id === "string";
|
|
35
|
+
}
|
|
2
36
|
var STATE_SECTIONS = [
|
|
3
37
|
"player",
|
|
4
38
|
"ship",
|
|
@@ -10,7 +44,7 @@ var STATE_SECTIONS = [
|
|
|
10
44
|
"skills"
|
|
11
45
|
];
|
|
12
46
|
// src/generated/actions.gen.ts
|
|
13
|
-
var GENERATED_SPEC_VERSION = "v0.
|
|
47
|
+
var GENERATED_SPEC_VERSION = "v0.508.0";
|
|
14
48
|
var ACTIONS = {
|
|
15
49
|
"spacemolt_auth/claim": {
|
|
16
50
|
tool: "spacemolt_auth",
|
|
@@ -81,6 +115,19 @@ var ACTIONS = {
|
|
|
81
115
|
{ name: "side_id", type: "number", required: false, description: "Side to join (optional for action=engage — auto-assigned by faction if omitted)" }
|
|
82
116
|
]
|
|
83
117
|
},
|
|
118
|
+
"spacemolt_battle/log": {
|
|
119
|
+
tool: "spacemolt_battle",
|
|
120
|
+
action: "log",
|
|
121
|
+
kind: "query",
|
|
122
|
+
summary: "View the tick-by-tick combat replay of a battle by ID",
|
|
123
|
+
responseType: "GetBattleLogResponse",
|
|
124
|
+
params: [
|
|
125
|
+
{ name: "id", type: "string", required: true, description: "Battle ID to replay (active or completed, yours or not)", positional: 0 },
|
|
126
|
+
{ name: "limit", type: "number", required: false, description: "Max ticks to return (default 50, max 200)" },
|
|
127
|
+
{ name: "tick_end", type: "number", required: false, description: "Last tick to include (default unbounded)" },
|
|
128
|
+
{ name: "tick_start", type: "number", required: false, description: "First tick to include (default 0)" }
|
|
129
|
+
]
|
|
130
|
+
},
|
|
84
131
|
"spacemolt_battle/reload": {
|
|
85
132
|
tool: "spacemolt_battle",
|
|
86
133
|
action: "reload",
|
|
@@ -118,6 +165,16 @@ var ACTIONS = {
|
|
|
118
165
|
responseType: "GetBattleStatusResponse",
|
|
119
166
|
params: []
|
|
120
167
|
},
|
|
168
|
+
"spacemolt_battle/summary": {
|
|
169
|
+
tool: "spacemolt_battle",
|
|
170
|
+
action: "summary",
|
|
171
|
+
kind: "query",
|
|
172
|
+
summary: "View the aggregate result of a battle by ID",
|
|
173
|
+
responseType: "BattleSummaryResponse",
|
|
174
|
+
params: [
|
|
175
|
+
{ name: "id", type: "string", required: true, description: "Battle ID to summarize (active or completed, yours or not)", positional: 0 }
|
|
176
|
+
]
|
|
177
|
+
},
|
|
121
178
|
"spacemolt_battle/target": {
|
|
122
179
|
tool: "spacemolt_battle",
|
|
123
180
|
action: "target",
|
|
@@ -322,10 +379,10 @@ var ACTIONS = {
|
|
|
322
379
|
tool: "spacemolt_facility",
|
|
323
380
|
action: "buy_ship_license",
|
|
324
381
|
kind: "mutation",
|
|
325
|
-
summary: "
|
|
382
|
+
summary: "License a specific ship design so your faction can build it at its own stations",
|
|
326
383
|
detailsType: "ShipLicenseResponse",
|
|
327
384
|
params: [
|
|
328
|
-
{ name: "
|
|
385
|
+
{ name: "ship_class", type: "string", required: true, description: "Ship class id to license (from ship_catalog), e.g. solarian_frigate" }
|
|
329
386
|
]
|
|
330
387
|
},
|
|
331
388
|
"spacemolt_facility/cancel_listing": {
|
|
@@ -348,6 +405,16 @@ var ACTIONS = {
|
|
|
348
405
|
{ name: "name", type: "string", required: true, description: "Name for the new faction outpost" }
|
|
349
406
|
]
|
|
350
407
|
},
|
|
408
|
+
"spacemolt_facility/disassemble": {
|
|
409
|
+
tool: "spacemolt_facility",
|
|
410
|
+
action: "disassemble",
|
|
411
|
+
kind: "mutation",
|
|
412
|
+
summary: "Manage facilities at stations (production, faction, personal, sales, and more)",
|
|
413
|
+
detailsType: "FacilityResponse",
|
|
414
|
+
params: [
|
|
415
|
+
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'dismantle', 'disassemble', 'faction_dismantle', 'faction_disassemble', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 }
|
|
416
|
+
]
|
|
417
|
+
},
|
|
351
418
|
"spacemolt_facility/dismantle": {
|
|
352
419
|
tool: "spacemolt_facility",
|
|
353
420
|
action: "dismantle",
|
|
@@ -355,7 +422,7 @@ var ACTIONS = {
|
|
|
355
422
|
summary: "Manage facilities at stations (production, faction, personal, sales, and more)",
|
|
356
423
|
detailsType: "FacilityResponse",
|
|
357
424
|
params: [
|
|
358
|
-
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 }
|
|
425
|
+
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'dismantle', 'disassemble', 'faction_dismantle', 'faction_disassemble', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 }
|
|
359
426
|
]
|
|
360
427
|
},
|
|
361
428
|
"spacemolt_facility/facility_set_description": {
|
|
@@ -366,7 +433,7 @@ var ACTIONS = {
|
|
|
366
433
|
responseType: "FacilityResponse",
|
|
367
434
|
params: [
|
|
368
435
|
{ name: "description", type: "string", required: false, description: "For 'personal_decorate': a text description of your personal quarters (what visitors see, hear, and feel). For 'set_description': a custom description (max 4000 chars) for any facility you own, overriding its default flavor text — e.g. re-flavoring your faction's bar. Send empty to clear it." },
|
|
369
|
-
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 }
|
|
436
|
+
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'dismantle', 'disassemble', 'faction_dismantle', 'faction_disassemble', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 }
|
|
370
437
|
]
|
|
371
438
|
},
|
|
372
439
|
"spacemolt_facility/faction_build": {
|
|
@@ -380,6 +447,16 @@ var ACTIONS = {
|
|
|
380
447
|
{ name: "facility_type", type: "string", required: true, description: "Facility type ID. For 'types' action: get full details for this specific type. For 'build'/'upgrade': the type to build/upgrade to.", positional: 0 }
|
|
381
448
|
]
|
|
382
449
|
},
|
|
450
|
+
"spacemolt_facility/faction_disassemble": {
|
|
451
|
+
tool: "spacemolt_facility",
|
|
452
|
+
action: "faction_disassemble",
|
|
453
|
+
kind: "mutation",
|
|
454
|
+
summary: "Manage facilities at stations (production, faction, personal, sales, and more)",
|
|
455
|
+
detailsType: "FacilityResponse",
|
|
456
|
+
params: [
|
|
457
|
+
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'dismantle', 'disassemble', 'faction_dismantle', 'faction_disassemble', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 }
|
|
458
|
+
]
|
|
459
|
+
},
|
|
383
460
|
"spacemolt_facility/faction_dismantle": {
|
|
384
461
|
tool: "spacemolt_facility",
|
|
385
462
|
action: "faction_dismantle",
|
|
@@ -387,7 +464,7 @@ var ACTIONS = {
|
|
|
387
464
|
summary: "Manage facilities at stations (production, faction, personal, sales, and more)",
|
|
388
465
|
detailsType: "FacilityResponse",
|
|
389
466
|
params: [
|
|
390
|
-
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 }
|
|
467
|
+
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'dismantle', 'disassemble', 'faction_dismantle', 'faction_disassemble', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 }
|
|
391
468
|
]
|
|
392
469
|
},
|
|
393
470
|
"spacemolt_facility/faction_list": {
|
|
@@ -414,7 +491,7 @@ var ACTIONS = {
|
|
|
414
491
|
detailsType: "FacilityResponse",
|
|
415
492
|
params: [
|
|
416
493
|
{ name: "bucket", type: "string", required: false, description: "For 'faction_build'/'faction_upgrade': a Storage Extension bucket (name or id) to source build/upgrade MATERIALS from, instead of the faction main store. Ship cargo backfills either way." },
|
|
417
|
-
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 },
|
|
494
|
+
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'dismantle', 'disassemble', 'faction_dismantle', 'faction_disassemble', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 },
|
|
418
495
|
{ name: "facility_type", type: "string", required: true, description: "Facility type ID. For 'types' action: get full details for this specific type. For 'build'/'upgrade': the type to build/upgrade to.", positional: 0 }
|
|
419
496
|
]
|
|
420
497
|
},
|
|
@@ -438,7 +515,7 @@ var ACTIONS = {
|
|
|
438
515
|
params: [
|
|
439
516
|
{ name: "deliver_to", type: "string", required: false, description: "Output destination for 'job_add': 'storage' (default), 'faction' (faction main store), or 'faction:<bucket name or id>' for a Storage Extension bucket." },
|
|
440
517
|
{ name: "direction", type: '"forward" | "reverse"', required: false, description: "Job direction: 'forward' runs the recipe normally (craft); 'reverse' recycles existing items back to inputs.", enumValues: ["forward", "reverse"] },
|
|
441
|
-
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 },
|
|
518
|
+
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'dismantle', 'disassemble', 'faction_dismantle', 'faction_disassemble', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 },
|
|
442
519
|
{ name: "quantity", type: "number", required: false, description: "For 'job_add': number of runs to queue." },
|
|
443
520
|
{ name: "recipe_id", type: "string", required: true, description: "Recipe ID to run (for 'job_add' action)." },
|
|
444
521
|
{ name: "source", type: "string", required: false, description: "Input source for 'job_add': where inputs/credits are pulled from. Same values as deliver_to; defaults to deliver_to." }
|
|
@@ -462,7 +539,7 @@ var ACTIONS = {
|
|
|
462
539
|
summary: "Manage facilities at stations (production, faction, personal, sales, and more)",
|
|
463
540
|
responseType: "FacilityResponse",
|
|
464
541
|
params: [
|
|
465
|
-
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 }
|
|
542
|
+
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'dismantle', 'disassemble', 'faction_dismantle', 'faction_disassemble', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 }
|
|
466
543
|
]
|
|
467
544
|
},
|
|
468
545
|
"spacemolt_facility/job_reorder": {
|
|
@@ -472,7 +549,7 @@ var ACTIONS = {
|
|
|
472
549
|
summary: "Manage facilities at stations (production, faction, personal, sales, and more)",
|
|
473
550
|
detailsType: "FacilityResponse",
|
|
474
551
|
params: [
|
|
475
|
-
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 },
|
|
552
|
+
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'dismantle', 'disassemble', 'faction_dismantle', 'faction_disassemble', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 },
|
|
476
553
|
{ name: "job_id", type: "string", required: true, description: "Job ID (for 'job_cancel', 'job_reorder'). Use action 'job_list' to see job IDs." },
|
|
477
554
|
{ name: "position", type: "number", required: false, description: "New queue position for 'job_reorder' (1-based)." }
|
|
478
555
|
]
|
|
@@ -492,7 +569,7 @@ var ACTIONS = {
|
|
|
492
569
|
summary: "Manage facilities at stations (production, faction, personal, sales, and more)",
|
|
493
570
|
detailsType: "FacilityResponse",
|
|
494
571
|
params: [
|
|
495
|
-
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 },
|
|
572
|
+
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'dismantle', 'disassemble', 'faction_dismantle', 'faction_disassemble', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 },
|
|
496
573
|
{ name: "faction", type: "boolean", required: false, description: "For 'list_for_sale': set true to list a faction-owned facility (requires manage_facilities permission)." },
|
|
497
574
|
{ name: "price", type: "number", required: true, description: "For 'list_for_sale': asking price in whole credits. For 'set_output_price': per-produced-unit rental price others pay, applied to the facility's recipe output(s) automatically — may be fractional (e.g. 0.25), the per-run fee is price × output quantity rounded to a whole credit. Used literally: 0 rents the facility out for free; negative is rejected." }
|
|
498
575
|
]
|
|
@@ -556,6 +633,16 @@ var ACTIONS = {
|
|
|
556
633
|
{ name: "player", type: "string", required: true, description: "Target player id or username (allow_player/remove_player/ban/unban)" }
|
|
557
634
|
]
|
|
558
635
|
},
|
|
636
|
+
"spacemolt_facility/repair": {
|
|
637
|
+
tool: "spacemolt_facility",
|
|
638
|
+
action: "repair",
|
|
639
|
+
kind: "mutation",
|
|
640
|
+
summary: "Manage facilities at stations (production, faction, personal, sales, and more)",
|
|
641
|
+
detailsType: "FacilityResponse",
|
|
642
|
+
params: [
|
|
643
|
+
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'dismantle', 'disassemble', 'faction_dismantle', 'faction_disassemble', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 }
|
|
644
|
+
]
|
|
645
|
+
},
|
|
559
646
|
"spacemolt_facility/set_access": {
|
|
560
647
|
tool: "spacemolt_facility",
|
|
561
648
|
action: "set_access",
|
|
@@ -564,7 +651,17 @@ var ACTIONS = {
|
|
|
564
651
|
detailsType: "FacilityResponse",
|
|
565
652
|
params: [
|
|
566
653
|
{ name: "access", type: '"private" | "public"', required: true, description: "For 'personal_decorate': who can visit your quarters. For 'set_access': 'public' opens your facility to renters, 'private' closes it.", enumValues: ["private", "public"] },
|
|
567
|
-
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 }
|
|
654
|
+
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'dismantle', 'disassemble', 'faction_dismantle', 'faction_disassemble', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 }
|
|
655
|
+
]
|
|
656
|
+
},
|
|
657
|
+
"spacemolt_facility/set_auto_buy_fuel": {
|
|
658
|
+
tool: "spacemolt_facility",
|
|
659
|
+
action: "set_auto_buy_fuel",
|
|
660
|
+
kind: "mutation",
|
|
661
|
+
summary: "Administer one of your faction's stations or outposts: rename, access control, and build policy",
|
|
662
|
+
detailsType: "StationConfigResponse",
|
|
663
|
+
params: [
|
|
664
|
+
{ name: "auto_buy_fuel", type: "boolean", required: false, description: "Whether the station automatically buys fuel from docked pilots at live scarcity-based prices, funded by the station economy (set_auto_buy_fuel; off by default)" }
|
|
568
665
|
]
|
|
569
666
|
},
|
|
570
667
|
"spacemolt_facility/set_build_policy": {
|
|
@@ -605,7 +702,7 @@ var ACTIONS = {
|
|
|
605
702
|
detailsType: "FacilityResponse",
|
|
606
703
|
params: [
|
|
607
704
|
{ name: "custom_name", type: "string", required: false, description: "For 'set_name': a custom name for the facility (3-32 chars) so multiple facilities of the same type stand out. Send empty to clear it." },
|
|
608
|
-
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 }
|
|
705
|
+
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'dismantle', 'disassemble', 'faction_dismantle', 'faction_disassemble', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 }
|
|
609
706
|
]
|
|
610
707
|
},
|
|
611
708
|
"spacemolt_facility/set_output_price": {
|
|
@@ -615,7 +712,7 @@ var ACTIONS = {
|
|
|
615
712
|
summary: "Manage facilities at stations (production, faction, personal, sales, and more)",
|
|
616
713
|
detailsType: "FacilityResponse",
|
|
617
714
|
params: [
|
|
618
|
-
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 },
|
|
715
|
+
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'dismantle', 'disassemble', 'faction_dismantle', 'faction_disassemble', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 },
|
|
619
716
|
{ name: "price", type: "number", required: false, description: "For 'list_for_sale': asking price in whole credits. For 'set_output_price': per-produced-unit rental price others pay, applied to the facility's recipe output(s) automatically — may be fractional (e.g. 0.25), the per-run fee is price × output quantity rounded to a whole credit. Used literally: 0 rents the facility out for free; negative is rejected." }
|
|
620
717
|
]
|
|
621
718
|
},
|
|
@@ -686,7 +783,7 @@ var ACTIONS = {
|
|
|
686
783
|
detailsType: "FacilityResponse",
|
|
687
784
|
params: [
|
|
688
785
|
{ name: "direction", type: '"to_faction" | "to_player"', required: true, description: "Transfer direction: 'to_faction' moves the facility to faction ownership; 'to_player' transfers it to a specific player.", enumValues: ["to_faction", "to_player"] },
|
|
689
|
-
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 },
|
|
786
|
+
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'dismantle', 'disassemble', 'faction_dismantle', 'faction_disassemble', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 },
|
|
690
787
|
{ name: "player_id", type: "string", required: false, description: "Target player ID for 'transfer' action with direction 'to_player'." }
|
|
691
788
|
]
|
|
692
789
|
},
|
|
@@ -723,7 +820,7 @@ var ACTIONS = {
|
|
|
723
820
|
detailsType: "FacilityResponse",
|
|
724
821
|
params: [
|
|
725
822
|
{ name: "bucket", type: "string", required: false, description: "For 'faction_build'/'faction_upgrade': a Storage Extension bucket (name or id) to source build/upgrade MATERIALS from, instead of the faction main store. Ship cargo backfills either way." },
|
|
726
|
-
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 },
|
|
823
|
+
{ name: "facility_id", type: "string", required: true, description: "Facility instance ID (required for 'upgrade', 'dismantle', 'disassemble', 'faction_dismantle', 'faction_disassemble', 'job_add', 'job_list', 'set_output_price', 'set_access', 'set_name', 'set_description' actions). Use action 'list' to see facility IDs.", positional: 1 },
|
|
727
824
|
{ name: "facility_type", type: "string", required: true, description: "Facility type ID. For 'types' action: get full details for this specific type. For 'build'/'upgrade': the type to build/upgrade to.", positional: 0 }
|
|
728
825
|
]
|
|
729
826
|
},
|
|
@@ -1412,9 +1509,7 @@ var ACTIONS = {
|
|
|
1412
1509
|
kind: "mutation",
|
|
1413
1510
|
summary: "Purchase ship insurance",
|
|
1414
1511
|
detailsType: "BuyInsuranceResponse",
|
|
1415
|
-
params: [
|
|
1416
|
-
{ name: "ticks", type: "number", required: true, description: "Number of ticks to insure for" }
|
|
1417
|
-
]
|
|
1512
|
+
params: []
|
|
1418
1513
|
},
|
|
1419
1514
|
"spacemolt_salvage/loot": {
|
|
1420
1515
|
tool: "spacemolt_salvage",
|
|
@@ -1566,8 +1661,9 @@ var ACTIONS = {
|
|
|
1566
1661
|
summary: "Commission a ship to be built at this shipyard",
|
|
1567
1662
|
detailsType: "CommissionShipResponse",
|
|
1568
1663
|
params: [
|
|
1664
|
+
{ name: "fund_from_faction", type: "boolean", required: false, description: "At your own faction's station: build from faction storage and treasury (requires ManageTreasury). Required there; credits-only and provide_materials are rejected." },
|
|
1569
1665
|
{ name: "id", type: "string", required: true, description: "Ship class ID to commission (use ship_catalog to see options)", positional: 0 },
|
|
1570
|
-
{ name: "provide_materials", type: "boolean", required: false, description: "
|
|
1666
|
+
{ name: "provide_materials", type: "boolean", required: false, description: "At an empire/NPC shipyard: if true, supply build materials from cargo/storage (cheaper); if false, pay credits for everything (default)." }
|
|
1571
1667
|
]
|
|
1572
1668
|
},
|
|
1573
1669
|
"spacemolt_ship/commission_status": {
|
|
@@ -1638,16 +1734,6 @@ var ACTIONS = {
|
|
|
1638
1734
|
{ name: "id", type: "string", required: true, description: "ID of the ship to permanently destroy (no credits returned). Remote order: works from anywhere on a ship parked at any station; cargo and modules are recovered to your storage at the station where the ship was parked. Use list_ships to see your fleet.", positional: 0 }
|
|
1639
1735
|
]
|
|
1640
1736
|
},
|
|
1641
|
-
"spacemolt_ship/sell_ship": {
|
|
1642
|
-
tool: "spacemolt_ship",
|
|
1643
|
-
action: "sell_ship",
|
|
1644
|
-
kind: "mutation",
|
|
1645
|
-
summary: "Sell a stored ship at the current station",
|
|
1646
|
-
detailsType: "SellShipResponse",
|
|
1647
|
-
params: [
|
|
1648
|
-
{ name: "id", type: "string", required: true, description: "ID of the stored ship to sell (use list_ships to see your fleet)", positional: 0 }
|
|
1649
|
-
]
|
|
1650
|
-
},
|
|
1651
1737
|
"spacemolt_ship/sell_ship_to_order": {
|
|
1652
1738
|
tool: "spacemolt_ship",
|
|
1653
1739
|
action: "sell_ship_to_order",
|
|
@@ -2117,10 +2203,10 @@ var ACTIONS = {
|
|
|
2117
2203
|
tool: "spacemolt",
|
|
2118
2204
|
action: "attack",
|
|
2119
2205
|
kind: "mutation",
|
|
2120
|
-
summary: "Attack another player, pirate,
|
|
2206
|
+
summary: "Attack another player, pirate, empire NPC, creature, or station",
|
|
2121
2207
|
detailsType: "AttackResponse",
|
|
2122
2208
|
params: [
|
|
2123
|
-
{ name: "id", type: "string", required: true, description: "
|
|
2209
|
+
{ name: "id", type: "string", required: true, description: "ID of the target: a player, pirate, empire NPC, wildlife creature, or station. Opening fire on a station starts a siege; shelling an empire station is a serious crime.", positional: 0 }
|
|
2124
2210
|
]
|
|
2125
2211
|
},
|
|
2126
2212
|
"spacemolt/buy": {
|
|
@@ -2180,7 +2266,7 @@ var ACTIONS = {
|
|
|
2180
2266
|
{ name: "job_id", type: "string", required: false, description: "Cancel this queued job (refunding its unconsumed inputs, labor, and rental fee) instead of crafting. Use action='queue' to list your job IDs. Setting job_id implies cancel." },
|
|
2181
2267
|
{ name: "job_ids", type: "string[]", required: false, description: "Bulk cancel: cancel many queued jobs in one action. Each ID is cancelled independently with per-job success/failure, so one bad ID doesn't sink the batch. Refunds the unconsumed escrow of every cancelled job." },
|
|
2182
2268
|
{ name: "jobs", type: "Record<string, unknown>[]", required: false, description: "Bulk mode: queue many crafts in one action. Each entry: {recipe_id, quantity, facility_id?, preset?, deliver_to?, source?}. When set, top-level recipe_id/quantity are ignored; each job is queued independently (partial success). Max 50." },
|
|
2183
|
-
{ name: "preset", type: '"fast" | "cheap" | "workshop"', required: false, description: "Auto-routing preset: 'fast' (fewest ticks, default) or 'cheap' (lowest fee). Auto-routing prefers your own facility, then your faction's, then a public rental, and only hand-crafts at the Station Workshop if none is available. Use 'workshop' to force hand-crafting even when you have a facility.", enumValues: ["fast", "cheap", "workshop"] },
|
|
2269
|
+
{ name: "preset", type: '"fast" | "cheap" | "prefer_own" | "workshop"', required: false, description: "Auto-routing preset: 'fast' (fewest ticks, default) or 'cheap' (lowest fee) — both pick the best facility globally, so a busy own facility may route to an idle public rental. Use 'prefer_own' to keep the job on your own (then faction) facility and only rent a public one when you have none that can run it. Auto-routing otherwise prefers your own facility, then your faction's, then a public rental, and only hand-crafts at the Station Workshop if none is available. Use 'workshop' to force hand-crafting even when you have a facility.", enumValues: ["fast", "cheap", "prefer_own", "workshop"] },
|
|
2184
2270
|
{ name: "quantity", type: "number", required: false, description: "Number of output items to make (default 1). Rounded up to a whole number of production runs, so a recipe that yields several items per run may produce a few extra.", positional: 1 },
|
|
2185
2271
|
{ name: "source", type: "string", required: false, description: "Where inputs and labor/rental credits are pulled FROM. Same values as deliver_to: 'storage', 'faction', or 'faction:<bucket>'. Defaults to deliver_to, so inputs and outputs share one store unless you set them differently — e.g. source='storage' deliver_to='faction:Crafting' pulls from your personal storage and deposits into a faction bucket." }
|
|
2186
2272
|
]
|
|
@@ -2441,6 +2527,16 @@ var ACTIONS = {
|
|
|
2441
2527
|
{ name: "id", type: "string", required: true, description: "Creature ID to hunt (from the 'creatures' list in get_nearby)", positional: 0 }
|
|
2442
2528
|
]
|
|
2443
2529
|
},
|
|
2530
|
+
"spacemolt/inspect": {
|
|
2531
|
+
tool: "spacemolt",
|
|
2532
|
+
action: "inspect",
|
|
2533
|
+
kind: "query",
|
|
2534
|
+
summary: "Inspect an item, module, ship class, system, visible POI, or docked base by ID",
|
|
2535
|
+
responseType: "InspectResponse",
|
|
2536
|
+
params: [
|
|
2537
|
+
{ name: "id", type: "string", required: true, description: "Item, module, ship class, system, POI, or base ID to inspect", positional: 0 }
|
|
2538
|
+
]
|
|
2539
|
+
},
|
|
2444
2540
|
"spacemolt/install_mod": {
|
|
2445
2541
|
tool: "spacemolt",
|
|
2446
2542
|
action: "install_mod",
|
|
@@ -2530,7 +2626,8 @@ var ACTIONS = {
|
|
|
2530
2626
|
{ name: "id", type: "string", required: false, description: "Recipe ID to recycle (the recycler consumes the recipe's outputs and returns a lossy fraction of its inputs). Use catalog with type=recipes to browse.", positional: 0 },
|
|
2531
2627
|
{ name: "job_id", type: "string", required: false, description: "Cancel this queued job (refunding its unconsumed escrow) instead of recycling. Setting job_id implies cancel." },
|
|
2532
2628
|
{ name: "job_ids", type: "string[]", required: false, description: "Bulk cancel: cancel many queued jobs in one action. Each ID is cancelled independently with per-job success/failure, so one bad ID doesn't sink the batch. Refunds the unconsumed escrow of every cancelled job." },
|
|
2533
|
-
{ name: "jobs", type: "Record<string, unknown>[]", required: false, description: "Bulk mode: recycle many recipes in one action. Each entry: {recipe_id, quantity, facility_id?, deliver_to?, source?}. When set, top-level recipe_id/quantity are ignored; each job is processed independently (partial success). Max 50." },
|
|
2629
|
+
{ name: "jobs", type: "Record<string, unknown>[]", required: false, description: "Bulk mode: recycle many recipes in one action. Each entry: {recipe_id, quantity, facility_id?, preset?, deliver_to?, source?}. When set, top-level recipe_id/quantity are ignored; each job is processed independently (partial success). Max 50." },
|
|
2630
|
+
{ name: "preset", type: '"fast" | "cheap" | "prefer_own"', required: false, description: "Auto-routing preset: 'fast' (fewest ticks, default) or 'cheap' (lowest fee) — both pick the best eligible recycler globally. Use 'prefer_own' to keep the job on your own (then faction) recycler whenever one can run it. 'workshop' doesn't apply — recycling always needs a real recycler facility.", enumValues: ["fast", "cheap", "prefer_own"] },
|
|
2534
2631
|
{ name: "quantity", type: "number", required: false, description: "Number of the recipe's output items to feed in and break down (default 1). Rounded up to a whole number of recycling runs.", positional: 1 },
|
|
2535
2632
|
{ name: "source", type: "string", required: false, description: "Where feedstock (and labor/rental credits) are pulled FROM. Same values as deliver_to. Defaults to deliver_to." }
|
|
2536
2633
|
]
|
|
@@ -2659,10 +2756,11 @@ var ACTIONS = {
|
|
|
2659
2756
|
tool: "spacemolt",
|
|
2660
2757
|
action: "unload_passenger",
|
|
2661
2758
|
kind: "mutation",
|
|
2662
|
-
summary: "Put a passenger (or everyone) off the ship
|
|
2759
|
+
summary: "Put a passenger (or everyone) off the ship here — or hand them off to another ship or your faction's transit lounge for a connecting flight",
|
|
2663
2760
|
detailsType: "UnloadPassengerResponse",
|
|
2664
2761
|
params: [
|
|
2665
|
-
{ name: "id", type: "string", required: true, description: 'Name (or citizen ID) of the passenger to put off the ship at the current station, or "all" to put every passenger off at once.', positional: 0 }
|
|
2762
|
+
{ name: "id", type: "string", required: true, description: 'Name (or citizen ID) of the passenger to put off the ship at the current station, or "all" to put every passenger off at once.', positional: 0 },
|
|
2763
|
+
{ name: "target", type: "string", required: false, description: `Optional connecting-flight handoff: "lounge" checks the passenger(s) into your faction's Transit Lounge here, or a ship ID/name transfers them to that docked ship (yours or a faction mate's, needs free berths). Fares and deadlines continue unchanged. Omit for a normal delivery/strand.` }
|
|
2666
2764
|
]
|
|
2667
2765
|
},
|
|
2668
2766
|
"spacemolt/unsubscribe_observation": {
|
|
@@ -2738,6 +2836,7 @@ function buildCommands(dispatch) {
|
|
|
2738
2836
|
get_tax_estimate: bind("spacemolt", "get_tax_estimate"),
|
|
2739
2837
|
get_version: bind("spacemolt", "get_version"),
|
|
2740
2838
|
hunt: bind("spacemolt", "hunt"),
|
|
2839
|
+
inspect: bind("spacemolt", "inspect"),
|
|
2741
2840
|
install_mod: bind("spacemolt", "install_mod"),
|
|
2742
2841
|
jettison: bind("spacemolt", "jettison"),
|
|
2743
2842
|
jump: bind("spacemolt", "jump"),
|
|
@@ -2774,10 +2873,12 @@ function buildCommands(dispatch) {
|
|
|
2774
2873
|
spacemolt_battle: {
|
|
2775
2874
|
advance: bind("spacemolt_battle", "advance"),
|
|
2776
2875
|
engage: bind("spacemolt_battle", "engage"),
|
|
2876
|
+
log: bind("spacemolt_battle", "log"),
|
|
2777
2877
|
reload: bind("spacemolt_battle", "reload"),
|
|
2778
2878
|
retreat: bind("spacemolt_battle", "retreat"),
|
|
2779
2879
|
stance: bind("spacemolt_battle", "stance"),
|
|
2780
2880
|
status: bind("spacemolt_battle", "status"),
|
|
2881
|
+
summary: bind("spacemolt_battle", "summary"),
|
|
2781
2882
|
target: bind("spacemolt_battle", "target")
|
|
2782
2883
|
},
|
|
2783
2884
|
spacemolt_citizenship: {
|
|
@@ -2807,9 +2908,11 @@ function buildCommands(dispatch) {
|
|
|
2807
2908
|
buy_ship_license: bind("spacemolt_facility", "buy_ship_license"),
|
|
2808
2909
|
cancel_listing: bind("spacemolt_facility", "cancel_listing"),
|
|
2809
2910
|
deploy_outpost: bind("spacemolt_facility", "deploy_outpost"),
|
|
2911
|
+
disassemble: bind("spacemolt_facility", "disassemble"),
|
|
2810
2912
|
dismantle: bind("spacemolt_facility", "dismantle"),
|
|
2811
2913
|
facility_set_description: bind("spacemolt_facility", "facility_set_description"),
|
|
2812
2914
|
faction_build: bind("spacemolt_facility", "faction_build"),
|
|
2915
|
+
faction_disassemble: bind("spacemolt_facility", "faction_disassemble"),
|
|
2813
2916
|
faction_dismantle: bind("spacemolt_facility", "faction_dismantle"),
|
|
2814
2917
|
faction_list: bind("spacemolt_facility", "faction_list"),
|
|
2815
2918
|
faction_owned: bind("spacemolt_facility", "faction_owned"),
|
|
@@ -2827,7 +2930,9 @@ function buildCommands(dispatch) {
|
|
|
2827
2930
|
personal_visit: bind("spacemolt_facility", "personal_visit"),
|
|
2828
2931
|
remove_faction: bind("spacemolt_facility", "remove_faction"),
|
|
2829
2932
|
remove_player: bind("spacemolt_facility", "remove_player"),
|
|
2933
|
+
repair: bind("spacemolt_facility", "repair"),
|
|
2830
2934
|
set_access: bind("spacemolt_facility", "set_access"),
|
|
2935
|
+
set_auto_buy_fuel: bind("spacemolt_facility", "set_auto_buy_fuel"),
|
|
2831
2936
|
set_build_policy: bind("spacemolt_facility", "set_build_policy"),
|
|
2832
2937
|
set_description: bind("spacemolt_facility", "set_description"),
|
|
2833
2938
|
set_market_fee: bind("spacemolt_facility", "set_market_fee"),
|
|
@@ -2948,7 +3053,6 @@ function buildCommands(dispatch) {
|
|
|
2948
3053
|
refit_ship: bind("spacemolt_ship", "refit_ship"),
|
|
2949
3054
|
rename_ship: bind("spacemolt_ship", "rename_ship"),
|
|
2950
3055
|
scrap_ship: bind("spacemolt_ship", "scrap_ship"),
|
|
2951
|
-
sell_ship: bind("spacemolt_ship", "sell_ship"),
|
|
2952
3056
|
sell_ship_to_order: bind("spacemolt_ship", "sell_ship_to_order"),
|
|
2953
3057
|
supply_commission: bind("spacemolt_ship", "supply_commission"),
|
|
2954
3058
|
switch_ship: bind("spacemolt_ship", "switch_ship"),
|
|
@@ -2999,11 +3103,26 @@ function buildCommands(dispatch) {
|
|
|
2999
3103
|
}
|
|
3000
3104
|
|
|
3001
3105
|
// src/auth/clerk.ts
|
|
3002
|
-
async function
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3106
|
+
async function resolveAuthHeaders(auth) {
|
|
3107
|
+
if (auth.apiKey === undefined && auth.headers === undefined) {
|
|
3108
|
+
throw new Error("Clerk auth requires an `apiKey` or `headers`");
|
|
3109
|
+
}
|
|
3110
|
+
const headers = new Headers(auth.apiKey === undefined ? undefined : { authorization: `Bearer ${auth.apiKey}` });
|
|
3111
|
+
const extra = typeof auth.headers === "function" ? await auth.headers() : auth.headers;
|
|
3112
|
+
new Headers(extra).forEach((value, key) => {
|
|
3113
|
+
headers.set(key, value);
|
|
3114
|
+
});
|
|
3115
|
+
return headers;
|
|
3116
|
+
}
|
|
3117
|
+
async function clerkFetch(fetchImpl, url, auth, init) {
|
|
3118
|
+
const headers = new Headers({ accept: "application/json" });
|
|
3119
|
+
(await resolveAuthHeaders(auth)).forEach((value, key) => {
|
|
3120
|
+
headers.set(key, value);
|
|
3121
|
+
});
|
|
3122
|
+
new Headers(init?.headers).forEach((value, key) => {
|
|
3123
|
+
headers.set(key, value);
|
|
3006
3124
|
});
|
|
3125
|
+
const res = await fetchImpl(url, { ...init, headers });
|
|
3007
3126
|
if (!res.ok) {
|
|
3008
3127
|
const body = await res.text().catch(() => "");
|
|
3009
3128
|
throw new Error(`${init?.method ?? "GET"} ${url} -> ${res.status} ${res.statusText}${body ? `: ${body}` : ""}`);
|
|
@@ -3012,8 +3131,10 @@ async function clerkFetch(fetchImpl, url, apiKey, init) {
|
|
|
3012
3131
|
}
|
|
3013
3132
|
async function mintWsToken(opts) {
|
|
3014
3133
|
const base = opts.httpBaseUrl.replace(/\/$/, "");
|
|
3015
|
-
const data = await clerkFetch(opts.fetchImpl ?? fetch, `${base}/api/player/${encodeURIComponent(opts.playerId)}/ws-token`, opts
|
|
3016
|
-
|
|
3134
|
+
const data = requireRecord(await clerkFetch(opts.fetchImpl ?? fetch, `${base}/api/player/${encodeURIComponent(opts.playerId)}/ws-token`, opts, {
|
|
3135
|
+
method: "POST"
|
|
3136
|
+
}), "ws-token response");
|
|
3137
|
+
if (typeof data.token !== "string" || !data.token)
|
|
3017
3138
|
throw new Error("ws-token response had no token");
|
|
3018
3139
|
return data.token;
|
|
3019
3140
|
}
|
|
@@ -3024,18 +3145,34 @@ class ClerkSource {
|
|
|
3024
3145
|
httpBaseUrl;
|
|
3025
3146
|
constructor(opts) {
|
|
3026
3147
|
this.opts = opts;
|
|
3148
|
+
if (opts.apiKey === undefined && opts.headers === undefined) {
|
|
3149
|
+
throw new Error("ClerkSource requires an `apiKey` or `headers`");
|
|
3150
|
+
}
|
|
3027
3151
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
3028
3152
|
this.httpBaseUrl = opts.httpBaseUrl.replace(/\/$/, "");
|
|
3029
3153
|
}
|
|
3030
3154
|
get apiKey() {
|
|
3031
3155
|
return this.opts.apiKey;
|
|
3032
3156
|
}
|
|
3157
|
+
async fetchRegistration() {
|
|
3158
|
+
const data = requireRecord(await clerkFetch(this.fetchImpl, `${this.httpBaseUrl}/api/registration-code`, this.opts), "registration-code response");
|
|
3159
|
+
const players = Array.isArray(data.players) ? data.players.filter((player) => isRecord(player) && typeof player.id === "string" && typeof player.username === "string" && typeof player.empire === "string" && typeof player.hidden === "boolean") : [];
|
|
3160
|
+
return {
|
|
3161
|
+
registrationCode: typeof data.registration_code === "string" ? data.registration_code : "",
|
|
3162
|
+
players
|
|
3163
|
+
};
|
|
3164
|
+
}
|
|
3033
3165
|
async listPlayers() {
|
|
3034
|
-
|
|
3035
|
-
return data.players ?? [];
|
|
3166
|
+
return (await this.fetchRegistration()).players;
|
|
3036
3167
|
}
|
|
3037
3168
|
mintWsToken(playerId) {
|
|
3038
|
-
return mintWsToken({
|
|
3169
|
+
return mintWsToken({
|
|
3170
|
+
httpBaseUrl: this.httpBaseUrl,
|
|
3171
|
+
apiKey: this.opts.apiKey,
|
|
3172
|
+
headers: this.opts.headers,
|
|
3173
|
+
playerId,
|
|
3174
|
+
fetchImpl: this.fetchImpl
|
|
3175
|
+
});
|
|
3039
3176
|
}
|
|
3040
3177
|
}
|
|
3041
3178
|
|
|
@@ -3112,7 +3249,7 @@ class EventStream {
|
|
|
3112
3249
|
if (next)
|
|
3113
3250
|
next({ value, done: false });
|
|
3114
3251
|
else
|
|
3115
|
-
this.buffer.push(value);
|
|
3252
|
+
this.buffer.push({ value });
|
|
3116
3253
|
}
|
|
3117
3254
|
end() {
|
|
3118
3255
|
if (this.ended)
|
|
@@ -3123,8 +3260,9 @@ class EventStream {
|
|
|
3123
3260
|
this.waiting.length = 0;
|
|
3124
3261
|
}
|
|
3125
3262
|
next() {
|
|
3126
|
-
|
|
3127
|
-
|
|
3263
|
+
const buffered = this.buffer.shift();
|
|
3264
|
+
if (buffered)
|
|
3265
|
+
return Promise.resolve({ value: buffered.value, done: false });
|
|
3128
3266
|
if (this.ended)
|
|
3129
3267
|
return Promise.resolve({ value: undefined, done: true });
|
|
3130
3268
|
return new Promise((resolve) => this.waiting.push(resolve));
|
|
@@ -3150,8 +3288,9 @@ class TypedEmitter {
|
|
|
3150
3288
|
set = new Set;
|
|
3151
3289
|
this.handlers.set(type, set);
|
|
3152
3290
|
}
|
|
3153
|
-
|
|
3154
|
-
|
|
3291
|
+
const wrapped = (payload) => handler(payload);
|
|
3292
|
+
set.add(wrapped);
|
|
3293
|
+
return () => set.delete(wrapped);
|
|
3155
3294
|
}
|
|
3156
3295
|
onAny(handler) {
|
|
3157
3296
|
this.anyHandlers.add(handler);
|
|
@@ -3315,8 +3454,12 @@ class ObservationCache {
|
|
|
3315
3454
|
}
|
|
3316
3455
|
|
|
3317
3456
|
// src/state/cache.ts
|
|
3318
|
-
function
|
|
3319
|
-
|
|
3457
|
+
function copySection(target, source, section) {
|
|
3458
|
+
const value = source[section];
|
|
3459
|
+
if (value === undefined)
|
|
3460
|
+
return false;
|
|
3461
|
+
target[section] = value;
|
|
3462
|
+
return true;
|
|
3320
3463
|
}
|
|
3321
3464
|
|
|
3322
3465
|
class StateCache {
|
|
@@ -3324,24 +3467,18 @@ class StateCache {
|
|
|
3324
3467
|
seed(snapshot) {
|
|
3325
3468
|
const next = {};
|
|
3326
3469
|
const changed = [];
|
|
3327
|
-
const src = asRecord(snapshot);
|
|
3328
3470
|
for (const section of STATE_SECTIONS) {
|
|
3329
|
-
if (
|
|
3330
|
-
asRecord(next)[section] = src[section];
|
|
3471
|
+
if (copySection(next, snapshot, section))
|
|
3331
3472
|
changed.push(section);
|
|
3332
|
-
}
|
|
3333
3473
|
}
|
|
3334
3474
|
this.state = next;
|
|
3335
3475
|
return changed;
|
|
3336
3476
|
}
|
|
3337
3477
|
applyDelta(delta) {
|
|
3338
3478
|
const changed = [];
|
|
3339
|
-
const src = asRecord(delta);
|
|
3340
3479
|
for (const section of STATE_SECTIONS) {
|
|
3341
|
-
if (
|
|
3342
|
-
asRecord(this.state)[section] = src[section];
|
|
3480
|
+
if (copySection(this.state, delta, section))
|
|
3343
3481
|
changed.push(section);
|
|
3344
|
-
}
|
|
3345
3482
|
}
|
|
3346
3483
|
return changed;
|
|
3347
3484
|
}
|
|
@@ -3349,7 +3486,7 @@ class StateCache {
|
|
|
3349
3486
|
const current = this.state[section];
|
|
3350
3487
|
if (current === undefined)
|
|
3351
3488
|
return [];
|
|
3352
|
-
|
|
3489
|
+
this.state[section] = { ...current, ...patch };
|
|
3353
3490
|
return [section];
|
|
3354
3491
|
}
|
|
3355
3492
|
snapshot() {
|
|
@@ -3388,13 +3525,12 @@ class StateCache {
|
|
|
3388
3525
|
function extractPendingAck(structured) {
|
|
3389
3526
|
if (!structured)
|
|
3390
3527
|
return;
|
|
3391
|
-
if (structured
|
|
3392
|
-
return { command: String(structured
|
|
3528
|
+
if (structured.pending === true) {
|
|
3529
|
+
return { command: String(structured.command ?? ""), message: String(structured.message ?? "") };
|
|
3393
3530
|
}
|
|
3394
|
-
const details = structured
|
|
3395
|
-
if (details &&
|
|
3396
|
-
|
|
3397
|
-
return { command: String(d["command"] ?? ""), message: String(d["message"] ?? "") };
|
|
3531
|
+
const details = structured.details;
|
|
3532
|
+
if (isRecord(details) && details.pending === true) {
|
|
3533
|
+
return { command: String(details.command ?? ""), message: String(details.message ?? "") };
|
|
3398
3534
|
}
|
|
3399
3535
|
return;
|
|
3400
3536
|
}
|
|
@@ -3426,6 +3562,8 @@ class Correlator {
|
|
|
3426
3562
|
return false;
|
|
3427
3563
|
switch (frame.type) {
|
|
3428
3564
|
case "result": {
|
|
3565
|
+
if (!isResultFrame(frame))
|
|
3566
|
+
return this.rejectInvalidResponse(requestId, pending, frame.type);
|
|
3429
3567
|
const payload = frame.payload;
|
|
3430
3568
|
if (pending.kind === "query") {
|
|
3431
3569
|
this.settle(requestId);
|
|
@@ -3437,7 +3575,7 @@ class Correlator {
|
|
|
3437
3575
|
if (!ack) {
|
|
3438
3576
|
this.settle(requestId);
|
|
3439
3577
|
pending.resolve({
|
|
3440
|
-
command: String(structured?.
|
|
3578
|
+
command: String(structured?.command ?? ""),
|
|
3441
3579
|
tick: 0,
|
|
3442
3580
|
delta: { details: structured }
|
|
3443
3581
|
});
|
|
@@ -3450,6 +3588,8 @@ class Correlator {
|
|
|
3450
3588
|
case "action_result": {
|
|
3451
3589
|
if (pending.kind !== "mutation")
|
|
3452
3590
|
return true;
|
|
3591
|
+
if (!isActionResultFrame(frame))
|
|
3592
|
+
return this.rejectInvalidResponse(requestId, pending, frame.type);
|
|
3453
3593
|
const payload = frame.payload;
|
|
3454
3594
|
this.settle(requestId);
|
|
3455
3595
|
pending.resolve({
|
|
@@ -3462,11 +3602,15 @@ class Correlator {
|
|
|
3462
3602
|
return true;
|
|
3463
3603
|
}
|
|
3464
3604
|
case "action_error": {
|
|
3605
|
+
if (!isActionErrorFrame(frame))
|
|
3606
|
+
return this.rejectInvalidResponse(requestId, pending, frame.type);
|
|
3465
3607
|
this.settle(requestId);
|
|
3466
3608
|
pending.reject(errorFromActionFrame(frame));
|
|
3467
3609
|
return true;
|
|
3468
3610
|
}
|
|
3469
3611
|
case "error": {
|
|
3612
|
+
if (!isErrorFrame(frame))
|
|
3613
|
+
return this.rejectInvalidResponse(requestId, pending, frame.type);
|
|
3470
3614
|
this.settle(requestId);
|
|
3471
3615
|
pending.reject(errorFromFrame(frame));
|
|
3472
3616
|
return true;
|
|
@@ -3483,10 +3627,43 @@ class Correlator {
|
|
|
3483
3627
|
settle(requestId) {
|
|
3484
3628
|
this.pending.delete(requestId);
|
|
3485
3629
|
}
|
|
3630
|
+
rejectInvalidResponse(requestId, pending, frameType) {
|
|
3631
|
+
this.settle(requestId);
|
|
3632
|
+
pending.reject(new SpacemoltError("invalid_response", `Malformed ${frameType} frame for request ${requestId}`));
|
|
3633
|
+
return true;
|
|
3634
|
+
}
|
|
3486
3635
|
}
|
|
3487
3636
|
|
|
3488
3637
|
// src/transport/socket.ts
|
|
3489
|
-
|
|
3638
|
+
class NativeWebSocketAdapter {
|
|
3639
|
+
socket;
|
|
3640
|
+
constructor(url) {
|
|
3641
|
+
this.socket = new WebSocket(url);
|
|
3642
|
+
}
|
|
3643
|
+
send(data) {
|
|
3644
|
+
this.socket.send(data);
|
|
3645
|
+
}
|
|
3646
|
+
close(code, reason) {
|
|
3647
|
+
this.socket.close(code, reason);
|
|
3648
|
+
}
|
|
3649
|
+
addEventListener(...args) {
|
|
3650
|
+
switch (args[0]) {
|
|
3651
|
+
case "open":
|
|
3652
|
+
this.socket.addEventListener("open", () => args[1]());
|
|
3653
|
+
break;
|
|
3654
|
+
case "message":
|
|
3655
|
+
this.socket.addEventListener("message", (event) => args[1]({ data: event.data }));
|
|
3656
|
+
break;
|
|
3657
|
+
case "close":
|
|
3658
|
+
this.socket.addEventListener("close", (event) => args[1]({ code: event.code, reason: event.reason }));
|
|
3659
|
+
break;
|
|
3660
|
+
case "error":
|
|
3661
|
+
this.socket.addEventListener("error", (event) => args[1](event));
|
|
3662
|
+
break;
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
var defaultFactory = (url) => new NativeWebSocketAdapter(url);
|
|
3490
3667
|
|
|
3491
3668
|
class Socket {
|
|
3492
3669
|
opts;
|
|
@@ -3542,7 +3719,10 @@ class Socket {
|
|
|
3542
3719
|
continue;
|
|
3543
3720
|
let frame;
|
|
3544
3721
|
try {
|
|
3545
|
-
|
|
3722
|
+
const parsed = JSON.parse(line);
|
|
3723
|
+
if (!isRecord(parsed) || typeof parsed.type !== "string" || parsed.request_id !== undefined && typeof parsed.request_id !== "string")
|
|
3724
|
+
throw new TypeError("frame must be an object with a type");
|
|
3725
|
+
frame = { ...parsed, type: parsed.type };
|
|
3546
3726
|
} catch (err) {
|
|
3547
3727
|
const head = line.slice(0, 200);
|
|
3548
3728
|
const tail = line.length > 400 ? line.slice(-200) : "";
|
|
@@ -3590,11 +3770,18 @@ function normalizeReconnect(opt) {
|
|
|
3590
3770
|
};
|
|
3591
3771
|
}
|
|
3592
3772
|
function retryAfterMs(err) {
|
|
3593
|
-
const
|
|
3773
|
+
const retryAfter = err.details?.retry_after;
|
|
3774
|
+
const fromDetails = typeof retryAfter === "number" ? retryAfter : undefined;
|
|
3594
3775
|
const match = err.message.match(/retry in (\d+)\s*second/i);
|
|
3595
3776
|
const seconds = fromDetails ?? (match ? Number(match[1]) : undefined);
|
|
3596
3777
|
return Math.max(250, (seconds ?? 1) * 1000);
|
|
3597
3778
|
}
|
|
3779
|
+
function requireStructuredContent(result, command) {
|
|
3780
|
+
if (result.structuredContent === undefined) {
|
|
3781
|
+
throw new SpacemoltError("invalid_response", `${command} returned no structured content`);
|
|
3782
|
+
}
|
|
3783
|
+
return result.structuredContent;
|
|
3784
|
+
}
|
|
3598
3785
|
|
|
3599
3786
|
class Account {
|
|
3600
3787
|
socket;
|
|
@@ -3619,10 +3806,11 @@ class Account {
|
|
|
3619
3806
|
_welcome = null;
|
|
3620
3807
|
_authenticated = false;
|
|
3621
3808
|
_loginPayload = null;
|
|
3809
|
+
_currentTick = 0;
|
|
3622
3810
|
_commands = null;
|
|
3623
3811
|
welcomeWaiter = null;
|
|
3624
3812
|
pendingAuth = null;
|
|
3625
|
-
|
|
3813
|
+
stateListeners = new Set;
|
|
3626
3814
|
userClosing = false;
|
|
3627
3815
|
reconnecting = false;
|
|
3628
3816
|
mutationLane = Promise.resolve();
|
|
@@ -3631,9 +3819,9 @@ class Account {
|
|
|
3631
3819
|
observationSubscribedState = false;
|
|
3632
3820
|
observationActiveScanState = false;
|
|
3633
3821
|
subscribedObservationPoiId;
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3822
|
+
reconnectedListeners = new Set;
|
|
3823
|
+
reconnectingListeners = new Set;
|
|
3824
|
+
disconnectedListeners = new Set;
|
|
3637
3825
|
constructor(opts = {}) {
|
|
3638
3826
|
this.seedState = opts.seedState ?? true;
|
|
3639
3827
|
this.url = opts.url ?? DEFAULT_URL;
|
|
@@ -3648,9 +3836,9 @@ class Account {
|
|
|
3648
3836
|
this._id = opts.id;
|
|
3649
3837
|
this.reconnectConfig = normalizeReconnect(opts.reconnect);
|
|
3650
3838
|
this.makeSocket();
|
|
3651
|
-
this.emitter.on("market_update", (
|
|
3652
|
-
this.emitter.on("observation_update", (
|
|
3653
|
-
this.observationCache.applyUpdate(
|
|
3839
|
+
this.emitter.on("market_update", (payload) => this.marketCache.applyUpdate(payload));
|
|
3840
|
+
this.emitter.on("observation_update", (payload) => {
|
|
3841
|
+
this.observationCache.applyUpdate(payload);
|
|
3654
3842
|
this.bridgeObservationToLocation();
|
|
3655
3843
|
});
|
|
3656
3844
|
}
|
|
@@ -3698,6 +3886,13 @@ class Account {
|
|
|
3698
3886
|
get welcome() {
|
|
3699
3887
|
return this._welcome;
|
|
3700
3888
|
}
|
|
3889
|
+
get currentTick() {
|
|
3890
|
+
return this._currentTick;
|
|
3891
|
+
}
|
|
3892
|
+
observeTick(tick) {
|
|
3893
|
+
if (typeof tick === "number" && tick > this._currentTick)
|
|
3894
|
+
this._currentTick = tick;
|
|
3895
|
+
}
|
|
3701
3896
|
get authenticated() {
|
|
3702
3897
|
return this._authenticated;
|
|
3703
3898
|
}
|
|
@@ -3788,19 +3983,32 @@ class Account {
|
|
|
3788
3983
|
}
|
|
3789
3984
|
}
|
|
3790
3985
|
async refresh() {
|
|
3791
|
-
const
|
|
3792
|
-
const snapshot =
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
this.stateListener?.(changed);
|
|
3799
|
-
}
|
|
3986
|
+
const result = await this.commands.spacemolt.get_status();
|
|
3987
|
+
const snapshot = requireStructuredContent(result, "spacemolt/get_status");
|
|
3988
|
+
const changed = this.cache.seed(snapshot);
|
|
3989
|
+
if (changed.includes("location"))
|
|
3990
|
+
this.checkSubscriptionsAgainstLocation();
|
|
3991
|
+
if (changed.length)
|
|
3992
|
+
this.emitStateChange(changed);
|
|
3800
3993
|
return this.cache.snapshot();
|
|
3801
3994
|
}
|
|
3802
3995
|
onStateChange(listener) {
|
|
3803
|
-
this.
|
|
3996
|
+
this.stateListeners.add(listener);
|
|
3997
|
+
return () => {
|
|
3998
|
+
this.stateListeners.delete(listener);
|
|
3999
|
+
};
|
|
4000
|
+
}
|
|
4001
|
+
notifyListeners(listeners, label, ...args) {
|
|
4002
|
+
for (const listener of listeners) {
|
|
4003
|
+
try {
|
|
4004
|
+
listener(...args);
|
|
4005
|
+
} catch (err) {
|
|
4006
|
+
console.warn(`[spacemolt] ${label} listener threw: ${err}`);
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
emitStateChange(changed) {
|
|
4011
|
+
this.notifyListeners(this.stateListeners, "onStateChange", changed);
|
|
3804
4012
|
}
|
|
3805
4013
|
async logout() {
|
|
3806
4014
|
await this.query("spacemolt_auth", "logout");
|
|
@@ -3850,7 +4058,7 @@ class Account {
|
|
|
3850
4058
|
}, (err) => {
|
|
3851
4059
|
if (timer)
|
|
3852
4060
|
clearTimeout(timer);
|
|
3853
|
-
reject(err);
|
|
4061
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
3854
4062
|
});
|
|
3855
4063
|
});
|
|
3856
4064
|
}
|
|
@@ -3871,13 +4079,12 @@ class Account {
|
|
|
3871
4079
|
return this.emitter.anyStream();
|
|
3872
4080
|
}
|
|
3873
4081
|
async subscribeMarket() {
|
|
3874
|
-
const
|
|
3875
|
-
const snapshot =
|
|
3876
|
-
|
|
3877
|
-
this.marketCache.seed(snapshot);
|
|
4082
|
+
const result = await this.commands.spacemolt_market.subscribe_market();
|
|
4083
|
+
const snapshot = requireStructuredContent(result, "spacemolt_market/subscribe_market");
|
|
4084
|
+
this.marketCache.seed(snapshot);
|
|
3878
4085
|
this.marketSubscribedState = true;
|
|
3879
|
-
this.subscribedMarketBaseId = snapshot
|
|
3880
|
-
return snapshot
|
|
4086
|
+
this.subscribedMarketBaseId = snapshot.base_id;
|
|
4087
|
+
return snapshot;
|
|
3881
4088
|
}
|
|
3882
4089
|
async unsubscribeMarket() {
|
|
3883
4090
|
const baseId = this.subscribedMarketBaseId ?? this.location?.docked_at ?? this.marketCache.bases()[0];
|
|
@@ -3891,16 +4098,14 @@ class Account {
|
|
|
3891
4098
|
return this.marketCache.book(baseId);
|
|
3892
4099
|
}
|
|
3893
4100
|
async subscribeObservation(activeScan = false) {
|
|
3894
|
-
const
|
|
3895
|
-
const snapshot =
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
this.bridgeObservationToLocation();
|
|
3899
|
-
}
|
|
4101
|
+
const result = await this.commands.spacemolt.subscribe_observation(activeScan ? { active_scan: true } : undefined);
|
|
4102
|
+
const snapshot = requireStructuredContent(result, "spacemolt/subscribe_observation");
|
|
4103
|
+
this.observationCache.seed(snapshot);
|
|
4104
|
+
this.bridgeObservationToLocation();
|
|
3900
4105
|
this.observationSubscribedState = true;
|
|
3901
4106
|
this.observationActiveScanState = activeScan;
|
|
3902
|
-
this.subscribedObservationPoiId = snapshot
|
|
3903
|
-
return snapshot
|
|
4107
|
+
this.subscribedObservationPoiId = snapshot.poi_id;
|
|
4108
|
+
return snapshot;
|
|
3904
4109
|
}
|
|
3905
4110
|
async unsubscribeObservation() {
|
|
3906
4111
|
this.observationSubscribedState = false;
|
|
@@ -3918,7 +4123,7 @@ class Account {
|
|
|
3918
4123
|
nearby_player_count: view.nearby.size
|
|
3919
4124
|
});
|
|
3920
4125
|
if (changed.length)
|
|
3921
|
-
this.
|
|
4126
|
+
this.emitStateChange(changed);
|
|
3922
4127
|
}
|
|
3923
4128
|
observation() {
|
|
3924
4129
|
return this.observationCache.current();
|
|
@@ -3940,13 +4145,22 @@ class Account {
|
|
|
3940
4145
|
this.socket.close();
|
|
3941
4146
|
}
|
|
3942
4147
|
onReconnected(listener) {
|
|
3943
|
-
this.
|
|
4148
|
+
this.reconnectedListeners.add(listener);
|
|
4149
|
+
return () => {
|
|
4150
|
+
this.reconnectedListeners.delete(listener);
|
|
4151
|
+
};
|
|
3944
4152
|
}
|
|
3945
4153
|
onReconnecting(listener) {
|
|
3946
|
-
this.
|
|
4154
|
+
this.reconnectingListeners.add(listener);
|
|
4155
|
+
return () => {
|
|
4156
|
+
this.reconnectingListeners.delete(listener);
|
|
4157
|
+
};
|
|
3947
4158
|
}
|
|
3948
4159
|
onDisconnected(listener) {
|
|
3949
|
-
this.
|
|
4160
|
+
this.disconnectedListeners.add(listener);
|
|
4161
|
+
return () => {
|
|
4162
|
+
this.disconnectedListeners.delete(listener);
|
|
4163
|
+
};
|
|
3950
4164
|
}
|
|
3951
4165
|
async authExchange(action, payload) {
|
|
3952
4166
|
const authPromise = new Promise((resolve, reject) => {
|
|
@@ -3989,19 +4203,28 @@ class Account {
|
|
|
3989
4203
|
return `r${++this.requestSeq}`;
|
|
3990
4204
|
}
|
|
3991
4205
|
routeFrame(frame) {
|
|
4206
|
+
if (isRecord(frame.payload))
|
|
4207
|
+
this.observeTick(frame.payload.tick);
|
|
3992
4208
|
switch (frame.type) {
|
|
3993
4209
|
case "welcome": {
|
|
4210
|
+
if (!isWelcomeFrame(frame))
|
|
4211
|
+
return this.warnMalformedFrame(frame);
|
|
3994
4212
|
const payload = frame.payload;
|
|
3995
4213
|
this._welcome = payload;
|
|
4214
|
+
this.observeTick(payload.current_tick);
|
|
3996
4215
|
this.welcomeWaiter?.resolve(payload);
|
|
3997
4216
|
this.welcomeWaiter = null;
|
|
3998
4217
|
return;
|
|
3999
4218
|
}
|
|
4000
4219
|
case "registered":
|
|
4220
|
+
if (!isRegisteredFrame(frame))
|
|
4221
|
+
return this.warnMalformedFrame(frame);
|
|
4001
4222
|
if (this.pendingAuth)
|
|
4002
4223
|
this.pendingAuth.registered = frame.payload;
|
|
4003
4224
|
return;
|
|
4004
4225
|
case "logged_in": {
|
|
4226
|
+
if (!isLoggedInFrame(frame))
|
|
4227
|
+
return this.warnMalformedFrame(frame);
|
|
4005
4228
|
const payload = frame.payload;
|
|
4006
4229
|
const auth = this.pendingAuth;
|
|
4007
4230
|
if (auth) {
|
|
@@ -4014,18 +4237,18 @@ class Account {
|
|
|
4014
4237
|
return;
|
|
4015
4238
|
}
|
|
4016
4239
|
case "action_result": {
|
|
4240
|
+
if (!isActionResultFrame(frame)) {
|
|
4241
|
+
if (!this.correlator.handle(frame))
|
|
4242
|
+
this.warnMalformedFrame(frame);
|
|
4243
|
+
return;
|
|
4244
|
+
}
|
|
4017
4245
|
const delta = frame.payload.result;
|
|
4018
4246
|
if (delta) {
|
|
4019
4247
|
const changed = this.cache.applyDelta(delta);
|
|
4020
4248
|
if (changed.includes("location"))
|
|
4021
4249
|
this.checkSubscriptionsAgainstLocation();
|
|
4022
|
-
if (changed.length)
|
|
4023
|
-
|
|
4024
|
-
this.stateListener?.(changed);
|
|
4025
|
-
} catch (err) {
|
|
4026
|
-
console.warn(`[spacemolt] onStateChange listener threw: ${err}`);
|
|
4027
|
-
}
|
|
4028
|
-
}
|
|
4250
|
+
if (changed.length)
|
|
4251
|
+
this.emitStateChange(changed);
|
|
4029
4252
|
}
|
|
4030
4253
|
if (!this.correlator.handle(frame)) {
|
|
4031
4254
|
console.warn(`[spacemolt] action_result for ${frame.request_id ?? "?"} arrived with no matching pending mutation (already timed out?)`);
|
|
@@ -4039,6 +4262,11 @@ class Account {
|
|
|
4039
4262
|
this.emitter.emit(frame);
|
|
4040
4263
|
return;
|
|
4041
4264
|
case "error": {
|
|
4265
|
+
if (!isErrorFrame(frame)) {
|
|
4266
|
+
if (!this.correlator.handle(frame))
|
|
4267
|
+
this.warnMalformedFrame(frame);
|
|
4268
|
+
return;
|
|
4269
|
+
}
|
|
4042
4270
|
if (this.correlator.handle(frame))
|
|
4043
4271
|
return;
|
|
4044
4272
|
const auth = this.pendingAuth;
|
|
@@ -4054,6 +4282,9 @@ class Account {
|
|
|
4054
4282
|
this.emitter.emit(frame);
|
|
4055
4283
|
}
|
|
4056
4284
|
}
|
|
4285
|
+
warnMalformedFrame(frame) {
|
|
4286
|
+
console.warn(`[spacemolt] dropped malformed ${frame.type} frame`);
|
|
4287
|
+
}
|
|
4057
4288
|
handleClose(err) {
|
|
4058
4289
|
this._authenticated = false;
|
|
4059
4290
|
this.correlator.rejectAll(err);
|
|
@@ -4071,7 +4302,7 @@ class Account {
|
|
|
4071
4302
|
if (this.shouldReconnect(err)) {
|
|
4072
4303
|
this.reconnectLoop(err);
|
|
4073
4304
|
} else if (!this.userClosing) {
|
|
4074
|
-
this.
|
|
4305
|
+
this.notifyListeners(this.disconnectedListeners, "onDisconnected", err);
|
|
4075
4306
|
}
|
|
4076
4307
|
}
|
|
4077
4308
|
shouldReconnect(err) {
|
|
@@ -4090,7 +4321,7 @@ class Account {
|
|
|
4090
4321
|
const cfg = this.reconnectConfig;
|
|
4091
4322
|
const retryAfterMs2 = retryAfterMsFromClose(err);
|
|
4092
4323
|
for (let attempt = 1;attempt <= cfg.maxRetries; attempt++) {
|
|
4093
|
-
this.
|
|
4324
|
+
this.notifyListeners(this.reconnectingListeners, "onReconnecting", attempt);
|
|
4094
4325
|
const backoff = attempt === 1 && retryAfterMs2 !== undefined ? retryAfterMs2 : Math.min(cfg.baseDelayMs * 2 ** (attempt - 1), cfg.maxDelayMs);
|
|
4095
4326
|
await delay(backoff);
|
|
4096
4327
|
if (this.userClosing) {
|
|
@@ -4100,12 +4331,12 @@ class Account {
|
|
|
4100
4331
|
try {
|
|
4101
4332
|
await this.reconnectOnce();
|
|
4102
4333
|
this.reconnecting = false;
|
|
4103
|
-
this.
|
|
4334
|
+
this.notifyListeners(this.reconnectedListeners, "onReconnected");
|
|
4104
4335
|
return;
|
|
4105
4336
|
} catch {}
|
|
4106
4337
|
}
|
|
4107
4338
|
this.reconnecting = false;
|
|
4108
|
-
this.
|
|
4339
|
+
this.notifyListeners(this.disconnectedListeners, "onDisconnected", err);
|
|
4109
4340
|
}
|
|
4110
4341
|
async reconnectOnce() {
|
|
4111
4342
|
if (!this.credentialsProvider)
|
|
@@ -4171,27 +4402,51 @@ class MemoryCredentialStore {
|
|
|
4171
4402
|
}
|
|
4172
4403
|
|
|
4173
4404
|
// src/data/catalog.ts
|
|
4174
|
-
|
|
4405
|
+
function catalogEntries(value) {
|
|
4406
|
+
return Array.isArray(value) ? value.filter((entry) => isRecord(entry)) : [];
|
|
4407
|
+
}
|
|
4408
|
+
function normalizeCatalog(value) {
|
|
4409
|
+
const data = requireRecord(value, "catalog response");
|
|
4410
|
+
return {
|
|
4411
|
+
version: typeof data.version === "string" ? data.version : undefined,
|
|
4412
|
+
ships: catalogEntries(data.ships),
|
|
4413
|
+
items: catalogEntries(data.items),
|
|
4414
|
+
recipes: catalogEntries(data.recipes),
|
|
4415
|
+
skills: catalogEntries(data.skills),
|
|
4416
|
+
facilities: catalogEntries(data.facilities)
|
|
4417
|
+
};
|
|
4418
|
+
}
|
|
4419
|
+
async function fetchCatalogConditional(httpBaseUrl, etag) {
|
|
4175
4420
|
const url = `${httpBaseUrl.replace(/\/$/, "")}/api/catalog.json`;
|
|
4176
|
-
const
|
|
4421
|
+
const headers = { accept: "application/json" };
|
|
4422
|
+
if (etag)
|
|
4423
|
+
headers["if-none-match"] = etag;
|
|
4424
|
+
const res = await fetch(url, { headers });
|
|
4425
|
+
if (res.status === 304)
|
|
4426
|
+
return { notModified: true, etag };
|
|
4177
4427
|
if (!res.ok)
|
|
4178
4428
|
throw new Error(`GET ${url} -> ${res.status} ${res.statusText}`);
|
|
4179
4429
|
const data = await res.json();
|
|
4180
4430
|
return {
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
recipes: data.recipes ?? [],
|
|
4185
|
-
skills: data.skills ?? [],
|
|
4186
|
-
facilities: data.facilities ?? []
|
|
4431
|
+
notModified: false,
|
|
4432
|
+
catalog: normalizeCatalog(data),
|
|
4433
|
+
etag: res.headers?.get?.("etag") ?? undefined
|
|
4187
4434
|
};
|
|
4188
4435
|
}
|
|
4436
|
+
async function fetchCatalog(httpBaseUrl) {
|
|
4437
|
+
const { catalog } = await fetchCatalogConditional(httpBaseUrl);
|
|
4438
|
+
if (!catalog)
|
|
4439
|
+
throw new Error("unconditional catalog fetch returned no catalog");
|
|
4440
|
+
return catalog;
|
|
4441
|
+
}
|
|
4189
4442
|
|
|
4190
4443
|
class CatalogCache {
|
|
4191
4444
|
catalog;
|
|
4445
|
+
etag;
|
|
4192
4446
|
indexes;
|
|
4193
|
-
constructor(catalog) {
|
|
4447
|
+
constructor(catalog, etag) {
|
|
4194
4448
|
this.catalog = catalog;
|
|
4449
|
+
this.etag = etag;
|
|
4195
4450
|
this.indexes = {
|
|
4196
4451
|
ships: index(catalog.ships),
|
|
4197
4452
|
items: index(catalog.items),
|
|
@@ -4201,7 +4456,18 @@ class CatalogCache {
|
|
|
4201
4456
|
};
|
|
4202
4457
|
}
|
|
4203
4458
|
static async load(httpBaseUrl) {
|
|
4204
|
-
|
|
4459
|
+
const { catalog, etag } = await fetchCatalogConditional(httpBaseUrl);
|
|
4460
|
+
if (!catalog)
|
|
4461
|
+
throw new Error("unconditional catalog fetch returned no catalog");
|
|
4462
|
+
return new CatalogCache(catalog, etag);
|
|
4463
|
+
}
|
|
4464
|
+
async revalidate(httpBaseUrl) {
|
|
4465
|
+
const result = await fetchCatalogConditional(httpBaseUrl, this.etag);
|
|
4466
|
+
if (result.notModified)
|
|
4467
|
+
return this;
|
|
4468
|
+
if (!result.catalog)
|
|
4469
|
+
throw new Error("modified catalog response returned no catalog");
|
|
4470
|
+
return new CatalogCache(result.catalog, result.etag);
|
|
4205
4471
|
}
|
|
4206
4472
|
get version() {
|
|
4207
4473
|
return this.catalog.version;
|
|
@@ -4253,8 +4519,10 @@ async function fetchMap(httpBaseUrl) {
|
|
|
4253
4519
|
const res = await fetch(url, { headers: { accept: "application/json" } });
|
|
4254
4520
|
if (!res.ok)
|
|
4255
4521
|
throw new Error(`GET ${url} -> ${res.status} ${res.statusText}`);
|
|
4256
|
-
const data = await res.json();
|
|
4257
|
-
|
|
4522
|
+
const data = requireRecord(await res.json(), "map response");
|
|
4523
|
+
const systems = Array.isArray(data.systems) ? data.systems.filter((system) => isRecord(system)) : [];
|
|
4524
|
+
const empires = isRecord(data.empires) ? Object.fromEntries(Object.entries(data.empires).filter((entry) => typeof entry[1] === "string")) : {};
|
|
4525
|
+
return { systems, empires };
|
|
4258
4526
|
}
|
|
4259
4527
|
|
|
4260
4528
|
class MapCache {
|
|
@@ -4303,6 +4571,7 @@ class SpacemoltClient {
|
|
|
4303
4571
|
store;
|
|
4304
4572
|
connected = new Map;
|
|
4305
4573
|
catalogCache;
|
|
4574
|
+
catalogFetchedAt = 0;
|
|
4306
4575
|
mapCache;
|
|
4307
4576
|
clerkSource;
|
|
4308
4577
|
accountConnectedListeners = new Set;
|
|
@@ -4343,8 +4612,13 @@ class SpacemoltClient {
|
|
|
4343
4612
|
return this.opts.httpBaseUrl ?? (this.opts.url ? httpBaseFromWs(this.opts.url) : DEFAULT_HTTP_BASE);
|
|
4344
4613
|
}
|
|
4345
4614
|
async catalog(force = false) {
|
|
4346
|
-
|
|
4347
|
-
|
|
4615
|
+
const maxAgeMs = this.opts.catalogMaxAgeMs ?? 3600000;
|
|
4616
|
+
const cached = this.catalogCache;
|
|
4617
|
+
const fresh = cached && !force && Date.now() - this.catalogFetchedAt < maxAgeMs;
|
|
4618
|
+
if (fresh)
|
|
4619
|
+
return cached;
|
|
4620
|
+
this.catalogCache = !this.catalogCache || force ? await CatalogCache.load(this.httpBaseUrl) : await this.catalogCache.revalidate(this.httpBaseUrl);
|
|
4621
|
+
this.catalogFetchedAt = Date.now();
|
|
4348
4622
|
return this.catalogCache;
|
|
4349
4623
|
}
|
|
4350
4624
|
async map(force = false) {
|
|
@@ -4366,16 +4640,22 @@ class SpacemoltClient {
|
|
|
4366
4640
|
async register(params) {
|
|
4367
4641
|
const account = this.createAccount(params.username);
|
|
4368
4642
|
this.connected.set(params.username, account);
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4643
|
+
try {
|
|
4644
|
+
await account.connect();
|
|
4645
|
+
const result = await account.register(params);
|
|
4646
|
+
await this.store.put({
|
|
4647
|
+
id: params.username,
|
|
4648
|
+
credentials: { kind: "login", username: params.username, password: result.password },
|
|
4649
|
+
playerId: result.player_id
|
|
4650
|
+
});
|
|
4651
|
+
account.onDisconnected((err) => this.handleAccountDisconnected(params.username, account, err));
|
|
4652
|
+
this.notifyAccountConnected(account);
|
|
4653
|
+
return { account, result };
|
|
4654
|
+
} catch (error) {
|
|
4655
|
+
account.close();
|
|
4656
|
+
this.connected.delete(params.username);
|
|
4657
|
+
throw error;
|
|
4658
|
+
}
|
|
4379
4659
|
}
|
|
4380
4660
|
connectRetryConfig() {
|
|
4381
4661
|
const retryOpt = this.opts.connectRetry ?? true;
|
|
@@ -4428,10 +4708,10 @@ class SpacemoltClient {
|
|
|
4428
4708
|
return this.connectIds(stored.map((s) => s.id), opts.onConnect);
|
|
4429
4709
|
}
|
|
4430
4710
|
async listOwnedPlayers() {
|
|
4431
|
-
return this.
|
|
4711
|
+
return this.requireClerkAuth().source.listPlayers();
|
|
4432
4712
|
}
|
|
4433
4713
|
async connectOwned(opts = {}) {
|
|
4434
|
-
const source = this.
|
|
4714
|
+
const { source, apiKey } = this.requireClerkAuth();
|
|
4435
4715
|
const players = await source.listPlayers();
|
|
4436
4716
|
const selected = opts.filter ? players.filter(opts.filter) : players;
|
|
4437
4717
|
const ids = [];
|
|
@@ -4440,7 +4720,7 @@ class SpacemoltClient {
|
|
|
4440
4720
|
id: player.username,
|
|
4441
4721
|
credentials: {
|
|
4442
4722
|
kind: "clerk",
|
|
4443
|
-
apiKey
|
|
4723
|
+
apiKey,
|
|
4444
4724
|
playerId: player.id,
|
|
4445
4725
|
httpBaseUrl: source.httpBaseUrl
|
|
4446
4726
|
},
|
|
@@ -4450,18 +4730,17 @@ class SpacemoltClient {
|
|
|
4450
4730
|
}
|
|
4451
4731
|
return this.connectIds(ids, opts.onConnect);
|
|
4452
4732
|
}
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
}
|
|
4458
|
-
this.clerkSource = new ClerkSource({
|
|
4459
|
-
apiKey: this.opts.clerkApiKey,
|
|
4460
|
-
httpBaseUrl: this.httpBaseUrl,
|
|
4461
|
-
fetchImpl: this.opts.fetchImpl
|
|
4462
|
-
});
|
|
4733
|
+
requireClerkAuth() {
|
|
4734
|
+
const apiKey = this.opts.clerkApiKey;
|
|
4735
|
+
if (!apiKey) {
|
|
4736
|
+
throw new Error("connectOwned/listOwnedPlayers require `clerkApiKey` in SpacemoltClientOptions");
|
|
4463
4737
|
}
|
|
4464
|
-
|
|
4738
|
+
this.clerkSource ??= new ClerkSource({
|
|
4739
|
+
apiKey,
|
|
4740
|
+
httpBaseUrl: this.httpBaseUrl,
|
|
4741
|
+
fetchImpl: this.opts.fetchImpl
|
|
4742
|
+
});
|
|
4743
|
+
return { source: this.clerkSource, apiKey };
|
|
4465
4744
|
}
|
|
4466
4745
|
async connectIds(ids, onConnect) {
|
|
4467
4746
|
const accounts = [];
|
|
@@ -4505,6 +4784,8 @@ class SpacemoltClient {
|
|
|
4505
4784
|
}
|
|
4506
4785
|
isFirst = false;
|
|
4507
4786
|
const task = this.rateLimitedQueue.shift();
|
|
4787
|
+
if (!task)
|
|
4788
|
+
break;
|
|
4508
4789
|
ranInCurrentBatch++;
|
|
4509
4790
|
await task();
|
|
4510
4791
|
}
|
|
@@ -4604,6 +4885,7 @@ var TYPED_NOTIFICATION_TYPES = [
|
|
|
4604
4885
|
"reconnected",
|
|
4605
4886
|
"scan_detected",
|
|
4606
4887
|
"skill_level_up",
|
|
4888
|
+
"station_repaired",
|
|
4607
4889
|
"trade_cancelled",
|
|
4608
4890
|
"trade_complete",
|
|
4609
4891
|
"trade_declined",
|
|
@@ -4612,8 +4894,16 @@ var TYPED_NOTIFICATION_TYPES = [
|
|
|
4612
4894
|
export {
|
|
4613
4895
|
retryAfterMsFromClose,
|
|
4614
4896
|
mintWsToken,
|
|
4897
|
+
isWelcomeFrame,
|
|
4898
|
+
isResultFrame,
|
|
4899
|
+
isRegisteredFrame,
|
|
4900
|
+
isLoggedInFrame,
|
|
4901
|
+
isErrorFrame,
|
|
4902
|
+
isActionResultFrame,
|
|
4903
|
+
isActionErrorFrame,
|
|
4615
4904
|
httpBaseFromWs,
|
|
4616
4905
|
fetchMap,
|
|
4906
|
+
fetchCatalogConditional,
|
|
4617
4907
|
fetchCatalog,
|
|
4618
4908
|
buildCommands,
|
|
4619
4909
|
TypedEmitter,
|