@diffenderfer-games/hub-contract 2.0.0-next.0 → 2.0.0-next.2

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 (81) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -21
  3. package/README.md +26 -26
  4. package/dist/index.d.ts +2683 -39
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +862 -18
  7. package/dist/index.js.map +1 -1
  8. package/package.json +53 -53
  9. package/src/api/analytics.ts +81 -81
  10. package/src/api/auth.ts +77 -77
  11. package/src/api/catalog-activity.ts +66 -0
  12. package/src/api/catalog.ts +177 -171
  13. package/src/api/daily.ts +122 -122
  14. package/src/api/economy/bundle.ts +69 -69
  15. package/src/api/index.ts +92 -85
  16. package/src/api/leaderboards.ts +151 -151
  17. package/src/api/messages.ts +142 -142
  18. package/src/api/mp/chat.ts +36 -36
  19. package/src/api/mp/invite-links.ts +42 -0
  20. package/src/api/mp/parties.ts +5 -0
  21. package/src/api/mp/races.ts +211 -211
  22. package/src/api/mp/rooms.ts +122 -90
  23. package/src/api/mp/snapshot.ts +17 -17
  24. package/src/api/mp.ts +135 -110
  25. package/src/api/notify.ts +114 -111
  26. package/src/api/offline.ts +45 -0
  27. package/src/api/progress/game-data.ts +189 -189
  28. package/src/api/progress.ts +59 -59
  29. package/src/api/race.ts +47 -47
  30. package/src/api/reports.ts +208 -208
  31. package/src/api/social/config.ts +47 -47
  32. package/src/api/social/endpoints.ts +179 -179
  33. package/src/api/social/people.ts +118 -118
  34. package/src/api/social/presence.ts +95 -95
  35. package/src/api/social.ts +9 -9
  36. package/src/api/tournaments/endpoints.ts +102 -0
  37. package/src/api/tournaments/play-online.ts +71 -0
  38. package/src/api/tournaments/shapes.ts +202 -0
  39. package/src/api/tournaments.ts +7 -0
  40. package/src/api/version.ts +40 -40
  41. package/src/coerce.ts +50 -50
  42. package/src/common.ts +42 -42
  43. package/src/entities.ts +62 -62
  44. package/src/errors/codes/core.ts +10 -10
  45. package/src/errors/codes/multiplayer.ts +56 -43
  46. package/src/errors/codes/play.ts +5 -5
  47. package/src/errors/codes/social.ts +31 -31
  48. package/src/errors/index.ts +88 -88
  49. package/src/index.ts +16 -16
  50. package/src/limits.ts +125 -100
  51. package/src/logic/daily.ts +54 -54
  52. package/src/logic/index.ts +8 -8
  53. package/src/logic/presence.ts +62 -62
  54. package/src/logic/time.ts +81 -81
  55. package/src/manifest/changes.ts +55 -55
  56. package/src/manifest/controls.ts +42 -42
  57. package/src/manifest/fields.ts +89 -89
  58. package/src/manifest/game.ts +139 -132
  59. package/src/manifest/index.ts +13 -12
  60. package/src/manifest/inject.ts +31 -31
  61. package/src/manifest/labels.ts +99 -0
  62. package/src/manifest/leaderboards.ts +29 -29
  63. package/src/manifest/multiplayer.ts +79 -71
  64. package/src/manifest/race.ts +93 -93
  65. package/src/middleware.ts +47 -47
  66. package/src/realtime/define.ts +52 -52
  67. package/src/realtime/envelope.ts +67 -67
  68. package/src/realtime/events/multiplayer.ts +9 -8
  69. package/src/realtime/events/parties.ts +31 -31
  70. package/src/realtime/events/races.ts +27 -27
  71. package/src/realtime/events/rooms.ts +52 -48
  72. package/src/realtime/events/social.ts +81 -81
  73. package/src/realtime/events/tournaments.ts +12 -0
  74. package/src/realtime/index.ts +24 -24
  75. package/src/realtime/ops/empty.ts +8 -8
  76. package/src/realtime/ops/hello.ts +40 -40
  77. package/src/realtime/ops/multiplayer.ts +8 -8
  78. package/src/realtime/ops/parties.ts +13 -13
  79. package/src/realtime/ops/races.ts +99 -99
  80. package/src/realtime/ops/rooms.ts +22 -8
  81. package/src/realtime/ops/social.ts +58 -58
