@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.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -21
- package/README.md +26 -26
- package/dist/index.d.ts +95 -95
- package/package.json +53 -53
- package/src/api/analytics.ts +81 -81
- package/src/api/auth.ts +77 -77
- package/src/api/catalog.ts +171 -171
- package/src/api/daily.ts +122 -122
- package/src/api/economy/bundle.ts +69 -69
- package/src/api/index.ts +85 -85
- package/src/api/leaderboards.ts +151 -151
- package/src/api/messages.ts +142 -142
- package/src/api/mp/chat.ts +36 -36
- package/src/api/mp/races.ts +211 -211
- package/src/api/mp/rooms.ts +90 -90
- package/src/api/mp/snapshot.ts +17 -17
- package/src/api/mp.ts +110 -110
- package/src/api/notify.ts +111 -111
- package/src/api/progress/game-data.ts +189 -189
- package/src/api/progress.ts +59 -59
- package/src/api/race.ts +47 -47
- package/src/api/reports.ts +208 -208
- package/src/api/social/config.ts +47 -47
- package/src/api/social/endpoints.ts +179 -179
- package/src/api/social/people.ts +118 -118
- package/src/api/social/presence.ts +95 -95
- package/src/api/social.ts +9 -9
- package/src/api/version.ts +40 -40
- package/src/coerce.ts +50 -50
- package/src/common.ts +42 -42
- package/src/entities.ts +62 -62
- package/src/errors/codes/core.ts +10 -10
- package/src/errors/codes/multiplayer.ts +43 -43
- package/src/errors/codes/play.ts +5 -5
- package/src/errors/codes/social.ts +31 -31
- package/src/errors/index.ts +88 -88
- package/src/index.ts +16 -16
- package/src/limits.ts +100 -100
- package/src/logic/daily.ts +54 -54
- package/src/logic/index.ts +8 -8
- package/src/logic/presence.ts +62 -62
- package/src/logic/time.ts +81 -81
- package/src/manifest/changes.ts +55 -55
- package/src/manifest/controls.ts +42 -42
- package/src/manifest/fields.ts +89 -89
- package/src/manifest/game.ts +132 -132
- package/src/manifest/index.ts +12 -12
- package/src/manifest/inject.ts +31 -31
- package/src/manifest/leaderboards.ts +29 -29
- package/src/manifest/multiplayer.ts +71 -71
- package/src/manifest/race.ts +93 -93
- package/src/middleware.ts +47 -47
- package/src/realtime/define.ts +52 -52
- package/src/realtime/envelope.ts +67 -67
- package/src/realtime/events/multiplayer.ts +8 -8
- package/src/realtime/events/parties.ts +31 -31
- package/src/realtime/events/races.ts +27 -27
- package/src/realtime/events/rooms.ts +48 -48
- package/src/realtime/events/social.ts +81 -81
- package/src/realtime/index.ts +24 -24
- package/src/realtime/ops/empty.ts +8 -8
- package/src/realtime/ops/hello.ts +40 -40
- package/src/realtime/ops/multiplayer.ts +8 -8
- package/src/realtime/ops/parties.ts +13 -13
- package/src/realtime/ops/races.ts +99 -99
- package/src/realtime/ops/rooms.ts +1 -1
- package/src/realtime/ops/social.ts +58 -58
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `game.multiplayer`: the opt-in that makes a game an online invite/join
|
|
3
|
-
* target (docs/multiplayer.md §7).
|
|
4
|
-
*/
|
|
5
|
-
import { z } from 'zod';
|
|
6
|
-
import { MANIFEST_LIMITS } from '../limits';
|
|
7
|
-
import { LobbySuffix, hasUniqueKeys, playerRange, trimmedText } from './fields';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* How players connect: `hub-rooms` (the hub relays room messages) or
|
|
11
|
-
* `own-server` (a process app runs its own game server; process apps only).
|
|
12
|
-
*/
|
|
13
|
-
export const Transport = z.enum(['hub-rooms', 'own-server']);
|
|
14
|
-
|
|
15
|
-
/** A mode key: letters, digits, `_` or `-`, 1 to 32 characters. */
|
|
16
|
-
export const ModeKey = z.string().regex(/^[A-Za-z0-9_-]{1,32}$/, 'must be 1-32 letters, digits, _ or -');
|
|
17
|
-
|
|
18
|
-
/** A game mode players can be invited into. */
|
|
19
|
-
export const MultiplayerMode = z.strictObject({
|
|
20
|
-
/** Sent with invites and launches as `mode`. */
|
|
21
|
-
key: ModeKey,
|
|
22
|
-
/** What players see. Defaults to the key. */
|
|
23
|
-
title: trimmedText(MANIFEST_LIMITS.modeTitleMax).optional(),
|
|
24
|
-
/** `[min, max]` players for this mode, within 1..8. Defaults to the game's. */
|
|
25
|
-
players: playerRange(1).optional(),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* A soften-list word this game's chat may keep (e.g. `shot` for a pool game):
|
|
30
|
-
* one lowercase word of 2 to 20 letters. Reject-list words never pass.
|
|
31
|
-
*/
|
|
32
|
-
export const ChatAllowWord = z.string().trim().toLowerCase().regex(/^[a-z]{2,20}$/, 'must be a single word of 2-20 letters');
|
|
33
|
-
|
|
34
|
-
/** The `game.multiplayer` block. */
|
|
35
|
-
export const GameMultiplayer = z.strictObject({
|
|
36
|
-
/** URL suffix invites and Join open, relative to `/<slug>/`. Default `''` (the game's root). */
|
|
37
|
-
lobby: LobbySuffix.default(''),
|
|
38
|
-
/** `[min, max]` players, within 1..8. Default `[2, 8]`. */
|
|
39
|
-
players: playerRange(1).default([2, 8]),
|
|
40
|
-
/**
|
|
41
|
-
* `hub-rooms` or `own-server`. Default: `own-server` for process apps,
|
|
42
|
-
* `hub-rooms` for static apps (which cannot use `own-server`).
|
|
43
|
-
*/
|
|
44
|
-
transport: Transport.optional(),
|
|
45
|
-
/** Friends can invite each other into this game. Default `true`. */
|
|
46
|
-
invites: z.boolean().default(true),
|
|
47
|
-
/** Friends can join a game in progress. Default `true`. */
|
|
48
|
-
join: z.boolean().default(true),
|
|
49
|
-
/** Friends can watch. Default `false`. */
|
|
50
|
-
spectate: z.boolean().default(false),
|
|
51
|
-
/** Offer quick match. Default `false`. */
|
|
52
|
-
quickMatch: z.boolean().default(false),
|
|
53
|
-
/** Modes players can be invited into (at most 16, unique keys). */
|
|
54
|
-
modes: z.array(MultiplayerMode).max(MANIFEST_LIMITS.modesMax).refine(hasUniqueKeys, 'mode keys must be unique').default([]),
|
|
55
|
-
/**
|
|
56
|
-
* Extra quick-chat phrases for this game (at most 24, each at most 40
|
|
57
|
-
* characters). Checked by the content filter at boot; failing ones are dropped.
|
|
58
|
-
*/
|
|
59
|
-
quickChat: z.array(trimmedText(MANIFEST_LIMITS.quickChatTextMax)).max(MANIFEST_LIMITS.quickChatMax).default([]),
|
|
60
|
-
/** Soften-list words this game's chat may keep (at most 20). */
|
|
61
|
-
chatAllow: z.array(ChatAllowWord).max(MANIFEST_LIMITS.chatAllowMax).default([]),
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
/** A normalised `game.multiplayer` block. */
|
|
65
|
-
export type GameMultiplayer = z.output<typeof GameMultiplayer>;
|
|
66
|
-
|
|
67
|
-
/** `game.multiplayer` for a static app: `own-server` needs a process app. */
|
|
68
|
-
export const StaticGameMultiplayer = GameMultiplayer.refine((multiplayer) => multiplayer.transport !== 'own-server', {
|
|
69
|
-
message: 'own-server needs a process app; static apps use hub-rooms',
|
|
70
|
-
path: ['transport'],
|
|
71
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* `game.multiplayer`: the opt-in that makes a game an online invite/join
|
|
3
|
+
* target (docs/multiplayer.md §7).
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { MANIFEST_LIMITS } from '../limits';
|
|
7
|
+
import { LobbySuffix, hasUniqueKeys, playerRange, trimmedText } from './fields';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* How players connect: `hub-rooms` (the hub relays room messages) or
|
|
11
|
+
* `own-server` (a process app runs its own game server; process apps only).
|
|
12
|
+
*/
|
|
13
|
+
export const Transport = z.enum(['hub-rooms', 'own-server']);
|
|
14
|
+
|
|
15
|
+
/** A mode key: letters, digits, `_` or `-`, 1 to 32 characters. */
|
|
16
|
+
export const ModeKey = z.string().regex(/^[A-Za-z0-9_-]{1,32}$/, 'must be 1-32 letters, digits, _ or -');
|
|
17
|
+
|
|
18
|
+
/** A game mode players can be invited into. */
|
|
19
|
+
export const MultiplayerMode = z.strictObject({
|
|
20
|
+
/** Sent with invites and launches as `mode`. */
|
|
21
|
+
key: ModeKey,
|
|
22
|
+
/** What players see. Defaults to the key. */
|
|
23
|
+
title: trimmedText(MANIFEST_LIMITS.modeTitleMax).optional(),
|
|
24
|
+
/** `[min, max]` players for this mode, within 1..8. Defaults to the game's. */
|
|
25
|
+
players: playerRange(1).optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A soften-list word this game's chat may keep (e.g. `shot` for a pool game):
|
|
30
|
+
* one lowercase word of 2 to 20 letters. Reject-list words never pass.
|
|
31
|
+
*/
|
|
32
|
+
export const ChatAllowWord = z.string().trim().toLowerCase().regex(/^[a-z]{2,20}$/, 'must be a single word of 2-20 letters');
|
|
33
|
+
|
|
34
|
+
/** The `game.multiplayer` block. */
|
|
35
|
+
export const GameMultiplayer = z.strictObject({
|
|
36
|
+
/** URL suffix invites and Join open, relative to `/<slug>/`. Default `''` (the game's root). */
|
|
37
|
+
lobby: LobbySuffix.default(''),
|
|
38
|
+
/** `[min, max]` players, within 1..8. Default `[2, 8]`. */
|
|
39
|
+
players: playerRange(1).default([2, 8]),
|
|
40
|
+
/**
|
|
41
|
+
* `hub-rooms` or `own-server`. Default: `own-server` for process apps,
|
|
42
|
+
* `hub-rooms` for static apps (which cannot use `own-server`).
|
|
43
|
+
*/
|
|
44
|
+
transport: Transport.optional(),
|
|
45
|
+
/** Friends can invite each other into this game. Default `true`. */
|
|
46
|
+
invites: z.boolean().default(true),
|
|
47
|
+
/** Friends can join a game in progress. Default `true`. */
|
|
48
|
+
join: z.boolean().default(true),
|
|
49
|
+
/** Friends can watch. Default `false`. */
|
|
50
|
+
spectate: z.boolean().default(false),
|
|
51
|
+
/** Offer quick match. Default `false`. */
|
|
52
|
+
quickMatch: z.boolean().default(false),
|
|
53
|
+
/** Modes players can be invited into (at most 16, unique keys). */
|
|
54
|
+
modes: z.array(MultiplayerMode).max(MANIFEST_LIMITS.modesMax).refine(hasUniqueKeys, 'mode keys must be unique').default([]),
|
|
55
|
+
/**
|
|
56
|
+
* Extra quick-chat phrases for this game (at most 24, each at most 40
|
|
57
|
+
* characters). Checked by the content filter at boot; failing ones are dropped.
|
|
58
|
+
*/
|
|
59
|
+
quickChat: z.array(trimmedText(MANIFEST_LIMITS.quickChatTextMax)).max(MANIFEST_LIMITS.quickChatMax).default([]),
|
|
60
|
+
/** Soften-list words this game's chat may keep (at most 20). */
|
|
61
|
+
chatAllow: z.array(ChatAllowWord).max(MANIFEST_LIMITS.chatAllowMax).default([]),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** A normalised `game.multiplayer` block. */
|
|
65
|
+
export type GameMultiplayer = z.output<typeof GameMultiplayer>;
|
|
66
|
+
|
|
67
|
+
/** `game.multiplayer` for a static app: `own-server` needs a process app. */
|
|
68
|
+
export const StaticGameMultiplayer = GameMultiplayer.refine((multiplayer) => multiplayer.transport !== 'own-server', {
|
|
69
|
+
message: 'own-server needs a process app; static apps use hub-rooms',
|
|
70
|
+
path: ['transport'],
|
|
71
|
+
});
|
package/src/manifest/race.ts
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `game.race`: the opt-in that turns a single-player game into a race target
|
|
3
|
-
* (docs/multiplayer.md §14, docs/plans/race.md §5).
|
|
4
|
-
*/
|
|
5
|
-
import { z } from 'zod';
|
|
6
|
-
import { RaceKey, RaceParamValue as RaceValue } from '../api/mp/races';
|
|
7
|
-
import { MANIFEST_LIMITS } from '../limits';
|
|
8
|
-
import { LobbySuffix, hasUniqueKeys, playerRange, trimmedText } from './fields';
|
|
9
|
-
|
|
10
|
-
/** A race param value. */
|
|
11
|
-
type RaceValue = z.infer<typeof RaceValue>;
|
|
12
|
-
|
|
13
|
-
const RaceTitle = trimmedText(MANIFEST_LIMITS.raceTitleMax);
|
|
14
|
-
|
|
15
|
-
/** One choice of a race param: a bare value, or a value with a title. */
|
|
16
|
-
export const RaceOption = z.union([
|
|
17
|
-
RaceValue,
|
|
18
|
-
z.strictObject({
|
|
19
|
-
/** The value the game receives in `r.params[key]`. */
|
|
20
|
-
value: RaceValue,
|
|
21
|
-
/** What the creator sees. Defaults to the value. Checked like quick-chat text. */
|
|
22
|
-
title: RaceTitle.optional(),
|
|
23
|
-
}),
|
|
24
|
-
]);
|
|
25
|
-
|
|
26
|
-
/** The value of an option, bare or titled. */
|
|
27
|
-
function optionValue(option: z.output<typeof RaceOption>): RaceValue {
|
|
28
|
-
return typeof option === 'object' ? option.value : option;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** A choice the race creator makes. The server only accepts declared values. */
|
|
32
|
-
export const RaceParam = z
|
|
33
|
-
.strictObject({
|
|
34
|
-
/** The key the game reads, `r.params[key]`. */
|
|
35
|
-
key: RaceKey,
|
|
36
|
-
/** What the creator sees. Defaults to the key. Checked like quick-chat text. */
|
|
37
|
-
title: RaceTitle.optional(),
|
|
38
|
-
/** The choices, 1 to 12. */
|
|
39
|
-
options: z.array(RaceOption).min(1).max(MANIFEST_LIMITS.raceOptionsMax),
|
|
40
|
-
/** The preselected value; one of the options' values. Defaults to the first. */
|
|
41
|
-
default: RaceValue.optional(),
|
|
42
|
-
})
|
|
43
|
-
.refine((p) => p.default === undefined || p.options.some((option) => optionValue(option) === p.default), {
|
|
44
|
-
message: 'default must be one of the option values',
|
|
45
|
-
path: ['default'],
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
/** How the live race HUD and the race history show a stat. */
|
|
49
|
-
export const RaceStatFormat = z.enum(['int', 'percent', 'time', 'bool', 'color']);
|
|
50
|
-
|
|
51
|
-
/** A value of {@link RaceStatFormat}. */
|
|
52
|
-
export type RaceStatFormat = z.infer<typeof RaceStatFormat>;
|
|
53
|
-
|
|
54
|
-
/** A stat the live race HUD and the race history show. */
|
|
55
|
-
export const RaceStat = z.strictObject({
|
|
56
|
-
/** The key the game sends in `hub.race.status({ [key]: value })`. */
|
|
57
|
-
key: RaceKey,
|
|
58
|
-
/** The column label. Defaults to the key. Checked like quick-chat text. */
|
|
59
|
-
title: RaceTitle.optional(),
|
|
60
|
-
/** `int` (default), `percent`, `time` (ms), `bool` or `color` (`#rrggbb`). */
|
|
61
|
-
format: RaceStatFormat.default('int'),
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
/** The `game.race` object form. */
|
|
65
|
-
export const RaceBlock = z.strictObject({
|
|
66
|
-
/** URL suffix race invites and Join open, relative to `/<slug>/`. Default `?race=lobby`. */
|
|
67
|
-
lobby: LobbySuffix.default('?race=lobby'),
|
|
68
|
-
/** `[min, max]` racers, within 2..8. Default `[2, 2]`. */
|
|
69
|
-
players: playerRange(2).default([2, 2]),
|
|
70
|
-
/** What the creator picks (at most 6, unique keys). */
|
|
71
|
-
params: z.array(RaceParam).max(MANIFEST_LIMITS.raceParamsMax).refine(hasUniqueKeys, 'param keys must be unique').default([]),
|
|
72
|
-
/** What the live HUD and the history show (at most 8, unique keys; the HUD shows the first 3). */
|
|
73
|
-
stats: z.array(RaceStat).max(MANIFEST_LIMITS.raceStatsMax).refine(hasUniqueKeys, 'stat keys must be unique').default([]),
|
|
74
|
-
/**
|
|
75
|
-
* The earliest a finish may count after the go, in ms (0 to 600000). A
|
|
76
|
-
* sooner finish is held until then. Default: the hub's `raceMinFinishMs`.
|
|
77
|
-
*/
|
|
78
|
-
minMs: z.number().int().min(0).max(MANIFEST_LIMITS.raceMinMsMax).optional(),
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
/** A normalised `game.race` block. */
|
|
82
|
-
export type RaceBlock = z.output<typeof RaceBlock>;
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* `game.race`: an object, `true` for all defaults (no params, no stats), or
|
|
86
|
-
* `false` for off. `true` parses to the object form, `false` to `undefined`.
|
|
87
|
-
*/
|
|
88
|
-
export const GameRace = z
|
|
89
|
-
.preprocess((v: boolean | z.input<typeof RaceBlock>) => (v === true ? {} : v === false ? undefined : v), RaceBlock.optional())
|
|
90
|
-
.optional();
|
|
91
|
-
|
|
92
|
-
/** `game.race` as a game writes it. */
|
|
93
|
-
export type GameRaceInput = z.input<typeof GameRace>;
|
|
1
|
+
/**
|
|
2
|
+
* `game.race`: the opt-in that turns a single-player game into a race target
|
|
3
|
+
* (docs/multiplayer.md §14, docs/plans/race.md §5).
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { RaceKey, RaceParamValue as RaceValue } from '../api/mp/races';
|
|
7
|
+
import { MANIFEST_LIMITS } from '../limits';
|
|
8
|
+
import { LobbySuffix, hasUniqueKeys, playerRange, trimmedText } from './fields';
|
|
9
|
+
|
|
10
|
+
/** A race param value. */
|
|
11
|
+
type RaceValue = z.infer<typeof RaceValue>;
|
|
12
|
+
|
|
13
|
+
const RaceTitle = trimmedText(MANIFEST_LIMITS.raceTitleMax);
|
|
14
|
+
|
|
15
|
+
/** One choice of a race param: a bare value, or a value with a title. */
|
|
16
|
+
export const RaceOption = z.union([
|
|
17
|
+
RaceValue,
|
|
18
|
+
z.strictObject({
|
|
19
|
+
/** The value the game receives in `r.params[key]`. */
|
|
20
|
+
value: RaceValue,
|
|
21
|
+
/** What the creator sees. Defaults to the value. Checked like quick-chat text. */
|
|
22
|
+
title: RaceTitle.optional(),
|
|
23
|
+
}),
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** The value of an option, bare or titled. */
|
|
27
|
+
function optionValue(option: z.output<typeof RaceOption>): RaceValue {
|
|
28
|
+
return typeof option === 'object' ? option.value : option;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A choice the race creator makes. The server only accepts declared values. */
|
|
32
|
+
export const RaceParam = z
|
|
33
|
+
.strictObject({
|
|
34
|
+
/** The key the game reads, `r.params[key]`. */
|
|
35
|
+
key: RaceKey,
|
|
36
|
+
/** What the creator sees. Defaults to the key. Checked like quick-chat text. */
|
|
37
|
+
title: RaceTitle.optional(),
|
|
38
|
+
/** The choices, 1 to 12. */
|
|
39
|
+
options: z.array(RaceOption).min(1).max(MANIFEST_LIMITS.raceOptionsMax),
|
|
40
|
+
/** The preselected value; one of the options' values. Defaults to the first. */
|
|
41
|
+
default: RaceValue.optional(),
|
|
42
|
+
})
|
|
43
|
+
.refine((p) => p.default === undefined || p.options.some((option) => optionValue(option) === p.default), {
|
|
44
|
+
message: 'default must be one of the option values',
|
|
45
|
+
path: ['default'],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** How the live race HUD and the race history show a stat. */
|
|
49
|
+
export const RaceStatFormat = z.enum(['int', 'percent', 'time', 'bool', 'color']);
|
|
50
|
+
|
|
51
|
+
/** A value of {@link RaceStatFormat}. */
|
|
52
|
+
export type RaceStatFormat = z.infer<typeof RaceStatFormat>;
|
|
53
|
+
|
|
54
|
+
/** A stat the live race HUD and the race history show. */
|
|
55
|
+
export const RaceStat = z.strictObject({
|
|
56
|
+
/** The key the game sends in `hub.race.status({ [key]: value })`. */
|
|
57
|
+
key: RaceKey,
|
|
58
|
+
/** The column label. Defaults to the key. Checked like quick-chat text. */
|
|
59
|
+
title: RaceTitle.optional(),
|
|
60
|
+
/** `int` (default), `percent`, `time` (ms), `bool` or `color` (`#rrggbb`). */
|
|
61
|
+
format: RaceStatFormat.default('int'),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** The `game.race` object form. */
|
|
65
|
+
export const RaceBlock = z.strictObject({
|
|
66
|
+
/** URL suffix race invites and Join open, relative to `/<slug>/`. Default `?race=lobby`. */
|
|
67
|
+
lobby: LobbySuffix.default('?race=lobby'),
|
|
68
|
+
/** `[min, max]` racers, within 2..8. Default `[2, 2]`. */
|
|
69
|
+
players: playerRange(2).default([2, 2]),
|
|
70
|
+
/** What the creator picks (at most 6, unique keys). */
|
|
71
|
+
params: z.array(RaceParam).max(MANIFEST_LIMITS.raceParamsMax).refine(hasUniqueKeys, 'param keys must be unique').default([]),
|
|
72
|
+
/** What the live HUD and the history show (at most 8, unique keys; the HUD shows the first 3). */
|
|
73
|
+
stats: z.array(RaceStat).max(MANIFEST_LIMITS.raceStatsMax).refine(hasUniqueKeys, 'stat keys must be unique').default([]),
|
|
74
|
+
/**
|
|
75
|
+
* The earliest a finish may count after the go, in ms (0 to 600000). A
|
|
76
|
+
* sooner finish is held until then. Default: the hub's `raceMinFinishMs`.
|
|
77
|
+
*/
|
|
78
|
+
minMs: z.number().int().min(0).max(MANIFEST_LIMITS.raceMinMsMax).optional(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/** A normalised `game.race` block. */
|
|
82
|
+
export type RaceBlock = z.output<typeof RaceBlock>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `game.race`: an object, `true` for all defaults (no params, no stats), or
|
|
86
|
+
* `false` for off. `true` parses to the object form, `false` to `undefined`.
|
|
87
|
+
*/
|
|
88
|
+
export const GameRace = z
|
|
89
|
+
.preprocess((v: boolean | z.input<typeof RaceBlock>) => (v === true ? {} : v === false ? undefined : v), RaceBlock.optional())
|
|
90
|
+
.optional();
|
|
91
|
+
|
|
92
|
+
/** `game.race` as a game writes it. */
|
|
93
|
+
export type GameRaceInput = z.input<typeof GameRace>;
|
package/src/middleware.ts
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Middleware **defs** for the public API (plan §6.1). A def is a contract
|
|
3
|
-
* only: a name, the context it provides and its dependencies. It holds no
|
|
4
|
-
* code, so it is safe in a published package; the server binds the real
|
|
5
|
-
* implementation. Each endpoint's guards are part of its declaration, which
|
|
6
|
-
* lets tests check rules such as "every social endpoint requires an active
|
|
7
|
-
* account" (plan §10.3).
|
|
8
|
-
*/
|
|
9
|
-
import { ctx, middleware } from '@ayepi/core';
|
|
10
|
-
import { Slug } from './common';
|
|
11
|
-
import type { SessionUser } from './entities';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Resolves the `hub_session` cookie (the user, or `null`) and the anonymous
|
|
15
|
-
* `hub_cid` client id. Every endpoint has it.
|
|
16
|
-
*/
|
|
17
|
-
export const session = middleware('session', {
|
|
18
|
-
provides: ctx<{ user: SessionUser | null; clientId: string }>(),
|
|
19
|
-
doc: { security: { cookieAuth: { type: 'apiKey', in: 'cookie', name: 'hub_session' } } },
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
/** Signed in, as a guest or an account. Otherwise 401 `signed_out`. */
|
|
23
|
-
export const requireUser = middleware('requireUser', {
|
|
24
|
-
provides: ctx<{ user: SessionUser }>(),
|
|
25
|
-
requires: [session],
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Signed in and not suspended. Otherwise 401 `signed_out` or 403
|
|
30
|
-
* `suspended`. Every social, multiplayer, race and trade endpoint has it.
|
|
31
|
-
*/
|
|
32
|
-
export const requireActive = middleware('requireActive', {
|
|
33
|
-
provides: ctx<{ user: SessionUser }>(),
|
|
34
|
-
requires: [requireUser],
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
/** Active and a claimed account (not a guest). Otherwise 403 `claim_required`. */
|
|
38
|
-
export const requireClaimed = middleware('requireClaimed', {
|
|
39
|
-
provides: ctx<{ user: SessionUser }>(),
|
|
40
|
-
requires: [requireActive],
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Owns the `:slug` path segment and checks it names a known game.
|
|
45
|
-
* Otherwise 404 "No such game".
|
|
46
|
-
*/
|
|
47
|
-
export const game = middleware.loader('slug', Slug, { requires: [session] });
|
|
1
|
+
/**
|
|
2
|
+
* Middleware **defs** for the public API (plan §6.1). A def is a contract
|
|
3
|
+
* only: a name, the context it provides and its dependencies. It holds no
|
|
4
|
+
* code, so it is safe in a published package; the server binds the real
|
|
5
|
+
* implementation. Each endpoint's guards are part of its declaration, which
|
|
6
|
+
* lets tests check rules such as "every social endpoint requires an active
|
|
7
|
+
* account" (plan §10.3).
|
|
8
|
+
*/
|
|
9
|
+
import { ctx, middleware } from '@ayepi/core';
|
|
10
|
+
import { Slug } from './common';
|
|
11
|
+
import type { SessionUser } from './entities';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolves the `hub_session` cookie (the user, or `null`) and the anonymous
|
|
15
|
+
* `hub_cid` client id. Every endpoint has it.
|
|
16
|
+
*/
|
|
17
|
+
export const session = middleware('session', {
|
|
18
|
+
provides: ctx<{ user: SessionUser | null; clientId: string }>(),
|
|
19
|
+
doc: { security: { cookieAuth: { type: 'apiKey', in: 'cookie', name: 'hub_session' } } },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/** Signed in, as a guest or an account. Otherwise 401 `signed_out`. */
|
|
23
|
+
export const requireUser = middleware('requireUser', {
|
|
24
|
+
provides: ctx<{ user: SessionUser }>(),
|
|
25
|
+
requires: [session],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Signed in and not suspended. Otherwise 401 `signed_out` or 403
|
|
30
|
+
* `suspended`. Every social, multiplayer, race and trade endpoint has it.
|
|
31
|
+
*/
|
|
32
|
+
export const requireActive = middleware('requireActive', {
|
|
33
|
+
provides: ctx<{ user: SessionUser }>(),
|
|
34
|
+
requires: [requireUser],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/** Active and a claimed account (not a guest). Otherwise 403 `claim_required`. */
|
|
38
|
+
export const requireClaimed = middleware('requireClaimed', {
|
|
39
|
+
provides: ctx<{ user: SessionUser }>(),
|
|
40
|
+
requires: [requireActive],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Owns the `:slug` path segment and checks it names a known game.
|
|
45
|
+
* Otherwise 404 "No such game".
|
|
46
|
+
*/
|
|
47
|
+
export const game = middleware.loader('slug', Slug, { requires: [session] });
|
package/src/realtime/define.ts
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Definers for socket ops and events (plan §5.2). One declaration per op or
|
|
3
|
-
* event feeds the server (validation), the runtime and test clients (types)
|
|
4
|
-
* and the generated op/event tables.
|
|
5
|
-
*/
|
|
6
|
-
import type { z } from 'zod';
|
|
7
|
-
import type { ErrorCode } from '../errors';
|
|
8
|
-
|
|
9
|
-
/** A client → server op on `/_ws`: its input, its ack and the codes it can fail with. */
|
|
10
|
-
export interface OpDef<D extends z.ZodType = z.ZodType, A extends z.ZodType = z.ZodType> {
|
|
11
|
-
/** The op's input (`d` in `{t:'op', id, op, d}`). */
|
|
12
|
-
readonly d: D;
|
|
13
|
-
/** The success ack's payload (`d` in `{t:'ack', ok:true, d}`). */
|
|
14
|
-
readonly ack: A;
|
|
15
|
-
/** Error codes this op can fail with, beyond the envelope's own (see `ENVELOPE_ERRORS`). */
|
|
16
|
-
readonly errors: readonly ErrorCode[];
|
|
17
|
-
/**
|
|
18
|
-
* The refusal a malformed `d` gets (status 400), when the hub names it
|
|
19
|
-
* (`bad_stats`, `bad_patch`). Without it the refusal is `bad_request`
|
|
20
|
-
* with the first validation message.
|
|
21
|
-
*/
|
|
22
|
-
readonly invalid?: { readonly code: ErrorCode; readonly error: string };
|
|
23
|
-
/** One line for the generated op table. */
|
|
24
|
-
readonly doc: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** A server → client event on `/_ws`. */
|
|
28
|
-
export interface EventDef<D extends z.ZodType = z.ZodType> {
|
|
29
|
-
/** The payload (`d` in `{t:'ev', ev, d}`). */
|
|
30
|
-
readonly d: D;
|
|
31
|
-
/** One line for the generated event table. */
|
|
32
|
-
readonly doc: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Declare an op (identity at runtime; keeps the literal types). */
|
|
36
|
-
export function op<const D extends z.ZodType, const A extends z.ZodType>(def: OpDef<D, A>): OpDef<D, A> {
|
|
37
|
-
return def;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Declare an event (identity at runtime; keeps the literal types). */
|
|
41
|
-
export function event<const D extends z.ZodType>(def: EventDef<D>): EventDef<D> {
|
|
42
|
-
return def;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** The input type of an op. */
|
|
46
|
-
export type OpInput<O extends OpDef> = z.input<O['d']>;
|
|
47
|
-
|
|
48
|
-
/** The ack payload type of an op. */
|
|
49
|
-
export type OpAck<O extends OpDef> = z.output<O['ack']>;
|
|
50
|
-
|
|
51
|
-
/** The payload type of an event. */
|
|
52
|
-
export type EventData<E extends EventDef> = z.output<E['d']>;
|
|
1
|
+
/**
|
|
2
|
+
* Definers for socket ops and events (plan §5.2). One declaration per op or
|
|
3
|
+
* event feeds the server (validation), the runtime and test clients (types)
|
|
4
|
+
* and the generated op/event tables.
|
|
5
|
+
*/
|
|
6
|
+
import type { z } from 'zod';
|
|
7
|
+
import type { ErrorCode } from '../errors';
|
|
8
|
+
|
|
9
|
+
/** A client → server op on `/_ws`: its input, its ack and the codes it can fail with. */
|
|
10
|
+
export interface OpDef<D extends z.ZodType = z.ZodType, A extends z.ZodType = z.ZodType> {
|
|
11
|
+
/** The op's input (`d` in `{t:'op', id, op, d}`). */
|
|
12
|
+
readonly d: D;
|
|
13
|
+
/** The success ack's payload (`d` in `{t:'ack', ok:true, d}`). */
|
|
14
|
+
readonly ack: A;
|
|
15
|
+
/** Error codes this op can fail with, beyond the envelope's own (see `ENVELOPE_ERRORS`). */
|
|
16
|
+
readonly errors: readonly ErrorCode[];
|
|
17
|
+
/**
|
|
18
|
+
* The refusal a malformed `d` gets (status 400), when the hub names it
|
|
19
|
+
* (`bad_stats`, `bad_patch`). Without it the refusal is `bad_request`
|
|
20
|
+
* with the first validation message.
|
|
21
|
+
*/
|
|
22
|
+
readonly invalid?: { readonly code: ErrorCode; readonly error: string };
|
|
23
|
+
/** One line for the generated op table. */
|
|
24
|
+
readonly doc: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A server → client event on `/_ws`. */
|
|
28
|
+
export interface EventDef<D extends z.ZodType = z.ZodType> {
|
|
29
|
+
/** The payload (`d` in `{t:'ev', ev, d}`). */
|
|
30
|
+
readonly d: D;
|
|
31
|
+
/** One line for the generated event table. */
|
|
32
|
+
readonly doc: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Declare an op (identity at runtime; keeps the literal types). */
|
|
36
|
+
export function op<const D extends z.ZodType, const A extends z.ZodType>(def: OpDef<D, A>): OpDef<D, A> {
|
|
37
|
+
return def;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Declare an event (identity at runtime; keeps the literal types). */
|
|
41
|
+
export function event<const D extends z.ZodType>(def: EventDef<D>): EventDef<D> {
|
|
42
|
+
return def;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The input type of an op. */
|
|
46
|
+
export type OpInput<O extends OpDef> = z.input<O['d']>;
|
|
47
|
+
|
|
48
|
+
/** The ack payload type of an op. */
|
|
49
|
+
export type OpAck<O extends OpDef> = z.output<O['ack']>;
|
|
50
|
+
|
|
51
|
+
/** The payload type of an event. */
|
|
52
|
+
export type EventData<E extends EventDef> = z.output<E['d']>;
|