@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
package/package.json
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@diffenderfer-games/hub-contract",
|
|
3
|
-
"version": "2.0.0-next.
|
|
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
|
+
}
|
package/src/api/analytics.ts
CHANGED
|
@@ -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
|
+
};
|