@spacemolt/lib 4.6.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMMANDS.md +20 -14
- 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 +119 -39
- package/dist/generated/notifications.gen.d.ts +4 -3
- package/dist/generated/openapi/types.gen.d.ts +1813 -270
- package/dist/index.d.ts +4 -4
- package/dist/index.js +460 -162
- 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.512.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,10 +515,14 @@ 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 },
|
|
519
|
+
{ name: "items", type: "{ item_id: string; quantity: number }[]", required: false, description: "For job_add pack_package: selected manifest items." },
|
|
520
|
+
{ name: "label", type: "string", required: false, description: "For job_add pack_package: player label." },
|
|
521
|
+
{ name: "package_id", type: "string", required: false, description: "For job_add unpack_package: package ID." },
|
|
442
522
|
{ name: "quantity", type: "number", required: false, description: "For 'job_add': number of runs to queue." },
|
|
443
523
|
{ name: "recipe_id", type: "string", required: true, description: "Recipe ID to run (for 'job_add' action)." },
|
|
444
|
-
{ 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." }
|
|
524
|
+
{ 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." },
|
|
525
|
+
{ name: "target", type: "string", required: false, description: "Package job_add output destination; defaults to source." }
|
|
445
526
|
]
|
|
446
527
|
},
|
|
447
528
|
"spacemolt_facility/job_cancel": {
|
|
@@ -462,7 +543,7 @@ var ACTIONS = {
|
|
|
462
543
|
summary: "Manage facilities at stations (production, faction, personal, sales, and more)",
|
|
463
544
|
responseType: "FacilityResponse",
|
|
464
545
|
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 }
|
|
546
|
+
{ 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
547
|
]
|
|
467
548
|
},
|
|
468
549
|
"spacemolt_facility/job_reorder": {
|
|
@@ -472,7 +553,7 @@ var ACTIONS = {
|
|
|
472
553
|
summary: "Manage facilities at stations (production, faction, personal, sales, and more)",
|
|
473
554
|
detailsType: "FacilityResponse",
|
|
474
555
|
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 },
|
|
556
|
+
{ 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
557
|
{ name: "job_id", type: "string", required: true, description: "Job ID (for 'job_cancel', 'job_reorder'). Use action 'job_list' to see job IDs." },
|
|
477
558
|
{ name: "position", type: "number", required: false, description: "New queue position for 'job_reorder' (1-based)." }
|
|
478
559
|
]
|
|
@@ -492,9 +573,9 @@ var ACTIONS = {
|
|
|
492
573
|
summary: "Manage facilities at stations (production, faction, personal, sales, and more)",
|
|
493
574
|
detailsType: "FacilityResponse",
|
|
494
575
|
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 },
|
|
576
|
+
{ 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
577
|
{ name: "faction", type: "boolean", required: false, description: "For 'list_for_sale': set true to list a faction-owned facility (requires manage_facilities permission)." },
|
|
497
|
-
{ 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
|
|
578
|
+
{ 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 on ordinary production, or a once-per-package-operation price on Logistics. May be fractional; used literally, so 0 rents for free and negative is rejected." }
|
|
498
579
|
]
|
|
499
580
|
},
|
|
500
581
|
"spacemolt_facility/owned": {
|
|
@@ -556,6 +637,16 @@ var ACTIONS = {
|
|
|
556
637
|
{ name: "player", type: "string", required: true, description: "Target player id or username (allow_player/remove_player/ban/unban)" }
|
|
557
638
|
]
|
|
558
639
|
},
|
|
640
|
+
"spacemolt_facility/repair": {
|
|
641
|
+
tool: "spacemolt_facility",
|
|
642
|
+
action: "repair",
|
|
643
|
+
kind: "mutation",
|
|
644
|
+
summary: "Manage facilities at stations (production, faction, personal, sales, and more)",
|
|
645
|
+
detailsType: "FacilityResponse",
|
|
646
|
+
params: [
|
|
647
|
+
{ 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 }
|
|
648
|
+
]
|
|
649
|
+
},
|
|
559
650
|
"spacemolt_facility/set_access": {
|
|
560
651
|
tool: "spacemolt_facility",
|
|
561
652
|
action: "set_access",
|
|
@@ -564,7 +655,17 @@ var ACTIONS = {
|
|
|
564
655
|
detailsType: "FacilityResponse",
|
|
565
656
|
params: [
|
|
566
657
|
{ 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 }
|
|
658
|
+
{ 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 }
|
|
659
|
+
]
|
|
660
|
+
},
|
|
661
|
+
"spacemolt_facility/set_auto_buy_fuel": {
|
|
662
|
+
tool: "spacemolt_facility",
|
|
663
|
+
action: "set_auto_buy_fuel",
|
|
664
|
+
kind: "mutation",
|
|
665
|
+
summary: "Administer one of your faction's stations or outposts: rename, access control, and build policy",
|
|
666
|
+
detailsType: "StationConfigResponse",
|
|
667
|
+
params: [
|
|
668
|
+
{ 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
669
|
]
|
|
569
670
|
},
|
|
570
671
|
"spacemolt_facility/set_build_policy": {
|
|
@@ -605,7 +706,7 @@ var ACTIONS = {
|
|
|
605
706
|
detailsType: "FacilityResponse",
|
|
606
707
|
params: [
|
|
607
708
|
{ 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 }
|
|
709
|
+
{ 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
710
|
]
|
|
610
711
|
},
|
|
611
712
|
"spacemolt_facility/set_output_price": {
|
|
@@ -615,8 +716,8 @@ var ACTIONS = {
|
|
|
615
716
|
summary: "Manage facilities at stations (production, faction, personal, sales, and more)",
|
|
616
717
|
detailsType: "FacilityResponse",
|
|
617
718
|
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 },
|
|
619
|
-
{ 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
|
|
719
|
+
{ 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 },
|
|
720
|
+
{ 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 on ordinary production, or a once-per-package-operation price on Logistics. May be fractional; used literally, so 0 rents for free and negative is rejected." }
|
|
620
721
|
]
|
|
621
722
|
},
|
|
622
723
|
"spacemolt_facility/set_public": {
|
|
@@ -686,7 +787,7 @@ var ACTIONS = {
|
|
|
686
787
|
detailsType: "FacilityResponse",
|
|
687
788
|
params: [
|
|
688
789
|
{ 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 },
|
|
790
|
+
{ 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
791
|
{ name: "player_id", type: "string", required: false, description: "Target player ID for 'transfer' action with direction 'to_player'." }
|
|
691
792
|
]
|
|
692
793
|
},
|
|
@@ -723,7 +824,7 @@ var ACTIONS = {
|
|
|
723
824
|
detailsType: "FacilityResponse",
|
|
724
825
|
params: [
|
|
725
826
|
{ 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 },
|
|
827
|
+
{ 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
828
|
{ 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
829
|
]
|
|
729
830
|
},
|
|
@@ -1412,9 +1513,7 @@ var ACTIONS = {
|
|
|
1412
1513
|
kind: "mutation",
|
|
1413
1514
|
summary: "Purchase ship insurance",
|
|
1414
1515
|
detailsType: "BuyInsuranceResponse",
|
|
1415
|
-
params: [
|
|
1416
|
-
{ name: "ticks", type: "number", required: true, description: "Number of ticks to insure for" }
|
|
1417
|
-
]
|
|
1516
|
+
params: []
|
|
1418
1517
|
},
|
|
1419
1518
|
"spacemolt_salvage/loot": {
|
|
1420
1519
|
tool: "spacemolt_salvage",
|
|
@@ -1566,8 +1665,9 @@ var ACTIONS = {
|
|
|
1566
1665
|
summary: "Commission a ship to be built at this shipyard",
|
|
1567
1666
|
detailsType: "CommissionShipResponse",
|
|
1568
1667
|
params: [
|
|
1668
|
+
{ 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
1669
|
{ 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: "
|
|
1670
|
+
{ 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
1671
|
]
|
|
1572
1672
|
},
|
|
1573
1673
|
"spacemolt_ship/commission_status": {
|
|
@@ -1638,16 +1738,6 @@ var ACTIONS = {
|
|
|
1638
1738
|
{ 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
1739
|
]
|
|
1640
1740
|
},
|
|
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
1741
|
"spacemolt_ship/sell_ship_to_order": {
|
|
1652
1742
|
tool: "spacemolt_ship",
|
|
1653
1743
|
action: "sell_ship_to_order",
|
|
@@ -2117,10 +2207,10 @@ var ACTIONS = {
|
|
|
2117
2207
|
tool: "spacemolt",
|
|
2118
2208
|
action: "attack",
|
|
2119
2209
|
kind: "mutation",
|
|
2120
|
-
summary: "Attack another player, pirate,
|
|
2210
|
+
summary: "Attack another player, pirate, empire NPC, creature, or station",
|
|
2121
2211
|
detailsType: "AttackResponse",
|
|
2122
2212
|
params: [
|
|
2123
|
-
{ name: "id", type: "string", required: true, description: "
|
|
2213
|
+
{ 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
2214
|
]
|
|
2125
2215
|
},
|
|
2126
2216
|
"spacemolt/buy": {
|
|
@@ -2177,12 +2267,16 @@ var ACTIONS = {
|
|
|
2177
2267
|
{ name: "dry_run", type: "boolean", required: false, description: "Return a cost+time quote (materials, labor, rental fee, auto-routed venue, ETA) without queuing or spending anything. Not supported with bulk jobs." },
|
|
2178
2268
|
{ name: "facility_id", type: "string", required: false, description: "Route to a specific facility ID (overrides auto-selection)." },
|
|
2179
2269
|
{ name: "id", type: "string", required: false, description: "Recipe ID to craft (use catalog with type=recipes to see available recipes). Inputs are escrowed from station storage at enqueue.", positional: 0 },
|
|
2270
|
+
{ name: "items", type: "{ item_id: string; quantity: number }[]", required: false, description: "For pack_package: selected items to pack; total unpacked size may not exceed 100." },
|
|
2180
2271
|
{ 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
2272
|
{ 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
|
-
{ name: "jobs", type: "
|
|
2183
|
-
{ name: "
|
|
2273
|
+
{ name: "jobs", type: "{ deliver_to?: string; facility_id?: string; items?: { item_id: string; quantity: number }[]; label?: string; package_id?: string; preset?: string; quantity?: number; recipe_id: string; source?: string; target?: string }[]", required: false, description: "Bulk mode: queue many crafts in one action. Package entries may include items, package_id, label, and target. Each job commits independently with partial success. Max 50." },
|
|
2274
|
+
{ name: "label", type: "string", required: false, description: "For pack_package: player-authored package label." },
|
|
2275
|
+
{ name: "package_id", type: "string", required: false, description: "For unpack_package: package instance ID to unpack." },
|
|
2276
|
+
{ 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
2277
|
{ 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
|
-
{ 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." }
|
|
2278
|
+
{ 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." },
|
|
2279
|
+
{ name: "target", type: "string", required: false, description: "For package recipes: output destination (storage, cargo, faction, or faction:<bucket>); defaults to source. deliver_to is accepted as an alias." }
|
|
2186
2280
|
]
|
|
2187
2281
|
},
|
|
2188
2282
|
"spacemolt/decline_mission": {
|
|
@@ -2441,6 +2535,16 @@ var ACTIONS = {
|
|
|
2441
2535
|
{ name: "id", type: "string", required: true, description: "Creature ID to hunt (from the 'creatures' list in get_nearby)", positional: 0 }
|
|
2442
2536
|
]
|
|
2443
2537
|
},
|
|
2538
|
+
"spacemolt/inspect": {
|
|
2539
|
+
tool: "spacemolt",
|
|
2540
|
+
action: "inspect",
|
|
2541
|
+
kind: "query",
|
|
2542
|
+
summary: "Inspect a visible package, item, module, ship class, system, POI, or docked base by ID",
|
|
2543
|
+
responseType: "InspectResponse",
|
|
2544
|
+
params: [
|
|
2545
|
+
{ name: "id", type: "string", required: true, description: "Visible package, item, module, ship class, system, POI, or base ID to inspect", positional: 0 }
|
|
2546
|
+
]
|
|
2547
|
+
},
|
|
2444
2548
|
"spacemolt/install_mod": {
|
|
2445
2549
|
tool: "spacemolt",
|
|
2446
2550
|
action: "install_mod",
|
|
@@ -2530,7 +2634,8 @@ var ACTIONS = {
|
|
|
2530
2634
|
{ 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
2635
|
{ 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
2636
|
{ 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." },
|
|
2637
|
+
{ 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." },
|
|
2638
|
+
{ 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
2639
|
{ 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
2640
|
{ 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
2641
|
]
|
|
@@ -2659,10 +2764,11 @@ var ACTIONS = {
|
|
|
2659
2764
|
tool: "spacemolt",
|
|
2660
2765
|
action: "unload_passenger",
|
|
2661
2766
|
kind: "mutation",
|
|
2662
|
-
summary: "Put a passenger (or everyone) off the ship
|
|
2767
|
+
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
2768
|
detailsType: "UnloadPassengerResponse",
|
|
2664
2769
|
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 }
|
|
2770
|
+
{ 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 },
|
|
2771
|
+
{ 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
2772
|
]
|
|
2667
2773
|
},
|
|
2668
2774
|
"spacemolt/unsubscribe_observation": {
|
|
@@ -2738,6 +2844,7 @@ function buildCommands(dispatch) {
|
|
|
2738
2844
|
get_tax_estimate: bind("spacemolt", "get_tax_estimate"),
|
|
2739
2845
|
get_version: bind("spacemolt", "get_version"),
|
|
2740
2846
|
hunt: bind("spacemolt", "hunt"),
|
|
2847
|
+
inspect: bind("spacemolt", "inspect"),
|
|
2741
2848
|
install_mod: bind("spacemolt", "install_mod"),
|
|
2742
2849
|
jettison: bind("spacemolt", "jettison"),
|
|
2743
2850
|
jump: bind("spacemolt", "jump"),
|
|
@@ -2774,10 +2881,12 @@ function buildCommands(dispatch) {
|
|
|
2774
2881
|
spacemolt_battle: {
|
|
2775
2882
|
advance: bind("spacemolt_battle", "advance"),
|
|
2776
2883
|
engage: bind("spacemolt_battle", "engage"),
|
|
2884
|
+
log: bind("spacemolt_battle", "log"),
|
|
2777
2885
|
reload: bind("spacemolt_battle", "reload"),
|
|
2778
2886
|
retreat: bind("spacemolt_battle", "retreat"),
|
|
2779
2887
|
stance: bind("spacemolt_battle", "stance"),
|
|
2780
2888
|
status: bind("spacemolt_battle", "status"),
|
|
2889
|
+
summary: bind("spacemolt_battle", "summary"),
|
|
2781
2890
|
target: bind("spacemolt_battle", "target")
|
|
2782
2891
|
},
|
|
2783
2892
|
spacemolt_citizenship: {
|
|
@@ -2807,9 +2916,11 @@ function buildCommands(dispatch) {
|
|
|
2807
2916
|
buy_ship_license: bind("spacemolt_facility", "buy_ship_license"),
|
|
2808
2917
|
cancel_listing: bind("spacemolt_facility", "cancel_listing"),
|
|
2809
2918
|
deploy_outpost: bind("spacemolt_facility", "deploy_outpost"),
|
|
2919
|
+
disassemble: bind("spacemolt_facility", "disassemble"),
|
|
2810
2920
|
dismantle: bind("spacemolt_facility", "dismantle"),
|
|
2811
2921
|
facility_set_description: bind("spacemolt_facility", "facility_set_description"),
|
|
2812
2922
|
faction_build: bind("spacemolt_facility", "faction_build"),
|
|
2923
|
+
faction_disassemble: bind("spacemolt_facility", "faction_disassemble"),
|
|
2813
2924
|
faction_dismantle: bind("spacemolt_facility", "faction_dismantle"),
|
|
2814
2925
|
faction_list: bind("spacemolt_facility", "faction_list"),
|
|
2815
2926
|
faction_owned: bind("spacemolt_facility", "faction_owned"),
|
|
@@ -2827,7 +2938,9 @@ function buildCommands(dispatch) {
|
|
|
2827
2938
|
personal_visit: bind("spacemolt_facility", "personal_visit"),
|
|
2828
2939
|
remove_faction: bind("spacemolt_facility", "remove_faction"),
|
|
2829
2940
|
remove_player: bind("spacemolt_facility", "remove_player"),
|
|
2941
|
+
repair: bind("spacemolt_facility", "repair"),
|
|
2830
2942
|
set_access: bind("spacemolt_facility", "set_access"),
|
|
2943
|
+
set_auto_buy_fuel: bind("spacemolt_facility", "set_auto_buy_fuel"),
|
|
2831
2944
|
set_build_policy: bind("spacemolt_facility", "set_build_policy"),
|
|
2832
2945
|
set_description: bind("spacemolt_facility", "set_description"),
|
|
2833
2946
|
set_market_fee: bind("spacemolt_facility", "set_market_fee"),
|
|
@@ -2948,7 +3061,6 @@ function buildCommands(dispatch) {
|
|
|
2948
3061
|
refit_ship: bind("spacemolt_ship", "refit_ship"),
|
|
2949
3062
|
rename_ship: bind("spacemolt_ship", "rename_ship"),
|
|
2950
3063
|
scrap_ship: bind("spacemolt_ship", "scrap_ship"),
|
|
2951
|
-
sell_ship: bind("spacemolt_ship", "sell_ship"),
|
|
2952
3064
|
sell_ship_to_order: bind("spacemolt_ship", "sell_ship_to_order"),
|
|
2953
3065
|
supply_commission: bind("spacemolt_ship", "supply_commission"),
|
|
2954
3066
|
switch_ship: bind("spacemolt_ship", "switch_ship"),
|
|
@@ -2999,11 +3111,26 @@ function buildCommands(dispatch) {
|
|
|
2999
3111
|
}
|
|
3000
3112
|
|
|
3001
3113
|
// src/auth/clerk.ts
|
|
3002
|
-
async function
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3114
|
+
async function resolveAuthHeaders(auth) {
|
|
3115
|
+
if (auth.apiKey === undefined && auth.headers === undefined) {
|
|
3116
|
+
throw new Error("Clerk auth requires an `apiKey` or `headers`");
|
|
3117
|
+
}
|
|
3118
|
+
const headers = new Headers(auth.apiKey === undefined ? undefined : { authorization: `Bearer ${auth.apiKey}` });
|
|
3119
|
+
const extra = typeof auth.headers === "function" ? await auth.headers() : auth.headers;
|
|
3120
|
+
new Headers(extra).forEach((value, key) => {
|
|
3121
|
+
headers.set(key, value);
|
|
3122
|
+
});
|
|
3123
|
+
return headers;
|
|
3124
|
+
}
|
|
3125
|
+
async function clerkFetch(fetchImpl, url, auth, init) {
|
|
3126
|
+
const headers = new Headers({ accept: "application/json" });
|
|
3127
|
+
(await resolveAuthHeaders(auth)).forEach((value, key) => {
|
|
3128
|
+
headers.set(key, value);
|
|
3129
|
+
});
|
|
3130
|
+
new Headers(init?.headers).forEach((value, key) => {
|
|
3131
|
+
headers.set(key, value);
|
|
3006
3132
|
});
|
|
3133
|
+
const res = await fetchImpl(url, { ...init, headers });
|
|
3007
3134
|
if (!res.ok) {
|
|
3008
3135
|
const body = await res.text().catch(() => "");
|
|
3009
3136
|
throw new Error(`${init?.method ?? "GET"} ${url} -> ${res.status} ${res.statusText}${body ? `: ${body}` : ""}`);
|
|
@@ -3012,8 +3139,10 @@ async function clerkFetch(fetchImpl, url, apiKey, init) {
|
|
|
3012
3139
|
}
|
|
3013
3140
|
async function mintWsToken(opts) {
|
|
3014
3141
|
const base = opts.httpBaseUrl.replace(/\/$/, "");
|
|
3015
|
-
const data = await clerkFetch(opts.fetchImpl ?? fetch, `${base}/api/player/${encodeURIComponent(opts.playerId)}/ws-token`, opts
|
|
3016
|
-
|
|
3142
|
+
const data = requireRecord(await clerkFetch(opts.fetchImpl ?? fetch, `${base}/api/player/${encodeURIComponent(opts.playerId)}/ws-token`, opts, {
|
|
3143
|
+
method: "POST"
|
|
3144
|
+
}), "ws-token response");
|
|
3145
|
+
if (typeof data.token !== "string" || !data.token)
|
|
3017
3146
|
throw new Error("ws-token response had no token");
|
|
3018
3147
|
return data.token;
|
|
3019
3148
|
}
|
|
@@ -3024,18 +3153,34 @@ class ClerkSource {
|
|
|
3024
3153
|
httpBaseUrl;
|
|
3025
3154
|
constructor(opts) {
|
|
3026
3155
|
this.opts = opts;
|
|
3156
|
+
if (opts.apiKey === undefined && opts.headers === undefined) {
|
|
3157
|
+
throw new Error("ClerkSource requires an `apiKey` or `headers`");
|
|
3158
|
+
}
|
|
3027
3159
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
3028
3160
|
this.httpBaseUrl = opts.httpBaseUrl.replace(/\/$/, "");
|
|
3029
3161
|
}
|
|
3030
3162
|
get apiKey() {
|
|
3031
3163
|
return this.opts.apiKey;
|
|
3032
3164
|
}
|
|
3165
|
+
async fetchRegistration() {
|
|
3166
|
+
const data = requireRecord(await clerkFetch(this.fetchImpl, `${this.httpBaseUrl}/api/registration-code`, this.opts), "registration-code response");
|
|
3167
|
+
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") : [];
|
|
3168
|
+
return {
|
|
3169
|
+
registrationCode: typeof data.registration_code === "string" ? data.registration_code : "",
|
|
3170
|
+
players
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3033
3173
|
async listPlayers() {
|
|
3034
|
-
|
|
3035
|
-
return data.players ?? [];
|
|
3174
|
+
return (await this.fetchRegistration()).players;
|
|
3036
3175
|
}
|
|
3037
3176
|
mintWsToken(playerId) {
|
|
3038
|
-
return mintWsToken({
|
|
3177
|
+
return mintWsToken({
|
|
3178
|
+
httpBaseUrl: this.httpBaseUrl,
|
|
3179
|
+
apiKey: this.opts.apiKey,
|
|
3180
|
+
headers: this.opts.headers,
|
|
3181
|
+
playerId,
|
|
3182
|
+
fetchImpl: this.fetchImpl
|
|
3183
|
+
});
|
|
3039
3184
|
}
|
|
3040
3185
|
}
|
|
3041
3186
|
|
|
@@ -3112,7 +3257,7 @@ class EventStream {
|
|
|
3112
3257
|
if (next)
|
|
3113
3258
|
next({ value, done: false });
|
|
3114
3259
|
else
|
|
3115
|
-
this.buffer.push(value);
|
|
3260
|
+
this.buffer.push({ value });
|
|
3116
3261
|
}
|
|
3117
3262
|
end() {
|
|
3118
3263
|
if (this.ended)
|
|
@@ -3123,8 +3268,9 @@ class EventStream {
|
|
|
3123
3268
|
this.waiting.length = 0;
|
|
3124
3269
|
}
|
|
3125
3270
|
next() {
|
|
3126
|
-
|
|
3127
|
-
|
|
3271
|
+
const buffered = this.buffer.shift();
|
|
3272
|
+
if (buffered)
|
|
3273
|
+
return Promise.resolve({ value: buffered.value, done: false });
|
|
3128
3274
|
if (this.ended)
|
|
3129
3275
|
return Promise.resolve({ value: undefined, done: true });
|
|
3130
3276
|
return new Promise((resolve) => this.waiting.push(resolve));
|
|
@@ -3150,8 +3296,9 @@ class TypedEmitter {
|
|
|
3150
3296
|
set = new Set;
|
|
3151
3297
|
this.handlers.set(type, set);
|
|
3152
3298
|
}
|
|
3153
|
-
|
|
3154
|
-
|
|
3299
|
+
const wrapped = (payload) => handler(payload);
|
|
3300
|
+
set.add(wrapped);
|
|
3301
|
+
return () => set.delete(wrapped);
|
|
3155
3302
|
}
|
|
3156
3303
|
onAny(handler) {
|
|
3157
3304
|
this.anyHandlers.add(handler);
|
|
@@ -3315,8 +3462,12 @@ class ObservationCache {
|
|
|
3315
3462
|
}
|
|
3316
3463
|
|
|
3317
3464
|
// src/state/cache.ts
|
|
3318
|
-
function
|
|
3319
|
-
|
|
3465
|
+
function copySection(target, source, section) {
|
|
3466
|
+
const value = source[section];
|
|
3467
|
+
if (value === undefined)
|
|
3468
|
+
return false;
|
|
3469
|
+
target[section] = value;
|
|
3470
|
+
return true;
|
|
3320
3471
|
}
|
|
3321
3472
|
|
|
3322
3473
|
class StateCache {
|
|
@@ -3324,24 +3475,18 @@ class StateCache {
|
|
|
3324
3475
|
seed(snapshot) {
|
|
3325
3476
|
const next = {};
|
|
3326
3477
|
const changed = [];
|
|
3327
|
-
const src = asRecord(snapshot);
|
|
3328
3478
|
for (const section of STATE_SECTIONS) {
|
|
3329
|
-
if (
|
|
3330
|
-
asRecord(next)[section] = src[section];
|
|
3479
|
+
if (copySection(next, snapshot, section))
|
|
3331
3480
|
changed.push(section);
|
|
3332
|
-
}
|
|
3333
3481
|
}
|
|
3334
3482
|
this.state = next;
|
|
3335
3483
|
return changed;
|
|
3336
3484
|
}
|
|
3337
3485
|
applyDelta(delta) {
|
|
3338
3486
|
const changed = [];
|
|
3339
|
-
const src = asRecord(delta);
|
|
3340
3487
|
for (const section of STATE_SECTIONS) {
|
|
3341
|
-
if (
|
|
3342
|
-
asRecord(this.state)[section] = src[section];
|
|
3488
|
+
if (copySection(this.state, delta, section))
|
|
3343
3489
|
changed.push(section);
|
|
3344
|
-
}
|
|
3345
3490
|
}
|
|
3346
3491
|
return changed;
|
|
3347
3492
|
}
|
|
@@ -3349,7 +3494,7 @@ class StateCache {
|
|
|
3349
3494
|
const current = this.state[section];
|
|
3350
3495
|
if (current === undefined)
|
|
3351
3496
|
return [];
|
|
3352
|
-
|
|
3497
|
+
this.state[section] = { ...current, ...patch };
|
|
3353
3498
|
return [section];
|
|
3354
3499
|
}
|
|
3355
3500
|
snapshot() {
|
|
@@ -3388,13 +3533,12 @@ class StateCache {
|
|
|
3388
3533
|
function extractPendingAck(structured) {
|
|
3389
3534
|
if (!structured)
|
|
3390
3535
|
return;
|
|
3391
|
-
if (structured
|
|
3392
|
-
return { command: String(structured
|
|
3536
|
+
if (structured.pending === true) {
|
|
3537
|
+
return { command: String(structured.command ?? ""), message: String(structured.message ?? "") };
|
|
3393
3538
|
}
|
|
3394
|
-
const details = structured
|
|
3395
|
-
if (details &&
|
|
3396
|
-
|
|
3397
|
-
return { command: String(d["command"] ?? ""), message: String(d["message"] ?? "") };
|
|
3539
|
+
const details = structured.details;
|
|
3540
|
+
if (isRecord(details) && details.pending === true) {
|
|
3541
|
+
return { command: String(details.command ?? ""), message: String(details.message ?? "") };
|
|
3398
3542
|
}
|
|
3399
3543
|
return;
|
|
3400
3544
|
}
|
|
@@ -3426,6 +3570,8 @@ class Correlator {
|
|
|
3426
3570
|
return false;
|
|
3427
3571
|
switch (frame.type) {
|
|
3428
3572
|
case "result": {
|
|
3573
|
+
if (!isResultFrame(frame))
|
|
3574
|
+
return this.rejectInvalidResponse(requestId, pending, frame.type);
|
|
3429
3575
|
const payload = frame.payload;
|
|
3430
3576
|
if (pending.kind === "query") {
|
|
3431
3577
|
this.settle(requestId);
|
|
@@ -3437,7 +3583,7 @@ class Correlator {
|
|
|
3437
3583
|
if (!ack) {
|
|
3438
3584
|
this.settle(requestId);
|
|
3439
3585
|
pending.resolve({
|
|
3440
|
-
command: String(structured?.
|
|
3586
|
+
command: String(structured?.command ?? ""),
|
|
3441
3587
|
tick: 0,
|
|
3442
3588
|
delta: { details: structured }
|
|
3443
3589
|
});
|
|
@@ -3450,6 +3596,8 @@ class Correlator {
|
|
|
3450
3596
|
case "action_result": {
|
|
3451
3597
|
if (pending.kind !== "mutation")
|
|
3452
3598
|
return true;
|
|
3599
|
+
if (!isActionResultFrame(frame))
|
|
3600
|
+
return this.rejectInvalidResponse(requestId, pending, frame.type);
|
|
3453
3601
|
const payload = frame.payload;
|
|
3454
3602
|
this.settle(requestId);
|
|
3455
3603
|
pending.resolve({
|
|
@@ -3462,11 +3610,15 @@ class Correlator {
|
|
|
3462
3610
|
return true;
|
|
3463
3611
|
}
|
|
3464
3612
|
case "action_error": {
|
|
3613
|
+
if (!isActionErrorFrame(frame))
|
|
3614
|
+
return this.rejectInvalidResponse(requestId, pending, frame.type);
|
|
3465
3615
|
this.settle(requestId);
|
|
3466
3616
|
pending.reject(errorFromActionFrame(frame));
|
|
3467
3617
|
return true;
|
|
3468
3618
|
}
|
|
3469
3619
|
case "error": {
|
|
3620
|
+
if (!isErrorFrame(frame))
|
|
3621
|
+
return this.rejectInvalidResponse(requestId, pending, frame.type);
|
|
3470
3622
|
this.settle(requestId);
|
|
3471
3623
|
pending.reject(errorFromFrame(frame));
|
|
3472
3624
|
return true;
|
|
@@ -3483,10 +3635,43 @@ class Correlator {
|
|
|
3483
3635
|
settle(requestId) {
|
|
3484
3636
|
this.pending.delete(requestId);
|
|
3485
3637
|
}
|
|
3638
|
+
rejectInvalidResponse(requestId, pending, frameType) {
|
|
3639
|
+
this.settle(requestId);
|
|
3640
|
+
pending.reject(new SpacemoltError("invalid_response", `Malformed ${frameType} frame for request ${requestId}`));
|
|
3641
|
+
return true;
|
|
3642
|
+
}
|
|
3486
3643
|
}
|
|
3487
3644
|
|
|
3488
3645
|
// src/transport/socket.ts
|
|
3489
|
-
|
|
3646
|
+
class NativeWebSocketAdapter {
|
|
3647
|
+
socket;
|
|
3648
|
+
constructor(url) {
|
|
3649
|
+
this.socket = new WebSocket(url);
|
|
3650
|
+
}
|
|
3651
|
+
send(data) {
|
|
3652
|
+
this.socket.send(data);
|
|
3653
|
+
}
|
|
3654
|
+
close(code, reason) {
|
|
3655
|
+
this.socket.close(code, reason);
|
|
3656
|
+
}
|
|
3657
|
+
addEventListener(...args) {
|
|
3658
|
+
switch (args[0]) {
|
|
3659
|
+
case "open":
|
|
3660
|
+
this.socket.addEventListener("open", () => args[1]());
|
|
3661
|
+
break;
|
|
3662
|
+
case "message":
|
|
3663
|
+
this.socket.addEventListener("message", (event) => args[1]({ data: event.data }));
|
|
3664
|
+
break;
|
|
3665
|
+
case "close":
|
|
3666
|
+
this.socket.addEventListener("close", (event) => args[1]({ code: event.code, reason: event.reason }));
|
|
3667
|
+
break;
|
|
3668
|
+
case "error":
|
|
3669
|
+
this.socket.addEventListener("error", (event) => args[1](event));
|
|
3670
|
+
break;
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
var defaultFactory = (url) => new NativeWebSocketAdapter(url);
|
|
3490
3675
|
|
|
3491
3676
|
class Socket {
|
|
3492
3677
|
opts;
|
|
@@ -3542,7 +3727,10 @@ class Socket {
|
|
|
3542
3727
|
continue;
|
|
3543
3728
|
let frame;
|
|
3544
3729
|
try {
|
|
3545
|
-
|
|
3730
|
+
const parsed = JSON.parse(line);
|
|
3731
|
+
if (!isRecord(parsed) || typeof parsed.type !== "string" || parsed.request_id !== undefined && typeof parsed.request_id !== "string")
|
|
3732
|
+
throw new TypeError("frame must be an object with a type");
|
|
3733
|
+
frame = { ...parsed, type: parsed.type };
|
|
3546
3734
|
} catch (err) {
|
|
3547
3735
|
const head = line.slice(0, 200);
|
|
3548
3736
|
const tail = line.length > 400 ? line.slice(-200) : "";
|
|
@@ -3590,11 +3778,18 @@ function normalizeReconnect(opt) {
|
|
|
3590
3778
|
};
|
|
3591
3779
|
}
|
|
3592
3780
|
function retryAfterMs(err) {
|
|
3593
|
-
const
|
|
3781
|
+
const retryAfter = err.details?.retry_after;
|
|
3782
|
+
const fromDetails = typeof retryAfter === "number" ? retryAfter : undefined;
|
|
3594
3783
|
const match = err.message.match(/retry in (\d+)\s*second/i);
|
|
3595
3784
|
const seconds = fromDetails ?? (match ? Number(match[1]) : undefined);
|
|
3596
3785
|
return Math.max(250, (seconds ?? 1) * 1000);
|
|
3597
3786
|
}
|
|
3787
|
+
function requireStructuredContent(result, command) {
|
|
3788
|
+
if (result.structuredContent === undefined) {
|
|
3789
|
+
throw new SpacemoltError("invalid_response", `${command} returned no structured content`);
|
|
3790
|
+
}
|
|
3791
|
+
return result.structuredContent;
|
|
3792
|
+
}
|
|
3598
3793
|
|
|
3599
3794
|
class Account {
|
|
3600
3795
|
socket;
|
|
@@ -3619,10 +3814,11 @@ class Account {
|
|
|
3619
3814
|
_welcome = null;
|
|
3620
3815
|
_authenticated = false;
|
|
3621
3816
|
_loginPayload = null;
|
|
3817
|
+
_currentTick = 0;
|
|
3622
3818
|
_commands = null;
|
|
3623
3819
|
welcomeWaiter = null;
|
|
3624
3820
|
pendingAuth = null;
|
|
3625
|
-
|
|
3821
|
+
stateListeners = new Set;
|
|
3626
3822
|
userClosing = false;
|
|
3627
3823
|
reconnecting = false;
|
|
3628
3824
|
mutationLane = Promise.resolve();
|
|
@@ -3631,9 +3827,9 @@ class Account {
|
|
|
3631
3827
|
observationSubscribedState = false;
|
|
3632
3828
|
observationActiveScanState = false;
|
|
3633
3829
|
subscribedObservationPoiId;
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3830
|
+
reconnectedListeners = new Set;
|
|
3831
|
+
reconnectingListeners = new Set;
|
|
3832
|
+
disconnectedListeners = new Set;
|
|
3637
3833
|
constructor(opts = {}) {
|
|
3638
3834
|
this.seedState = opts.seedState ?? true;
|
|
3639
3835
|
this.url = opts.url ?? DEFAULT_URL;
|
|
@@ -3648,9 +3844,9 @@ class Account {
|
|
|
3648
3844
|
this._id = opts.id;
|
|
3649
3845
|
this.reconnectConfig = normalizeReconnect(opts.reconnect);
|
|
3650
3846
|
this.makeSocket();
|
|
3651
|
-
this.emitter.on("market_update", (
|
|
3652
|
-
this.emitter.on("observation_update", (
|
|
3653
|
-
this.observationCache.applyUpdate(
|
|
3847
|
+
this.emitter.on("market_update", (payload) => this.marketCache.applyUpdate(payload));
|
|
3848
|
+
this.emitter.on("observation_update", (payload) => {
|
|
3849
|
+
this.observationCache.applyUpdate(payload);
|
|
3654
3850
|
this.bridgeObservationToLocation();
|
|
3655
3851
|
});
|
|
3656
3852
|
}
|
|
@@ -3698,6 +3894,13 @@ class Account {
|
|
|
3698
3894
|
get welcome() {
|
|
3699
3895
|
return this._welcome;
|
|
3700
3896
|
}
|
|
3897
|
+
get currentTick() {
|
|
3898
|
+
return this._currentTick;
|
|
3899
|
+
}
|
|
3900
|
+
observeTick(tick) {
|
|
3901
|
+
if (typeof tick === "number" && tick > this._currentTick)
|
|
3902
|
+
this._currentTick = tick;
|
|
3903
|
+
}
|
|
3701
3904
|
get authenticated() {
|
|
3702
3905
|
return this._authenticated;
|
|
3703
3906
|
}
|
|
@@ -3788,19 +3991,32 @@ class Account {
|
|
|
3788
3991
|
}
|
|
3789
3992
|
}
|
|
3790
3993
|
async refresh() {
|
|
3791
|
-
const
|
|
3792
|
-
const snapshot =
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
this.stateListener?.(changed);
|
|
3799
|
-
}
|
|
3994
|
+
const result = await this.commands.spacemolt.get_status();
|
|
3995
|
+
const snapshot = requireStructuredContent(result, "spacemolt/get_status");
|
|
3996
|
+
const changed = this.cache.seed(snapshot);
|
|
3997
|
+
if (changed.includes("location"))
|
|
3998
|
+
this.checkSubscriptionsAgainstLocation();
|
|
3999
|
+
if (changed.length)
|
|
4000
|
+
this.emitStateChange(changed);
|
|
3800
4001
|
return this.cache.snapshot();
|
|
3801
4002
|
}
|
|
3802
4003
|
onStateChange(listener) {
|
|
3803
|
-
this.
|
|
4004
|
+
this.stateListeners.add(listener);
|
|
4005
|
+
return () => {
|
|
4006
|
+
this.stateListeners.delete(listener);
|
|
4007
|
+
};
|
|
4008
|
+
}
|
|
4009
|
+
notifyListeners(listeners, label, ...args) {
|
|
4010
|
+
for (const listener of listeners) {
|
|
4011
|
+
try {
|
|
4012
|
+
listener(...args);
|
|
4013
|
+
} catch (err) {
|
|
4014
|
+
console.warn(`[spacemolt] ${label} listener threw: ${err}`);
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
emitStateChange(changed) {
|
|
4019
|
+
this.notifyListeners(this.stateListeners, "onStateChange", changed);
|
|
3804
4020
|
}
|
|
3805
4021
|
async logout() {
|
|
3806
4022
|
await this.query("spacemolt_auth", "logout");
|
|
@@ -3850,7 +4066,7 @@ class Account {
|
|
|
3850
4066
|
}, (err) => {
|
|
3851
4067
|
if (timer)
|
|
3852
4068
|
clearTimeout(timer);
|
|
3853
|
-
reject(err);
|
|
4069
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
3854
4070
|
});
|
|
3855
4071
|
});
|
|
3856
4072
|
}
|
|
@@ -3871,13 +4087,12 @@ class Account {
|
|
|
3871
4087
|
return this.emitter.anyStream();
|
|
3872
4088
|
}
|
|
3873
4089
|
async subscribeMarket() {
|
|
3874
|
-
const
|
|
3875
|
-
const snapshot =
|
|
3876
|
-
|
|
3877
|
-
this.marketCache.seed(snapshot);
|
|
4090
|
+
const result = await this.commands.spacemolt_market.subscribe_market();
|
|
4091
|
+
const snapshot = requireStructuredContent(result, "spacemolt_market/subscribe_market");
|
|
4092
|
+
this.marketCache.seed(snapshot);
|
|
3878
4093
|
this.marketSubscribedState = true;
|
|
3879
|
-
this.subscribedMarketBaseId = snapshot
|
|
3880
|
-
return snapshot
|
|
4094
|
+
this.subscribedMarketBaseId = snapshot.base_id;
|
|
4095
|
+
return snapshot;
|
|
3881
4096
|
}
|
|
3882
4097
|
async unsubscribeMarket() {
|
|
3883
4098
|
const baseId = this.subscribedMarketBaseId ?? this.location?.docked_at ?? this.marketCache.bases()[0];
|
|
@@ -3891,16 +4106,14 @@ class Account {
|
|
|
3891
4106
|
return this.marketCache.book(baseId);
|
|
3892
4107
|
}
|
|
3893
4108
|
async subscribeObservation(activeScan = false) {
|
|
3894
|
-
const
|
|
3895
|
-
const snapshot =
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
this.bridgeObservationToLocation();
|
|
3899
|
-
}
|
|
4109
|
+
const result = await this.commands.spacemolt.subscribe_observation(activeScan ? { active_scan: true } : undefined);
|
|
4110
|
+
const snapshot = requireStructuredContent(result, "spacemolt/subscribe_observation");
|
|
4111
|
+
this.observationCache.seed(snapshot);
|
|
4112
|
+
this.bridgeObservationToLocation();
|
|
3900
4113
|
this.observationSubscribedState = true;
|
|
3901
4114
|
this.observationActiveScanState = activeScan;
|
|
3902
|
-
this.subscribedObservationPoiId = snapshot
|
|
3903
|
-
return snapshot
|
|
4115
|
+
this.subscribedObservationPoiId = snapshot.poi_id;
|
|
4116
|
+
return snapshot;
|
|
3904
4117
|
}
|
|
3905
4118
|
async unsubscribeObservation() {
|
|
3906
4119
|
this.observationSubscribedState = false;
|
|
@@ -3918,7 +4131,7 @@ class Account {
|
|
|
3918
4131
|
nearby_player_count: view.nearby.size
|
|
3919
4132
|
});
|
|
3920
4133
|
if (changed.length)
|
|
3921
|
-
this.
|
|
4134
|
+
this.emitStateChange(changed);
|
|
3922
4135
|
}
|
|
3923
4136
|
observation() {
|
|
3924
4137
|
return this.observationCache.current();
|
|
@@ -3940,13 +4153,22 @@ class Account {
|
|
|
3940
4153
|
this.socket.close();
|
|
3941
4154
|
}
|
|
3942
4155
|
onReconnected(listener) {
|
|
3943
|
-
this.
|
|
4156
|
+
this.reconnectedListeners.add(listener);
|
|
4157
|
+
return () => {
|
|
4158
|
+
this.reconnectedListeners.delete(listener);
|
|
4159
|
+
};
|
|
3944
4160
|
}
|
|
3945
4161
|
onReconnecting(listener) {
|
|
3946
|
-
this.
|
|
4162
|
+
this.reconnectingListeners.add(listener);
|
|
4163
|
+
return () => {
|
|
4164
|
+
this.reconnectingListeners.delete(listener);
|
|
4165
|
+
};
|
|
3947
4166
|
}
|
|
3948
4167
|
onDisconnected(listener) {
|
|
3949
|
-
this.
|
|
4168
|
+
this.disconnectedListeners.add(listener);
|
|
4169
|
+
return () => {
|
|
4170
|
+
this.disconnectedListeners.delete(listener);
|
|
4171
|
+
};
|
|
3950
4172
|
}
|
|
3951
4173
|
async authExchange(action, payload) {
|
|
3952
4174
|
const authPromise = new Promise((resolve, reject) => {
|
|
@@ -3989,19 +4211,28 @@ class Account {
|
|
|
3989
4211
|
return `r${++this.requestSeq}`;
|
|
3990
4212
|
}
|
|
3991
4213
|
routeFrame(frame) {
|
|
4214
|
+
if (isRecord(frame.payload))
|
|
4215
|
+
this.observeTick(frame.payload.tick);
|
|
3992
4216
|
switch (frame.type) {
|
|
3993
4217
|
case "welcome": {
|
|
4218
|
+
if (!isWelcomeFrame(frame))
|
|
4219
|
+
return this.warnMalformedFrame(frame);
|
|
3994
4220
|
const payload = frame.payload;
|
|
3995
4221
|
this._welcome = payload;
|
|
4222
|
+
this.observeTick(payload.current_tick);
|
|
3996
4223
|
this.welcomeWaiter?.resolve(payload);
|
|
3997
4224
|
this.welcomeWaiter = null;
|
|
3998
4225
|
return;
|
|
3999
4226
|
}
|
|
4000
4227
|
case "registered":
|
|
4228
|
+
if (!isRegisteredFrame(frame))
|
|
4229
|
+
return this.warnMalformedFrame(frame);
|
|
4001
4230
|
if (this.pendingAuth)
|
|
4002
4231
|
this.pendingAuth.registered = frame.payload;
|
|
4003
4232
|
return;
|
|
4004
4233
|
case "logged_in": {
|
|
4234
|
+
if (!isLoggedInFrame(frame))
|
|
4235
|
+
return this.warnMalformedFrame(frame);
|
|
4005
4236
|
const payload = frame.payload;
|
|
4006
4237
|
const auth = this.pendingAuth;
|
|
4007
4238
|
if (auth) {
|
|
@@ -4014,18 +4245,18 @@ class Account {
|
|
|
4014
4245
|
return;
|
|
4015
4246
|
}
|
|
4016
4247
|
case "action_result": {
|
|
4248
|
+
if (!isActionResultFrame(frame)) {
|
|
4249
|
+
if (!this.correlator.handle(frame))
|
|
4250
|
+
this.warnMalformedFrame(frame);
|
|
4251
|
+
return;
|
|
4252
|
+
}
|
|
4017
4253
|
const delta = frame.payload.result;
|
|
4018
4254
|
if (delta) {
|
|
4019
4255
|
const changed = this.cache.applyDelta(delta);
|
|
4020
4256
|
if (changed.includes("location"))
|
|
4021
4257
|
this.checkSubscriptionsAgainstLocation();
|
|
4022
|
-
if (changed.length)
|
|
4023
|
-
|
|
4024
|
-
this.stateListener?.(changed);
|
|
4025
|
-
} catch (err) {
|
|
4026
|
-
console.warn(`[spacemolt] onStateChange listener threw: ${err}`);
|
|
4027
|
-
}
|
|
4028
|
-
}
|
|
4258
|
+
if (changed.length)
|
|
4259
|
+
this.emitStateChange(changed);
|
|
4029
4260
|
}
|
|
4030
4261
|
if (!this.correlator.handle(frame)) {
|
|
4031
4262
|
console.warn(`[spacemolt] action_result for ${frame.request_id ?? "?"} arrived with no matching pending mutation (already timed out?)`);
|
|
@@ -4039,6 +4270,11 @@ class Account {
|
|
|
4039
4270
|
this.emitter.emit(frame);
|
|
4040
4271
|
return;
|
|
4041
4272
|
case "error": {
|
|
4273
|
+
if (!isErrorFrame(frame)) {
|
|
4274
|
+
if (!this.correlator.handle(frame))
|
|
4275
|
+
this.warnMalformedFrame(frame);
|
|
4276
|
+
return;
|
|
4277
|
+
}
|
|
4042
4278
|
if (this.correlator.handle(frame))
|
|
4043
4279
|
return;
|
|
4044
4280
|
const auth = this.pendingAuth;
|
|
@@ -4054,6 +4290,9 @@ class Account {
|
|
|
4054
4290
|
this.emitter.emit(frame);
|
|
4055
4291
|
}
|
|
4056
4292
|
}
|
|
4293
|
+
warnMalformedFrame(frame) {
|
|
4294
|
+
console.warn(`[spacemolt] dropped malformed ${frame.type} frame`);
|
|
4295
|
+
}
|
|
4057
4296
|
handleClose(err) {
|
|
4058
4297
|
this._authenticated = false;
|
|
4059
4298
|
this.correlator.rejectAll(err);
|
|
@@ -4071,7 +4310,7 @@ class Account {
|
|
|
4071
4310
|
if (this.shouldReconnect(err)) {
|
|
4072
4311
|
this.reconnectLoop(err);
|
|
4073
4312
|
} else if (!this.userClosing) {
|
|
4074
|
-
this.
|
|
4313
|
+
this.notifyListeners(this.disconnectedListeners, "onDisconnected", err);
|
|
4075
4314
|
}
|
|
4076
4315
|
}
|
|
4077
4316
|
shouldReconnect(err) {
|
|
@@ -4090,7 +4329,7 @@ class Account {
|
|
|
4090
4329
|
const cfg = this.reconnectConfig;
|
|
4091
4330
|
const retryAfterMs2 = retryAfterMsFromClose(err);
|
|
4092
4331
|
for (let attempt = 1;attempt <= cfg.maxRetries; attempt++) {
|
|
4093
|
-
this.
|
|
4332
|
+
this.notifyListeners(this.reconnectingListeners, "onReconnecting", attempt);
|
|
4094
4333
|
const backoff = attempt === 1 && retryAfterMs2 !== undefined ? retryAfterMs2 : Math.min(cfg.baseDelayMs * 2 ** (attempt - 1), cfg.maxDelayMs);
|
|
4095
4334
|
await delay(backoff);
|
|
4096
4335
|
if (this.userClosing) {
|
|
@@ -4100,12 +4339,12 @@ class Account {
|
|
|
4100
4339
|
try {
|
|
4101
4340
|
await this.reconnectOnce();
|
|
4102
4341
|
this.reconnecting = false;
|
|
4103
|
-
this.
|
|
4342
|
+
this.notifyListeners(this.reconnectedListeners, "onReconnected");
|
|
4104
4343
|
return;
|
|
4105
4344
|
} catch {}
|
|
4106
4345
|
}
|
|
4107
4346
|
this.reconnecting = false;
|
|
4108
|
-
this.
|
|
4347
|
+
this.notifyListeners(this.disconnectedListeners, "onDisconnected", err);
|
|
4109
4348
|
}
|
|
4110
4349
|
async reconnectOnce() {
|
|
4111
4350
|
if (!this.credentialsProvider)
|
|
@@ -4171,27 +4410,51 @@ class MemoryCredentialStore {
|
|
|
4171
4410
|
}
|
|
4172
4411
|
|
|
4173
4412
|
// src/data/catalog.ts
|
|
4174
|
-
|
|
4413
|
+
function catalogEntries(value) {
|
|
4414
|
+
return Array.isArray(value) ? value.filter((entry) => isRecord(entry)) : [];
|
|
4415
|
+
}
|
|
4416
|
+
function normalizeCatalog(value) {
|
|
4417
|
+
const data = requireRecord(value, "catalog response");
|
|
4418
|
+
return {
|
|
4419
|
+
version: typeof data.version === "string" ? data.version : undefined,
|
|
4420
|
+
ships: catalogEntries(data.ships),
|
|
4421
|
+
items: catalogEntries(data.items),
|
|
4422
|
+
recipes: catalogEntries(data.recipes),
|
|
4423
|
+
skills: catalogEntries(data.skills),
|
|
4424
|
+
facilities: catalogEntries(data.facilities)
|
|
4425
|
+
};
|
|
4426
|
+
}
|
|
4427
|
+
async function fetchCatalogConditional(httpBaseUrl, etag) {
|
|
4175
4428
|
const url = `${httpBaseUrl.replace(/\/$/, "")}/api/catalog.json`;
|
|
4176
|
-
const
|
|
4429
|
+
const headers = { accept: "application/json" };
|
|
4430
|
+
if (etag)
|
|
4431
|
+
headers["if-none-match"] = etag;
|
|
4432
|
+
const res = await fetch(url, { headers });
|
|
4433
|
+
if (res.status === 304)
|
|
4434
|
+
return { notModified: true, etag };
|
|
4177
4435
|
if (!res.ok)
|
|
4178
4436
|
throw new Error(`GET ${url} -> ${res.status} ${res.statusText}`);
|
|
4179
4437
|
const data = await res.json();
|
|
4180
4438
|
return {
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
recipes: data.recipes ?? [],
|
|
4185
|
-
skills: data.skills ?? [],
|
|
4186
|
-
facilities: data.facilities ?? []
|
|
4439
|
+
notModified: false,
|
|
4440
|
+
catalog: normalizeCatalog(data),
|
|
4441
|
+
etag: res.headers?.get?.("etag") ?? undefined
|
|
4187
4442
|
};
|
|
4188
4443
|
}
|
|
4444
|
+
async function fetchCatalog(httpBaseUrl) {
|
|
4445
|
+
const { catalog } = await fetchCatalogConditional(httpBaseUrl);
|
|
4446
|
+
if (!catalog)
|
|
4447
|
+
throw new Error("unconditional catalog fetch returned no catalog");
|
|
4448
|
+
return catalog;
|
|
4449
|
+
}
|
|
4189
4450
|
|
|
4190
4451
|
class CatalogCache {
|
|
4191
4452
|
catalog;
|
|
4453
|
+
etag;
|
|
4192
4454
|
indexes;
|
|
4193
|
-
constructor(catalog) {
|
|
4455
|
+
constructor(catalog, etag) {
|
|
4194
4456
|
this.catalog = catalog;
|
|
4457
|
+
this.etag = etag;
|
|
4195
4458
|
this.indexes = {
|
|
4196
4459
|
ships: index(catalog.ships),
|
|
4197
4460
|
items: index(catalog.items),
|
|
@@ -4201,7 +4464,18 @@ class CatalogCache {
|
|
|
4201
4464
|
};
|
|
4202
4465
|
}
|
|
4203
4466
|
static async load(httpBaseUrl) {
|
|
4204
|
-
|
|
4467
|
+
const { catalog, etag } = await fetchCatalogConditional(httpBaseUrl);
|
|
4468
|
+
if (!catalog)
|
|
4469
|
+
throw new Error("unconditional catalog fetch returned no catalog");
|
|
4470
|
+
return new CatalogCache(catalog, etag);
|
|
4471
|
+
}
|
|
4472
|
+
async revalidate(httpBaseUrl) {
|
|
4473
|
+
const result = await fetchCatalogConditional(httpBaseUrl, this.etag);
|
|
4474
|
+
if (result.notModified)
|
|
4475
|
+
return this;
|
|
4476
|
+
if (!result.catalog)
|
|
4477
|
+
throw new Error("modified catalog response returned no catalog");
|
|
4478
|
+
return new CatalogCache(result.catalog, result.etag);
|
|
4205
4479
|
}
|
|
4206
4480
|
get version() {
|
|
4207
4481
|
return this.catalog.version;
|
|
@@ -4253,8 +4527,10 @@ async function fetchMap(httpBaseUrl) {
|
|
|
4253
4527
|
const res = await fetch(url, { headers: { accept: "application/json" } });
|
|
4254
4528
|
if (!res.ok)
|
|
4255
4529
|
throw new Error(`GET ${url} -> ${res.status} ${res.statusText}`);
|
|
4256
|
-
const data = await res.json();
|
|
4257
|
-
|
|
4530
|
+
const data = requireRecord(await res.json(), "map response");
|
|
4531
|
+
const systems = Array.isArray(data.systems) ? data.systems.filter((system) => isRecord(system)) : [];
|
|
4532
|
+
const empires = isRecord(data.empires) ? Object.fromEntries(Object.entries(data.empires).filter((entry) => typeof entry[1] === "string")) : {};
|
|
4533
|
+
return { systems, empires };
|
|
4258
4534
|
}
|
|
4259
4535
|
|
|
4260
4536
|
class MapCache {
|
|
@@ -4303,6 +4579,7 @@ class SpacemoltClient {
|
|
|
4303
4579
|
store;
|
|
4304
4580
|
connected = new Map;
|
|
4305
4581
|
catalogCache;
|
|
4582
|
+
catalogFetchedAt = 0;
|
|
4306
4583
|
mapCache;
|
|
4307
4584
|
clerkSource;
|
|
4308
4585
|
accountConnectedListeners = new Set;
|
|
@@ -4343,8 +4620,13 @@ class SpacemoltClient {
|
|
|
4343
4620
|
return this.opts.httpBaseUrl ?? (this.opts.url ? httpBaseFromWs(this.opts.url) : DEFAULT_HTTP_BASE);
|
|
4344
4621
|
}
|
|
4345
4622
|
async catalog(force = false) {
|
|
4346
|
-
|
|
4347
|
-
|
|
4623
|
+
const maxAgeMs = this.opts.catalogMaxAgeMs ?? 3600000;
|
|
4624
|
+
const cached = this.catalogCache;
|
|
4625
|
+
const fresh = cached && !force && Date.now() - this.catalogFetchedAt < maxAgeMs;
|
|
4626
|
+
if (fresh)
|
|
4627
|
+
return cached;
|
|
4628
|
+
this.catalogCache = !this.catalogCache || force ? await CatalogCache.load(this.httpBaseUrl) : await this.catalogCache.revalidate(this.httpBaseUrl);
|
|
4629
|
+
this.catalogFetchedAt = Date.now();
|
|
4348
4630
|
return this.catalogCache;
|
|
4349
4631
|
}
|
|
4350
4632
|
async map(force = false) {
|
|
@@ -4366,16 +4648,22 @@ class SpacemoltClient {
|
|
|
4366
4648
|
async register(params) {
|
|
4367
4649
|
const account = this.createAccount(params.username);
|
|
4368
4650
|
this.connected.set(params.username, account);
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4651
|
+
try {
|
|
4652
|
+
await account.connect();
|
|
4653
|
+
const result = await account.register(params);
|
|
4654
|
+
await this.store.put({
|
|
4655
|
+
id: params.username,
|
|
4656
|
+
credentials: { kind: "login", username: params.username, password: result.password },
|
|
4657
|
+
playerId: result.player_id
|
|
4658
|
+
});
|
|
4659
|
+
account.onDisconnected((err) => this.handleAccountDisconnected(params.username, account, err));
|
|
4660
|
+
this.notifyAccountConnected(account);
|
|
4661
|
+
return { account, result };
|
|
4662
|
+
} catch (error) {
|
|
4663
|
+
account.close();
|
|
4664
|
+
this.connected.delete(params.username);
|
|
4665
|
+
throw error;
|
|
4666
|
+
}
|
|
4379
4667
|
}
|
|
4380
4668
|
connectRetryConfig() {
|
|
4381
4669
|
const retryOpt = this.opts.connectRetry ?? true;
|
|
@@ -4428,10 +4716,10 @@ class SpacemoltClient {
|
|
|
4428
4716
|
return this.connectIds(stored.map((s) => s.id), opts.onConnect);
|
|
4429
4717
|
}
|
|
4430
4718
|
async listOwnedPlayers() {
|
|
4431
|
-
return this.
|
|
4719
|
+
return this.requireClerkAuth().source.listPlayers();
|
|
4432
4720
|
}
|
|
4433
4721
|
async connectOwned(opts = {}) {
|
|
4434
|
-
const source = this.
|
|
4722
|
+
const { source, apiKey } = this.requireClerkAuth();
|
|
4435
4723
|
const players = await source.listPlayers();
|
|
4436
4724
|
const selected = opts.filter ? players.filter(opts.filter) : players;
|
|
4437
4725
|
const ids = [];
|
|
@@ -4440,7 +4728,7 @@ class SpacemoltClient {
|
|
|
4440
4728
|
id: player.username,
|
|
4441
4729
|
credentials: {
|
|
4442
4730
|
kind: "clerk",
|
|
4443
|
-
apiKey
|
|
4731
|
+
apiKey,
|
|
4444
4732
|
playerId: player.id,
|
|
4445
4733
|
httpBaseUrl: source.httpBaseUrl
|
|
4446
4734
|
},
|
|
@@ -4450,18 +4738,17 @@ class SpacemoltClient {
|
|
|
4450
4738
|
}
|
|
4451
4739
|
return this.connectIds(ids, opts.onConnect);
|
|
4452
4740
|
}
|
|
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
|
-
});
|
|
4741
|
+
requireClerkAuth() {
|
|
4742
|
+
const apiKey = this.opts.clerkApiKey;
|
|
4743
|
+
if (!apiKey) {
|
|
4744
|
+
throw new Error("connectOwned/listOwnedPlayers require `clerkApiKey` in SpacemoltClientOptions");
|
|
4463
4745
|
}
|
|
4464
|
-
|
|
4746
|
+
this.clerkSource ??= new ClerkSource({
|
|
4747
|
+
apiKey,
|
|
4748
|
+
httpBaseUrl: this.httpBaseUrl,
|
|
4749
|
+
fetchImpl: this.opts.fetchImpl
|
|
4750
|
+
});
|
|
4751
|
+
return { source: this.clerkSource, apiKey };
|
|
4465
4752
|
}
|
|
4466
4753
|
async connectIds(ids, onConnect) {
|
|
4467
4754
|
const accounts = [];
|
|
@@ -4505,6 +4792,8 @@ class SpacemoltClient {
|
|
|
4505
4792
|
}
|
|
4506
4793
|
isFirst = false;
|
|
4507
4794
|
const task = this.rateLimitedQueue.shift();
|
|
4795
|
+
if (!task)
|
|
4796
|
+
break;
|
|
4508
4797
|
ranInCurrentBatch++;
|
|
4509
4798
|
await task();
|
|
4510
4799
|
}
|
|
@@ -4604,6 +4893,7 @@ var TYPED_NOTIFICATION_TYPES = [
|
|
|
4604
4893
|
"reconnected",
|
|
4605
4894
|
"scan_detected",
|
|
4606
4895
|
"skill_level_up",
|
|
4896
|
+
"station_repaired",
|
|
4607
4897
|
"trade_cancelled",
|
|
4608
4898
|
"trade_complete",
|
|
4609
4899
|
"trade_declined",
|
|
@@ -4612,8 +4902,16 @@ var TYPED_NOTIFICATION_TYPES = [
|
|
|
4612
4902
|
export {
|
|
4613
4903
|
retryAfterMsFromClose,
|
|
4614
4904
|
mintWsToken,
|
|
4905
|
+
isWelcomeFrame,
|
|
4906
|
+
isResultFrame,
|
|
4907
|
+
isRegisteredFrame,
|
|
4908
|
+
isLoggedInFrame,
|
|
4909
|
+
isErrorFrame,
|
|
4910
|
+
isActionResultFrame,
|
|
4911
|
+
isActionErrorFrame,
|
|
4615
4912
|
httpBaseFromWs,
|
|
4616
4913
|
fetchMap,
|
|
4914
|
+
fetchCatalogConditional,
|
|
4617
4915
|
fetchCatalog,
|
|
4618
4916
|
buildCommands,
|
|
4619
4917
|
TypedEmitter,
|