@bedrock-rbx/ocale 0.1.0-beta.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/LICENSE +21 -0
- package/dist/badges.d.mts +186 -0
- package/dist/badges.d.mts.map +1 -0
- package/dist/badges.mjs +309 -0
- package/dist/badges.mjs.map +1 -0
- package/dist/developer-products.d.mts +245 -0
- package/dist/developer-products.d.mts.map +1 -0
- package/dist/developer-products.mjs +388 -0
- package/dist/developer-products.mjs.map +1 -0
- package/dist/game-passes.d.mts +210 -0
- package/dist/game-passes.d.mts.map +1 -0
- package/dist/game-passes.mjs +397 -0
- package/dist/game-passes.mjs.map +1 -0
- package/dist/index.d.mts +191 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/places.d.mts +161 -0
- package/dist/places.d.mts.map +1 -0
- package/dist/places.mjs +403 -0
- package/dist/places.mjs.map +1 -0
- package/dist/price-information-CmpscMc4.mjs +42 -0
- package/dist/price-information-CmpscMc4.mjs.map +1 -0
- package/dist/rate-limit-BBU_4xnZ.mjs +135 -0
- package/dist/rate-limit-BBU_4xnZ.mjs.map +1 -0
- package/dist/resource-client-CaS_j3yg.mjs +652 -0
- package/dist/resource-client-CaS_j3yg.mjs.map +1 -0
- package/dist/to-blob-1BtHsDGK.mjs +18 -0
- package/dist/to-blob-1BtHsDGK.mjs.map +1 -0
- package/dist/types-YCTsM8Qd.d.mts +214 -0
- package/dist/types-YCTsM8Qd.d.mts.map +1 -0
- package/dist/universes.d.mts +387 -0
- package/dist/universes.d.mts.map +1 -0
- package/dist/universes.mjs +705 -0
- package/dist/universes.mjs.map +1 -0
- package/dist/validation-CTZzJhmd.mjs +38 -0
- package/dist/validation-CTZzJhmd.mjs.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import { i as ApiError } from "./rate-limit-BBU_4xnZ.mjs";
|
|
2
|
+
import { t as ValidationError } from "./validation-CTZzJhmd.mjs";
|
|
3
|
+
import { t as toBlob } from "./to-blob-1BtHsDGK.mjs";
|
|
4
|
+
import { a as IDEMPOTENT_METHOD_DEFAULTS, i as CREATE_METHOD_DEFAULTS, n as okRequest, o as isRecord, r as parseEmptyResponse, t as ResourceClient } from "./resource-client-CaS_j3yg.mjs";
|
|
5
|
+
//#region src/domains/cloud-v2/universes/builders.ts
|
|
6
|
+
/**
|
|
7
|
+
* Dodges `unicorn/no-null` while still emitting a literal `null` onto
|
|
8
|
+
* the wire, which the Open Cloud `Cloud_UpdateUniverse` endpoint
|
|
9
|
+
* requires to clear a nullable field (for example disabling private
|
|
10
|
+
* servers or removing a social link).
|
|
11
|
+
*/
|
|
12
|
+
const NULL_SENTINEL = JSON.parse("null");
|
|
13
|
+
/**
|
|
14
|
+
* Builds a `GET` request for the Open Cloud "get universe" endpoint.
|
|
15
|
+
*
|
|
16
|
+
* @param parameters - The universe identifier.
|
|
17
|
+
* @returns A success result wrapping the request; the builder cannot fail.
|
|
18
|
+
*/
|
|
19
|
+
function buildGetRequest(parameters) {
|
|
20
|
+
return okRequest({
|
|
21
|
+
method: "GET",
|
|
22
|
+
url: `/cloud/v2/universes/${parameters.universeId}`
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Builds a `PATCH` request for the Open Cloud "update universe"
|
|
27
|
+
* endpoint. Derives the `updateMask` query string from the keys
|
|
28
|
+
* present on `parameters` and emits a JSON body containing those same
|
|
29
|
+
* fields, translating `undefined` values to JSON `null` so Roblox
|
|
30
|
+
* clears the corresponding server-side value.
|
|
31
|
+
*
|
|
32
|
+
* @param parameters - The universe identifier plus the fields to update.
|
|
33
|
+
* @returns A success result wrapping the request, or a
|
|
34
|
+
* {@link ValidationError} when no updatable fields were supplied.
|
|
35
|
+
*/
|
|
36
|
+
function buildUpdateRequest(parameters) {
|
|
37
|
+
const fieldKeys = extractUpdateFieldKeys(parameters);
|
|
38
|
+
if (fieldKeys.length === 0) return {
|
|
39
|
+
err: new ValidationError("Update must include at least one field", { code: "empty_update" }),
|
|
40
|
+
success: false
|
|
41
|
+
};
|
|
42
|
+
const body = {};
|
|
43
|
+
for (const key of fieldKeys) body[key] = bodyValueFor(parameters, key);
|
|
44
|
+
const updateMask = fieldKeys.join(",");
|
|
45
|
+
return {
|
|
46
|
+
data: {
|
|
47
|
+
body,
|
|
48
|
+
headers: { "content-type": "application/json" },
|
|
49
|
+
method: "PATCH",
|
|
50
|
+
url: `/cloud/v2/universes/${parameters.universeId}?updateMask=${updateMask}`
|
|
51
|
+
},
|
|
52
|
+
success: true
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function extractUpdateFieldKeys(parameters) {
|
|
56
|
+
return Object.keys(parameters).filter((key) => key !== "universeId");
|
|
57
|
+
}
|
|
58
|
+
function bodyValueFor(parameters, key) {
|
|
59
|
+
const value = Reflect.get(parameters, key);
|
|
60
|
+
return value === void 0 ? NULL_SENTINEL : value;
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/domains/cloud-v2/universes/operations.ts
|
|
64
|
+
const PER_MINUTE = 100;
|
|
65
|
+
const SECONDS_PER_MINUTE = 60;
|
|
66
|
+
/**
|
|
67
|
+
* Per-second request ceiling for reading a universe, from the Open
|
|
68
|
+
* Cloud OpenAPI schema (100 requests per minute per API key owner).
|
|
69
|
+
*/
|
|
70
|
+
const GET_OPERATION_LIMIT = Object.freeze({
|
|
71
|
+
maxPerSecond: PER_MINUTE / SECONDS_PER_MINUTE,
|
|
72
|
+
operationKey: "universes.get"
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Per-second request ceiling for updating a universe, from the Open
|
|
76
|
+
* Cloud OpenAPI schema (100 requests per minute per API key owner).
|
|
77
|
+
* Keyed independently from {@link GET_OPERATION_LIMIT} so reads and
|
|
78
|
+
* updates do not share a queue; upstream quota accounting is not
|
|
79
|
+
* documented as shared and the conservative default is fewer
|
|
80
|
+
* cross-method contention surprises.
|
|
81
|
+
*/
|
|
82
|
+
const UPDATE_OPERATION_LIMIT = Object.freeze({
|
|
83
|
+
maxPerSecond: PER_MINUTE / SECONDS_PER_MINUTE,
|
|
84
|
+
operationKey: "universes.update"
|
|
85
|
+
});
|
|
86
|
+
/**
|
|
87
|
+
* Scopes required to update a universe, sourced from `x-roblox-scopes`
|
|
88
|
+
* on the `Cloud_UpdateUniverse` operation in the vendored OpenAPI schema.
|
|
89
|
+
* `Cloud_GetUniverse` declares no scope, so the GET method intentionally
|
|
90
|
+
* does not declare `requiredScopes` and a 401/403 there surfaces as a
|
|
91
|
+
* generic ApiError.
|
|
92
|
+
*/
|
|
93
|
+
const UPDATE_REQUIRED_SCOPES = Object.freeze(["universe:write"]);
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/domains/cloud-v2/universes/parsers.ts
|
|
96
|
+
const VISIBILITY_MAP = {
|
|
97
|
+
PRIVATE: "private",
|
|
98
|
+
PUBLIC: "public",
|
|
99
|
+
VISIBILITY_UNSPECIFIED: "unspecified"
|
|
100
|
+
};
|
|
101
|
+
const AGE_RATING_MAP = {
|
|
102
|
+
AGE_RATING_9_PLUS: "9Plus",
|
|
103
|
+
AGE_RATING_13_PLUS: "13Plus",
|
|
104
|
+
AGE_RATING_17_PLUS: "17Plus",
|
|
105
|
+
AGE_RATING_ALL: "all",
|
|
106
|
+
AGE_RATING_UNSPECIFIED: "unspecified"
|
|
107
|
+
};
|
|
108
|
+
const MALFORMED_MESSAGE = "Malformed universe response";
|
|
109
|
+
/**
|
|
110
|
+
* Parses a successful Open Cloud `Universe` response body into the
|
|
111
|
+
* public {@link Universe} shape.
|
|
112
|
+
*
|
|
113
|
+
* @param response - The full {@link HttpResponse} from the Open Cloud API.
|
|
114
|
+
* @returns A success result wrapping the parsed {@link Universe}, or
|
|
115
|
+
* an {@link ApiError} when the body does not match the wire schema.
|
|
116
|
+
*/
|
|
117
|
+
function parseUniverseResponse(response) {
|
|
118
|
+
const { body, status: statusCode } = response;
|
|
119
|
+
if (!isUniverseWire(body)) return malformed(statusCode);
|
|
120
|
+
const ownerResult = resolveOwner(body);
|
|
121
|
+
if (!ownerResult.success) return malformed(statusCode);
|
|
122
|
+
const id = /^universes\/(\d+)$/.exec(body.path)?.[1];
|
|
123
|
+
if (id === void 0) return malformed(statusCode);
|
|
124
|
+
return {
|
|
125
|
+
data: toUniverse({
|
|
126
|
+
id,
|
|
127
|
+
body,
|
|
128
|
+
owner: ownerResult.data
|
|
129
|
+
}),
|
|
130
|
+
success: true
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function malformed(statusCode) {
|
|
134
|
+
return {
|
|
135
|
+
err: new ApiError(MALFORMED_MESSAGE, { statusCode }),
|
|
136
|
+
success: false
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function extractRootPlaceId(rootPlace) {
|
|
140
|
+
if (rootPlace === void 0) return;
|
|
141
|
+
return /\/places\/(\d+)$/.exec(rootPlace)?.[1];
|
|
142
|
+
}
|
|
143
|
+
function toSocialLink(wire) {
|
|
144
|
+
if (wire === void 0) return;
|
|
145
|
+
return {
|
|
146
|
+
title: wire.title,
|
|
147
|
+
uri: wire.uri
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function toUniverse(args) {
|
|
151
|
+
const { id, body, owner } = args;
|
|
152
|
+
return {
|
|
153
|
+
id,
|
|
154
|
+
ageRating: AGE_RATING_MAP[body.ageRating],
|
|
155
|
+
consoleEnabled: body.consoleEnabled ?? false,
|
|
156
|
+
createdAt: new Date(body.createTime),
|
|
157
|
+
description: body.description,
|
|
158
|
+
desktopEnabled: body.desktopEnabled ?? false,
|
|
159
|
+
discordSocialLink: toSocialLink(body.discordSocialLink),
|
|
160
|
+
displayName: body.displayName,
|
|
161
|
+
facebookSocialLink: toSocialLink(body.facebookSocialLink),
|
|
162
|
+
guildedSocialLink: toSocialLink(body.guildedSocialLink),
|
|
163
|
+
mobileEnabled: body.mobileEnabled ?? false,
|
|
164
|
+
owner,
|
|
165
|
+
privateServerPriceRobux: body.privateServerPriceRobux ?? void 0,
|
|
166
|
+
robloxGroupSocialLink: toSocialLink(body.robloxGroupSocialLink),
|
|
167
|
+
rootPlaceId: extractRootPlaceId(body.rootPlace),
|
|
168
|
+
tabletEnabled: body.tabletEnabled ?? false,
|
|
169
|
+
twitchSocialLink: toSocialLink(body.twitchSocialLink),
|
|
170
|
+
twitterSocialLink: toSocialLink(body.twitterSocialLink),
|
|
171
|
+
updatedAt: new Date(body.updateTime),
|
|
172
|
+
visibility: VISIBILITY_MAP[body.visibility],
|
|
173
|
+
voiceChatEnabled: body.voiceChatEnabled ?? false,
|
|
174
|
+
vrEnabled: body.vrEnabled ?? false,
|
|
175
|
+
youtubeSocialLink: toSocialLink(body.youtubeSocialLink)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function isVisibilityWire(value) {
|
|
179
|
+
return value === "PRIVATE" || value === "PUBLIC" || value === "VISIBILITY_UNSPECIFIED";
|
|
180
|
+
}
|
|
181
|
+
function isAgeRatingWire(value) {
|
|
182
|
+
return value === "AGE_RATING_13_PLUS" || value === "AGE_RATING_17_PLUS" || value === "AGE_RATING_9_PLUS" || value === "AGE_RATING_ALL" || value === "AGE_RATING_UNSPECIFIED";
|
|
183
|
+
}
|
|
184
|
+
function hasValidRequiredFields(body) {
|
|
185
|
+
return typeof body["path"] === "string" && typeof body["createTime"] === "string" && typeof body["updateTime"] === "string" && typeof body["displayName"] === "string" && typeof body["description"] === "string" && isVisibilityWire(body["visibility"]) && isAgeRatingWire(body["ageRating"]);
|
|
186
|
+
}
|
|
187
|
+
function isSocialLinkWire(value) {
|
|
188
|
+
if (!isRecord(value)) return false;
|
|
189
|
+
return typeof value["title"] === "string" && typeof value["uri"] === "string";
|
|
190
|
+
}
|
|
191
|
+
function isOptionalSocialLink(value) {
|
|
192
|
+
return value === void 0 || value === null || isSocialLinkWire(value);
|
|
193
|
+
}
|
|
194
|
+
function isOptionalBoolean(value) {
|
|
195
|
+
return value === void 0 || value === null || typeof value === "boolean";
|
|
196
|
+
}
|
|
197
|
+
function hasValidOptionalFields(body) {
|
|
198
|
+
const priceField = body["privateServerPriceRobux"] ?? void 0;
|
|
199
|
+
if (priceField !== void 0 && typeof priceField !== "number") return false;
|
|
200
|
+
const rootPlace = body["rootPlace"] ?? void 0;
|
|
201
|
+
if (rootPlace !== void 0 && typeof rootPlace !== "string") return false;
|
|
202
|
+
return isOptionalBoolean(body["voiceChatEnabled"]) && isOptionalBoolean(body["desktopEnabled"]) && isOptionalBoolean(body["mobileEnabled"]) && isOptionalBoolean(body["tabletEnabled"]) && isOptionalBoolean(body["consoleEnabled"]) && isOptionalBoolean(body["vrEnabled"]) && isOptionalSocialLink(body["facebookSocialLink"]) && isOptionalSocialLink(body["twitterSocialLink"]) && isOptionalSocialLink(body["youtubeSocialLink"]) && isOptionalSocialLink(body["twitchSocialLink"]) && isOptionalSocialLink(body["discordSocialLink"]) && isOptionalSocialLink(body["robloxGroupSocialLink"]) && isOptionalSocialLink(body["guildedSocialLink"]);
|
|
203
|
+
}
|
|
204
|
+
function isUniverseWire(body) {
|
|
205
|
+
if (!isRecord(body)) return false;
|
|
206
|
+
return hasValidRequiredFields(body) && hasValidOptionalFields(body);
|
|
207
|
+
}
|
|
208
|
+
function extractOwnerId(resourcePath) {
|
|
209
|
+
return /^(?:users|groups)\/(\d+)$/.exec(resourcePath)?.[1];
|
|
210
|
+
}
|
|
211
|
+
function resolveOwner(body) {
|
|
212
|
+
if (typeof body.user === "string") {
|
|
213
|
+
const id = extractOwnerId(body.user);
|
|
214
|
+
if (id !== void 0) return {
|
|
215
|
+
data: {
|
|
216
|
+
id,
|
|
217
|
+
kind: "user"
|
|
218
|
+
},
|
|
219
|
+
success: true
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (typeof body.group === "string") {
|
|
223
|
+
const id = extractOwnerId(body.group);
|
|
224
|
+
if (id !== void 0) return {
|
|
225
|
+
data: {
|
|
226
|
+
id,
|
|
227
|
+
kind: "group"
|
|
228
|
+
},
|
|
229
|
+
success: true
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
err: void 0,
|
|
234
|
+
success: false
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region src/domains/game-internationalization/game-icon/builders.ts
|
|
239
|
+
/**
|
|
240
|
+
* Builds a `POST` request for the localized "upload experience icon"
|
|
241
|
+
* endpoint. A successful upload replaces any existing icon for the same
|
|
242
|
+
* `(universeId, languageCode)` pair.
|
|
243
|
+
*
|
|
244
|
+
* @param parameters - Universe and language identifiers plus the image
|
|
245
|
+
* bytes to upload.
|
|
246
|
+
* @returns A pure {@link HttpRequest} describing the upload call.
|
|
247
|
+
*/
|
|
248
|
+
function buildUploadIconRequest(parameters) {
|
|
249
|
+
const body = new FormData();
|
|
250
|
+
body.append("request.files", toBlob(parameters.image));
|
|
251
|
+
return {
|
|
252
|
+
body,
|
|
253
|
+
method: "POST",
|
|
254
|
+
url: `/legacy-game-internationalization/v1/game-icon/games/${parameters.universeId}/language-codes/${parameters.languageCode}`
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Builds a `DELETE` request for the localized "delete experience icon"
|
|
259
|
+
* endpoint. Removing the source-language icon is rejected server-side;
|
|
260
|
+
* deleting the icon for a non-source language clears that translation.
|
|
261
|
+
*
|
|
262
|
+
* @param parameters - Universe and language identifiers of the icon to
|
|
263
|
+
* delete.
|
|
264
|
+
* @returns A pure {@link HttpRequest} describing the delete call.
|
|
265
|
+
*/
|
|
266
|
+
function buildDeleteIconRequest(parameters) {
|
|
267
|
+
return {
|
|
268
|
+
method: "DELETE",
|
|
269
|
+
url: `/legacy-game-internationalization/v1/game-icon/games/${parameters.universeId}/language-codes/${parameters.languageCode}`
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Builds a `GET` request for the "list experience icons" endpoint. The
|
|
274
|
+
* server returns one entry per locale that has an icon registered.
|
|
275
|
+
*
|
|
276
|
+
* @param parameters - Universe identifier whose icons to list.
|
|
277
|
+
* @returns A pure {@link HttpRequest} describing the list call.
|
|
278
|
+
*/
|
|
279
|
+
function buildListIconsRequest(parameters) {
|
|
280
|
+
return {
|
|
281
|
+
method: "GET",
|
|
282
|
+
url: `/legacy-game-internationalization/v1/game-icon/games/${parameters.universeId}`
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/domains/game-internationalization/game-icon/operations.ts
|
|
287
|
+
/**
|
|
288
|
+
* Per-second request ceiling for every game-icon Operation bound on
|
|
289
|
+
* `UniversesClient.icon`. The legacy `gameinternationalization` service caps
|
|
290
|
+
* each API key at 100 requests per minute *shared across the entire service*
|
|
291
|
+
* (see the `x-roblox-rate-limits` extension on every operation in the
|
|
292
|
+
* vendored Open Cloud spec), so all methods queue against the same operation
|
|
293
|
+
* key.
|
|
294
|
+
*/
|
|
295
|
+
const ICON_OPERATION_LIMIT = Object.freeze({
|
|
296
|
+
maxPerSecond: 100 / 60,
|
|
297
|
+
operationKey: "experience-icon"
|
|
298
|
+
});
|
|
299
|
+
/**
|
|
300
|
+
* Scopes required for every game-icon operation, sourced from
|
|
301
|
+
* `x-roblox-scopes` on the legacy `gameinternationalization` icon
|
|
302
|
+
* endpoints in the vendored OpenAPI schema.
|
|
303
|
+
*/
|
|
304
|
+
const ICON_REQUIRED_SCOPES = Object.freeze(["legacy-universe:manage"]);
|
|
305
|
+
//#endregion
|
|
306
|
+
//#region src/domains/game-internationalization/game-icon/parsers.ts
|
|
307
|
+
/**
|
|
308
|
+
* Parses a successful icon-list response into a public array of
|
|
309
|
+
* {@link ExperienceIcon} entries.
|
|
310
|
+
*
|
|
311
|
+
* @param response - The full {@link HttpResponse} from the Open Cloud API.
|
|
312
|
+
* @returns A success result wrapping the converted icon list, or an
|
|
313
|
+
* `ApiError` when the body does not match the wire schema.
|
|
314
|
+
*/
|
|
315
|
+
function parseIconListResponse(response) {
|
|
316
|
+
const { body, status: statusCode } = response;
|
|
317
|
+
if (!isGameIconListWire(body)) return {
|
|
318
|
+
err: new ApiError("Malformed icon list response", { statusCode }),
|
|
319
|
+
success: false
|
|
320
|
+
};
|
|
321
|
+
return {
|
|
322
|
+
data: body.data.map(toExperienceIcon),
|
|
323
|
+
success: true
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function isGameIconState(value) {
|
|
327
|
+
return value === "Approved" || value === "Error" || value === "PendingReview" || value === "Rejected" || value === "UnAvailable";
|
|
328
|
+
}
|
|
329
|
+
function isGetGameIconResponseWire(value) {
|
|
330
|
+
if (!isRecord(value)) return false;
|
|
331
|
+
return typeof value["imageId"] === "string" && typeof value["imageUrl"] === "string" && typeof value["languageCode"] === "string" && isGameIconState(value["state"]);
|
|
332
|
+
}
|
|
333
|
+
function isGameIconListWire(body) {
|
|
334
|
+
if (!isRecord(body)) return false;
|
|
335
|
+
const { data } = body;
|
|
336
|
+
if (!Array.isArray(data)) return false;
|
|
337
|
+
return data.every(isGetGameIconResponseWire);
|
|
338
|
+
}
|
|
339
|
+
function toExperienceIcon(wire) {
|
|
340
|
+
return {
|
|
341
|
+
imageId: wire.imageId,
|
|
342
|
+
imageUrl: wire.imageUrl,
|
|
343
|
+
languageCode: wire.languageCode,
|
|
344
|
+
state: wire.state
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
//#endregion
|
|
348
|
+
//#region src/domains/game-internationalization/game-thumbnails/builders.ts
|
|
349
|
+
/**
|
|
350
|
+
* Builds a `POST` request for the localized "upload experience thumbnail"
|
|
351
|
+
* endpoint. Each successful upload appends a new entry to the carousel.
|
|
352
|
+
*
|
|
353
|
+
* @param parameters - Universe and language identifiers plus the image
|
|
354
|
+
* bytes to upload.
|
|
355
|
+
* @returns A pure {@link HttpRequest} describing the upload call.
|
|
356
|
+
*/
|
|
357
|
+
function buildUploadThumbnailRequest(parameters) {
|
|
358
|
+
const body = new FormData();
|
|
359
|
+
body.append("gameThumbnailRequest.files", toBlob(parameters.image));
|
|
360
|
+
return {
|
|
361
|
+
body,
|
|
362
|
+
method: "POST",
|
|
363
|
+
url: `/legacy-game-internationalization/v1/game-thumbnails/games/${parameters.universeId}/language-codes/${parameters.languageCode}/image`
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Builds a `DELETE` request for the "delete experience thumbnail" endpoint.
|
|
368
|
+
*
|
|
369
|
+
* @param parameters - Universe, language, and image identifiers of the
|
|
370
|
+
* thumbnail to delete.
|
|
371
|
+
* @returns A pure {@link HttpRequest} describing the delete call.
|
|
372
|
+
*/
|
|
373
|
+
function buildDeleteThumbnailRequest(parameters) {
|
|
374
|
+
return {
|
|
375
|
+
method: "DELETE",
|
|
376
|
+
url: `/legacy-game-internationalization/v1/game-thumbnails/games/${parameters.universeId}/language-codes/${parameters.languageCode}/images/${parameters.imageId}`
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Builds a `POST` request for the "reorder experience thumbnails" endpoint.
|
|
381
|
+
* Validates each supplied image ID at the wire boundary so a typo cannot
|
|
382
|
+
* silently serialize as JSON `null` and corrupt the request.
|
|
383
|
+
*
|
|
384
|
+
* @param parameters - Universe, language, and the desired display order.
|
|
385
|
+
* @returns A success result wrapping the request, or a
|
|
386
|
+
* {@link ValidationError} when `orderedImageIds` is empty or any ID is not
|
|
387
|
+
* a positive integer within the safe-integer range.
|
|
388
|
+
*/
|
|
389
|
+
function buildReorderThumbnailsRequest(parameters) {
|
|
390
|
+
const { languageCode, orderedImageIds, universeId } = parameters;
|
|
391
|
+
const idsResult = parseOrderedImageIds(orderedImageIds);
|
|
392
|
+
if (!idsResult.success) return idsResult;
|
|
393
|
+
return {
|
|
394
|
+
data: {
|
|
395
|
+
body: { mediaAssetIds: idsResult.data },
|
|
396
|
+
method: "POST",
|
|
397
|
+
url: `/legacy-game-internationalization/v1/game-thumbnails/games/${universeId}/language-codes/${languageCode}/images/order`
|
|
398
|
+
},
|
|
399
|
+
success: true
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function parseImageId(value) {
|
|
403
|
+
if (!/^[1-9]\d*$/.test(value)) return;
|
|
404
|
+
const parsed = Number(value);
|
|
405
|
+
if (!Number.isSafeInteger(parsed)) return;
|
|
406
|
+
return parsed;
|
|
407
|
+
}
|
|
408
|
+
function appendParsedId(accumulator, id) {
|
|
409
|
+
if (!accumulator.success) return accumulator;
|
|
410
|
+
const parsed = parseImageId(id);
|
|
411
|
+
if (parsed === void 0) return {
|
|
412
|
+
err: new ValidationError(`orderedImageIds entry ${JSON.stringify(id)} is not a positive integer ID`, { code: "invalid_image_id" }),
|
|
413
|
+
success: false
|
|
414
|
+
};
|
|
415
|
+
return {
|
|
416
|
+
data: [...accumulator.data, parsed],
|
|
417
|
+
success: true
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function parseOrderedImageIds(orderedImageIds) {
|
|
421
|
+
if (orderedImageIds.length === 0) return {
|
|
422
|
+
err: new ValidationError("orderedImageIds must contain at least one image ID", { code: "empty_image_ids" }),
|
|
423
|
+
success: false
|
|
424
|
+
};
|
|
425
|
+
return orderedImageIds.reduce(appendParsedId, {
|
|
426
|
+
data: [],
|
|
427
|
+
success: true
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
//#endregion
|
|
431
|
+
//#region src/domains/game-internationalization/game-thumbnails/operations.ts
|
|
432
|
+
/**
|
|
433
|
+
* Per-second request ceiling for every game-thumbnails Operation bound on
|
|
434
|
+
* `UniversesClient.thumbnails`. The legacy `gameinternationalization`
|
|
435
|
+
* service caps each API key at 100 requests per minute *shared across the
|
|
436
|
+
* entire service* (see the `x-roblox-rate-limits` extension on every
|
|
437
|
+
* operation in the vendored Open Cloud spec), so all methods queue against
|
|
438
|
+
* the same operation key.
|
|
439
|
+
*/
|
|
440
|
+
const THUMBNAILS_OPERATION_LIMIT = Object.freeze({
|
|
441
|
+
maxPerSecond: 100 / 60,
|
|
442
|
+
operationKey: "experience-thumbnails"
|
|
443
|
+
});
|
|
444
|
+
/**
|
|
445
|
+
* Scopes required for every game-thumbnails operation, sourced from
|
|
446
|
+
* `x-roblox-scopes` on the legacy `gameinternationalization` thumbnail
|
|
447
|
+
* endpoints in the vendored OpenAPI schema.
|
|
448
|
+
*/
|
|
449
|
+
const THUMBNAILS_REQUIRED_SCOPES = Object.freeze(["legacy-universe:manage"]);
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region src/domains/game-internationalization/game-thumbnails/parsers.ts
|
|
452
|
+
/**
|
|
453
|
+
* Parses a successful thumbnail-upload response into the public
|
|
454
|
+
* {@link UploadedExperienceThumbnail} shape, returning a {@link Result}
|
|
455
|
+
* so callers can handle malformed payloads without exceptions.
|
|
456
|
+
*
|
|
457
|
+
* @param response - The full {@link HttpResponse} from the Open Cloud API.
|
|
458
|
+
* @returns A success result wrapping the converted upload, or an
|
|
459
|
+
* `ApiError` when the body does not match the wire schema.
|
|
460
|
+
*/
|
|
461
|
+
function parseThumbnailUploadResponse(response) {
|
|
462
|
+
const { body, status: statusCode } = response;
|
|
463
|
+
if (!isGameThumbnailUploadWire(body)) return {
|
|
464
|
+
err: new ApiError("Malformed thumbnail upload response", { statusCode }),
|
|
465
|
+
success: false
|
|
466
|
+
};
|
|
467
|
+
return {
|
|
468
|
+
data: { mediaAssetId: body.mediaAssetId },
|
|
469
|
+
success: true
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function isGameThumbnailUploadWire(body) {
|
|
473
|
+
if (!isRecord(body)) return false;
|
|
474
|
+
return typeof body["mediaAssetId"] === "string";
|
|
475
|
+
}
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/resources/universes/client.ts
|
|
478
|
+
const GET_SPEC = Object.freeze({
|
|
479
|
+
buildRequest: buildGetRequest,
|
|
480
|
+
methodDefaults: {},
|
|
481
|
+
methodKind: "idempotent",
|
|
482
|
+
operationLimit: GET_OPERATION_LIMIT,
|
|
483
|
+
parse: parseUniverseResponse
|
|
484
|
+
});
|
|
485
|
+
const UPDATE_SPEC = Object.freeze({
|
|
486
|
+
buildRequest: buildUpdateRequest,
|
|
487
|
+
methodDefaults: {},
|
|
488
|
+
methodKind: "idempotent",
|
|
489
|
+
operationLimit: UPDATE_OPERATION_LIMIT,
|
|
490
|
+
parse: parseUniverseResponse,
|
|
491
|
+
requiredScopes: UPDATE_REQUIRED_SCOPES
|
|
492
|
+
});
|
|
493
|
+
function buildIconUploadOkRequest(parameters) {
|
|
494
|
+
return okRequest(buildUploadIconRequest(parameters));
|
|
495
|
+
}
|
|
496
|
+
function buildIconDeleteOkRequest(parameters) {
|
|
497
|
+
return okRequest(buildDeleteIconRequest(parameters));
|
|
498
|
+
}
|
|
499
|
+
function buildIconListOkRequest(parameters) {
|
|
500
|
+
return okRequest(buildListIconsRequest(parameters));
|
|
501
|
+
}
|
|
502
|
+
const ICON_UPLOAD_SPEC = Object.freeze({
|
|
503
|
+
buildRequest: buildIconUploadOkRequest,
|
|
504
|
+
methodDefaults: CREATE_METHOD_DEFAULTS,
|
|
505
|
+
methodKind: "create",
|
|
506
|
+
operationLimit: ICON_OPERATION_LIMIT,
|
|
507
|
+
parse: parseEmptyResponse,
|
|
508
|
+
requiredScopes: ICON_REQUIRED_SCOPES
|
|
509
|
+
});
|
|
510
|
+
const ICON_DELETE_SPEC = Object.freeze({
|
|
511
|
+
buildRequest: buildIconDeleteOkRequest,
|
|
512
|
+
methodDefaults: IDEMPOTENT_METHOD_DEFAULTS,
|
|
513
|
+
methodKind: "idempotent",
|
|
514
|
+
operationLimit: ICON_OPERATION_LIMIT,
|
|
515
|
+
parse: parseEmptyResponse,
|
|
516
|
+
requiredScopes: ICON_REQUIRED_SCOPES
|
|
517
|
+
});
|
|
518
|
+
const ICON_LIST_SPEC = Object.freeze({
|
|
519
|
+
buildRequest: buildIconListOkRequest,
|
|
520
|
+
methodDefaults: IDEMPOTENT_METHOD_DEFAULTS,
|
|
521
|
+
methodKind: "idempotent",
|
|
522
|
+
operationLimit: ICON_OPERATION_LIMIT,
|
|
523
|
+
parse: parseIconListResponse,
|
|
524
|
+
requiredScopes: ICON_REQUIRED_SCOPES
|
|
525
|
+
});
|
|
526
|
+
function buildThumbnailUploadOkRequest(parameters) {
|
|
527
|
+
return okRequest(buildUploadThumbnailRequest(parameters));
|
|
528
|
+
}
|
|
529
|
+
function buildThumbnailDeleteOkRequest(parameters) {
|
|
530
|
+
return okRequest(buildDeleteThumbnailRequest(parameters));
|
|
531
|
+
}
|
|
532
|
+
const THUMBNAIL_UPLOAD_SPEC = Object.freeze({
|
|
533
|
+
buildRequest: buildThumbnailUploadOkRequest,
|
|
534
|
+
methodDefaults: CREATE_METHOD_DEFAULTS,
|
|
535
|
+
methodKind: "create",
|
|
536
|
+
operationLimit: THUMBNAILS_OPERATION_LIMIT,
|
|
537
|
+
parse: parseThumbnailUploadResponse,
|
|
538
|
+
requiredScopes: THUMBNAILS_REQUIRED_SCOPES
|
|
539
|
+
});
|
|
540
|
+
const THUMBNAIL_DELETE_SPEC = Object.freeze({
|
|
541
|
+
buildRequest: buildThumbnailDeleteOkRequest,
|
|
542
|
+
methodDefaults: IDEMPOTENT_METHOD_DEFAULTS,
|
|
543
|
+
methodKind: "idempotent",
|
|
544
|
+
operationLimit: THUMBNAILS_OPERATION_LIMIT,
|
|
545
|
+
parse: parseEmptyResponse,
|
|
546
|
+
requiredScopes: THUMBNAILS_REQUIRED_SCOPES
|
|
547
|
+
});
|
|
548
|
+
const THUMBNAIL_REORDER_SPEC = Object.freeze({
|
|
549
|
+
buildRequest: buildReorderThumbnailsRequest,
|
|
550
|
+
methodDefaults: IDEMPOTENT_METHOD_DEFAULTS,
|
|
551
|
+
methodKind: "idempotent",
|
|
552
|
+
operationLimit: THUMBNAILS_OPERATION_LIMIT,
|
|
553
|
+
parse: parseEmptyResponse,
|
|
554
|
+
requiredScopes: THUMBNAILS_REQUIRED_SCOPES
|
|
555
|
+
});
|
|
556
|
+
/**
|
|
557
|
+
* Public client for the Roblox Open Cloud `Universe` resource. Wires
|
|
558
|
+
* the request builders, the injected
|
|
559
|
+
* {@link OpenCloudClientOptions.httpClient}, and the response parser
|
|
560
|
+
* into a single ergonomic surface. Every method returns a
|
|
561
|
+
* {@link Result} so callers handle failure explicitly; no thrown
|
|
562
|
+
* {@link OpenCloudError} ever escapes the client.
|
|
563
|
+
*
|
|
564
|
+
* Partial updates use a Google-style `updateMask` query string derived
|
|
565
|
+
* from the keys present on the update parameters. Setting a clearable
|
|
566
|
+
* field (`privateServerPriceRobux` or any social link) to `undefined`
|
|
567
|
+
* sends JSON `null` for that field so the server clears the
|
|
568
|
+
* corresponding value.
|
|
569
|
+
*
|
|
570
|
+
* Localized experience-icon and experience-thumbnail Operations are
|
|
571
|
+
* bound on the {@link UniversesClient.icon} and
|
|
572
|
+
* {@link UniversesClient.thumbnails} Operation Groups so callers reach
|
|
573
|
+
* for one client per universe.
|
|
574
|
+
*
|
|
575
|
+
* @example
|
|
576
|
+
*
|
|
577
|
+
* ```ts
|
|
578
|
+
* import { UniversesClient } from "@bedrock-rbx/ocale/universes";
|
|
579
|
+
*
|
|
580
|
+
* const client = new UniversesClient({ apiKey: "your-key" });
|
|
581
|
+
* expect(client).toBeInstanceOf(UniversesClient);
|
|
582
|
+
* ```
|
|
583
|
+
*/
|
|
584
|
+
var UniversesClient = class {
|
|
585
|
+
#inner;
|
|
586
|
+
/**
|
|
587
|
+
* Operation Group exposing the localized experience-icon
|
|
588
|
+
* Operations (`upload`, `delete`, `list`) backed by the
|
|
589
|
+
* `legacy-game-internationalization` domain. Shares the parent
|
|
590
|
+
* client's HTTP, rate-limit, and retry plumbing.
|
|
591
|
+
*/
|
|
592
|
+
icon;
|
|
593
|
+
/**
|
|
594
|
+
* Operation Group exposing the localized experience-thumbnail
|
|
595
|
+
* Operations (`upload`, `delete`, `reorder`) backed by the
|
|
596
|
+
* `legacy-game-internationalization` domain. No list-thumbnails
|
|
597
|
+
* endpoint is bridged; consumers must track uploaded
|
|
598
|
+
* `mediaAssetId`s in their own state store to reconcile against
|
|
599
|
+
* the existing carousel. Shares the parent client's HTTP,
|
|
600
|
+
* rate-limit, and retry plumbing.
|
|
601
|
+
*/
|
|
602
|
+
thumbnails;
|
|
603
|
+
/**
|
|
604
|
+
* Creates a new {@link UniversesClient}. Configuration is frozen
|
|
605
|
+
* on construction; per-request overrides are accepted on each
|
|
606
|
+
* method.
|
|
607
|
+
*
|
|
608
|
+
* @param options - Client-level configuration including the API key.
|
|
609
|
+
*/
|
|
610
|
+
constructor(options) {
|
|
611
|
+
this.#inner = new ResourceClient(options);
|
|
612
|
+
this.icon = createIconHandle(this.#inner);
|
|
613
|
+
this.thumbnails = createThumbnailsHandle(this.#inner);
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Fetches the current configuration of a universe.
|
|
617
|
+
*
|
|
618
|
+
* @param parameters - The universe identifier.
|
|
619
|
+
* @param options - Optional per-request overrides (e.g. A different
|
|
620
|
+
* {@link OpenCloudClientOptions.apiKey} for this call only).
|
|
621
|
+
* @returns A {@link Result} wrapping the parsed {@link Universe}
|
|
622
|
+
* or the {@link OpenCloudError} that caused the request to fail.
|
|
623
|
+
*/
|
|
624
|
+
async get(parameters, options) {
|
|
625
|
+
return this.#inner.execute({
|
|
626
|
+
options,
|
|
627
|
+
parameters,
|
|
628
|
+
spec: GET_SPEC
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Partially updates a universe's configuration. The fields
|
|
633
|
+
* supplied on `parameters` (excluding `universeId`) are forwarded
|
|
634
|
+
* to the server via a Google-style `updateMask`; unmentioned
|
|
635
|
+
* fields are left untouched.
|
|
636
|
+
*
|
|
637
|
+
* @param parameters - The universe identifier and the fields to
|
|
638
|
+
* update. At least one updatable field must be supplied.
|
|
639
|
+
* @param options - Optional per-request overrides (e.g. A different
|
|
640
|
+
* {@link OpenCloudClientOptions.apiKey} for this call only).
|
|
641
|
+
* @returns A {@link Result} wrapping the parsed {@link Universe}
|
|
642
|
+
* or the {@link OpenCloudError} that caused the request to fail.
|
|
643
|
+
*/
|
|
644
|
+
async update(parameters, options) {
|
|
645
|
+
return this.#inner.execute({
|
|
646
|
+
options,
|
|
647
|
+
parameters,
|
|
648
|
+
spec: UPDATE_SPEC
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
function createIconHandle(inner) {
|
|
653
|
+
return {
|
|
654
|
+
async delete(parameters, options) {
|
|
655
|
+
return inner.execute({
|
|
656
|
+
options,
|
|
657
|
+
parameters,
|
|
658
|
+
spec: ICON_DELETE_SPEC
|
|
659
|
+
});
|
|
660
|
+
},
|
|
661
|
+
async list(parameters, options) {
|
|
662
|
+
return inner.execute({
|
|
663
|
+
options,
|
|
664
|
+
parameters,
|
|
665
|
+
spec: ICON_LIST_SPEC
|
|
666
|
+
});
|
|
667
|
+
},
|
|
668
|
+
async upload(parameters, options) {
|
|
669
|
+
return inner.execute({
|
|
670
|
+
options,
|
|
671
|
+
parameters,
|
|
672
|
+
spec: ICON_UPLOAD_SPEC
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
function createThumbnailsHandle(inner) {
|
|
678
|
+
return {
|
|
679
|
+
async delete(parameters, options) {
|
|
680
|
+
return inner.execute({
|
|
681
|
+
options,
|
|
682
|
+
parameters,
|
|
683
|
+
spec: THUMBNAIL_DELETE_SPEC
|
|
684
|
+
});
|
|
685
|
+
},
|
|
686
|
+
async reorder(parameters, options) {
|
|
687
|
+
return inner.execute({
|
|
688
|
+
options,
|
|
689
|
+
parameters,
|
|
690
|
+
spec: THUMBNAIL_REORDER_SPEC
|
|
691
|
+
});
|
|
692
|
+
},
|
|
693
|
+
async upload(parameters, options) {
|
|
694
|
+
return inner.execute({
|
|
695
|
+
options,
|
|
696
|
+
parameters,
|
|
697
|
+
spec: THUMBNAIL_UPLOAD_SPEC
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
//#endregion
|
|
703
|
+
export { UniversesClient };
|
|
704
|
+
|
|
705
|
+
//# sourceMappingURL=universes.mjs.map
|