@anweather/game-plugin-ssdk 1.16.0 → 1.18.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.
Files changed (47) hide show
  1. package/dist-cjs/runtime/auth.js +8 -0
  2. package/dist-cjs/runtime/blobClient.js +20 -2
  3. package/dist-cjs/runtime/conformance.js +6 -0
  4. package/dist-cjs/runtime/index.js +24 -1
  5. package/dist-cjs/runtime/live/checkpoint.js +31 -0
  6. package/dist-cjs/runtime/live/conformance.js +215 -0
  7. package/dist-cjs/runtime/live/harness.js +144 -0
  8. package/dist-cjs/runtime/live/outbox.js +72 -0
  9. package/dist-cjs/runtime/live/protocol.js +227 -0
  10. package/dist-cjs/runtime/live/rooms.js +579 -0
  11. package/dist-cjs/runtime/live/server.js +105 -0
  12. package/dist-cjs/runtime/servePlugin.js +8 -2
  13. package/dist-cjs/runtime/spaceClient.js +214 -0
  14. package/dist-cjs/src/models/models_0.js +43 -1
  15. package/dist-cjs/src/protocols/Aws_restJson1.js +9 -0
  16. package/dist-cjs/tsconfig.cjs.tsbuildinfo +1 -1
  17. package/dist-es/runtime/auth.js +5 -0
  18. package/dist-es/runtime/blobClient.js +19 -2
  19. package/dist-es/runtime/conformance.js +6 -0
  20. package/dist-es/runtime/index.js +7 -1
  21. package/dist-es/runtime/live/checkpoint.js +28 -0
  22. package/dist-es/runtime/live/conformance.js +210 -0
  23. package/dist-es/runtime/live/harness.js +141 -0
  24. package/dist-es/runtime/live/outbox.js +68 -0
  25. package/dist-es/runtime/live/protocol.js +217 -0
  26. package/dist-es/runtime/live/rooms.js +575 -0
  27. package/dist-es/runtime/live/server.js +102 -0
  28. package/dist-es/runtime/servePlugin.js +8 -2
  29. package/dist-es/runtime/spaceClient.js +205 -0
  30. package/dist-es/src/models/models_0.js +42 -0
  31. package/dist-es/src/protocols/Aws_restJson1.js +9 -0
  32. package/dist-es/tsconfig.es.tsbuildinfo +1 -1
  33. package/dist-types/runtime/auth.d.ts +3 -0
  34. package/dist-types/runtime/blobClient.d.ts +27 -2
  35. package/dist-types/runtime/conformance.d.ts +6 -0
  36. package/dist-types/runtime/index.d.ts +15 -2
  37. package/dist-types/runtime/live/checkpoint.d.ts +15 -0
  38. package/dist-types/runtime/live/conformance.d.ts +24 -0
  39. package/dist-types/runtime/live/harness.d.ts +71 -0
  40. package/dist-types/runtime/live/outbox.d.ts +39 -0
  41. package/dist-types/runtime/live/protocol.d.ts +169 -0
  42. package/dist-types/runtime/live/rooms.d.ts +302 -0
  43. package/dist-types/runtime/live/server.d.ts +36 -0
  44. package/dist-types/runtime/servePlugin.d.ts +8 -0
  45. package/dist-types/runtime/spaceClient.d.ts +159 -0
  46. package/dist-types/src/models/models_0.d.ts +23 -0
  47. package/package.json +6 -3
@@ -0,0 +1,3 @@
1
+ /** True when `header` is exactly `Bearer <token>`. Hashing both sides first
2
+ * gives timingSafeEqual equal-length inputs without leaking the length. */
3
+ export declare function bearerMatches(header: string | undefined, token: string): boolean;
@@ -1,10 +1,25 @@
1
- /** `save:{id}`, `player:{id}` or `world:{uuid}` -- see the model's doc. */
2
- export type BlobOwnerKey = `save:${string}` | `player:${string}` | `world:${string}`;
1
+ /** `save:{id}`, `player:{id}`, `world:{uuid}` or `space:{spaceId}` -- see
2
+ * the model's doc. A `space:` owner can only be written once the Space
3
+ * exists (and is this game type's); check current membership first, see
4
+ * spaceClient.ts. */
5
+ export type BlobOwnerKey = `save:${string}` | `player:${string}` | `world:${string}` | `space:${string}`;
3
6
  export declare const blobOwner: {
4
7
  save: (saveFileId: string) => BlobOwnerKey;
5
8
  player: (participantId: string) => BlobOwnerKey;
6
9
  world: (worldId: string) => BlobOwnerKey;
10
+ space: (spaceId: string) => BlobOwnerKey;
7
11
  };
