@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.
@@ -0,0 +1,403 @@
1
+ import { i as ApiError } from "./rate-limit-BBU_4xnZ.mjs";
2
+ import { t as ValidationError } from "./validation-CTZzJhmd.mjs";
3
+ import { i as CREATE_METHOD_DEFAULTS, o as isRecord, t as ResourceClient } from "./resource-client-CaS_j3yg.mjs";
4
+ //#region src/domains/cloud-v2/places/builders.ts
5
+ const NON_UPDATABLE_KEYS = new Set(["placeId", "universeId"]);
6
+ /**
7
+ * Builds a `PATCH` request for the Open Cloud "update place" endpoint.
8
+ * Derives the `updateMask` query string from the keys present on
9
+ * `parameters` (excluding the identifiers) and emits a JSON body
10
+ * containing those same fields.
11
+ *
12
+ * @param parameters - The universe and place identifiers plus the fields
13
+ * to update.
14
+ * @returns A success result wrapping the request, or a
15
+ * {@link ValidationError} when no updatable fields were supplied.
16
+ */
17
+ function buildUpdateRequest(parameters) {
18
+ const fieldKeys = extractUpdateFieldKeys(parameters);
19
+ if (fieldKeys.length === 0) return {
20
+ err: new ValidationError("Update must include at least one field", { code: "empty_update" }),
21
+ success: false
22
+ };
23
+ const body = Object.fromEntries(fieldKeys.map((key) => [key, Reflect.get(parameters, key)]));
24
+ const updateMask = fieldKeys.join(",");
25
+ const { placeId, universeId } = parameters;
26
+ return {
27
+ data: {
28
+ body,
29
+ headers: { "content-type": "application/json" },
30
+ method: "PATCH",
31
+ url: `/cloud/v2/universes/${universeId}/places/${placeId}?updateMask=${updateMask}`
32
+ },
33
+ success: true
34
+ };
35
+ }
36
+ function extractUpdateFieldKeys(parameters) {
37
+ return Object.keys(parameters).filter((key) => !NON_UPDATABLE_KEYS.has(key));
38
+ }
39
+ /**
40
+ * Per-second request ceiling for updating a place, from the Open Cloud
41
+ * OpenAPI schema (100 requests per minute per API key owner). Keyed
42
+ * independently from the publish operation so publish and update do
43
+ * not share a queue; upstream quota accounting is not documented as
44
+ * shared and the conservative default is fewer cross-method
45
+ * contention surprises.
46
+ */
47
+ const UPDATE_OPERATION_LIMIT = Object.freeze({
48
+ maxPerSecond: 100 / 60,
49
+ operationKey: "places.update"
50
+ });
51
+ /**
52
+ * Scopes required to update a place's metadata, sourced from
53
+ * `x-roblox-scopes` on the `Cloud_UpdatePlace` operation in the vendored
54
+ * OpenAPI schema.
55
+ */
56
+ const UPDATE_REQUIRED_SCOPES = Object.freeze(["universe.place:write"]);
57
+ //#endregion
58
+ //#region src/domains/cloud-v2/places/parsers.ts
59
+ const MALFORMED_PLACE_MESSAGE = "Malformed place response";
60
+ /**
61
+ * Parses a successful Open Cloud `Place` response body into the public
62
+ * {@link Place} shape.
63
+ *
64
+ * @param response - The full {@link HttpResponse} from the Open Cloud API.
65
+ * @returns A success result wrapping the parsed {@link Place}, or an
66
+ * {@link ApiError} when the body does not match the wire schema.
67
+ */
68
+ function parsePlaceResponse(response) {
69
+ const { body, status: statusCode } = response;
70
+ if (!isPlaceWire(body)) return malformedPlace(statusCode);
71
+ const match = /^universes\/(\d+)\/places\/(\d+)$/.exec(body.path);
72
+ const universeId = match?.[1];
73
+ const id = match?.[2];
74
+ if (id === void 0 || universeId === void 0) return malformedPlace(statusCode);
75
+ return {
76
+ data: toPlace({
77
+ id,
78
+ body,
79
+ universeId
80
+ }),
81
+ success: true
82
+ };
83
+ }
84
+ function malformedPlace(statusCode) {
85
+ return {
86
+ err: new ApiError(MALFORMED_PLACE_MESSAGE, { statusCode }),
87
+ success: false
88
+ };
89
+ }
90
+ function toPlace(args) {
91
+ const { id, body, universeId } = args;
92
+ return {
93
+ id,
94
+ createdAt: new Date(body.createTime),
95
+ description: body.description,
96
+ displayName: body.displayName,
97
+ root: body.root ?? false,
98
+ serverSize: body.serverSize ?? void 0,
99
+ universeId,
100
+ universeRuntimeCreation: body.universeRuntimeCreation ?? false,
101
+ updatedAt: new Date(body.updateTime)
102
+ };
103
+ }
104
+ function hasValidPlaceRequired(body) {
105
+ return typeof body["path"] === "string" && typeof body["createTime"] === "string" && typeof body["updateTime"] === "string" && typeof body["displayName"] === "string" && typeof body["description"] === "string";
106
+ }
107
+ function isOptionalBoolean(value) {
108
+ return value === void 0 || value === null || typeof value === "boolean";
109
+ }
110
+ function hasValidPlaceOptional(body) {
111
+ const serverSize = body["serverSize"] ?? void 0;
112
+ return (serverSize === void 0 || typeof serverSize === "number") && isOptionalBoolean(body["root"]) && isOptionalBoolean(body["universeRuntimeCreation"]);
113
+ }
114
+ function isPlaceWire(body) {
115
+ return isRecord(body) && hasValidPlaceRequired(body) && hasValidPlaceOptional(body);
116
+ }
117
+ //#endregion
118
+ //#region src/domains/universes/places/signatures.ts
119
+ /**
120
+ * Magic-byte prefix every Roblox binary place file (`.rbxl`) starts with.
121
+ * The first 8 bytes spell out `<roblox!` in ASCII; the remaining 6 bytes
122
+ * (`\x89\xff\r\n\x1a\n`) are the binary-format marker that distinguishes a
123
+ * binary place file from the XML form (`.rbxlx`), whose ASCII-only header
124
+ * begins with `<roblox `.
125
+ */
126
+ const RBXL_SIGNATURE = new Uint8Array([
127
+ 60,
128
+ 114,
129
+ 111,
130
+ 98,
131
+ 108,
132
+ 111,
133
+ 120,
134
+ 33,
135
+ 137,
136
+ 255,
137
+ 13,
138
+ 10,
139
+ 26,
140
+ 10
141
+ ]);
142
+ /**
143
+ * Magic-byte prefix every Roblox XML place file (`.rbxlx`) starts with.
144
+ * Equivalent to the ASCII string `<roblox ` (note the trailing space): a
145
+ * well-formed rbxlx file opens with `<roblox` followed by attributes, while
146
+ * an rbxl file uses `<roblox!` (exclamation mark) as the eighth byte. The
147
+ * trailing space is what proves the file is the XML variant rather than
148
+ * the binary one.
149
+ */
150
+ const RBXLX_SIGNATURE = new Uint8Array([
151
+ 60,
152
+ 114,
153
+ 111,
154
+ 98,
155
+ 108,
156
+ 111,
157
+ 120,
158
+ 32
159
+ ]);
160
+ /**
161
+ * Reports whether `body` begins with `signature`. A pure byte-prefix check
162
+ * with no allocation; used by the place builder to disambiguate `.rbxl` and
163
+ * `.rbxlx` payloads against their declared `format`.
164
+ *
165
+ * @param body - The caller-supplied place file bytes.
166
+ * @param signature - One of the frozen signature constants from this module.
167
+ * @returns `true` if every byte of `signature` matches `body[0..signature.length]`.
168
+ */
169
+ function matchesSignature(body, signature) {
170
+ for (const [index, expected] of signature.entries()) if (body[index] !== expected) return false;
171
+ return true;
172
+ }
173
+ //#endregion
174
+ //#region src/domains/universes/places/builders.ts
175
+ const CONTENT_TYPE_BY_FORMAT = {
176
+ rbxl: "application/octet-stream",
177
+ rbxlx: "application/xml"
178
+ };
179
+ /**
180
+ * Builds a `POST` request for the Open Cloud "publish place version"
181
+ * endpoint. Performs two local validations before producing any
182
+ * {@link HttpRequest}: a non-empty body check and a magic-byte check
183
+ * that the bytes' actual format matches `parameters.format`.
184
+ *
185
+ * @param parameters - Universe and place identifiers, the place file
186
+ * bytes, and the declared `format` of those bytes.
187
+ * @param versionType - `"Published"` for `publish()`, `"Saved"` for
188
+ * `save()`; baked into the `?versionType=` query string.
189
+ * @returns A success result wrapping the request on success, or a
190
+ * {@link ValidationError} when the body is empty or its magic bytes
191
+ * disagree with `parameters.format`.
192
+ */
193
+ function buildPublishRequest(parameters, versionType) {
194
+ const { body, format, placeId, universeId } = parameters;
195
+ if (body.length === 0) return {
196
+ err: new ValidationError("Place body is empty", { code: "empty_body" }),
197
+ success: false
198
+ };
199
+ if (!matchesSignature(body, format === "rbxl" ? RBXL_SIGNATURE : RBXLX_SIGNATURE)) return {
200
+ err: new ValidationError(`Place body does not match the declared "${format}" format`, { code: "format_mismatch" }),
201
+ success: false
202
+ };
203
+ return {
204
+ data: {
205
+ body,
206
+ headers: { "content-type": CONTENT_TYPE_BY_FORMAT[format] },
207
+ method: "POST",
208
+ url: `/universes/v1/${universeId}/places/${placeId}/versions?versionType=${versionType}`
209
+ },
210
+ success: true
211
+ };
212
+ }
213
+ //#endregion
214
+ //#region src/domains/universes/places/operations.ts
215
+ /**
216
+ * Per-second request ceiling for publishing or saving a place version,
217
+ * from the Open Cloud OpenAPI schema (30 requests per minute, expressed
218
+ * here as `0.5` per second). The publish and save methods both reference
219
+ * this constant so that a single per-API-key queue serves both, matching
220
+ * Roblox's server-side accounting which counts both call types against
221
+ * the same per-minute quota.
222
+ */
223
+ const PUBLISH_OPERATION_LIMIT = Object.freeze({
224
+ maxPerSecond: .5,
225
+ operationKey: "places.publishVersion"
226
+ });
227
+ /**
228
+ * Scopes required to publish or save a place version, sourced from
229
+ * `x-roblox-scopes` on the `Places_CreatePlaceVersionApiKey` operation
230
+ * in the vendored OpenAPI schema.
231
+ */
232
+ const PUBLISH_REQUIRED_SCOPES = Object.freeze(["universe-places:write"]);
233
+ //#endregion
234
+ //#region src/domains/universes/places/parsers.ts
235
+ /**
236
+ * Parses a successful publish-version response into the public
237
+ * {@link PlaceVersion} shape. The Roblox endpoint sometimes returns the
238
+ * JSON-shaped body under a `text/plain` `Content-Type`, so the body may
239
+ * arrive either pre-decoded as a JSON object or still in its raw string
240
+ * form; both are accepted here.
241
+ *
242
+ * @param response - The full {@link HttpResponse} from the Open Cloud API.
243
+ * @returns A success result wrapping the parsed {@link PlaceVersion}, or
244
+ * an {@link ApiError} when the body is malformed or its `versionNumber`
245
+ * field is missing/wrong-typed.
246
+ */
247
+ function parsePublishResponse(response) {
248
+ const { body, status: statusCode } = response;
249
+ const decodeResult = decodeBody(body, statusCode);
250
+ if (!decodeResult.success) return decodeResult;
251
+ if (!isPlaceVersionWire(decodeResult.data)) return {
252
+ err: new ApiError("Malformed publish response", { statusCode }),
253
+ success: false
254
+ };
255
+ return {
256
+ data: { versionNumber: decodeResult.data.versionNumber },
257
+ success: true
258
+ };
259
+ }
260
+ function decodeBody(body, statusCode) {
261
+ if (typeof body !== "string") return {
262
+ data: body,
263
+ success: true
264
+ };
265
+ try {
266
+ return {
267
+ data: JSON.parse(body),
268
+ success: true
269
+ };
270
+ } catch {
271
+ return {
272
+ err: new ApiError("Malformed publish response", { statusCode }),
273
+ success: false
274
+ };
275
+ }
276
+ }
277
+ function isPlaceVersionWire(value) {
278
+ if (!isRecord(value)) return false;
279
+ return typeof value["versionNumber"] === "number";
280
+ }
281
+ //#endregion
282
+ //#region src/resources/places/client.ts
283
+ function makeSpec(versionType) {
284
+ return Object.freeze({
285
+ buildRequest: (parameters) => buildPublishRequest(parameters, versionType),
286
+ methodDefaults: CREATE_METHOD_DEFAULTS,
287
+ methodKind: "create",
288
+ operationLimit: PUBLISH_OPERATION_LIMIT,
289
+ parse: parsePublishResponse,
290
+ requiredScopes: PUBLISH_REQUIRED_SCOPES
291
+ });
292
+ }
293
+ const PUBLISH_SPEC = makeSpec("Published");
294
+ const SAVE_SPEC = makeSpec("Saved");
295
+ const UPDATE_SPEC = Object.freeze({
296
+ buildRequest: buildUpdateRequest,
297
+ methodDefaults: {},
298
+ methodKind: "idempotent",
299
+ operationLimit: UPDATE_OPERATION_LIMIT,
300
+ parse: parsePlaceResponse,
301
+ requiredScopes: UPDATE_REQUIRED_SCOPES
302
+ });
303
+ /**
304
+ * Public client for the Roblox Open Cloud `Place` resource. Covers
305
+ * place-version publishing (`publish`, `save`) and place-configuration
306
+ * updates (`update`). Wires the request builders, the injected
307
+ * {@link OpenCloudClientOptions.httpClient}, and the response parsers
308
+ * into a single ergonomic surface. Every method returns a {@link Result}
309
+ * so callers handle failure explicitly; no thrown {@link OpenCloudError}
310
+ * ever escapes the client.
311
+ *
312
+ * Publishing or saving a 5xx-failed place version is not retried
313
+ * automatically: Roblox does not support idempotency keys, so a retry
314
+ * could publish a duplicate version unnoticed. Callers that *can* detect
315
+ * duplicates externally may opt back into 5xx retry per-call by passing
316
+ * `retryableStatuses` on the second argument. The `update` method, by
317
+ * contrast, is idempotent and retries both 429 and 5xx automatically.
318
+ *
319
+ * @example
320
+ *
321
+ * ```ts
322
+ * import { PlacesClient } from "@bedrock-rbx/ocale/places";
323
+ *
324
+ * const client = new PlacesClient({ apiKey: "your-key" });
325
+ * expect(client).toBeInstanceOf(PlacesClient);
326
+ * ```
327
+ */
328
+ var PlacesClient = class {
329
+ #inner;
330
+ /**
331
+ * Creates a new {@link PlacesClient}. Configuration is frozen on
332
+ * construction; per-request overrides are accepted on each method.
333
+ *
334
+ * @param options - Client-level configuration including the API key.
335
+ */
336
+ constructor(options) {
337
+ this.#inner = new ResourceClient(options);
338
+ }
339
+ /**
340
+ * Publishes a new live version of a place.
341
+ *
342
+ * @param parameters - Universe and place identifiers, the place file
343
+ * bytes, and their declared `format`.
344
+ * @param options - Optional per-request overrides (e.g. A different
345
+ * {@link OpenCloudClientOptions.apiKey} for this call only).
346
+ * @returns A {@link Result} wrapping the parsed {@link PlaceVersion}
347
+ * or the {@link OpenCloudError} that caused the request to fail.
348
+ */
349
+ async publish(parameters, options) {
350
+ return this.#inner.execute({
351
+ options,
352
+ parameters,
353
+ spec: PUBLISH_SPEC
354
+ });
355
+ }
356
+ /**
357
+ * Saves a new draft version of a place. Identical to {@link publish}
358
+ * except the resulting version is not made live; consumers can list or
359
+ * promote it later. Shares a single per-API-key rate-limit queue with
360
+ * `publish` because Roblox attributes both calls to the same per-minute
361
+ * quota.
362
+ *
363
+ * @param parameters - Universe and place identifiers, the place file
364
+ * bytes, and their declared `format`.
365
+ * @param options - Optional per-request overrides (e.g. A different
366
+ * {@link OpenCloudClientOptions.apiKey} for this call only).
367
+ * @returns A {@link Result} wrapping the parsed {@link PlaceVersion}
368
+ * or the {@link OpenCloudError} that caused the request to fail.
369
+ */
370
+ async save(parameters, options) {
371
+ return this.#inner.execute({
372
+ options,
373
+ parameters,
374
+ spec: SAVE_SPEC
375
+ });
376
+ }
377
+ /**
378
+ * Partially updates a place's configuration. The fields supplied on
379
+ * `parameters` (excluding the identifiers) are forwarded to the
380
+ * server via a Google-style `updateMask`; unmentioned fields are
381
+ * left untouched. The universe's root place is the canonical place
382
+ * to update when changing a universe's description or display name:
383
+ * both are derived server-side from the root place.
384
+ *
385
+ * @param parameters - The universe and place identifiers and the
386
+ * fields to update. At least one writable field must be supplied.
387
+ * @param options - Optional per-request overrides (e.g. A different
388
+ * {@link OpenCloudClientOptions.apiKey} for this call only).
389
+ * @returns A {@link Result} wrapping the parsed {@link Place} or
390
+ * the {@link OpenCloudError} that caused the request to fail.
391
+ */
392
+ async update(parameters, options) {
393
+ return this.#inner.execute({
394
+ options,
395
+ parameters,
396
+ spec: UPDATE_SPEC
397
+ });
398
+ }
399
+ };
400
+ //#endregion
401
+ export { PlacesClient };
402
+
403
+ //# sourceMappingURL=places.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"places.mjs","names":["#inner"],"sources":["../src/domains/cloud-v2/places/builders.ts","../src/domains/cloud-v2/places/operations.ts","../src/domains/cloud-v2/places/parsers.ts","../src/domains/universes/places/signatures.ts","../src/domains/universes/places/builders.ts","../src/domains/universes/places/operations.ts","../src/domains/universes/places/parsers.ts","../src/resources/places/client.ts"],"sourcesContent":["import type { HttpRequest } from \"../../../client/types.ts\";\nimport { ValidationError } from \"../../../errors/validation.ts\";\nimport type { Result } from \"../../../types.ts\";\nimport type { UpdatePlaceParameters } from \"./types.ts\";\n\nconst NON_UPDATABLE_KEYS: ReadonlySet<string> = new Set([\"placeId\", \"universeId\"]);\n\n/**\n * Builds a `PATCH` request for the Open Cloud \"update place\" endpoint.\n * Derives the `updateMask` query string from the keys present on\n * `parameters` (excluding the identifiers) and emits a JSON body\n * containing those same fields.\n *\n * @param parameters - The universe and place identifiers plus the fields\n * to update.\n * @returns A success result wrapping the request, or a\n * {@link ValidationError} when no updatable fields were supplied.\n */\nexport function buildUpdateRequest(\n\tparameters: UpdatePlaceParameters,\n): Result<HttpRequest, ValidationError> {\n\tconst fieldKeys = extractUpdateFieldKeys(parameters);\n\n\tif (fieldKeys.length === 0) {\n\t\treturn {\n\t\t\terr: new ValidationError(\"Update must include at least one field\", {\n\t\t\t\tcode: \"empty_update\",\n\t\t\t}),\n\t\t\tsuccess: false,\n\t\t};\n\t}\n\n\tconst body = Object.fromEntries(\n\t\tfieldKeys.map((key): readonly [string, unknown] => [key, Reflect.get(parameters, key)]),\n\t);\n\tconst updateMask = fieldKeys.join(\",\");\n\tconst { placeId, universeId } = parameters;\n\treturn {\n\t\tdata: {\n\t\t\tbody,\n\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\tmethod: \"PATCH\",\n\t\t\turl: `/cloud/v2/universes/${universeId}/places/${placeId}?updateMask=${updateMask}`,\n\t\t},\n\t\tsuccess: true,\n\t};\n}\n\nfunction extractUpdateFieldKeys(parameters: UpdatePlaceParameters): ReadonlyArray<string> {\n\treturn Object.keys(parameters).filter((key) => !NON_UPDATABLE_KEYS.has(key));\n}\n","import type { OperationLimit } from \"../../../internal/http/rate-limit-queue.ts\";\n\nconst UPDATE_PER_MINUTE = 100;\nconst SECONDS_PER_MINUTE = 60;\n\n/**\n * Per-second request ceiling for updating a place, from the Open Cloud\n * OpenAPI schema (100 requests per minute per API key owner). Keyed\n * independently from the publish operation so publish and update do\n * not share a queue; upstream quota accounting is not documented as\n * shared and the conservative default is fewer cross-method\n * contention surprises.\n */\nexport const UPDATE_OPERATION_LIMIT: OperationLimit = Object.freeze({\n\tmaxPerSecond: UPDATE_PER_MINUTE / SECONDS_PER_MINUTE,\n\toperationKey: \"places.update\",\n});\n\n/**\n * Scopes required to update a place's metadata, sourced from\n * `x-roblox-scopes` on the `Cloud_UpdatePlace` operation in the vendored\n * OpenAPI schema.\n */\nexport const UPDATE_REQUIRED_SCOPES: ReadonlyArray<string> = Object.freeze([\n\t\"universe.place:write\",\n]);\n","import type { HttpResponse } from \"../../../client/types.ts\";\nimport { ApiError } from \"../../../errors/api-error.ts\";\nimport { isRecord } from \"../../../internal/utils/is-record.ts\";\nimport type { Result } from \"../../../types.ts\";\nimport type { Place } from \"./types.ts\";\nimport type { PlaceWire } from \"./wire.ts\";\n\nconst MALFORMED_PLACE_MESSAGE = \"Malformed place response\";\n\ninterface ToPlaceArgs {\n\treadonly id: string;\n\treadonly body: PlaceWire;\n\treadonly universeId: string;\n}\n\n/**\n * Parses a successful Open Cloud `Place` response body into the public\n * {@link Place} shape.\n *\n * @param response - The full {@link HttpResponse} from the Open Cloud API.\n * @returns A success result wrapping the parsed {@link Place}, or an\n * {@link ApiError} when the body does not match the wire schema.\n */\nexport function parsePlaceResponse(response: HttpResponse): Result<Place, ApiError> {\n\tconst { body, status: statusCode } = response;\n\n\tif (!isPlaceWire(body)) {\n\t\treturn malformedPlace(statusCode);\n\t}\n\n\tconst match = /^universes\\/(\\d+)\\/places\\/(\\d+)$/.exec(body.path);\n\tconst universeId = match?.[1];\n\tconst id = match?.[2];\n\tif (id === undefined || universeId === undefined) {\n\t\treturn malformedPlace(statusCode);\n\t}\n\n\treturn { data: toPlace({ id, body, universeId }), success: true };\n}\n\nfunction malformedPlace(statusCode: number): Result<Place, ApiError> {\n\treturn {\n\t\terr: new ApiError(MALFORMED_PLACE_MESSAGE, { statusCode }),\n\t\tsuccess: false,\n\t};\n}\n\nfunction toPlace(args: ToPlaceArgs): Place {\n\tconst { id, body, universeId } = args;\n\treturn {\n\t\tid,\n\t\tcreatedAt: new Date(body.createTime),\n\t\tdescription: body.description,\n\t\tdisplayName: body.displayName,\n\t\troot: body.root ?? false,\n\t\tserverSize: body.serverSize ?? undefined,\n\t\tuniverseId,\n\t\tuniverseRuntimeCreation: body.universeRuntimeCreation ?? false,\n\t\tupdatedAt: new Date(body.updateTime),\n\t};\n}\n\nfunction hasValidPlaceRequired(body: Record<string, unknown>): boolean {\n\treturn (\n\t\ttypeof body[\"path\"] === \"string\" &&\n\t\ttypeof body[\"createTime\"] === \"string\" &&\n\t\ttypeof body[\"updateTime\"] === \"string\" &&\n\t\ttypeof body[\"displayName\"] === \"string\" &&\n\t\ttypeof body[\"description\"] === \"string\"\n\t);\n}\n\nfunction isOptionalBoolean(value: unknown): boolean {\n\treturn value === undefined || value === null || typeof value === \"boolean\";\n}\n\nfunction hasValidPlaceOptional(body: Record<string, unknown>): boolean {\n\tconst serverSize = body[\"serverSize\"] ?? undefined;\n\treturn (\n\t\t(serverSize === undefined || typeof serverSize === \"number\") &&\n\t\tisOptionalBoolean(body[\"root\"]) &&\n\t\tisOptionalBoolean(body[\"universeRuntimeCreation\"])\n\t);\n}\n\nfunction isPlaceWire(body: unknown): body is PlaceWire {\n\treturn isRecord(body) && hasValidPlaceRequired(body) && hasValidPlaceOptional(body);\n}\n","/**\n * Magic-byte prefix every Roblox binary place file (`.rbxl`) starts with.\n * The first 8 bytes spell out `<roblox!` in ASCII; the remaining 6 bytes\n * (`\\x89\\xff\\r\\n\\x1a\\n`) are the binary-format marker that distinguishes a\n * binary place file from the XML form (`.rbxlx`), whose ASCII-only header\n * begins with `<roblox `.\n */\nexport const RBXL_SIGNATURE: Readonly<Uint8Array<ArrayBuffer>> = new Uint8Array([\n\t0x3c, 0x72, 0x6f, 0x62, 0x6c, 0x6f, 0x78, 0x21, 0x89, 0xff, 0x0d, 0x0a, 0x1a, 0x0a,\n]);\n\n/**\n * Magic-byte prefix every Roblox XML place file (`.rbxlx`) starts with.\n * Equivalent to the ASCII string `<roblox ` (note the trailing space): a\n * well-formed rbxlx file opens with `<roblox` followed by attributes, while\n * an rbxl file uses `<roblox!` (exclamation mark) as the eighth byte. The\n * trailing space is what proves the file is the XML variant rather than\n * the binary one.\n */\nexport const RBXLX_SIGNATURE: Readonly<Uint8Array<ArrayBuffer>> = new Uint8Array([\n\t0x3c, 0x72, 0x6f, 0x62, 0x6c, 0x6f, 0x78, 0x20,\n]);\n\n/**\n * Reports whether `body` begins with `signature`. A pure byte-prefix check\n * with no allocation; used by the place builder to disambiguate `.rbxl` and\n * `.rbxlx` payloads against their declared `format`.\n *\n * @param body - The caller-supplied place file bytes.\n * @param signature - One of the frozen signature constants from this module.\n * @returns `true` if every byte of `signature` matches `body[0..signature.length]`.\n */\nexport function matchesSignature(\n\tbody: Uint8Array,\n\tsignature: Readonly<Uint8Array<ArrayBuffer>>,\n): boolean {\n\tfor (const [index, expected] of signature.entries()) {\n\t\tif (body[index] !== expected) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn true;\n}\n","import type { HttpRequest } from \"../../../client/types.ts\";\nimport { ValidationError } from \"../../../errors/validation.ts\";\nimport type { Result } from \"../../../types.ts\";\nimport { matchesSignature, RBXL_SIGNATURE, RBXLX_SIGNATURE } from \"./signatures.ts\";\nimport type { PublishParameters } from \"./types.ts\";\n\n/**\n * Whether a publish call writes a live (`Published`) or draft (`Saved`)\n * version. Surfaces only as the `versionType` query string on the\n * underlying HTTP request.\n */\ntype VersionType = \"Published\" | \"Saved\";\n\nconst CONTENT_TYPE_BY_FORMAT: Readonly<Record<PublishParameters[\"format\"], string>> = {\n\trbxl: \"application/octet-stream\",\n\trbxlx: \"application/xml\",\n};\n\n/**\n * Builds a `POST` request for the Open Cloud \"publish place version\"\n * endpoint. Performs two local validations before producing any\n * {@link HttpRequest}: a non-empty body check and a magic-byte check\n * that the bytes' actual format matches `parameters.format`.\n *\n * @param parameters - Universe and place identifiers, the place file\n * bytes, and the declared `format` of those bytes.\n * @param versionType - `\"Published\"` for `publish()`, `\"Saved\"` for\n * `save()`; baked into the `?versionType=` query string.\n * @returns A success result wrapping the request on success, or a\n * {@link ValidationError} when the body is empty or its magic bytes\n * disagree with `parameters.format`.\n */\nexport function buildPublishRequest(\n\tparameters: PublishParameters,\n\tversionType: VersionType,\n): Result<HttpRequest, ValidationError> {\n\tconst { body, format, placeId, universeId } = parameters;\n\n\tif (body.length === 0) {\n\t\treturn {\n\t\t\terr: new ValidationError(\"Place body is empty\", { code: \"empty_body\" }),\n\t\t\tsuccess: false,\n\t\t};\n\t}\n\n\tconst expectedSignature = format === \"rbxl\" ? RBXL_SIGNATURE : RBXLX_SIGNATURE;\n\tif (!matchesSignature(body, expectedSignature)) {\n\t\treturn {\n\t\t\terr: new ValidationError(`Place body does not match the declared \"${format}\" format`, {\n\t\t\t\tcode: \"format_mismatch\",\n\t\t\t}),\n\t\t\tsuccess: false,\n\t\t};\n\t}\n\n\treturn {\n\t\tdata: {\n\t\t\tbody,\n\t\t\theaders: { \"content-type\": CONTENT_TYPE_BY_FORMAT[format] },\n\t\t\tmethod: \"POST\",\n\t\t\turl: `/universes/v1/${universeId}/places/${placeId}/versions?versionType=${versionType}`,\n\t\t},\n\t\tsuccess: true,\n\t};\n}\n","import type { OperationLimit } from \"../../../internal/http/rate-limit-queue.ts\";\n\n/**\n * Per-second request ceiling for publishing or saving a place version,\n * from the Open Cloud OpenAPI schema (30 requests per minute, expressed\n * here as `0.5` per second). The publish and save methods both reference\n * this constant so that a single per-API-key queue serves both, matching\n * Roblox's server-side accounting which counts both call types against\n * the same per-minute quota.\n */\nexport const PUBLISH_OPERATION_LIMIT: OperationLimit = Object.freeze({\n\tmaxPerSecond: 0.5,\n\toperationKey: \"places.publishVersion\",\n});\n\n/**\n * Scopes required to publish or save a place version, sourced from\n * `x-roblox-scopes` on the `Places_CreatePlaceVersionApiKey` operation\n * in the vendored OpenAPI schema.\n */\nexport const PUBLISH_REQUIRED_SCOPES: ReadonlyArray<string> = Object.freeze([\n\t\"universe-places:write\",\n]);\n","import type { HttpResponse } from \"../../../client/types.ts\";\nimport { ApiError } from \"../../../errors/api-error.ts\";\nimport { isRecord } from \"../../../internal/utils/is-record.ts\";\nimport type { Result } from \"../../../types.ts\";\nimport type { PlaceVersion } from \"./types.ts\";\nimport type { PlaceVersionWire } from \"./wire.ts\";\n\n/**\n * Parses a successful publish-version response into the public\n * {@link PlaceVersion} shape. The Roblox endpoint sometimes returns the\n * JSON-shaped body under a `text/plain` `Content-Type`, so the body may\n * arrive either pre-decoded as a JSON object or still in its raw string\n * form; both are accepted here.\n *\n * @param response - The full {@link HttpResponse} from the Open Cloud API.\n * @returns A success result wrapping the parsed {@link PlaceVersion}, or\n * an {@link ApiError} when the body is malformed or its `versionNumber`\n * field is missing/wrong-typed.\n */\nexport function parsePublishResponse(response: HttpResponse): Result<PlaceVersion, ApiError> {\n\tconst { body, status: statusCode } = response;\n\n\tconst decodeResult = decodeBody(body, statusCode);\n\tif (!decodeResult.success) {\n\t\treturn decodeResult;\n\t}\n\n\tif (!isPlaceVersionWire(decodeResult.data)) {\n\t\treturn {\n\t\t\terr: new ApiError(\"Malformed publish response\", { statusCode }),\n\t\t\tsuccess: false,\n\t\t};\n\t}\n\n\treturn {\n\t\tdata: { versionNumber: decodeResult.data.versionNumber },\n\t\tsuccess: true,\n\t};\n}\n\nfunction decodeBody(body: unknown, statusCode: number): Result<unknown, ApiError> {\n\tif (typeof body !== \"string\") {\n\t\treturn { data: body, success: true };\n\t}\n\n\ttry {\n\t\treturn { data: JSON.parse(body), success: true };\n\t} catch {\n\t\treturn {\n\t\t\terr: new ApiError(\"Malformed publish response\", { statusCode }),\n\t\t\tsuccess: false,\n\t\t};\n\t}\n}\n\nfunction isPlaceVersionWire(value: unknown): value is PlaceVersionWire {\n\tif (!isRecord(value)) {\n\t\treturn false;\n\t}\n\n\treturn typeof value[\"versionNumber\"] === \"number\";\n}\n","import type { OpenCloudClientOptions, RequestOptions } from \"../../client/types.ts\";\nimport { buildUpdateRequest } from \"../../domains/cloud-v2/places/builders.ts\";\nimport {\n\tUPDATE_OPERATION_LIMIT,\n\tUPDATE_REQUIRED_SCOPES,\n} from \"../../domains/cloud-v2/places/operations.ts\";\nimport { parsePlaceResponse } from \"../../domains/cloud-v2/places/parsers.ts\";\nimport type { Place, UpdatePlaceParameters } from \"../../domains/cloud-v2/places/types.ts\";\nimport { buildPublishRequest } from \"../../domains/universes/places/builders.ts\";\nimport {\n\tPUBLISH_OPERATION_LIMIT,\n\tPUBLISH_REQUIRED_SCOPES,\n} from \"../../domains/universes/places/operations.ts\";\nimport { parsePublishResponse } from \"../../domains/universes/places/parsers.ts\";\nimport type { PlaceVersion, PublishParameters } from \"../../domains/universes/places/types.ts\";\nimport type { OpenCloudError } from \"../../errors/base.ts\";\nimport { CREATE_METHOD_DEFAULTS } from \"../../internal/http/retry.ts\";\nimport { ResourceClient, type ResourceMethodSpec } from \"../../internal/resource-client.ts\";\nimport type { Result } from \"../../types.ts\";\n\nfunction makeSpec(\n\tversionType: \"Published\" | \"Saved\",\n): ResourceMethodSpec<PublishParameters, PlaceVersion> {\n\treturn Object.freeze({\n\t\tbuildRequest: (parameters: PublishParameters) =>\n\t\t\tbuildPublishRequest(parameters, versionType),\n\t\tmethodDefaults: CREATE_METHOD_DEFAULTS,\n\t\tmethodKind: \"create\",\n\t\toperationLimit: PUBLISH_OPERATION_LIMIT,\n\t\tparse: parsePublishResponse,\n\t\trequiredScopes: PUBLISH_REQUIRED_SCOPES,\n\t});\n}\n\nconst PUBLISH_SPEC = makeSpec(\"Published\");\nconst SAVE_SPEC = makeSpec(\"Saved\");\n\nconst UPDATE_SPEC: ResourceMethodSpec<UpdatePlaceParameters, Place> = Object.freeze({\n\tbuildRequest: buildUpdateRequest,\n\tmethodDefaults: {},\n\tmethodKind: \"idempotent\",\n\toperationLimit: UPDATE_OPERATION_LIMIT,\n\tparse: parsePlaceResponse,\n\trequiredScopes: UPDATE_REQUIRED_SCOPES,\n});\n\n/**\n * Public client for the Roblox Open Cloud `Place` resource. Covers\n * place-version publishing (`publish`, `save`) and place-configuration\n * updates (`update`). Wires the request builders, the injected\n * {@link OpenCloudClientOptions.httpClient}, and the response parsers\n * into a single ergonomic surface. Every method returns a {@link Result}\n * so callers handle failure explicitly; no thrown {@link OpenCloudError}\n * ever escapes the client.\n *\n * Publishing or saving a 5xx-failed place version is not retried\n * automatically: Roblox does not support idempotency keys, so a retry\n * could publish a duplicate version unnoticed. Callers that *can* detect\n * duplicates externally may opt back into 5xx retry per-call by passing\n * `retryableStatuses` on the second argument. The `update` method, by\n * contrast, is idempotent and retries both 429 and 5xx automatically.\n *\n * @example\n *\n * ```ts\n * import { PlacesClient } from \"@bedrock-rbx/ocale/places\";\n *\n * const client = new PlacesClient({ apiKey: \"your-key\" });\n * expect(client).toBeInstanceOf(PlacesClient);\n * ```\n */\nexport class PlacesClient {\n\treadonly #inner: ResourceClient;\n\n\t/**\n\t * Creates a new {@link PlacesClient}. Configuration is frozen on\n\t * construction; per-request overrides are accepted on each method.\n\t *\n\t * @param options - Client-level configuration including the API key.\n\t */\n\tconstructor(options: OpenCloudClientOptions) {\n\t\tthis.#inner = new ResourceClient(options);\n\t}\n\n\t/**\n\t * Publishes a new live version of a place.\n\t *\n\t * @param parameters - Universe and place identifiers, the place file\n\t * bytes, and their declared `format`.\n\t * @param options - Optional per-request overrides (e.g. A different\n\t * {@link OpenCloudClientOptions.apiKey} for this call only).\n\t * @returns A {@link Result} wrapping the parsed {@link PlaceVersion}\n\t * or the {@link OpenCloudError} that caused the request to fail.\n\t */\n\tpublic async publish(\n\t\tparameters: PublishParameters,\n\t\toptions?: RequestOptions,\n\t): Promise<Result<PlaceVersion, OpenCloudError>> {\n\t\treturn this.#inner.execute({ options, parameters, spec: PUBLISH_SPEC });\n\t}\n\n\t/**\n\t * Saves a new draft version of a place. Identical to {@link publish}\n\t * except the resulting version is not made live; consumers can list or\n\t * promote it later. Shares a single per-API-key rate-limit queue with\n\t * `publish` because Roblox attributes both calls to the same per-minute\n\t * quota.\n\t *\n\t * @param parameters - Universe and place identifiers, the place file\n\t * bytes, and their declared `format`.\n\t * @param options - Optional per-request overrides (e.g. A different\n\t * {@link OpenCloudClientOptions.apiKey} for this call only).\n\t * @returns A {@link Result} wrapping the parsed {@link PlaceVersion}\n\t * or the {@link OpenCloudError} that caused the request to fail.\n\t */\n\tpublic async save(\n\t\tparameters: PublishParameters,\n\t\toptions?: RequestOptions,\n\t): Promise<Result<PlaceVersion, OpenCloudError>> {\n\t\treturn this.#inner.execute({ options, parameters, spec: SAVE_SPEC });\n\t}\n\n\t/**\n\t * Partially updates a place's configuration. The fields supplied on\n\t * `parameters` (excluding the identifiers) are forwarded to the\n\t * server via a Google-style `updateMask`; unmentioned fields are\n\t * left untouched. The universe's root place is the canonical place\n\t * to update when changing a universe's description or display name:\n\t * both are derived server-side from the root place.\n\t *\n\t * @param parameters - The universe and place identifiers and the\n\t * fields to update. At least one writable field must be supplied.\n\t * @param options - Optional per-request overrides (e.g. A different\n\t * {@link OpenCloudClientOptions.apiKey} for this call only).\n\t * @returns A {@link Result} wrapping the parsed {@link Place} or\n\t * the {@link OpenCloudError} that caused the request to fail.\n\t */\n\tpublic async update(\n\t\tparameters: UpdatePlaceParameters,\n\t\toptions?: RequestOptions,\n\t): Promise<Result<Place, OpenCloudError>> {\n\t\treturn this.#inner.execute({ options, parameters, spec: UPDATE_SPEC });\n\t}\n}\n"],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import { o as isRecord } from "./resource-client-CaS_j3yg.mjs";
2
+ //#region src/internal/price-information.ts
3
+ /**
4
+ * Narrows `value` to {@link PriceInformationLike} for a given feature literal
5
+ * union by delegating per-element validation to the supplied `isFeature`
6
+ * predicate.
7
+ *
8
+ * @template F - The pricing-feature literal union the caller wants to narrow to.
9
+ * @param value - Unknown wire value to validate.
10
+ * @param isFeature - Type guard for a single `enabledFeatures` element.
11
+ * @returns `true` when `value` is a record whose `defaultPriceInRobux` is a
12
+ * number, `null`, or absent and whose `enabledFeatures` is an array of
13
+ * values that all satisfy `isFeature`.
14
+ */
15
+ function isPriceInformationLike(value, isFeature) {
16
+ if (!isRecord(value)) return false;
17
+ const defaultPrice = value["defaultPriceInRobux"] ?? void 0;
18
+ if (defaultPrice !== void 0 && typeof defaultPrice !== "number") return false;
19
+ const features = value["enabledFeatures"];
20
+ if (!Array.isArray(features)) return false;
21
+ for (const feature of features) if (!isFeature(feature)) return false;
22
+ return true;
23
+ }
24
+ /**
25
+ * Returns a fresh {@link PriceInformationLike} value with a new
26
+ * `enabledFeatures` array, so the caller can hand the result on without
27
+ * exposing the wire object's internal storage.
28
+ *
29
+ * @template F - The pricing-feature literal union of the input.
30
+ * @param wire - Already-validated wire shape.
31
+ * @returns A new record with the same defaults and a copied feature array.
32
+ */
33
+ function copyPriceInformation(wire) {
34
+ return {
35
+ defaultPriceInRobux: wire.defaultPriceInRobux ?? void 0,
36
+ enabledFeatures: [...wire.enabledFeatures]
37
+ };
38
+ }
39
+ //#endregion
40
+ export { isPriceInformationLike as n, copyPriceInformation as t };
41
+
42
+ //# sourceMappingURL=price-information-CmpscMc4.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"price-information-CmpscMc4.mjs","names":[],"sources":["../src/internal/price-information.ts"],"sourcesContent":["import { isRecord } from \"./utils/is-record.ts\";\n\n/**\n * Wire shape shared by every Roblox commerce resource that carries a\n * `priceInformation` block (game passes, developer products, ...). Resources\n * vary in the literal set their `enabledFeatures` may contain, so the feature\n * type is left as a parameter `F`.\n *\n * @template F - The string-literal union for this resource's pricing-feature flags.\n */\nexport interface PriceInformationLike<F extends string> {\n\t/** Default Robux price; `undefined` when the schema returns null. */\n\treadonly defaultPriceInRobux: number | undefined;\n\t/** Enabled pricing feature flags, in the order returned by the API. */\n\treadonly enabledFeatures: ReadonlyArray<F>;\n}\n\n/**\n * Narrows `value` to {@link PriceInformationLike} for a given feature literal\n * union by delegating per-element validation to the supplied `isFeature`\n * predicate.\n *\n * @template F - The pricing-feature literal union the caller wants to narrow to.\n * @param value - Unknown wire value to validate.\n * @param isFeature - Type guard for a single `enabledFeatures` element.\n * @returns `true` when `value` is a record whose `defaultPriceInRobux` is a\n * number, `null`, or absent and whose `enabledFeatures` is an array of\n * values that all satisfy `isFeature`.\n */\nexport function isPriceInformationLike<F extends string>(\n\tvalue: unknown,\n\tisFeature: (candidate: unknown) => candidate is F,\n): value is PriceInformationLike<F> {\n\tif (!isRecord(value)) {\n\t\treturn false;\n\t}\n\n\tconst defaultPrice = value[\"defaultPriceInRobux\"] ?? undefined;\n\tif (defaultPrice !== undefined && typeof defaultPrice !== \"number\") {\n\t\treturn false;\n\t}\n\n\tconst features = value[\"enabledFeatures\"];\n\tif (!Array.isArray(features)) {\n\t\treturn false;\n\t}\n\n\tfor (const feature of features) {\n\t\tif (!isFeature(feature)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn true;\n}\n\n/**\n * Returns a fresh {@link PriceInformationLike} value with a new\n * `enabledFeatures` array, so the caller can hand the result on without\n * exposing the wire object's internal storage.\n *\n * @template F - The pricing-feature literal union of the input.\n * @param wire - Already-validated wire shape.\n * @returns A new record with the same defaults and a copied feature array.\n */\nexport function copyPriceInformation<F extends string>(\n\twire: PriceInformationLike<F>,\n): PriceInformationLike<F> {\n\treturn {\n\t\tdefaultPriceInRobux: wire.defaultPriceInRobux ?? undefined,\n\t\tenabledFeatures: [...wire.enabledFeatures],\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;AA6BA,SAAgB,uBACf,OACA,WACmC;AACnC,KAAI,CAAC,SAAS,MAAM,CACnB,QAAO;CAGR,MAAM,eAAe,MAAM,0BAA0B,KAAA;AACrD,KAAI,iBAAiB,KAAA,KAAa,OAAO,iBAAiB,SACzD,QAAO;CAGR,MAAM,WAAW,MAAM;AACvB,KAAI,CAAC,MAAM,QAAQ,SAAS,CAC3B,QAAO;AAGR,MAAK,MAAM,WAAW,SACrB,KAAI,CAAC,UAAU,QAAQ,CACtB,QAAO;AAIT,QAAO;;;;;;;;;;;AAYR,SAAgB,qBACf,MAC0B;AAC1B,QAAO;EACN,qBAAqB,KAAK,uBAAuB,KAAA;EACjD,iBAAiB,CAAC,GAAG,KAAK,gBAAgB;EAC1C"}
@@ -0,0 +1,135 @@
1
+ //#region src/errors/base.ts
2
+ /**
3
+ * Base error class for all Open Cloud SDK errors.
4
+ *
5
+ * All specific error types (RateLimitError, ApiError, NetworkError)
6
+ * extend this class, enabling `instanceof OpenCloudError` checks.
7
+ */
8
+ var OpenCloudError = class extends Error {
9
+ name = "OpenCloudError";
10
+ };
11
+ //#endregion
12
+ //#region src/errors/api-error.ts
13
+ /**
14
+ * Thrown when the Roblox Open Cloud API returns a non-2xx response
15
+ * that is not a rate limit (429).
16
+ *
17
+ * @example
18
+ *
19
+ * ```ts
20
+ * import { ApiError } from "@bedrock-rbx/ocale";
21
+ *
22
+ * const error = new ApiError("Game pass not found", {
23
+ * code: "NotFound",
24
+ * statusCode: 404,
25
+ * });
26
+ *
27
+ * expect(error).toBeInstanceOf(ApiError);
28
+ * expect(error.statusCode).toBe(404);
29
+ * expect(error.code).toBe("NotFound");
30
+ * ```
31
+ */
32
+ var ApiError = class extends OpenCloudError {
33
+ code;
34
+ name = "ApiError";
35
+ statusCode;
36
+ /**
37
+ * Creates a new ApiError.
38
+ *
39
+ * @param message - Human-readable error description.
40
+ * @param options - Error options including status code and optional error code.
41
+ */
42
+ constructor(message, options) {
43
+ super(message, options);
44
+ this.statusCode = options.statusCode;
45
+ this.code = options.code;
46
+ }
47
+ };
48
+ //#endregion
49
+ //#region src/errors/network-error.ts
50
+ /**
51
+ * Thrown when a network-level failure prevents the request from reaching
52
+ * the Roblox Open Cloud API (e.g., DNS resolution failure, connection timeout).
53
+ */
54
+ var NetworkError = class extends OpenCloudError {
55
+ name = "NetworkError";
56
+ };
57
+ //#endregion
58
+ //#region src/errors/permission-error.ts
59
+ /**
60
+ * Thrown when the Roblox Open Cloud API returns a 401 or 403 for an operation
61
+ * whose required scopes are known. Subclass of {@link ApiError} carrying the
62
+ * scope strings the caller's credential is missing plus the operation key, so
63
+ * a CLI consumer can tell the user exactly which scope to grant on their API
64
+ * key.
65
+ *
66
+ * @example
67
+ *
68
+ * ```ts
69
+ * import { PermissionError } from "@bedrock-rbx/ocale";
70
+ *
71
+ * const error = new PermissionError("HTTP 403", {
72
+ * operationKey: "developer-products.create",
73
+ * requiredScopes: ["creator-store-product:write"],
74
+ * statusCode: 403,
75
+ * });
76
+ *
77
+ * expect(error).toBeInstanceOf(PermissionError);
78
+ * expect(error.requiredScopes).toStrictEqual(["creator-store-product:write"]);
79
+ * expect(error.operationKey).toBe("developer-products.create");
80
+ * ```
81
+ */
82
+ var PermissionError = class extends ApiError {
83
+ name = "PermissionError";
84
+ operationKey;
85
+ requiredScopes;
86
+ /**
87
+ * Creates a new PermissionError.
88
+ *
89
+ * @param message - Human-readable error description.
90
+ * @param options - Error options including status code, the operation key,
91
+ * and the scopes the caller's credential must carry.
92
+ */
93
+ constructor(message, options) {
94
+ super(message, options);
95
+ this.operationKey = options.operationKey;
96
+ this.requiredScopes = options.requiredScopes;
97
+ }
98
+ };
99
+ //#endregion
100
+ //#region src/errors/rate-limit.ts
101
+ /**
102
+ * Thrown when the Roblox Open Cloud API returns a 429 Too Many Requests response.
103
+ * Contains the server-suggested retry delay.
104
+ *
105
+ * @example
106
+ *
107
+ * ```ts
108
+ * import { RateLimitError } from "@bedrock-rbx/ocale";
109
+ *
110
+ * const error = new RateLimitError("Too many requests", {
111
+ * retryAfterSeconds: 30,
112
+ * });
113
+ *
114
+ * expect(error).toBeInstanceOf(RateLimitError);
115
+ * expect(error.retryAfterSeconds).toBe(30);
116
+ * ```
117
+ */
118
+ var RateLimitError = class extends OpenCloudError {
119
+ name = "RateLimitError";
120
+ retryAfterSeconds;
121
+ /**
122
+ * Creates a new RateLimitError.
123
+ *
124
+ * @param message - Human-readable error description.
125
+ * @param options - Error options including the retry delay.
126
+ */
127
+ constructor(message, options) {
128
+ super(message, options);
129
+ this.retryAfterSeconds = options.retryAfterSeconds;
130
+ }
131
+ };
132
+ //#endregion
133
+ export { OpenCloudError as a, ApiError as i, PermissionError as n, NetworkError as r, RateLimitError as t };
134
+
135
+ //# sourceMappingURL=rate-limit-BBU_4xnZ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit-BBU_4xnZ.mjs","names":[],"sources":["../src/errors/base.ts","../src/errors/api-error.ts","../src/errors/network-error.ts","../src/errors/permission-error.ts","../src/errors/rate-limit.ts"],"sourcesContent":["/**\n * Base error class for all Open Cloud SDK errors.\n *\n * All specific error types (RateLimitError, ApiError, NetworkError)\n * extend this class, enabling `instanceof OpenCloudError` checks.\n */\nexport class OpenCloudError extends Error {\n\tpublic override readonly name: string = \"OpenCloudError\";\n}\n","import { OpenCloudError } from \"./base.ts\";\n\n/**\n * Options for constructing an {@link ApiError}.\n */\nexport interface ApiErrorOptions extends ErrorOptions {\n\t/** Optional machine-readable error code from the API. */\n\tcode?: string | undefined;\n\t/** HTTP status code from the API response. */\n\tstatusCode: number;\n}\n\n/**\n * Thrown when the Roblox Open Cloud API returns a non-2xx response\n * that is not a rate limit (429).\n *\n * @example\n *\n * ```ts\n * import { ApiError } from \"@bedrock-rbx/ocale\";\n *\n * const error = new ApiError(\"Game pass not found\", {\n * code: \"NotFound\",\n * statusCode: 404,\n * });\n *\n * expect(error).toBeInstanceOf(ApiError);\n * expect(error.statusCode).toBe(404);\n * expect(error.code).toBe(\"NotFound\");\n * ```\n */\nexport class ApiError extends OpenCloudError {\n\tpublic readonly code: string | undefined;\n\tpublic override readonly name: string = \"ApiError\";\n\tpublic readonly statusCode: number;\n\n\t/**\n\t * Creates a new ApiError.\n\t *\n\t * @param message - Human-readable error description.\n\t * @param options - Error options including status code and optional error code.\n\t */\n\tconstructor(message: string, options: ApiErrorOptions) {\n\t\tsuper(message, options);\n\t\tthis.statusCode = options.statusCode;\n\t\tthis.code = options.code;\n\t}\n}\n","import { OpenCloudError } from \"./base.ts\";\n\n/**\n * Thrown when a network-level failure prevents the request from reaching\n * the Roblox Open Cloud API (e.g., DNS resolution failure, connection timeout).\n */\nexport class NetworkError extends OpenCloudError {\n\tpublic override readonly name: string = \"NetworkError\";\n}\n","import { ApiError, type ApiErrorOptions } from \"./api-error.ts\";\n\n/**\n * Options for constructing a {@link PermissionError}.\n */\nexport interface PermissionErrorOptions extends ApiErrorOptions {\n\t/**\n\t * Stable identifier of the Open Cloud operation that returned the\n\t * permission failure (matches `OperationLimit.operationKey`, e.g.\n\t * `\"developer-products.create\"`).\n\t */\n\toperationKey: string;\n\t/**\n\t * Scope strings the API key or OAuth token must carry for the failing\n\t * operation, sourced from the vendored OpenAPI schema's `x-roblox-scopes`\n\t * for that operationId.\n\t */\n\trequiredScopes: ReadonlyArray<string>;\n}\n\n/**\n * Thrown when the Roblox Open Cloud API returns a 401 or 403 for an operation\n * whose required scopes are known. Subclass of {@link ApiError} carrying the\n * scope strings the caller's credential is missing plus the operation key, so\n * a CLI consumer can tell the user exactly which scope to grant on their API\n * key.\n *\n * @example\n *\n * ```ts\n * import { PermissionError } from \"@bedrock-rbx/ocale\";\n *\n * const error = new PermissionError(\"HTTP 403\", {\n * operationKey: \"developer-products.create\",\n * requiredScopes: [\"creator-store-product:write\"],\n * statusCode: 403,\n * });\n *\n * expect(error).toBeInstanceOf(PermissionError);\n * expect(error.requiredScopes).toStrictEqual([\"creator-store-product:write\"]);\n * expect(error.operationKey).toBe(\"developer-products.create\");\n * ```\n */\nexport class PermissionError extends ApiError {\n\tpublic override readonly name: string = \"PermissionError\";\n\tpublic readonly operationKey: string;\n\tpublic readonly requiredScopes: ReadonlyArray<string>;\n\n\t/**\n\t * Creates a new PermissionError.\n\t *\n\t * @param message - Human-readable error description.\n\t * @param options - Error options including status code, the operation key,\n\t * and the scopes the caller's credential must carry.\n\t */\n\tconstructor(message: string, options: PermissionErrorOptions) {\n\t\tsuper(message, options);\n\t\tthis.operationKey = options.operationKey;\n\t\tthis.requiredScopes = options.requiredScopes;\n\t}\n}\n","import { OpenCloudError } from \"./base.ts\";\n\n/**\n * Options for constructing a {@link RateLimitError}.\n */\nexport interface RateLimitErrorOptions extends ErrorOptions {\n\t/** Seconds to wait before retrying the request. */\n\tretryAfterSeconds: number;\n}\n\n/**\n * Thrown when the Roblox Open Cloud API returns a 429 Too Many Requests response.\n * Contains the server-suggested retry delay.\n *\n * @example\n *\n * ```ts\n * import { RateLimitError } from \"@bedrock-rbx/ocale\";\n *\n * const error = new RateLimitError(\"Too many requests\", {\n * retryAfterSeconds: 30,\n * });\n *\n * expect(error).toBeInstanceOf(RateLimitError);\n * expect(error.retryAfterSeconds).toBe(30);\n * ```\n */\nexport class RateLimitError extends OpenCloudError {\n\tpublic override readonly name = \"RateLimitError\";\n\tpublic readonly retryAfterSeconds: number;\n\n\t/**\n\t * Creates a new RateLimitError.\n\t *\n\t * @param message - Human-readable error description.\n\t * @param options - Error options including the retry delay.\n\t */\n\tconstructor(message: string, options: RateLimitErrorOptions) {\n\t\tsuper(message, options);\n\t\tthis.retryAfterSeconds = options.retryAfterSeconds;\n\t}\n}\n"],"mappings":";;;;;;;AAMA,IAAa,iBAAb,cAAoC,MAAM;CACzC,OAAwC;;;;;;;;;;;;;;;;;;;;;;;ACwBzC,IAAa,WAAb,cAA8B,eAAe;CAC5C;CACA,OAAwC;CACxC;;;;;;;CAQA,YAAY,SAAiB,SAA0B;AACtD,QAAM,SAAS,QAAQ;AACvB,OAAK,aAAa,QAAQ;AAC1B,OAAK,OAAO,QAAQ;;;;;;;;;ACvCtB,IAAa,eAAb,cAAkC,eAAe;CAChD,OAAwC;;;;;;;;;;;;;;;;;;;;;;;;;;;ACoCzC,IAAa,kBAAb,cAAqC,SAAS;CAC7C,OAAwC;CACxC;CACA;;;;;;;;CASA,YAAY,SAAiB,SAAiC;AAC7D,QAAM,SAAS,QAAQ;AACvB,OAAK,eAAe,QAAQ;AAC5B,OAAK,iBAAiB,QAAQ;;;;;;;;;;;;;;;;;;;;;;AC/BhC,IAAa,iBAAb,cAAoC,eAAe;CAClD,OAAgC;CAChC;;;;;;;CAQA,YAAY,SAAiB,SAAgC;AAC5D,QAAM,SAAS,QAAQ;AACvB,OAAK,oBAAoB,QAAQ"}