@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,189 +1,189 @@
1
- /**
2
- * The caller's own per-game data: profile, save slots, numeric stats, and
3
- * resetting a game. Guests count: a guest is a real account row.
4
- */
5
- import { use } from '@ayepi/core';
6
- import { z } from 'zod';
7
- import { Json, Key, Millis, Ok, Slug } from '../../common';
8
- import { ContentRejected, ErrorBody } from '../../errors';
9
- import { game, requireUser, session } from '../../middleware';
10
- import { LooseText, requiredText } from '../../coerce';
11
-
12
- /** A player's profile in one game. */
13
- export const Profile = z.object({
14
- game: Slug,
15
- /** Shown to other players; `null` when never set. */
16
- displayName: z.string().nullable(),
17
- /** Any JSON the game stores (`{}` by default). */
18
- data: Json,
19
- /** Epoch ms. */
20
- updatedAt: Millis,
21
- });
22
-
23
- /** A game profile. */
24
- export type Profile = z.infer<typeof Profile>;
25
-
26
- /** A save slot's metadata (no payload). */
27
- export const SaveMeta = z.object({
28
- slot: Key,
29
- label: z.string().nullable(),
30
- /** Epoch ms. */
31
- updatedAt: Millis,
32
- });
33
-
34
- /** A save slot's metadata. */
35
- export type SaveMeta = z.infer<typeof SaveMeta>;
36
-
37
- /** A save slot with its payload. */
38
- export const Save = SaveMeta.extend({
39
- /** The stored JSON (`null` if it could not be read back). */
40
- data: Json,
41
- });
42
-
43
- /** A save slot. */
44
- export type Save = z.infer<typeof Save>;
45
-
46
- /** A player's numeric stats in one game: stat {@link Key} to value. */
47
- export const Stats = z.record(Key, z.number());
48
-
49
- /** A player's stats. */
50
- export type Stats = z.infer<typeof Stats>;
51
-
52
- /** How a stat update combines with the stored value. */
53
- export const StatMode = z.enum(['set', 'incr', 'max']);
54
-
55
- /** A stat update mode. */
56
- export type StatMode = z.infer<typeof StatMode>;
57
-
58
- /**
59
- * A {@link Key} read like the hub's `validKey` (`String(x || '')`), failing
60
- * with its message: "<label> must match /^[A-Za-z0-9_.:-]{1,64}$/ (1–64 chars)."
61
- */
62
- function keyField(label: string) {
63
- const message = `${label} must match /^[A-Za-z0-9_.:-]{1,64}$/ (1–64 chars).`;
64
- return requiredText(message).refine((text) => Key.safeParse(text).success, message);
65
- }
66
-
67
- /** A number read like the hub's `finiteNumber` (`Number(x)`), failing with "<label> must be a finite number.". */
68
- function finiteNumberField(label: string) {
69
- return z.coerce.number<number>({ error: `${label} must be a finite number.` });
70
- }
71
-
72
- /**
73
- * A stat update's mode. The hub reads `body.mode || 'set'`, so a falsy value
74
- * (`null`, `''`, `0`, `false`) also means `set`. Clients should send a
75
- * {@link StatMode} or nothing.
76
- */
77
- const StatModeField = z
78
- .union([StatMode, z.literal(['', 0, false]), z.null()], { error: 'mode must be set, incr, or max.' })
79
- .optional()
80
- .transform((mode) => mode || 'set');
81
-
82
- /**
83
- * A save's payload: any JSON, but present. (The union with `never` exists only
84
- * so a missing value fails with the hub's message rather than zod's.)
85
- */
86
- const SaveData = z.union([Json, z.never()], { error: 'Missing "data".' });
87
-
88
- /** `:slot` of a save. */
89
- const SlotParams = z.object({ slot: keyField('slot') });
90
-
91
- /** The per-game data endpoints. */
92
- export const gameDataEndpoints = {
93
- ...use(session, game).path('/games/:slug').group({
94
- /**
95
- * A player's stats. Defaults to the caller's own (401 when signed out);
96
- * `?user=<id>` reads anyone's, since stats are public like a profile.
97
- */
98
- getGameStats: {
99
- method: 'GET', path: '/stats',
100
- query: z.object({
101
- /** Whose stats to read (a user id); omit for the caller's. */
102
- user: finiteNumberField('user').optional(),
103
- }),
104
- response: z.object({ stats: Stats }),
105
- errors: { 400: ErrorBody, 401: ErrorBody, 404: ErrorBody },
106
- doc: { summary: "A player's stats in this game", tags: ['progress'] },
107
- },
108
- }),
109
- ...use(requireUser, game).path('/games/:slug').group({
110
- getGameProfile: {
111
- method: 'GET', path: '/profile',
112
- response: z.object({ profile: Profile.nullable() }),
113
- errors: { 401: ErrorBody, 404: ErrorBody },
114
- doc: { summary: "The caller's profile in this game, or null", tags: ['progress'] },
115
- },
116
- /**
117
- * Create or replace the caller's profile. A new or changed display name
118
- * is moderated (422) because other players see it.
119
- */
120
- setGameProfile: {
121
- method: 'PUT', path: '/profile',
122
- body: z.object({
123
- /** Up to 60 characters (longer is clipped); `null` or missing clears it. */
124
- displayName: LooseText.nullish(),
125
- /** Replaces the stored data; missing stores `{}`. */
126
- data: Json.optional(),
127
- }),
128
- response: z.object({ profile: Profile }),
129
- errors: { 401: ErrorBody, 404: ErrorBody, 422: ContentRejected },
130
- doc: { summary: "Create or replace the caller's profile in this game", tags: ['progress'] },
131
- },
132
- listSaves: {
133
- method: 'GET', path: '/saves',
134
- response: z.object({ saves: z.array(SaveMeta) }),
135
- errors: { 401: ErrorBody, 404: ErrorBody },
136
- doc: { summary: "The caller's save slots, newest first (no payloads)", tags: ['progress'] },
137
- },
138
- getSave: {
139
- method: 'GET', path: '/saves/:slot', params: SlotParams,
140
- response: z.object({ save: Save.nullable() }),
141
- errors: { 400: ErrorBody, 401: ErrorBody, 404: ErrorBody },
142
- doc: { summary: 'One save slot, or null when empty', tags: ['progress'] },
143
- },
144
- writeSave: {
145
- method: 'PUT', path: '/saves/:slot', params: SlotParams,
146
- body: z.object({
147
- /** Any JSON up to 64 KB serialized (`HUB_MAX_SAVE_BYTES`); 413 over. */
148
- data: SaveData,
149
- /** Up to 80 characters (longer is clipped). */
150
- label: LooseText.nullish(),
151
- }),
152
- response: z.object({
153
- ok: z.literal(true),
154
- /** Epoch ms. */
155
- updatedAt: Millis,
156
- }),
157
- errors: { 400: ErrorBody, 401: ErrorBody, 404: ErrorBody, 413: ErrorBody },
158
- doc: { summary: 'Create or overwrite a save slot', tags: ['progress'] },
159
- },
160
- deleteSave: {
161
- method: 'DELETE', path: '/saves/:slot', params: SlotParams, response: Ok,
162
- errors: { 400: ErrorBody, 401: ErrorBody, 404: ErrorBody },
163
- doc: { summary: 'Delete a save slot (idempotent)', tags: ['progress'] },
164
- },
165
- /** Update one stat. Backs the client's `setStat`, `incrStat` and `maxStat`. */
166
- updateGameStat: {
167
- method: 'POST', path: '/stats',
168
- body: z.object({
169
- key: keyField('key'),
170
- value: finiteNumberField('value'),
171
- /** Default `set`. */
172
- mode: StatModeField,
173
- }),
174
- response: z.object({ key: Key, value: z.number() }),
175
- errors: { 400: ErrorBody, 401: ErrorBody, 404: ErrorBody },
176
- doc: { summary: 'Set, add to, or raise one stat; returns its new value', tags: ['progress'] },
177
- },
178
- /**
179
- * Wipe the caller's saves, stats, inventory and coins in this game.
180
- * Pending trades are closed first, refunding other players' escrow.
181
- * Leaderboard entries and the profile are kept.
182
- */
183
- resetMyGameProgress: {
184
- method: 'POST', path: '/reset', response: Ok,
185
- errors: { 401: ErrorBody, 404: ErrorBody },
186
- doc: { summary: "Reset the caller's progress in this game", tags: ['progress'] },
187
- },
188
- }),
189
- };
1
+ /**
2
+ * The caller's own per-game data: profile, save slots, numeric stats, and
3
+ * resetting a game. Guests count: a guest is a real account row.
4
+ */
5
+ import { use } from '@ayepi/core';
6
+ import { z } from 'zod';
7
+ import { Json, Key, Millis, Ok, Slug } from '../../common';
8
+ import { ContentRejected, ErrorBody } from '../../errors';
9
+ import { game, requireUser, session } from '../../middleware';
10
+ import { LooseText, requiredText } from '../../coerce';
11
+
12
+ /** A player's profile in one game. */
13
+ export const Profile = z.object({
14
+ game: Slug,
15
+ /** Shown to other players; `null` when never set. */
16
+ displayName: z.string().nullable(),
17
+ /** Any JSON the game stores (`{}` by default). */
18
+ data: Json,
19
+ /** Epoch ms. */
20
+ updatedAt: Millis,
21
+ });
22
+
23
+ /** A game profile. */
24
+ export type Profile = z.infer<typeof Profile>;
25
+
26
+ /** A save slot's metadata (no payload). */
27
+ export const SaveMeta = z.object({
28
+ slot: Key,
29
+ label: z.string().nullable(),
30
+ /** Epoch ms. */
31
+ updatedAt: Millis,
32
+ });
33
+
34
+ /** A save slot's metadata. */
35
+ export type SaveMeta = z.infer<typeof SaveMeta>;
36
+
37
+ /** A save slot with its payload. */
38
+ export const Save = SaveMeta.extend({
39
+ /** The stored JSON (`null` if it could not be read back). */
40
+ data: Json,
41
+ });
42
+
43
+ /** A save slot. */
44
+ export type Save = z.infer<typeof Save>;
45
+
46
+ /** A player's numeric stats in one game: stat {@link Key} to value. */
47
+ export const Stats = z.record(Key, z.number());
48
+
49
+ /** A player's stats. */
50
+ export type Stats = z.infer<typeof Stats>;
51
+
52
+ /** How a stat update combines with the stored value. */
53
+ export const StatMode = z.enum(['set', 'incr', 'max']);
54
+
55
+ /** A stat update mode. */
56
+ export type StatMode = z.infer<typeof StatMode>;
57
+
58
+ /**
59
+ * A {@link Key} read like the hub's `validKey` (`String(x || '')`), failing
60
+ * with its message: "<label> must match /^[A-Za-z0-9_.:-]{1,64}$/ (1–64 chars)."
61
+ */
62
+ function keyField(label: string) {
63
+ const message = `${label} must match /^[A-Za-z0-9_.:-]{1,64}$/ (1–64 chars).`;
64
+ return requiredText(message).refine((text) => Key.safeParse(text).success, message);
65
+ }
66
+
67
+ /** A number read like the hub's `finiteNumber` (`Number(x)`), failing with "<label> must be a finite number.". */
68
+ function finiteNumberField(label: string) {
69
+ return z.coerce.number<number>({ error: `${label} must be a finite number.` });
70
+ }
71
+
72
+ /**
73
+ * A stat update's mode. The hub reads `body.mode || 'set'`, so a falsy value
74
+ * (`null`, `''`, `0`, `false`) also means `set`. Clients should send a
75
+ * {@link StatMode} or nothing.
76
+ */
77
+ const StatModeField = z
78
+ .union([StatMode, z.literal(['', 0, false]), z.null()], { error: 'mode must be set, incr, or max.' })
79
+ .optional()
80
+ .transform((mode) => mode || 'set');
81
+
82
+ /**
83
+ * A save's payload: any JSON, but present. (The union with `never` exists only
84
+ * so a missing value fails with the hub's message rather than zod's.)
85
+ */
86
+ const SaveData = z.union([Json, z.never()], { error: 'Missing "data".' });
87
+
88
+ /** `:slot` of a save. */
89
+ const SlotParams = z.object({ slot: keyField('slot') });
90
+
91
+ /** The per-game data endpoints. */
92
+ export const gameDataEndpoints = {
93
+ ...use(session, game).path('/games/:slug').group({
94
+ /**
95
+ * A player's stats. Defaults to the caller's own (401 when signed out);
96
+ * `?user=<id>` reads anyone's, since stats are public like a profile.
97
+ */
98
+ getGameStats: {
99
+ method: 'GET', path: '/stats',
100
+ query: z.object({
101
+ /** Whose stats to read (a user id); omit for the caller's. */
102
+ user: finiteNumberField('user').optional(),
103
+ }),
104
+ response: z.object({ stats: Stats }),
105
+ errors: { 400: ErrorBody, 401: ErrorBody, 404: ErrorBody },
106
+ doc: { summary: "A player's stats in this game", tags: ['progress'] },
107
+ },
108
+ }),
109
+ ...use(requireUser, game).path('/games/:slug').group({
110
+ getGameProfile: {
111
+ method: 'GET', path: '/profile',
112
+ response: z.object({ profile: Profile.nullable() }),
113
+ errors: { 401: ErrorBody, 404: ErrorBody },
114
+ doc: { summary: "The caller's profile in this game, or null", tags: ['progress'] },
115
+ },
116
+ /**
117
+ * Create or replace the caller's profile. A new or changed display name
118
+ * is moderated (422) because other players see it.
119
+ */
120
+ setGameProfile: {
121
+ method: 'PUT', path: '/profile',
122
+ body: z.object({
123
+ /** Up to 60 characters (longer is clipped); `null` or missing clears it. */
124
+ displayName: LooseText.nullish(),
125
+ /** Replaces the stored data; missing stores `{}`. */
126
+ data: Json.optional(),
127
+ }),
128
+ response: z.object({ profile: Profile }),
129
+ errors: { 401: ErrorBody, 404: ErrorBody, 422: ContentRejected },
130
+ doc: { summary: "Create or replace the caller's profile in this game", tags: ['progress'] },
131
+ },
132
+ listSaves: {
133
+ method: 'GET', path: '/saves',
134
+ response: z.object({ saves: z.array(SaveMeta) }),
135
+ errors: { 401: ErrorBody, 404: ErrorBody },
136
+ doc: { summary: "The caller's save slots, newest first (no payloads)", tags: ['progress'] },
137
+ },
138
+ getSave: {
139
+ method: 'GET', path: '/saves/:slot', params: SlotParams,
140
+ response: z.object({ save: Save.nullable() }),
141
+ errors: { 400: ErrorBody, 401: ErrorBody, 404: ErrorBody },
142
+ doc: { summary: 'One save slot, or null when empty', tags: ['progress'] },
143
+ },
144
+ writeSave: {
145
+ method: 'PUT', path: '/saves/:slot', params: SlotParams,
146
+ body: z.object({
147
+ /** Any JSON up to 64 KB serialized (`HUB_MAX_SAVE_BYTES`); 413 over. */
148
+ data: SaveData,
149
+ /** Up to 80 characters (longer is clipped). */
150
+ label: LooseText.nullish(),
151
+ }),
152
+ response: z.object({
153
+ ok: z.literal(true),
154
+ /** Epoch ms. */
155
+ updatedAt: Millis,
156
+ }),
157
+ errors: { 400: ErrorBody, 401: ErrorBody, 404: ErrorBody, 413: ErrorBody },
158
+ doc: { summary: 'Create or overwrite a save slot', tags: ['progress'] },
159
+ },
160
+ deleteSave: {
161
+ method: 'DELETE', path: '/saves/:slot', params: SlotParams, response: Ok,
162
+ errors: { 400: ErrorBody, 401: ErrorBody, 404: ErrorBody },
163
+ doc: { summary: 'Delete a save slot (idempotent)', tags: ['progress'] },
164
+ },
165
+ /** Update one stat. Backs the client's `setStat`, `incrStat` and `maxStat`. */
166
+ updateGameStat: {
167
+ method: 'POST', path: '/stats',
168
+ body: z.object({
169
+ key: keyField('key'),
170
+ value: finiteNumberField('value'),
171
+ /** Default `set`. */
172
+ mode: StatModeField,
173
+ }),
174
+ response: z.object({ key: Key, value: z.number() }),
175
+ errors: { 400: ErrorBody, 401: ErrorBody, 404: ErrorBody },
176
+ doc: { summary: 'Set, add to, or raise one stat; returns its new value', tags: ['progress'] },
177
+ },
178
+ /**
179
+ * Wipe the caller's saves, stats, inventory and coins in this game.
180
+ * Pending trades are closed first, refunding other players' escrow.
181
+ * Leaderboard entries and the profile are kept.
182
+ */
183
+ resetMyGameProgress: {
184
+ method: 'POST', path: '/reset', response: Ok,
185
+ errors: { 401: ErrorBody, 404: ErrorBody },
186
+ doc: { summary: "Reset the caller's progress in this game", tags: ['progress'] },
187
+ },
188
+ }),
189
+ };
@@ -1,59 +1,59 @@
1
- /**
2
- * Per-game player data: profiles, saves, stats, game reset, and the player's
3
- * records and snapshot. The per-game endpoints live in `progress/game-data.ts`.
4
- */
5
- import { z } from 'zod';
6
- import { Slug } from '../common';
7
- import { SessionUser } from '../entities';
8
- import { session } from '../middleware';
9
- import { Inventory } from './economy/bundle';
10
- import { GameLeaderboards, GameRecords, TopQuery } from './leaderboards';
11
- import { gameDataEndpoints, Profile, Save, Stats } from './progress/game-data';
12
-
13
- export { Profile, Save, SaveMeta, StatMode, Stats } from './progress/game-data';
14
-
15
- /** Everything the caller has in one game, as the snapshot carries it. */
16
- export const GameSnapshot = z.object({
17
- profile: Profile.nullable(),
18
- /** Newest first, with payloads. */
19
- saves: z.array(Save),
20
- stats: Stats,
21
- inventory: Inventory,
22
- });
23
-
24
- /** One game's slice of the snapshot. */
25
- export type GameSnapshot = z.infer<typeof GameSnapshot>;
26
-
27
- /** The progress endpoints. */
28
- export const progressEndpoints = {
29
- ...gameDataEndpoints,
30
- ...session.group({
31
- /**
32
- * The caller's leaderboard standings, grouped by game (empty when
33
- * signed out). `?game=` limits it to one game; empty means every game,
34
- * and text that is no game's slug matches nothing, as in the old hub.
35
- */
36
- listMyLeaderboardRecords: {
37
- method: 'GET', path: '/me/records',
38
- query: z.object({ game: z.string().optional() }),
39
- response: z.object({ games: z.array(GameRecords) }),
40
- doc: { summary: "The caller's leaderboard standings", tags: ['progress'] },
41
- },
42
- /**
43
- * Everything the caller has, in one request, to warm the offline cache
44
- * for every game (the client's `prefetchAll`). Signed out, only the
45
- * global boards are filled in.
46
- */
47
- getMyOfflineSnapshot: {
48
- method: 'GET', path: '/me/snapshot', query: TopQuery,
49
- response: z.object({
50
- user: SessionUser.nullable(),
51
- /** By slug: every game the caller has any profile, save, stat or holding in. */
52
- games: z.record(Slug, GameSnapshot),
53
- records: z.array(GameRecords),
54
- leaderboards: z.array(GameLeaderboards),
55
- }),
56
- doc: { summary: "The caller's data across every game, plus the global boards", tags: ['progress'] },
57
- },
58
- }),
59
- };
1
+ /**
2
+ * Per-game player data: profiles, saves, stats, game reset, and the player's
3
+ * records and snapshot. The per-game endpoints live in `progress/game-data.ts`.
4
+ */
5
+ import { z } from 'zod';
6
+ import { Slug } from '../common';
7
+ import { SessionUser } from '../entities';
8
+ import { session } from '../middleware';
9
+ import { Inventory } from './economy/bundle';
10
+ import { GameLeaderboards, GameRecords, TopQuery } from './leaderboards';
11
+ import { gameDataEndpoints, Profile, Save, Stats } from './progress/game-data';
12
+
13
+ export { Profile, Save, SaveMeta, StatMode, Stats } from './progress/game-data';
14
+
15
+ /** Everything the caller has in one game, as the snapshot carries it. */
16
+ export const GameSnapshot = z.object({
17
+ profile: Profile.nullable(),
18
+ /** Newest first, with payloads. */
19
+ saves: z.array(Save),
20
+ stats: Stats,
21
+ inventory: Inventory,
22
+ });
23
+
24
+ /** One game's slice of the snapshot. */
25
+ export type GameSnapshot = z.infer<typeof GameSnapshot>;
26
+
27
+ /** The progress endpoints. */
28
+ export const progressEndpoints = {
29
+ ...gameDataEndpoints,
30
+ ...session.group({
31
+ /**
32
+ * The caller's leaderboard standings, grouped by game (empty when
33
+ * signed out). `?game=` limits it to one game; empty means every game,
34
+ * and text that is no game's slug matches nothing, as in the old hub.
35
+ */
36
+ listMyLeaderboardRecords: {
37
+ method: 'GET', path: '/me/records',
38
+ query: z.object({ game: z.string().optional() }),
39
+ response: z.object({ games: z.array(GameRecords) }),
40
+ doc: { summary: "The caller's leaderboard standings", tags: ['progress'] },
41
+ },
42
+ /**
43
+ * Everything the caller has, in one request, to warm the offline cache
44
+ * for every game (the client's `prefetchAll`). Signed out, only the
45
+ * global boards are filled in.
46
+ */
47
+ getMyOfflineSnapshot: {
48
+ method: 'GET', path: '/me/snapshot', query: TopQuery,
49
+ response: z.object({
50
+ user: SessionUser.nullable(),
51
+ /** By slug: every game the caller has any profile, save, stat or holding in. */
52
+ games: z.record(Slug, GameSnapshot),
53
+ records: z.array(GameRecords),
54
+ leaderboards: z.array(GameLeaderboards),
55
+ }),
56
+ doc: { summary: "The caller's data across every game, plus the global boards", tags: ['progress'] },
57
+ },
58
+ }),
59
+ };
package/src/api/race.ts CHANGED
@@ -1,47 +1,47 @@
1
- /**
2
- * Races: the signed-in player's own race history (docs/plans/race.md §7).
3
- * The live race runs over the socket (`race.*` ops and events).
4
- */
5
- import { z } from 'zod';
6
- import { ErrorBody } from '../errors';
7
- import { requireActive } from '../middleware';
8
- import { RaceRecord } from './mp/races';
9
-
10
- /** An optional positive-integer query param; empty means "not set". */
11
- const OptionalCount = z.union([z.literal(''), z.coerce.number().int().positive()]).optional();
12
-
13
- /** `GET /race/history` filters. */
14
- export const RaceHistoryQuery = z.object({
15
- /** Only this game (default: every game). */
16
- game: z.union([z.literal(''), z.string().regex(/^[A-Za-z0-9_-]{1,64}$/)]).optional(),
17
- /** Only races with this player (a user id). */
18
- with: OptionalCount,
19
- /** Only races older than this race id (paging). */
20
- before: OptionalCount,
21
- /** Page size, 1 to 50 (default 20; larger values are capped). */
22
- limit: OptionalCount,
23
- });
24
-
25
- /** A page of the player's races, newest first. */
26
- export const RaceHistory = z.object({
27
- races: z.array(RaceRecord),
28
- /** More races exist before the last one. */
29
- more: z.boolean(),
30
- /** Totals over every race matching the filters (not just this page). */
31
- summary: z.object({
32
- races: z.number().int().nonnegative(),
33
- wins: z.number().int().nonnegative(),
34
- losses: z.number().int().nonnegative(),
35
- }),
36
- });
37
-
38
- /** The race endpoints. */
39
- export const raceEndpoints = {
40
- ...requireActive.group({
41
- listMyRaceHistory: {
42
- method: 'GET', path: '/race/history', query: RaceHistoryQuery, response: RaceHistory,
43
- errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody },
44
- doc: { summary: "The signed-in player's decided races, newest first", tags: ['race'] },
45
- },
46
- }),
47
- };
1
+ /**
2
+ * Races: the signed-in player's own race history (docs/plans/race.md §7).
3
+ * The live race runs over the socket (`race.*` ops and events).
4
+ */
5
+ import { z } from 'zod';
6
+ import { ErrorBody } from '../errors';
7
+ import { requireActive } from '../middleware';
8
+ import { RaceRecord } from './mp/races';
9
+
10
+ /** An optional positive-integer query param; empty means "not set". */
11
+ const OptionalCount = z.union([z.literal(''), z.coerce.number().int().positive()]).optional();
12
+
13
+ /** `GET /race/history` filters. */
14
+ export const RaceHistoryQuery = z.object({
15
+ /** Only this game (default: every game). */
16
+ game: z.union([z.literal(''), z.string().regex(/^[A-Za-z0-9_-]{1,64}$/)]).optional(),
17
+ /** Only races with this player (a user id). */
18
+ with: OptionalCount,
19
+ /** Only races older than this race id (paging). */
20
+ before: OptionalCount,
21
+ /** Page size, 1 to 50 (default 20; larger values are capped). */
22
+ limit: OptionalCount,
23
+ });
24
+
25
+ /** A page of the player's races, newest first. */
26
+ export const RaceHistory = z.object({
27
+ races: z.array(RaceRecord),
28
+ /** More races exist before the last one. */
29
+ more: z.boolean(),
30
+ /** Totals over every race matching the filters (not just this page). */
31
+ summary: z.object({
32
+ races: z.number().int().nonnegative(),
33
+ wins: z.number().int().nonnegative(),
34
+ losses: z.number().int().nonnegative(),
35
+ }),
36
+ });
37
+
38
+ /** The race endpoints. */
39
+ export const raceEndpoints = {
40
+ ...requireActive.group({
41
+ listMyRaceHistory: {
42
+ method: 'GET', path: '/race/history', query: RaceHistoryQuery, response: RaceHistory,
43
+ errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody },
44
+ doc: { summary: "The signed-in player's decided races, newest first", tags: ['race'] },
45
+ },
46
+ }),
47
+ };