xpt-shared-types 1.16.0 → 1.19.0

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.
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ /**
3
+ * Player bans — the contract between `utils/playerBan.ts` in xpt-strapi and
4
+ * the ban dialog, ban list and host-bans page in xpt-client.
5
+ *
6
+ * Two scopes (`PlayerBanScope`, generated): **tournament** keeps a player out
7
+ * of one event and is set by its owner or an admin; **host** keeps them out
8
+ * of every tournament one host owns, now and later, and only that host sets
9
+ * it. The platform level is the account block, not a ban.
10
+ *
11
+ * A ban is active until it is lifted or its `expiresAt` passes; `null` is
12
+ * permanent. Lifted bans are kept, so a list can show history.
13
+ *
14
+ * Routes:
15
+ * - `GET|POST /tournaments/:id/bans`, `DELETE /tournaments/:id/bans/:userId`
16
+ * - `GET|POST /host-bans`, `DELETE /host-bans/:userId`
17
+ * Both GETs take `?includeLifted=true`.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.BAN_DURATIONS = exports.MAX_BAN_REASON_LENGTH = void 0;
21
+ exports.banExpiryFor = banExpiryFor;
22
+ exports.isBanActive = isBanActive;
23
+ exports.MAX_BAN_REASON_LENGTH = 500;
24
+ /** The durations the ban dialog offers. */
25
+ exports.BAN_DURATIONS = [
26
+ { key: '1d', label: '24 hours', days: 1 },
27
+ { key: '7d', label: '7 days', days: 7 },
28
+ { key: '30d', label: '30 days', days: 30 },
29
+ { key: 'permanent', label: 'Permanent', days: null },
30
+ ];
31
+ /** `expiresAt` for a duration, or null for permanent. */
32
+ function banExpiryFor(key, now = new Date()) {
33
+ var _a, _b;
34
+ const days = (_b = (_a = exports.BAN_DURATIONS.find((d) => d.key === key)) === null || _a === void 0 ? void 0 : _a.days) !== null && _b !== void 0 ? _b : null;
35
+ return days == null
36
+ ? null
37
+ : new Date(now.getTime() + days * 86400000).toISOString();
38
+ }
39
+ function isBanActive(ban, now = new Date()) {
40
+ if (ban.liftedAt)
41
+ return false;
42
+ if (!ban.expiresAt)
43
+ return true;
44
+ return new Date(ban.expiresAt).getTime() > now.getTime();
45
+ }
@@ -36,7 +36,9 @@ export type TournamentStaffAction =
36
36
  /** Record scores, start matches, edit match details. */
37
37
  | 'manageMatches'
38
38
  /** Watch any lobby and post as staff in its chat. */
39
- | 'enterLobby';
39
+ | 'enterLobby'
40
+ /** Ban players from this tournament and lift those bans. */
41
+ | 'manageBans';
40
42
  export declare const TOURNAMENT_STAFF_CAPABILITIES: Record<TournamentStaffAction, readonly TournamentStaffRole[]>;
41
43
  export declare const TOURNAMENT_STAFF_ROLE_LABELS: Record<TournamentStaffRole, string>;
42
44
  export declare const tournamentStaffRoleLabel: (role: string | null | undefined) => string;
