@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
package/src/api/notify.ts CHANGED
@@ -1,111 +1,114 @@
1
- /**
2
- * Notification preferences and Web Push subscriptions
3
- * (multiplayer-impl.md §5.4). Everything is off by default and claimed-only.
4
- * Which channel an alert takes (a live `ev:notice` toast or a push) is the
5
- * hub's decision; see `ev:notice`.
6
- */
7
- import { z } from 'zod';
8
- import { Ok, Slug, UserId } from '../common';
9
- import { ErrorBody } from '../errors';
10
- import { requireActive, requireClaimed } from '../middleware';
11
-
12
- /** Notification preferences (`GET`/`PUT /social/notify`, `GET /social/me`). */
13
- export const NotifyPrefs = z.object({
14
- /** Push a new DM when no tab is open. */
15
- dm: z.boolean(),
16
- /** Push a friend request when no tab is open. */
17
- friend_request: z.boolean(),
18
- /** "Tell me when any friend comes online", on top of the per-friend bells. */
19
- friendOnlineAll: z.boolean(),
20
- /** Friends whose bell is on, ascending. */
21
- friendOnline: z.array(UserId),
22
- /** Games followed for "someone's waiting in a lobby", sorted. */
23
- lobbyOpen: z.array(Slug),
24
- });
25
-
26
- /** Notification preferences. */
27
- export type NotifyPrefs = z.infer<typeof NotifyPrefs>;
28
-
29
- /** The preference kinds `PUT /social/notify` can flip. */
30
- export const NotifyPrefKind = z.enum(['dm', 'friend_request', 'friend_online', 'lobby_open'], {
31
- error: 'kind must be dm, friend_request, friend_online or lobby_open.',
32
- });
33
-
34
- /** A preference kind. */
35
- export type NotifyPrefKind = z.infer<typeof NotifyPrefKind>;
36
-
37
- /**
38
- * One `PUT /social/notify` change. `target`: for `friend_online`, a friend's
39
- * id (a number, or its decimal string) for that friend's bell, or absent
40
- * (`''`, `null`) for the "any friend" switch; for `lobby_open`, the game slug.
41
- * Ignored for `dm` and `friend_request`. Any other number or text is a 404
42
- * (not a friend, no such game), as on the old hub. `enabled` is checked
43
- * first, so its message wins when both fields are wrong.
44
- */
45
- export const NotifyChange = z.object({
46
- enabled: z.boolean({ error: 'enabled must be true or false.' }),
47
- kind: NotifyPrefKind,
48
- target: z.union([z.string(), z.number()]).nullable().optional(),
49
- });
50
-
51
- /** A notification preference change. */
52
- export type NotifyChange = z.infer<typeof NotifyChange>;
53
-
54
- /** The 400 for a subscription the hub can't push to (malformed, or not on a known push service). */
55
- export const UNSUPPORTED_SUBSCRIPTION = "That browser subscription isn't supported.";
56
-
57
- /**
58
- * A browser push subscription, as `PushSubscription.toJSON()` gives it. The
59
- * endpoint must be `https` on a known push service; `p256dh` is a 65-byte
60
- * and `auth` an at-least-16-byte base64url key.
61
- */
62
- export const PushSubscriptionJson = z.object({
63
- endpoint: z.string().max(1000, { error: UNSUPPORTED_SUBSCRIPTION }),
64
- /** When the subscription expires (epoch ms), `null` when it doesn't. */
65
- expirationTime: z.number().nullable().optional(),
66
- keys: z.object({
67
- p256dh: z.string().max(200, { error: UNSUPPORTED_SUBSCRIPTION }),
68
- auth: z.string().max(100, { error: UNSUPPORTED_SUBSCRIPTION }),
69
- }),
70
- });
71
-
72
- /** A browser push subscription. */
73
- export type PushSubscriptionJson = z.infer<typeof PushSubscriptionJson>;
74
-
75
- /** The notification endpoints. */
76
- export const notifyEndpoints = {
77
- ...requireActive.group({
78
- getNotificationPreferences: {
79
- method: 'GET', path: '/social/notify', response: NotifyPrefs,
80
- errors: { 401: ErrorBody, 403: ErrorBody },
81
- doc: { summary: 'Your notification preferences (all off for guests)', tags: ['notify'] },
82
- },
83
- unsubscribeFromPush: {
84
- method: 'DELETE', path: '/social/push',
85
- body: z.object({ endpoint: z.string({ error: 'endpoint is required.' }).min(1, { error: 'endpoint is required.' }) }), response: Ok,
86
- errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody },
87
- doc: { summary: "Forget one of this account's push subscriptions", tags: ['notify'] },
88
- },
89
- }),
90
- ...requireClaimed.group({
91
- setNotificationPreferences: {
92
- method: 'PUT', path: '/social/notify', body: NotifyChange, response: NotifyPrefs,
93
- errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody },
94
- doc: {
95
- summary: 'Flip one notification preference',
96
- description: '404 for a friend bell on a non-friend or an unknown game; 409 past 50 followed games.',
97
- tags: ['notify'],
98
- },
99
- },
100
- subscribeToPush: {
101
- method: 'POST', path: '/social/push',
102
- body: z.object({ subscription: z.union([PushSubscriptionJson, z.never()], { error: UNSUPPORTED_SUBSCRIPTION }) }), response: Ok,
103
- errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody },
104
- doc: {
105
- summary: "Store this browser's push subscription",
106
- description: 'One browser is one endpoint: subscribing from another account moves it. Past the per-user cap the oldest is dropped.',
107
- tags: ['notify'],
108
- },
109
- },
110
- }),
111
- };
1
+ /**
2
+ * Notification preferences and Web Push subscriptions
3
+ * (multiplayer-impl.md §5.4). Everything is off by default and claimed-only.
4
+ * Which channel an alert takes (a live `ev:notice` toast or a push) is the
5
+ * hub's decision; see `ev:notice`.
6
+ */
7
+ import { z } from 'zod';
8
+ import { Ok, Slug, UserId } from '../common';
9
+ import { ErrorBody } from '../errors';
10
+ import { requireActive, requireClaimed } from '../middleware';
11
+
12
+ /** Notification preferences (`GET`/`PUT /social/notify`, `GET /social/me`). */
13
+ export const NotifyPrefs = z.object({
14
+ /** Push a new DM when no tab is open. */
15
+ dm: z.boolean(),
16
+ /** Push a friend request when no tab is open. */
17
+ friend_request: z.boolean(),
18
+ /** "Tell me when any friend comes online", on top of the per-friend bells. */
19
+ friendOnlineAll: z.boolean(),
20
+ /** Friends whose bell is on, ascending. */
21
+ friendOnline: z.array(UserId),
22
+ /**
23
+ * Games followed for "someone wants to play" (the game sheet's notify toggle):
24
+ * an open public lobby or race, or a tournament opening, sorted.
25
+ */
26
+ lobbyOpen: z.array(Slug),
27
+ });
28
+
29
+ /** Notification preferences. */
30
+ export type NotifyPrefs = z.infer<typeof NotifyPrefs>;
31
+
32
+ /** The preference kinds `PUT /social/notify` can flip. */
33
+ export const NotifyPrefKind = z.enum(['dm', 'friend_request', 'friend_online', 'lobby_open'], {
34
+ error: 'kind must be dm, friend_request, friend_online or lobby_open.',
35
+ });
36
+
37
+ /** A preference kind. */
38
+ export type NotifyPrefKind = z.infer<typeof NotifyPrefKind>;
39
+
40
+ /**
41
+ * One `PUT /social/notify` change. `target`: for `friend_online`, a friend's
42
+ * id (a number, or its decimal string) for that friend's bell, or absent
43
+ * (`''`, `null`) for the "any friend" switch; for `lobby_open`, the game slug.
44
+ * Ignored for `dm` and `friend_request`. Any other number or text is a 404
45
+ * (not a friend, no such game), as on the old hub. `enabled` is checked
46
+ * first, so its message wins when both fields are wrong.
47
+ */
48
+ export const NotifyChange = z.object({
49
+ enabled: z.boolean({ error: 'enabled must be true or false.' }),
50
+ kind: NotifyPrefKind,
51
+ target: z.union([z.string(), z.number()]).nullable().optional(),
52
+ });
53
+
54
+ /** A notification preference change. */
55
+ export type NotifyChange = z.infer<typeof NotifyChange>;
56
+
57
+ /** The 400 for a subscription the hub can't push to (malformed, or not on a known push service). */
58
+ export const UNSUPPORTED_SUBSCRIPTION = "That browser subscription isn't supported.";
59
+
60
+ /**
61
+ * A browser push subscription, as `PushSubscription.toJSON()` gives it. The
62
+ * endpoint must be `https` on a known push service; `p256dh` is a 65-byte
63
+ * and `auth` an at-least-16-byte base64url key.
64
+ */
65
+ export const PushSubscriptionJson = z.object({
66
+ endpoint: z.string().max(1000, { error: UNSUPPORTED_SUBSCRIPTION }),
67
+ /** When the subscription expires (epoch ms), `null` when it doesn't. */
68
+ expirationTime: z.number().nullable().optional(),
69
+ keys: z.object({
70
+ p256dh: z.string().max(200, { error: UNSUPPORTED_SUBSCRIPTION }),
71
+ auth: z.string().max(100, { error: UNSUPPORTED_SUBSCRIPTION }),
72
+ }),
73
+ });
74
+
75
+ /** A browser push subscription. */
76
+ export type PushSubscriptionJson = z.infer<typeof PushSubscriptionJson>;
77
+
78
+ /** The notification endpoints. */
79
+ export const notifyEndpoints = {
80
+ ...requireActive.group({
81
+ getNotificationPreferences: {
82
+ method: 'GET', path: '/social/notify', response: NotifyPrefs,
83
+ errors: { 401: ErrorBody, 403: ErrorBody },
84
+ doc: { summary: 'Your notification preferences (all off for guests)', tags: ['notify'] },
85
+ },
86
+ unsubscribeFromPush: {
87
+ method: 'DELETE', path: '/social/push',
88
+ body: z.object({ endpoint: z.string({ error: 'endpoint is required.' }).min(1, { error: 'endpoint is required.' }) }), response: Ok,
89
+ errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody },
90
+ doc: { summary: "Forget one of this account's push subscriptions", tags: ['notify'] },
91
+ },
92
+ }),
93
+ ...requireClaimed.group({
94
+ setNotificationPreferences: {
95
+ method: 'PUT', path: '/social/notify', body: NotifyChange, response: NotifyPrefs,
96
+ errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody, 404: ErrorBody, 409: ErrorBody },
97
+ doc: {
98
+ summary: 'Flip one notification preference',
99
+ description: '404 for a friend bell on a non-friend or an unknown game; 409 past 50 followed games.',
100
+ tags: ['notify'],
101
+ },
102
+ },
103
+ subscribeToPush: {
104
+ method: 'POST', path: '/social/push',
105
+ body: z.object({ subscription: z.union([PushSubscriptionJson, z.never()], { error: UNSUPPORTED_SUBSCRIPTION }) }), response: Ok,
106
+ errors: { 400: ErrorBody, 401: ErrorBody, 403: ErrorBody },
107
+ doc: {
108
+ summary: "Store this browser's push subscription",
109
+ description: 'One browser is one endpoint: subscribing from another account moves it. Past the per-user cap the oldest is dropped.',
110
+ tags: ['notify'],
111
+ },
112
+ },
113
+ }),
114
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Offline replay metadata: which public endpoints a client may queue while
3
+ * offline and send later, and how the hub makes a replay land exactly once.
4
+ *
5
+ * A replayable call carries a client-made operation key in the
6
+ * {@link OFFLINE_OP_HEADER} header. The hub remembers the reply to each key
7
+ * for {@link OFFLINE_DEDUPE_TTL_MS}; a second request with the same key gets
8
+ * that reply again without running the endpoint twice. Endpoints left out
9
+ * are online-only (social, multiplayer, trades) or replay on their own terms
10
+ * (analytics events are buffered and sent at least once, without a key).
11
+ */
12
+ import { z } from 'zod';
13
+
14
+ /** The request header that carries a replayable call's operation key. */
15
+ export const OFFLINE_OP_HEADER = 'x-hub-op';
16
+
17
+ /** An operation key: letters, digits and `_ . : -`, 1 to 100 characters. */
18
+ export const OfflineOpKey = z.string().regex(/^[A-Za-z0-9_.:-]{1,100}$/);
19
+
20
+ /** How long the hub remembers an operation key's reply: 7 days. */
21
+ export const OFFLINE_DEDUPE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
22
+
23
+ /** How an endpoint behaves offline: `replay` means it may be queued and sent later, deduplicated by its operation key. */
24
+ export type OfflineMode = 'replay';
25
+
26
+ /** The public endpoints that support offline replay, by name. */
27
+ export const OFFLINE_REPLAY_ENDPOINTS = {
28
+ writeSave: 'replay',
29
+ deleteSave: 'replay',
30
+ updateGameStat: 'replay',
31
+ setGameProfile: 'replay',
32
+ submitScore: 'replay',
33
+ completeDailyChallenge: 'replay',
34
+ grantInventoryItems: 'replay',
35
+ exchangeInventoryItems: 'replay',
36
+ setGameFavorite: 'replay',
37
+ } as const satisfies Readonly<Record<string, OfflineMode>>;
38
+
39
+ /** The name of an endpoint that supports offline replay. */
40
+ export type OfflineReplayEndpoint = keyof typeof OFFLINE_REPLAY_ENDPOINTS;
41
+
42
+ /** Whether endpoint `name` supports offline replay. */
43
+ export function isOfflineReplayEndpoint(name: string): name is OfflineReplayEndpoint {
44
+ return Object.hasOwn(OFFLINE_REPLAY_ENDPOINTS, name);
45
+ }