@spacemolt/lib 4.6.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/AGENTS.md +118 -0
- package/COMMANDS.md +386 -0
- package/LICENSE +21 -0
- package/README.md +298 -0
- package/dist/account.d.ts +389 -0
- package/dist/auth/clerk.d.ts +58 -0
- package/dist/auth/credentials.d.ts +52 -0
- package/dist/auth/file-store.d.ts +20 -0
- package/dist/client.d.ts +305 -0
- package/dist/data/catalog.d.ts +39 -0
- package/dist/data/map.d.ts +30 -0
- package/dist/errors.d.ts +52 -0
- package/dist/events/emitter.d.ts +58 -0
- package/dist/generated/actions.gen.d.ts +25 -0
- package/dist/generated/commands.gen.d.ts +1790 -0
- package/dist/generated/notifications.gen.d.ts +41 -0
- package/dist/generated/openapi/index.d.ts +1 -0
- package/dist/generated/openapi/types.gen.d.ts +19080 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +4638 -0
- package/dist/node.d.ts +8 -0
- package/dist/node.js +49 -0
- package/dist/protocol.d.ts +178 -0
- package/dist/state/cache.d.ts +43 -0
- package/dist/state/market.d.ts +30 -0
- package/dist/state/observation.d.ts +38 -0
- package/dist/transport/correlator.d.ts +54 -0
- package/dist/transport/socket.d.ts +60 -0
- package/llms.txt +23 -0
- package/package.json +49 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single authenticated SpaceMolt connection.
|
|
3
|
+
*
|
|
4
|
+
* Owns one WebSocket, performs raw-credential auth, and exposes the
|
|
5
|
+
* query/mutation command API, the local state cache (M2), and the typed push
|
|
6
|
+
* event system + market/observation subscriptions (M3).
|
|
7
|
+
*
|
|
8
|
+
* Auth frames (`registered`, `logged_in`) are handled out-of-band from the
|
|
9
|
+
* `request_id` correlator: after `register` the `logged_in` frame is an
|
|
10
|
+
* unsolicited push with no `request_id`, so auth is sequenced by frame type
|
|
11
|
+
* instead (only one auth exchange is ever in flight on a connection).
|
|
12
|
+
*/
|
|
13
|
+
import { type Commands } from './generated/commands.gen.js';
|
|
14
|
+
import type { AuthCredentials } from './auth/credentials.js';
|
|
15
|
+
import type { SubscribeMarketResponse, SubscribeObservationResponse } from './generated/openapi/types.gen.js';
|
|
16
|
+
import type { NotificationPayloads, TypedNotificationType } from './generated/notifications.gen.js';
|
|
17
|
+
import { ConnectionClosedError } from './errors.js';
|
|
18
|
+
import { type MarketBook } from './state/market.js';
|
|
19
|
+
import { type ObservationView } from './state/observation.js';
|
|
20
|
+
import type { GameState, MutationAck, MutationResult, QueryResult, RawFrame, StateSection, WelcomeFrame } from './protocol.js';
|
|
21
|
+
import { type WebSocketFactory } from './transport/socket.js';
|
|
22
|
+
export type LoggedInPayload = Record<string, unknown>;
|
|
23
|
+
export interface ReconnectOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Max reconnect attempts before giving up and emitting `disconnected`.
|
|
26
|
+
* Default `Infinity` — a real game-server restart is a routine, recurring
|
|
27
|
+
* event (not an edge case) and can legitimately take several minutes; a
|
|
28
|
+
* finite default tuned for a brief network blip (the old default was 10,
|
|
29
|
+
* exhausted in ~3.5 minutes at the default backoff) would abandon the
|
|
30
|
+
* account mid-restart and require manual intervention to reconnect it.
|
|
31
|
+
* Backoff still caps at `maxDelayMs` per attempt, so this never busy-loops.
|
|
32
|
+
* Pass a finite value to restore the old give-up behavior.
|
|
33
|
+
*/
|
|
34
|
+
maxRetries?: number;
|
|
35
|
+
/** Base backoff in ms (doubles per attempt). Default 1000. */
|
|
36
|
+
baseDelayMs?: number;
|
|
37
|
+
/** Backoff ceiling in ms. Default 30000. */
|
|
38
|
+
maxDelayMs?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface AccountOptions {
|
|
41
|
+
/** WebSocket URL of the v2 endpoint. Defaults to the production server. */
|
|
42
|
+
url?: string;
|
|
43
|
+
/** Inject a WebSocket implementation (tests, custom runtimes). */
|
|
44
|
+
webSocketFactory?: WebSocketFactory;
|
|
45
|
+
/**
|
|
46
|
+
* After authenticating, issue a `get_status` query to seed the local state
|
|
47
|
+
* cache with the canonical full state. Default `true`. Disable to avoid the
|
|
48
|
+
* extra round-trip; the cache then fills from the first mutation delta.
|
|
49
|
+
*/
|
|
50
|
+
seedState?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Auto-reconnect + re-auth on an unexpected disconnect. Requires
|
|
53
|
+
* `credentials`. Off unless enabled. Never reconnects after a deliberate
|
|
54
|
+
* `close()`, a `session_replaced` (4001), or an `auth_timeout` (4002).
|
|
55
|
+
*/
|
|
56
|
+
reconnect?: boolean | ReconnectOptions;
|
|
57
|
+
/**
|
|
58
|
+
* Supplies credentials for re-auth on reconnect. `login_token` credentials
|
|
59
|
+
* are single-use and can't reconnect; `clerk` credentials re-mint a fresh
|
|
60
|
+
* token each time, so they reconnect cleanly.
|
|
61
|
+
*/
|
|
62
|
+
credentials?: () => AuthCredentials | Promise<AuthCredentials>;
|
|
63
|
+
/** Inject a `fetch` implementation, used by `clerk` auth (tests, runtimes). */
|
|
64
|
+
fetchImpl?: typeof fetch;
|
|
65
|
+
/** Max automatic retries when a command is `rate_limited`. Default 5. */
|
|
66
|
+
maxRateLimitRetries?: number;
|
|
67
|
+
/** Stable id this account is managed under (e.g. the credential-store key / username). Exposed as `account.id`. */
|
|
68
|
+
id?: string;
|
|
69
|
+
/**
|
|
70
|
+
* How long to wait for the server's `welcome` frame and for a
|
|
71
|
+
* `logged_in`/error response to an auth attempt, before giving up. Default
|
|
72
|
+
* 15000 (15s — see `SpacemoltClientOptions.connectTimeoutMs` for why this
|
|
73
|
+
* exists).
|
|
74
|
+
*/
|
|
75
|
+
connectTimeoutMs?: number;
|
|
76
|
+
/**
|
|
77
|
+
* How long to wait for a query's response before giving up. Queries
|
|
78
|
+
* resolve synchronously server-side (no game-tick delay, unlike
|
|
79
|
+
* mutations) — per the game's own design there's no legitimate reason for
|
|
80
|
+
* one to take long, so unlike mutations (which can legitimately take
|
|
81
|
+
* minutes, e.g. a travel/jump's transit time) it's safe to bound these.
|
|
82
|
+
* Default 15000 (15s).
|
|
83
|
+
*/
|
|
84
|
+
queryTimeoutMs?: number;
|
|
85
|
+
/**
|
|
86
|
+
* How long to wait, after a mutation's pending ack arrives, for its final
|
|
87
|
+
* `action_result`/`action_error` outcome before giving up, for the two
|
|
88
|
+
* commands whose transit time is distance-based and can legitimately span
|
|
89
|
+
* many ticks: `jump` and `travel`. Every other mutation settles on the same
|
|
90
|
+
* tick it was queued on (per the game's own design — see
|
|
91
|
+
* `fastMutationTimeoutMs`) and uses that much shorter bound instead.
|
|
92
|
+
* Without this, a single silently-dropped outcome frame hangs forever AND
|
|
93
|
+
* wedges every subsequent mutation on this account behind it (mutations
|
|
94
|
+
* are serialized per account, see `enqueueMutation`). Default 600000 (10
|
|
95
|
+
* minutes).
|
|
96
|
+
*/
|
|
97
|
+
mutationTimeoutMs?: number;
|
|
98
|
+
/**
|
|
99
|
+
* How long to wait, after a mutation's pending ack arrives, for its final
|
|
100
|
+
* outcome — for every mutation except `jump`/`travel` (see
|
|
101
|
+
* `mutationTimeoutMs`). These settle within the same tick they were queued
|
|
102
|
+
* on, so there's no legitimate reason for one to take anywhere near as long
|
|
103
|
+
* as a transit; bounding them tightly turns a dropped outcome frame into a
|
|
104
|
+
* fast, visible failure instead of a many-minute stall each retry cycle.
|
|
105
|
+
* Kept well above one tick (default 180000 — 3 minutes, ~18 ticks at the
|
|
106
|
+
* game's 10s tick rate) so a merely-slow tick or a `rate_limited` resend
|
|
107
|
+
* can't trip it. Default 180000 (3 minutes).
|
|
108
|
+
*/
|
|
109
|
+
fastMutationTimeoutMs?: number;
|
|
110
|
+
}
|
|
111
|
+
export interface RegisterParams {
|
|
112
|
+
username: string;
|
|
113
|
+
empire: string;
|
|
114
|
+
registration_code?: string;
|
|
115
|
+
}
|
|
116
|
+
export interface RegisterResult {
|
|
117
|
+
/** 256-bit hex credential — store it; it cannot be recovered. */
|
|
118
|
+
password: string;
|
|
119
|
+
player_id: string;
|
|
120
|
+
/** Full initial game state from the following `logged_in` frame. */
|
|
121
|
+
state: LoggedInPayload;
|
|
122
|
+
}
|
|
123
|
+
export declare class Account {
|
|
124
|
+
private socket;
|
|
125
|
+
private readonly correlator;
|
|
126
|
+
private readonly cache;
|
|
127
|
+
private readonly emitter;
|
|
128
|
+
private readonly marketCache;
|
|
129
|
+
private readonly observationCache;
|
|
130
|
+
private readonly seedState;
|
|
131
|
+
private readonly url;
|
|
132
|
+
private readonly webSocketFactory?;
|
|
133
|
+
private readonly reconnectConfig;
|
|
134
|
+
private readonly credentialsProvider?;
|
|
135
|
+
private readonly fetchImpl?;
|
|
136
|
+
private readonly maxRateLimitRetries;
|
|
137
|
+
private readonly connectTimeoutMs;
|
|
138
|
+
private readonly queryTimeoutMs;
|
|
139
|
+
private readonly mutationTimeoutMs;
|
|
140
|
+
private readonly fastMutationTimeoutMs;
|
|
141
|
+
private readonly _id;
|
|
142
|
+
private requestSeq;
|
|
143
|
+
private _welcome;
|
|
144
|
+
private _authenticated;
|
|
145
|
+
private _loginPayload;
|
|
146
|
+
private _commands;
|
|
147
|
+
private welcomeWaiter;
|
|
148
|
+
private pendingAuth;
|
|
149
|
+
private stateListener;
|
|
150
|
+
private userClosing;
|
|
151
|
+
private reconnecting;
|
|
152
|
+
private mutationLane;
|
|
153
|
+
private marketSubscribedState;
|
|
154
|
+
private subscribedMarketBaseId;
|
|
155
|
+
private observationSubscribedState;
|
|
156
|
+
private observationActiveScanState;
|
|
157
|
+
private subscribedObservationPoiId;
|
|
158
|
+
private reconnectedListener;
|
|
159
|
+
private reconnectingListener;
|
|
160
|
+
private disconnectedListener;
|
|
161
|
+
constructor(opts?: AccountOptions);
|
|
162
|
+
private makeSocket;
|
|
163
|
+
/** Live view of the cached game state. Treat as read-only. */
|
|
164
|
+
get state(): Readonly<GameState>;
|
|
165
|
+
/** The id this account is managed under (store key / username), if any. */
|
|
166
|
+
get id(): string | undefined;
|
|
167
|
+
get player(): GameState['player'];
|
|
168
|
+
get ship(): GameState['ship'];
|
|
169
|
+
get location(): GameState['location'];
|
|
170
|
+
get cargo(): GameState['cargo'];
|
|
171
|
+
get skills(): GameState['skills'];
|
|
172
|
+
get credits(): number | undefined;
|
|
173
|
+
/** True when a tick-deferred action is queued for this account. */
|
|
174
|
+
get hasPendingAction(): boolean;
|
|
175
|
+
/** The raw `logged_in` payload from the last successful auth (login extras). */
|
|
176
|
+
get loginPayload(): LoggedInPayload | null;
|
|
177
|
+
/**
|
|
178
|
+
* Typed, generated command facade grouped by tool:
|
|
179
|
+
* `account.commands.spacemolt.jump({ id: 'sol' })`,
|
|
180
|
+
* `account.commands.spacemolt_market.view_market({ item_id: 'iron_ore' })`.
|
|
181
|
+
* Each method dispatches through `send`, so pacing and the state cache apply.
|
|
182
|
+
*/
|
|
183
|
+
get commands(): Commands;
|
|
184
|
+
/** The server's `welcome` payload, available after `connect()` resolves. */
|
|
185
|
+
get welcome(): WelcomeFrame['payload'] | null;
|
|
186
|
+
get authenticated(): boolean;
|
|
187
|
+
/**
|
|
188
|
+
* Whether `subscribeMarket()` is currently active on this instance —
|
|
189
|
+
* `resubscribe()` reads this directly to restore the subscription across a
|
|
190
|
+
* reconnect (this account's own `reconnectLoop`, or `reconnectOnce()`
|
|
191
|
+
* called by `SpacemoltClient` for a client-managed account).
|
|
192
|
+
*/
|
|
193
|
+
get marketSubscribed(): boolean;
|
|
194
|
+
/** Whether `subscribeObservation()` is currently active — see `marketSubscribed`. */
|
|
195
|
+
get observationSubscribed(): boolean;
|
|
196
|
+
/** The `activeScan` flag passed to the active `subscribeObservation()` call, if any — see `marketSubscribed`. */
|
|
197
|
+
get observationActiveScan(): boolean;
|
|
198
|
+
/** Open the connection and resolve once the `welcome` frame arrives. */
|
|
199
|
+
connect(): Promise<WelcomeFrame['payload']>;
|
|
200
|
+
private open;
|
|
201
|
+
/** Register a new account; resolves with the generated credentials + state. */
|
|
202
|
+
register(params: RegisterParams): Promise<RegisterResult>;
|
|
203
|
+
/** Authenticate an existing account with username + password. */
|
|
204
|
+
login(params: {
|
|
205
|
+
username: string;
|
|
206
|
+
password: string;
|
|
207
|
+
}): Promise<LoggedInPayload>;
|
|
208
|
+
/** Authenticate with a short-lived single-use token (web/Clerk path). */
|
|
209
|
+
loginToken(token: string): Promise<LoggedInPayload>;
|
|
210
|
+
/**
|
|
211
|
+
* Authenticate from a stored `AuthCredentials` (dispatches by kind).
|
|
212
|
+
* Auto-retries on `rate_limited` (waiting the server's interval) — for
|
|
213
|
+
* `clerk` credentials each retry mints a fresh ws-token, so a transient
|
|
214
|
+
* rejection doesn't waste a single-use token.
|
|
215
|
+
*/
|
|
216
|
+
authenticate(creds: AuthCredentials): Promise<void>;
|
|
217
|
+
private authenticateOnce;
|
|
218
|
+
/**
|
|
219
|
+
* Re-seed the cache from the canonical full state (`get_status`). Returns the
|
|
220
|
+
* cached state. Called automatically after auth unless `seedState` is false.
|
|
221
|
+
*/
|
|
222
|
+
refresh(): Promise<Readonly<GameState>>;
|
|
223
|
+
/** Register a listener fired with the sections that changed on each update. */
|
|
224
|
+
onStateChange(listener: (changed: StateSection[]) => void): void;
|
|
225
|
+
/** Log out, releasing the connection's authenticated session. */
|
|
226
|
+
logout(): Promise<void>;
|
|
227
|
+
/**
|
|
228
|
+
* Run a read-only command; resolves synchronously with the result.
|
|
229
|
+
* Auto-retries on `rate_limited` (waiting the server's interval). Bounded
|
|
230
|
+
* by `queryTimeoutMs` — unlike a mutation, a query has no legitimate reason
|
|
231
|
+
* to take long, so a response that never arrives (the server silently
|
|
232
|
+
* drops it rather than erroring) fails cleanly instead of hanging forever.
|
|
233
|
+
*/
|
|
234
|
+
query(tool: string, action: string, payload?: Record<string, unknown>): Promise<QueryResult>;
|
|
235
|
+
/**
|
|
236
|
+
* Run a mutation; resolves when the action executes on a later tick (after
|
|
237
|
+
* the immediate pending ack). `onAck` fires when the pending ack arrives.
|
|
238
|
+
*
|
|
239
|
+
* Mutations are serialized per account — the next is sent only after the
|
|
240
|
+
* previous resolves — because the server queues one action per tick and
|
|
241
|
+
* rejects a concurrent mutation with `action_pending`. Also auto-retries on
|
|
242
|
+
* `rate_limited`.
|
|
243
|
+
*
|
|
244
|
+
* Bounded in two phases (see `awaitMutationWithTimeout`) rather than left
|
|
245
|
+
* unbounded: a dropped outcome frame should fail cleanly, not hang this
|
|
246
|
+
* call (and every mutation queued behind it on this account) forever. Only
|
|
247
|
+
* `jump`/`travel` (`TRANSIT_ACTIONS`) get the generous `mutationTimeoutMs`;
|
|
248
|
+
* every other mutation — `mine`, `craft`, etc. — settles within the tick it
|
|
249
|
+
* was queued on and gets the much shorter `fastMutationTimeoutMs`.
|
|
250
|
+
*/
|
|
251
|
+
mutate(tool: string, action: string, payload?: Record<string, unknown>, onAck?: (ack: MutationAck) => void): Promise<MutationResult>;
|
|
252
|
+
/**
|
|
253
|
+
* Waits for a mutation's outcome with two-phase timeout protection. The
|
|
254
|
+
* pending ack (`result` frame) is a synchronous validation step with no
|
|
255
|
+
* game-tick delay — like a query, there's no legitimate reason for it to be
|
|
256
|
+
* missing, so it's bounded by `queryTimeoutMs`. Once the ack arrives, the
|
|
257
|
+
* final `action_result`/`action_error` can legitimately take many ticks for
|
|
258
|
+
* a transit mutation, so that phase gets the caller-supplied
|
|
259
|
+
* `mutationTimeoutMs` (either the long transit bound or the short
|
|
260
|
+
* same-tick bound — see `mutate`) instead. Either phase timing out cancels
|
|
261
|
+
* the correlator entry — otherwise a truly-dropped response would wedge
|
|
262
|
+
* every later mutation on this account behind it forever (see
|
|
263
|
+
* `enqueueMutation`).
|
|
264
|
+
*/
|
|
265
|
+
private awaitMutationWithTimeout;
|
|
266
|
+
/**
|
|
267
|
+
* Run a command, dispatching to `query`/`mutate` based on the spec's
|
|
268
|
+
* `x-is-mutation` classification. Unknown commands are treated as queries.
|
|
269
|
+
*/
|
|
270
|
+
send(tool: string, action: string, payload?: Record<string, unknown>): Promise<QueryResult | MutationResult>;
|
|
271
|
+
/**
|
|
272
|
+
* Listen for a specific server push by msg_type. The handler receives the
|
|
273
|
+
* typed payload for published notification types, or a loosely-typed object
|
|
274
|
+
* for not-yet-typed pushes. Returns an unsubscribe function.
|
|
275
|
+
*/
|
|
276
|
+
on<K extends TypedNotificationType>(type: K, handler: (payload: NotificationPayloads[K]) => void): () => void;
|
|
277
|
+
on(type: string, handler: (payload: Record<string, unknown>) => void): () => void;
|
|
278
|
+
/** Listen for every server push frame. Returns an unsubscribe function. */
|
|
279
|
+
onAny(handler: (frame: RawFrame) => void): () => void;
|
|
280
|
+
/**
|
|
281
|
+
* Async-iterate one push msg_type's payloads:
|
|
282
|
+
* `for await (const msg of account.events('chat_message')) { ... }`.
|
|
283
|
+
* Buffered, so a slow consumer won't drop frames; `break` unsubscribes.
|
|
284
|
+
*/
|
|
285
|
+
events<K extends TypedNotificationType>(type: K): AsyncIterableIterator<NotificationPayloads[K]>;
|
|
286
|
+
events(type: string): AsyncIterableIterator<Record<string, unknown>>;
|
|
287
|
+
/** Async-iterate every push frame. */
|
|
288
|
+
anyEvents(): AsyncIterableIterator<RawFrame>;
|
|
289
|
+
/**
|
|
290
|
+
* Subscribe to the order book of the station you're docked at. Returns the
|
|
291
|
+
* baseline snapshot and seeds the market cache; `market_update` pushes are
|
|
292
|
+
* merged automatically. Read with `account.market(baseId)`.
|
|
293
|
+
*
|
|
294
|
+
* The server subscription is scoped to "wherever you're currently docked"
|
|
295
|
+
* and has no explicit unsubscribe signal when it lapses: it's silently
|
|
296
|
+
* dropped server-side the instant you undock (`flushMarketSubscriptions`,
|
|
297
|
+
* `internal/game/market_subscriptions.go`), with no notification telling
|
|
298
|
+
* the client this happened. `checkSubscriptionsAgainstLocation` below is
|
|
299
|
+
* the client-side half of that — it watches for the same docked_at change
|
|
300
|
+
* and drops the stale book proactively, so `market()` can't keep serving a
|
|
301
|
+
* book the server has already stopped updating.
|
|
302
|
+
*/
|
|
303
|
+
subscribeMarket(): Promise<SubscribeMarketResponse>;
|
|
304
|
+
/** Unsubscribe from the current station's market and drop its cached book. */
|
|
305
|
+
unsubscribeMarket(): Promise<void>;
|
|
306
|
+
/** The cached order book for a base, if subscribed. */
|
|
307
|
+
market(baseId: string): MarketBook | undefined;
|
|
308
|
+
/**
|
|
309
|
+
* Subscribe to a change-feed of player presence at your current POI/system.
|
|
310
|
+
* Returns the baseline and seeds the observation cache; `observation_update`
|
|
311
|
+
* pushes are merged automatically. Read with `account.observation()`. Also
|
|
312
|
+
* bridges POI-scoped presence into the base state cache's
|
|
313
|
+
* `location.nearby_players`/`nearby_player_count` (see
|
|
314
|
+
* `bridgeObservationToLocation`), so `account.state.location` reflects live
|
|
315
|
+
* data too once subscribed, instead of only refreshing on the next
|
|
316
|
+
* `get_status`/mutation delta.
|
|
317
|
+
*
|
|
318
|
+
* Scoped to your current POI the same way `subscribeMarket` is scoped to
|
|
319
|
+
* your current dock — see the docked_at note there; the same silent
|
|
320
|
+
* server-side drop on leaving the POI applies here
|
|
321
|
+
* (`internal/game/observation_subscriptions.go`), and
|
|
322
|
+
* `checkSubscriptionsAgainstLocation` guards against it the same way.
|
|
323
|
+
*/
|
|
324
|
+
subscribeObservation(activeScan?: boolean): Promise<SubscribeObservationResponse>;
|
|
325
|
+
/** Unsubscribe from the observation watch and clear its cache. */
|
|
326
|
+
unsubscribeObservation(): Promise<void>;
|
|
327
|
+
/**
|
|
328
|
+
* Mirrors the observation watch's POI-scoped player presence into
|
|
329
|
+
* `location.nearby_players`/`nearby_player_count` in the base state cache,
|
|
330
|
+
* so code reading `account.state.location` (rather than
|
|
331
|
+
* `account.observation()`) sees live presence data once subscribed. NPC and
|
|
332
|
+
* pirate presence (`nearby_empire_npcs`/`nearby_pirates`, their counts, and
|
|
333
|
+
* `offline_collapsed`) have no equivalent in the observation feed — it only
|
|
334
|
+
* ever carries player presence — so those fields are left untouched by this
|
|
335
|
+
* bridge and still only refresh on the next `get_status`/mutation delta.
|
|
336
|
+
* No-ops if `location` hasn't been seeded yet or nothing is subscribed.
|
|
337
|
+
*/
|
|
338
|
+
private bridgeObservationToLocation;
|
|
339
|
+
/** The current observation watch view, if subscribed. */
|
|
340
|
+
observation(): ObservationView | null;
|
|
341
|
+
/**
|
|
342
|
+
* Detects a docked/POI change away from an active market or observation
|
|
343
|
+
* subscription and proactively drops the now-dead cache entry, mirroring
|
|
344
|
+
* the server's own silent unsubscribe-on-move (see the notes on
|
|
345
|
+
* `subscribeMarket`/`subscribeObservation`). Without this, `market()`/
|
|
346
|
+
* `observation()` would keep serving whatever was last cached — frozen,
|
|
347
|
+
* with nothing distinguishing it from a genuinely live book — for as long
|
|
348
|
+
* as nothing else happens to touch that specific base/POI again. Called
|
|
349
|
+
* after every state update that could carry a `location` change.
|
|
350
|
+
*/
|
|
351
|
+
private checkSubscriptionsAgainstLocation;
|
|
352
|
+
/** Close the connection deliberately. Suppresses auto-reconnect. */
|
|
353
|
+
close(): void;
|
|
354
|
+
/** Fired after a successful reconnect + re-auth. */
|
|
355
|
+
onReconnected(listener: () => void): void;
|
|
356
|
+
/** Fired at the start of each reconnect attempt (1-based). */
|
|
357
|
+
onReconnecting(listener: (attempt: number) => void): void;
|
|
358
|
+
/** Fired when the connection is gone for good (non-reconnectable or retries exhausted). */
|
|
359
|
+
onDisconnected(listener: (err: ConnectionClosedError) => void): void;
|
|
360
|
+
private authExchange;
|
|
361
|
+
/** Best-effort seed of the state cache after auth; failures are non-fatal. */
|
|
362
|
+
private maybeSeedState;
|
|
363
|
+
private beginAuth;
|
|
364
|
+
private sendFrame;
|
|
365
|
+
private nextRequestId;
|
|
366
|
+
private routeFrame;
|
|
367
|
+
private handleClose;
|
|
368
|
+
private shouldReconnect;
|
|
369
|
+
private reconnectLoop;
|
|
370
|
+
/**
|
|
371
|
+
* Reconnect this account's underlying connection in place: a fresh socket,
|
|
372
|
+
* re-authenticated, subscriptions restored — while preserving this
|
|
373
|
+
* instance's identity (cache, correlator, every wired listener). A single
|
|
374
|
+
* attempt with no internal retry/backoff; the caller owns retry pacing
|
|
375
|
+
* (this account's own `reconnectLoop`, or `SpacemoltClient`'s rate-limited
|
|
376
|
+
* queue for a client-managed account — see `handleAccountDisconnected`).
|
|
377
|
+
* Requires `credentials` (constructor option).
|
|
378
|
+
*
|
|
379
|
+
* Closes the current socket first — a no-op on the normal disconnect path
|
|
380
|
+
* (already closed), but avoids orphaning a still-open one if this is ever
|
|
381
|
+
* called outside that path (e.g. a future forced reconnect while a socket
|
|
382
|
+
* is technically still alive but not receiving pushes).
|
|
383
|
+
*/
|
|
384
|
+
reconnectOnce(): Promise<void>;
|
|
385
|
+
private resubscribe;
|
|
386
|
+
private withRateLimitRetry;
|
|
387
|
+
/** Serialize mutations: each runs only after the previous settles. */
|
|
388
|
+
private enqueueMutation;
|
|
389
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clerk-backed multi-account source.
|
|
3
|
+
*
|
|
4
|
+
* Authenticates with a Clerk **API key** (the headless-client credential —
|
|
5
|
+
* generate one from the website; see docs/live-testing.md) to enumerate the
|
|
6
|
+
* player accounts the Clerk user owns and to mint short-lived, single-use
|
|
7
|
+
* WebSocket login tokens for them. This lets one client manage every account you
|
|
8
|
+
* own without storing per-account game passwords.
|
|
9
|
+
*
|
|
10
|
+
* The API key is sent as `Authorization: Bearer <key>` to two gameserver
|
|
11
|
+
* endpoints:
|
|
12
|
+
* - `GET /api/registration-code` → the owned players list.
|
|
13
|
+
* - `POST /api/player/{id}/ws-token` → a single-use WS login token.
|
|
14
|
+
*
|
|
15
|
+
* ws-tokens are valid ~5 minutes and consumed on connect, so mint one per
|
|
16
|
+
* (re)connect rather than caching — the durable credential is the Clerk key.
|
|
17
|
+
*
|
|
18
|
+
* Uses only web-standard `fetch`, so it stays browser-safe.
|
|
19
|
+
*/
|
|
20
|
+
/** A player account owned by the authenticated Clerk user. */
|
|
21
|
+
export interface ClerkPlayer {
|
|
22
|
+
id: string;
|
|
23
|
+
username: string;
|
|
24
|
+
empire: string;
|
|
25
|
+
/** Hidden in the player's dashboard; still connectable. */
|
|
26
|
+
hidden: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface ClerkSourceOptions {
|
|
29
|
+
/** Clerk API key, sent as a Bearer token. Keep it secret. */
|
|
30
|
+
apiKey: string;
|
|
31
|
+
/** HTTP origin of the gameserver, e.g. `https://game.spacemolt.com`. */
|
|
32
|
+
httpBaseUrl: string;
|
|
33
|
+
/** Inject a `fetch` implementation (tests, custom runtimes). */
|
|
34
|
+
fetchImpl?: typeof fetch;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Mint a single-use WebSocket login token for one owned player. The token is
|
|
38
|
+
* valid ~5 minutes and consumed on connect, so call this once per (re)connect.
|
|
39
|
+
*/
|
|
40
|
+
export declare function mintWsToken(opts: {
|
|
41
|
+
httpBaseUrl: string;
|
|
42
|
+
apiKey: string;
|
|
43
|
+
playerId: string;
|
|
44
|
+
fetchImpl?: typeof fetch;
|
|
45
|
+
}): Promise<string>;
|
|
46
|
+
export declare class ClerkSource {
|
|
47
|
+
private readonly opts;
|
|
48
|
+
private readonly fetchImpl;
|
|
49
|
+
/** HTTP origin of the gameserver (trailing slash stripped). */
|
|
50
|
+
readonly httpBaseUrl: string;
|
|
51
|
+
constructor(opts: ClerkSourceOptions);
|
|
52
|
+
/** The Clerk API key this source authenticates with. */
|
|
53
|
+
get apiKey(): string;
|
|
54
|
+
/** List the player accounts the authenticated Clerk user owns. */
|
|
55
|
+
listPlayers(): Promise<ClerkPlayer[]>;
|
|
56
|
+
/** Mint a single-use WS login token for one owned player. */
|
|
57
|
+
mintWsToken(playerId: string): Promise<string>;
|
|
58
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable credential storage for multi-account use.
|
|
3
|
+
*
|
|
4
|
+
* The `CredentialStore` interface is the only persistence seam in the library;
|
|
5
|
+
* swap the implementation to control where credentials live (memory, a file,
|
|
6
|
+
* the browser, a secrets manager, …). The core never imports Node built-ins —
|
|
7
|
+
* the file-backed store does, and lives here in `src/auth/` for that reason.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* How an account authenticates. Raw credentials (login / login_token / register)
|
|
11
|
+
* plus `clerk`: a Clerk API key + the owned player's id, from which the account
|
|
12
|
+
* mints a fresh single-use WS token on each (re)connect (see `src/auth/clerk.ts`).
|
|
13
|
+
*/
|
|
14
|
+
export type AuthCredentials = {
|
|
15
|
+
kind: 'login';
|
|
16
|
+
username: string;
|
|
17
|
+
password: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'login_token';
|
|
20
|
+
token: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'clerk';
|
|
23
|
+
apiKey: string;
|
|
24
|
+
playerId: string;
|
|
25
|
+
httpBaseUrl: string;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'register';
|
|
28
|
+
username: string;
|
|
29
|
+
empire: string;
|
|
30
|
+
registration_code?: string;
|
|
31
|
+
};
|
|
32
|
+
export interface StoredAccount {
|
|
33
|
+
/** Stable handle for this account within the client (e.g. the username). */
|
|
34
|
+
id: string;
|
|
35
|
+
credentials: AuthCredentials;
|
|
36
|
+
/** Filled in after a successful auth. */
|
|
37
|
+
playerId?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface CredentialStore {
|
|
40
|
+
list(): Promise<StoredAccount[]>;
|
|
41
|
+
get(id: string): Promise<StoredAccount | undefined>;
|
|
42
|
+
put(account: StoredAccount): Promise<void>;
|
|
43
|
+
remove(id: string): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
/** In-memory store. The default; nothing is persisted across process restarts. */
|
|
46
|
+
export declare class MemoryCredentialStore implements CredentialStore {
|
|
47
|
+
private readonly accounts;
|
|
48
|
+
list(): Promise<StoredAccount[]>;
|
|
49
|
+
get(id: string): Promise<StoredAccount | undefined>;
|
|
50
|
+
put(account: StoredAccount): Promise<void>;
|
|
51
|
+
remove(id: string): Promise<void>;
|
|
52
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-backed credential store (Node / Bun).
|
|
3
|
+
*
|
|
4
|
+
* Persists accounts as JSON. Credentials are stored in plaintext — the same
|
|
5
|
+
* tradeoff the CLI makes — so point it at a path your environment protects.
|
|
6
|
+
* Imports Node built-ins; do not import this from browser bundles (use a
|
|
7
|
+
* custom `CredentialStore` there instead).
|
|
8
|
+
*/
|
|
9
|
+
import type { CredentialStore, StoredAccount } from './credentials.js';
|
|
10
|
+
export declare class FileCredentialStore implements CredentialStore {
|
|
11
|
+
private readonly path;
|
|
12
|
+
private cache;
|
|
13
|
+
constructor(path: string);
|
|
14
|
+
private load;
|
|
15
|
+
private save;
|
|
16
|
+
list(): Promise<StoredAccount[]>;
|
|
17
|
+
get(id: string): Promise<StoredAccount | undefined>;
|
|
18
|
+
put(account: StoredAccount): Promise<void>;
|
|
19
|
+
remove(id: string): Promise<void>;
|
|
20
|
+
}
|