@roster-lock/types 1.0.0 → 1.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.
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/v1/runtime/game-launcher/index.ts"],"names":[],"mappings":""}
@@ -17,4 +17,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./download"), exports);
18
18
  __exportStar(require("./unstrusted-script"), exports);
19
19
  __exportStar(require("./piece-selection-sort"), exports);
20
+ __exportStar(require("./game-launcher"), exports);
21
+ __exportStar(require("./matchmaker-bridge"), exports);
20
22
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/v1/runtime/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AACA,6CAA2B;AAC3B,sDAAoC;AACpC,yDAAuC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/v1/runtime/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AACA,6CAA2B;AAC3B,sDAAoC;AACpC,yDAAuC;AACvC,kDAAgC;AAChC,sDAAoC"}
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MATCHMAKER_BRIDGE_PATHS = void 0;
4
+ exports.MATCHMAKER_BRIDGE_PATHS = {
5
+ installGameLauncherPlugin: "installGameLauncherPlugin",
6
+ getInstalledGameLauncherPlugins: "getInstalledGameLauncherPlugins",
7
+ getIdentity: "getIdentity",
8
+ requestSelection: "requestSelection",
9
+ updateGameLauncherSettings: "updateGameLauncherSettings",
10
+ initiateRelay: "initiateRelay",
11
+ ready: "ready",
12
+ };
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/v1/runtime/matchmaker-bridge/index.ts"],"names":[],"mappings":";;;AA6Ea,QAAA,uBAAuB,GAAG;IACrC,yBAAyB,EAAE,2BAA2B;IACtD,+BAA+B,EAAE,iCAAiC;IAClE,WAAW,EAAE,aAAa;IAC1B,gBAAgB,EAAE,kBAAkB;IACpC,0BAA0B,EAAE,4BAA4B;IACxD,aAAa,EAAE,eAAe;IAC9B,KAAK,EAAE,OAAO;CACN,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roster-lock/types",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Types for roster lock",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,6 +19,9 @@
19
19
  "rimraf": "^6.1.3",
20
20
  "typescript": "^7.0.2"
21
21
  },
22
+ "dependencies": {
23
+ "ajv": "^8.20.0"
24
+ },
22
25
  "scripts": {
23
26
  "build": "rimraf ./dist && tsc"
24
27
  }
@@ -3,6 +3,10 @@ import { PieceId, Sha256 } from "../shared";
3
3
 
4
4
  type URLType = string;
5
5
  type DownloadableSource = string;
6
+ // A "data:image/<type>;base64,<data>" URI, embedded directly in the lock file rather
7
+ // than fetched - unlike downloadSources, selection UIs need this available offline/at
8
+ // select-time, before any piece content has been downloaded.
9
+ type ImageDataURI = string;
6
10
 