12
+ /** One owner holding blobs, as ListBlobOwners reports it. */
13
+ export interface BlobOwnerSummary {
14
+ ownerKey: string;
15
+ blobCount: number;
16
+ totalBytes: number;
17
+ }
18
+ export interface BlobOwnerPage {
19
+ owners: BlobOwnerSummary[];
20
+ /** Pass as `after` for the next page; absent on the last one. */
21
+ nextToken?: string;
22
+ }
8
23
  export interface BlobClientOptions {
9
24
  /** The platform's base URL, e.g. http://platform:8080. */
10
25
  platformUrl: string;
@@ -70,8 +85,18 @@ export interface BlobClient {
70
85
  totalBytes: number;
71
86
  quotaBytes: number;
72
87
  }>;
88
+ /** Owners of this game type holding blobs, by key prefix ("world:",
89
+ * "space", or "" for all), sorted by key, one page (default 500, at
90
+ * most 1000) at a time. See allBlobOwners for every page. */
91
+ listOwners(prefix?: string, page?: {
92
+ after?: string;
93
+ limit?: number;
94
+ }): Promise<BlobOwnerPage>;
73
95
  }
74
96
  export declare function createBlobClient(options: BlobClientOptions): BlobClient;
97
+ /** Every owner with this prefix, following nextToken page by page -- e.g.
98
+ * a backfill's `for await (const o of allBlobOwners(blobs, "world:"))`. */
99
+ export declare function allBlobOwners(client: BlobClient, prefix?: string, pageSize?: number): AsyncGenerator<BlobOwnerSummary>;
75
100
  /** Read `key`, apply `change` to its value (undefined when absent), and
76
101
  * write the result conditional on the version read -- re-reading and
77
102
  * re-applying on a VersionConflict, up to `maxAttempts` times. `change`
@@ -1,9 +1,15 @@
1
1
  import type { GamePluginService } from "../src/server/GamePluginService";
2
+ import { type LiveConformanceOptions } from "./live/conformance";
3
+ import type { LiveOptions } from "./live/rooms";
2
4
  export interface ConformanceOptions {
3
5
  /** Participant ids to seat. Defaults to metadata.minPlayers generated ids. */
4
6
  participants?: string[];
5
7
  /** InitializeGame config. Defaults to {}. */
6
8
  config?: unknown;
9
+ /** The plugin's servePlugin `live` option: adds the room checks (a real
10
+ * /v1/live on a throwaway port; see live/conformance.ts). */
11
+ live?: LiveOptions;
12
+ liveOptions?: LiveConformanceOptions;
7
13
  }
