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

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 (68) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -21
  3. package/README.md +26 -26
  4. package/dist/index.d.ts +95 -95
  5. package/package.json +53 -53
  6. package/src/api/analytics.ts +81 -81
  7. package/src/api/auth.ts +77 -77
  8. package/src/api/catalog.ts +171 -171
  9. package/src/api/daily.ts +122 -122
  10. package/src/api/economy/bundle.ts +69 -69
  11. package/src/api/index.ts +85 -85
  12. package/src/api/leaderboards.ts +151 -151
  13. package/src/api/messages.ts +142 -142
  14. package/src/api/mp/chat.ts +36 -36
  15. package/src/api/mp/races.ts +211 -211
  16. package/src/api/mp/rooms.ts +90 -90
  17. package/src/api/mp/snapshot.ts +17 -17
  18. package/src/api/mp.ts +110 -110
  19. package/src/api/notify.ts +111 -111
  20. package/src/api/progress/game-data.ts +189 -189
  21. package/src/api/progress.ts +59 -59
  22. package/src/api/race.ts +47 -47
  23. package/src/api/reports.ts +208 -208
  24. package/src/api/social/config.ts +47 -47
  25. package/src/api/social/endpoints.ts +179 -179
  26. package/src/api/social/people.ts +118 -118
  27. package/src/api/social/presence.ts +95 -95
  28. package/src/api/social.ts +9 -9
  29. package/src/api/version.ts +40 -40
  30. package/src/coerce.ts +50 -50
  31. package/src/common.ts +42 -42
  32. package/src/entities.ts +62 -62
  33. package/src/errors/codes/core.ts +10 -10
  34. package/src/errors/codes/multiplayer.ts +43 -43
  35. package/src/errors/codes/play.ts +5 -5
  36. package/src/errors/codes/social.ts +31 -31
  37. package/src/errors/index.ts +88 -88
  38. package/src/index.ts +16 -16
  39. package/src/limits.ts +100 -100
  40. package/src/logic/daily.ts +54 -54
  41. package/src/logic/index.ts +8 -8
  42. package/src/logic/presence.ts +62 -62
  43. package/src/logic/time.ts +81 -81
  44. package/src/manifest/changes.ts +55 -55
  45. package/src/manifest/controls.ts +42 -42
  46. package/src/manifest/fields.ts +89 -89
  47. package/src/manifest/game.ts +132 -132
  48. package/src/manifest/index.ts +12 -12
  49. package/src/manifest/inject.ts +31 -31
  50. package/src/manifest/leaderboards.ts +29 -29
  51. package/src/manifest/multiplayer.ts +71 -71
  52. package/src/manifest/race.ts +93 -93
  53. package/src/middleware.ts +47 -47
  54. package/src/realtime/define.ts +52 -52
  55. package/src/realtime/envelope.ts +67 -67
  56. package/src/realtime/events/multiplayer.ts +8 -8
  57. package/src/realtime/events/parties.ts +31 -31
  58. package/src/realtime/events/races.ts +27 -27
  59. package/src/realtime/events/rooms.ts +48 -48
  60. package/src/realtime/events/social.ts +81 -81
  61. package/src/realtime/index.ts +24 -24
  62. package/src/realtime/ops/empty.ts +8 -8
  63. package/src/realtime/ops/hello.ts +40 -40
  64. package/src/realtime/ops/multiplayer.ts +8 -8
  65. package/src/realtime/ops/parties.ts +13 -13
  66. package/src/realtime/ops/races.ts +99 -99
  67. package/src/realtime/ops/rooms.ts +1 -1
  68. package/src/realtime/ops/social.ts +58 -58