7
11
  export type RosterLockPiece = {
8
12
  id: PieceId,
@@ -15,7 +19,7 @@ export type RosterLockPiece = {
15
19
  name: string,
16
20
  author: string,
17
21
  url: URLType,
18
- image?: URLType,
22
+ image?: ImageDataURI,
19
23
  }
20
24
  downloadSources: Array<DownloadableSource>,
21
25
  pathVariables: Record<string, string>,
@@ -8,7 +8,7 @@ import { SelectionUnselectableConfig } from "./selection-types/unselectable";
8
8
 
9
9
  export * from "./meta";
10
10
 
11
- export {
11
+ export type {
12
12
  SelectionPreselectedConfig,
13
13
  SelectionGameControlledConfig,
14
14
  SelectionNormalConfig,
@@ -16,7 +16,7 @@ export {
16
16
  UserSelectionValidation,
17
17
  };
18
18
 
19
- export { UntrustedScriptRef };
19
+ export type { UntrustedScriptRef };
20
20
 
21
21
  export type RosterLockSelectionConfig = {
22
22
  piece: Record<PieceType, (
@@ -2,7 +2,11 @@
2
2
  // Room Creation
3
3
  export type RoomConfig = {
4
4
  matchmakerId: string;
5
- coordinatorId: string;
5
+ // `false` is an explicit, deliberate "this room has no game coordinator"
6
+ // choice (e.g. a fully-internal connection mode) - kept required rather
7
+ // than optional so a matchmaker can't omit it by accident and have that
8
+ // silently treated as an opt-out.
9
+ coordinatorId: string | false;
6
10
  roomId: string;
7
11
  rosterConfigHash: string;
8
12
  machines: RoomMachine[];
@@ -0,0 +1,228 @@
1
+
2
+ import { RosterLockV1Config } from "../../lock";
3
+ import { RosterLockV1SyncDLResult } from "../../request";
4
+ import { RoomMachine } from "../../relay";
5
+ import { Sha256 } from "../../shared";
6
+ import type { AnySchema, JSONSchemaType } from "ajv";
7
+
8
+ export type ConnectionMode = "direct-tcp" | "room" | "internal";
9
+
10
+ // Mirrors NodeJS.Platform/NodeJS.Architecture (node_modules/@types/node's
11
+ // process.d.ts) rather than importing them - this package deliberately
12
+ // carries no @types/node dependency since it's also consumed by browser
13
+ // code (see package.json's empty "types" compiler option). A caller with
14
+ // @types/node can still pass process.platform/process.arch straight
15
+ // through unchanged.
16
+ export type PlatformName = (
17
+ "aix" | "android" | "darwin" | "freebsd" | "haiku" | "linux" |
18
+ "openbsd" | "sunos" | "win32" | "cygwin" | "netbsd"
19
+ );
20
+ export type ArchName = (
21
+ "arm" | "arm64" | "ia32" | "loong64" | "mips" | "mipsel" |
22
+ "ppc64" | "riscv64" | "s390x" | "x64"
23
+ );
24
+
25
+ // What a caller resolves a multi-platform binaryLocation bundle against -
26
+ // see docs/v2/binary-location.md.
27
+ export type PlatformTarget = {
28
+ platform: PlatformName,
29
+ arch: ArchName,
30
+ };
31
+
32
+ // The two variants shared as-is between ConnectionSetup and ConnectionConfig
33
+ // below - neither needs anything resolved before a plugin can use it.
34
+ type RoomOrInternalConnection = (
35
+ // The room's actual connection work happens entirely on the game side -
36
+ // `version` just tells it which of its own supported approaches to use
37
+ // (see GameLauncherPlugin.supportedRoomVersions), `url` is where it does
38
+ // that work. No separate protocol field: whatever `version` names already
39
+ // implies its protocol.
40
+ | { type: "room", version: string, url: string }
41
+ // The game has its own online connection story - roster-lock hands it a
42
+ // selection and gets out of the way.
43
+ | { type: "internal" }
44
+ );
45
+
46
+ // What a matchmaker/caller hands to match-agent's start-game route - a
47
+ // direct-tcp room's host address isn't knowable upfront by whoever builds
48
+ // this (it may be behind NAT, or simply not yet bound its listen socket), so
49
+ // both variants instead carry the address of a small rendezvous coordinator
50
+ // (see plugins/game-launcher/shared/direct-ip-coordinator and
51
+ // docs/v2/ikemen-go/game-coordinator.md) rather than a resolved address.
52
+ // match-agent's GameLauncher.startGame is what turns this into a
53
+ // ConnectionConfig below before a plugin ever sees it - no plugin talks to a
54
+ // coordinator itself.
55
+ export type ConnectionSetup = (
56
+ | { type: "direct-tcp", party: "host", port: number, coordinator: { host: string, port: number } }
57
+ | { type: "direct-tcp", party: "client", port: number, coordinator: { host: string, port: number } }
58
+ | RoomOrInternalConnection
59
+ );
60
+
61
+ // What a GameLauncherPlugin's startGame actually receives - direct-tcp is
62
+ // already resolved by this point (see ConnectionSetup above), so a plugin
63
+ // only ever deals with an address to dial, never a coordinator of its own.
64
+ export type ConnectionConfig = (
65
+ | { type: "direct-tcp", party: "host", port: number }
66
+ // hostIp is what a coordinator (or whatever else resolved this) reported
67
+ // - the same value startGame would otherwise have had to go discover
68
+ // itself.
69
+ | { type: "direct-tcp", party: "client", port: number, hostIp: string }
70
+ | RoomOrInternalConnection
71
+ );
72
+
73
+ export type GameProcessHandle = {
74
+ // Fires when the process this plugin is directly watching exits - not
75
+ // necessarily the same as "the game is over". Some games are started via
76
+ // an intermediary (a platform launcher, a wrapper script) that spawns the
77
+ // real game and exits 0 once it's handed off; for those, onExit only means
78
+ // the hand-off succeeded, not that play has ended - the plugin may have no
79
+ // handle to the actual game process at all. A plugin watching the real
80
+ // game process directly (e.g. spawning the engine binary itself, like
81
+ // ikemen-go does) can report both meaningfully; one that can't should say
82
+ // so in its publicInfo rather than let callers assume more than it can
83
+ // promise.
84
+ exited: false | { code: number },
85
+ onExit: (cb: (code: number | null) => void) => void,
86
+ onCrash: (cb: (error: Error) => void) => void,
87
+ // Best-effort - may not actually be able to stop the game if it's already
88
+ // handed off to a process this plugin no longer has a handle to.
89
+ stop: () => Promise<void>,
90
+ };
91
+
92
+ export type StartGameArgs<T> = {
93
+ // Handed to the game coordinator on the relay room's success webhook -
94
+ // lets a plugin correlate itself back to that room if it ever needs to.
95
+ relayRoomId: string,
96
+ currentMachine: {
97
+ machineId: string,
98
+ publicKey: string,
99
+ // A temp file - written with restrictive/owner-only permissions before
100
+ // startGame is invoked, deleted once the returned GameProcessHandle
101
+ // reports exit/crash - containing this machine's private key, for
102
+ // plugins that need to sign messages to identify the user to a game
103
+ // coordinator. Most games (Ikemen included) have no identity concept in
104
+ // their own netcode and never touch this.
105
+ //
106
+ // Handled as a file the framework manages (see
107
+ // @roster-lock/plugin-runtime's GameLauncher) rather than handing plugins
108
+ // the raw key directly, so a plugin is safe by default even if it does
109
+ // something careless with it - e.g. forwarding it into the spawned
110
+ // process's argv/env would otherwise leak to other users via `ps`/`/proc`
111
+ // on a shared machine (see docs/v2/test-environments.md's Internet Cafe
112
+ // / Arcade targets). A plugin that does need to hand the game itself an
113
+ // identity can still pass this same file path through unchanged.
114
+ privateKeyFile: string,
115
+ },
116
+ allMachines: Array<RoomMachine>,
117
+ selectionResult: RosterLockV1SyncDLResult,
118
+ rosterConfig: RosterLockV1Config,
119
+ // Lets a plugin without direct filesystem access to the downloaded pieces
120
+ // (selectionResult.downloadResults[...].folder assumes it's running on the
121
+ // same machine as match-agent) pull file contents over HTTP instead.
122
+ matchAgent: { port: number, authCode: string },
123
+ // Room-shared settings every participant agreed to at room-creation time
124
+ // (e.g. team mode, round time). Validated against gameConfigSchema.
125
+ gameConfig: T,
126
+ };
127
+
128
+ export type GameLauncherPlugin<T> = {
129
+ name: string,
130
+ publicInfo: {
131
+ title: string,
132
+ description: string,
133
+ },
134
+ // Which ConnectionConfig["type"] values this plugin actually knows how to
135
+ // consume in startGame - not just "would make sense for", since a mode
136
+ // like "room" typically depends on unbuilt bridge infrastructure. Only
137
+ // declare a mode once startGame genuinely handles it.
138
+ supportedConnectionModes: Array<ConnectionMode>,
139
+ // Which ConnectionConfig["room"]["version"] values this plugin supports.
140
+ // Only meaningful if "room" is in supportedConnectionModes.
141
+ supportedRoomVersions?: Array<string>,
142
+ // Identifies which RosterLockEngineConfig["pieceDefinitions"] shape this
143
+ // plugin was built against - a hash of that shape alone (not engine.name,
144
+ // engine.version, or engine.officialSelections, which are roster-author
145
+ // choices, not part of what the plugin structurally needs). Before a room
146
+ // can be created, this is compared against the same hash computed over the
147
+ // roster config's own engine.pieceDefinitions ("Engine Sha" in
148
+ // docs/v2/ikemen-go/general-plan.md) - a mismatch means the plugin's
149
+ // assumptions about piece layout (e.g. ikemen-go's defName path variable)
150
+ // don't hold for that roster.
151
+ engineSha: Sha256,
152
+ // JSON Schema for StartGameArgs["gameConfig"]. Set even when empty ({}).
153
+ gameConfigSchema: JSONSchemaType<T>,
154
+ // JSON Schema for this plugin's other per-machine settings (e.g. a
155
+ // preferred direct-tcp port). Excludes binaryLocation, which every Game
156
+ // Runner has by definition - see the functions below. Set even when empty.
157
+ localConfigSchema: AnySchema,
158
+ // Every platform/arch this plugin knows how to run on at all, independent
159
+ // of whether any particular binaryLocation currently has a binary for one
160
+ // of them - see docs/v2/binary-location.md. Lets a browsing/install UI
161
+ // warn "not supported on macOS" before the user has configured a
162
+ // binaryLocation to check against.
163
+ supportedPlatforms: Array<PlatformTarget>,
164
+
165
+ // binaryLocation is resolved locally (e.g. from match-agent's own config)
166
+ // and is what "disabled until configured" is about - every function below
167
+ // needs it pointed at something real to do anything useful. As of
168
+ // docs/v2/binary-location.md it's the root of a multi-platform bundle, not
169
+ // a path to one executable - each function below that needs a concrete
170
+ // binary resolves `target` against it itself; the layout convention for
171
+ // doing that is the plugin's own to define.
172
+ //
173
+ // `target` is required everywhere a concrete binary has to be resolved,
174
+ // rather than any function defaulting to "the current host" implicitly -
175
+ // see docs/v2/binary-location.md's "Platform targeting per function" for
176
+ // why (getSupportedVersion is the one exception, since it's a
177
+ // platform-agnostic upstream-metadata query).
178
+ getLocalVersion: (binaryLocation: string, target: PlatformTarget) => Promise<{ title: string, id: string }>,
179
+ // Knowing binaryLocation can help (e.g. picking a release channel), but
180
+ // this is expected to mostly be a network call, not a local read. No
181
+ // target - the answer ("what's the latest upstream version") doesn't vary
182
+ // by platform.
183
+ getSupportedVersion: (binaryLocation: string) => Promise<{ title: string, id: string }>,
184
+ // Optional - a plugin without one just means no in-app update; the user
185
+ // downloads a new version and re-points binaryLocation at it themselves.
186
+ // `target` is a floor, not a ceiling: the call guarantees at least that
187
+ // platform/arch gets updated, but a plugin may update other slots too as
188
+ // a side effect (e.g. one upstream artifact that already bundles every
189
+ // platform). Callers never loop over supportedPlatforms to force full
190
+ // coverage themselves - whether the bundle ends up complete for every
191
+ // platform after one call is entirely the plugin's own choice.
192
+ updateBinary?: (binaryLocation: string, target: PlatformTarget) => Promise<void>,
193
+ // Checks whether binaryLocation actually has a usable binary for `target`
194
+ // - "does the binary this host would run actually exist at the resolved
195
+ // path" (and on POSIX, is it executable) - so a caller can surface a
196
+ // plugin-authored reason (e.g. "no linux-x64 build in this folder")
197
+ // instead of a raw ENOENT/spawn error from startGame. Takes the same
198
+ // target startGame will actually receive, since that's the call it's
199
+ // answering for.
200
+ validateBinaryLocation: (
201
+ binaryLocation: string, target: PlatformTarget
202
+ ) => Promise<{ valid: true } | { valid: false, message: string }>,
203
+
204
+ startGame: (
205
+ binaryLocation: string, target: PlatformTarget, connectionConfig: ConnectionConfig, args: StartGameArgs<T>
206
+ ) => Promise<GameProcessHandle>,
207
+ };
208
+
209
+ // GameLauncherPlugin<T> is for *implementing* a plugin, where T (the plugin's
210
+ // own gameConfig shape) is known and worth enforcing at both
211
+ // gameConfigSchema and startGame. A caller that's just routing to *some*
212
+ // installed plugin by name (match-agent, plugin-runtime's GameLauncher) never
213
+ // knows T - it received the plugin dynamically, and gameConfig is exactly
214
+ // what gameConfigSchema exists to describe/validate at runtime instead.
215
+ //
216
+ // GameLauncherPlugin<any> alone doesn't work for that: T=any is right for the
217
+ // startGame/StartGameArgs positions (the usual erasure for storing/calling
218
+ // a value whose real generic argument varies per plugin instance - see
219
+ // PluginTypeMap["untrusted-script"]'s UntrustedScript<any> for the same
220
+ // pattern elsewhere), but it also leaks into gameConfigSchema, turning it
221
+ // into JSONSchemaType<any> - not the same thing as "some JSON Schema", and
222
+ // not what a caller that never touches T actually wants. localConfigSchema
223
+ // (AnySchema, no generic) is already the right shape for "opaque schema a
224
+ // caller passes through" - gameConfigSchema needs that same shape once T
225
+ // isn't known, hence the override below rather than just GameLauncherPlugin<any>.
226
+ export type AnyGameLauncherPlugin = Omit<GameLauncherPlugin<any>, "gameConfigSchema"> & {
227
+ gameConfigSchema: AnySchema,
228
+ };
@@ -2,3 +2,5 @@
2
2
  export * from "./download";
3
3
  export * from "./unstrusted-script";
4
4
  export * from "./piece-selection-sort";
5
+ export * from "./game-launcher";
6
+ export * from "./matchmaker-bridge";
@@ -0,0 +1,86 @@
1
+ import { RosterLockV1Config } from "../../lock";
2
+ import { UserSelection } from "../../request";
3
+
4
+ // The wire protocol between a matchmaker UI (loaded in an <iframe>, e.g.
5
+ // examples/services/match-makers/titled-room/client) and its host shell (e.g.
6
+ // core/match-agent/client), carried over a postMessage-backed MessageBridge
7
+ // (@roster-lock/utils). All six calls are iframe -> host; the host never
8
+ // initiates. MessageBridge itself is untyped (see its own docs) - these
9
+ // types exist purely so both sides' thin bridge wrapper modules agree on
10
+ // shape, not because MessageBridge enforces it.
11
+ //
12
+ // Deliberately excludes any raw-signing/private-key primitive - titled-room
13
+ // (the first matchmaker built against this protocol) uses JWT/bearer auth,
14
+ // not signatures, so nothing needs one yet. A future signature-auth
15
+ // matchmaker would need a new request added here, not a workaround.
16
+
17
+ // installGameLauncherPlugin(pluginName) - host installs the plugin package and
18
+ // opens a lightbox that also lets the user finish local config
19
+ // (binaryLocation) before resolving.
20
+ export type InstallGameLauncherPluginRequest = { pluginName: string };
21
+ export type InstallGameLauncherPluginResponse = {};
22
+
23
+ // getInstalledGameLauncherPlugins() - what game-launcher plugins this machine
24
+ // already has installed, so a matchmaker's room UI can tell a user "you
25
+ // don't have this one yet" without ever touching match-agent itself.
26
+ // gameConfigSchema is included so a matchmaker can render the room-shared
27
+ // settings a game launcher needs (e.g. ikemen-go's teamMode/roundTime/rounds)
28
+ // as a real form (e.g. via rjsf) instead of asking the user for raw JSON -
29
+ // it's already public (GameLauncherPlugin.gameConfigSchema, exposed by match-
30
+ // agent's own /v1/game-launcher/available route), just not previously carried
31
+ // across this bridge. Left as `unknown` here rather than ajv's AnySchema,
32
+ // matching how match-agent-client's own GameLauncherSettingsForm already treats
33
+ // localConfigSchema at this same kind of boundary - a consumer narrows it
34
+ // (e.g. an RJSFSchema type guard) at render time instead.
35
+ export type GetInstalledGameLauncherPluginsResponse = Array<{ id: string, version: string, gameConfigSchema: unknown }>;
36
+
37
+ // getIdentity() - never includes the private key, only what's safe for a
38
+ // matchmaker to see. playerCount reflects the host's own local player-slot
39
+ // count (Join Settings), not anything the guest can set.
40
+ export type GetIdentityResponse = { publicKey: string, machineId: string, playerCount: number };
41
+
42
+ // requestSelection(rosterLockConfig, numPlayers) - host opens the Selection
43
+ // lightbox for exactly `numPlayers` local player slots and resolves with the
44
+ // built selection once confirmed. Unlike the other calls, the selection
45
+ // *content* does cross back to the guest here - it isn't secret, only the
46
+ // private key is.
47
+ export type RequestSelectionRequest = { rosterLockConfig: RosterLockV1Config, numPlayers: number };
48
+ export type RequestSelectionResponse = Record<number, UserSelection>;
49
+
50
+ // updateGameLauncherSettings(pluginName, gameConfig) - room-shared settings a
51
+ // game launcher needs before it can start (e.g. ikemen-go's teamMode). The
52
+ // host holds onto this and folds it into the eventual
53
+ // /v1/game-launcher/:pluginName/start call it makes later, since the host
54
+ // (not the guest) is the one that ends up calling that route.
55
+ export type UpdateGameLauncherSettingsRequest = { pluginName: string, gameConfig: unknown };
56
+ export type UpdateGameLauncherSettingsResponse = {};
57
+
58
+ // initiateRelay(...) - fire-and-forget event, not a request: the guest is
59
+ // about to be torn down (host navigates away from the iframe entirely), so
60
+ // there's nothing useful to await. Host merges this with whatever it already
61
+ // has from requestSelection/updateGameLauncherSettings and navigates to
62
+ // wherever it runs its own download/start-game flow.
63
+ export type InitiateRelayEvent = {
64
+ relay: { url: string, roomId: string },
65
+ rosterConfig: RosterLockV1Config,
66
+ gameLauncherPlugin: string,
67
+ isHost: boolean,
68
+ // A direct-tcp game launcher's rendezvous coordinator (see
69
+ // plugins/game-launcher/shared/direct-ip-coordinator and
70
+ // docs/v2/ikemen-go/game-coordinator.md) - null for game launchers that
71
+ // don't use one (a matchmaker resolves this the same way it resolves
72
+ // `relay`, e.g. titled-room's /room/start response).
73
+ coordinator: { host: string, port: number } | null,
74
+ };
75
+
76
+ // Bridge message/event path names, so both sides reference the same literal
77
+ // strings instead of hand-typing them.
78
+ export const MATCHMAKER_BRIDGE_PATHS = {
79
+ installGameLauncherPlugin: "installGameLauncherPlugin",
80
+ getInstalledGameLauncherPlugins: "getInstalledGameLauncherPlugins",
81
+ getIdentity: "getIdentity",
82
+ requestSelection: "requestSelection",
83
+ updateGameLauncherSettings: "updateGameLauncherSettings",
84
+ initiateRelay: "initiateRelay",
85
+ ready: "ready",
86
+ } as const;