@spacemolt/lib 4.6.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/node.js CHANGED
@@ -2,6 +2,52 @@
2
2
  import { readFile, writeFile, rename, mkdir } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
4
 
5
+ // src/validation.ts
6
+ function isRecord(value) {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+
10
+ // src/auth/credentials.ts
11
+ function hasString(value, key) {
12
+ return typeof value[key] === "string";
13
+ }
14
+ function isStoredAccount(value) {
15
+ if (!isRecord(value) || !hasString(value, "id") || !isRecord(value.credentials))
16
+ return false;
17
+ const credentials = value.credentials;
18
+ switch (credentials.kind) {
19
+ case "login":
20
+ return hasString(credentials, "username") && hasString(credentials, "password");
21
+ case "login_token":
22
+ return hasString(credentials, "token");
23
+ case "clerk":
24
+ return hasString(credentials, "apiKey") && hasString(credentials, "playerId") && hasString(credentials, "httpBaseUrl");
25
+ case "register":
26
+ return hasString(credentials, "username") && hasString(credentials, "empire") && (credentials.registration_code === undefined || typeof credentials.registration_code === "string");
27
+ default:
28
+ return false;
29
+ }
30
+ }
31
+
32
+ class MemoryCredentialStore {
33
+ accounts = new Map;
34
+ list() {
35
+ return Promise.resolve([...this.accounts.values()]);
36
+ }
37
+ get(id) {
38
+ return Promise.resolve(this.accounts.get(id));
39
+ }
40
+ put(account) {
41
+ this.accounts.set(account.id, account);
42
+ return Promise.resolve();
43
+ }
44
+ remove(id) {
45
+ this.accounts.delete(id);
46
+ return Promise.resolve();
47
+ }
48
+ }
49
+
50
+ // src/auth/file-store.ts
5
51
  class FileCredentialStore {
6
52
  path;
7
53
  cache = null;
@@ -14,9 +60,22 @@ class FileCredentialStore {
14
60
  try {
15
61
  const raw = await readFile(this.path, "utf-8");
16
62
  const parsed = JSON.parse(raw);
17
- this.cache = parsed.accounts ?? {};
18
- } catch {
19
- this.cache = {};
63
+ if (!isRecord(parsed) || parsed.version !== 1 || !isRecord(parsed.accounts)) {
64
+ throw new Error(`Invalid credential store format in ${this.path}`);
65
+ }
66
+ const accounts = {};
67
+ for (const [id, account] of Object.entries(parsed.accounts)) {
68
+ if (!isStoredAccount(account) || account.id !== id) {
69
+ throw new Error(`Invalid account ${JSON.stringify(id)} in credential store ${this.path}`);
70
+ }
71
+ accounts[id] = account;
72
+ }
73
+ this.cache = accounts;
74
+ } catch (error) {
75
+ if (isMissingFileError(error))
76
+ this.cache = {};
77
+ else
78
+ throw error;
20
79
  }
21
80
  return this.cache;
22
81
  }
@@ -24,7 +83,7 @@ class FileCredentialStore {
24
83
  const data = { version: 1, accounts: this.cache ?? {} };
25
84
  await mkdir(dirname(this.path), { recursive: true });
26
85
  const tmp = `${this.path}.tmp-${process.pid}`;
27
- await writeFile(tmp, JSON.stringify(data, null, 2));
86
+ await writeFile(tmp, JSON.stringify(data, null, 2), { mode: 384 });
28
87
  await rename(tmp, this.path);
29
88
  }
30
89
  async list() {
@@ -44,6 +103,9 @@ class FileCredentialStore {
44
103
  await this.save();
45
104
  }
46
105
  }
106
+ function isMissingFileError(error) {
107
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
108
+ }
47
109
  export {
48
110
  FileCredentialStore
49
111
  };
@@ -125,6 +125,13 @@ export interface RawFrame {
125
125
  payload?: unknown;
126
126
  }
127
127
  export type OutboundFrame = ResultFrame | ActionResultFrame | ActionErrorFrame | ErrorFrame | WelcomeFrame | LoggedInFrame | RegisteredFrame | NotificationFrame;
128
+ export declare function isResultFrame(frame: RawFrame): frame is ResultFrame;
129
+ export declare function isActionResultFrame(frame: RawFrame): frame is ActionResultFrame;
130
+ export declare function isActionErrorFrame(frame: RawFrame): frame is ActionErrorFrame;
131
+ export declare function isErrorFrame(frame: RawFrame): frame is ErrorFrame;
132
+ export declare function isWelcomeFrame(frame: RawFrame): frame is WelcomeFrame;
133
+ export declare function isLoggedInFrame(frame: RawFrame): frame is LoggedInFrame;
134
+ export declare function isRegisteredFrame(frame: RawFrame): frame is RegisteredFrame;
128
135
  /**
129
136
  * The cacheable game-state sections — the eight independently-tracked sections
130
137
  * the server emits deltas for. Section shapes are derived from the spec's
@@ -31,7 +31,7 @@
31
31
  * ordering beyond per-request_id sequencing.
32
32
  */
33
33
  import type { MutationAck, MutationResult, QueryResult, RawFrame } from '../protocol.js';
34
- import { ConnectionClosedError, SpacemoltError } from '../errors.js';
34
+ import { type ConnectionClosedError, SpacemoltError } from '../errors.js';
35
35
  export type RequestKind = 'query' | 'mutation';
36
36
  export declare class Correlator {
37
37
  private readonly pending;
@@ -51,4 +51,5 @@ export declare class Correlator {
51
51
  /** Reject every in-flight request — used when the socket closes. */
52
52
  rejectAll(err: ConnectionClosedError | SpacemoltError): void;
53
53
  private settle;
54
+ private rejectInvalidResponse;
54
55
  }
@@ -10,18 +10,20 @@
10
10
  import type { InboundFrame, RawFrame } from '../protocol.js';
11
11
  import { ConnectionClosedError } from '../errors.js';
12
12
  /** The minimal web-standard WebSocket surface this library depends on. */
13
+ export interface MessageEventLike {
14
+ data: unknown;
15
+ }
16
+ export interface CloseEventLike {
17
+ code?: number;
18
+ reason?: string;
19
+ }
13
20
  export interface WebSocketLike {
14
21
  send(data: string): void;
15
22
  close(code?: number, reason?: string): void;
16
23
  addEventListener(type: 'open', listener: () => void): void;
17
- addEventListener(type: 'message', listener: (ev: {
18
- data: unknown;
19
- }) => void): void;
20
- addEventListener(type: 'close', listener: (ev: {
21
- code?: number;
22
- reason?: string;
23
- }) => void): void;
24
- addEventListener(type: 'error', listener: (ev: unknown) => void): void;
24
+ addEventListener(type: 'message', listener: (event: MessageEventLike) => void): void;
25
+ addEventListener(type: 'close', listener: (event: CloseEventLike) => void): void;
26
+ addEventListener(type: 'error', listener: (event: unknown) => void): void;
25
27
  }
26
28
  export type WebSocketFactory = (url: string) => WebSocketLike;
27
29
  export interface SocketOptions {
@@ -0,0 +1,4 @@
1
+ /** True when a value is a non-null, non-array object with string keys. */
2
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
3
+ /** Require an object at an external-data boundary and include its source in failures. */
4
+ export declare function requireRecord(value: unknown, source: string): Record<string, unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spacemolt/lib",
3
- "version": "4.6.0",
3
+ "version": "7.1.0",
4
4
  "description": "The definitive TypeScript library for SpaceMolt. WebSocket-v2-first, multi-account, with local state caches. Internals are regenerated from the server's OpenAPI spec.",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
@@ -24,26 +24,37 @@
24
24
  "scripts": {
25
25
  "fetch-spec": "bun run scripts/fetch-spec.ts",
26
26
  "generate": "bun run openapi-ts && bun run scripts/generate.ts",
27
+ "generate:check": "bun run scripts/check-generated.ts",
28
+ "check": "bun run format:check && bun run lint && bun run generate:check && bun run typecheck && bun test && bun run build:browser-check && bun run build && bun run build:smoke",
29
+ "format": "biome format --write .",
30
+ "format:check": "biome format .",
31
+ "lint": "biome lint .",
27
32
  "typecheck": "bun x tsc --noEmit",
28
33
  "prepare": "bun run build",
29
34
  "build": "rm -rf dist && bun build src/index.ts --outfile dist/index.js --target node --format esm && bun build src/node.ts --outfile dist/node.js --target node --format esm && bun x tsc -p tsconfig.build.json && bun run scripts/fix-dts-extensions.ts",
35
+ "build:smoke": "node scripts/check-build.mjs",
30
36
  "build:browser-check": "bun build src/index.ts --target browser --outfile /dev/null",
31
37
  "test": "bun test"
32
38
  },
33
39
  "repository": {
34
40
  "type": "git",
35
- "url": "https://github.com/SpaceMolt/spacemolt-lib.git"
41
+ "url": "git+https://github.com/SpaceMolt/spacemolt-lib.git"
36
42
  },
37
43
  "homepage": "https://www.spacemolt.com",
38
44
  "license": "MIT",
45
+ "engines": {
46
+ "node": ">=22"
47
+ },
39
48
  "publishConfig": {
40
49
  "access": "public"
41
50
  },
42
51
  "devDependencies": {
52
+ "@biomejs/biome": "^2.5.3",
43
53
  "@hey-api/openapi-ts": "^0.73.0",
44
- "@types/bun": "^1.3.9"
54
+ "@types/bun": "^1.3.9",
55
+ "typescript": "^5.9.3"
45
56
  },
46
57
  "peerDependencies": {
47
- "typescript": "^5"
58
+ "typescript": "^5.5"
48
59
  }
49
60
  }