@@ -36,6 +36,7 @@ exports.TOURNAMENT_STAFF_CAPABILITIES = {
36
36
  reviewJoinRequests: ADMIN_UP,
37
37
  resolveDispute: ADMIN_UP,
38
38
  seedBracket: ADMIN_UP,
39
+ manageBans: ADMIN_UP,
39
40
  manageMatches: ALL_STAFF,
40
41
  enterLobby: ALL_STAFF,
41
42
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xpt-shared-types",
3
- "version": "1.16.0",
3
+ "version": "1.19.0",
4
4
  "description": "Shared types and data for XPT projects",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -22,6 +22,28 @@ export type FriendshipStatus =
22
22
  | "pending"
23
23
  | "accepted";
24
24
 
25
+ /** `GameAccount.code` */
26
+ export type GameAccountCode =
27
+ | "steam"
28
+ | "psn"
29
+ | "xbox"
30
+ | "battlenet"
31
+ | "riot"
32
+ | "epic"
33
+ | "ea"
34
+ | "activision"
35
+ | "ubisoft"
36
+ | "nintendo"
37
+ | "fightcade"
38
+ | "apple"
39
+ | "googleplay";
40
+
41
+ /** `GameAccount.verification` */
42
+ export type GameAccountVerification =
43
+ | "none"
44
+ | "format"
45
+ | "linked";
46
+
25
47
  /** `Game.genre` */
26
48
  export type GameGenre =
27
49
  | "FPS"
@@ -126,6 +148,11 @@ export type PlatformCode =
126
148
  | "nintendo"
127
149
  | "mobile";
128
150
 
151
+ /** `PlayerBan.scope` */
152
+ export type PlayerBanScope =
153
+ | "tournament"
154
+ | "host";
155
+
129
156
  /** `Referral.status` */
130
157
  export type ReferralStatus =
131
158
  | "pending"
@@ -228,6 +255,10 @@ export type TournamentVisibility =
228
255
  | "public"
229
256
  | "unlisted";
230
257
 
258
+ /** `UserGameAccount.verificationMethod` */
259
+ export type UserGameAccountVerificationMethod =
260
+ | "linked";
261
+
231
262
  /** `UserTransaction.stripeStatus` */
232
263
  export type UserTransactionStripeStatus =
233
264
  | "pending"
@@ -11,6 +11,8 @@ import type {
11
11
  AdminActionTargetType,
12
12
  FaqCategory,
13
13
  FriendshipStatus,
14
+ GameAccountCode,
15
+ GameAccountVerification,
14
16
  GameGenre,
15
17
  GameRequestCustomLobbies,
16
18
  GameRequestGenre,
@@ -23,6 +25,7 @@ import type {
23
25
  MatchStreamPlatform,
24
26
  MatchWinnerSlot,
25
27
  PlatformCode,
28
+ PlayerBanScope,
26
29
  ReferralStatus,
27
30
  TeamCurrentStatus,
28
31
  TeamInviteStatus,
@@ -39,6 +42,7 @@ import type {
39
42
  TournamentTeamSize,
40
43
  TournamentType,
41
44
  TournamentVisibility,
45
+ UserGameAccountVerificationMethod,
42
46
  UserTransactionStripeStatus,
43
47
  UserTransactionType,
44
48
  } from './enums';
@@ -126,6 +130,8 @@ export interface GameInput {
126
130
  /** Write payload for `game-account`. */
127
131
  export interface GameAccountInput {
128
132
  name?: string;
133
+ code?: GameAccountCode;
134
+ verification?: GameAccountVerification;
129
135
  tournaments?: RelationInput | RelationInput[];
130
136
  user_game_accounts?: RelationInput | RelationInput[];
131
137
  imgThumb?: MediaInput;
@@ -133,6 +139,15 @@ export interface GameAccountInput {
133
139
  platforms?: RelationInput | RelationInput[];
134
140
  }
135
141
 
142
+ /** Write payload for `game-account-link`. */
143
+ export interface GameAccountLinkInput {
144
+ token?: string;
145
+ users_permissions_user?: RelationInput;
146
+ game_account?: RelationInput;
147
+ expiresAt?: string;
148
+ returnTo?: string;
149
+ }
150
+
136
151
  /** Write payload for `game-request`. */
137
152
  export interface GameRequestInput {
138
153
  gameTitle?: string;
@@ -247,6 +262,19 @@ export interface PlatformInput {
247
262
  game_accounts?: RelationInput | RelationInput[];
248
263
  }
249
264
 
265
+ /** Write payload for `player-ban`. */
266
+ export interface PlayerBanInput {
267
+ scope?: PlayerBanScope;
268
+ tournament?: RelationInput;
269
+ host?: RelationInput;
270
+ users_permissions_user?: RelationInput;
271
+ bannedBy?: RelationInput;
272
+ reason?: string;
273
+ expiresAt?: string;
274
+ liftedAt?: string;
275
+ liftedBy?: RelationInput;
276
+ }
277
+
250
278
  /** Write payload for `preset-avatar`. */
251
279
  export interface PresetAvatarInput {
252
280
  image?: MediaInput;
@@ -494,6 +522,9 @@ export interface UserInput {
494
522
  export interface UserGameAccountInput {
495
523
  accountId?: string;
496
524
  isPrivate?: boolean;
525
+ verifiedAt?: string;
526
+ verificationMethod?: UserGameAccountVerificationMethod;
527
+ providerAccountId?: string;
497
528
  users_permissions_user?: RelationInput;
498
529
  game_account?: RelationInput;
499
530
  }
@@ -12,6 +12,8 @@ import type {
12
12
  AdminActionTargetType,
13
13
  FaqCategory,
14
14
  FriendshipStatus,
15
+ GameAccountCode,
16
+ GameAccountVerification,
15
17
  GameGenre,
16
18
  GameRequestCustomLobbies,
17
19
  GameRequestGenre,
@@ -24,6 +26,7 @@ import type {
24
26
  MatchStreamPlatform,
25
27
  MatchWinnerSlot,
26
28
  PlatformCode,
29
+ PlayerBanScope,
27
30
  ReferralStatus,
28
31
  TeamCurrentStatus,
29
32
  TeamInviteStatus,
@@ -40,6 +43,7 @@ import type {
40
43
  TournamentTeamSize,
41
44
  TournamentType,
42
45
  TournamentVisibility,
46
+ UserGameAccountVerificationMethod,
43
47
  UserTransactionStripeStatus,
44
48
  UserTransactionType,
45
49
  } from './enums';
@@ -127,6 +131,8 @@ export interface Game extends StrapiDocument {
127
131
  /** `game-account` */
128
132
  export interface GameAccount extends StrapiDocument {
129
133
  name?: string | null;
134
+ code?: GameAccountCode | null;
135
+ verification?: GameAccountVerification | null;
130
136
  tournaments?: Tournament[];
131
137
  user_game_accounts?: UserGameAccount[];
132
138
  imgThumb?: StrapiMedia | null;
@@ -134,6 +140,15 @@ export interface GameAccount extends StrapiDocument {
134
140
  platforms?: Platform[];
135
141
  }
136
142
 
143
+ /** `game-account-link` */
144
+ export interface GameAccountLink extends StrapiDocument {
145
+ token?: string | null;
146
+ users_permissions_user?: User | null;
147
+ game_account?: GameAccount | null;
148
+ expiresAt?: string | null;
149
+ returnTo?: string | null;
150
+ }
151
+
137
152
  /** `game-request` */
138
153
  export interface GameRequest extends StrapiDocument {
139
154
  gameTitle?: string | null;
@@ -248,6 +263,19 @@ export interface Platform extends StrapiDocument {
248
263
  game_accounts?: GameAccount[];
249
264
  }
250
265
 
266
+ /** `player-ban` */
267
+ export interface PlayerBan extends StrapiDocument {
268
+ scope?: PlayerBanScope | null;
269
+ tournament?: Tournament | null;
270
+ host?: User | null;
271
+ users_permissions_user?: User | null;
272
+ bannedBy?: User | null;
273
+ reason?: string | null;
274
+ expiresAt?: string | null;
275
+ liftedAt?: string | null;
276
+ liftedBy?: User | null;
277
+ }
278
+
251
279
  /** `preset-avatar` */
252
280
  export interface PresetAvatar extends StrapiDocument {
253
281
  image?: StrapiMedia | null;
@@ -495,6 +523,9 @@ export interface User extends StrapiDocument {
495
523
  export interface UserGameAccount extends StrapiDocument {
496
524
  accountId?: string | null;
497
525
  isPrivate?: boolean | null;
526
+ verifiedAt?: string | null;
527
+ verificationMethod?: UserGameAccountVerificationMethod | null;
528
+ providerAccountId?: string | null;
498
529
  users_permissions_user?: User | null;
499
530
  game_account?: GameAccount | null;
500
531
  }
@@ -0,0 +1,306 @@
1
+ /**
2
+ * What a player's handle on each account network looks like.
3
+ *
4
+ * A handle is the one thing opponents actually exchange — the PSN ID, the
5
+ * BattleTag, the Riot ID — so a typo in it is a match that cannot start. Until
6
+ * these rules existed one regex covered every network, and it happily accepted
7
+ * `Name#1234` for PSN (where `#` is not allowed) while rejecting nothing that a
8
+ * real PSN or Xbox rule would.
9
+ *
10
+ * None of this proves ownership. A network whose handles can be linked by
11
+ * OAuth (Steam, Battle.net, Xbox) gets a `linked` tier later; this file is the
12
+ * `format` tier that every network can have today. Which tier applies is a
13
+ * property of the catalogue row (`GameAccount.verification`), set by editors,
14
+ * so a rule that turns out wrong for some network can be switched off without a
15
+ * deploy: set the row to `none` and the generic rule applies.
16
+ *
17
+ * Lives here because the settings page validates as the player types and the
18
+ * backend validates again on write. Two copies of a regex is two chances for a
19
+ * handle to pass one and fail the other.
20
+ *
21
+ * Pure on purpose: no I/O, no profanity list (each app keeps its own), so the
22
+ * rules are unit-tested directly (xpt-strapi
23
+ * `tests/unit/utils/gameAccountHandle.test.ts`).
24
+ */
25
+
26
+ import type {
27
+ GameAccountCode,
28
+ GameAccountVerification,
29
+ } from '../generated/enums';
30
+
31
+ export interface HandleRule {
32
+ /** The network this rule is for; absent on the generic fallback. */
33
+ code?: GameAccountCode;
34
+ /** What the field is called in copy: "PSN online ID", "BattleTag". */
35
+ label: string;
36
+ /** The field's placeholder. */
37
+ placeholder: string;
38
+ /** One sentence under the field saying what a valid value looks like. */
39
+ hint: string;
40
+ /** Applied to the normalized value. */
41
+ pattern: RegExp;
42
+ min: number;
43
+ max: number;
44
+ /**
45
+ * Rewrites the trimmed input before `pattern` is applied: a Steam profile
46
+ * URL down to its id, a bare Nintendo friend code into `SW-xxxx-xxxx-xxxx`.
47
+ * The result is what gets stored.
48
+ */
49
+ normalize?: (value: string) => string;
50
+ }
51
+
52
+ export type HandleCheck =
53
+ | { ok: true; value: string }
54
+ | { ok: false; message: string };
55
+
56
+ /** `error.details.name` values the game-account write endpoints raise. */
57
+ export type GameAccountErrorName =
58
+ | 'HandleInvalid'
59
+ | 'HandleTaken'
60
+ /** The row is verified by a link; unlink before changing the ID. */
61
+ | 'HandleLocked'
62
+ /** No configured link provider for this network. */
63
+ | 'LinkUnavailable'
64
+ /** The provider account is already linked to another XPT user. */
65
+ | 'LinkConflict';
66
+
67
+ /**
68
+ * Networks whose handles can be verified by signing in to the network. The
69
+ * catalogue's `verification: 'linked'` is only valid for these; whether a
70
+ * provider is actually configured on a given environment is answered at
71
+ * runtime by `GET /user-game-accounts/link/providers`.
72
+ */
73
+ export const LINK_PROVIDER_CODES = ['steam', 'battlenet'] as const satisfies
74
+ readonly GameAccountCode[];
75
+
76
+ export type LinkProviderCode = (typeof LINK_PROVIDER_CODES)[number];
77
+
78
+ export function isLinkableCode(
79
+ code: string | null | undefined
80
+ ): code is LinkProviderCode {
81
+ return (LINK_PROVIDER_CODES as readonly string[]).includes(code ?? '');
82
+ }
83
+
84
+ /**
85
+ * Why a link attempt came back without a verified account — the `linkError`
86
+ * query value the callback redirects to the settings page with.
87
+ */
88
+ export type AccountLinkError =
89
+ /** The attempt was unknown, already used, or older than its window. */
90
+ | 'expired'
91
+ /** The player cancelled at the provider. */
92
+ | 'denied'
93
+ /** The provider's answer did not verify. */
94
+ | 'invalid'
95
+ /** That provider account is already linked to another XPT user. */
96
+ | 'taken'
97
+ /** The provider is not configured, or the network is not linkable. */
98
+ | 'unavailable'
99
+ /** Accounts are frozen while the player is in a running tournament. */
100
+ | 'activeTournament';
101
+
102
+ /** True when a row's handle was proved by signing in to the network. */
103
+ export function isVerifiedAccount(
104
+ row:
105
+ | { verifiedAt?: string | Date | null; verificationMethod?: string | null }
106
+ | null
107
+ | undefined
108
+ ): boolean {
109
+ return !!row?.verifiedAt && row.verificationMethod === 'linked';
110
+ }
111
+
112
+ /**
113
+ * The rule every network gets when it has no stricter one. Also the backstop
114
+ * the backend applies to every write regardless of network, so every rule
115
+ * below must produce a value that passes this one.
116
+ *
117
+ * Letters are `\p{L}` rather than `A-Z`: BattleTags and Riot IDs allow accented
118
+ * names, and a rule that rejected "Ærøn#1234" would be wrong, not strict.
119
+ */
120
+ export const GENERIC_HANDLE_RULE: HandleRule = {
121
+ label: 'Game ID',
122
+ placeholder: 'Game ID',
123
+ hint: 'Enter your ID exactly as it appears in-game. Letters, numbers, spaces and _ . # - are allowed.',
124
+ pattern: /^[\p{L}\p{N}_ .#-]+$/u,
125
+ min: 3,
126
+ max: 32,
127
+ };
128
+
129
+ const STEAM_PROFILE_URL =
130
+ /^(?:https?:\/\/)?(?:www\.)?steamcommunity\.com\/(?:id|profiles)\/([^/?#\s]+)\/?(?:[?#].*)?$/i;
131
+
132
+ /**
133
+ * Per-network rules. Initial values, and product decisions rather than facts:
134
+ * each is the documented shape as of writing, kept a little looser than the
135
+ * network's own signup validator so a real handle is never refused.
136
+ */
137
+ export const HANDLE_RULES: Partial<Record<GameAccountCode, HandleRule>> = {
138
+ psn: {
139
+ code: 'psn',
140
+ label: 'PSN online ID',
141
+ placeholder: 'PSN online ID',
142
+ hint: '3 to 16 letters, numbers, hyphens or underscores, starting with a letter. No # tag.',
143
+ pattern: /^[A-Za-z][A-Za-z0-9_-]{2,15}$/,
144
+ min: 3,
145
+ max: 16,
146
+ },
147
+ xbox: {
148
+ code: 'xbox',
149
+ label: 'Gamertag',
150
+ placeholder: 'Gamertag or Gamertag#1234',
151
+ hint: 'Letters, numbers and single spaces, with the #1234 suffix if your gamertag has one.',
152
+ pattern: /^[A-Za-z0-9]+(?: [A-Za-z0-9]+)*(?:#\d{3,4})?$/,
153
+ min: 3,
154
+ max: 20,
155
+ },
156
+ battlenet: {
157
+ code: 'battlenet',
158
+ label: 'BattleTag',
159
+ placeholder: 'Name#1234',
160
+ hint: 'Your BattleTag including the number, e.g. Name#1234.',
161
+ pattern: /^\p{L}[\p{L}\p{N}]{2,11}#\d{4,7}$/u,
162
+ min: 8,
163
+ max: 20,
164
+ },
165
+ riot: {
166
+ code: 'riot',
167
+ label: 'Riot ID',
168
+ placeholder: 'GameName#TAG',
169
+ hint: 'Your Riot ID including the tagline, e.g. GameName#TAG.',
170
+ pattern: /^[\p{L}\p{N}](?:[\p{L}\p{N} ]{1,14}[\p{L}\p{N}])?#[\p{L}\p{N}]{3,5}$/u,
171
+ min: 7,
172
+ max: 22,
173
+ },
174
+ epic: {
175
+ code: 'epic',
176
+ label: 'Epic display name',
177
+ placeholder: 'Epic display name',
178
+ hint: '3 to 16 letters, numbers, spaces or - _ . as shown on your Epic account.',
179
+ pattern: /^[\p{L}\p{N}][\p{L}\p{N} ._-]{1,14}[\p{L}\p{N}._-]$/u,
180
+ min: 3,
181
+ max: 16,
182
+ },
183
+ ea: {
184
+ code: 'ea',
185
+ label: 'EA ID',
186
+ placeholder: 'EA ID',
187
+ hint: '4 to 16 letters, numbers, hyphens or underscores.',
188
+ pattern: /^[A-Za-z0-9_-]{4,16}$/,
189
+ min: 4,
190
+ max: 16,
191
+ },
192
+ activision: {
193
+ code: 'activision',
194
+ label: 'Activision ID',
195
+ placeholder: 'Name#1234567',
196
+ hint: 'Your Activision ID including the number, e.g. Name#1234567.',
197
+ pattern: /^[A-Za-z0-9 _-]{2,16}#\d{4,8}$/,
198
+ min: 7,
199
+ max: 25,
200
+ },
201
+ ubisoft: {
202
+ code: 'ubisoft',
203
+ label: 'Ubisoft username',
204
+ placeholder: 'Ubisoft username',
205
+ hint: '3 to 15 letters, numbers or - _ . as shown in Ubisoft Connect.',
206
+ pattern: /^[A-Za-z0-9._-]{3,15}$/,
207
+ min: 3,
208
+ max: 15,
209
+ },
210
+ steam: {
211
+ code: 'steam',
212
+ label: 'Steam ID',
213
+ placeholder: 'Friend code or profile URL',
214
+ hint: 'Your Steam friend code (Friends > Add a Friend), your SteamID64, or your steamcommunity.com profile URL.',
215
+ // A profile URL is the easiest thing for a player to paste; what gets
216
+ // stored is the id or custom name at the end of it.
217
+ normalize: (value) => value.replace(STEAM_PROFILE_URL, '$1'),
218
+ pattern: /^[A-Za-z0-9_-]{3,32}$/,
219
+ min: 3,
220
+ max: 32,
221
+ },
222
+ nintendo: {
223
+ code: 'nintendo',
224
+ label: 'Nintendo friend code',
225
+ placeholder: 'SW-0000-0000-0000',
226
+ hint: 'Your 12-digit Switch friend code, e.g. SW-1234-5678-9012.',
227
+ normalize: (value) => {
228
+ const digits = value.replace(/^sw/i, '').replace(/[\s-]/g, '');
229
+ return /^\d{12}$/.test(digits)
230
+ ? `SW-${digits.slice(0, 4)}-${digits.slice(4, 8)}-${digits.slice(8)}`
231
+ : value;
232
+ },
233
+ pattern: /^SW-\d{4}-\d{4}-\d{4}$/,
234
+ min: 17,
235
+ max: 17,
236
+ },
237
+ // fightcade, apple and googleplay have no documented handle shape and stay
238
+ // on the generic rule.
239
+ };
240
+
241
+ /**
242
+ * The rule a network's handles are checked against: its own when the
243
+ * catalogue row asks for format checks (or is linkable, which implies them
244
+ * for anything typed by hand) and a rule exists for its code, otherwise the
245
+ * generic one.
246
+ */
247
+ export function handleRuleFor(
248
+ network:
249
+ | {
250
+ code?: GameAccountCode | string | null;
251
+ verification?: GameAccountVerification | string | null;
252
+ }
253
+ | null
254
+ | undefined
255
+ ): HandleRule {
256
+ const checked =
257
+ network?.verification === 'format' || network?.verification === 'linked';
258
+ if (!checked || !network?.code) {
259
+ return GENERIC_HANDLE_RULE;
260
+ }
261
+ return HANDLE_RULES[network.code as GameAccountCode] ?? GENERIC_HANDLE_RULE;
262
+ }
263
+
264
+ /** True when the code has a rule of its own, so `format` means something. */
265
+ export function hasHandleRule(code: string | null | undefined): boolean {
266
+ return !!code && Object.prototype.hasOwnProperty.call(HANDLE_RULES, code);
267
+ }
268
+
269
+ /**
270
+ * Checks `raw` against `rule` and returns the value to store: trimmed, runs of
271
+ * whitespace collapsed to one space, then the rule's own normalization.
272
+ *
273
+ * The message is the one the player reads, so it says what a valid value looks
274
+ * like rather than which check failed.
275
+ */
276
+ export function checkHandle(
277
+ raw: string | null | undefined,
278
+ rule: HandleRule
279
+ ): HandleCheck {
280
+ const collapsed = (raw ?? '').trim().replace(/\s+/g, ' ');
281
+ if (!collapsed) {
282
+ return { ok: false, message: `Please enter your ${rule.label}` };
283
+ }
284
+
285
+ const value = rule.normalize ? rule.normalize(collapsed) : collapsed;
286
+
287
+ if (value.length < rule.min) {
288
+ return { ok: false, message: `${rule.label} is too short. ${rule.hint}` };
289
+ }
290
+ if (value.length > rule.max) {
291
+ return { ok: false, message: `${rule.label} is too long. ${rule.hint}` };
292
+ }
293
+ if (!rule.pattern.test(value)) {
294
+ return {
295
+ ok: false,
296
+ message: `That doesn't look like a valid ${rule.label}. ${rule.hint}`,
297
+ };
298
+ }
299
+
300
+ return { ok: true, value };
301
+ }
302
+
303
+ /** The key two handles are compared on: same network, same letters, any case. */
304
+ export function normalizeHandleKey(value: string): string {
305
+ return value.trim().replace(/\s+/g, ' ').toLowerCase();
306
+ }
@@ -2,8 +2,10 @@
2
2
  * Hand-written types that are not derived from a Strapi content type.
3
3
  */
4
4
  export * from './country';
5
+ export * from './gameAccountHandle';
5
6
  export * from './lobby';
6
7
  export * from './notification';
8
+ export * from './playerBan';
7
9
  export * from './tournamentEntry';
8
10
  export * from './tournamentInvite';
9
11
  export * from './tournamentJoinRequest';