8
14
  export interface ConformanceReport {
9
15
  passed: string[];
@@ -8,5 +8,18 @@ export type { RegistrationOptions, RegistrationLoop } from "./registration";
8
8
  export { runConformance, assertConformance } from "./conformance";
9
9
  export type { ConformanceOptions, ConformanceReport } from "./conformance";
10
10
  export { PLUGIN_PROTOCOL_VERSION, runtimeVersion } from "./version";
11
- export { createBlobClient, casUpdate, blobOwner, BlobStoreError, isVersionConflict } from "./blobClient";
12
- export type { BlobClient, BlobClientOptions, BlobOwnerKey, Blob, BlobItem, BlobInfo, BlobWrite, BlobErrorType, } from "./blobClient";
11
+ export { createBlobClient, casUpdate, blobOwner, allBlobOwners, BlobStoreError, isVersionConflict } from "./blobClient";
12
+ export type { BlobClient, BlobClientOptions, BlobOwnerKey, Blob, BlobItem, BlobInfo, BlobWrite, BlobErrorType, BlobOwnerSummary, BlobOwnerPage, } from "./blobClient";
13
+ export { attachLive } from "./live/server";
14
+ export type { AttachLiveOptions, LiveServer } from "./live/server";
15
+ export type { LiveOptions, Room, RoomHandlers, Conn, RoomCloseReason, RoomTimer, SendOptions, BroadcastOptions, KickOptions, } from "./live/rooms";
16
+ export { createLiveHarness } from "./live/harness";
17
+ export { loadRoomCheckpoint } from "./live/checkpoint";
18
+ export type { RoomCheckpoint } from "./live/checkpoint";
19
+ export type { LiveHarness, FakeLink, FakeClient, JoinSpec, HarnessMessage } from "./live/harness";
20
+ export { runLiveConformance, assertLiveConformance } from "./live/conformance";
21
+ export type { LiveConformanceOptions, LiveConformanceReport } from "./live/conformance";
22
+ export { LIVE_PROTOCOL_VERSION, LIVE_PATH, LinkCloseCode, MAX_LINK_FRAME_BYTES } from "./live/protocol";
23
+ export type { RoomScope, SpaceMember, PresenceState, Role } from "./live/protocol";
24
+ export { createSpaceClient, createMembershipGuard, isAlreadyASpace, SpaceApiError, NotASpaceMemberError, FakeSpaceClient, MAX_MEMBERSHIP_AGE_MS, } from "./spaceClient";
25
+ export type { SpaceClient, SpaceClientOptions, PluginSpace, PluginSpaceMember, SpaceImport, SpaceImportMember, SpaceImportResult, SpaceErrorType, SpaceCounts, SpaceRole, SpaceMemberStatus, MembershipGuard, MembershipGuardOptions, } from "./spaceClient";
@@ -0,0 +1,15 @@
1
+ import { type BlobClient, type BlobOwnerKey } from "../blobClient";
2
+ import type { Room } from "./rooms";
3
+ export interface RoomCheckpoint<T> {
4
+ /** What onRoom loaded (or the last successful save); undefined if none. */
5
+ readonly value: T | undefined;
6
+ /** The blob version this room owns; 0 before the first write. */
7
+ readonly version: number;
8
+ /** Write `value` if nobody else has written since this room's last read
9
+ * or write. False on a conflict: another room took over the scope (and,
10
+ * when a room was given, this one is closed). Other errors throw. */
11
+ save(value: T): Promise<boolean>;
12
+ }
13
+ export declare function loadRoomCheckpoint<T>(client: BlobClient, owner: BlobOwnerKey, key: string, opts?: {
14
+ room?: Room;
15
+ }): Promise<RoomCheckpoint<T>>;
@@ -0,0 +1,24 @@
1
+ import type { LiveOptions } from "./rooms";
2
+ export interface LiveConformanceOptions {
3
+ /** The game type to claim on the link. Default "conformance". */
4
+ gameType?: string;
5
+ /** Scope to open. Default a fresh "game:{uuid}". */
6
+ scope?: string;
7
+ /** The payload of every Signal the storm sends. Default {}: a room must
8
+ * treat client data as untrusted and must not throw on it. */
9
+ signal?: unknown;
10
+ /** Connections in the join/leave storm. Default 100. */
11
+ stormSize?: number;
12
+ /** How long to wait for `ready` and for the room to settle. Default 5 s. */
13
+ timeoutMs?: number;
14
+ }
15
+ export interface LiveConformanceReport {
16
+ passed: string[];
17
+ failed: {
18
+ check: string;
19
+ reason: string;
20
+ }[];
21
+ }
22
+ export declare function runLiveConformance(live: LiveOptions, options?: LiveConformanceOptions): Promise<LiveConformanceReport>;
23
+ /** runLiveConformance, throwing one error naming every failed check. */
24
+ export declare function assertLiveConformance(live: LiveOptions, options?: LiveConformanceOptions): Promise<LiveConformanceReport>;
@@ -0,0 +1,71 @@
1
+ import { type PresenceState, type Role, type SpaceMember } from "./protocol";
2
+ import { type LiveOptions, type Room } from "./rooms";
3
+ export interface HarnessMessage {
4
+ /** Always "host": the only sender a Hosted client hears from. */
5
+ from: "host";
6
+ k?: string;
7
+ d: unknown;
8
+ }
9
+ export interface FakeClient {
10
+ readonly conn: string;
11
+ readonly participantId: string;
12
+ /** Signals the room sent this connection, oldest first. */
13
+ readonly received: HarnessMessage[];
14
+ /** Set once the room kicked this connection. */
15
+ readonly kicked: {
16
+ code: number;
17
+ reason?: string;
18
+ } | undefined;
19
+ readonly open: boolean;
20
+ signal(d: unknown): void;
21
+ presence(state: PresenceState): void;
22
+ leave(reason?: string): void;
23
+ }
24
+ export interface JoinSpec {
25
+ participantId: string;
26
+ playerId?: string;
27
+ displayName?: string;
28
+ role?: Role;
29
+ gameId?: string;
30
+ hidden?: boolean;
31
+ state?: PresenceState;
32
+ /** Defaults to a fresh "c1", "c2"... per link. */
33
+ conn?: string;
34
+ }
35
+ export interface FakeLink {
36
+ readonly scope: string;
37
+ /** True once the room answered `ready`. */
38
+ readonly ready: boolean;
39
+ /** Set once the plugin closed the link. */
40
+ readonly closedByPlugin: {
41
+ code: number;
42
+ reason: string;
43
+ } | undefined;
44
+ /** Every plugin frame on this link, decoded, oldest first. */
45
+ readonly frames: unknown[];
46
+ join(spec: JoinSpec): FakeClient;
47
+ game(gameId: string, revision: number): void;
48
+ members(members: SpaceMember[]): void;
49
+ /** Push any text frame, well-formed or not (conformance, fuzzing). */
50
+ raw(text: string): void;
51
+ /** The platform closes the link: 1000 idle, 4410 the game ended, 1012 a
52
+ * platform restart... */
53
+ close(code?: number): void;
54
+ }
55
+ export interface LiveHarness {
56
+ /** Open a room link for `scope` ("game:{id}" or "space:{id}"). A second
57
+ * link for a scope already open is the deploy-overlap case. */
58
+ openLink(scope: string, opts?: {
59
+ epoch?: string;
60
+ }): FakeLink;
61
+ /** Resolves once every room has loaded and handled everything sent so far. */
62
+ settled(): Promise<void>;
63
+ room(scope: string): Room | undefined;
64
+ rooms(): Room[];
65
+ /** Plugin shutdown: onClose("shutdown") everywhere, links closed with 1012. */
66
+ shutdown(): Promise<void>;
67
+ }
68
+ export declare function createLiveHarness(live: LiveOptions, opts?: {
69
+ gameType?: string;
70
+ log?: Pick<Console, "log" | "error">;
71
+ }): LiveHarness;
@@ -0,0 +1,39 @@
1
+ export interface LinkWriter {
2
+ /** Hand `text` to the socket; call `done` once it has been flushed (or
3
+ * failed: the link is going away and its close handles the rest). */
4
+ write(text: string, done: () => void): void;
5
+ }
6
+ export interface OutboxOptions {
7
+ /** Bytes in flight (written, not yet flushed) before frames queue here. */
8
+ highWaterBytes?: number;
9
+ /** Queued frames before the oldest droppable one is discarded. */
10
+ maxQueuedFrames?: number;
11
+ }
12
+ export interface OutboxStats {
13
+ /** Frames waiting here right now. */
14
+ queued: number;
15
+ /** Frames replaced by a newer one with the same key. */
16
+ coalesced: number;
17
+ /** Frames discarded because the queue was full. */
18
+ dropped: number;
19
+ }
20
+ export declare const DEFAULT_HIGH_WATER_BYTES: number;
21
+ export declare const DEFAULT_MAX_QUEUED_FRAMES = 1024;
22
+ export declare class Outbox {
23
+ private readonly writer;
24
+ private readonly highWater;
25
+ private readonly maxQueued;
26
+ private readonly queue;
27
+ private inflight;
28
+ private pumping;
29
+ private closed;
30
+ readonly stats: OutboxStats;
31
+ constructor(writer: LinkWriter, options?: OutboxOptions);
32
+ push(text: string, opts: {
33
+ key?: string;
34
+ droppable: boolean;
35
+ }): void;
36
+ /** Stop writing; whatever is queued is abandoned with the link. */
37
+ close(): void;
38
+ private pump;
39
+ }
@@ -0,0 +1,169 @@
1
+ /** The X-Live-Protocol value this runtime speaks. */
2
+ export declare const LIVE_PROTOCOL_VERSION = 1;
3
+ /** The upgrade path, beside the RPC operations under /v1. */
4
+ export declare const LIVE_PATH = "/v1/live";
5
+ export declare const LIVE_HEADERS: {
6
+ readonly auth: "x-plugin-auth";
7
+ readonly scope: "x-live-scope";
8
+ readonly epoch: "x-live-epoch";
9
+ readonly gameType: "x-game-type";
10
+ readonly protocol: "x-live-protocol";
11
+ };
12
+ /** The biggest frame either side may put on the link. A client Signal is at
13
+ * most 16 KiB (the Hosted client ceiling, design §10.1), so a platform frame
14
+ * carrying one fits with room for the envelope; a plugin `send` over this is
15
+ * refused by the runtime before it is written. */
16
+ export declare const MAX_LINK_FRAME_BYTES: number;
17
+ /** Close codes on the room link (PROTOCOL.md §6). */
18
+ export declare const LinkCloseCode: {
19
+ /** Platform: the channel has been idle for its grace; the room is done.
20
+ * Plugin: `room.close()`, the room is discarded (the platform reopens a
21
+ * fresh link if clients remain). */
22
+ readonly NORMAL: 1000;
23
+ /** Platform: going away without a restart planned. */
24
+ readonly GOING_AWAY: 1001;
25
+ /** Malformed handshake follow-up or repeated protocol abuse. */
26
+ readonly POLICY: 1008;
27
+ /** A frame over MAX_LINK_FRAME_BYTES. */
28
+ readonly TOO_BIG: 1009;
29
+ /** The plugin's room failed (setup threw, or it fell too far behind). */
30
+ readonly INTERNAL_ERROR: 1011;
31
+ /** Either side is restarting; the platform reopens with backoff. */
32
+ readonly RESTART: 1012;
33
+ /** Platform: the link fell behind its inbound bound (design §10.3). */
34
+ readonly TOO_SLOW: 4008;
35
+ /** Plugin: a newer link with the same X-Live-Epoch replaced this one
36
+ * (PROTOCOL.md §5.4). The platform has already moved to the new link. */
37
+ readonly SUPERSEDED: 4409;
38
+ /** Platform: the scope ended (its Game completed or was abandoned). */
39
+ readonly GONE: 4410;
40
+ };
41
+ export type Role = "PARTICIPANT" | "SPECTATOR";
42
+ export type PresenceState = "ACTIVE" | "AWAY";
43
+ /** One entry of a Space's membership, the same shape as config.space.members. */
44
+ export interface SpaceMember {
45
+ playerId: string;
46
+ displayName: string;
47
+ role: string;
48
+ status: string;
49
+ }
50
+ export type RoomScope = {
51
+ kind: "game";
52
+ id: string;
53
+ key: string;
54
+ } | {
55
+ kind: "space";
56
+ id: string;
57
+ key: string;
58
+ };
59
+ export interface JoinFrame {
60
+ t: "join";
61
+ conn: string;
62
+ participantId: string;
63
+ playerId: string;
64
+ displayName: string;
65
+ role: Role;
66
+ gameId: string;
67
+ hidden: boolean;
68
+ state: PresenceState;
69
+ }
70
+ export interface LeaveFrame {
71
+ t: "leave";
72
+ conn: string;
73
+ reason: string;
74
+ }
75
+ export interface SigFrame {
76
+ t: "sig";
77
+ conn: string;
78
+ participantId: string;
79
+ d: unknown;
80
+ }
81
+ export interface PresenceFrame {
82
+ t: "presence";
83
+ conn: string;
84
+ state: PresenceState;
85
+ }
86
+ export interface GameFrame {
87
+ t: "game";
88
+ gameId: string;
89
+ revision: number;
90
+ }
91
+ export interface MembersFrame {
92
+ t: "members";
93
+ members: SpaceMember[];
94
+ }
95
+ export type PlatformFrame = JoinFrame | LeaveFrame | SigFrame | PresenceFrame | GameFrame | MembersFrame;
96
+ export interface SendFrame {
97
+ t: "send";
98
+ to: "all" | string[];
99
+ k?: string;
100
+ d: unknown;
101
+ }
102
+ export interface KickFrame {
103
+ t: "kick";
104
+ conn: string;
105
+ code: number;
106
+ reason?: string;
107
+ }
108
+ export interface ReadyFrame {
109
+ t: "ready";
110
+ }
111
+ export type PluginFrame = SendFrame | KickFrame | ReadyFrame;
112
+ export type Decoded<F> = {
113
+ ok: true;
114
+ frame: F;
115
+ }
116
+ /** Well-formed JSON object with a `t` this version doesn't know: ignore. */
117
+ | {
118
+ ok: false;
119
+ unknown: true;
120
+ t: string;
121
+ }
122
+ /** Not a JSON object, or a known frame missing/mistyping a field: drop. */
123
+ | {
124
+ ok: false;
125
+ unknown: false;
126
+ error: string;
127
+ };
128
+ /** A platform -> plugin frame, as the runtime reads it off the link. */
129
+ export declare function decodePlatformFrame(text: string): Decoded<PlatformFrame>;
130
+ /** A plugin -> platform frame, as the platform (or the loopback harness)
131
+ * reads it. The runtime never needs this itself; it is here so the harness
132
+ * and the tests decode exactly what the platform will. */
133
+ export declare function decodePluginFrame(text: string): Decoded<PluginFrame>;
134
+ /** A WebSocket close reason is at most 123 bytes; 120 characters of the
135
+ * plugin's own leaves room for nothing to be cut mid-character in practice. */
136
+ export declare const MAX_KICK_REASON = 120;
137
+ /** 4403 keeps the client out (terminal, design §6.3); 4900-4999 are the
138
+ * plugin's own, and the client reconnects after them like any other
139
+ * non-terminal close. Nothing else: a plugin must not be able to fake the
140
+ * platform's own meanings (4401 auth, 4409 superseded, 4410 game over...). */
141
+ export declare function isKickCode(v: unknown): v is number;
142
+ /** Encode either direction. `undefined` fields are dropped by JSON itself. */
143
+ export declare function encodeFrame(frame: PluginFrame | PlatformFrame): string;
144
+ export declare function byteLength(text: string): number;
145
+ /** "game:{gameId}" or "space:{spaceId}"; anything else is null. */
146
+ export declare function parseScope(header: string | undefined): RoomScope | null;
147
+ export interface HandshakeExpectations {
148
+ gameType: string;
149
+ /** Absent: no token configured (local dev), every caller is accepted --
150
+ * the same rule as the RPC routes. */
151
+ authToken?: string;
152
+ }
153
+ export type HandshakeResult = {
154
+ ok: true;
155
+ scope: RoomScope;
156
+ epoch: string;
157
+ protocol: number;
158
+ } | {
159
+ ok: false;
160
+ status: 400 | 401 | 404;
161
+ message: string;
162
+ };
163
+ type Headers = Record<string, string | string[] | undefined>;
164
+ /** Check an upgrade request's headers (lower-cased, as node gives them).
165
+ * The order is the order a platform engineer would want the errors in:
166
+ * auth first, so nothing about the plugin leaks to an unauthenticated
167
+ * caller. */
168
+ export declare function checkHandshake(headers: Headers, expect: HandshakeExpectations): HandshakeResult;
169
+ export {};