@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/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.485.0";
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: "Buy an empire shipbuilding license so your faction can build that empire's hulls at its own stations",
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: "empire", type: '"solarian" | "voidborn" | "crimson" | "nebula" | "outerrim"', required: true, description: "Empire to license shipbuilding from", enumValues: ["solarian", "voidborn", "crimson", "nebula", "outerrim"] }
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: "If true, supply build materials from cargo/storage (cheaper). If false, pay credits for everything (default)." }
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, or empire NPC",
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: "Player ID to attack", positional: 0 }
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 at the current station",
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 clerkFetch(fetchImpl, url, apiKey, init) {
3003
- const res = await fetchImpl(url, {
3004
- ...init,
3005
- headers: { accept: "application/json", authorization: `Bearer ${apiKey}`, ...init?.headers ?? {} }
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.apiKey, { method: "POST" });
3016
- if (!data.token)
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
- const data = await clerkFetch(this.fetchImpl, `${this.httpBaseUrl}/api/registration-code`, this.opts.apiKey);
3035
- return data.players ?? [];
3166
+ return (await this.fetchRegistration()).players;
3036
3167
  }
3037
3168
  mintWsToken(playerId) {
3038
- return mintWsToken({ httpBaseUrl: this.httpBaseUrl, apiKey: this.opts.apiKey, playerId, fetchImpl: this.fetchImpl });
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
- if (this.buffer.length)
3127
- return Promise.resolve({ value: this.buffer.shift(), done: false });
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
- set.add(handler);
3154
- return () => set.delete(handler);
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 asRecord(value) {
3319
- return value;
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 (src[section] !== undefined) {
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 (src[section] !== undefined) {
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
- asRecord(this.state)[section] = { ...asRecord(current), ...patch };
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["pending"] === true) {
3392
- return { command: String(structured["command"] ?? ""), message: String(structured["message"] ?? "") };
3528
+ if (structured.pending === true) {
3529
+ return { command: String(structured.command ?? ""), message: String(structured.message ?? "") };
3393
3530
  }
3394
- const details = structured["details"];
3395
- if (details && typeof details === "object" && details["pending"] === true) {
3396
- const d = details;
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?.["command"] ?? ""),
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
- var defaultFactory = (url) => new WebSocket(url);
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
- frame = JSON.parse(line);
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 fromDetails = typeof err.details?.["retry_after"] === "number" ? err.details["retry_after"] : undefined;
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
- stateListener = null;
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
- reconnectedListener = null;
3635
- reconnectingListener = null;
3636
- disconnectedListener = null;
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", (p) => this.marketCache.applyUpdate(p));
3652
- this.emitter.on("observation_update", (p) => {
3653
- this.observationCache.applyUpdate(p);
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 res = await this.query("spacemolt", "get_status");
3792
- const snapshot = res.structuredContent;
3793
- if (snapshot) {
3794
- const changed = this.cache.seed(snapshot);
3795
- if (changed.includes("location"))
3796
- this.checkSubscriptionsAgainstLocation();
3797
- if (changed.length)
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.stateListener = listener;
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 res = await this.query("spacemolt_market", "subscribe_market");
3875
- const snapshot = res.structuredContent;
3876
- if (snapshot)
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?.base_id;
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 res = await this.query("spacemolt", "subscribe_observation", activeScan ? { active_scan: true } : undefined);
3895
- const snapshot = res.structuredContent;
3896
- if (snapshot) {
3897
- this.observationCache.seed(snapshot);
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?.poi_id;
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.stateListener?.(changed);
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.reconnectedListener = listener;
4148
+ this.reconnectedListeners.add(listener);
4149
+ return () => {
4150
+ this.reconnectedListeners.delete(listener);
4151
+ };
3944
4152
  }
3945
4153
  onReconnecting(listener) {
3946
- this.reconnectingListener = listener;
4154
+ this.reconnectingListeners.add(listener);
4155
+ return () => {
4156
+ this.reconnectingListeners.delete(listener);
4157
+ };
3947
4158
  }
3948
4159
  onDisconnected(listener) {
3949
- this.disconnectedListener = listener;
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
- try {
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.disconnectedListener?.(err);
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.reconnectingListener?.(attempt);
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.reconnectedListener?.();
4334
+ this.notifyListeners(this.reconnectedListeners, "onReconnected");
4104
4335
  return;
4105
4336
  } catch {}
4106
4337
  }
4107
4338
  this.reconnecting = false;
4108
- this.disconnectedListener?.(err);
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
- async function fetchCatalog(httpBaseUrl) {
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 res = await fetch(url, { headers: { accept: "application/json" } });
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
- version: data.version,
4182
- ships: data.ships ?? [],
4183
- items: data.items ?? [],
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
- return new CatalogCache(await fetchCatalog(httpBaseUrl));
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
- return { systems: data.systems ?? [], empires: data.empires ?? {} };
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
- if (force || !this.catalogCache)
4347
- this.catalogCache = await CatalogCache.load(this.httpBaseUrl);
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
- await account.connect();
4370
- const result = await account.register(params);
4371
- await this.store.put({
4372
- id: params.username,
4373
- credentials: { kind: "login", username: params.username, password: result.password },
4374
- playerId: result.player_id
4375
- });
4376
- account.onDisconnected((err) => this.handleAccountDisconnected(params.username, account, err));
4377
- this.notifyAccountConnected(account);
4378
- return { account, result };
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.requireClerkSource().listPlayers();
4711
+ return this.requireClerkAuth().source.listPlayers();
4432
4712
  }
4433
4713
  async connectOwned(opts = {}) {
4434
- const source = this.requireClerkSource();
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: source.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
- requireClerkSource() {
4454
- if (!this.clerkSource) {
4455
- if (!this.opts.clerkApiKey) {
4456
- throw new Error("connectOwned/listOwnedPlayers require `clerkApiKey` in SpacemoltClientOptions");
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
- return this.clerkSource;
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,