@@ -1,90 +1,122 @@
1
- /**
2
- * Hub relay rooms (multiplayer-impl.md §6): realtime multiplayer for static
3
- * games, relayed (never refereed) by the hub. A race is a room with a
4
- * `race` block on top. Shared by the `room.*` / `race.*` ops and events and
5
- * the `hello` snapshot.
6
- */
7
- import { z } from 'zod';
8
- import { Json, Slug, UserId } from '../../common';
9
- import { ChatMode } from '../../entities';
10
- import { GameMode, OpaqueId } from './parties';
11
- import { RaceState } from './races';
12
-
13
- /** A room's join code: 5 characters from `A-Z` and `2-9` (no `I`, `O`, `0`, `1`). */
14
- export const RoomCode = z.string().regex(/^[A-HJ-NP-Z2-9]{5}$/);
15
-
16
- /** A room's lifecycle: new players join only in the lobby. */
17
- export const RoomPhase = z.enum(['lobby', 'playing', 'ended']);
18
-
19
- /**
20
- * Who hears a send. `host` (default): guests' unaddressed sends reach the
21
- * host only. `relay`: every unaddressed send reaches everyone, sender
22
- * included.
23
- */
24
- export const RoomModel = z.enum(['host', 'relay']);
25
-
26
- /** A seat in a room, keyed by user (any tab of that user reclaims it). */
27
- export const RoomSeat = z.object({
28
- userId: UserId,
29
- /** The player's username when they sat down. */
30
- name: z.string(),
31
- /** Avatar token; never empty. */
32
- avatar: z.string(),
33
- ready: z.boolean(),
34
- /** `false` while the seat is held for a dropped player's reconnect grace. */
35
- connected: z.boolean(),
36
- /** Small per-seat game data (team, colour…), at most 2 KB of JSON. */
37
- data: Json.optional(),
38
- });
39
-
40
- /** The public description of a room (`room` event, room and race acks, the `hello` snapshot). */
41
- export const RoomInfo = z.object({
42
- id: OpaqueId,
43
- code: RoomCode,
44
- game: Slug,
45
- /** `race` for race rooms. */
46
- mode: GameMode.optional(),
47
- hostId: UserId,
48
- /** Listed in `room.list` / `race.list`. */
49
- public: z.boolean(),
50
- phase: RoomPhase,
51
- maxPlayers: z.number().int().positive(),
52
- model: RoomModel,
53
- seats: z.array(RoomSeat),
54
- /** How many are watching. */
55
- spectators: z.number().int().nonnegative(),
56
- /** The chat mode for the seated players. */
57
- chatMode: ChatMode,
58
- /** Present on race rooms. */
59
- race: RaceState.optional(),
60
- });
61
-
62
- /** A room. */
63
- export type RoomInfo = z.infer<typeof RoomInfo>;
64
-
65
- /** A room's shared state for late joiners: a JSON object, at most 32 KB. */
66
- export const RoomState = z.record(z.string(), Json);
67
-
68
- /** A relayed game message (`room.msg`, and a join's replay history). */
69
- export const RoomMessage = z.object({
70
- from: UserId,
71
- data: Json,
72
- /** Per-room sequence number, shared with state patches. */
73
- seq: z.number().int().positive(),
74
- /** Set when the message went to one seat only. */
75
- to: UserId.optional(),
76
- });
77
-
78
- /** How to find a room: by its code (case-insensitive) or its id. */
79
- export const RoomTarget = z.object({
80
- code: z.string().optional(),
81
- id: z.string().optional(),
82
- /** Watch instead of taking a seat (if the game allows spectators). */
83
- spectate: z.boolean().optional(),
84
- });
85
-
86
- /** Why a room went away for this tab (`room.closed`). */
87
- export const RoomClosedReason = z.enum(['empty', 'ended', 'host_left', 'moved', 'other_tab', 'removed']);
88
-
89
- /** Why a seat was freed (`room.abandoned`). */
90
- export const SeatGoneReason = z.enum(['left', 'abandoned', 'kicked']);
1
+ /**
2
+ * Hub relay rooms (multiplayer-impl.md §6): realtime multiplayer for static
3
+ * games, relayed (never refereed) by the hub. A race is a room with a
4
+ * `race` block on top. Shared by the `room.*` / `race.*` ops and events and
5
+ * the `hello` snapshot.
6
+ */
7
+ import { z } from 'zod';
8
+ import { Json, Millis, Slug, UserId } from '../../common';
9
+ import { ChatMode } from '../../entities';
10
+ import { GameMode, OpaqueId } from './parties';
11
+ import { RaceState } from './races';
12
+
13
+ /** A room's join code: 5 characters from `A-Z` and `2-9` (no `I`, `O`, `0`, `1`). */
14
+ export const RoomCode = z.string().regex(/^[A-HJ-NP-Z2-9]{5}$/);
15
+
16
+ /** A room's lifecycle: new players join only in the lobby. */
17
+ export const RoomPhase = z.enum(['lobby', 'playing', 'ended']);
18
+
19
+ /**
20
+ * Who hears a send. `host` (default): guests' unaddressed sends reach the
21
+ * host only. `relay`: every unaddressed send reaches everyone, sender
22
+ * included.
23
+ */
24
+ export const RoomModel = z.enum(['host', 'relay']);
25
+
26
+ /**
27
+ * When the host role moves on mid-game. `grace` (default): only once the
28
+ * host's seat is freed (they left, or their reconnect grace ran out), so a
29
+ * host whose browser holds hidden state can come back and carry on. `drop`:
30
+ * as soon as the host drops, for games whose whole truth is in the shared
31
+ * state, so the match never waits.
32
+ */
33
+ export const HostMigration = z.enum(['grace', 'drop']);
34
+
35
+ /** A seat in a room, keyed by user (any tab of that user reclaims it). */
36
+ export const RoomSeat = z.object({
37
+ userId: UserId,
38
+ /** The player's username when they sat down. */
39
+ name: z.string(),
40
+ /** Avatar token; never empty. */
41
+ avatar: z.string(),
42
+ ready: z.boolean(),
43
+ /** `false` while the seat is held for a dropped player's reconnect grace. */
44
+ connected: z.boolean(),
45
+ /** While the seat is held (`connected: false`): when the grace runs out and the seat is freed (epoch ms). */
46
+ graceEndsAt: Millis.optional(),
47
+ /** Small per-seat game data (team, colour…), at most 2 KB of JSON. */
48
+ data: Json.optional(),
49
+ });
50
+
51
+ /** The public description of a room (`room` event, room and race acks, the `hello` snapshot). */
52
+ export const RoomInfo = z.object({
53
+ id: OpaqueId,
54
+ code: RoomCode,
55
+ game: Slug,
56
+ /** `race` for race rooms. */
57
+ mode: GameMode.optional(),
58
+ hostId: UserId,
59
+ /** Listed in `room.list` / `race.list`. */
60
+ public: z.boolean(),
61
+ phase: RoomPhase,
62
+ maxPlayers: z.number().int().positive(),
63
+ model: RoomModel,
64
+ seats: z.array(RoomSeat),
65
+ /** How many are watching. */
66
+ spectators: z.number().int().nonnegative(),
67
+ /** The chat mode for the seated players. */
68
+ chatMode: ChatMode,
69
+ /** Present on race rooms. */
70
+ race: RaceState.optional(),
71
+ });
72
+
73
+ /** A room. */
74
+ export type RoomInfo = z.infer<typeof RoomInfo>;
75
+
76
+ /** A room's shared state for late joiners: a JSON object, at most 32 KB. */
77
+ export const RoomState = z.record(z.string(), Json);
78
+
79
+ /** A relayed game message (`room.msg`, and a join's replay history). */
80
+ export const RoomMessage = z.object({
81
+ from: UserId,
82
+ data: Json,
83
+ /** Per-room sequence number, shared with state patches. */
84
+ seq: z.number().int().positive(),
85
+ /** Set when the message went to one seat only. */
86
+ to: UserId.optional(),
87
+ });
88
+
89
+ /** How to find a room: by its code (case-insensitive) or its id. */
90
+ export const RoomTarget = z.object({
91
+ code: z.string().optional(),
92
+ id: z.string().optional(),
93
+ /** Watch instead of taking a seat (if the game allows spectators). */
94
+ spectate: z.boolean().optional(),
95
+ });
96
+
97
+ /** Why a room went away for this tab (`room.closed`). */
98
+ export const RoomClosedReason = z.enum(['empty', 'ended', 'host_left', 'moved', 'other_tab', 'removed']);
99
+
100
+ /** Why a seat was freed (`room.abandoned`, `room.seat` `gone`). */
101
+ export const SeatGoneReason = z.enum(['left', 'abandoned', 'kicked']);
102
+
103
+ /**
104
+ * What happened to a seat (`room.seat`): `away` (the player dropped; the seat
105
+ * is held until `graceEndsAt`, so the game can hand it to its computer
106
+ * player), `back` (they reclaimed it: give control back), `gone` (freed for
107
+ * good: left, kicked, or the grace ran out).
108
+ */
109
+ export const SeatStatus = z.enum(['away', 'back', 'gone']);
110
+
111
+ /** A seat change (`room.seat`). */
112
+ export const SeatChange = z.object({
113
+ userId: UserId,
114
+ status: SeatStatus,
115
+ /** `away`: when the held seat is freed (epoch ms). */
116
+ graceEndsAt: Millis.optional(),
117
+ /** `gone`: why. */
118
+ reason: SeatGoneReason.optional(),
119
+ });
120
+
121
+ /** A seat change. */
122
+ export type SeatChange = z.infer<typeof SeatChange>;
@@ -1,17 +1,17 @@
1
- /** The multiplayer part of the socket `hello` snapshot (multiplayer-impl.md §4.5). */
2
- import { z } from 'zod';
3
- import { Invite, Party } from './parties';
4
- import { RoomInfo } from './rooms';
5
-
6
- /**
7
- * The multiplayer fields of the `hello` ack, to spread into the snapshot
8
- * schema (`z.object({ ...other, ...MultiplayerSnapshot.shape })`).
9
- */
10
- export const MultiplayerSnapshot = z.object({
11
- /** Pending invites addressed to me. */
12
- invites: z.array(Invite),
13
- /** The party I'm in, or `null`. */
14
- party: Party.nullable(),
15
- /** The room seat I still hold (not ended), or `null`; `room.join` it again to reconnect. */
16
- room: RoomInfo.nullable(),
17
- });
1
+ /** The multiplayer part of the socket `hello` snapshot (multiplayer-impl.md §4.5). */
2
+ import { z } from 'zod';
3
+ import { Invite, Party } from './parties';
4
+ import { RoomInfo } from './rooms';
5
+
6
+ /**
7
+ * The multiplayer fields of the `hello` ack, to spread into the snapshot
8
+ * schema (`z.object({ ...other, ...MultiplayerSnapshot.shape })`).
9
+ */
10
+ export const MultiplayerSnapshot = z.object({
11
+ /** Pending invites addressed to me. */
12
+ invites: z.array(Invite),
13
+ /** The party I'm in, or `null`. */
14
+ party: Party.nullable(),
15
+ /** The room seat I still hold (not ended), or `null`; `room.join` it again to reconnect. */
16
+ room: RoomInfo.nullable(),
17
+ });
package/src/api/mp.ts CHANGED
@@ -1,110 +1,135 @@
1
- /**
2
- * Multiplayer REST (multiplayer-impl.md §5.6): invites, launch tokens,
3
- * parties, joining a friend's game, and game-server tickets. Every route
4
- * needs an active account; invites need a claimed one. The live state is in
5
- * the hub's memory and changes are pushed over `/_ws` (`invite`,
6
- * `invite.update`, `party`).
7
- */
8
- import { z } from 'zod';
9
- import { Millis, Ok, Slug, UserIdParam } from '../common';
10
- import { ErrorBody } from '../errors';
11
- import { requireActive, requireClaimed } from '../middleware';
12
- import { GameMode, Invite, JoinInfo, Launch, LaunchTicket, OpaqueId } from './mp/parties';
13
-
14
- export * from './mp/chat';
15
- export * from './mp/parties';
16
- export * from './mp/races';
17
- export * from './mp/rooms';
18
- export * from './mp/snapshot';
19
-
20
- /** `:id` of an invite or party. */
21
- const IdParams = z.object({ id: OpaqueId });
22
-
23
- /** The answer to `POST /mp/invites`: the invite, plus the inviter's own `host` launch. */
24
- export const InviteCreated = LaunchTicket.extend({ invite: Invite });
25
-
26
- /** A game-server identity ticket. */
27
- export const GameTicket = z.object({
28
- /** `base64url(payload).base64url(hmac)`, verified offline by the game's server with `HUB_APP_KEY`. */
29
- ticket: z.string(),
30
- /** Epoch ms; tickets live 60 s. */
31
- expiresAt: Millis,
32
- });
33
-
34
- /** The mp endpoints. */
35
- export const mpEndpoints = {
36
- ...requireClaimed.group({
37
- sendGameInvite: {
38
- method: 'POST', path: '/mp/invites',
39
- body: z.object({
40
- /** The friend to invite. */
41
- to: UserIdParam,
42
- game: Slug,
43
- /** One of the game's modes; race-only games default to `race`. */
44
- mode: GameMode.nullish(),
45
- }),
46
- response: InviteCreated,
47
- errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody, 429: ErrorBody },
48
- doc: {
49
- summary: 'Invite a friend to a game; creates or reuses your party and returns your host launch',
50
- tags: ['mp'],
51
- },
52
- },
53
- acceptGameInvite: {
54
- method: 'POST', path: '/mp/invites/:id/accept', params: IdParams, response: LaunchTicket,
55
- errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody },
56
- doc: { summary: "Accept an invite: join the inviter's party and get a guest launch", tags: ['mp'] },
57
- },
58
- declineGameInvite: {
59
- method: 'POST', path: '/mp/invites/:id/decline', params: IdParams, response: Ok,
60
- errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
61
- doc: { summary: 'Decline an invite addressed to you', tags: ['mp'] },
62
- },
63
- cancelGameInvite: {
64
- method: 'DELETE', path: '/mp/invites/:id', params: IdParams, response: Ok,
65
- errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
66
- doc: { summary: 'Cancel an invite you sent', tags: ['mp'] },
67
- },
68
- }),
69
- ...requireActive.group({
70
- redeemLaunchToken: {
71
- method: 'GET', path: '/mp/launch/:token', params: z.object({ token: z.string() }),
72
- response: z.object({ launch: Launch }),
73
- errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
74
- doc: { summary: 'Redeem a launch token (single use, bound to the signed-in user)', tags: ['mp'] },
75
- },
76
- setPartyJoinInfo: {
77
- method: 'POST', path: '/mp/parties/:id/join-info', params: IdParams,
78
- body: z.object({ info: JoinInfo }),
79
- response: Ok,
80
- errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
81
- doc: { summary: 'Party leader: publish where the party should connect', tags: ['mp'] },
82
- },
83
- leaveParty: {
84
- method: 'POST', path: '/mp/parties/:id/leave', params: IdParams, response: Ok,
85
- errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
86
- doc: { summary: 'Leave a party (the leader role passes on; an empty party dissolves)', tags: ['mp'] },
87
- },
88
- launchIntoFriendsRoom: {
89
- method: 'POST', path: '/mp/join',
90
- body: z.object({
91
- /** The player whose game to join or watch. */
92
- userId: UserIdParam,
93
- /** Watch instead of play. */
94
- watch: z.boolean().optional(),
95
- /** Joining a stranger's public lobby from a `lobby_open` alert (not a friend's game). */
96
- fromLobbyOpen: z.boolean().optional(),
97
- }),
98
- response: LaunchTicket,
99
- errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody, 429: ErrorBody },
100
- doc: { summary: "A Join or Watch launch into the room a friend's presence advertises", tags: ['mp'] },
101
- },
102
- getGameServerTicket: {
103
- method: 'POST', path: '/mp/ticket',
104
- body: z.object({ slug: Slug }),
105
- response: GameTicket,
106
- errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 429: ErrorBody },
107
- doc: { summary: "A 60 s identity ticket for a game's own server", tags: ['mp'] },
108
- },
109
- }),
110
- };
1
+ /**
2
+ * Multiplayer REST (multiplayer-impl.md §5.6): invites, launch tokens,
3
+ * parties, joining a friend's game, and game-server tickets. Every route
4
+ * needs an active account; invites need a claimed one. The live state is in
5
+ * the hub's memory and changes are pushed over `/_ws` (`invite`,
6
+ * `invite.update`, `party`).
7
+ */
8
+ import { z } from 'zod';
9
+ import { Millis, Ok, Slug, UserIdParam } from '../common';
10
+ import { ErrorBody } from '../errors';
11
+ import { requireActive, requireClaimed } from '../middleware';
12
+ import { InviteLink, InviteLinkCreate, InviteLinkParams, InviteLinkQuery } from './mp/invite-links';
13
+ import { GameMode, Invite, JoinInfo, Launch, LaunchTicket, OpaqueId } from './mp/parties';
14
+
15
+ export * from './mp/chat';
16
+ export * from './mp/invite-links';
17
+ export * from './mp/parties';
18
+ export * from './mp/races';
19
+ export * from './mp/rooms';
20
+ export * from './mp/snapshot';
21
+
22
+ /** `:id` of an invite or party. */
23
+ const IdParams = z.object({ id: OpaqueId });
24
+
25
+ /** The answer to `POST /mp/invites`: the invite, plus the inviter's own `host` launch. */
26
+ export const InviteCreated = LaunchTicket.extend({ invite: Invite });
27
+
28
+ /** A game-server identity ticket. */
29
+ export const GameTicket = z.object({
30
+ /** `base64url(payload).base64url(hmac)`, verified offline by the game's server with `HUB_APP_KEY`. */
31
+ ticket: z.string(),
32
+ /** Epoch ms; tickets live 60 s. */
33
+ expiresAt: Millis,
34
+ });
35
+
36
+ /** The mp endpoints. */
37
+ export const mpEndpoints = {
38
+ ...requireClaimed.group({
39
+ sendGameInvite: {
40
+ method: 'POST', path: '/mp/invites',
41
+ body: z.object({
42
+ /** The friend to invite. */
43
+ to: UserIdParam,
44
+ game: Slug,
45
+ /** One of the game's modes; race-only games default to `race`. */
46
+ mode: GameMode.nullish(),
47
+ }),
48
+ response: InviteCreated,
49
+ errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody, 429: ErrorBody },
50
+ doc: {
51
+ summary: 'Invite a friend to a game; creates or reuses your party and returns your host launch',
52
+ tags: ['mp'],
53
+ },
54
+ },
55
+ acceptGameInvite: {
56
+ method: 'POST', path: '/mp/invites/:id/accept', params: IdParams, response: LaunchTicket,
57
+ errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody },
58
+ doc: { summary: "Accept an invite: join the inviter's party and get a guest launch", tags: ['mp'] },
59
+ },
60
+ declineGameInvite: {
61
+ method: 'POST', path: '/mp/invites/:id/decline', params: IdParams, response: Ok,
62
+ errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
63
+ doc: { summary: 'Decline an invite addressed to you', tags: ['mp'] },
64
+ },
65
+ createInviteLink: {
66
+ method: 'POST', path: '/mp/links', body: InviteLinkCreate, response: InviteLink,
67
+ errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody, 429: ErrorBody },
68
+ doc: {
69
+ summary: "An invite link to the room I'm in (reused while it lives): anyone who opens it joins, or watches when it's full",
70
+ tags: ['mp'],
71
+ },
72
+ },
73
+ listInviteLinks: {
74
+ method: 'GET', path: '/mp/links', query: InviteLinkQuery, response: z.object({ links: z.array(InviteLink) }),
75
+ errors: { 401: ErrorBody, 403: ErrorBody },
76
+ doc: { summary: 'My live invite links in a game, plus the links to rooms I host', tags: ['mp'] },
77
+ },
78
+ revokeInviteLink: {
79
+ method: 'DELETE', path: '/mp/links/:token', params: InviteLinkParams, response: Ok,
80
+ errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
81
+ doc: { summary: "Revoke an invite link (its creator, or the host of the room it opens)", tags: ['mp'] },
82
+ },
83
+ cancelGameInvite: {
84
+ method: 'DELETE', path: '/mp/invites/:id', params: IdParams, response: Ok,
85
+ errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
86
+ doc: { summary: 'Cancel an invite you sent', tags: ['mp'] },
87
+ },
88
+ }),
89
+ ...requireActive.group({
90
+ redeemLaunchToken: {
91
+ method: 'GET', path: '/mp/launch/:token', params: z.object({ token: z.string() }),
92
+ response: z.object({ launch: Launch }),
93
+ errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
94
+ doc: { summary: 'Redeem a launch token (single use, bound to the signed-in user)', tags: ['mp'] },
95
+ },
96
+ openInviteLink: {
97
+ method: 'POST', path: '/mp/links/:token/open', params: InviteLinkParams, response: LaunchTicket,
98
+ errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody, 429: ErrorBody },
99
+ doc: { summary: 'Open an invite link: a join launch when a seat is free, else a watch launch', tags: ['mp'] },
100
+ },
101
+ setPartyJoinInfo: {
102
+ method: 'POST', path: '/mp/parties/:id/join-info', params: IdParams,
103
+ body: z.object({ info: JoinInfo }),
104
+ response: Ok,
105
+ errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
106
+ doc: { summary: 'Party leader: publish where the party should connect', tags: ['mp'] },
107
+ },
108
+ leaveParty: {
109
+ method: 'POST', path: '/mp/parties/:id/leave', params: IdParams, response: Ok,
110
+ errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody },
111
+ doc: { summary: 'Leave a party (the leader role passes on; an empty party dissolves)', tags: ['mp'] },
112
+ },
113
+ launchIntoFriendsRoom: {
114
+ method: 'POST', path: '/mp/join',
115
+ body: z.object({
116
+ /** The player whose game to join or watch. */
117
+ userId: UserIdParam,
118
+ /** Watch instead of play. */
119
+ watch: z.boolean().optional(),
120
+ /** Joining a stranger's public lobby from a `lobby_open` alert (not a friend's game). */
121
+ fromLobbyOpen: z.boolean().optional(),
122
+ }),
123
+ response: LaunchTicket,
124
+ errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody, 429: ErrorBody },
125
+ doc: { summary: "A Join or Watch launch into the room a friend's presence advertises", tags: ['mp'] },
126
+ },
127
+ getGameServerTicket: {
128
+ method: 'POST', path: '/mp/ticket',
129
+ body: z.object({ slug: Slug }),
130
+ response: GameTicket,
131
+ errors: { 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 429: ErrorBody },
132
+ doc: { summary: "A 60 s identity ticket for a game's own server", tags: ['mp'] },
133
+ },
134
+ }),
135
+ };