@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
package/package.json CHANGED
@@ -1,53 +1,53 @@
1
- {
2
- "name": "@diffenderfer-games/hub-contract",
3
- "version": "2.0.0-next.0",
4
- "description": "The diffenderfer.games hub wire contract: zod schemas and types for its REST endpoints, socket ops and events, errors and the game.* manifest.",
5
- "license": "MIT",
6
- "type": "module",
7
- "sideEffects": false,
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/diffenderfer-games/diffenderfer-games.git",
11
- "directory": "next/packages/public/contract"
12
- },
13
- "engines": {
14
- "node": ">=20"
15
- },
16
- "files": [
17
- "dist",
18
- "src",
19
- "README.md",
20
- "CHANGELOG.md",
21
- "LICENSE"
22
- ],
23
- "exports": {
24
- ".": {
25
- "source": "./src/index.ts",
26
- "types": "./dist/index.d.ts",
27
- "default": "./dist/index.js"
28
- },
29
- "./package.json": "./package.json"
30
- },
31
- "types": "./dist/index.d.ts",
32
- "scripts": {
33
- "build": "tsdown",
34
- "typecheck": "tsc -p tsconfig.json"
35
- },
36
- "dependencies": {
37
- "@ayepi/core": "0.4.1"
38
- },
39
- "peerDependencies": {
40
- "zod": "^4.1.0"
41
- },
42
- "peerDependenciesMeta": {
43
- "zod": {
44
- "optional": true
45
- }
46
- },
47
- "devDependencies": {
48
- "zod": "4.6.5"
49
- },
50
- "publishConfig": {
51
- "access": "public"
52
- }
53
- }
1
+ {
2
+ "name": "@diffenderfer-games/hub-contract",
3
+ "version": "2.0.0-next.1",
4
+ "description": "The diffenderfer.games hub wire contract: zod schemas and types for its REST endpoints, socket ops and events, errors and the game.* manifest.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/diffenderfer-games/diffenderfer-games.git",
11
+ "directory": "next/packages/public/contract"
12
+ },
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src",
19
+ "README.md",
20
+ "CHANGELOG.md",
21
+ "LICENSE"
22
+ ],
23
+ "exports": {
24
+ ".": {
25
+ "source": "./src/index.ts",
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ },
29
+ "./package.json": "./package.json"
30
+ },
31
+ "types": "./dist/index.d.ts",
32
+ "scripts": {
33
+ "build": "tsdown",
34
+ "typecheck": "tsc -p tsconfig.json"
35
+ },
36
+ "dependencies": {
37
+ "@ayepi/core": "0.4.1"
38
+ },
39
+ "peerDependencies": {
40
+ "zod": "^4.1.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "zod": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "devDependencies": {
48
+ "zod": "4.6.5"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ }
53
+ }
@@ -1,81 +1,81 @@
1
- /**
2
- * Gameplay events and client error logs sent by games and the menu, and the
3
- * play counts built from them. Events need no account: they carry the user id
4
- * when signed in and always the anonymous `hub_cid` client id.
5
- */
6
- import { use } from '@ayepi/core';
7
- import { z } from 'zod';
8
- import { Ok, Slug } from '../common';
9
- import { ErrorBody } from '../errors';
10
- import { game, session } from '../middleware';
11
- import { LooseText, requiredText } from '../coerce';
12
-
13
- /** Play counts for one game or the whole catalog. A player is an account, else a client id. */
14
- export const Metrics = z.object({
15
- /** `play` events recorded. */
16
- totalPlays: z.number().int().nonnegative(),
17
- /** Distinct players who sent any event. */
18
- uniquePlayers: z.number().int().nonnegative(),
19
- /** Distinct players with a `play` or `heartbeat` in the last 5 minutes (`HUB_ACTIVE_WINDOW_MS`). */
20
- activeNow: z.number().int().nonnegative(),
21
- });
22
-
23
- /** Play counts. */
24
- export type Metrics = z.infer<typeof Metrics>;
25
-
26
- /** The analytics endpoints. */
27
- export const analyticsEndpoints = {
28
- ...session.group({
29
- getSiteAnalytics: {
30
- method: 'GET', path: '/analytics',
31
- response: z.object({
32
- global: Metrics,
33
- games: z.array(Metrics.extend({ slug: Slug, title: z.string() })),
34
- }),
35
- doc: { summary: 'Catalog-wide play counts with a per-game breakdown', tags: ['analytics'] },
36
- },
37
- /**
38
- * Report a client-side failure (for example the social runtime failing
39
- * to load) to the admin console. Open to signed-out visitors; limited to
40
- * 20 an hour per IP, after which it answers `{ ok: false }` and drops the
41
- * report. Fields are clipped, never rejected.
42
- */
43
- reportClientError: {
44
- method: 'POST', path: '/client-log',
45
- body: z.object({
46
- /** Which part failed, up to 40 characters (`unknown` when empty). */
47
- where: LooseText.optional(),
48
- /** Up to 300 characters. */
49
- message: LooseText.optional(),
50
- /** Up to 1200 characters. */
51
- stack: LooseText.optional(),
52
- /** The page, up to 200 characters. */
53
- url: LooseText.optional(),
54
- }),
55
- response: z.object({
56
- /** `false` when the report was dropped by the rate limit. */
57
- ok: z.boolean(),
58
- }),
59
- doc: { summary: 'Report a client error to the admin console', tags: ['analytics'] },
60
- },
61
- }),
62
- ...use(session, game).path('/games/:slug').group({
63
- /** Record one event. The menu sends `play` on load and periodic `heartbeat`s; games may add their own. */
64
- recordGameEvent: {
65
- method: 'POST', path: '/events',
66
- body: z.object({
67
- /** 1 to 32 of letters, digits and `_ . : -` (surrounding spaces are trimmed). */
68
- type: requiredText('Invalid event type.').pipe(z.string().trim().regex(/^[a-z0-9_.:-]{1,32}$/i, 'Invalid event type.')),
69
- }),
70
- response: Ok,
71
- errors: { 400: ErrorBody, 404: ErrorBody },
72
- doc: { summary: 'Record a gameplay event', tags: ['analytics'] },
73
- },
74
- getGameAnalytics: {
75
- method: 'GET', path: '/analytics',
76
- response: Metrics.extend({ slug: Slug }),
77
- errors: { 404: ErrorBody },
78
- doc: { summary: "This game's play counts", tags: ['analytics'] },
79
- },
80
- }),
81
- };
1
+ /**
2
+ * Gameplay events and client error logs sent by games and the menu, and the
3
+ * play counts built from them. Events need no account: they carry the user id
4
+ * when signed in and always the anonymous `hub_cid` client id.
5
+ */
6
+ import { use } from '@ayepi/core';
7
+ import { z } from 'zod';
8
+ import { Ok, Slug } from '../common';
9
+ import { ErrorBody } from '../errors';
10
+ import { game, session } from '../middleware';
11
+ import { LooseText, requiredText } from '../coerce';
12
+
13
+ /** Play counts for one game or the whole catalog. A player is an account, else a client id. */
14
+ export const Metrics = z.object({
15
+ /** `play` events recorded. */
16
+ totalPlays: z.number().int().nonnegative(),
17
+ /** Distinct players who sent any event. */
18
+ uniquePlayers: z.number().int().nonnegative(),
19
+ /** Distinct players with a `play` or `heartbeat` in the last 5 minutes (`HUB_ACTIVE_WINDOW_MS`). */
20
+ activeNow: z.number().int().nonnegative(),
21
+ });
22
+
23
+ /** Play counts. */
24
+ export type Metrics = z.infer<typeof Metrics>;
25
+
26
+ /** The analytics endpoints. */
27
+ export const analyticsEndpoints = {
28
+ ...session.group({
29
+ getSiteAnalytics: {
30
+ method: 'GET', path: '/analytics',
31
+ response: z.object({
32
+ global: Metrics,
33
+ games: z.array(Metrics.extend({ slug: Slug, title: z.string() })),
34
+ }),
35
+ doc: { summary: 'Catalog-wide play counts with a per-game breakdown', tags: ['analytics'] },
36
+ },
37
+ /**
38
+ * Report a client-side failure (for example the social runtime failing
39
+ * to load) to the admin console. Open to signed-out visitors; limited to
40
+ * 20 an hour per IP, after which it answers `{ ok: false }` and drops the
41
+ * report. Fields are clipped, never rejected.
42
+ */
43
+ reportClientError: {
44
+ method: 'POST', path: '/client-log',
45
+ body: z.object({
46
+ /** Which part failed, up to 40 characters (`unknown` when empty). */
47
+ where: LooseText.optional(),
48
+ /** Up to 300 characters. */
49
+ message: LooseText.optional(),
50
+ /** Up to 1200 characters. */
51
+ stack: LooseText.optional(),
52
+ /** The page, up to 200 characters. */
53
+ url: LooseText.optional(),
54
+ }),
55
+ response: z.object({
56
+ /** `false` when the report was dropped by the rate limit. */
57
+ ok: z.boolean(),
58
+ }),
59
+ doc: { summary: 'Report a client error to the admin console', tags: ['analytics'] },
60
+ },
61
+ }),
62
+ ...use(session, game).path('/games/:slug').group({
63
+ /** Record one event. The menu sends `play` on load and periodic `heartbeat`s; games may add their own. */
64
+ recordGameEvent: {
65
+ method: 'POST', path: '/events',
66
+ body: z.object({
67
+ /** 1 to 32 of letters, digits and `_ . : -` (surrounding spaces are trimmed). */
68
+ type: requiredText('Invalid event type.').pipe(z.string().trim().regex(/^[a-z0-9_.:-]{1,32}$/i, 'Invalid event type.')),
69
+ }),
70
+ response: Ok,
71
+ errors: { 400: ErrorBody, 404: ErrorBody },
72
+ doc: { summary: 'Record a gameplay event', tags: ['analytics'] },
73
+ },
74
+ getGameAnalytics: {
75
+ method: 'GET', path: '/analytics',
76
+ response: Metrics.extend({ slug: Slug }),
77
+ errors: { 404: ErrorBody },
78
+ doc: { summary: "This game's play counts", tags: ['analytics'] },
79
+ },
80
+ }),
81
+ };
package/src/api/auth.ts CHANGED
@@ -1,77 +1,77 @@
1
- /**
2
- * Accounts and sessions: guests, claiming, sign-up, log-in, log-out, and the
3
- * current user (`/me`). Sessions ride the `hub_session` cookie.
4
- */
5
- import { z } from 'zod';
6
- import { Ok, Slug } from '../common';
7
- import { AuthUser, SessionUser } from '../entities';
8
- import { ContentRejected, ErrorBody } from '../errors';
9
- import { requireUser, session } from '../middleware';
10
-
11
- /**
12
- * A credential field, read like the hub's `String(x ?? '')`: a missing field
13
- * is empty text (the hub then answers with its own rule, or a failed log-in)
14
- * and a number becomes text. Clients should send a string.
15
- */
16
- function credentialText(message: string) {
17
- return z.union([z.string(), z.number()], { error: message }).nullish().transform((value) => String(value ?? ''));
18
- }
19
-
20
- /** Username + password, as the sign-up, claim and log-in forms send them. */
21
- export const Credentials = z.object({
22
- /** 2 to 24 characters: letters, digits, `_`, `-` and spaces. */
23
- username: credentialText('Username can use letters, numbers, _, -, and spaces.'),
24
- /** At least 6 characters. */
25
- password: credentialText('Password must be at least 6 characters.'),
26
- });
27
-
28
- /** `{ user }`: the session's account after an auth call. */
29
- export const AuthResult = z.object({ user: AuthUser });
30
-
31
- /** A 400 from sign-up or claim: a bad name or password, or a name the filter rejects. */
32
- const CredentialsError = z.union([ContentRejected, ErrorBody]);
33
-
34
- /** The auth endpoints. */
35
- export const authEndpoints = {
36
- ...session.group({
37
- /**
38
- * Make sure a session exists. Idempotent: returns the current user if
39
- * signed in (the full {@link SessionUser}), else creates a guest.
40
- */
41
- ensureGuestSession: {
42
- method: 'POST', path: '/auth/guest',
43
- response: z.object({ user: z.union([SessionUser, AuthUser]) }),
44
- doc: { summary: 'Ensure a session (creates a guest if needed)', tags: ['auth'] },
45
- },
46
- signUpWithPassword: {
47
- method: 'POST', path: '/auth/signup', body: Credentials, response: AuthResult,
48
- errors: { 400: CredentialsError, 409: ErrorBody, 429: ErrorBody },
49
- doc: { summary: 'Create an account and sign in', tags: ['auth'] },
50
- },
51
- logInWithPassword: {
52
- method: 'POST', path: '/auth/login', body: Credentials, response: AuthResult,
53
- errors: { 401: ErrorBody, 429: ErrorBody },
54
- doc: { summary: 'Sign in; a guest session on this device is merged into the account', tags: ['auth'] },
55
- },
56
- logOut: {
57
- method: 'POST', path: '/auth/logout', response: Ok,
58
- doc: { summary: 'Sign out and clear the session cookie', tags: ['auth'] },
59
- },
60
- getCurrentUser: {
61
- method: 'GET', path: '/me',
62
- response: z.object({
63
- user: SessionUser.nullable(),
64
- /** Games the user has a profile in. */
65
- profiles: z.array(z.object({ game: Slug, displayName: z.string().nullable() })),
66
- }),
67
- doc: { summary: 'The current user (or null) and their game profiles', tags: ['auth'] },
68
- },
69
- }),
70
- ...requireUser.group({
71
- claimGuestAccount: {
72
- method: 'POST', path: '/auth/claim', body: Credentials, response: AuthResult,
73
- errors: { 400: CredentialsError, 401: ErrorBody, 409: ErrorBody, 429: ErrorBody },
74
- doc: { summary: 'Turn the current guest into an account, keeping its progress', tags: ['auth'] },
75
- },
76
- }),
77
- };
1
+ /**
2
+ * Accounts and sessions: guests, claiming, sign-up, log-in, log-out, and the
3
+ * current user (`/me`). Sessions ride the `hub_session` cookie.
4
+ */
5
+ import { z } from 'zod';
6
+ import { Ok, Slug } from '../common';
7
+ import { AuthUser, SessionUser } from '../entities';
8
+ import { ContentRejected, ErrorBody } from '../errors';
9
+ import { requireUser, session } from '../middleware';
10
+
11
+ /**
12
+ * A credential field, read like the hub's `String(x ?? '')`: a missing field
13
+ * is empty text (the hub then answers with its own rule, or a failed log-in)
14
+ * and a number becomes text. Clients should send a string.
15
+ */
16
+ function credentialText(message: string) {
17
+ return z.union([z.string(), z.number()], { error: message }).nullish().transform((value) => String(value ?? ''));
18
+ }
19
+
20
+ /** Username + password, as the sign-up, claim and log-in forms send them. */
21
+ export const Credentials = z.object({
22
+ /** 2 to 24 characters: letters, digits, `_`, `-` and spaces. */
23
+ username: credentialText('Username can use letters, numbers, _, -, and spaces.'),
24
+ /** At least 6 characters. */
25
+ password: credentialText('Password must be at least 6 characters.'),
26
+ });
27
+
28
+ /** `{ user }`: the session's account after an auth call. */
29
+ export const AuthResult = z.object({ user: AuthUser });
30
+
31
+ /** A 400 from sign-up or claim: a bad name or password, or a name the filter rejects. */
32
+ const CredentialsError = z.union([ContentRejected, ErrorBody]);
33
+
34
+ /** The auth endpoints. */
35
+ export const authEndpoints = {
36
+ ...session.group({
37
+ /**
38
+ * Make sure a session exists. Idempotent: returns the current user if
39
+ * signed in (the full {@link SessionUser}), else creates a guest.
40
+ */
41
+ ensureGuestSession: {
42
+ method: 'POST', path: '/auth/guest',
43
+ response: z.object({ user: z.union([SessionUser, AuthUser]) }),
44
+ doc: { summary: 'Ensure a session (creates a guest if needed)', tags: ['auth'] },
45
+ },
46
+ signUpWithPassword: {
47
+ method: 'POST', path: '/auth/signup', body: Credentials, response: AuthResult,
48
+ errors: { 400: CredentialsError, 409: ErrorBody, 429: ErrorBody },
49
+ doc: { summary: 'Create an account and sign in', tags: ['auth'] },
50
+ },
51
+ logInWithPassword: {
52
+ method: 'POST', path: '/auth/login', body: Credentials, response: AuthResult,
53
+ errors: { 401: ErrorBody, 429: ErrorBody },
54
+ doc: { summary: 'Sign in; a guest session on this device is merged into the account', tags: ['auth'] },
55
+ },
56
+ logOut: {
57
+ method: 'POST', path: '/auth/logout', response: Ok,
58
+ doc: { summary: 'Sign out and clear the session cookie', tags: ['auth'] },
59
+ },
60
+ getCurrentUser: {
61
+ method: 'GET', path: '/me',
62
+ response: z.object({
63
+ user: SessionUser.nullable(),
64
+ /** Games the user has a profile in. */
65
+ profiles: z.array(z.object({ game: Slug, displayName: z.string().nullable() })),
66
+ }),
67
+ doc: { summary: 'The current user (or null) and their game profiles', tags: ['auth'] },
68
+ },
69
+ }),
70
+ ...requireUser.group({
71
+ claimGuestAccount: {
72
+ method: 'POST', path: '/auth/claim', body: Credentials, response: AuthResult,
73
+ errors: { 400: CredentialsError, 401: ErrorBody, 409: ErrorBody, 429: ErrorBody },
74
+ doc: { summary: 'Turn the current guest into an account, keeping its progress', tags: ['auth'] },
75
+ },
76
+ }),
77
+ };