roblox-deeplink-parser 0.1.20

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/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -0,0 +1,3 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: [RoSeal-Extension]
@@ -0,0 +1,29 @@
1
+ name: Publish on JSR/
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ release:
7
+ types: [created]
8
+
9
+ jobs:
10
+ build:
11
+ name: Build
12
+ runs-on: ubuntu-latest
13
+
14
+ permissions:
15
+ contents: write
16
+ id-token: write
17
+
18
+ steps:
19
+ - name: Setup Repo
20
+ uses: actions/checkout@v4
21
+
22
+ - name: Setup Bun
23
+ uses: oven-sh/setup-bun@v2
24
+ with:
25
+ registry-url: 'https://registry.npmjs.org'
26
+
27
+ - name: Publish on JSR
28
+ if: github.event_name == 'release' && github.repository == 'RoSeal-Extension/Roblox-DeepLink-Parser'
29
+ run: bunx jsr publish --allow-slow-types --allow-dirty
@@ -0,0 +1,8 @@
1
+ {
2
+ "editor.defaultFormatter": "biomejs.biome",
3
+ "editor.formatOnSave": true,
4
+ "editor.codeActionsOnSave": {
5
+ "source.organizeImports": "explicit",
6
+ "source.fixAll": "explicit"
7
+ },
8
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Julli4n
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # Roblox DeepLink Parser
2
+ Parse Roblox deeplinks (roblox://*), website links (www.roblox.com), and appsflyer links (ro.blox.com) to get params and get new appysflyer/deeplink/website URLs.
3
+
4
+ ## Example
5
+ ```ts
6
+ import DeepLinkParser from "@roseal/roblox-deeplink-parser";
7
+ const parser = new DeepLinkParser();
8
+
9
+ // same as parser.parseWebsiteLink
10
+ parser.parseLink("https://www.roblox.com/my/avatar").then((link) => {
11
+ if (link) {
12
+ // roblox://navigation/avatar
13
+ console.log(link.toProtocolUrl());
14
+ // https://www.roblox.com/my/avatar
15
+ console.log(link.toWebsiteUrl());
16
+ // https://ro.blox.com/Ebh5?af_dp=roblox%3A%2F%2Fnavigation%2Favatar&af_web_dp=https%3A%2F%2Fwww.roblox.com%2Fmy%2Favatar
17
+ console.log(link.toAppsFlyerUrl());
18
+ }
19
+ });
20
+
21
+ // ... and you can do these as well specifically:
22
+ parser.parseProtocolLink("roblox://navigation/avatar")
23
+ parser.parseAppsFlyerLink("https://ro.blox.com/Ebh5?af_dp=roblox%3A%2F%2Fnavigation%2Favatar&af_web_dp=https%3A%2F%2Fwww.roblox.com%2Fmy%2Favatar");
24
+ parser.parseWebsiteLink("https://www.roblox.com/my/avatar");
25
+ ```
26
+
27
+ ## Reference
28
+ | Deeplink | Website Equivalent | Description |
29
+ | -------- | ------------------ | ----------- |
30
+ | `roblox://experiences/start` or `roblox://` <br />Search Parameters:<br />- placeId<br />- userId<br />- gameInstanceId<br /> - accessCode<br />- linkCode<br />- launchData<br /> - joinAttemptId<br />- joinAttemptOrigin<br />- reservedServerAccessCode<br />- callId<br />- browserTrackerId<br />- referralPage<br />- referredByPlayerId<br />- eventId<br />- isoContext | `www.roblox.com/games/start` <br />Search Parameters:<br />- placeId<br /> - gameInstanceId<br /> - accessCode<br />- linkCode<br />- launchData<br /> - joinAttemptId<br />- joinAttemptOrigin<br />- reservedServerAccessCode<br />- callId<br />- browserTrackerId<br />- referralPage<br />- referredByPlayerId | Join a server or user, for more information please see [Bloxstrap wiki](https://github.com/bloxstraplabs/bloxstrap/wiki/A-deep-dive-on-how-the-Roblox-bootstrapper-works#starting-roblox)
31
+ | `roblox://navigation/profile` <br />Search Parameters:<br />- groupId<br />- userId<br />- friendshipSourceType | `www.roblox.com/users/{userId}/profile` or `www.roblox.com/communities/{groupId}/name` | Open webview of a user or community on the website
32
+ | `roblox://navigation/group` <br />Search Parameters:<br />- groupId<br />- forumCategoryId<br />- forumPostId<br />- forumCommentId| `www.roblox.com/communities/{groupId}/name` | Open webview of a community page
33
+ | `roblox://navigation/profile_card` <br />Search Parameters:<br />- userId | `www.roblox.com/users/{userId}/profile` | Opens the native app view of a user profile
34
+ | `roblox://navigation/content_posts` <br />Search Parameters:<br />- userId<br />- postId | N/A | Show content posts "captures" from a user or a specific post
35
+ | `roblox://navigation/share_links` <br />Search Parameters:<br />- type<br />- code | `www.roblox.com/share-links` <br />Search Parameters:<br />- type<br />- code | Resolve a share link, which could be a friend invite, experience details link, experience invite, etc.
36
+ | `roblox://navigation/gift_cards` | `www.roblox.com/giftcards` | Open webview of the giftcards page
37
+ | `roblox://navigation/external_web_link` <br />Search Parameters:<br />- domain (zendesk)<br />- locale<br />- articleId | `en.help.roblox.com/hc/{locale}/articles/{articleId}` | Open webview of a zendesk article
38
+ | `roblox://navigation/chat` <br />Search Parameters:<br />- userId<br />- chatId<br />- entryPoint | N/A | Open chat conversation with a user or another type of conversation (group)
39
+ | `roblox://navigation/appeals` <br />Search Parameters:<br />- vid | `www.roblox.com/report-appeals#/v/{vid}` | Open webview of appeals page or a certain appeal request
40
+ | `roblox://navigation/home` | `www.roblox.com/home` | Open home page
41
+ | `roblox://navigation/event_details` <br />Search Parameters:<br />- eventId | `www.roblox.com/events/{eventId}` | Open details of an experience event
42
+ | `roblox://navigation/crossdevice` <br />Search Parameters:<br />- code | `www.roblox.com/crossdevicelogin/confirmcode` | Open crossdevice login page, and automatically enter the code if there was one in the deeplink
43
+ | `roblox://navigation/contacts` <br />Search Parameters:<br />- contactId<br />- assetId<br />- avatarImageUrl | N/A | Open contacts page (or friend requests page)
44
+ | `roblox://navigation/avatar_clothing_sort` | N/A | Open avatar clothing sorting page
45
+ | `roblox://navigation/avatar_profile_picture_editor` | N/A | Open avatar profile picture editor page
46
+ | `roblox://navigation/catalog` | `www.roblox.com/catalog` | Open avatar marketplace page
47
+ | `roblox://navigation/catalog/equip` <br />Search Parameters:<br />- itemId<br />- itemType (Asset\|Bundle) | N/A | Equip or try on an avatar item
48
+ | `roblox://navigation/friends` | `www.roblox.com/users/friends` | View the currently authenticated user's friends
49
+ | `roblox://navigation/avatar` <br />Search Parameters:<br />- itemType (Character)<br />- itemId | `www.roblox.com/my/avatar` | Open the avatar editor page
50
+ | `roblox://navigation/more` | N/A | Open the More section of the app
51
+ | `roblox://navigation/games` | `www.roblox.com/charts` | Open the charts page
52
+ | `roblox://navigation/sort` <br />Search Parameters:<br />- sortName | `www.roblox.com/charts#/sortName/{sortId}` | Open the charts sort page
53
+ | `roblox://navigation/item_details` <br />Search Parameters:<br />- itemType (Look\|Asset\|Bundle)<br />- itemId | `www.roblox.com/catalog/{itemId}/name`, `www.roblox.com/bundles/{itemId}/name`, or `www.roblox.com/looks/{itemId}/name` | Open the details page of an avatar asset, bundle, or look
54
+ | `roblox://navigation/account_info` | `www.roblox.com/my/account#!/info` | Open webview of the account settings info page
55
+ | `roblox://navigation/notification_settings` | `www.roblox.com/my/account#!/notifications` | Open webview of the account settings notifications page
56
+ | `roblox://navigation/privacy_settings` | `www.roblox.com/my/account#!/privacy` | Open webview of the account settings privacy page
57
+ | `roblox://navigation/parental_controls` | `www.roblox.com/my/account#!/parental-controls` | Open webview of the account settings parental controls page
58
+ | `roblox://navigation/spending_settings` | `www.roblox.com/my/account#!/payment-methods` | Open webview of the account settings payment methods page
59
+ | `roblox://navigation/qr_code_redemption` <br />Search Parameters:<br />- itemType (Asset\|Bundle)<br />- itemId | N/A | Redeem an item which can be obtained by scanning a QR code
60
+ | `roblox://navigation/game_details` <br />Search Parameters:<br />- gameId<br />- privateServerLinkCode | `www.roblox.com/games/{placeId}/name` <br />Search Parameters:<br />- privateServerLinkCode | Opens the details page of an experience
61
+ | `roblox://navigation/security_alert` <br />Search Parameters:<br />- payload | `www.roblox.com/security-feedback` <br />Search Parameters:<br />- payload | Opens security feedback page
62
+ | `roblox://navigation/experience_sort` <br />Search Parameters:<br />- sortId | N/A | Opens an experience sort page on the home page
63
+ | `roblox://navigation/party` | N/A | Opens the party page
64
+ | `roblox://navigation/app_permissions_settings` | `www.roblox.com/my/account#!/app-permissions` | Opens the app permissions page
65
+ | `roblox://navigation/screentime_subsettings` | `www.roblox.com/my/account#!/privacy/Screentime` | Opens the screentime subsettings page
66
+ | `roblox://navigation/blocked_experiences_subsettings` | `www.roblox.com/my/account#!/privacy/ContentRestrictions/BlockedExperiences` | Opens the blocked experiences subsettings page
67
+ | `roblox://navigation/blocked_users_subsettings` | `www.roblox.com/my/account#!/privacy/BlockedUsers` | Opens the blocked users subsettings page
68
+ | `roblox://navigation/experience_chat_subsettings` | `www.roblox.com/my/account#!/privacy/ExperienceChat` | Opens the experience chat subsettings page
69
+ | `roblox://navigation/party_subsettings` | `www.roblox.com/my/account#!/privacy/Party` | Opens the party subsettings page
70
+ | `roblox://navigation/voice_subsettings` | `www.roblox.com/my/account#!/privacy/Voice` | Opens the voice subsettings page
71
+ | `roblox://navigation/trading_inventory_subsettings` | `www.roblox.com/my/account#!/privacy/TradingInventory` | Opens the trading inventory subsettings page
72
+ | `roblox://navigation/friends_contacts_subsettings` | `www.roblox.com/my/account#!/privacy/FriendsAndContacts` | Opens the friends and contacts subsettings page
73
+ | `roblox://navigation/private_server_subsettings` | `www.roblox.com/my/account#!/privacy/PrivateServers` | Opens the private server subsettings page
74
+ | `roblox://navigation/visibility_subsettings` | `www.roblox.com/my/account#!/privacy/Visibility` | Opens the visibility subsettings page
75
+ | `roblox://navigation/fae` | N/A | Opens the facial age estimation page
76
+ | `roblox://navigation/fae_upsell_overlay` | N/A | Opens the facial age estimation overlay page
77
+ | `roblox://navigation/account_recovery` <br />Search Parameters:<br />- origin<br />- username<br />- recoverySessionId | `www.roblox.com/login/forgot-password-or-username` <br />Search Parameters:<br />- origin<br />- username<br />- recoverySessionId | Opens the account recovery page
78
+ | `roblox://navigation/messages` | `https://www.roblox.com/my/messages` | Opens the systen messages page
package/biome.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
3
+ "vcs": {
4
+ "enabled": false,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": false
7
+ },
8
+ "files": {
9
+ "ignoreUnknown": false,
10
+ "includes": ["**"]
11
+ },
12
+ "formatter": {
13
+ "enabled": true,
14
+ "indentStyle": "tab"
15
+ },
16
+ "assist": { "actions": { "source": { "organizeImports": "on" } } },
17
+ "linter": {
18
+ "enabled": true,
19
+ "rules": {
20
+ "recommended": true
21
+ }
22
+ },
23
+ "javascript": {
24
+ "formatter": {
25
+ "quoteStyle": "double"
26
+ }
27
+ }
28
+ }
package/jsr.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@roseal/roblox-deeplink-parser",
3
+ "version": "0.1.20",
4
+ "exports": {
5
+ ".": "./src/DeepLinkParser.ts",
6
+ "./authedProtocol": "./src/AuthedProtocolParser.ts"
7
+ }
8
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "roblox-deeplink-parser",
3
+ "private": false,
4
+ "version": "0.1.20",
5
+ "type": "module",
6
+ "main": "./dist/DeepLinkParser.js",
7
+ "module": "./dist/DeepLinkParser.js",
8
+ "types": "./dist/DeepLinkParser.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/DeepLinkParser.js",
12
+ "types": "./dist/DeepLinkParser.d.ts"
13
+ },
14
+ "./authedProtocol": {
15
+ "import": "./dist/AuthedProtocolParser.js",
16
+ "types": "./dist/AuthedProtocolParser.d.ts"
17
+ }
18
+ },
19
+ "devDependencies": {
20
+ "@types/bun": "latest",
21
+ "typescript": "latest",
22
+ "@biomejs/biome": "latest"
23
+ },
24
+ "scripts": {
25
+ "build": "bunx bunup src/DeepLinkParser.ts src/AuthedProtocolParser.ts"
26
+ }
27
+ }
@@ -0,0 +1,292 @@
1
+ import {
2
+ DEFAULT_PLACELAUNCHER_URL,
3
+ DEFAULT_ROBLOX_PLAYER_AUTHED_PROTOCOL,
4
+ DEFAULT_ROBLOX_STUDIO_AUTH_AUTHED_PROTOCOL,
5
+ DEFAULT_ROBLOX_STUDIO_AUTHED_PROTOCOL,
6
+ } from "./utils/constants";
7
+
8
+ export type AuthedProtocolUrls<T> = {
9
+ robloxPlayerProtocol: T;
10
+ robloxStudioProtocol: T;
11
+ robloxStudioAuthProtocol: T;
12
+ placeLauncherUrl: string;
13
+ };
14
+
15
+ export type AuthedProtocolParserConstructorProps<T extends string> = {
16
+ urls?: Partial<AuthedProtocolUrls<T>>;
17
+ };
18
+
19
+ export type AuthedLaunchMode =
20
+ | "edit"
21
+ | "plugin"
22
+ | "play"
23
+ | "build"
24
+ | "app"
25
+ | "asset";
26
+
27
+ export type AuthLaunchExp = "InApp" | "PreferInApp" | "InBrowser";
28
+
29
+ export type AuthedStudioTask =
30
+ | "EditFile"
31
+ | "EditPlace"
32
+ | "EditPlaceRevision"
33
+ | "StartClient"
34
+ | "StartTeamTest"
35
+ | "InstallPlugin"
36
+ | "TryAsset"
37
+ | "RemoteDebug"
38
+ | "StartServer";
39
+
40
+ export type AuthedDistributorType = "Global" | "ChinaJoinVenture";
41
+
42
+ export type AuthedOtherParams = {
43
+ browsertrackerid?: number;
44
+ robloxLocale?: string;
45
+ gameLocale?: string;
46
+ channel?: string;
47
+ LaunchExp?: AuthLaunchExp;
48
+ avatar?: string;
49
+ assetid?: number;
50
+ pluginid?: number;
51
+ placeid?: number;
52
+ universeid?: number;
53
+ script?: string;
54
+ placelauncherurl?: string;
55
+ task?: AuthedStudioTask;
56
+ traceId?: string;
57
+ userId?: number;
58
+ browser?: string;
59
+ distributorType?: AuthedDistributorType;
60
+ [key: string]: string | number | undefined;
61
+ };
62
+
63
+ export type BuildAuthedProtocolUrlParameters<T extends string> = {
64
+ type: T;
65
+ launchMode?: AuthedLaunchMode;
66
+ gameInfo?: string;
67
+ launchTime?: string;
68
+ baseUrl?: string;
69
+ otherParams?: AuthedOtherParams;
70
+
71
+ // auth params
72
+ state?: string;
73
+ code?: string;
74
+ };
75
+
76
+ export type BuildAuthedPlaceLauncherRequest =
77
+ | "RequestGame"
78
+ | "RequestCloudEdit"
79
+ | "RequestGameJob"
80
+ | "RequestFollowUser"
81
+ | "RequestPrivateGame"
82
+ | "RequestPlayTogetherGame"
83
+ | "RequestGameApp"
84
+ | "RequestInvalid"
85
+ | "RequestPlayWithParty"
86
+ | "CheckGameJobStatus"
87
+ | "RequestReservedGame"
88
+ | "RequestCrossExpVoice";
89
+
90
+ export type AuthedPrivateGameMode = "ReservedServer";
91
+
92
+ export type BuildAuthedPlaceLauncherURLParameters<T extends string> = {
93
+ request: BuildAuthedPlaceLauncherRequest;
94
+ placeId?: number;
95
+ jobId?: string;
96
+ gameId?: string;
97
+ gender?: string;
98
+ genderId?: number;
99
+ accessCode?: string;
100
+ linkCode?: string;
101
+ launchData?: string;
102
+ privateGameMode?: AuthedPrivateGameMode;
103
+ teleportType?: string;
104
+ reservedServerAccessCode?: string;
105
+ referralPage?: string;
106
+ referredByPlayerId?: number;
107
+ conversationId?: number;
108
+ isPartyLeader?: boolean;
109
+ isTeleport?: boolean;
110
+ partyGuid?: string;
111
+ isPlayTogetherGame?: boolean;
112
+ joinAttemptId?: string;
113
+ joinAttemptOrigin?: T;
114
+ callId?: string;
115
+ browserTrackerId?: number;
116
+ eventId?: string;
117
+ isolationContext?: string;
118
+ gameJoinContext?: string;
119
+ userId?: number;
120
+ };
121
+
122
+ export default class AuthedProtocolParser<T extends string, U extends string> {
123
+ public _urls: AuthedProtocolUrls<T> = {
124
+ robloxPlayerProtocol: DEFAULT_ROBLOX_PLAYER_AUTHED_PROTOCOL as T,
125
+ robloxStudioProtocol: DEFAULT_ROBLOX_STUDIO_AUTHED_PROTOCOL as T,
126
+ robloxStudioAuthProtocol: DEFAULT_ROBLOX_STUDIO_AUTH_AUTHED_PROTOCOL as T,
127
+ placeLauncherUrl: DEFAULT_PLACELAUNCHER_URL,
128
+ };
129
+
130
+ constructor(props?: AuthedProtocolParserConstructorProps<T>) {
131
+ if (props?.urls) {
132
+ if (props.urls.robloxPlayerProtocol) {
133
+ this._urls.robloxPlayerProtocol = props.urls.robloxPlayerProtocol;
134
+ }
135
+ if (props.urls.robloxStudioProtocol) {
136
+ this._urls.robloxStudioProtocol = props.urls.robloxStudioProtocol;
137
+ }
138
+ if (props.urls.robloxStudioAuthProtocol) {
139
+ this._urls.robloxStudioAuthProtocol =
140
+ props.urls.robloxStudioAuthProtocol;
141
+ }
142
+
143
+ if (props.urls.placeLauncherUrl) {
144
+ this._urls.placeLauncherUrl = props.urls.placeLauncherUrl;
145
+ }
146
+ }
147
+ }
148
+
149
+ public buildAuthedProtocolUrl(
150
+ request: BuildAuthedProtocolUrlParameters<T>,
151
+ ): string {
152
+ if (
153
+ request.type === this._urls.robloxStudioAuthProtocol &&
154
+ request.state &&
155
+ request.code
156
+ ) {
157
+ return `${this._urls.robloxStudioAuthProtocol}://?state=${request.state}&code=${request.code}`;
158
+ }
159
+
160
+ const params: string[] = ["1"];
161
+
162
+ if (request.launchMode) params.push(`launchmode:${request.launchMode}`);
163
+ if (request.gameInfo) params.push(`gameinfo:${request.gameInfo}`);
164
+ if (request.launchTime) params.push(`launchtime:${request.launchTime}`);
165
+ if (request.baseUrl) params.push(`baseUrl:${request.baseUrl}`);
166
+
167
+ if (request.otherParams) {
168
+ for (const key in request.otherParams) {
169
+ const value = request.otherParams[key];
170
+ if (key === value) {
171
+ params.push(key);
172
+ } else if (value !== undefined && value !== null) {
173
+ params.push(`${key}:${encodeURIComponent(value)}`);
174
+ }
175
+ }
176
+ }
177
+
178
+ return `${request.type}:${params.join("+")}`;
179
+ }
180
+
181
+ public buildAuthedPlaceLauncherUrl(
182
+ request: BuildAuthedPlaceLauncherURLParameters<U>,
183
+ ): string {
184
+ const url = new URL(this._urls.placeLauncherUrl);
185
+ const stringifiedParameters: Record<string, string> = {};
186
+ for (const key in request) {
187
+ const value = request[key as keyof typeof request];
188
+
189
+ if (value !== undefined && value !== null) {
190
+ stringifiedParameters[key] = String(value);
191
+ }
192
+ }
193
+
194
+ url.search = new URLSearchParams(stringifiedParameters).toString();
195
+ return url.toString();
196
+ }
197
+
198
+ public parseAuthedProtocolUrl(
199
+ url: string,
200
+ ): BuildAuthedProtocolUrlParameters<T> | null {
201
+ if (url.startsWith(`${this._urls.robloxStudioAuthProtocol}:`)) {
202
+ const urlObj = new URL(url);
203
+ return {
204
+ type: this._urls.robloxStudioAuthProtocol,
205
+ state: urlObj.searchParams.get("state") ?? undefined,
206
+ code: urlObj.searchParams.get("code") ?? undefined,
207
+ };
208
+ }
209
+
210
+ const isPlayer = url.startsWith(`${this._urls.robloxPlayerProtocol}:1`);
211
+ const isStudio = url.startsWith(`${this._urls.robloxStudioProtocol}:1`);
212
+ if (!isPlayer && !isStudio) {
213
+ return null;
214
+ }
215
+
216
+ const split = url.split("+");
217
+
218
+ const type = isPlayer
219
+ ? this._urls.robloxPlayerProtocol
220
+ : this._urls.robloxStudioProtocol;
221
+
222
+ const params: BuildAuthedProtocolUrlParameters<T> = {
223
+ type,
224
+ };
225
+
226
+ for (let i = 1; i < split.length; i++) {
227
+ const item = split[i];
228
+ const [key, value] = item.split(":");
229
+ if (!value) continue;
230
+
231
+ switch (key.toLowerCase()) {
232
+ case "launchmode": {
233
+ params.launchMode = value as AuthedLaunchMode;
234
+ break;
235
+ }
236
+ case "gameinfo": {
237
+ params.gameInfo = value;
238
+ break;
239
+ }
240
+ case "launchtime": {
241
+ params.launchTime = value;
242
+ break;
243
+ }
244
+ default: {
245
+ params.otherParams ??= {};
246
+ params.otherParams[key] = decodeURIComponent(value);
247
+ }
248
+ }
249
+ }
250
+
251
+ return params;
252
+ }
253
+
254
+ public parseAuthedPlaceLauncherUrl(
255
+ urlStr: string,
256
+ ): BuildAuthedPlaceLauncherURLParameters<U> | null {
257
+ try {
258
+ const url = new URL(urlStr);
259
+
260
+ const request = url.searchParams.get("request");
261
+ if (!request) return null;
262
+
263
+ const obj: BuildAuthedPlaceLauncherURLParameters<U> = {
264
+ request: request as BuildAuthedPlaceLauncherRequest,
265
+ };
266
+
267
+ for (const [key, value] of url.searchParams) {
268
+ let valueToSet: number | boolean | string;
269
+ if (value === "true") {
270
+ valueToSet = true;
271
+ } else if (value === "false") {
272
+ valueToSet = false;
273
+ } else {
274
+ const parsedNumber = Number(value);
275
+
276
+ if (!Number.isNaN(parsedNumber)) {
277
+ valueToSet = parsedNumber;
278
+ } else {
279
+ valueToSet = value;
280
+ }
281
+ }
282
+
283
+ // @ts-expect-error: Fine tbh
284
+ obj[key] = valueToSet;
285
+ }
286
+
287
+ return obj;
288
+ } catch {
289
+ return null;
290
+ }
291
+ }
292
+ }