@@ -1,95 +1,95 @@
1
- /**
2
- * Presence: what a player is doing right now, as their friends see it, and
3
- * the anonymous online counts (multiplayer-impl.md §4.4). Games report a
4
- * {@link PresenceInput} per tab (socket op `presence`); the hub folds a
5
- * user's tabs into one {@link Presence} and pushes it to friends
6
- * (`ev:presence`).
7
- */
8
- import { z } from 'zod';
9
- import { Millis, Slug } from '../../common';
10
-
11
- /** Activity kinds, least engaged first. The most engaged tab wins; hidden or idle tabs count as `away`. */
12
- export const PresenceKind = z.enum(['away', 'menu', 'lobby', 'watching', 'playing']);
13
-
14
- /** An activity kind. */
15
- export type PresenceKind = z.infer<typeof PresenceKind>;
16
-
17
- /** A room reference friends can join with: 1 to 64 of `A-Z a-z 0-9 _ : -`. */
18
- export const PresenceRoom = z.string().regex(/^[A-Za-z0-9_:-]{1,64}$/);
19
-
20
- /** A game mode key: 1 to 32 of `A-Z a-z 0-9 _ -`. */
21
- export const PresenceMode = z.string().regex(/^[A-Za-z0-9_-]{1,32}$/);
22
-
23
- /** The fields a tab reports and friends see back, with the limits the hub enforces. */
24
- const PresenceFields = z.object({
25
- kind: PresenceKind,
26
- /** A short status line (at most 40 characters), filtered; a rejected one is dropped. */
27
- detail: z.string().max(40).optional(),
28
- /** Friends may join. Only ever `true` when present. */
29
- joinable: z.boolean().optional(),
30
- /** Friends may watch. Only ever `true` when present. */
31
- watchable: z.boolean().optional(),
32
- /** Join info for friends: the game's own room reference, or the hub room's code. */
33
- room: PresenceRoom.optional(),
34
- /** A public lobby: friends who follow the game may get a `lobby_open` alert. */
35
- public: z.boolean().optional(),
36
- /** Free seats, 0 to 64. */
37
- openSeats: z.number().int().min(0).max(64).optional(),
38
- mode: PresenceMode.optional(),
39
- });
40
-
41
- /**
42
- * What a game reports for its tab (socket op `presence`). The hub is
43
- * lenient: an unknown `kind`, or input that isn't an object at all (`null`),
44
- * counts as `{kind:'menu'}`; unknown fields, and fields that break a rule (a
45
- * bad `room`, a fractional `openSeats`, a field of the wrong type), are
46
- * ignored one by one, so friends only ever
47
- * see {@link Presence}'s fields; `detail` is trimmed to 40 characters before
48
- * filtering.
49
- */
50
- export const PresenceInput = z.looseObject({
51
- kind: PresenceKind.catch('menu'),
52
- detail: z.string().optional().catch(undefined),
53
- joinable: z.boolean().optional().catch(undefined),
54
- watchable: z.boolean().optional().catch(undefined),
55
- room: z.string().optional().catch(undefined),
56
- public: z.boolean().optional().catch(undefined),
57
- openSeats: z.number().optional().catch(undefined),
58
- mode: z.string().optional().catch(undefined),
59
- }).catch({ kind: 'menu' });
60
-
61
- /** A presence report. */
62
- export type PresenceInput = z.input<typeof PresenceInput>;
63
-
64
- /** A user's aggregated presence: their most engaged tab, decorated by the hub. */
65
- export const Presence = PresenceFields.extend({
66
- /** The game the tab is on, `null` on the catalog and other hub pages. */
67
- game: Slug.nullable(),
68
- /** That game's title, `null` with `game`. */
69
- gameTitle: z.string().nullable(),
70
- /** When this activity (kind + game) started (epoch ms). */
71
- since: Millis,
72
- /** Party size, present when the user is in a party of two or more. */
73
- partySize: z.number().int().min(2).optional(),
74
- });
75
-
76
- /** A user's aggregated presence. */
77
- export type Presence = z.infer<typeof Presence>;
78
-
79
- /** A non-negative head count. */
80
- const HeadCount = z.number().int().nonnegative();
81
-
82
- /**
83
- * Anonymous online counts (`GET /social/counts`, `ev:counts`, the `hello`
84
- * snapshot): everyone with a live presence, guests and hidden players
85
- * included.
86
- */
87
- export const Counts = z.object({
88
- /** Everyone online. */
89
- online: HeadCount,
90
- /** Players per game slug (games with nobody are absent). */
91
- byGame: z.record(Slug, HeadCount),
92
- });
93
-
94
- /** Online counts. */
95
- export type Counts = z.infer<typeof Counts>;
1
+ /**
2
+ * Presence: what a player is doing right now, as their friends see it, and
3
+ * the anonymous online counts (multiplayer-impl.md §4.4). Games report a
4
+ * {@link PresenceInput} per tab (socket op `presence`); the hub folds a
5
+ * user's tabs into one {@link Presence} and pushes it to friends
6
+ * (`ev:presence`).
7
+ */
8
+ import { z } from 'zod';
9
+ import { Millis, Slug } from '../../common';
10
+
11
+ /** Activity kinds, least engaged first. The most engaged tab wins; hidden or idle tabs count as `away`. */
12
+ export const PresenceKind = z.enum(['away', 'menu', 'lobby', 'watching', 'playing']);
13
+
14
+ /** An activity kind. */
15
+ export type PresenceKind = z.infer<typeof PresenceKind>;
16
+
17
+ /** A room reference friends can join with: 1 to 64 of `A-Z a-z 0-9 _ : -`. */
18
+ export const PresenceRoom = z.string().regex(/^[A-Za-z0-9_:-]{1,64}$/);
19
+
20
+ /** A game mode key: 1 to 32 of `A-Z a-z 0-9 _ -`. */
21
+ export const PresenceMode = z.string().regex(/^[A-Za-z0-9_-]{1,32}$/);
22
+
23
+ /** The fields a tab reports and friends see back, with the limits the hub enforces. */
24
+ const PresenceFields = z.object({
25
+ kind: PresenceKind,
26
+ /** A short status line (at most 40 characters), filtered; a rejected one is dropped. */
27
+ detail: z.string().max(40).optional(),
28
+ /** Friends may join. Only ever `true` when present. */
29
+ joinable: z.boolean().optional(),
30
+ /** Friends may watch. Only ever `true` when present. */
31
+ watchable: z.boolean().optional(),
32
+ /** Join info for friends: the game's own room reference, or the hub room's code. */
33
+ room: PresenceRoom.optional(),
34
+ /** A public lobby: friends who follow the game may get a `lobby_open` alert. */
35
+ public: z.boolean().optional(),
36
+ /** Free seats, 0 to 64. */
37
+ openSeats: z.number().int().min(0).max(64).optional(),
38
+ mode: PresenceMode.optional(),
39
+ });
40
+
41
+ /**
42
+ * What a game reports for its tab (socket op `presence`). The hub is
43
+ * lenient: an unknown `kind`, or input that isn't an object at all (`null`),
44
+ * counts as `{kind:'menu'}`; unknown fields, and fields that break a rule (a
45
+ * bad `room`, a fractional `openSeats`, a field of the wrong type), are
46
+ * ignored one by one, so friends only ever
47
+ * see {@link Presence}'s fields; `detail` is trimmed to 40 characters before
48
+ * filtering.
49
+ */
50
+ export const PresenceInput = z.looseObject({
51
+ kind: PresenceKind.catch('menu'),
52
+ detail: z.string().optional().catch(undefined),
53
+ joinable: z.boolean().optional().catch(undefined),
54
+ watchable: z.boolean().optional().catch(undefined),
55
+ room: z.string().optional().catch(undefined),
56
+ public: z.boolean().optional().catch(undefined),
57
+ openSeats: z.number().optional().catch(undefined),
58
+ mode: z.string().optional().catch(undefined),
59
+ }).catch({ kind: 'menu' });
60
+
61
+ /** A presence report. */
62
+ export type PresenceInput = z.input<typeof PresenceInput>;
63
+
64
+ /** A user's aggregated presence: their most engaged tab, decorated by the hub. */
65
+ export const Presence = PresenceFields.extend({
66
+ /** The game the tab is on, `null` on the catalog and other hub pages. */
67
+ game: Slug.nullable(),
68
+ /** That game's title, `null` with `game`. */
69
+ gameTitle: z.string().nullable(),
70
+ /** When this activity (kind + game) started (epoch ms). */
71
+ since: Millis,
72
+ /** Party size, present when the user is in a party of two or more. */
73
+ partySize: z.number().int().min(2).optional(),
74
+ });
75
+
76
+ /** A user's aggregated presence. */
77
+ export type Presence = z.infer<typeof Presence>;
78
+
79
+ /** A non-negative head count. */
80
+ const HeadCount = z.number().int().nonnegative();
81
+
82
+ /**
83
+ * Anonymous online counts (`GET /social/counts`, `ev:counts`, the `hello`
84
+ * snapshot): everyone with a live presence, guests and hidden players
85
+ * included.
86
+ */
87
+ export const Counts = z.object({
88
+ /** Everyone online. */
89
+ online: HeadCount,
90
+ /** Players per game slug (games with nobody are absent). */
91
+ byGame: z.record(Slug, HeadCount),
92
+ });
93
+
94
+ /** Online counts. */
95
+ export type Counts = z.infer<typeof Counts>;
package/src/api/social.ts CHANGED
@@ -1,9 +1,9 @@
1
- /**
2
- * Friends, blocks, the social profile and settings, player cards and counts,
3
- * plus the shared social shapes (config, presence, people) the socket
4
- * `hello` snapshot and events reuse. Split by topic under `social/`.
5
- */
6
- export * from './social/config';
7
- export * from './social/presence';
8
- export * from './social/people';
9
- export * from './social/endpoints';
1
+ /**
2
+ * Friends, blocks, the social profile and settings, player cards and counts,
3
+ * plus the shared social shapes (config, presence, people) the socket
4
+ * `hello` snapshot and events reuse. Split by topic under `social/`.
5
+ */
6
+ export * from './social/config';
7
+ export * from './social/presence';
8
+ export * from './social/people';
9
+ export * from './social/endpoints';
@@ -1,40 +1,40 @@
1
- /**
2
- * `GET /version`: which client versions this hub serves (plan §4.3.3). Games'
3
- * clients read it once per page, and `hub-doctor` reads it in a game's CI.
4
- * The old hub has no such route and answers 404 "No such API endpoint.", which
5
- * clients read as "a hub from before versioning".
6
- */
7
- import { z } from 'zod';
8
- import { session } from '../middleware';
9
-
10
- /**
11
- * A semver range in the npm form: comparators (`>=2.0.0-0`, `<3.0.0`)
12
- * separated by spaces are all required; `||` separates alternatives.
13
- */
14
- export const VersionRange = z.string().min(1);
15
-
16
- /** What `GET /version` answers. */
17
- export const HubVersion = z.object({
18
- /** The hub release this server was built from: the version of the client it serves at `/_hub/hub.js`. */
19
- server: z.string(),
20
- /** The version of `@diffenderfer-games/hub-contract` the server implements. */
21
- contract: z.string(),
22
- /** Client versions the server supports (`x-hub-client`); a client outside it should be updated. */
23
- supported: VersionRange,
24
- /** Supported client versions that are deprecated: they still work, and `hub-doctor` warns. */
25
- deprecated: VersionRange,
26
- });
27
-
28
- /** The hub's version and the client versions it supports. */
29
- export type HubVersion = z.infer<typeof HubVersion>;
30
-
31
- /** The version endpoints. */
32
- export const versionEndpoints = {
33
- ...session.group({
34
- getHubVersion: {
35
- method: 'GET', path: '/version',
36
- response: HubVersion,
37
- doc: { summary: 'The hub version and the client versions it supports', tags: ['catalog'] },
38
- },
39
- }),
40
- };
1
+ /**
2
+ * `GET /version`: which client versions this hub serves (plan §4.3.3). Games'
3
+ * clients read it once per page, and `hub-doctor` reads it in a game's CI.
4
+ * The old hub has no such route and answers 404 "No such API endpoint.", which
5
+ * clients read as "a hub from before versioning".
6
+ */
7
+ import { z } from 'zod';
8
+ import { session } from '../middleware';
9
+
10
+ /**
11
+ * A semver range in the npm form: comparators (`>=2.0.0-0`, `<3.0.0`)
12
+ * separated by spaces are all required; `||` separates alternatives.
13
+ */
14
+ export const VersionRange = z.string().min(1);
15
+
16
+ /** What `GET /version` answers. */
17
+ export const HubVersion = z.object({
18
+ /** The hub release this server was built from: the version of the client it serves at `/_hub/hub.js`. */
19
+ server: z.string(),
20
+ /** The version of `@diffenderfer-games/hub-contract` the server implements. */
21
+ contract: z.string(),
22
+ /** Client versions the server supports (`x-hub-client`); a client outside it should be updated. */
23
+ supported: VersionRange,
24
+ /** Supported client versions that are deprecated: they still work, and `hub-doctor` warns. */
25
+ deprecated: VersionRange,
26
+ });
27
+
28
+ /** The hub's version and the client versions it supports. */
29
+ export type HubVersion = z.infer<typeof HubVersion>;
30
+
31
+ /** The version endpoints. */
32
+ export const versionEndpoints = {
33
+ ...session.group({
34
+ getHubVersion: {
35
+ method: 'GET', path: '/version',
36
+ response: HubVersion,
37
+ doc: { summary: 'The hub version and the client versions it supports', tags: ['catalog'] },
38
+ },
39
+ }),
40
+ };
package/src/coerce.ts CHANGED
@@ -1,50 +1,50 @@
1
- /**
2
- * Request coercions the single-player handlers apply today. The old hub reads
3
- * many request fields loosely (`Number(x)`, `String(x)`, truthiness), and the
4
- * contract must accept every request the hub accepts, so these schemas
5
- * coerce the same way while keeping the input type clients should send.
6
- */
7
- import { z } from 'zod';
8
-
9
- /**
10
- * A finite number, read like the hub's `Number(x)`: numeric strings (and
11
- * query-string values) pass. Clients should send a JSON number.
12
- */
13
- export const LooseNumber = z.coerce.number<number>();
14
-
15
- /**
16
- * A flag read by truthiness, like the hub's `if (body.on)`: any value is
17
- * accepted and becomes `true` or `false`. Clients should send a boolean.
18
- */
19
- export const Truthy = z.coerce.boolean<boolean>();
20
-
21
- /**
22
- * Free text read with `String(x)`; the hub trims or clips it to its own limit
23
- * rather than rejecting it. Clients should send a string.
24
- */
25
- export const LooseText = z.coerce.string<string>();
26
-
27
- /**
28
- * A row count from the query string (`?limit=`, `?top=`). The hub clamps it to
29
- * its own range and falls back to a default for `0` or a missing value.
30
- */
31
- export const RowCount = LooseNumber.int();
32
-
33
- /**
34
- * Required text read like the hub's `String(x || '')`, with the old hub's
35
- * message for a missing or invalid value. Numbers are accepted and become
36
- * strings; anything else fails with `message`. Refine the result with
37
- * `.pipe(...)` (e.g. a pattern carrying the same message).
38
- */
39
- export function requiredText(message: string) {
40
- return z.union([z.string(), z.number()], { error: message }).transform(String);
41
- }
42
-
43
- /**
44
- * A positive integer id read like the hub's `Number(x)` (numeric strings
45
- * pass), with the old hub's message for any bad value (`"id must be a user
46
- * id."`). Clients should send a JSON number.
47
- */
48
- export function requiredId(message: string) {
49
- return z.coerce.number<number>({ error: message }).int({ error: message }).positive({ error: message });
50
- }
1
+ /**
2
+ * Request coercions the single-player handlers apply today. The old hub reads
3
+ * many request fields loosely (`Number(x)`, `String(x)`, truthiness), and the
4
+ * contract must accept every request the hub accepts, so these schemas
5
+ * coerce the same way while keeping the input type clients should send.
6
+ */
7
+ import { z } from 'zod';
8
+
9
+ /**
10
+ * A finite number, read like the hub's `Number(x)`: numeric strings (and
11
+ * query-string values) pass. Clients should send a JSON number.
12
+ */
13
+ export const LooseNumber = z.coerce.number<number>();
14
+
15
+ /**
16
+ * A flag read by truthiness, like the hub's `if (body.on)`: any value is
17
+ * accepted and becomes `true` or `false`. Clients should send a boolean.
18
+ */
19
+ export const Truthy = z.coerce.boolean<boolean>();
20
+
21
+ /**
22
+ * Free text read with `String(x)`; the hub trims or clips it to its own limit
23
+ * rather than rejecting it. Clients should send a string.
24
+ */
25
+ export const LooseText = z.coerce.string<string>();
26
+
27
+ /**
28
+ * A row count from the query string (`?limit=`, `?top=`). The hub clamps it to
29
+ * its own range and falls back to a default for `0` or a missing value.
30
+ */
31
+ export const RowCount = LooseNumber.int();
32
+
33
+ /**
34
+ * Required text read like the hub's `String(x || '')`, with the old hub's
35
+ * message for a missing or invalid value. Numbers are accepted and become
36
+ * strings; anything else fails with `message`. Refine the result with
37
+ * `.pipe(...)` (e.g. a pattern carrying the same message).
38
+ */
39
+ export function requiredText(message: string) {
40
+ return z.union([z.string(), z.number()], { error: message }).transform(String);
41
+ }
42
+
43
+ /**
44
+ * A positive integer id read like the hub's `Number(x)` (numeric strings
45
+ * pass), with the old hub's message for any bad value (`"id must be a user
46
+ * id."`). Clients should send a JSON number.
47
+ */
48
+ export function requiredId(message: string) {
49
+ return z.coerce.number<number>({ error: message }).int({ error: message }).positive({ error: message });
50
+ }
package/src/common.ts CHANGED
@@ -1,42 +1,42 @@
1
- /**
2
- * Primitives shared by every part of the contract. Each one is the single
3
- * definition of its rule: the server validates with it, the client's types
4
- * come from it, and the docs are generated from it.
5
- */
6
- import { z } from 'zod';
7
-
8
- /** A user id: a positive integer. In paths it arrives as a string and is coerced. */
9
- export const UserId = z.number().int().positive();
10
-
11
- /** A user id taken from a URL path segment (`/players/:userId`). */
12
- export const UserIdParam = z.coerce.number().int().positive();
13
-
14
- /** A numeric row id taken from a URL path segment (`/trades/:id`, `/reports/:id`). */
15
- export const IdParam = z.coerce.number().int().positive();
16
-
17
- /** Milliseconds since the Unix epoch. */
18
- export const Millis = z.number().int().nonnegative();
19
-
20
- /** A game's slug: its folder name under `apps/`, as used in `/<slug>/` URLs. */
21
- export const Slug = z.string().min(1).max(64);
22
-
23
- /**
24
- * An app-chosen identifier: a save slot, leaderboard key or stat name.
25
- * Letters, digits and `_ . : -`, 1 to 64 characters.
26
- */
27
- export const Key = z.string().regex(/^[A-Za-z0-9_.:-]{1,64}$/);
28
-
29
- /** Any JSON value (saves, metadata, room state). */
30
- export const Json = z.json();
31
-
32
- /** A JSON value as stored and returned by the hub: never `undefined`. */
33
- export type Json = z.infer<typeof Json>;
34
-
35
- /** The `{ ok: true }` body many write endpoints answer with. */
36
- export const Ok = z.object({ ok: z.literal(true) });
37
-
38
- /** A UTC day key, `YYYY-MM-DD`. */
39
- export const DayKey = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
40
-
41
- /** A `#rrggbb` colour. */
42
- export const HexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/);
1
+ /**
2
+ * Primitives shared by every part of the contract. Each one is the single
3
+ * definition of its rule: the server validates with it, the client's types
4
+ * come from it, and the docs are generated from it.
5
+ */
6
+ import { z } from 'zod';
7
+
8
+ /** A user id: a positive integer. In paths it arrives as a string and is coerced. */
9
+ export const UserId = z.number().int().positive();
10
+
11
+ /** A user id taken from a URL path segment (`/players/:userId`). */
12
+ export const UserIdParam = z.coerce.number().int().positive();
13
+
14
+ /** A numeric row id taken from a URL path segment (`/trades/:id`, `/reports/:id`). */
15
+ export const IdParam = z.coerce.number().int().positive();
16
+
17
+ /** Milliseconds since the Unix epoch. */
18
+ export const Millis = z.number().int().nonnegative();
19
+
20
+ /** A game's slug: its folder name under `apps/`, as used in `/<slug>/` URLs. */
21
+ export const Slug = z.string().min(1).max(64);
22
+
23
+ /**
24
+ * An app-chosen identifier: a save slot, leaderboard key or stat name.
25
+ * Letters, digits and `_ . : -`, 1 to 64 characters.
26
+ */
27
+ export const Key = z.string().regex(/^[A-Za-z0-9_.:-]{1,64}$/);
28
+
29
+ /** Any JSON value (saves, metadata, room state). */
30
+ export const Json = z.json();
31
+
32
+ /** A JSON value as stored and returned by the hub: never `undefined`. */
33
+ export type Json = z.infer<typeof Json>;
34
+
35
+ /** The `{ ok: true }` body many write endpoints answer with. */
36
+ export const Ok = z.object({ ok: z.literal(true) });
37
+
38
+ /** A UTC day key, `YYYY-MM-DD`. */
39
+ export const DayKey = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
40
+
41
+ /** A `#rrggbb` colour. */
42
+ export const HexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/);
package/src/entities.ts CHANGED
@@ -1,62 +1,62 @@
1
- /**
2
- * Entity shapes that several areas of the API share. Shapes used by only one
3
- * area live in that area's `api/` file.
4
- */
5
- import { z } from 'zod';
6
- import { Millis, UserId } from './common';
7
-
8
- /** Account roles. An empty string is a normal player. */
9
- export const Role = z.enum(['', 'admin']);
10
-
11
- /**
12
- * The signed-in user as the hub sees it (`GET /me`, socket `hello`).
13
- * `id`, `username` and `guest` are the long-standing fields; the rest drive
14
- * social permissions and are only ever sent to the user themself.
15
- */
16
- export const SessionUser = z.object({
17
- id: UserId,
18
- username: z.string(),
19
- /** A temporary account with no password yet; it can be claimed. */
20
- guest: z.boolean(),
21
- role: Role,
22
- /** Avatar token, `''` for the default. */
23
- avatar: z.string(),
24
- /** A parent locked chat for this account. */
25
- chatLock: z.boolean(),
26
- /** Muted until this time (epoch ms), `0` when not muted. */
27
- mutedUntil: Millis,
28
- /** Suspended until this time (epoch ms), `0` when not suspended. */
29
- suspendedUntil: Millis,
30
- });
31
-
32
- /** The signed-in user. */
33
- export type SessionUser = z.infer<typeof SessionUser>;
34
-
35
- /** The short account shape the auth endpoints return for a new session. */
36
- export const AuthUser = SessionUser.pick({ id: true, username: true, guest: true });
37
-
38
- /** The short account shape. */
39
- export type AuthUser = z.infer<typeof AuthUser>;
40
-
41
- /**
42
- * The effective chat mode of a conversation, room or the whole site: free
43
- * text (still filtered), quick-chat phrases only, or no messaging at all.
44
- * The strictest of the operator setting, the admin switch and every
45
- * participant (guests and parent-locked accounts are `quick`) applies.
46
- */
47
- export const ChatMode = z.enum(['on', 'quick', 'off']);
48
-
49
- /** A chat mode. */
50
- export type ChatMode = z.infer<typeof ChatMode>;
51
-
52
- /** Another player as lists and cards show them. */
53
- export const PlayerLite = z.object({
54
- id: UserId,
55
- username: z.string(),
56
- /** Avatar token; never empty (a default is derived from the id). */
57
- avatar: z.string(),
58
- guest: z.boolean(),
59
- });
60
-
61
- /** Another player. */
62
- export type PlayerLite = z.infer<typeof PlayerLite>;
1
+ /**
2
+ * Entity shapes that several areas of the API share. Shapes used by only one
3
+ * area live in that area's `api/` file.
4
+ */
5
+ import { z } from 'zod';
6
+ import { Millis, UserId } from './common';
7
+
8
+ /** Account roles. An empty string is a normal player. */
9
+ export const Role = z.enum(['', 'admin']);
10
+
11
+ /**
12
+ * The signed-in user as the hub sees it (`GET /me`, socket `hello`).
13
+ * `id`, `username` and `guest` are the long-standing fields; the rest drive
14
+ * social permissions and are only ever sent to the user themself.
15
+ */
16
+ export const SessionUser = z.object({
17
+ id: UserId,
18
+ username: z.string(),
19
+ /** A temporary account with no password yet; it can be claimed. */
20
+ guest: z.boolean(),
21
+ role: Role,
22
+ /** Avatar token, `''` for the default. */
23
+ avatar: z.string(),
24
+ /** A parent locked chat for this account. */
25
+ chatLock: z.boolean(),
26
+ /** Muted until this time (epoch ms), `0` when not muted. */
27
+ mutedUntil: Millis,
28
+ /** Suspended until this time (epoch ms), `0` when not suspended. */
29
+ suspendedUntil: Millis,
30
+ });
31
+
32
+ /** The signed-in user. */
33
+ export type SessionUser = z.infer<typeof SessionUser>;
34
+
35
+ /** The short account shape the auth endpoints return for a new session. */
36
+ export const AuthUser = SessionUser.pick({ id: true, username: true, guest: true });
37
+
38
+ /** The short account shape. */
39
+ export type AuthUser = z.infer<typeof AuthUser>;
40
+
41
+ /**
42
+ * The effective chat mode of a conversation, room or the whole site: free
43
+ * text (still filtered), quick-chat phrases only, or no messaging at all.
44
+ * The strictest of the operator setting, the admin switch and every
45
+ * participant (guests and parent-locked accounts are `quick`) applies.
46
+ */
47
+ export const ChatMode = z.enum(['on', 'quick', 'off']);
48
+
49
+ /** A chat mode. */
50
+ export type ChatMode = z.infer<typeof ChatMode>;
51
+
52
+ /** Another player as lists and cards show them. */
53
+ export const PlayerLite = z.object({
54
+ id: UserId,
55
+ username: z.string(),
56
+ /** Avatar token; never empty (a default is derived from the id). */
57
+ avatar: z.string(),
58
+ guest: z.boolean(),
59
+ });
60
+
61
+ /** Another player. */
62
+ export type PlayerLite = z.infer<typeof PlayerLite>;
@@ -1,10 +1,10 @@
1
- /** Error codes any endpoint or socket op can return. */
2
- export const CORE_CODES = [
3
- 'bad_request',
4
- 'signed_out',
5
- 'suspended',
6
- 'claim_required',
7
- 'rate_limited',
8
- 'content_rejected',
9
- 'too_large',
10
- ] as const;
1
+ /** Error codes any endpoint or socket op can return. */
2
+ export const CORE_CODES = [
3
+ 'bad_request',
4
+ 'signed_out',
5
+ 'suspended',
6
+ 'claim_required',
7
+ 'rate_limited',
8
+ 'content_rejected',
9
+ 'too_large',
10
+ ] as const;